-
Notifications
You must be signed in to change notification settings - Fork 677
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
Changes from 3 commits
3757a97
6f3f983
7abb389
f77f32e
c5f9e4a
748a330
c85906d
3e9b3af
3420904
1755973
35d51ea
fedc750
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,6 @@ npm-debug.log* | |
coverage | ||
*.info | ||
costs-reports.json | ||
node_modules | ||
node_modules | ||
*.log.txt | ||
history.txt |
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) | ||
(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))) |
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)))) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { ParsedTransactionResult, tx } from "@hirosystems/clarinet-sdk"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, ""); | ||
} |
There was a problem hiding this comment.
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:
The tricky part might be the extra work around regexes, though the heavily lifting part around fast-check shouldn't be an issue.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).