diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d659f654..730e0b18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,29 @@ on: - master jobs: + fmt-lint: + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Checkout code + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + + - name: Use Node.js 24 + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + with: + node-version: 24 + + - name: Install dependencies + run: npm ci + + - name: Format check + run: npm run fmt:check + + - name: Lint + run: npm run lint + tests: + needs: [fmt-lint] runs-on: ${{ matrix.os }} timeout-minutes: 10 strategy: @@ -33,6 +55,7 @@ jobs: run: npm test examples: + needs: [fmt-lint] runs-on: ${{ matrix.os }} timeout-minutes: 10 strategy: diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..e96d0469 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist +node_modules +tsconfig.json \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..0022644e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all", + "printWidth": 80, + "plugins": ["@ianvs/prettier-plugin-sort-imports"], + "importOrder": ["", "", "", "", "^[.]"] +} diff --git a/app.tests.ts b/app.tests.ts index 5e613be5..fe36149c 100644 --- a/app.tests.ts +++ b/app.tests.ts @@ -1,11 +1,13 @@ +import fs, { rmSync } from "node:fs"; +import { resolve } from "node:path"; + import { red } from "ansicolor"; + import { getManifestFileName, main } from "./app"; import { version } from "./package.json"; -import { resolve } from "path"; -import fs, { rmSync } from "fs"; -import { createIsolatedTestEnvironment } from "./test.utils"; -import { LOG_DIVIDER } from "./shared"; import { getFailureFilePath } from "./persistence"; +import { LOG_DIVIDER } from "./shared"; +import { createIsolatedTestEnvironment } from "./test.utils"; const isolatedTestEnvPrefix = "rendezvous-test-app-"; @@ -34,10 +36,10 @@ describe("Command-line arguments handling", () => { `; const noManifestMessage = red( - `\nNo path to Clarinet project provided. Supply it immediately or face the relentless scrutiny of your contract's vulnerabilities.` + `\nNo path to Clarinet project provided. Supply it immediately or face the relentless scrutiny of your contract's vulnerabilities.`, ); const noContractNameMessage = red( - `\nNo target contract name provided. Please provide the contract name to be fuzzed.` + `\nNo target contract name provided. Please provide the contract name to be fuzzed.`, ); const manifestDirPlaceholder = "isolated-example"; @@ -51,7 +53,7 @@ describe("Command-line arguments handling", () => { process.argv = argv; expect(await main()).toBeUndefined(); process.argv = initialArgv; - } + }, ); it("logs the help message at the end when --help is specified", async () => { @@ -107,7 +109,7 @@ describe("Command-line arguments handling", () => { process.argv = initialArgv; jest.restoreAllMocks(); - } + }, ); it.each([ @@ -126,7 +128,7 @@ describe("Command-line arguments handling", () => { ["node", "app.js", manifestDirPlaceholder, "counter"], [ red( - `\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant.` + `\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant.`, ), helpMessage, ], @@ -136,7 +138,7 @@ describe("Command-line arguments handling", () => { ["node", "app.js", manifestDirPlaceholder, "counter", "--bail"], [ red( - `\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant.` + `\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant.`, ), helpMessage, ], @@ -146,7 +148,7 @@ describe("Command-line arguments handling", () => { ["node", "app.js", manifestDirPlaceholder, "counter", "--seed=123"], [ red( - `\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant.` + `\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant.`, ), helpMessage, ], @@ -156,7 +158,7 @@ describe("Command-line arguments handling", () => { ["node", "app.js", manifestDirPlaceholder, "counter", "--runs=10"], [ red( - `\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant.` + `\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant.`, ), helpMessage, ], @@ -173,7 +175,7 @@ describe("Command-line arguments handling", () => { ], [ red( - `\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant.` + `\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant.`, ), helpMessage, ], @@ -465,12 +467,12 @@ describe("Command-line arguments handling", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); // Update argv to use the isolated test environment. const updatedArgv = argv.map((arg) => - arg === manifestDirPlaceholder ? tempDir : arg + arg === manifestDirPlaceholder ? tempDir : arg, ); process.argv = updatedArgv; @@ -491,7 +493,7 @@ describe("Command-line arguments handling", () => { expectedLogs.forEach((expectedLog) => { // Update expected log to use the isolated test environment path. const updatedExpectedLog = expectedLog.startsWith( - "Using manifest path:" + "Using manifest path:", ) ? expectedLog.replace(manifestDirPlaceholder, tempDir) : expectedLog; @@ -503,7 +505,7 @@ describe("Command-line arguments handling", () => { process.argv = initialArgv; jest.restoreAllMocks(); rmSync(tempDir, { recursive: true, force: true }); - } + }, ); }); diff --git a/app.ts b/app.ts index 98672311..31c670aa 100644 --- a/app.ts +++ b/app.ts @@ -1,19 +1,21 @@ #!/usr/bin/env node -import { join, resolve } from "path"; -import { EventEmitter } from "events"; -import { checkProperties } from "./property"; +import { EventEmitter } from "node:events"; +import { existsSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { parseArgs } from "node:util"; + +import { initSimnet } from "@stacks/clarinet-sdk"; +import { red } from "ansicolor"; + import { checkInvariants } from "./invariant"; +import { version } from "./package.json"; +import { checkProperties } from "./property"; import { getContractNameFromContractId, getFunctionsFromContractInterfaces, getSimnetDeployerContractsInterfaces, LOG_DIVIDER, } from "./shared"; -import { version } from "./package.json"; -import { red } from "ansicolor"; -import { existsSync } from "fs"; -import { parseArgs } from "util"; -import { initSimnet } from "@stacks/clarinet-sdk"; const logger = (log: string, logLevel: "log" | "error" | "info" = "log") => { console[logLevel](log); @@ -29,10 +31,10 @@ const logger = (log: string, logLevel: "log" | "error" | "info" = "log") => { */ export const getManifestFileName = ( manifestDir: string, - targetContractName: string + targetContractName: string, ) => { const isCustomManifest = existsSync( - resolve(manifestDir, `Clarinet-${targetContractName}.toml`) + resolve(manifestDir, `Clarinet-${targetContractName}.toml`), ); if (isCustomManifest) { @@ -63,7 +65,7 @@ const helpMessage = ` Learn more: https://stacks-network.github.io/rendezvous/ `; -export async function main() { +export const main = async () => { const radio = new EventEmitter(); radio.on("logMessage", (log) => logger(log)); radio.on("logFailure", (log) => logger(red(log), "error")); @@ -112,8 +114,8 @@ export async function main() { radio.emit( "logMessage", red( - "\nNo path to Clarinet project provided. Supply it immediately or face the relentless scrutiny of your contract's vulnerabilities." - ) + "\nNo path to Clarinet project provided. Supply it immediately or face the relentless scrutiny of your contract's vulnerabilities.", + ), ); radio.emit("logMessage", helpMessage); return; @@ -123,8 +125,8 @@ export async function main() { radio.emit( "logMessage", red( - "\nNo target contract name provided. Please provide the contract name to be fuzzed." - ) + "\nNo target contract name provided. Please provide the contract name to be fuzzed.", + ), ); radio.emit("logMessage", helpMessage); return; @@ -134,8 +136,8 @@ export async function main() { radio.emit( "logMessage", red( - "\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant." - ) + "\nInvalid type provided. Please provide the type of test to be executed. Possible values: test, invariant.", + ), ); radio.emit("logMessage", helpMessage); return; @@ -149,7 +151,7 @@ export async function main() { */ const manifestPath = join( runConfig.manifestDir, - getManifestFileName(runConfig.manifestDir, runConfig.sutContractName) + getManifestFileName(runConfig.manifestDir, runConfig.sutContractName), ); radio.emit("logMessage", `Using manifest path: ${manifestPath}`); radio.emit("logMessage", `Target contract: ${runConfig.sutContractName}`); @@ -187,28 +189,28 @@ export async function main() { /** * The list of contract IDs for the SUT contract names, as per the simnet. */ - const rendezvousList = Array.from( - getSimnetDeployerContractsInterfaces(simnet).keys(), - ).filter( + const rendezvousList = [ + ...getSimnetDeployerContractsInterfaces(simnet).keys(), + ].filter( (deployedContract) => getContractNameFromContractId(deployedContract) === - runConfig.sutContractName + runConfig.sutContractName, ); if (rendezvousList.length === 0) { radio.emit( "logFailure", - `\nContract "${runConfig.sutContractName}" not found among project contracts.\n` + `\nContract "${runConfig.sutContractName}" not found among project contracts.\n`, ); return; } const rendezvousAllFunctions = getFunctionsFromContractInterfaces( new Map( - Array.from(getSimnetDeployerContractsInterfaces(simnet)).filter( - ([contractId]) => rendezvousList.includes(contractId) - ) - ) + [...getSimnetDeployerContractsInterfaces(simnet)].filter(([contractId]) => + rendezvousList.includes(contractId), + ), + ), ); // Select the testing routine based on `type`. @@ -226,7 +228,7 @@ export async function main() { runConfig.dial, runConfig.bail, runConfig.regr, - radio + radio, ); break; } @@ -241,12 +243,12 @@ export async function main() { runConfig.runs, runConfig.bail, runConfig.regr, - radio + radio, ); break; } } -} +}; if (require.main === module) { main(); diff --git a/dialer.tests.ts b/dialer.tests.ts index c7eb2ff3..79b24396 100644 --- a/dialer.tests.ts +++ b/dialer.tests.ts @@ -1,6 +1,7 @@ -import { join } from "path"; -import { DialerContext } from "./dialer.types"; +import { join } from "node:path"; + import { DialerRegistry } from "./dialer"; +import type { DialerContext } from "./dialer.types"; const dialPath = join("example", "dialer.ts"); @@ -82,7 +83,7 @@ describe("DialerRegistry interaction", () => { // Act & Assert expect(registry.registerDialers()).rejects.toThrow( - "process.exit was called" + "process.exit was called", ); }); }); diff --git a/dialer.ts b/dialer.ts index 9acd6c07..fec96735 100644 --- a/dialer.ts +++ b/dialer.ts @@ -1,6 +1,7 @@ -import { existsSync } from "fs"; -import { resolve } from "path"; -import { Dialer, DialerContext } from "./dialer.types"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; + +import type { Dialer, DialerContext } from "./dialer.types"; // In telephony, a registry is used for maintaining a known set of handlers, // devices, or processes. This aligns with this class's purpose. Dialers are diff --git a/dialer.types.ts b/dialer.types.ts index 1d3f662b..28cc7272 100644 --- a/dialer.types.ts +++ b/dialer.types.ts @@ -1,11 +1,12 @@ -import { ParsedTransactionResult } from "@stacks/clarinet-sdk"; -import { ClarityValue } from "@stacks/transactions"; -import { EnrichedContractInterfaceFunction } from "./shared.types"; +import type { ParsedTransactionResult } from "@stacks/clarinet-sdk"; +import type { ClarityValue } from "@stacks/transactions"; + +import type { EnrichedContractInterfaceFunction } from "./shared.types"; export type Dialer = (context: DialerContext) => Promise | void; -export type DialerContext = { +export interface DialerContext { clarityValueArguments: ClarityValue[]; functionCall: ParsedTransactionResult | undefined; selectedFunction: EnrichedContractInterfaceFunction; -}; +} diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index e275b91e..ea89c562 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -18,6 +18,7 @@ To keep things simple and to maintain quality, please follow these guidelines. 2. **Create a fork and branch:** Work in a dedicated branch. Use short, clear names: + ``` git checkout -b my-fix ``` @@ -30,6 +31,7 @@ To keep things simple and to maintain quality, please follow these guidelines. - Keep logic small and focused. Example code comment: + ```js // Good: Explains why, offering crucial context. // Bad: Focuses on how or adds unnecessary verbosity. @@ -38,6 +40,7 @@ To keep things simple and to maintain quality, please follow these guidelines. 4. **Write tests:** Test your changes. Add or update tests in `*.tests.ts` files. Run tests: + ``` npm test ``` @@ -57,6 +60,7 @@ To keep things simple and to maintain quality, please follow these guidelines. 6. **Open a pull request (PR) targeting the `master` branch:** Keep it small and focused. Explain what and why. Example PR description: + ``` This PR fixes a minor off-by-one error in the property-based tests. The fuzzing process now produces the expected results. @@ -81,11 +85,11 @@ Your contributions help keep `rv` robust, helpful, and accessible to everyone. --- -*This CONTRIBUTING guide is crafted with inspiration from the following:* +_This CONTRIBUTING guide is crafted with inspiration from the following:_ - [AutoFixture CONTRIBUTING.md](https://github.com/AutoFixture/AutoFixture/blob/master/CONTRIBUTING.md) by AutoFixture contributors - [Hedgehog STYLE_GUIDE.md](https://github.com/hedgehogqa/haskell-hedgehog/blob/master/STYLE_GUIDE.md) by Hedgehog contributors - [10 Tips for Better Pull Requests](https://blog.ploeh.dk/2015/01/15/10-tips-for-better-pull-requests/) by Mark Seemann (ploeh) - [The Importance of Comments](https://ayende.com/blog/163297/the-importance-of-comments) by Oren Eini (Ayende Rahien) -*(These references also highlight some of our roots and past influences.)* +_(These references also highlight some of our roots and past influences.)_ diff --git a/docs/chapter_1.md b/docs/chapter_1.md index bb0090e6..55da8fb5 100644 --- a/docs/chapter_1.md +++ b/docs/chapter_1.md @@ -23,8 +23,13 @@ The idea behind Rendezvous originates from several inspiring sources: We are deeply grateful to the Stacks Open Internet Foundation for supporting our work and providing crucial assistance, and to the open-source community for their continuous support and contributions. [^1]: Heterogeneous Clarinet Test-Suites: + [^2]: Hughes, J. (2004). "Testing the Hard Stuff and Staying Sane". In Proceedings of the ACM SIGPLAN Workshop on Haskell (Haskell '04). + [^3]: poxl: + [^4]: fast-check: + [^5]: hedgehog: + [^6]: echidna: diff --git a/docs/chapter_3.md b/docs/chapter_3.md index 8782dbfa..7364b10e 100644 --- a/docs/chapter_3.md +++ b/docs/chapter_3.md @@ -2,7 +2,7 @@ ## Programming In vs. Into a Language -Steve McConnell's *Code Complete* discusses the concept of "programming in vs. into a language." This distinction is particularly relevant for Clarity smart contract development: +Steve McConnell's _Code Complete_ discusses the concept of "programming in vs. into a language." This distinction is particularly relevant for Clarity smart contract development: - **Programming into a language**: Thinking in one language (like TypeScript) and then translating those thoughts into another language (like Clarity). This approach often leads to code that doesn't fully leverage the target language's strengths. diff --git a/docs/chapter_4.md b/docs/chapter_4.md index f958d262..d880e63d 100644 --- a/docs/chapter_4.md +++ b/docs/chapter_4.md @@ -4,16 +4,16 @@ Rendezvous offers two complementary testing methodologies: property-based testin ## Property-Based Testing vs. Invariant Testing -| Property-Based Testing | Invariant Testing | -|------------------------|-------------------| -| Tests individual functions | Tests system-wide properties | -| Generates random inputs for specific functions | Generates random sequences of function calls | +| Property-Based Testing | Invariant Testing | +| ---------------------------------------------------- | --------------------------------------------------------- | +| Tests individual functions | Tests system-wide properties | +| Generates random inputs for specific functions | Generates random sequences of function calls | | Verifies function behavior is correct for all inputs | Verifies state remains valid after any operation sequence | -| Helps find edge cases in specific functions | Helps find unexpected interactions between functions | +| Helps find edge cases in specific functions | Helps find unexpected interactions between functions | ## Understanding Property-Based Testing ->Property-based testing verifies that specific properties of your code hold true across a wide range of inputs. Instead of manually crafting test cases, you define a property, and Rendezvous automatically generates test inputs. +> Property-based testing verifies that specific properties of your code hold true across a wide range of inputs. Instead of manually crafting test cases, you define a property, and Rendezvous automatically generates test inputs. ### Key Concepts @@ -52,7 +52,7 @@ In this example, `seq` is automatically generated by Rendezvous with different r ## Understanding Invariant Testing ->Invariant testing ensures that certain conditions about your contract's state remain true regardless of which operations are performed in which order. +> Invariant testing ensures that certain conditions about your contract's state remain true regardless of which operations are performed in which order. ### Key Concepts @@ -99,12 +99,16 @@ In this example, the invariant verifies that if increment has been called more t ## Testing from Inside vs. Outside ### Testing from Inside + Writing tests in Clarity alongside your contract code provides: + - Direct access to internal state and private functions - Natural expression of contract properties in the same language ### Testing from Outside + Using external tools (like TypeScript libraries) enables: + - Testing integration between multiple contracts - Examining transaction-level concerns like events - Using pre- and post-execution functions (see Dialers, Chapter 6) diff --git a/docs/chapter_5.md b/docs/chapter_5.md index c0079824..0afe8b70 100644 --- a/docs/chapter_5.md +++ b/docs/chapter_5.md @@ -17,12 +17,14 @@ This chapter covers how to install Rendezvous and set up your environment for ef [Project Setup](#project-setup) [Troubleshooting Installation Issues](#troubleshooting-installation-issues) - - [Common Issues and Solutions](#common-issues-and-solutions) + +- [Common Issues and Solutions](#common-issues-and-solutions) [Uninstalling Rendezvous](#uninstalling-rendezvous) - - [Removing a Local Installation](#removing-a-local-installation) - - [Removing a Global Installation](#removing-a-global-installation) - - [Removing a Development Installation](#removing-a-development-installation) + +- [Removing a Local Installation](#removing-a-local-installation) +- [Removing a Global Installation](#removing-a-global-installation) +- [Removing a Development Installation](#removing-a-development-installation) [Next Steps](#next-steps) @@ -60,21 +62,25 @@ With a global installation, you can run the `rv` command from any directory with If you want to contribute to Rendezvous or run it from source: 1. Clone the repository: + ```bash git clone https://github.com/stacks-network/rendezvous.git ``` 2. Navigate to the project directory: + ```bash cd rendezvous ``` 3. Install dependencies: + ```bash npm install ``` 4. Build the project: + ```bash npm run build ``` @@ -113,10 +119,10 @@ my-project/ └── Devnet.toml ``` ->Key points to note: +> Key points to note: > ->1. Rendezvous functions (invariants and property-based tests) are written directly inside the contract file, annotated with `#[env(simnet)]` for conditional deployment. ->2. A valid `Clarinet.toml` file must exist at the project root. +> 1. Rendezvous functions (invariants and property-based tests) are written directly inside the contract file, annotated with `#[env(simnet)]` for conditional deployment. +> 2. A valid `Clarinet.toml` file must exist at the project root. ## Troubleshooting Installation Issues @@ -176,6 +182,7 @@ npm uninstall -g @stacks/rendezvous If you installed from source: 1. If you linked the package globally, unlink it first: + ```bash npm unlink -g @stacks/rendezvous ``` diff --git a/docs/chapter_6.md b/docs/chapter_6.md index 150856d1..2472fba1 100644 --- a/docs/chapter_6.md +++ b/docs/chapter_6.md @@ -5,32 +5,38 @@ This chapter explains how to use Rendezvous in different situations. By the end, ## What's Inside [Running Rendezvous](#running-rendezvous) - - [Positional Arguments](#positional-arguments) - - [Options](#options) - - [Summary](#summary) + +- [Positional Arguments](#positional-arguments) +- [Options](#options) +- [Summary](#summary) [Understanding Rendezvous](#understanding-rendezvous) - - [Example](#example) + +- [Example](#example) [The Rendezvous Context](#the-rendezvous-context) - - [How the Context Works](#how-the-context-works) - - [Using the context to write invariants](#using-the-context-to-write-invariants) + +- [How the Context Works](#how-the-context-works) +- [Using the context to write invariants](#using-the-context-to-write-invariants) [Discarding Property-Based Tests](#discarding-property-based-tests) - - [Discard Function](#discard-function) - - [In-Place Discarding](#in-place-discarding) - - [Discarding summary](#discarding-summary) + +- [Discard Function](#discard-function) +- [In-Place Discarding](#in-place-discarding) +- [Discarding summary](#discarding-summary) [Custom Manifest Files](#custom-manifest-files) - - [Why use a custom manifest?](#why-use-a-custom-manifest) - - [A test double for `sbtc-registry`](#a-test-double-for-sbtc-registry) - - [A Custom Manifest File](#a-custom-manifest-file) - - [How It Works](#how-it-works) + +- [Why use a custom manifest?](#why-use-a-custom-manifest) +- [A test double for `sbtc-registry`](#a-test-double-for-sbtc-registry) +- [A Custom Manifest File](#a-custom-manifest-file) +- [How It Works](#how-it-works) [Trait Reference Parameters](#trait-reference-parameters) - - [How Trait Reference Selection Works](#how-trait-reference-selection-works) - - [Example](#example-1) - - [Adding More Implementations](#adding-more-implementations) + +- [How Trait Reference Selection Works](#how-trait-reference-selection-works) +- [Example](#example-1) +- [Adding More Implementations](#adding-more-implementations) --- @@ -235,12 +241,12 @@ async function postTransferSip010PrintEvent(context) { // Find the print event in the function call events. const sip010PrintEvent = functionCallEvents.find( - (ev) => ev.event === "print_event" + (ev) => ev.event === "print_event", ); if (!sip010PrintEvent) { throw new Error( - "No print event found. The transfer function must emit the SIP-010 print event containing the memo!" + "No print event found. The transfer function must emit the SIP-010 print event containing the memo!", ); } @@ -249,7 +255,7 @@ async function postTransferSip010PrintEvent(context) { // Validate that the emitted print event matches the memo argument. if (sip010PrintEventValue !== hexMemoArgumentValue) { throw new Error( - `Print event memo value does not match the memo argument: ${hexMemoArgumentValue} !== ${sip010PrintEventValue}` + `Print event memo value does not match the memo argument: ${hexMemoArgumentValue} !== ${sip010PrintEventValue}`, ); } } diff --git a/docs/chapter_7.md b/docs/chapter_7.md index 6668007d..0e1d1a9e 100644 --- a/docs/chapter_7.md +++ b/docs/chapter_7.md @@ -145,7 +145,7 @@ describe("stx-defi unit tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); expect(result).toBeOk(Cl.bool(true)); @@ -159,7 +159,7 @@ describe("stx-defi unit tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); expect(depositResult).toBeOk(Cl.bool(true)); @@ -167,7 +167,7 @@ describe("stx-defi unit tests", () => { "stx-defi", "borrow", [Cl.uint(amountToBorrow)], - address1 + address1, ); expect(result).toBeOk(Cl.bool(true)); @@ -181,21 +181,21 @@ describe("stx-defi unit tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); simnet.callPublicFn( "stx-defi", "borrow", [Cl.uint(amountToBorrow)], - address1 + address1, ); const { result } = simnet.callReadOnlyFn( "stx-defi", "get-loan-amount", [], - address1 + address1, ); expect(result).toBeOk(Cl.uint(amountToBorrow)); @@ -209,14 +209,14 @@ describe("stx-defi unit tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); const { result } = simnet.callPublicFn( "stx-defi", "borrow", [Cl.uint(amountToBorrow)], - address1 + address1, ); // err-overborrow @@ -240,7 +240,6 @@ The main functions and state of the contract are now covered by tests. Line cove Rendezvous lets you test a broader range of inputs, not just specific examples. Let's see how to write your first property-based test and why it matters. - ### Add an Ice-Breaker Test Before writing any meaningful properties, it's a good idea to check that Rendezvous can run. Add a simple "always-true" test, annotated with `#[env(simnet)]`: @@ -598,7 +597,7 @@ it("loan amount is correct after single borrow", () => { "stx-defi", "borrow", [Cl.uint(amountToBorrow)], - address1 + address1, ); // ... }); diff --git a/docs/chapter_8.md b/docs/chapter_8.md index 3cf6d1f6..ea1faeb4 100644 --- a/docs/chapter_8.md +++ b/docs/chapter_8.md @@ -5,30 +5,34 @@ The Rendezvous repo has a Clarinet project, `example`, that shows how to test Cl ## What's Inside [The `counter` Contract](#the-counter-contract) - - [Invariants](#invariants) - - [Invariant logic](#invariant-logic) - - [Checking the invariants](#checking-the-invariants) - - [Property-Based Tests](#property-based-tests) - - [Test logic](#test-logic) - - [Checking the properties](#checking-the-properties) + +- [Invariants](#invariants) + - [Invariant logic](#invariant-logic) + - [Checking the invariants](#checking-the-invariants) +- [Property-Based Tests](#property-based-tests) + - [Test logic](#test-logic) + - [Checking the properties](#checking-the-properties) [The `cargo` Contract](#the-cargo-contract) - - [Invariants](#invariants-1) - - [Invariant logic](#invariant-logic-1) - - [Checking the invariants](#checking-the-invariants-1) - - [Property-Based Tests](#property-based-tests-1) - - [Test logic](#test-logic-1) - - [Checking the properties](#checking-the-properties-1) + +- [Invariants](#invariants-1) + - [Invariant logic](#invariant-logic-1) + - [Checking the invariants](#checking-the-invariants-1) +- [Property-Based Tests](#property-based-tests-1) + - [Test logic](#test-logic-1) + - [Checking the properties](#checking-the-properties-1) [The `reverse` Contract](#the-reverse-contract) - - [Property-Based Tests](#property-based-tests-2) - - [Test logic](#test-logic-2) - - [Checking the properties (shrinking)](#checking-the-properties-2) + +- [Property-Based Tests](#property-based-tests-2) + - [Test logic](#test-logic-2) + - [Checking the properties (shrinking)](#checking-the-properties-2) [The `slice` Contract](#the-slice-contract) - - [Property-Based Tests](#property-based-tests-3) - - [Test logic](#test-logic-3) - - [Checking the properties (discarding)](#checking-the-properties-3) + +- [Property-Based Tests](#property-based-tests-3) + - [Test logic](#test-logic-3) + - [Checking the properties (discarding)](#checking-the-properties-3) --- diff --git a/example/sip010.cjs b/example/sip010.cjs index 76be9323..c26a9583 100644 --- a/example/sip010.cjs +++ b/example/sip010.cjs @@ -44,12 +44,12 @@ async function postTransferSip010PrintEvent(context) { } const sip010PrintEvent = functionCallEvents.find( - (ev) => ev.event === "print_event" + (ev) => ev.event === "print_event", ); if (!sip010PrintEvent) { throw new Error( - "No print event found. The transfer function must emit the SIP-010 print event containing the memo!" + "No print event found. The transfer function must emit the SIP-010 print event containing the memo!", ); } @@ -58,7 +58,7 @@ async function postTransferSip010PrintEvent(context) { if (printEventValue !== memoValue) { throw new Error( - `The print event memo value is not equal to the memo parameter value: ${memoValue} !== ${printEventValue}` + `The print event memo value is not equal to the memo parameter value: ${memoValue} !== ${printEventValue}`, ); } diff --git a/example/tests/stx-defi.test.ts b/example/tests/stx-defi.test.ts index 0028b969..5970c267 100644 --- a/example/tests/stx-defi.test.ts +++ b/example/tests/stx-defi.test.ts @@ -18,7 +18,7 @@ describe("stx-defi tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); // Assert @@ -34,7 +34,7 @@ describe("stx-defi tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); // Assert @@ -50,7 +50,7 @@ describe("stx-defi tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); // Act @@ -58,7 +58,7 @@ describe("stx-defi tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); // Assert @@ -75,7 +75,7 @@ describe("stx-defi tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); // Act @@ -83,7 +83,7 @@ describe("stx-defi tests", () => { "stx-defi", "borrow", [Cl.uint(amountToBorrow)], - address1 + address1, ); // Assert @@ -91,7 +91,7 @@ describe("stx-defi tests", () => { "stx-defi", "get-amount-owed", [], - address1 + address1, ); expect(result).toBeOk(Cl.uint(amountToBorrow)); @@ -106,7 +106,7 @@ describe("stx-defi tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); // Act @@ -114,7 +114,7 @@ describe("stx-defi tests", () => { "stx-defi", "borrow", [Cl.uint(amountToBorrow)], - address1 + address1, ); // Assert @@ -122,7 +122,7 @@ describe("stx-defi tests", () => { "stx-defi", "get-amount-owed", [], - address1 + address1, ); expect(result).toBeOk(Cl.uint(0)); @@ -137,14 +137,14 @@ describe("stx-defi tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); simnet.callPublicFn( "stx-defi", "borrow", [Cl.uint(amountToBorrow)], - address1 + address1, ); // Act @@ -152,7 +152,7 @@ describe("stx-defi tests", () => { "stx-defi", "repay", [Cl.uint(amountToBorrow)], - address1 + address1, ); // Assert @@ -160,7 +160,7 @@ describe("stx-defi tests", () => { "stx-defi", "get-amount-owed", [], - address1 + address1, ); expect(result).toBeOk(Cl.uint(0)); }); @@ -176,7 +176,7 @@ describe("stx-defi tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); // Act @@ -184,7 +184,7 @@ describe("stx-defi tests", () => { "stx-defi", "borrow", [Cl.uint(amountToBorrow)], - address1 + address1, ); simnet.mineEmptyBurnBlocks(blocksToPass); @@ -194,7 +194,7 @@ describe("stx-defi tests", () => { "stx-defi", "get-amount-owed", [], - address1 + address1, ); expect(owedAmount).toBeOk(Cl.uint(amountToBorrow + accruedInterest)); @@ -210,21 +210,21 @@ describe("stx-defi tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); simnet.callPublicFn( "stx-defi", "borrow", [Cl.uint(amountToBorrow)], - address1 + address1, ); simnet.callPublicFn( "stx-defi", "repay", [Cl.uint(amountToBorrow)], - address1 + address1, ); // Act @@ -232,7 +232,7 @@ describe("stx-defi tests", () => { "stx-defi", "claim-yield", [], - address1 + address1, ); // Assert @@ -249,14 +249,14 @@ describe("stx-defi tests", () => { "stx-defi", "deposit", [Cl.uint(amountToDeposit)], - address1 + address1, ); simnet.callPublicFn( "stx-defi", "borrow", [Cl.uint(amountToBorrow)], - address1 + address1, ); simnet.mineEmptyBurnBlocks(blocksToPass); @@ -265,7 +265,7 @@ describe("stx-defi tests", () => { "stx-defi", "repay", [Cl.uint(amountToBorrow)], - address1 + address1, ); // Act @@ -273,7 +273,7 @@ describe("stx-defi tests", () => { "stx-defi", "claim-yield", [], - address1 + address1, ); // Assert diff --git a/example/vitest.config.js b/example/vitest.config.js index 2a127315..beb68571 100644 --- a/example/vitest.config.js +++ b/example/vitest.config.js @@ -1,10 +1,10 @@ /// -import { defineConfig } from "vite"; import { - vitestSetupFilePath, getClarinetVitestsArgv, + vitestSetupFilePath, } from "@stacks/clarinet-sdk/vitest"; +import { defineConfig } from "vite"; export default defineConfig({ test: { diff --git a/heatstroke.tests.ts b/heatstroke.tests.ts index 80cc8f6b..bfae9927 100644 --- a/heatstroke.tests.ts +++ b/heatstroke.tests.ts @@ -1,21 +1,23 @@ -import { EventEmitter } from "events"; -import { rmSync } from "fs"; -import { join, resolve } from "path"; +import { EventEmitter } from "node:events"; +import { rmSync } from "node:fs"; +import { join, resolve } from "node:path"; + import { initSimnet } from "@stacks/clarinet-sdk"; -import { ContractInterfaceFunction } from "@stacks/clarinet-sdk-wasm"; +import type { ContractInterfaceFunction } from "@stacks/clarinet-sdk-wasm"; import fc from "fast-check"; + import { reporter } from "./heatstroke"; +import { FalsifiedInvariantError } from "./invariant"; +import { PropertyTestError } from "./property"; import { getContractNameFromContractId } from "./shared"; import { createIsolatedTestEnvironment } from "./test.utils"; -import { PropertyTestError } from "./property"; -import { FalsifiedInvariantError } from "./invariant"; const isolatedTestEnvPrefix = "rendezvous-test-heatstroke-"; const asciiString = () => fc.string({ unit: fc.constantFrom( - ..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + ..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", ), minLength: 1, }); @@ -25,7 +27,7 @@ describe("Custom reporter logging", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); @@ -45,10 +47,10 @@ describe("Custom reporter logging", () => { access: asciiString(), outputs: fc.array(asciiString()), args: fc.anything(), - }) + }), ), selectedFunctionsArgsList: fc.tuple( - fc.array(fc.oneof(asciiString(), fc.nat(), fc.boolean())) + fc.array(fc.oneof(asciiString(), fc.nat(), fc.boolean())), ), selectedInvariant: fc.record({ name: asciiString(), @@ -57,21 +59,21 @@ describe("Custom reporter logging", () => { args: fc.anything(), }), invariantArgs: fc.array( - fc.oneof(asciiString(), fc.nat(), fc.boolean()) + fc.oneof(asciiString(), fc.nat(), fc.boolean()), ), errorMessage: asciiString(), clarityError: asciiString(), sutCallers: fc.array( fc.constantFrom( ...new Map( - [...simnet.getAccounts()].filter(([key]) => key !== "faucet") - ).entries() - ) + [...simnet.getAccounts()].filter(([key]) => key !== "faucet"), + ).entries(), + ), ), invariantCaller: fc.constantFrom( ...new Map( - [...simnet.getAccounts()].filter(([key]) => key !== "faucet") - ).entries() + [...simnet.getAccounts()].filter(([key]) => key !== "faucet"), + ).entries(), ), }), (r: { @@ -135,7 +137,7 @@ describe("Custom reporter logging", () => { `Seed : ${r.seed}`, `\nCounterexample:`, `- Contract : ${getContractNameFromContractId( - rendezvousContractId + rendezvousContractId, )}`, `- Functions: ${r.selectedFunctions .map((selectedFunction) => selectedFunction.name) @@ -144,7 +146,7 @@ describe("Custom reporter logging", () => { .join(", ")})`, `- Arguments: ${r.selectedFunctionsArgsList .map((selectedFunctionArgs) => - JSON.stringify(selectedFunctionArgs) + JSON.stringify(selectedFunctionArgs), ) .join(", ")}`, `- Callers : ${r.sutCallers @@ -152,7 +154,7 @@ describe("Custom reporter logging", () => { .join(", ")}`, `- Outputs : ${r.selectedFunctions .map((selectedFunction) => - JSON.stringify(selectedFunction.outputs) + JSON.stringify(selectedFunction.outputs), ) .join(", ")}`, `- Invariant: ${r.selectedInvariant.name} (${r.selectedInvariant.access})`, @@ -169,9 +171,9 @@ describe("Custom reporter logging", () => { ]; expect(emittedErrorLogs).toEqual(expectedMessages); - } + }, ), - { numRuns: 10 } + { numRuns: 10 }, ); // Teardown @@ -183,7 +185,7 @@ describe("Custom reporter logging", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); @@ -204,10 +206,10 @@ describe("Custom reporter logging", () => { access: asciiString(), outputs: fc.array(asciiString()), args: fc.anything(), - }) + }), ), selectedFunctionsArgsList: fc.tuple( - fc.array(fc.oneof(asciiString(), fc.nat(), fc.boolean())) + fc.array(fc.oneof(asciiString(), fc.nat(), fc.boolean())), ), selectedInvariant: fc.record({ name: asciiString(), @@ -216,21 +218,21 @@ describe("Custom reporter logging", () => { args: fc.anything(), }), invariantArgs: fc.array( - fc.oneof(asciiString(), fc.nat(), fc.boolean()) + fc.oneof(asciiString(), fc.nat(), fc.boolean()), ), errorMessage: asciiString(), clarityError: asciiString(), sutCallers: fc.array( fc.constantFrom( ...new Map( - [...simnet.getAccounts()].filter(([key]) => key !== "faucet") - ).entries() - ) + [...simnet.getAccounts()].filter(([key]) => key !== "faucet"), + ).entries(), + ), ), invariantCaller: fc.constantFrom( ...new Map( - [...simnet.getAccounts()].filter(([key]) => key !== "faucet") - ).entries() + [...simnet.getAccounts()].filter(([key]) => key !== "faucet"), + ).entries(), ), }), (r: { @@ -297,7 +299,7 @@ describe("Custom reporter logging", () => { `Path : ${r.path}`, `\nCounterexample:`, `- Contract : ${getContractNameFromContractId( - rendezvousContractId + rendezvousContractId, )}`, `- Functions: ${r.selectedFunctions .map((selectedFunction) => selectedFunction.name) @@ -306,7 +308,7 @@ describe("Custom reporter logging", () => { .join(", ")})`, `- Arguments: ${r.selectedFunctionsArgsList .map((selectedFunctionArgs) => - JSON.stringify(selectedFunctionArgs) + JSON.stringify(selectedFunctionArgs), ) .join(", ")}`, `- Callers : ${r.sutCallers @@ -314,7 +316,7 @@ describe("Custom reporter logging", () => { .join(", ")}`, `- Outputs : ${r.selectedFunctions .map((selectedFunction) => - JSON.stringify(selectedFunction.outputs) + JSON.stringify(selectedFunction.outputs), ) .join(", ")}`, `- Invariant: ${r.selectedInvariant.name} (${r.selectedInvariant.access})`, @@ -332,9 +334,9 @@ describe("Custom reporter logging", () => { expect(emittedErrorLogs).toEqual(expectedMessages); radio.removeAllListeners(); - } + }, ), - { numRuns: 10 } + { numRuns: 10 }, ); // Teardown @@ -346,7 +348,7 @@ describe("Custom reporter logging", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); @@ -365,10 +367,10 @@ describe("Custom reporter logging", () => { access: asciiString(), outputs: fc.array(asciiString()), args: fc.anything(), - }) + }), ), selectedFunctionsArgsList: fc.tuple( - fc.array(fc.oneof(asciiString(), fc.nat(), fc.boolean())) + fc.array(fc.oneof(asciiString(), fc.nat(), fc.boolean())), ), selectedInvariant: fc.record({ name: asciiString(), @@ -377,20 +379,20 @@ describe("Custom reporter logging", () => { args: fc.anything(), }), invariantArgs: fc.array( - fc.oneof(asciiString(), fc.nat(), fc.boolean()) + fc.oneof(asciiString(), fc.nat(), fc.boolean()), ), errorMessage: asciiString(), sutCallers: fc.array( fc.constantFrom( ...new Map( - [...simnet.getAccounts()].filter(([key]) => key !== "faucet") - ).entries() - ) + [...simnet.getAccounts()].filter(([key]) => key !== "faucet"), + ).entries(), + ), ), invariantCaller: fc.constantFrom( ...new Map( - [...simnet.getAccounts()].filter(([key]) => key !== "faucet") - ).entries() + [...simnet.getAccounts()].filter(([key]) => key !== "faucet"), + ).entries(), ), }), (r: { @@ -452,9 +454,9 @@ describe("Custom reporter logging", () => { // Verify expect(emittedErrorLogs).toEqual([]); radio.removeAllListeners(); - } + }, ), - { numRuns: 10 } + { numRuns: 10 }, ); // Teardown @@ -465,7 +467,7 @@ describe("Custom reporter logging", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); @@ -486,14 +488,14 @@ describe("Custom reporter logging", () => { args: fc.anything(), }), functionArgs: fc.array( - fc.oneof(asciiString(), fc.nat(), fc.boolean()) + fc.oneof(asciiString(), fc.nat(), fc.boolean()), ), errorMessage: asciiString(), clarityError: asciiString(), testCaller: fc.constantFrom( ...new Map( - [...simnet.getAccounts()].filter(([key]) => key !== "faucet") - ).entries() + [...simnet.getAccounts()].filter(([key]) => key !== "faucet"), + ).entries(), ), }), (r: { @@ -545,13 +547,13 @@ describe("Custom reporter logging", () => { `Seed : ${r.seed}`, `\nCounterexample:`, `- Contract : ${getContractNameFromContractId( - rendezvousContractId + rendezvousContractId, )}`, `- Test Function : ${r.selectedTestFunction.name} (${r.selectedTestFunction.access})`, `- Arguments : ${JSON.stringify(r.functionArgs)}`, `- Caller : ${r.testCaller[0]}`, `- Outputs : ${JSON.stringify( - r.selectedTestFunction.outputs + r.selectedTestFunction.outputs, )}`, `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n`, `The test function "${ @@ -564,9 +566,9 @@ describe("Custom reporter logging", () => { ]; expect(emittedErrorLogs).toEqual(expectedMessages); - } + }, ), - { numRuns: 10 } + { numRuns: 10 }, ); // Teardown @@ -578,7 +580,7 @@ describe("Custom reporter logging", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); @@ -600,14 +602,14 @@ describe("Custom reporter logging", () => { args: fc.anything(), }), functionArgs: fc.array( - fc.oneof(asciiString(), fc.nat(), fc.boolean()) + fc.oneof(asciiString(), fc.nat(), fc.boolean()), ), errorMessage: asciiString(), clarityError: asciiString(), testCaller: fc.constantFrom( ...new Map( - [...simnet.getAccounts()].filter(([key]) => key !== "faucet") - ).entries() + [...simnet.getAccounts()].filter(([key]) => key !== "faucet"), + ).entries(), ), }), (r: { @@ -662,13 +664,13 @@ describe("Custom reporter logging", () => { `Path : ${r.path}`, `\nCounterexample:`, `- Contract : ${getContractNameFromContractId( - rendezvousContractId + rendezvousContractId, )}`, `- Test Function : ${r.selectedTestFunction.name} (${r.selectedTestFunction.access})`, `- Arguments : ${JSON.stringify(r.functionArgs)}`, `- Caller : ${r.testCaller[0]}`, `- Outputs : ${JSON.stringify( - r.selectedTestFunction.outputs + r.selectedTestFunction.outputs, )}`, `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n`, `The test function "${ @@ -681,9 +683,9 @@ describe("Custom reporter logging", () => { ]; expect(emittedErrorLogs).toEqual(expectedMessages); - } + }, ), - { numRuns: 10 } + { numRuns: 10 }, ); // Teardown @@ -695,7 +697,7 @@ describe("Custom reporter logging", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); @@ -714,13 +716,13 @@ describe("Custom reporter logging", () => { args: fc.anything(), }), functionArgs: fc.array( - fc.oneof(asciiString(), fc.nat(), fc.boolean()) + fc.oneof(asciiString(), fc.nat(), fc.boolean()), ), errorMessage: asciiString(), testCaller: fc.constantFrom( ...new Map( - [...simnet.getAccounts()].filter(([key]) => key !== "faucet") - ).entries() + [...simnet.getAccounts()].filter(([key]) => key !== "faucet"), + ).entries(), ), }), (r: { @@ -768,9 +770,9 @@ describe("Custom reporter logging", () => { // Verify expect(emittedErrorLogs).toEqual([]); - } + }, ), - { numRuns: 10 } + { numRuns: 10 }, ); // Teardown diff --git a/heatstroke.ts b/heatstroke.ts index 0def358c..f683e190 100644 --- a/heatstroke.ts +++ b/heatstroke.ts @@ -1,16 +1,18 @@ -import { EventEmitter } from "events"; -import { ContractInterfaceFunction } from "@stacks/clarinet-sdk-wasm"; +import type { EventEmitter } from "node:events"; + +import type { ContractInterfaceFunction } from "@stacks/clarinet-sdk-wasm"; import { green } from "ansicolor"; -import { + +import type { InvariantCounterExample, RunDetails, Statistics, StatisticsTreeOptions, TestCounterExample, } from "./heatstroke.types"; +import type { FalsifiedInvariantError } from "./invariant"; +import type { PropertyTestError } from "./property"; import { getContractNameFromContractId } from "./shared"; -import { PropertyTestError } from "./property"; -import { FalsifiedInvariantError } from "./invariant"; /** * Heatstrokes Reporter @@ -29,12 +31,12 @@ import { FalsifiedInvariantError } from "./invariant"; * @param type The type of test that failed: invariant or property. * @returns void */ -export function reporter( +export const reporter = ( runDetails: RunDetails, radio: EventEmitter, type: "invariant" | "test", - statistics: Statistics -) { + statistics: Statistics, +) => { const { counterexample, failed, numRuns, path, seed } = runDetails; if (failed) { @@ -50,7 +52,7 @@ export function reporter( // Report general run data. radio.emit( "logFailure", - `\nError: Property failed after ${numRuns} tests.` + `\nError: Property failed after ${numRuns} tests.`, ); radio.emit("logFailure", `Seed : ${seed}`); if (path) { @@ -64,58 +66,58 @@ export function reporter( radio.emit( "logFailure", `- Contract : ${getContractNameFromContractId( - ce.rendezvousContractId - )}` + ce.rendezvousContractId, + )}`, ); radio.emit( "logFailure", `- Functions: ${ce.selectedFunctions .map( (selectedFunction: ContractInterfaceFunction) => - selectedFunction.name + selectedFunction.name, ) .join(", ")} (${ce.selectedFunctions .map( (selectedFunction: ContractInterfaceFunction) => - selectedFunction.access + selectedFunction.access, ) - .join(", ")})` + .join(", ")})`, ); radio.emit( "logFailure", `- Arguments: ${ce.selectedFunctionsArgsList .map((selectedFunctionArgs: any[]) => - JSON.stringify(selectedFunctionArgs) + JSON.stringify(selectedFunctionArgs), ) - .join(", ")}` + .join(", ")}`, ); radio.emit( "logFailure", `- Callers : ${ce.sutCallers .map((sutCaller: [string, string]) => sutCaller[0]) - .join(", ")}` + .join(", ")}`, ); radio.emit( "logFailure", `- Outputs : ${ce.selectedFunctions .map((selectedFunction: ContractInterfaceFunction) => - JSON.stringify(selectedFunction.outputs) + JSON.stringify(selectedFunction.outputs), ) - .join(", ")}` + .join(", ")}`, ); radio.emit( "logFailure", - `- Invariant: ${ce.selectedInvariant.name} (${ce.selectedInvariant.access})` + `- Invariant: ${ce.selectedInvariant.name} (${ce.selectedInvariant.access})`, ); radio.emit( "logFailure", - `- Arguments: ${JSON.stringify(ce.invariantArgs)}` + `- Arguments: ${JSON.stringify(ce.invariantArgs)}`, ); radio.emit("logFailure", `- Caller : ${ce.invariantCaller[0]}`); radio.emit( "logFailure", - `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n` + `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n`, ); const formattedError = `The invariant "${ @@ -138,26 +140,26 @@ export function reporter( radio.emit( "logFailure", `- Contract : ${getContractNameFromContractId( - ce.rendezvousContractId - )}` + ce.rendezvousContractId, + )}`, ); radio.emit( "logFailure", - `- Test Function : ${ce.selectedTestFunction.name} (${ce.selectedTestFunction.access})` + `- Test Function : ${ce.selectedTestFunction.name} (${ce.selectedTestFunction.access})`, ); radio.emit( "logFailure", - `- Arguments : ${JSON.stringify(ce.functionArgs)}` + `- Arguments : ${JSON.stringify(ce.functionArgs)}`, ); radio.emit("logFailure", `- Caller : ${ce.testCaller[0]}`); radio.emit( "logFailure", - `- Outputs : ${JSON.stringify(ce.selectedTestFunction.outputs)}` + `- Outputs : ${JSON.stringify(ce.selectedTestFunction.outputs)}`, ); radio.emit( "logFailure", - `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n` + `\nWhat happened? Rendezvous went on a rampage and found a weak spot:\n`, ); const formattedError = `The test function "${ @@ -182,13 +184,13 @@ export function reporter( green( `\nOK, ${ type === "invariant" ? "invariants" : "properties" - } passed after ${numRuns} runs.\n` - ) + } passed after ${numRuns} runs.\n`, + ), ); } reportStatistics(statistics, type, radio); radio.emit("logMessage", "\n"); -} +}; const ARROW = "->"; const SUCCESS_SYMBOL = "+"; @@ -201,11 +203,11 @@ const WARN_SYMBOL = "!"; * @param type The type of test being reported. * @param radio The event emitter for logging messages. */ -function reportStatistics( +const reportStatistics = ( statistics: Statistics, type: "invariant" | "test", - radio: EventEmitter -): void { + radio: EventEmitter, +): void => { if ( (type === "invariant" && (!statistics.invariant || !statistics.sut)) || (type === "test" && !statistics.test) @@ -242,24 +244,24 @@ function reportStatistics( radio.emit("logMessage", "\nLEGEND:\n"); radio.emit( "logMessage", - " SUCCESSFUL calls executed and advanced the test" + " SUCCESSFUL calls executed and advanced the test", ); radio.emit( "logMessage", - " IGNORED calls failed but did not affect the test" + " IGNORED calls failed but did not affect the test", ); radio.emit( "logMessage", - " PASSED invariants maintained system integrity" + " PASSED invariants maintained system integrity", ); radio.emit( "logMessage", - " FAILED invariants indicate contract vulnerabilities" + " FAILED invariants indicate contract vulnerabilities", ); if (computeTotalCount(statistics.invariant!.failed) > 0) { radio.emit( "logFailure", - "\n! FAILED invariants require immediate attention as they indicate that your contract can enter an invalid state under certain conditions." + "\n! FAILED invariants require immediate attention as they indicate that your contract can enter an invalid state under certain conditions.", ); } break; @@ -284,26 +286,26 @@ function reportStatistics( radio.emit("logMessage", "\nLEGEND:\n"); radio.emit( "logMessage", - " PASSED properties verified for given inputs" + " PASSED properties verified for given inputs", ); radio.emit( "logMessage", - " DISCARDED skipped due to invalid preconditions" + " DISCARDED skipped due to invalid preconditions", ); radio.emit( "logMessage", - " FAILED property violations or unexpected behavior" + " FAILED property violations or unexpected behavior", ); if (computeTotalCount(statistics.test!.failed) > 0) { radio.emit( "logFailure", - "\n! FAILED tests indicate that your function properties don't hold for all inputs. Review the counterexamples above for debugging." + "\n! FAILED tests indicate that your function properties don't hold for all inputs. Review the counterexamples above for debugging.", ); } break; } } -} +}; /** * Displays a tree structure of data. @@ -311,18 +313,18 @@ function reportStatistics( * @param radio The event emitter for logging messages. * @param options Configuration options for tree display. */ -function logAsTree( +const logAsTree = ( tree: Record, radio: EventEmitter, - options: StatisticsTreeOptions = {} -): void { + options: StatisticsTreeOptions = {}, +): void => { const { isLastSection = false, baseIndent = " " } = options; const printTree = ( node: Record, + radioEmitter: EventEmitter, indent: string = baseIndent, - isLastParent: boolean = true, - radio: EventEmitter + isLastParent = true, ): void => { const keys = Object.keys(node); @@ -333,23 +335,23 @@ function logAsTree( const leadingChar = isLastSection ? " " : "│"; if (typeof node[key] === "object" && node[key] !== null) { - radio.emit( + radioEmitter.emit( "logMessage", - `${leadingChar} ${indent}${connector} ${ARROW} ${key}` + `${leadingChar} ${indent}${connector} ${ARROW} ${key}`, ); - printTree(node[key], nextIndent, isLast, radio); + printTree(node[key], radioEmitter, nextIndent, isLast); } else { const count = node[key] as number; - radio.emit( + radioEmitter.emit( "logMessage", - `${leadingChar} ${indent}${connector} ${key}: x${count}` + `${leadingChar} ${indent}${connector} ${key}: x${count}`, ); } }); }; - printTree(tree, baseIndent, true, radio); -} + printTree(tree, radio, baseIndent); +}; /** * Computes the total number of failures from a failure map. diff --git a/heatstroke.types.ts b/heatstroke.types.ts index 1dc9cb03..6cd73c00 100644 --- a/heatstroke.types.ts +++ b/heatstroke.types.ts @@ -1,6 +1,6 @@ -import { ContractInterfaceFunction } from "@stacks/clarinet-sdk-wasm"; +import type { ContractInterfaceFunction } from "@stacks/clarinet-sdk-wasm"; -export type RunDetails = { +export interface RunDetails { failed: boolean; counterexample: CounterExample[]; numRuns: number; @@ -8,18 +8,18 @@ export type RunDetails = { path?: string; error?: Error; errorInstance?: Error; -}; +} type CounterExample = TestCounterExample | InvariantCounterExample; -export type TestCounterExample = { +export interface TestCounterExample { rendezvousContractId: string; selectedTestFunction: ContractInterfaceFunction; functionArgs: any; testCaller: [string, string]; -}; +} -export type InvariantCounterExample = { +export interface InvariantCounterExample { rendezvousContractId: string; selectedFunctions: ContractInterfaceFunction[]; selectedFunctionsArgsList: any[]; @@ -27,29 +27,29 @@ export type InvariantCounterExample = { selectedInvariant: ContractInterfaceFunction; invariantArgs: any; invariantCaller: [string, string]; -}; +} -type SutFunctionStatistics = { +interface SutFunctionStatistics { successful: Map; failed: Map; -}; +} -type InvariantFunctionStatistics = { +interface InvariantFunctionStatistics { successful: Map; failed: Map; -}; +} -type TestFunctionStatistics = { +interface TestFunctionStatistics { successful: Map; discarded: Map; failed: Map; -}; +} -export type Statistics = { +export interface Statistics { sut?: SutFunctionStatistics; invariant?: InvariantFunctionStatistics; test?: TestFunctionStatistics; -}; +} /** * Options for configuring tree statistics reporting. diff --git a/invariant.tests.ts b/invariant.tests.ts index b2efe300..de6875f9 100644 --- a/invariant.tests.ts +++ b/invariant.tests.ts @@ -1,13 +1,15 @@ +import { rmSync } from "node:fs"; +import { join, resolve } from "node:path"; + import { initSimnet } from "@stacks/clarinet-sdk"; +import { Cl } from "@stacks/transactions"; + import { initializeClarityContext, initializeLocalContext } from "./invariant"; import { getContractNameFromContractId, getFunctionsFromContractInterfaces, getSimnetDeployerContractsInterfaces, } from "./shared"; -import { join, resolve } from "path"; -import { rmSync } from "fs"; -import { Cl } from "@stacks/transactions"; import { createIsolatedTestEnvironment } from "./test.utils"; const isolatedTestEnvPrefix = "rendezvous-test-invariant-"; @@ -21,16 +23,13 @@ describe("Simnet contracts operations", () => { ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); - const sutContractsInterfaces = - getSimnetDeployerContractsInterfaces(simnet); + const sutContractsInterfaces = getSimnetDeployerContractsInterfaces(simnet); const sutContractsAllFunctions = getFunctionsFromContractInterfaces( sutContractsInterfaces, ); // Pick the first contract for testing. - const [contractId, functions] = Array.from( - sutContractsAllFunctions.entries(), - )[0]; + const [contractId, functions] = [...sutContractsAllFunctions.entries()][0]; const expectedInitialContext = { [contractId]: Object.fromEntries(functions.map((f) => [f.name, 0])), @@ -54,24 +53,22 @@ describe("Simnet contracts operations", () => { ); const simnet = await initSimnet(join(tempDir, "Clarinet.toml")); - const rendezvousList = Array.from( - getSimnetDeployerContractsInterfaces(simnet).keys(), - ).filter((deployedContract) => + const rendezvousList = [ + ...getSimnetDeployerContractsInterfaces(simnet).keys(), + ].filter((deployedContract) => ["counter"].includes(getContractNameFromContractId(deployedContract)), ); const rendezvousAllFunctions = getFunctionsFromContractInterfaces( new Map( - Array.from(getSimnetDeployerContractsInterfaces(simnet)).filter( + [...getSimnetDeployerContractsInterfaces(simnet)].filter( ([contractId]) => rendezvousList.includes(contractId), ), ), ); // Pick the first contract for testing. - const [contractId, functions] = Array.from( - rendezvousAllFunctions.entries(), - )[0]; + const [contractId, functions] = [...rendezvousAllFunctions.entries()][0]; // Exercise initializeClarityContext(simnet, contractId, functions); @@ -94,13 +91,11 @@ describe("Simnet contracts operations", () => { // The JS representation of Clarity `(some (tuple (called uint)))`, where // `called` is initialized to 0. const expectedClarityValue = Cl.some(Cl.tuple({ called: Cl.uint(0) })); - const expectedContext = functions.map((f) => { - return { - contractId, - functionName: f.name, - called: expectedClarityValue, - }; - }); + const expectedContext = functions.map((f) => ({ + contractId, + functionName: f.name, + called: expectedClarityValue, + })); expect(actualContext).toEqual(expectedContext); diff --git a/invariant.ts b/invariant.ts index 996f580c..e1c10de2 100644 --- a/invariant.ts +++ b/invariant.ts @@ -1,5 +1,21 @@ -import { Simnet } from "@stacks/clarinet-sdk"; -import { EventEmitter } from "events"; +import type { EventEmitter } from "node:events"; +import { resolve } from "node:path"; + +import type { Simnet } from "@stacks/clarinet-sdk"; +import type { ContractInterfaceFunction } from "@stacks/clarinet-sdk-wasm"; +import { Cl, cvToJSON, cvToString } from "@stacks/transactions"; +import { dim, green, red, underline, yellow } from "ansicolor"; +import fc from "fast-check"; + +import { DialerRegistry, PostDialerError, PreDialerError } from "./dialer"; +import { reporter } from "./heatstroke"; +import type { Statistics } from "./heatstroke.types"; +import type { LocalContext } from "./invariant.types"; +import { + getFailureFilePath, + loadFailures, + persistFailure, +} from "./persistence"; import { argsToCV, functionToArbitrary, @@ -7,25 +23,15 @@ import { getFunctionsListForContract, LOG_DIVIDER, } from "./shared"; -import { LocalContext } from "./invariant.types"; -import { Cl, cvToJSON, cvToString } from "@stacks/transactions"; -import { reporter } from "./heatstroke"; -import fc from "fast-check"; -import { dim, green, red, underline, yellow } from "ansicolor"; -import { ContractInterfaceFunction } from "@stacks/clarinet-sdk-wasm"; +import type { EnrichedContractInterfaceFunction } from "./shared.types"; import { buildTraitReferenceMap, enrichInterfaceWithTraitData, extractProjectTraitImplementations, - isTraitReferenceFunction, getNonTestableTraitFunctions, + isTraitReferenceFunction, } from "./traits"; -import { EnrichedContractInterfaceFunction } from "./shared.types"; -import { DialerRegistry, PostDialerError, PreDialerError } from "./dialer"; -import { Statistics } from "./heatstroke.types"; -import { getFailureFilePath, loadFailures, persistFailure } from "./persistence"; -import { resolve } from "path"; -import { ImplementedTraitType } from "./traits.types"; +import type { ImplementedTraitType } from "./traits.types"; /** * Runs invariant testing on the target contract and logs the progress. Reports @@ -54,7 +60,7 @@ export const checkInvariants = async ( dial: string | undefined, bail: boolean, regr: boolean, - radio: EventEmitter + radio: EventEmitter, ) => { // The Rendezvous identifier is the first one in the list. Only one contract // can be fuzzed at a time. @@ -69,20 +75,21 @@ export const checkInvariants = async ( // arrays of their invariant functions. This map will be used to access the // invariant functions for each Rendezvous contract afterwards. const rendezvousInvariantFunctions = filterInvariantFunctions( - rendezvousAllFunctions + rendezvousAllFunctions, ); const sutFunctions = rendezvousSutFunctions.get(rendezvousContractId)!; const traitReferenceSutFunctions = sutFunctions.filter( - isTraitReferenceFunction + isTraitReferenceFunction, ); const invariantFunctions = rendezvousInvariantFunctions.get(rendezvousContractId)!; const traitReferenceInvariantFunctions = invariantFunctions.filter( - isTraitReferenceFunction + isTraitReferenceFunction, ); - const targetContractName = getContractNameFromContractId(rendezvousContractId); + const targetContractName = + getContractNameFromContractId(rendezvousContractId); const sutTraitReferenceMap = buildTraitReferenceMap(sutFunctions); const invariantTraitReferenceMap = buildTraitReferenceMap(invariantFunctions); @@ -92,7 +99,7 @@ export const checkInvariants = async ( simnet.getContractAST(targetContractName), sutTraitReferenceMap, sutFunctions, - rendezvousContractId + rendezvousContractId, ) : rendezvousSutFunctions; @@ -102,7 +109,7 @@ export const checkInvariants = async ( simnet.getContractAST(targetContractName), invariantTraitReferenceMap, invariantFunctions, - rendezvousContractId + rendezvousContractId, ) : rendezvousInvariantFunctions; @@ -117,7 +124,7 @@ export const checkInvariants = async ( enrichedSutFunctionsInterfaces, sutTraitReferenceMap, projectTraitImplementations, - rendezvousContractId + rendezvousContractId, ); // Extract invariant functions with missing trait implementations. These @@ -127,14 +134,14 @@ export const checkInvariants = async ( enrichedInvariantFunctionsInterfaces, invariantTraitReferenceMap, projectTraitImplementations, - rendezvousContractId + rendezvousContractId, ); // Emit warnings for functions with missing trait implementations emitMissingTraitWarnings( radio, sutFunctionsWithMissingTraits, - invariantFunctionsWithMissingTraits + invariantFunctionsWithMissingTraits, ); // Filter out functions with missing trait implementations from the enriched @@ -161,20 +168,20 @@ export const checkInvariants = async ( const functions = getFunctionsListForContract( executableSutFunctions, - rendezvousContractId + rendezvousContractId, ); const invariants = getFunctionsListForContract( executableInvariantFunctions, - rendezvousContractId + rendezvousContractId, ); if (functions?.length === 0) { radio.emit( "logMessage", red( - `No public functions found for the "${targetContractName}" contract. Without public functions, no state transitions can happen inside the contract, and the invariant test is not meaningful.\n` - ) + `No public functions found for the "${targetContractName}" contract. Without public functions, no state transitions can happen inside the contract, and the invariant test is not meaningful.\n`, + ), ); return; } @@ -183,8 +190,8 @@ export const checkInvariants = async ( radio.emit( "logMessage", red( - `No invariant functions found for the "${targetContractName}" contract. Beware, for your contract may be exposed to unforeseen issues.\n` - ) + `No invariant functions found for the "${targetContractName}" contract. Beware, for your contract may be exposed to unforeseen issues.\n`, + ), ); return; } @@ -194,12 +201,12 @@ export const checkInvariants = async ( radio.emit( "logMessage", `Regressions loaded from: ${resolve( - getFailureFilePath(rendezvousContractId) - )}` + getFailureFilePath(rendezvousContractId), + )}`, ); radio.emit( "logMessage", - `Loading ${targetContractName} contract regressions...\n` + `Loading ${targetContractName} contract regressions...\n`, ); const regressions = loadFailures(rendezvousContractId, "invariant"); @@ -207,8 +214,8 @@ export const checkInvariants = async ( radio.emit( "logMessage", `Found ${underline( - `${regressions.length} regressions` - )} for the ${targetContractName} contract.\n` + `${regressions.length} regressions`, + )} for the ${targetContractName} contract.\n`, ); for (const regression of regressions) { @@ -218,7 +225,7 @@ export const checkInvariants = async ( regression.seed, regression.numRuns, regression.dial, - regression.timestamp + regression.timestamp, ); await resetSession(); @@ -241,7 +248,7 @@ export const checkInvariants = async ( // Run fresh invariant tests using user-provided configuration. radio.emit( "logMessage", - `Starting fresh round of invariant testing for the ${targetContractName} contract using user-provided configuration...\n` + `Starting fresh round of invariant testing for the ${targetContractName} contract using user-provided configuration...\n`, ); await invariantTest({ @@ -293,7 +300,7 @@ interface InvariantTestContext { * @returns A promise that resolves when the invariant test is complete. */ const invariantTest = async ( - config: InvariantTestConfig & InvariantTestContext + config: InvariantTestConfig & InvariantTestContext, ) => { const { simnet, @@ -312,9 +319,9 @@ const invariantTest = async ( // Derive accounts and addresses from simnet. const simnetAccounts = simnet.getAccounts(); const eligibleAccounts = new Map( - [...simnetAccounts].filter(([key]) => key !== "faucet") + [...simnetAccounts].filter(([key]) => key !== "faucet"), ); - const simnetAddresses = Array.from(simnetAccounts.values()); + const simnetAddresses = [...simnetAccounts.values()]; /** * The dialer registry, which is used to keep track of all the custom dialers @@ -384,7 +391,7 @@ const invariantTest = async ( }), selectedInvariant: fc.constantFrom(...invariants), }) - .map((selectedFunctions) => ({ ...r, ...selectedFunctions })) + .map((selectedFunctions) => ({ ...r, ...selectedFunctions })), ) .chain((r) => fc @@ -394,7 +401,7 @@ const invariantTest = async ( { minLength: r.selectedFunctions.length, maxLength: r.selectedFunctions.length, - } + }, ), selectedFunctionsArgsList: fc.tuple( ...r.selectedFunctions.map((selectedFunction) => @@ -402,20 +409,20 @@ const invariantTest = async ( ...functionToArbitrary( selectedFunction, simnetAddresses, - projectTraitImplementations - ) - ) - ) + projectTraitImplementations, + ), + ), + ), ), invariantArgs: fc.tuple( ...functionToArbitrary( r.selectedInvariant, simnetAddresses, - projectTraitImplementations - ) + projectTraitImplementations, + ), ), }) - .map((args) => ({ ...r, ...args })) + .map((args) => ({ ...r, ...args })), ) .chain((r) => fc @@ -433,16 +440,16 @@ const invariantTest = async ( }) : fc.constant(0), }) - .map((burnBlocks) => ({ ...r, ...burnBlocks })) + .map((burnBlocks) => ({ ...r, ...burnBlocks })), ), async (r) => { const selectedFunctionsArgsCV = r.selectedFunctions.map( (selectedFunction, index) => - argsToCV(selectedFunction, r.selectedFunctionsArgsList[index]) + argsToCV(selectedFunction, r.selectedFunctionsArgsList[index]), ); const selectedInvariantArgsCV = argsToCV( r.selectedInvariant, - r.invariantArgs + r.invariantArgs, ); for (const [index, selectedFunction] of r.selectedFunctions.entries()) { @@ -477,7 +484,7 @@ const invariantTest = async ( r.rendezvousContractId, selectedFunction.name, selectedFunctionsArgsCV[index], - sutCallerAddress + sutCallerAddress, ); const functionCallResultJson = cvToJSON(functionCall.result); @@ -488,13 +495,13 @@ const invariantTest = async ( // experiance by providing important information about the function // call during the run. const selectedFunctionClarityResult = cvToString( - functionCall.result + functionCall.result, ); if (functionCallResultJson.success) { statistics.sut!.successful.set( selectedFunction.name, - statistics.sut!.successful.get(selectedFunction.name)! + 1 + statistics.sut!.successful.get(selectedFunction.name)! + 1, ); localContext[r.rendezvousContractId][selectedFunction.name]++; @@ -504,10 +511,10 @@ const invariantTest = async ( [ Cl.stringAscii(selectedFunction.name), Cl.uint( - localContext[r.rendezvousContractId][selectedFunction.name] + localContext[r.rendezvousContractId][selectedFunction.name], ), ], - simnet.deployer + simnet.deployer, ); // Function call passed. @@ -519,7 +526,7 @@ const invariantTest = async ( `${targetContractName} ` + `${underline(selectedFunction.name)} ` + `${printedFunctionArgs} ` + - green(selectedFunctionClarityResult) + green(selectedFunctionClarityResult), ); try { @@ -537,7 +544,7 @@ const invariantTest = async ( // Function call failed. statistics.sut!.failed.set( selectedFunction.name, - statistics.sut!.failed.get(selectedFunction.name)! + 1 + statistics.sut!.failed.get(selectedFunction.name)! + 1, ); radio.emit( "logMessage", @@ -548,8 +555,8 @@ const invariantTest = async ( `${targetContractName} ` + `${underline(selectedFunction.name)} ` + `${printedFunctionArgs} ` + - red(selectedFunctionClarityResult) - ) + red(selectedFunctionClarityResult), + ), ); } } catch (error: any) { @@ -577,8 +584,8 @@ const invariantTest = async ( `${targetContractName} ` + `${underline(selectedFunction.name)} ` + `${printedFunctionArgs} ` + - red(displayedError) - ) + red(displayedError), + ), ); } } @@ -604,7 +611,7 @@ const invariantTest = async ( r.rendezvousContractId, r.selectedInvariant.name, selectedInvariantArgsCV, - invariantCallerAddress + invariantCallerAddress, ); const invariantCallResultJson = cvToJSON(invariantCallResult); @@ -615,7 +622,7 @@ const invariantTest = async ( statistics.invariant!.successful.set( r.selectedInvariant.name, statistics.invariant!.successful.get(r.selectedInvariant.name)! + - 1 + 1, ); radio.emit( "logMessage", @@ -626,12 +633,12 @@ const invariantTest = async ( `${targetContractName} ` + `${underline(r.selectedInvariant.name)} ` + `${printedInvariantArgs} ` + - green(invariantCallClarityResult) + green(invariantCallClarityResult), ); } else { statistics.invariant!.failed.set( r.selectedInvariant.name, - statistics.invariant!.failed.get(r.selectedInvariant.name)! + 1 + statistics.invariant!.failed.get(r.selectedInvariant.name)! + 1, ); radio.emit( "logMessage", @@ -643,8 +650,8 @@ const invariantTest = async ( `${targetContractName} ` + `${underline(r.selectedInvariant.name)} ` + `${printedInvariantArgs} ` + - red(invariantCallClarityResult) - ) + red(invariantCallClarityResult), + ), ); // Invariant call went through, but returned something other than @@ -652,7 +659,7 @@ const invariantTest = async ( // runtime errors. throw new FalsifiedInvariantError( `Invariant failed for ${targetContractName} contract: "${r.selectedInvariant.name}" returned ${invariantCallClarityResult}`, - invariantCallClarityResult + invariantCallClarityResult, ); } } catch (error: any) { @@ -668,8 +675,8 @@ const invariantTest = async ( `[FAIL] ` + `${targetContractName} ` + `${underline(r.selectedInvariant.name)} ` + - printedInvariantArgs - ) + printedInvariantArgs, + ), ); } @@ -680,7 +687,7 @@ const invariantTest = async ( if (r.canMineBlocks) { simnet.mineEmptyBurnBlocks(r.burnBlocks); } - } + }, ), { endOnFailure: bail, @@ -688,7 +695,7 @@ const invariantTest = async ( reporter: radioReporter, seed: seed, verbose: true, - } + }, ); }; @@ -696,11 +703,11 @@ const invariantTest = async ( * Emits warnings for functions that reference traits without eligible * implementations. */ -function emitMissingTraitWarnings( +const emitMissingTraitWarnings = ( radio: EventEmitter, sutFunctions: string[], - invariantFunctions: string[] -): void { + invariantFunctions: string[], +): void => { if (sutFunctions.length === 0 && invariantFunctions.length === 0) { return; } @@ -710,8 +717,8 @@ function emitMissingTraitWarnings( radio.emit( "logMessage", yellow( - `\nWarning: The following SUT functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n` - ) + `\nWarning: The following SUT functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`, + ), ); } @@ -720,18 +727,18 @@ function emitMissingTraitWarnings( radio.emit( "logMessage", yellow( - `\nWarning: The following invariant functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n` - ) + `\nWarning: The following invariant functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`, + ), ); } radio.emit( "logMessage", yellow( - `Note: You can add contracts implementing traits either as project contracts or as Clarinet requirements.\n` - ) + `Note: You can add contracts implementing traits either as project contracts or as Clarinet requirements.\n`, + ), ); -} +}; /** * Initializes the local context, setting the number of times each function @@ -742,7 +749,7 @@ function emitMissingTraitWarnings( */ export const initializeLocalContext = ( contractId: string, - functions: EnrichedContractInterfaceFunction[] + functions: EnrichedContractInterfaceFunction[], ): LocalContext => ({ [contractId]: Object.fromEntries(functions.map((f) => [f.name, 0])), }); @@ -757,19 +764,19 @@ export const initializeLocalContext = ( export const initializeClarityContext = ( simnet: Simnet, contractId: string, - functions: EnrichedContractInterfaceFunction[] + functions: EnrichedContractInterfaceFunction[], ) => { functions.forEach((fn) => { const { result: initialize } = simnet.callPublicFn( contractId, "update-context", [Cl.stringAscii(fn.name), Cl.uint(0)], - simnet.deployer + simnet.deployer, ); const jsonResult = cvToJSON(initialize); if (!jsonResult.value || !jsonResult.success) { throw new Error( - `Failed to initialize the context for function: ${fn.name}.` + `Failed to initialize the context for function: ${fn.name}.`, ); } }); @@ -787,7 +794,7 @@ export const initializeClarityContext = ( * contract. */ const filterSutFunctions = ( - allFunctionsMap: Map + allFunctionsMap: Map, ) => new Map( Array.from(allFunctionsMap, ([contractId, functions]) => [ @@ -796,22 +803,22 @@ const filterSutFunctions = ( (f) => f.access === "public" && f.name !== "update-context" && - !f.name.startsWith("test-") + !f.name.startsWith("test-"), ), - ]) + ]), ); const filterInvariantFunctions = ( - allFunctionsMap: Map + allFunctionsMap: Map, ) => new Map( Array.from(allFunctionsMap, ([contractId, functions]) => [ contractId, functions.filter( ({ access, name }) => - access === "read_only" && name.startsWith("invariant-") + access === "read_only" && name.startsWith("invariant-"), ), - ]) + ]), ); export class FalsifiedInvariantError extends Error { @@ -828,19 +835,19 @@ const emitInvariantRegressionTestHeader = ( seed: number, numRuns: number, dial: string | undefined, - timestamp: number + timestamp: number, ) => { radio.emit("logMessage", LOG_DIVIDER); radio.emit( "logMessage", ` Running ${underline( - timestamp + timestamp, )} regression test for the ${targetContractName} contract with: - Seed: ${seed} - Runs: ${numRuns} - Dial: ${dial ?? "none (default)"} -` +`, ); }; diff --git a/invariant.types.ts b/invariant.types.ts index 62811fd9..eadacfea 100644 --- a/invariant.types.ts +++ b/invariant.types.ts @@ -1,3 +1,7 @@ +type ContractId = string; + +type SutFunctionName = string; + /** * LocalContext is a data structure used to track the number of times each SUT * function is called for every contract. It is a nested map where: @@ -5,8 +9,4 @@ * - The inner key is the SUT function name within the contract. * - The value is the count of times the SUT function has been invoked. */ -export type LocalContext = { - [contractId: string]: { - [functionName: string]: number; - }; -}; +export type LocalContext = Record>; diff --git a/jest.config.js b/jest.config.js index 29a2262b..8bab1329 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,7 @@ module.exports = { silent: true, modulePathIgnorePatterns: ["/dist/"], setupFilesAfterEnv: ["/jest.setup.js"], - testTimeout: 600000, // 10 minutes + testTimeout: 600_000, // 10 minutes maxWorkers: 1, collectCoverage: false, collectCoverageFrom: [ diff --git a/jest.setup.js b/jest.setup.js index a45b041a..f3d1078c 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,9 +1,9 @@ const { initSimnet } = require("@stacks/clarinet-sdk"); -const { join, resolve } = require("path"); +const { join, resolve } = require("node:path"); // Ensure that the Clarinet project cache and deployment plan are initialized // before all the tests run. beforeAll(async () => { const manifestPath = join(resolve(__dirname, "example"), "Clarinet.toml"); await initSimnet(manifestPath); -}, 30000); +}, 30_000); diff --git a/oxlintrc.json b/oxlintrc.json new file mode 100644 index 00000000..00fd7648 --- /dev/null +++ b/oxlintrc.json @@ -0,0 +1,26 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "rules": { + "no-magic-numbers": "off", + "sort-keys": "off", + "sort-imports": "off", + "capitalized-comments": "off", + "id-length": "off", + "max-params": "off", + "max-statements": "off", + "prefer-destructuring": "off", + "no-ternary": "off", + "no-nested-ternary": "off", + "no-await-in-loop": "off", + "no-continue": "off", + "prefer-template": "off" + }, + "categories": { + "correctness": "error", + "suspicious": "error", + "perf": "error", + "style": "warn" + }, + "ignorePatterns": ["dist", "example", "node_modules"], + "plugins": ["typescript"] +} diff --git a/package-lock.json b/package-lock.json index f8f7e595..987f5d36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,9 +18,12 @@ "rv": "dist/app.js" }, "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.7.1", "@stacks/clarinet-sdk-wasm": "^3.15.0", "@types/jest": "^30.0.0", "jest": "^30.2.0", + "oxlint": "^1.58.0", + "prettier": "^3.8.1", "ts-jest": "^29.4.6", "typescript": "^5.9.3" } @@ -555,6 +558,54 @@ "tslib": "^2.4.0" } }, + "node_modules/@ianvs/prettier-plugin-sort-imports": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/@ianvs/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.7.1.tgz", + "integrity": "sha512-jmTNYGlg95tlsoG3JLCcuC4BrFELJtLirLAkQW/71lXSyOhVt/Xj7xWbbGcuVbNq1gwWgSyMrPjJc9Z30hynVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/generator": "^7.26.2", + "@babel/parser": "^7.26.2", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "semver": "^7.5.2" + }, + "peerDependencies": { + "@prettier/plugin-oxc": "^0.0.4 || ^0.1.0", + "@vue/compiler-sfc": "2.7.x || 3.x", + "content-tag": "^4.0.0", + "prettier": "2 || 3 || ^4.0.0-0", + "prettier-plugin-ember-template-tag": "^2.1.0" + }, + "peerDependenciesMeta": { + "@prettier/plugin-oxc": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "content-tag": { + "optional": true + }, + "prettier-plugin-ember-template-tag": { + "optional": true + } + } + }, + "node_modules/@ianvs/prettier-plugin-sort-imports/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1026,6 +1077,329 @@ ], "license": "MIT" }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.58.0.tgz", + "integrity": "sha512-1T7UN3SsWWxpWyWGn1cT3ASNJOo+pI3eUkmEl7HgtowapcV8kslYpFQcYn431VuxghXakPNlbjRwhqmR37PFOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.58.0.tgz", + "integrity": "sha512-GryzujxuiRv2YFF7bRy8mKcxlbuAN+euVUtGJt9KKbLT8JBUIosamVhcthLh+VEr6KE6cjeVMAQxKAzJcoN7dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.58.0.tgz", + "integrity": "sha512-7/bRSJIwl4GxeZL9rPZ11anNTyUO9epZrfEJH/ZMla3+/gbQ6xZixh9nOhsZ0QwsTW7/5J2A/fHbD1udC5DQQA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.58.0.tgz", + "integrity": "sha512-EqdtJSiHweS2vfILNrpyJ6HUwpEq2g7+4Zx1FPi4hu3Hu7tC3znF6ufbXO8Ub2LD4mGgznjI7kSdku9NDD1Mkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.58.0.tgz", + "integrity": "sha512-VQt5TH4M42mY20F545G637RKxV/yjwVtKk2vfXuazfReSIiuvWBnv+FVSvIV5fKVTJNjt3GSJibh6JecbhGdBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.58.0.tgz", + "integrity": "sha512-fBYcj4ucwpAtjJT3oeBdFBYKvNyjRSK+cyuvBOTQjh0jvKp4yeA4S/D0IsCHus/VPaNG5L48qQkh+Vjy3HL2/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.58.0.tgz", + "integrity": "sha512-0BeuFfwlUHlJ1xpEdSD1YO3vByEFGPg36uLjK1JgFaxFb4W6w17F8ET8sz5cheZ4+x5f2xzdnRrrWv83E3Yd8g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.58.0.tgz", + "integrity": "sha512-TXlZgnPTlxrQzxG9ZXU7BNwx1Ilrr17P3GwZY0If2EzrinqRH3zXPc3HrRcBJgcsoZNMuNL5YivtkJYgp467UQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.58.0.tgz", + "integrity": "sha512-zSoYRo5dxHLcUx93Stl2hW3hSNjPt99O70eRVWt5A1zwJ+FPjeCCANCD2a9R4JbHsdcl11TIQOjyigcRVOH2mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.58.0.tgz", + "integrity": "sha512-NQ0U/lqxH2/VxBYeAIvMNUK1y0a1bJ3ZicqkF2c6wfakbEciP9jvIE4yNzCFpZaqeIeRYaV7AVGqEO1yrfVPjA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.58.0.tgz", + "integrity": "sha512-X9J+kr3gIC9FT8GuZt0ekzpNUtkBVzMVU4KiKDSlocyQuEgi3gBbXYN8UkQiV77FTusLDPsovjo95YedHr+3yg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.58.0.tgz", + "integrity": "sha512-CDze3pi1OO3Wvb/QsXjmLEY4XPKGM6kIo82ssNOgmcl1IdndF9VSGAE38YLhADWmOac7fjqhBw82LozuUVxD0Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.58.0.tgz", + "integrity": "sha512-b/89glbxFaEAcA6Uf1FvCNecBJEgcUTsV1quzrqXM/o4R1M4u+2KCVuyGCayN2UpsRWtGGLb+Ver0tBBpxaPog==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.58.0.tgz", + "integrity": "sha512-0/yYpkq9VJFCEcuRlrViGj8pJUFFvNS4EkEREaN7CB1EcLXJIaVSSa5eCihwBGXtOZxhnblWgxks9juRdNQI7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.58.0.tgz", + "integrity": "sha512-hr6FNvmcAXiH+JxSvaJ4SJ1HofkdqEElXICW9sm3/Rd5eC3t7kzvmLyRAB3NngKO2wzXRCAm4Z/mGWfrsS4X8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.58.0.tgz", + "integrity": "sha512-R+O368VXgRql1K6Xar+FEo7NEwfo13EibPMoTv3sesYQedRXd6m30Dh/7lZMxnrQVFfeo4EOfYIP4FpcgWQNHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.58.0.tgz", + "integrity": "sha512-Q0FZiAY/3c4YRj4z3h9K1PgaByrifrfbBoODSeX7gy97UtB7pySPUQfC2B/GbxWU6k7CzQrRy5gME10PltLAFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.58.0.tgz", + "integrity": "sha512-Y8FKBABrSPp9H0QkRLHDHOSUgM/309a3IvOVgPcVxYcX70wxJrk608CuTg7w+C6vEd724X5wJoNkBcGYfH7nNQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.58.0.tgz", + "integrity": "sha512-bCn5rbiz5My+Bj7M09sDcnqW0QJyINRVxdZ65x1/Y2tGrMwherwK/lpk+HRQCKvXa8pcaQdF5KY5j54VGZLwNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3725,6 +4099,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oxlint": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.58.0.tgz", + "integrity": "sha512-t4s9leczDMqlvOSjnbCQe7gtoLkWgBGZ7sBdCJ9EOj5IXFSG/X7OAzK4yuH4iW+4cAYe8kLFbC8tuYMwWZm+Cg==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.58.0", + "@oxlint/binding-android-arm64": "1.58.0", + "@oxlint/binding-darwin-arm64": "1.58.0", + "@oxlint/binding-darwin-x64": "1.58.0", + "@oxlint/binding-freebsd-x64": "1.58.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.58.0", + "@oxlint/binding-linux-arm-musleabihf": "1.58.0", + "@oxlint/binding-linux-arm64-gnu": "1.58.0", + "@oxlint/binding-linux-arm64-musl": "1.58.0", + "@oxlint/binding-linux-ppc64-gnu": "1.58.0", + "@oxlint/binding-linux-riscv64-gnu": "1.58.0", + "@oxlint/binding-linux-riscv64-musl": "1.58.0", + "@oxlint/binding-linux-s390x-gnu": "1.58.0", + "@oxlint/binding-linux-x64-gnu": "1.58.0", + "@oxlint/binding-linux-x64-musl": "1.58.0", + "@oxlint/binding-openharmony-arm64": "1.58.0", + "@oxlint/binding-win32-arm64-msvc": "1.58.0", + "@oxlint/binding-win32-ia32-msvc": "1.58.0", + "@oxlint/binding-win32-x64-msvc": "1.58.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=0.18.0" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + } + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -3899,6 +4318,22 @@ "node": ">=8" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-format": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", diff --git a/package.json b/package.json index fc9263a0..0849c533 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,10 @@ }, "scripts": { "build": "npx -p typescript tsc --project tsconfig.json && node -e \"if (process.platform !== 'win32') require('fs').chmodSync('./dist/app.js', 0o755);\"", + "fmt": "prettier --write .", + "fmt:check": "prettier --check .", + "lint": "oxlint -c oxlintrc.json", + "lint:fix": "oxlint -c oxlintrc.json --fix", "test": "npx jest", "test:coverage": "npx jest --coverage" }, @@ -36,9 +40,12 @@ "fast-check": "^4.5.3" }, "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.7.1", "@stacks/clarinet-sdk-wasm": "^3.15.0", "@types/jest": "^30.0.0", "jest": "^30.2.0", + "oxlint": "^1.58.0", + "prettier": "^3.8.1", "ts-jest": "^29.4.6", "typescript": "^5.9.3" } diff --git a/persistence.tests.ts b/persistence.tests.ts index 7ea8da78..508508f3 100644 --- a/persistence.tests.ts +++ b/persistence.tests.ts @@ -1,13 +1,15 @@ -import { mkdirSync, rmSync, statSync } from "fs"; -import { tmpdir } from "os"; -import { join, resolve } from "path"; +import { mkdirSync, rmSync, statSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +import fc from "fast-check"; + +import type { RunDetails } from "./heatstroke.types"; import { getFailureFilePath, - persistFailure, loadFailures, + persistFailure, } from "./persistence"; -import { RunDetails } from "./heatstroke.types"; -import fc from "fast-check"; const temporaryTestBaseDir = resolve(tmpdir(), "rendezvous-test-persistence"); @@ -24,8 +26,8 @@ const createTemporaryCustomTestBaseDir = (dirName: string) => { // Mock RunDetails helper const createMockRunDetails = ( seed: number, - failed: boolean = true, - numRuns: number = 100 + failed = true, + numRuns = 100, ): RunDetails => ({ failed, seed, @@ -45,7 +47,7 @@ describe("Failure Persistence", () => { // Assert expect(filePath).toBe( - resolve(".rendezvous-regressions", `${TEST_CONTRACT_ID}.json`) + resolve(".rendezvous-regressions", `${TEST_CONTRACT_ID}.json`), ); }); @@ -58,15 +60,15 @@ describe("Failure Persistence", () => { // Act const filePath = getFailureFilePath( TEST_CONTRACT_ID, - customBaseDir + customBaseDir, ); // Assert expect(filePath).toBe( - resolve(customBaseDir, `${TEST_CONTRACT_ID}.json`) + resolve(customBaseDir, `${TEST_CONTRACT_ID}.json`), ); - } - ) + }, + ), ); }); }); @@ -93,7 +95,7 @@ describe("Failure Persistence", () => { // Verify expect( - statSync(getFailureFilePath(TEST_CONTRACT_ID, customBaseDir)) + statSync(getFailureFilePath(TEST_CONTRACT_ID, customBaseDir)), ).toBeDefined(); // Teardown @@ -101,8 +103,8 @@ describe("Failure Persistence", () => { recursive: true, force: true, }); - } - ) + }, + ), ); }); @@ -129,7 +131,7 @@ describe("Failure Persistence", () => { undefined, { baseDir: customBaseDir, - } + }, ); persistFailure(runDetails2, "test", TEST_CONTRACT_ID, undefined, { baseDir: customBaseDir, @@ -141,7 +143,7 @@ describe("Failure Persistence", () => { "invariant", { baseDir: customBaseDir, - } + }, ); const testFailures = loadFailures(TEST_CONTRACT_ID, "test", { baseDir: customBaseDir, @@ -156,8 +158,8 @@ describe("Failure Persistence", () => { recursive: true, force: true, }); - } - ) + }, + ), ); }); @@ -195,8 +197,8 @@ describe("Failure Persistence", () => { recursive: true, force: true, }); - } - ) + }, + ), ); }); @@ -221,7 +223,7 @@ describe("Failure Persistence", () => { undefined, { baseDir: customBaseDir, - } + }, ); persistFailure(runDetails, "test", TEST_CONTRACT_ID, undefined, { baseDir: customBaseDir, @@ -233,7 +235,7 @@ describe("Failure Persistence", () => { "invariant", { baseDir: customBaseDir, - } + }, ); const testFailures = loadFailures(TEST_CONTRACT_ID, "test", { baseDir: customBaseDir, @@ -248,8 +250,8 @@ describe("Failure Persistence", () => { recursive: true, force: true, }); - } - ) + }, + ), ); }); @@ -277,7 +279,7 @@ describe("Failure Persistence", () => { "invariant", TEST_CONTRACT_ID, undefined, - { baseDir: customBaseDir } + { baseDir: customBaseDir }, ); }); @@ -292,8 +294,8 @@ describe("Failure Persistence", () => { // Teardown rmSync(customBaseDir, { recursive: true, force: true }); - } - ) + }, + ), ); }); @@ -319,7 +321,7 @@ describe("Failure Persistence", () => { undefined, { baseDir: customBaseDir, - } + }, ); const after = Date.now(); @@ -333,8 +335,8 @@ describe("Failure Persistence", () => { // Teardown rmSync(customBaseDir, { recursive: true, force: true }); - } - ) + }, + ), ); }); @@ -360,7 +362,7 @@ describe("Failure Persistence", () => { undefined, { baseDir: customBaseDir, - } + }, ); // Verify @@ -371,8 +373,8 @@ describe("Failure Persistence", () => { // Teardown rmSync(customBaseDir, { recursive: true, force: true }); - } - ) + }, + ), ); }); @@ -403,8 +405,8 @@ describe("Failure Persistence", () => { // Teardown rmSync(customBaseDir, { recursive: true, force: true }); - } - ) + }, + ), ); }); }); @@ -431,8 +433,8 @@ describe("Failure Persistence", () => { // Teardown rmSync(customBaseDir, { recursive: true, force: true }); - } - ) + }, + ), ); }); @@ -454,7 +456,7 @@ describe("Failure Persistence", () => { undefined, { baseDir: customBaseDir, - } + }, ); // Exercise @@ -467,8 +469,8 @@ describe("Failure Persistence", () => { // Teardown rmSync(customBaseDir, { recursive: true, force: true }); - } - ) + }, + ), ); }); }); diff --git a/persistence.ts b/persistence.ts index 9a6c3546..a7d09566 100644 --- a/persistence.ts +++ b/persistence.ts @@ -1,6 +1,7 @@ -import { mkdirSync, readFileSync, writeFileSync } from "fs"; -import { resolve } from "path"; -import { RunDetails } from "./heatstroke.types"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import type { RunDetails } from "./heatstroke.types"; /** * Represents a persisted failure record for regression testing. @@ -48,10 +49,8 @@ const DEFAULT_CONFIG: Required = { */ export const getFailureFilePath = ( contractId: string, - baseDir: string = DEFAULT_CONFIG.baseDir -): string => { - return resolve(baseDir, `${contractId}.json`); -}; + baseDir: string = DEFAULT_CONFIG.baseDir, +): string => resolve(baseDir, `${contractId}.json`); /** * Loads the failure store for a contract, or creates an empty one. @@ -61,14 +60,14 @@ export const getFailureFilePath = ( */ const loadFailureStore = ( contractId: string, - baseDir: string = DEFAULT_CONFIG.baseDir + baseDir: string = DEFAULT_CONFIG.baseDir, ): FailureStore => { const filePath = getFailureFilePath(contractId, baseDir); try { const content = readFileSync(filePath, "utf-8"); return JSON.parse(content); - } catch (error: any) { + } catch { return { invariant: [], test: [] }; } }; @@ -82,7 +81,7 @@ const loadFailureStore = ( const saveFailureStore = ( contractId: string, baseDir: string, - store: FailureStore + store: FailureStore, ): void => { // Ensure the base directory exists. mkdirSync(baseDir, { recursive: true }); @@ -105,7 +104,7 @@ export const persistFailure = ( type: "invariant" | "test", contractId: string, dial: string | undefined, - config?: PersistenceConfig + config?: PersistenceConfig, ): void => { const { baseDir } = { ...DEFAULT_CONFIG, ...config }; @@ -150,7 +149,7 @@ export const persistFailure = ( export const loadFailures = ( contractId: string, type: "invariant" | "test", - config?: PersistenceConfig + config?: PersistenceConfig, ): FailureRecord[] => { const { baseDir } = { ...DEFAULT_CONFIG, ...config }; const store = loadFailureStore(contractId, baseDir); diff --git a/property.tests.ts b/property.tests.ts index 8f767c33..ae7142ff 100644 --- a/property.tests.ts +++ b/property.tests.ts @@ -1,20 +1,22 @@ +import { rmSync } from "node:fs"; +import { join, resolve } from "node:path"; + import { initSimnet } from "@stacks/clarinet-sdk"; -import { - isParamsMatch, - isReturnTypeBoolean, - isTestDiscardedInPlace, -} from "./property"; -import { rmSync } from "fs"; -import { join, resolve } from "path"; -import fc from "fast-check"; -import { createIsolatedTestEnvironment } from "./test.utils"; -import { +import type { ContractInterfaceFunction, ContractInterfaceFunctionAccess, ContractInterfaceFunctionArg, ContractInterfaceFunctionOutput, } from "@stacks/clarinet-sdk-wasm"; import { cvToJSON } from "@stacks/transactions"; +import fc from "fast-check"; + +import { + isParamsMatch, + isReturnTypeBoolean, + isTestDiscardedInPlace, +} from "./property"; +import { createIsolatedTestEnvironment } from "./test.utils"; const isolatedTestEnvPrefix = "rendezvous-test-property-"; @@ -29,7 +31,7 @@ describe("Test discarding related operations", () => { fc.record({ name: fc.string(), type: fc.constantFrom("int128", "uint128", "bool", "principal"), - }) + }), ), outputs: fc.record({ type: fc.constant("bool") }), }), @@ -47,8 +49,8 @@ describe("Test discarding related operations", () => { }; const actual = isReturnTypeBoolean(discardFunctionInterface); expect(actual).toBe(true); - } - ) + }, + ), ); }); @@ -62,7 +64,7 @@ describe("Test discarding related operations", () => { fc.record({ name: fc.string(), type: fc.constantFrom("int128", "uint128", "bool", "principal"), - }) + }), ), outputs: fc.record({ type: fc.constantFrom("int128", "uint128", "principal"), @@ -82,8 +84,8 @@ describe("Test discarding related operations", () => { }; const actual = isReturnTypeBoolean(discardFunctionInterface); expect(actual).toBe(false); - } - ) + }, + ), ); }); @@ -97,7 +99,7 @@ describe("Test discarding related operations", () => { fc.record({ name: fc.string(), type: fc.constantFrom("int128", "uint128", "bool", "principal"), - }) + }), ), outputs: fc.record({ type: fc.constant("bool") }), }), @@ -121,11 +123,11 @@ describe("Test discarding related operations", () => { }; const actual = isParamsMatch( testFunctionInterface, - discardFunctionInterface + discardFunctionInterface, ); expect(actual).toBe(true); - } - ) + }, + ), ); }); @@ -144,9 +146,9 @@ describe("Test discarding related operations", () => { "int128", "uint128", "bool", - "principal" + "principal", ), - }) + }), ), outputs: fc.record({ type: fc.constant("bool") }), }), @@ -160,9 +162,9 @@ describe("Test discarding related operations", () => { "int128", "uint128", "bool", - "principal" + "principal", ), - }) + }), ), outputs: fc.record({ type: fc.constant("bool") }), }), @@ -170,13 +172,13 @@ describe("Test discarding related operations", () => { .filter( (r) => JSON.stringify( - [...r.testFn.args].sort((a, b) => a.name.localeCompare(b.name)) + [...r.testFn.args].sort((a, b) => a.name.localeCompare(b.name)), ) !== JSON.stringify( [...r.discardFn.args].sort((a, b) => - a.name.localeCompare(b.name) - ) - ) + a.name.localeCompare(b.name), + ), + ), ), (r: { testFn: { @@ -206,11 +208,11 @@ describe("Test discarding related operations", () => { }; const actual = isParamsMatch( testFunctionInterface, - discardFunctionInterface + discardFunctionInterface, ); expect(actual).toBe(false); - } - ) + }, + ), ); }); @@ -218,7 +220,7 @@ describe("Test discarding related operations", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); @@ -227,14 +229,14 @@ describe("Test discarding related operations", () => { "contract", "(define-public (discarded-fn) (ok false))", { clarityVersion: 2 }, - simnet.deployer + simnet.deployer, ); const { result: functionCallResult } = simnet.callPublicFn( "contract", "discarded-fn", [], - simnet.deployer + simnet.deployer, ); const functionCallResultJson = cvToJSON(functionCallResult); @@ -253,7 +255,7 @@ describe("Test discarding related operations", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); @@ -262,14 +264,14 @@ describe("Test discarding related operations", () => { "contract", "(define-public (not-discarded-fn) (ok true))", { clarityVersion: 2 }, - simnet.deployer + simnet.deployer, ); const { result: functionCallResult } = simnet.callPublicFn( "contract", "not-discarded-fn", [], - simnet.deployer + simnet.deployer, ); const functionCallResultJson = cvToJSON(functionCallResult); diff --git a/property.ts b/property.ts index aefbe602..535dbafa 100644 --- a/property.ts +++ b/property.ts @@ -1,9 +1,19 @@ -import { Simnet } from "@stacks/clarinet-sdk"; -import { EventEmitter } from "events"; -import fc from "fast-check"; +import type { EventEmitter } from "node:events"; +import { resolve } from "node:path"; + +import type { Simnet } from "@stacks/clarinet-sdk"; +import type { ContractInterfaceFunction } from "@stacks/clarinet-sdk-wasm"; import { cvToJSON, cvToString } from "@stacks/transactions"; +import { dim, green, red, underline, yellow } from "ansicolor"; +import fc from "fast-check"; + import { reporter } from "./heatstroke"; -import { Statistics } from "./heatstroke.types"; +import type { Statistics } from "./heatstroke.types"; +import { + getFailureFilePath, + loadFailures, + persistFailure, +} from "./persistence"; import { argsToCV, functionToArbitrary, @@ -11,19 +21,15 @@ import { getFunctionsListForContract, LOG_DIVIDER, } from "./shared"; -import { dim, green, red, underline, yellow } from "ansicolor"; -import { ContractInterfaceFunction } from "@stacks/clarinet-sdk-wasm"; +import type { EnrichedContractInterfaceFunction } from "./shared.types"; import { buildTraitReferenceMap, enrichInterfaceWithTraitData, extractProjectTraitImplementations, - isTraitReferenceFunction, getNonTestableTraitFunctions, + isTraitReferenceFunction, } from "./traits"; -import { getFailureFilePath, loadFailures, persistFailure } from "./persistence"; -import { resolve } from "path"; -import { ImplementedTraitType } from "./traits.types"; -import { EnrichedContractInterfaceFunction } from "./shared.types"; +import type { ImplementedTraitType } from "./traits.types"; /** * Runs property-based tests on the target contract and logs the progress. @@ -50,24 +56,26 @@ export const checkProperties = async ( runs: number | undefined, bail: boolean, regr: boolean, - radio: EventEmitter + radio: EventEmitter, ) => { // A map where the keys are the test contract identifiers and the values are // arrays of their test functions. This map will be used to access the test // functions for each test contract in the property-based testing routine. const testContractsTestFunctions = filterTestFunctions( - rendezvousAllFunctions + rendezvousAllFunctions, ); const rendezvousContractId = rendezvousList[0]; - const allTestFunctions = testContractsTestFunctions.get(rendezvousContractId)!; + const allTestFunctions = + testContractsTestFunctions.get(rendezvousContractId)!; const traitReferenceFunctionsCount = allTestFunctions.filter( - isTraitReferenceFunction + isTraitReferenceFunction, ).length; - const targetContractName = getContractNameFromContractId(rendezvousContractId); + const targetContractName = + getContractNameFromContractId(rendezvousContractId); const traitReferenceMap = buildTraitReferenceMap(allTestFunctions); const enrichedTestFunctionsInterfaces = @@ -76,7 +84,7 @@ export const checkProperties = async ( simnet.getContractAST(targetContractName), traitReferenceMap, allTestFunctions, - rendezvousContractId + rendezvousContractId, ) : testContractsTestFunctions; @@ -93,7 +101,7 @@ export const checkProperties = async ( enrichedTestFunctionsInterfaces, traitReferenceMap, projectTraitImplementations, - rendezvousContractId + rendezvousContractId, ) : []; @@ -111,8 +119,8 @@ export const checkProperties = async ( .filter( (functionInterface) => !functionsMissingTraitImplementations.includes( - functionInterface.name - ) + functionInterface.name, + ), ), ], ]); @@ -124,9 +132,9 @@ export const checkProperties = async ( Array.from(rendezvousAllFunctions, ([contractId, functions]) => [ contractId, functions.filter( - ({ access, name }) => access === "read_only" && name.startsWith("can-") + ({ access, name }) => access === "read_only" && name.startsWith("can-"), ), - ]) + ]), ); // Pair each test function with its corresponding discard function. When a @@ -144,27 +152,26 @@ export const checkProperties = async ( ?.find((pf) => pf.name === `can-${f.name}`); return [f.name, discardFunction?.name]; - }) + }), ), - ] - ) + ], + ), ); - const hasDiscardFunctionErrors = Array.from( - testContractsPairedFunctions - ).some(([contractId, pairedMap]) => - Array.from(pairedMap).some(([testFunctionName, discardFunctionName]) => - discardFunctionName - ? !validateDiscardFunction( - contractId, - discardFunctionName, - testFunctionName, - testContractsDiscardFunctions, - testContractsTestFunctions, - radio - ) - : false - ) + const hasDiscardFunctionErrors = [...testContractsPairedFunctions].some( + ([contractId, pairedMap]) => + [...pairedMap].some(([testFunctionName, discardFunctionName]) => + discardFunctionName + ? !validateDiscardFunction( + contractId, + discardFunctionName, + testFunctionName, + testContractsDiscardFunctions, + testContractsTestFunctions, + radio, + ) + : false, + ), ); if (hasDiscardFunctionErrors) { @@ -173,13 +180,15 @@ export const checkProperties = async ( const testFunctions = getFunctionsListForContract( executableTestContractsTestFunctions, - rendezvousContractId + rendezvousContractId, ); if (testFunctions?.length === 0) { radio.emit( "logMessage", - red(`No test functions found for the "${targetContractName}" contract.\n`) + red( + `No test functions found for the "${targetContractName}" contract.\n`, + ), ); return; } @@ -189,12 +198,12 @@ export const checkProperties = async ( radio.emit( "logMessage", `Regressions loaded from: ${resolve( - getFailureFilePath(rendezvousContractId) - )}` + getFailureFilePath(rendezvousContractId), + )}`, ); radio.emit( "logMessage", - `Loading ${targetContractName} contract regressions...\n` + `Loading ${targetContractName} contract regressions...\n`, ); const regressions = loadFailures(rendezvousContractId, "test"); @@ -202,8 +211,8 @@ export const checkProperties = async ( radio.emit( "logMessage", `Found ${underline( - `${regressions.length} regressions` - )} for the ${targetContractName} contract.\n` + `${regressions.length} regressions`, + )} for the ${targetContractName} contract.\n`, ); for (const regression of regressions) { @@ -212,7 +221,7 @@ export const checkProperties = async ( targetContractName, regression.seed, regression.numRuns, - regression.timestamp + regression.timestamp, ); await resetSession(); @@ -237,7 +246,7 @@ export const checkProperties = async ( // Run fresh tests using user-provided configuration. radio.emit( "logMessage", - `Starting fresh round of property testing for the ${targetContractName} contract using user-provided configuration...\n` + `Starting fresh round of property testing for the ${targetContractName} contract using user-provided configuration...\n`, ); await propertyTest({ @@ -287,7 +296,7 @@ interface PropertyTestContext { * @returns A promise that resolves when the property test is complete. */ const propertyTest = async ( - config: PropertyTestConfig & PropertyTestContext + config: PropertyTestConfig & PropertyTestContext, ) => { const { simnet, @@ -305,9 +314,9 @@ const propertyTest = async ( // Derive accounts and addresses from simnet. const simnetAccounts = simnet.getAccounts(); const eligibleAccounts = new Map( - [...simnetAccounts].filter(([key]) => key !== "faucet") + [...simnetAccounts].filter(([key]) => key !== "faucet"), ); - const simnetAddresses = Array.from(simnetAccounts.values()); + const simnetAddresses = [...simnetAccounts.values()]; const statistics: Statistics = { test: { @@ -333,7 +342,7 @@ const propertyTest = async ( "test", rendezvousContractId, // No dialers in property-based testing. - undefined + undefined, ); } }; @@ -354,7 +363,7 @@ const propertyTest = async ( .map((selectedTestFunction) => ({ ...r, ...selectedTestFunction, - })) + })), ) .chain((r) => fc @@ -363,11 +372,11 @@ const propertyTest = async ( ...functionToArbitrary( r.selectedTestFunction, simnetAddresses, - projectTraitImplementations - ) + projectTraitImplementations, + ), ), }) - .map((args) => ({ ...r, ...args })) + .map((args) => ({ ...r, ...args })), ) .chain((r) => fc @@ -385,12 +394,12 @@ const propertyTest = async ( }) : fc.constant(0), }) - .map((burnBlocks) => ({ ...r, ...burnBlocks })) + .map((burnBlocks) => ({ ...r, ...burnBlocks })), ), async (r) => { const selectedTestFunctionArgs = argsToCV( r.selectedTestFunction, - r.functionArgs + r.functionArgs, ); const printedTestFunctionArgs = r.functionArgs @@ -416,13 +425,13 @@ const propertyTest = async ( selectedTestFunctionArgs, r.rendezvousContractId, simnet, - testCallerAddress + testCallerAddress, ); if (discarded) { statistics.test!.discarded.set( r.selectedTestFunction.name, - statistics.test!.discarded.get(r.selectedTestFunction.name)! + 1 + statistics.test!.discarded.get(r.selectedTestFunction.name)! + 1, ); radio.emit( "logMessage", @@ -432,7 +441,7 @@ const propertyTest = async ( `${yellow("[WARN]")} ` + `${targetContractName} ` + `${underline(r.selectedTestFunction.name)} ` + - dim(printedTestFunctionArgs) + dim(printedTestFunctionArgs), ); } else { try { @@ -442,23 +451,24 @@ const propertyTest = async ( r.rendezvousContractId, r.selectedTestFunction.name, selectedTestFunctionArgs, - testCallerAddress + testCallerAddress, ); const testFunctionCallResultJson = cvToJSON(testFunctionCallResult); const discardedInPlace = isTestDiscardedInPlace( - testFunctionCallResultJson + testFunctionCallResultJson, ); const testFunctionCallClarityResult = cvToString( - testFunctionCallResult + testFunctionCallResult, ); if (discardedInPlace) { statistics.test!.discarded.set( r.selectedTestFunction.name, - statistics.test!.discarded.get(r.selectedTestFunction.name)! + 1 + statistics.test!.discarded.get(r.selectedTestFunction.name)! + + 1, ); radio.emit( "logMessage", @@ -469,7 +479,7 @@ const propertyTest = async ( `${targetContractName} ` + `${underline(r.selectedTestFunction.name)} ` + `${dim(printedTestFunctionArgs)} ` + - yellow(testFunctionCallClarityResult) + yellow(testFunctionCallClarityResult), ); } else if ( !discardedInPlace && @@ -479,7 +489,7 @@ const propertyTest = async ( statistics.test!.successful.set( r.selectedTestFunction.name, statistics.test!.successful.get(r.selectedTestFunction.name)! + - 1 + 1, ); radio.emit( "logMessage", @@ -490,7 +500,7 @@ const propertyTest = async ( `${targetContractName} ` + `${underline(r.selectedTestFunction.name)} ` + `${printedTestFunctionArgs} ` + - green(testFunctionCallClarityResult) + green(testFunctionCallClarityResult), ); if (r.canMineBlocks) { @@ -499,14 +509,14 @@ const propertyTest = async ( } else { statistics.test!.failed.set( r.selectedTestFunction.name, - statistics.test!.failed.get(r.selectedTestFunction.name)! + 1 + statistics.test!.failed.get(r.selectedTestFunction.name)! + 1, ); // The function call did not result in (ok true) or (ok false). // Either the test failed or the test function returned an // unexpected value i.e. `(ok 1)`. throw new PropertyTestError( `Test failed for ${targetContractName} contract: "${r.selectedTestFunction.name}" returned ${testFunctionCallClarityResult}`, - testFunctionCallClarityResult + testFunctionCallClarityResult, ); } } catch (error: any) { @@ -514,10 +524,10 @@ const propertyTest = async ( error instanceof PropertyTestError ? error.clarityError : error && - typeof error === "string" && - error.toLowerCase().includes("runtime") - ? "(runtime)" - : "(unknown)"; + typeof error === "string" && + error.toLowerCase().includes("runtime") + ? "(runtime)" + : "(unknown)"; // Capture the error and log the test failure. radio.emit( @@ -530,15 +540,15 @@ const propertyTest = async ( `${targetContractName} ` + `${underline(r.selectedTestFunction.name)} ` + `${printedTestFunctionArgs} ` + - displayedError - ) + displayedError, + ), ); // Re-throw the error for fast-check to catch and process. throw error; } } - } + }, ), { endOnFailure: bail, @@ -546,7 +556,7 @@ const propertyTest = async ( reporter: radioReporter, seed: seed, verbose: true, - } + }, ); }; @@ -556,7 +566,7 @@ const propertyTest = async ( */ const emitMissingTraitWarning = ( radio: EventEmitter, - functionNames: string[] + functionNames: string[], ) => { if (functionNames.length === 0) { return; @@ -566,14 +576,14 @@ const emitMissingTraitWarning = ( radio.emit( "logMessage", yellow( - `\nWarning: The following test functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n` - ) + `\nWarning: The following test functions reference traits without eligible implementations and will be skipped:\n\n${functionList}\n`, + ), ); radio.emit( "logMessage", yellow( - `Note: You can add contracts implementing traits either as project contracts or as requirements.\n` - ) + `Note: You can add contracts implementing traits either as project contracts or as requirements.\n`, + ), ); }; @@ -585,32 +595,32 @@ const emitPropertyRegressionTestHeader = ( targetContractName: string, seed: number, numRuns: number, - timestamp: number + timestamp: number, ) => { radio.emit("logMessage", LOG_DIVIDER); radio.emit( "logMessage", ` Running ${underline( - timestamp + timestamp, )} regression test for the ${targetContractName} contract with: - Seed: ${seed} - Runs: ${numRuns} -` +`, ); }; const filterTestFunctions = ( - allFunctionsMap: Map + allFunctionsMap: Map, ) => new Map( Array.from(allFunctionsMap, ([contractId, functions]) => [ contractId, functions.filter( - (f) => f.access === "public" && f.name.startsWith("test-") + (f) => f.access === "public" && f.name.startsWith("test-"), ), - ]) + ]), ); export const isTestDiscardedInPlace = (testFunctionCallResultJson: any) => @@ -631,15 +641,17 @@ const isTestDiscarded = ( selectedTestFunctionArgs: any[], contractId: string, simnet: Simnet, - selectedCaller: string + selectedCaller: string, ) => { - if (!discardFunctionName) return false; + if (!discardFunctionName) { + return false; + } const { result: discardFunctionCallResult } = simnet.callReadOnlyFn( contractId, discardFunctionName, selectedTestFunctionArgs, - selectedCaller + selectedCaller, ); const jsonDiscardFunctionCallResult = cvToJSON(discardFunctionCallResult); return jsonDiscardFunctionCallResult.value === false; @@ -662,7 +674,7 @@ const validateDiscardFunction = ( testFunctionName: string, testContractsDiscardFunctions: Map, testContractsTestFunctions: Map, - radio: EventEmitter + radio: EventEmitter, ) => { const testFunction = testContractsTestFunctions .get(contractId) @@ -671,16 +683,18 @@ const validateDiscardFunction = ( .get(contractId) ?.find((f) => f.name === discardFunctionName); - if (!testFunction || !discardFunction) return false; + if (!testFunction || !discardFunction) { + return false; + } if (!isParamsMatch(testFunction, discardFunction)) { radio.emit( "logMessage", red( `\nError: Parameter mismatch for discard function "${discardFunctionName}" in contract "${getContractNameFromContractId( - contractId - )}".\n` - ) + contractId, + )}".\n`, + ), ); return false; } @@ -690,9 +704,9 @@ const validateDiscardFunction = ( "logMessage", red( `\nError: Return type must be boolean for discard function "${discardFunctionName}" in contract "${getContractNameFromContractId( - contractId - )}".\n` - ) + contractId, + )}".\n`, + ), ); return false; } @@ -709,13 +723,13 @@ const validateDiscardFunction = ( */ export const isParamsMatch = ( testFunctionInterface: ContractInterfaceFunction, - discardFunctionInterface: ContractInterfaceFunction + discardFunctionInterface: ContractInterfaceFunction, ) => { const sortedTestFunctionArgs = [...testFunctionInterface.args].sort((a, b) => - a.name.localeCompare(b.name) + a.name.localeCompare(b.name), ); const sortedDiscardFunctionArgs = [...discardFunctionInterface.args].sort( - (a, b) => a.name.localeCompare(b.name) + (a, b) => a.name.localeCompare(b.name), ); return ( JSON.stringify(sortedTestFunctionArgs) === @@ -729,7 +743,7 @@ export const isParamsMatch = ( * @returns A boolean indicating if the return type is boolean. */ export const isReturnTypeBoolean = ( - discardFunctionInterface: ContractInterfaceFunction + discardFunctionInterface: ContractInterfaceFunction, ) => discardFunctionInterface.outputs.type === "bool"; export class PropertyTestError extends Error { diff --git a/shared.tests.ts b/shared.tests.ts index 70798aaf..da12b2ba 100644 --- a/shared.tests.ts +++ b/shared.tests.ts @@ -1,14 +1,15 @@ +import { rmSync } from "node:fs"; +import { join, resolve } from "node:path"; + import { initSimnet } from "@stacks/clarinet-sdk"; +import fc from "fast-check"; + import { + getContractNameFromContractId, getFunctionsFromContractInterfaces, getFunctionsListForContract, getSimnetDeployerContractsInterfaces, - hexaString, - getContractNameFromContractId, } from "./shared"; -import { rmSync } from "fs"; -import { join, resolve } from "path"; -import fc from "fast-check"; import { createIsolatedTestEnvironment } from "./test.utils"; const isolatedTestEnvPrefix = "rendezvous-test-shared-"; @@ -18,14 +19,14 @@ describe("Simnet contracts operations", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); const expectedDeployerContracts = new Map( - Array.from(simnet.getContractsInterfaces()).filter( - ([key]) => key.split(".")[0] === simnet.deployer - ) + [...simnet.getContractsInterfaces()].filter( + ([key]) => key.split(".")[0] === simnet.deployer, + ), ); // Exercise @@ -43,25 +44,25 @@ describe("Simnet contracts operations", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); const sutContractsInterfaces = getSimnetDeployerContractsInterfaces(simnet); - const sutContractsList = Array.from(sutContractsInterfaces.keys()); + const sutContractsList = [...sutContractsInterfaces.keys()]; const allFunctionsMap = new Map( Array.from(sutContractsInterfaces, ([contractId, contractInterface]) => [ contractId, contractInterface.functions, - ]) + ]), ); const expectedContractFunctionsList = sutContractsList.map( - (contractId) => allFunctionsMap.get(contractId) || [] + (contractId) => allFunctionsMap.get(contractId) || [], ); // Exercise const actualContractFunctionsList = sutContractsList.map((contractId) => - getFunctionsListForContract(allFunctionsMap, contractId) + getFunctionsListForContract(allFunctionsMap, contractId), ); // Verify @@ -75,7 +76,7 @@ describe("Simnet contracts operations", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const manifestPath = join(tempDir, "Clarinet.toml"); const simnet = await initSimnet(manifestPath); @@ -84,12 +85,12 @@ describe("Simnet contracts operations", () => { Array.from(sutContractsInterfaces, ([contractId, contractInterface]) => [ contractId, contractInterface.functions, - ]) + ]), ); // Exercise const actualAllFunctionsMap = getFunctionsFromContractInterfaces( - sutContractsInterfaces + sutContractsInterfaces, ); // Verify @@ -118,8 +119,8 @@ describe("Contract identifier parsing", () => { // Assert expect(actual).toBe(contractName); - } - ) + }, + ), ); }); }); diff --git a/shared.ts b/shared.ts index 1021afc4..7867fb73 100644 --- a/shared.ts +++ b/shared.ts @@ -1,5 +1,19 @@ -import fc from "fast-check"; +import type { Simnet } from "@stacks/clarinet-sdk"; +import type { + ContractInterfaceFunction, + IContractInterface, +} from "@stacks/clarinet-sdk-wasm"; import { + Cl, + optionalCVOf, + principalCV, + responseErrorCV, + responseOkCV, + type ClarityValue, +} from "@stacks/transactions"; +import fc from "fast-check"; + +import type { BaseTypesToArbitrary, BaseTypesToCV, ComplexTypesToArbitrary, @@ -10,21 +24,8 @@ import { ResponseStatus, TupleData, } from "./shared.types"; -import { Simnet } from "@stacks/clarinet-sdk"; -import { - ContractInterfaceFunction, - IContractInterface, -} from "@stacks/clarinet-sdk-wasm"; -import { - Cl, - ClarityValue, - optionalCVOf, - principalCV, - responseErrorCV, - responseOkCV, -} from "@stacks/transactions"; import { getContractIdsImplementingTrait } from "./traits"; -import { ImplementedTraitType, ImportedTraitType } from "./traits.types"; +import type { ImplementedTraitType, ImportedTraitType } from "./traits.types"; /** 79 characters long divider for logging. */ export const LOG_DIVIDER = @@ -37,12 +38,12 @@ export const LOG_DIVIDER = * @returns The contract IDs mapped to their interfaces. */ export const getSimnetDeployerContractsInterfaces = ( - simnet: Simnet + simnet: Simnet, ): Map => new Map( - Array.from(simnet.getContractsInterfaces()).filter( - ([key]) => key.split(".")[0] === simnet.deployer - ) + [...simnet.getContractsInterfaces()].filter( + ([key]) => key.split(".")[0] === simnet.deployer, + ), ); /** @@ -52,18 +53,18 @@ export const getSimnetDeployerContractsInterfaces = ( * @returns The contract IDs mapped to their function interfaces. */ export const getFunctionsFromContractInterfaces = ( - contractInterfaces: Map + contractInterfaces: Map, ): Map => new Map( Array.from(contractInterfaces, ([contractId, contractInterface]) => [ contractId, contractInterface.functions, - ]) + ]), ); export const getFunctionsListForContract = ( functionsMap: Map, - contractId: string + contractId: string, ) => functionsMap.get(contractId) || []; /** Dynamically generates fast-check arbitraries for a given function @@ -77,14 +78,14 @@ export const getFunctionsListForContract = ( export const functionToArbitrary = ( functionInterface: EnrichedContractInterfaceFunction, addresses: string[], - projectTraitImplementations: Record + projectTraitImplementations: Record, ): fc.Arbitrary[] => functionInterface.args.map((arg) => parameterTypeToArbitrary( arg.type as EnrichedParameterType, addresses, - projectTraitImplementations - ) + projectTraitImplementations, + ), ); /** @@ -98,24 +99,27 @@ export const functionToArbitrary = ( const parameterTypeToArbitrary = ( type: EnrichedParameterType, addresses: string[], - projectTraitImplementations: Record + projectTraitImplementations: Record, ): fc.Arbitrary => { if (typeof type === "string") { // The type is a base type. if (type === "principal") { - if (addresses.length === 0) + if (addresses.length === 0) { throw new Error( - "No addresses could be retrieved from the simnet instance!" + "No addresses could be retrieved from the simnet instance!", ); + } return baseTypesToArbitrary.principal(addresses); - } else return baseTypesToArbitrary[type]; + } else { + return baseTypesToArbitrary[type]; + } } else { // The type is a complex type. if ("buffer" in type) { return complexTypesToArbitrary["buffer"](type.buffer.length); } else if ("string-ascii" in type) { return complexTypesToArbitrary["string-ascii"]( - type["string-ascii"].length + type["string-ascii"].length, ); } else if ("string-utf8" in type) { return complexTypesToArbitrary["string-utf8"](type["string-utf8"].length); @@ -124,31 +128,31 @@ const parameterTypeToArbitrary = ( type.list.type, type.list.length, addresses, - projectTraitImplementations + projectTraitImplementations, ); } else if ("tuple" in type) { return complexTypesToArbitrary["tuple"]( type.tuple, addresses, - projectTraitImplementations + projectTraitImplementations, ); } else if ("optional" in type) { return complexTypesToArbitrary["optional"]( type.optional, addresses, - projectTraitImplementations + projectTraitImplementations, ); } else if ("response" in type) { return complexTypesToArbitrary.response( type.response.ok, type.response.error, addresses, - projectTraitImplementations + projectTraitImplementations, ); } else if ("trait_reference" in type) { return complexTypesToArbitrary.trait_reference( type.trait_reference, - projectTraitImplementations + projectTraitImplementations, ); } else { throw new Error(`Unsupported complex type: ${JSON.stringify(type)}`); @@ -186,25 +190,25 @@ const complexTypesToArbitrary: ComplexTypesToArbitrary = { type: EnrichedParameterType, length: number, addresses: string[], - projectTraitImplementations: Record + projectTraitImplementations: Record, ) => fc.array( parameterTypeToArbitrary(type, addresses, projectTraitImplementations), { maxLength: length, - } + }, ), tuple: ( items: { name: string; type: EnrichedParameterType }[], addresses: string[], - projectTraitImplementations: Record + projectTraitImplementations: Record, ) => { - const tupleArbitraries: { [key: string]: fc.Arbitrary } = {}; + const tupleArbitraries: Record> = {}; items.forEach((item) => { tupleArbitraries[item.name] = parameterTypeToArbitrary( item.type, addresses, - projectTraitImplementations + projectTraitImplementations, ); }); return fc.record(tupleArbitraries); @@ -212,16 +216,16 @@ const complexTypesToArbitrary: ComplexTypesToArbitrary = { optional: ( type: EnrichedParameterType, addresses: string[], - projectTraitImplementations: Record + projectTraitImplementations: Record, ) => fc.option( - parameterTypeToArbitrary(type, addresses, projectTraitImplementations) + parameterTypeToArbitrary(type, addresses, projectTraitImplementations), ), response: ( okType: EnrichedParameterType, errType: EnrichedParameterType, addresses: string[], - projectTraitImplementations: Record + projectTraitImplementations: Record, ) => fc.oneof( fc.record({ @@ -229,7 +233,7 @@ const complexTypesToArbitrary: ComplexTypesToArbitrary = { value: parameterTypeToArbitrary( okType, addresses, - projectTraitImplementations + projectTraitImplementations, ), }), fc.record({ @@ -237,18 +241,20 @@ const complexTypesToArbitrary: ComplexTypesToArbitrary = { value: parameterTypeToArbitrary( errType, addresses, - projectTraitImplementations + projectTraitImplementations, ), - }) + }), ), trait_reference: ( traitData: ImportedTraitType, - projectTraitImplementations: Record - ) => { - return fc.constantFrom( - ...getContractIdsImplementingTrait(traitData, projectTraitImplementations) - ); - }, + projectTraitImplementations: Record, + ) => + fc.constantFrom( + ...getContractIdsImplementingTrait( + traitData, + projectTraitImplementations, + ), + ), }; /** @@ -264,7 +270,7 @@ const complexTypesToArbitrary: ComplexTypesToArbitrary = { * https://github.com/dubzzz/fast-check/commit/3f4f1203a8863c07d22b45591bf0de1fac02b948 */ export const hexaString = ( - constraints: fc.StringConstraints = {} + constraints: fc.StringConstraints = {}, ): fc.Arbitrary => { const hexa = (): fc.Arbitrary => { const hexCharSet = "0123456789abcdef"; @@ -286,10 +292,10 @@ const charSet = */ export const argsToCV = ( functionInterface: EnrichedContractInterfaceFunction, - generatedArguments: any[] + generatedArguments: any[], ) => functionInterface.args.map((arg, i) => - argToCV(generatedArguments[i], arg.type as EnrichedParameterType) + argToCV(generatedArguments[i], arg.type as EnrichedParameterType), ); /** @@ -300,21 +306,26 @@ export const argsToCV = ( */ const argToCV = ( generatedArgument: any, - type: EnrichedParameterType + type: EnrichedParameterType, ): ClarityValue => { if (isBaseType(type)) { // Base type. switch (type) { - case "int128": + case "int128": { return baseTypesToCV.int128(generatedArgument as number); - case "uint128": + } + case "uint128": { return baseTypesToCV.uint128(generatedArgument as number); - case "bool": + } + case "bool": { return baseTypesToCV.bool(generatedArgument as boolean); - case "principal": + } + case "principal": { return baseTypesToCV.principal(generatedArgument as string); - default: + } + default: { throw new Error(`Unsupported base parameter type: ${type}`); + } } } else { // Complex type. @@ -326,15 +337,15 @@ const argToCV = ( return complexTypesToCV["string-utf8"](generatedArgument); } else if ("list" in type) { const listItems = generatedArgument.map((item: any) => - argToCV(item, type.list.type) + argToCV(item, type.list.type), ); return complexTypesToCV.list(listItems); } else if ("tuple" in type) { - const tupleData: { [key: string]: ClarityValue } = {}; + const tupleData: Record = {}; type.tuple.forEach((field) => { tupleData[field.name] = argToCV( generatedArgument[field.name], - field.type + field.type, ); }); return complexTypesToCV.tuple(tupleData); @@ -342,7 +353,7 @@ const argToCV = ( return optionalCVOf( generatedArgument ? argToCV(generatedArgument, type.optional) - : undefined + : undefined, ); } else if ("response" in type) { const status = generatedArgument.status as ResponseStatus; @@ -353,7 +364,7 @@ const argToCV = ( return complexTypesToCV.trait_reference(generatedArgument); } else { throw new Error( - `Unsupported complex parameter type: ${JSON.stringify(type)}` + `Unsupported complex parameter type: ${JSON.stringify(type)}`, ); } } @@ -376,28 +387,25 @@ const complexTypesToCV: ComplexTypesToCV = { buffer: (arg: string) => Cl.bufferFromHex(arg), "string-ascii": (arg: string) => Cl.stringAscii(arg), "string-utf8": (arg: string) => Cl.stringUtf8(arg), - list: (items: ClarityValue[]) => { - return Cl.list(items); - }, - tuple: (tupleData: TupleData) => { - return Cl.tuple(tupleData); - }, + list: (items: ClarityValue[]) => Cl.list(items), + tuple: (tupleData: TupleData) => Cl.tuple(tupleData), optional: (arg: ClarityValue | null) => arg ? optionalCVOf(arg) : optionalCVOf(undefined), response: (status: ResponseStatus, value: ClarityValue) => { - if (status === "ok") return responseOkCV(value); - else if (status === "error") return responseErrorCV(value); - else throw new Error(`Unsupported response status: ${status}`); + if (status === "ok") { + return responseOkCV(value); + } else if (status === "error") { + return responseErrorCV(value); + } else { + throw new Error(`Unsupported response status: ${status}`); + } }, trait_reference: (traitImplementation: string) => principalCV(traitImplementation), }; -const isBaseType = (type: EnrichedParameterType): type is EnrichedBaseType => { - return ["int128", "uint128", "bool", "principal"].includes( - type as EnrichedBaseType - ); -}; +const isBaseType = (type: EnrichedParameterType): type is EnrichedBaseType => + ["int128", "uint128", "bool", "principal"].includes(type as EnrichedBaseType); export const getContractNameFromContractId = (contractId: string): string => contractId.split(".")[1]; diff --git a/shared.types.ts b/shared.types.ts index a59edc57..28fb992d 100644 --- a/shared.types.ts +++ b/shared.types.ts @@ -1,9 +1,9 @@ -import { +import type { ContractInterfaceFunctionAccess, ContractInterfaceFunctionArg, ContractInterfaceFunctionOutput, } from "@stacks/clarinet-sdk-wasm"; -import { +import type { boolCV, bufferCV, ClarityValue, @@ -18,43 +18,45 @@ import { tupleCV, uintCV, } from "@stacks/transactions"; -import fc from "fast-check"; -import { ImplementedTraitType, ImportedTraitType } from "./traits.types"; +import type fc from "fast-check"; + +import type { ImplementedTraitType, ImportedTraitType } from "./traits.types"; // Types used for Clarity Value conversion. -type ImportedTraitReferenceFunctionArg = { +interface ImportedTraitReferenceFunctionArg { type: { trait_reference: ImportedTraitType; }; name: string; -}; +} /** * The type of the function interface, after the contract interface is * "enriched" with additional information about trait references. */ -export type EnrichedContractInterfaceFunction = { +export interface EnrichedContractInterfaceFunction { args: (ContractInterfaceFunctionArg | ImportedTraitReferenceFunctionArg)[]; name: string; access: ContractInterfaceFunctionAccess; outputs: ContractInterfaceFunctionOutput; -}; +} export type ResponseStatus = "ok" | "error"; -export type TupleData = { - [key: string]: T; -}; +export type TupleData = Record< + string, + T +>; -export type BaseTypesToCV = { +export interface BaseTypesToCV { int128: (arg: number) => ReturnType; uint128: (arg: number) => ReturnType; bool: (arg: boolean) => ReturnType; principal: (arg: string) => ReturnType; -}; +} -export type ComplexTypesToCV = { +export interface ComplexTypesToCV { buffer: (arg: string) => ReturnType; "string-ascii": (arg: string) => ReturnType; "string-utf8": (arg: string) => ReturnType; @@ -63,10 +65,10 @@ export type ComplexTypesToCV = { optional: (arg: ClarityValue | null) => ReturnType; response: ( status: ResponseStatus, - value: ClarityValue + value: ClarityValue, ) => ReturnType; trait_reference: (trait: string) => ReturnType; -}; +} // Types used for argument generation. @@ -116,14 +118,14 @@ export type ParameterType = BaseType | ComplexType; /** The Clarity parameter types after the contract interface is "enriched". */ export type EnrichedParameterType = EnrichedBaseType | EnrichedComplexType; -export type BaseTypesToArbitrary = { +export interface BaseTypesToArbitrary { int128: ReturnType; uint128: ReturnType; bool: ReturnType; principal: (addresses: string[]) => ReturnType; -}; +} -export type ComplexTypesToArbitrary = { +export interface ComplexTypesToArbitrary { buffer: (length: number) => fc.Arbitrary; "string-ascii": (length: number) => fc.Arbitrary; "string-utf8": (length: number) => fc.Arbitrary; @@ -131,26 +133,26 @@ export type ComplexTypesToArbitrary = { type: EnrichedParameterType, length: number, addresses: string[], - projectTraitImplementations: Record + projectTraitImplementations: Record, ) => fc.Arbitrary; tuple: ( items: { name: string; type: EnrichedParameterType }[], addresses: string[], - projectTraitImplementations: Record + projectTraitImplementations: Record, ) => fc.Arbitrary; optional: ( type: EnrichedParameterType, addresses: string[], - projectTraitImplementations: Record + projectTraitImplementations: Record, ) => fc.Arbitrary; response: ( okType: EnrichedParameterType, errType: EnrichedParameterType, addresses: string[], - projectTraitImplementations: Record + projectTraitImplementations: Record, ) => fc.Arbitrary; trait_reference: ( traitData: ImportedTraitType, - projectTraitImplementations: Record + projectTraitImplementations: Record, ) => fc.Arbitrary; -}; +} diff --git a/test.utils.ts b/test.utils.ts index ed41ef5d..fcb7dffa 100644 --- a/test.utils.ts +++ b/test.utils.ts @@ -1,6 +1,6 @@ -import { join } from "path"; -import { mkdtempSync, cpSync } from "fs"; -import { tmpdir } from "os"; +import { cpSync, mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; /** * Creates an isolated test environment by copying the Clarinet project to a @@ -12,11 +12,11 @@ import { tmpdir } from "os"; * @returns The path to the temporary directory containing the isolated project * copy. */ -export function createIsolatedTestEnvironment( +export const createIsolatedTestEnvironment = ( manifestDir: string, - testPrefix: string -): string { + testPrefix: string, +): string => { const tempDir = mkdtempSync(join(tmpdir(), testPrefix)); cpSync(manifestDir, tempDir, { recursive: true }); return tempDir; -} +}; diff --git a/traits.tests.ts b/traits.tests.ts index 2a5911e4..ee2bfa59 100644 --- a/traits.tests.ts +++ b/traits.tests.ts @@ -1,10 +1,14 @@ -import { +import { rmSync } from "node:fs"; +import { join, resolve } from "node:path"; + +import { initSimnet } from "@stacks/clarinet-sdk"; +import type { ContractInterfaceFunction, IContractAST, } from "@stacks/clarinet-sdk-wasm"; -import { initSimnet } from "@stacks/clarinet-sdk"; -import { rmSync } from "fs"; -import { join, resolve } from "path"; + +import type { EnrichedContractInterfaceFunction } from "./shared.types"; +import { createIsolatedTestEnvironment } from "./test.utils"; import { buildTraitReferenceMap, enrichInterfaceWithTraitData, @@ -13,9 +17,7 @@ import { getNonTestableTraitFunctions, isTraitReferenceFunction, } from "./traits"; -import { createIsolatedTestEnvironment } from "./test.utils"; -import { EnrichedContractInterfaceFunction } from "./shared.types"; -import { ImplementedTraitType } from "./traits.types"; +import type { ImplementedTraitType } from "./traits.types"; const isolatedTestEnvPrefix = "rendezvous-test-traits-"; @@ -28,7 +30,7 @@ describe("Trait reference processing", () => { const expected = new Map( Object.entries({ "test-trait": { token: "trait_reference" }, - }) + }), ); // Act @@ -46,7 +48,7 @@ describe("Trait reference processing", () => { const expected = new Map( Object.entries({ "test-trait": { token: "trait_reference" }, - }) + }), ); // Act @@ -66,7 +68,7 @@ describe("Trait reference processing", () => { Object.entries({ "test-trait": { token: "trait_reference" }, "invariant-trait": { token: "trait_reference" }, - }) + }), ); // Act @@ -84,7 +86,7 @@ describe("Trait reference processing", () => { const expected = new Map( Object.entries({ "test-trait": { token: "trait_reference" }, - }) + }), ); // Act @@ -102,7 +104,7 @@ describe("Trait reference processing", () => { const expected = new Map( Object.entries({ "test-trait": { e: "trait_reference" }, - }) + }), ); // Act @@ -122,7 +124,7 @@ describe("Trait reference processing", () => { "test-trait": { "tuple-param": { tuple: { token: "trait_reference" } }, }, - }) + }), ); // Act @@ -142,7 +144,7 @@ describe("Trait reference processing", () => { "test-trait": { "token-list": { list: "trait_reference" }, }, - }) + }), ); // Act @@ -162,7 +164,7 @@ describe("Trait reference processing", () => { "test-trait": { "resp-trait-param": { response: { ok: "trait_reference" } }, }, - }) + }), ); // Act @@ -182,7 +184,7 @@ describe("Trait reference processing", () => { "test-trait": { "resp-trait-param": { response: { error: "trait_reference" } }, }, - }) + }), ); // Act @@ -204,7 +206,7 @@ describe("Trait reference processing", () => { response: { ok: "trait_reference", error: "trait_reference" }, }, }, - }) + }), ); // Act @@ -226,7 +228,7 @@ describe("Trait reference processing", () => { optional: "trait_reference", }, }, - }) + }), ); // Act @@ -248,7 +250,7 @@ describe("Trait reference processing", () => { list: { tuple: { "mad-inner": "trait_reference" } }, }, }, - }) + }), ); // Act @@ -276,7 +278,7 @@ describe("Trait reference processing", () => { }, }, }, - }) + }), ); // Act @@ -318,7 +320,7 @@ describe("Trait reference processing", () => { }, }, }, - }) + }), ); // Act @@ -358,7 +360,7 @@ describe("Trait reference processing", () => { }, }, }, - }) + }), ); // Act @@ -449,7 +451,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -457,7 +459,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -545,7 +547,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -553,7 +555,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -673,7 +675,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -681,7 +683,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -769,7 +771,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -777,7 +779,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -875,7 +877,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -883,7 +885,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -977,7 +979,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -985,7 +987,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -1097,7 +1099,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -1105,7 +1107,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -1197,7 +1199,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -1205,7 +1207,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -1297,7 +1299,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -1305,7 +1307,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -1397,7 +1399,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -1405,7 +1407,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -1517,7 +1519,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -1525,7 +1527,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -1614,7 +1616,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -1622,7 +1624,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -1709,7 +1711,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -1717,7 +1719,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -1863,7 +1865,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -1871,7 +1873,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -2055,7 +2057,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -2063,7 +2065,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -2243,7 +2245,7 @@ describe("Trait reference processing", () => { }, }, ], - }) + }), ); // Act @@ -2251,7 +2253,7 @@ describe("Trait reference processing", () => { ast, traitReferenceMap, allFunctionsInterfaces, - targetContractId + targetContractId, ); // Assert @@ -2262,7 +2264,7 @@ describe("Trait reference processing", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const simnet = await initSimnet(join(tempDir, "Clarinet.toml")); @@ -2295,7 +2297,7 @@ describe("Trait reference processing", () => { // Exercise const actual = new Set( - getContractIdsImplementingTrait(traitData, projectTraitImplementations) + getContractIdsImplementingTrait(traitData, projectTraitImplementations), ); // Verify @@ -2309,7 +2311,7 @@ describe("Trait reference processing", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const simnet = await initSimnet(join(tempDir, "Clarinet.toml")); @@ -2338,7 +2340,7 @@ describe("Trait reference processing", () => { // Exercise const actual = getContractIdsImplementingTrait( traitImplementationData, - projectTraitImplementations + projectTraitImplementations, ); // Verify @@ -2352,7 +2354,7 @@ describe("Trait reference processing", () => { // Setup const tempDir = createIsolatedTestEnvironment( resolve(__dirname, "example"), - isolatedTestEnvPrefix + isolatedTestEnvPrefix, ); const simnet = await initSimnet(join(tempDir, "Clarinet.toml")); @@ -2385,7 +2387,7 @@ describe("Trait reference processing", () => { // Exercise const actual = new Set( - getContractIdsImplementingTrait(traitData, projectTraitImplementations) + getContractIdsImplementingTrait(traitData, projectTraitImplementations), ); // Verify @@ -2855,7 +2857,7 @@ describe("Non-testable trait function filtering", () => { enrichedFunctionsInterfaces, traitReferenceMap, projectTraitImplementations, - contractId + contractId, ); // Assert diff --git a/traits.ts b/traits.ts index f51c98f0..07b54eec 100644 --- a/traits.ts +++ b/traits.ts @@ -1,16 +1,17 @@ -import { +import type { Simnet } from "@stacks/clarinet-sdk"; +import type { Atom, ContractInterfaceFunction, IContractAST, List, TraitReference, } from "@stacks/clarinet-sdk-wasm"; -import { + +import type { EnrichedContractInterfaceFunction, ParameterType, } from "./shared.types"; -import { Simnet } from "@stacks/clarinet-sdk"; -import { +import type { DefinedTraitType, ImplementedTraitType, ImportedTraitType, @@ -32,34 +33,34 @@ export const enrichInterfaceWithTraitData = ( ast: IContractAST, traitReferenceMap: Map, functionInterfaceList: ContractInterfaceFunction[], - targetContractId: string + targetContractId: string, ): Map => { const enriched = new Map(); const enrichArgs = ( args: any[], functionName: string, - traitReferenceMap: any, - path: string[] = [] - ): any[] => { - return args.map((arg) => { + traitRefMapNode: any, + path: string[] = [], + ): any[] => + args.map((arg) => { const listNested = !arg.name; const currentPath = listNested ? path : [...path, arg.name]; // Exit early if the traitReferenceMap does not have anything we are // looking for. It means that the current parameter does not have an // associated trait reference. if ( - !traitReferenceMap || - (!traitReferenceMap[arg.name] && - !traitReferenceMap.tuple && - !traitReferenceMap.list && - !traitReferenceMap.response && - !traitReferenceMap.optional && - !traitReferenceMap[arg.name]?.tuple && - !traitReferenceMap[arg.name]?.list && - !traitReferenceMap[arg.name]?.response && - !traitReferenceMap[arg.name]?.optional && - traitReferenceMap !== "trait_reference") + !traitRefMapNode || + (!traitRefMapNode[arg.name] && + !traitRefMapNode.tuple && + !traitRefMapNode.list && + !traitRefMapNode.response && + !traitRefMapNode.optional && + !traitRefMapNode[arg.name]?.tuple && + !traitRefMapNode[arg.name]?.list && + !traitRefMapNode[arg.name]?.response && + !traitRefMapNode[arg.name]?.optional && + traitRefMapNode !== "trait_reference") ) { return arg; } @@ -71,9 +72,9 @@ export const enrichInterfaceWithTraitData = ( arg.type.tuple, functionName, listNested - ? traitReferenceMap.tuple - : traitReferenceMap[arg.name]?.tuple, - currentPath + ? traitRefMapNode.tuple + : traitRefMapNode[arg.name]?.tuple, + currentPath, ), }, }; @@ -85,15 +86,15 @@ export const enrichInterfaceWithTraitData = ( [arg.type.list], functionName, listNested - ? traitReferenceMap.list - : traitReferenceMap[arg.name]?.list, + ? traitRefMapNode.list + : traitRefMapNode[arg.name]?.list, arg.type.list.type.tuple ? [...currentPath, "tuple"] : arg.type.list.type.response - ? [...currentPath, "response"] - : arg.type.list.type.optional - ? [...currentPath, "optional"] - : [...currentPath, "list"] + ? [...currentPath, "response"] + : arg.type.list.type.optional + ? [...currentPath, "optional"] + : [...currentPath, "list"], )[0], }, }; @@ -106,20 +107,20 @@ export const enrichInterfaceWithTraitData = ( functionName, { ok: listNested - ? traitReferenceMap.response?.ok - : traitReferenceMap[arg.name]?.response?.ok, + ? traitRefMapNode.response?.ok + : traitRefMapNode[arg.name]?.response?.ok, }, - okPath + okPath, )[0]; const errorTraitReference = enrichArgs( [{ name: "error", type: arg.type.response.error }], functionName, { error: listNested - ? traitReferenceMap.response?.error - : traitReferenceMap[arg.name]?.response?.error, + ? traitRefMapNode.response?.error + : traitRefMapNode[arg.name]?.response?.error, }, - errorPath + errorPath, )[0]; return { ...arg, @@ -137,10 +138,10 @@ export const enrichInterfaceWithTraitData = ( functionName, { optional: listNested - ? traitReferenceMap.optional - : traitReferenceMap[arg.name]?.optional, + ? traitRefMapNode.optional + : traitRefMapNode[arg.name]?.optional, }, - optionalPath + optionalPath, )[0]; return { ...arg, @@ -148,12 +149,12 @@ export const enrichInterfaceWithTraitData = ( optional: optionalTraitReference.type, }, }; - } else if (traitReferenceMap && traitReferenceMap[arg.name]) { + } else if (traitRefMapNode && traitRefMapNode[arg.name]) { const [traitReferenceName, traitReferenceImport] = getTraitReferenceData( ast, functionName, - currentPath.filter((x) => x !== undefined) + currentPath.filter((x) => x !== undefined), ); if (traitReferenceName && traitReferenceImport) { return { @@ -166,7 +167,7 @@ export const enrichInterfaceWithTraitData = ( }, }; } - } else if (traitReferenceMap === "trait_reference") { + } else if (traitRefMapNode === "trait_reference") { const [traitReferenceName, traitReferenceImport] = getTraitReferenceData(ast, functionName, path); if (traitReferenceName && traitReferenceImport) { @@ -184,14 +185,11 @@ export const enrichInterfaceWithTraitData = ( return arg; }); - }; - const enrichedFunctions = functionInterfaceList.map((f) => { - return { - ...f, - args: enrichArgs(f.args, f.name, traitReferenceMap.get(f.name)), - }; - }); + const enrichedFunctions = functionInterfaceList.map((f) => ({ + ...f, + args: enrichArgs(f.args, f.name, traitReferenceMap.get(f.name)), + })); enriched.set(targetContractId, enrichedFunctions); return enriched; @@ -213,7 +211,7 @@ export const enrichInterfaceWithTraitData = ( const getTraitReferenceData = ( ast: IContractAST, functionName: string, - parameterPath: string[] + parameterPath: string[], ): [string, ImportedTraitType] | [undefined, undefined] => { /** * Recursively searches for a trait reference import details in the contract @@ -229,7 +227,7 @@ const getTraitReferenceData = ( */ const findTraitReference = ( functionParameterNodes: any[], - path: string[] + path: string[], ): [string, ImportedTraitType] | [undefined, undefined] => { for (const parameterNode of functionParameterNodes) { // Check if the current parameter node is a trait reference in the first @@ -293,10 +291,12 @@ const getTraitReferenceData = ( // parameter list. const result = findTraitReference( (nestedParameterList as List).List, - path.slice(1) + path.slice(1), ); - if (result[0] !== undefined) return result; + if (result[0] !== undefined) { + return result; + } } } } @@ -322,7 +322,7 @@ const getTraitReferenceData = ( if ( !potentialFunctionDefinitionAtom || !["define-public", "define-read-only"].includes( - (potentialFunctionDefinitionAtom.expr as Atom).Atom.toString() + (potentialFunctionDefinitionAtom.expr as Atom).Atom.toString(), ) ) { continue; @@ -361,11 +361,12 @@ const getTraitReferenceData = ( const traitReferenceImportData = findTraitReference( functionParameterNodes, - parameterPath + parameterPath, ); - if (traitReferenceImportData[0] !== undefined) + if (traitReferenceImportData[0] !== undefined) { return traitReferenceImportData; + } } return [undefined, undefined]; }; @@ -378,7 +379,7 @@ const getTraitReferenceData = ( * @returns The function names mapped to their trait reference parameter paths. */ export const buildTraitReferenceMap = ( - functionInterfaces: ContractInterfaceFunction[] + functionInterfaces: ContractInterfaceFunction[], ): Map => { const traitReferenceMap = new Map(); @@ -449,7 +450,7 @@ export const buildTraitReferenceMap = ( */ export const getContractIdsImplementingTrait = ( trait: ImportedTraitType | DefinedTraitType, - projectTraitImplementations: Record + projectTraitImplementations: Record, ): string[] => { const contracts = Object.keys(projectTraitImplementations); @@ -466,16 +467,16 @@ export const getContractIdsImplementingTrait = ( JSON.stringify(implementedTrait.contract_identifier.issuer) === JSON.stringify( (trait as ImportedTraitType).import.Imported?.contract_identifier - .issuer + .issuer, ) || JSON.stringify(implementedTrait.contract_identifier.issuer) === JSON.stringify( (trait as DefinedTraitType).import.Defined?.contract_identifier - .issuer + .issuer, ); return isTraitNamesMatch && isTraitIssuersMatch; - } + }, ); return traitImplemented; }); @@ -490,7 +491,7 @@ export const getContractIdsImplementingTrait = ( * otherwise. */ export const isTraitReferenceFunction = ( - fn: ContractInterfaceFunction + fn: ContractInterfaceFunction, ): boolean => { const hasTraitReference = (type: ParameterType): boolean => { if (typeof type === "string") { @@ -498,22 +499,32 @@ export const isTraitReferenceFunction = ( return type === "trait_reference"; } else { // The type is a complex type. - if ("buffer" in type) return false; - if ("string-ascii" in type) return false; - if ("string-utf8" in type) return false; - if ("list" in type) + if ("buffer" in type) { + return false; + } + if ("string-ascii" in type) { + return false; + } + if ("string-utf8" in type) { + return false; + } + if ("list" in type) { return hasTraitReference(type.list.type as ParameterType); - if ("tuple" in type) + } + if ("tuple" in type) { return type.tuple.some((item) => - hasTraitReference(item.type as ParameterType) + hasTraitReference(item.type as ParameterType), ); - if ("optional" in type) + } + if ("optional" in type) { return hasTraitReference(type.optional as ParameterType); - if ("response" in type) + } + if ("response" in type) { return ( hasTraitReference(type.response.ok as ParameterType) || hasTraitReference(type.response.error as ParameterType) ); + } // Default to false for unexpected types. return false; } @@ -570,15 +581,17 @@ export const getNonTestableTraitFunctions = ( enrichedFunctionsInterfaces: Map, traitReferenceMap: Map, projectTraitImplementations: Record, - contractId: string + contractId: string, ): string[] => { const hasTraitReferenceWithoutImplementation = (type: any): boolean => { - if (!type) return false; + if (!type) { + return false; + } if (typeof type === "object" && "trait_reference" in type) { const contractIdsImplementingTrait = getContractIdsImplementingTrait( type.trait_reference as ImportedTraitType | DefinedTraitType, - projectTraitImplementations + projectTraitImplementations, ); return contractIdsImplementingTrait.length === 0; } @@ -589,7 +602,7 @@ export const getNonTestableTraitFunctions = ( hasTraitReferenceWithoutImplementation(type.list.type)) || ("tuple" in type && type.tuple.some((item: any) => - hasTraitReferenceWithoutImplementation(item.type) + hasTraitReferenceWithoutImplementation(item.type), )) || ("optional" in type && hasTraitReferenceWithoutImplementation(type.optional)) || @@ -602,14 +615,14 @@ export const getNonTestableTraitFunctions = ( return false; }; - return Array.from(traitReferenceMap.keys()).filter((functionName) => { + return [...traitReferenceMap.keys()].filter((functionName) => { const enrichedFunctionInterface = enrichedFunctionsInterfaces .get(contractId) ?.find((f) => f.name === functionName); return ( enrichedFunctionInterface?.args.some((param) => - hasTraitReferenceWithoutImplementation(param.type) + hasTraitReferenceWithoutImplementation(param.type), ) ?? false ); }); diff --git a/traits.types.ts b/traits.types.ts index d44f3296..f93b7f76 100644 --- a/traits.types.ts +++ b/traits.types.ts @@ -2,36 +2,36 @@ * The trait reference structure, as it appears in the AST under the * `implemented_traits` field. */ -export type ImplementedTraitType = { +export interface ImplementedTraitType { name: string; contract_identifier: { issuer: number[]; name: string }; -}; +} /** * The imported trait reference structure, as it appears in the AST under a * `TraitReference` node. Used to represent the `trait_reference` data for an * imported trait `(use-trait .)`. */ -export type ImportedTraitType = { +export interface ImportedTraitType { name: string; import: { Imported: TraitData; }; -}; +} /** * The defined trait reference structure, as it appears in the AST under a * `TraitReference` node. Used to represent the `trait_reference` data for a * defined trait `(define-trait ())`. */ -export type DefinedTraitType = { +export interface DefinedTraitType { name: string; import: { Defined: TraitData; }; -}; +} -type TraitData = { +interface TraitData { name: string; - contract_identifier: { issuer: Array; name: string }; -}; + contract_identifier: { issuer: any[]; name: string }; +}