UNPKG

@bowtieddevops/bowtieddevops-test-package-3

Version:
179 lines (163 loc) 5.46 kB
import { ParsedTransactionResult, tx } from "@hirosystems/clarinet-sdk"; import * as fs from "fs"; import path from "path"; import { describe, it } from "vitest"; import { CallInfo, FunctionAnnotations, FunctionBody, extractTestAnnotationsAndCalls, } from "./parser/clarity-parser-flow-tests"; import { expectOk, isValidTestFunction } from "./parser/test-helpers"; /** * Returns true if the contract is a test contract using the flow convention * @param contractName name of the contract * @returns */ function isTestContract(contractName: string) { return contractName.substring(contractName.length - 10) === "_flow_test"; } export function generateFlowTests(simnet: any) { const accounts = simnet.getAccounts(); clearLogFile(); // for each test contract create a test suite simnet.getContractsInterfaces().forEach((contract, contractFQN) => { if (!isTestContract(contractFQN)) { return; } describe(contractFQN, () => { // determine whether the contract has a prepare function 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, simnet, accounts); }); }); }); }); } /** * Mines one or more blocks based on the functions calls in the test function. * The function body must be one of the following: * 1. (unwrap! (contract-call? .contract-name function-name args)) * 2. (try! (function-name)) * * @param contractFQN the contract id * @param testFunctionName the name of the test function containing calls * @param calls a list of function calls with annotations part of the test function body */ function mineBlocksFromFunctionBody( contractFQN: string, testFunctionName: string, calls: FunctionBody, simnet: any, accounts: any ) { let blockStarted = false; let txs: any[] = []; let block: ParsedTransactionResult[] = []; // go through all function calls and // bundle function them into blocks for (const { callAnnotations, callInfo } of calls) { // mine empty blocks const mineBlocksBefore = parseInt(callAnnotations["mine-blocks-before"] as string) || 0; // get caller address const caller = accounts.get( (callAnnotations["caller"] as string) || "deployer" )!; if (mineBlocksBefore >= 1) { if (blockStarted) { writeToLogFile(txs); // mine block with txs and assert ok on all of them block = simnet.mineBlock(txs); for (let index = 0; index < txs.length; index++) { expectOk(block, contractFQN, testFunctionName, index); } txs = []; blockStarted = false; } // mine empty blocks if necessary 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; } } /** * creates a Tx * @param callInfo * @param contractPrincipal * @param callerAddress * @returns */ 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 ); } /** * writes data to a log file that represents the sequence of blocks mined * @param data */ 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`); } } /** * clears the log file */ function clearLogFile() { const filePath = path.join(__dirname, "clar-flow-test.log.txt"); fs.writeFileSync(filePath, ""); }