UNPKG

@bowtieddevops/bowtieddevops-test-package-3

Version:
203 lines (180 loc) 6.38 kB
import { ClarityValue } from "@stacks/transactions"; import { stringToCV } from "./string-to-cv"; import { Simnet } from "@hirosystems/clarinet-sdk"; export type FunctionAnnotations = { [key: string]: string | boolean }; export type FunctionBody = { callAnnotations: FunctionAnnotations[]; callInfo: CallInfo; }[]; export type ContractCall = { callAnnotations: FunctionAnnotations; callInfo: CallInfo; }; export type CallInfo = { contractName: string; functionName: string; args: { type: string; value: ClarityValue }[]; }; const functionRegex = /^([ \t]{0,};;[ \t]{0,}@[^()]+?)\n[ \t]{0,}\(define-public[\s]+\((.+?)[ \t|)]/gm; const annotationsRegex = /^;;[ \t]{1,}@([a-z-]+)(?:$|[ \t]+?(.+?))$/; const callRegex = /\n*^([ \t]{0,};;[ \t]{0,}@[\s\S]+?)\n[ \t]{0,}(\((?:[^()]*|\((?:[^()]*|\([^()]*\))*\))*\))/gm; /** * Parser function for flow unit tests. * * Flow unit tests can be used for tx calls where * the tx-sender should be equal to the contract-caller. * * Takes the whole contract source and returns an object containing * the function annotations and function bodies for each function. * @param contractSource * @returns */ export function extractTestAnnotationsAndCalls(contractSource: string, simnet: Simnet) { const functionAnnotations: any = {}; const functionBodies: any = {}; contractSource = contractSource.replace(/\r/g, ""); const matches1 = contractSource.matchAll(functionRegex); let indexStart: number = -1; let headerLength: number = 0; let indexEnd: number = -1; let lastFunctionName: string = ""; let contractCalls: { callAnnotations: FunctionAnnotations; callInfo: CallInfo; }[]; for (const [functionHeader, comments, functionName] of matches1) { if (functionName.substring(0, 5) !== "test-") continue; functionAnnotations[functionName] = {}; const lines = comments.split("\n"); for (const line of lines) { const [, prop, value] = line.match(annotationsRegex) || []; if (prop) functionAnnotations[functionName][prop] = value ?? true; } if (indexStart < 0) { indexStart = contractSource.indexOf(functionHeader); headerLength = functionHeader.length; lastFunctionName = functionName; } else { indexEnd = contractSource.indexOf(functionHeader); const lastFunctionBody = contractSource.substring( indexStart + headerLength, indexEnd ); // add contracts calls in functions body for last function contractCalls = extractContractCalls(lastFunctionBody, simnet); functionBodies[lastFunctionName] = contractCalls; indexStart = indexEnd; headerLength = functionHeader.length; lastFunctionName = functionName; } } const lastFunctionBody = contractSource.substring(indexStart + headerLength); contractCalls = extractContractCalls(lastFunctionBody, simnet); functionBodies[lastFunctionName] = contractCalls; return [functionAnnotations, functionBodies]; } /** * Takes a string and returns an array of objects containing * the call annotations and call info within the function body. * * The function body should look like this * (begin * ... lines of code.. * (ok true)) * * Only two lines of code are accepted: * 1. (unwrap! (contract-call? .contract-name function-name args)) * 2. (try! (function-name)) * @param lastFunctionBody * @returns */ export function extractContractCalls(lastFunctionBody: string, simnet: Simnet) { const calls = lastFunctionBody.matchAll(callRegex); const contractCalls: ContractCall[] = []; for (const [, comments, call] of calls) { const callAnnotations: FunctionAnnotations = {}; const lines = comments.split("\n"); for (const line of lines) { const [, prop, value] = line.trim().match(annotationsRegex) || []; if (prop) callAnnotations[prop] = value ?? true; } // try to extract call info from (unwrap! (contract-call? ...)) let callInfo = extractUnwrapInfo(call, simnet); if (!callInfo) { // try to extract call info from (try! (my-function)) callInfo = extractCallInfo(call); } if (callInfo) { contractCalls.push({ callAnnotations, callInfo }); } else { throw new Error(`Could not extract call info from ${call}`); } } return contractCalls; } /** * handle (unwrap! (contract-call? ...)) statements * @param statement * @returns */ function extractUnwrapInfo(statement: string, simnet: Simnet): CallInfo | null { const match = statement.match( /\(unwrap! \(contract-call\? \.(.+?) (.+?)(( .+?)*)\)/ ); if (!match) return null; const contractName = match[1]; const functionName = match[2]; const argStrings = splitArgs(match[3]); let fn: any; simnet.getContractsInterfaces().forEach((contract, contractFQN) => { const [_, ctrName] = contractFQN.split("."); if (ctrName === contractName) { fn = contract.functions.find((f) => f.name === functionName); if (!fn) { throw `function ${functionName} not found in contract ${contractName}`; } } }); if (!fn) { throw `function ${functionName} not found in contract ${contractName}`; } const args = fn.args.map((arg: any, index: number) => stringToCV(argStrings[index], arg.type) ); return { contractName, functionName, args, }; } function extractCallInfo(statement: string) { const match = statement.match(/\(try! \((.+?)\)\)/); if (!match) return null; return { contractName: "", functionName: match[1], args: [] }; } // take a string containing function arguments and // split them correctly into an array of argument strings function splitArgs(argString: string): string[] { const splitArgs: string[] = []; let argStart = 0; let brackets = 0; // curly brackets let rbrackets = 0; // round brackets for (let i = 0; i < argString.length; i++) { const char = argString[i]; if (char === "{") brackets++; if (char === "}") brackets--; if (char === "(") rbrackets++; if (char === ")") rbrackets--; const atLastChar = i === argString.length - 1; if ((char === " " && brackets === 0 && rbrackets === 0) || atLastChar) { const newArg = argString.slice(argStart, i + (atLastChar ? 1 : 0)); if (newArg.trim()) { splitArgs.push(newArg.trim()); } argStart = i + 1; } } return splitArgs; }