Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests in Clarity #4100

Closed
wants to merge 12 commits into from
4 changes: 3 additions & 1 deletion contrib/core-contract-tests/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ npm-debug.log*
coverage
*.info
costs-reports.json
node_modules
node_modules
*.log.txt
history.txt
8 changes: 8 additions & 0 deletions contrib/core-contract-tests/Clarinet.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,11 @@ costs_version = 1
[contracts.bns]
path = "../../stackslib/src/chainstate/stacks/boot/bns.clar"
depends_on = []

[contracts.bns_test]
path = "contracts/bns-tests/bns_test.clar"
depends_on = []

[contracts.bns_flow_test]
path = "contracts/bns-tests/bns_flow_test.clar"
depends_on = []
19 changes: 19 additions & 0 deletions contrib/core-contract-tests/contracts/bns-tests/bns_flow_test.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
(define-constant ERR_NAMESPACE_NOT_FOUND 1005)

;; @name: test delegation to wallet_2, stacking and revoking
(define-public (test-name-registration)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a future PR I think it's doable (and a bit challenging too) to enable tests with parameters, to run those tests also as fuzz tests:

(define-public (test-name-registration (hashed-salted-fqn (buff 20))
                                       (stx-to-burn uint)
                                       (namespace (buff 20))
                                       (name (buff 48))
                                       (salt (buff 20))
                                       (zonefile-hash (buff 20)))

The tricky part might be the extra work around regexes, though the heavily lifting part around fast-check shouldn't be an issue.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can specify parameter testing / fuzzing with annotations. The regexes originally came from a concept implementation to enable us to write Clarity tests in Clarity. After that, they were good enough so we just left it. If we want to make it more robust we will need to switch to a LISP interpreter, should not be too hard.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can sort this part out, I can integrate this with fast-check and we'll have it generate/fuzz everything for us. What's very nice about this is the shrinking part; if there's a bug/edge-case detected, fast-check will give us the minimum counterexample and a seed to reproduce the failure (if the bug/edge-case is detected when running on CI).

(begin
;; @caller wallet_1
(unwrap! (contract-call? .bns name-preorder 0x0123456789abcdef01230123456789abcdef0123 u1000000) (err "name-preorder by wallet 1 should succeed"))
;; @caller wallet_2
(unwrap! (contract-call? .bns name-preorder 0x30123456789abcdef01230123456789abcdef012 u1000000) (err "name-preorder by wallet 2 should succeed"))

;; @mine-blocks-before 100
;; @caller wallet_1
(try! (register))
(ok true)))

(define-public (register)
(let ((result (contract-call? .bns name-register 0x123456 0x123456 0x123456 0x)))
(asserts! (is-eq result (err ERR_NAMESPACE_NOT_FOUND)) (err "name-register should fail"))
(ok true)))
10 changes: 10 additions & 0 deletions contrib/core-contract-tests/contracts/bns-tests/bns_test.clar
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
;; @name: test preorder and publish with invalid names
;; @caller: wallet_1
(define-constant ERR_NAMESPACE_NOT_FOUND 1005)

(define-public (test-name-registration)
(begin
(unwrap! (contract-call? .bns name-preorder 0x0123456789abcdef01230123456789abcdef0123 u1000000) (err "preorder should succeeed"))
(let ((result (contract-call? .bns name-register 0x123456 0x123456 0x123456 0x)))
(asserts! (is-eq result (err ERR_NAMESPACE_NOT_FOUND)) (err "registration should fail"))
(ok true))))
31 changes: 31 additions & 0 deletions contrib/core-contract-tests/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions contrib/core-contract-tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@hirosystems/clarinet-sdk": "^1.1.0",
"@stacks/transactions": "^6.9.0",
"chokidar-cli": "^3.0.0",
"path": "^0.12.7",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vitest": "^0.34.4",
Expand Down
138 changes: 138 additions & 0 deletions contrib/core-contract-tests/tests/clar/clar-flow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { ParsedTransactionResult, tx } from "@hirosystems/clarinet-sdk";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, do you think you can add some method-level comments for each of these functions in this file and the clar.test.ts file? I'm having a hard time following how these functions fit together. Thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parser evolved over time during our work on sBTC Mini. We just discussed in the Clarity WG to clean it up and perhaps introduce a more robust parser. The current files have served our needs and were recently edited to support clarinet-sdk.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments have been added

import { describe, it } from "vitest";
import {
CallInfo,
FunctionAnnotations,
FunctionBody,
extractTestAnnotationsAndCalls,
} from "./utils/clarity-parser";
import { expectOk, isValidTestFunction } from "./utils/test-helpers";
import path from "path";
import * as fs from "fs";

function isTestContract(contractName: string) {
return contractName.substring(contractName.length - 10) === "_flow_test";
}

const accounts = simnet.getAccounts();
clearLogFile();

simnet.getContractsInterfaces().forEach((contract, contractFQN) => {
if (!isTestContract(contractFQN)) {
return;
}

describe(contractFQN, () => {
const hasDefaultPrepareFunction =
contract.functions.findIndex((f) => f.name === "prepare") >= 0;

contract.functions.forEach((functionCall) => {
if (!isValidTestFunction(functionCall)) {
return;
}

const functionName = functionCall.name;
const source = simnet.getContractSource(contractFQN)!;
const [annotations, functionBodies] =
extractTestAnnotationsAndCalls(source);
const functionAnnotations: FunctionAnnotations =
annotations[functionName] || {};
const testname = `${functionCall.name}${
functionAnnotations.name ? `: ${functionAnnotations.name}` : ""
}`;
it(testname, () => {
writeToLogFile(`\n\n${testname}\n\n`);
if (hasDefaultPrepareFunction && !functionAnnotations.prepare)
functionAnnotations.prepare = "prepare";
if (functionAnnotations["no-prepare"])
delete functionAnnotations.prepare;

const functionBody = functionBodies[functionName] || [];

mineBlocksFromFunctionBody(contractFQN, functionName, functionBody);
});
});
});
});
function mineBlocksFromFunctionBody(
contractFQN: string,
testFunctionName: string,
calls: FunctionBody
) {
let blockStarted = false;
let txs: any[] = [];
let block: ParsedTransactionResult[] = [];

for (const { callAnnotations, callInfo } of calls) {
// mine empty blocks
const mineBlocksBefore =
parseInt(callAnnotations["mine-blocks-before"] as string) || 0;
const caller = accounts.get(
(callAnnotations["caller"] as string) || "deployer"
)!;

if (mineBlocksBefore >= 1) {
if (blockStarted) {
writeToLogFile(txs);
block = simnet.mineBlock(txs);
for (let index = 0; index < txs.length; index++) {
expectOk(block, contractFQN, testFunctionName, index);
}
txs = [];
blockStarted = false;
}
if (mineBlocksBefore > 1) {
simnet.mineEmptyBlocks(mineBlocksBefore - 1);
writeToLogFile(mineBlocksBefore - 1);
}
}
// start a new block if necessary
if (!blockStarted) {
blockStarted = true;
}
// add tx to current block
txs.push(generateCallWithArguments(callInfo, contractFQN, caller));
}
// close final block
if (blockStarted) {
writeToLogFile(txs);
block = simnet.mineBlock(txs);
for (let index = 0; index < txs.length; index++) {
expectOk(block, contractFQN, testFunctionName, index);
}
txs = [];
blockStarted = false;
}
}

function generateCallWithArguments(
callInfo: CallInfo,
contractPrincipal: string,
callerAddress: string
) {
const contractName = callInfo.contractName || contractPrincipal;
const functionName = callInfo.functionName;

return tx.callPublicFn(
contractName,
functionName,
callInfo.args.map((arg) => arg.value),
callerAddress
);
}

function writeToLogFile(data: ParsedTransactionResult[] | number | string) {
const filePath = path.join(__dirname, "clar-flow-test.log.txt");
if (typeof data === "number") {
fs.appendFileSync(filePath, `${data} empty blocks\n`);
} else if (typeof data === "string") {
fs.appendFileSync(filePath, `${data}\n`);
} else {
fs.appendFileSync(filePath, `block:\n${JSON.stringify(data, null, 2)}\n`);
}
}

function clearLogFile() {
const filePath = path.join(__dirname, "clar-flow-test.log.txt");
fs.writeFileSync(filePath, "");
}
Loading