UNPKG

@tonkite/jest-tolk

Version:

<p align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tonkite/tonkite/main/assets/logo-dark.svg"> <img alt="tonkite logo" src="https://raw.githubusercontent.com/tonkite/tonkite/main/a

360 lines (359 loc) 15.3 kB
"use strict"; /** * Copyright 2024 Scaleton Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; const test_result_1 = require("@jest/test-result"); const tolk_js_1 = require("@ton/tolk-js"); const fs = __importStar(require("node:fs")); const sandbox_1 = require("@ton/sandbox"); const core_1 = require("@ton/core"); const node_crypto_1 = require("node:crypto"); const jest_message_util_1 = require("jest-message-util"); const jest_matcher_utils_1 = require("jest-matcher-utils"); const chalk_1 = __importDefault(require("chalk")); const assert = __importStar(require("node:assert")); const annotations_1 = require("./annotations"); const source_code_1 = require("./source-code"); const assertions_1 = require("./assertions"); const debug_reader_1 = require("./utils/debug-reader"); const int_strategy_1 = require("./fuzz/int-strategy"); const bool_strategy_1 = require("./fuzz/bool-strategy"); const address_strategy_1 = require("./fuzz/address-strategy"); class SkipRun extends Error { } function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } } function extractStackTrace(vmLogs) { return vmLogs .split(/\n/) .filter((line) => { if (line.startsWith('gas remaining:')) return false; if (line.startsWith('stack:')) return false; if (line.startsWith('code cell hash:')) return false; return true; }) .map((line) => { let matches; if ((matches = line.match(/^execute ([^ $]+)(.*)/))) { return chalk_1.default.cyan(matches[1]) + chalk_1.default.dim(matches[2]); } if ((matches = line.match(/^(handling exception code \d+:\s*)(.*)/))) { return matches[1] + chalk_1.default.bold(matches[2]); } return line; }) .slice(-8) .join('\n'); } const runTest = async ({ testPath, config, globalConfig, }) => { for (const setupFile of config.setupFiles) { try { require(setupFile); } catch (error) { throw new Error(`Setup file "${setupFile}" failed: ${error}`); } } const entrypointFileName = testPath.replace(config.rootDir + '/', ''); const result = await (0, tolk_js_1.runTolkCompiler)({ entrypointFileName, fsReadCallback: (path) => { const content = fs.readFileSync(path).toString(); // NOTE: It's required to have either onInternalMessage() or main() method. if (path == entrypointFileName) { const disableMain = !!content.match(/(^|\n)\s*\/\/\s+@no-main/); if (!disableMain) { return content + '\n\n' + 'fun main() {}'; } } return content; }, withStackComments: true, }); if (result.status !== 'ok') { throw result.message; } const testSourceCode = result.sourcesSnapshot.find(({ filename }) => filename === entrypointFileName); if (!testSourceCode) { throw new Error(`Expected behaviour: ${entrypointFileName} not found in a snapshot.`); } let numFailingTests = 0; let numPassingTests = 0; let numPendingTests = 0; let numTodoTests = 0; const testResults = []; const testCases = await (0, source_code_1.extractGetMethods)(testSourceCode.contents); // common setup const executor = await sandbox_1.Executor.create(); const code = core_1.Cell.fromBase64(result.codeBoc64); const data = (0, core_1.beginCell)().endCell(); const DEFAULT_RUNS = 100; const DEFAULT_ADDRESS = new core_1.Address(0, (0, node_crypto_1.randomBytes)(32)); const DEFAULT_RANDOM_SEED = (0, node_crypto_1.randomBytes)(32); const DEFAULT_BALANCE = (0, core_1.toNano)('1'); const DEFAULT_GAS_LIMIT = 2n ** 62n; const DEFAULT_UNIX_TIME = Math.floor(Date.now() / 1000); const testNamePattern = globalConfig.testNamePattern && new RegExp(globalConfig.testNamePattern, 'i'); for (const testCase of testCases) { let annotations = {}; let duration = 0; const testCaseName = testCase.methodName .replace(/^(test|testFuzz)_/, '') .replace(/_/g, ' '); try { annotations = testCase.docBlock ? (0, annotations_1.extractAnnotationsFromDocBlock)(testCase.docBlock) : {}; const isTest = testCase.methodName.match(/^test_/); const isFuzzTest = testCase.methodName.match(/^testFuzz_/); if (!isTest && !isFuzzTest) { continue; } const skip = annotations.skip || (testNamePattern && !testNamePattern.test(testCase.methodName)); if (skip) { testResults.push({ duration: 0, failureDetails: [], failureMessages: [], numPassingAsserts: 0, status: 'pending', ancestorTitles: annotations.scope ? [annotations.scope] : [], title: testCaseName, fullName: testCaseName, }); numPendingTests += 1; continue; } if (annotations.todo) { testResults.push({ duration: 0, failureDetails: [], failureMessages: [], numPassingAsserts: 0, status: 'todo', ancestorTitles: annotations.scope ? [annotations.scope] : [], title: testCaseName, fullName: testCaseName, }); numTodoTests += 1; continue; } if (testCase.parameters.length && !isFuzzTest) { throw Error('Only fuzz tests can have arguments.'); } const runTest = async (stack = []) => { const start = Date.now(); const { output, debugLogs } = await executor.runGetMethod({ code, data, methodId: testCase.methodId ?? (0, core_1.getMethodId)(testCase.methodName), unixTime: DEFAULT_UNIX_TIME, balance: DEFAULT_BALANCE, stack, address: DEFAULT_ADDRESS, randomSeed: DEFAULT_RANDOM_SEED, verbosity: 'full_location_gas', config: sandbox_1.defaultConfig, gasLimit: BigInt(annotations.gasLimit ?? DEFAULT_GAS_LIMIT), debugEnabled: true, }); const end = Date.now(); duration += end - start; let expectedExitCode = 0; const debugReader = debug_reader_1.DebugReader.fromLogs(debugLogs); const handlers = { TEST_EXIT_CODE: () => { expectedExitCode = debugReader.nextInt(); }, TEST_ASSUME: () => { throw new SkipRun(); }, ...assertions_1.ASSERTIONS, }; while (!debugReader.isEOF()) { const entry = debugReader.next(); if (handlers[entry]) { handlers[entry](debugReader); } else { console.log('unknown value:', entry); } } if (!output.success) { throw `Execution failed: ${output.error}`; } if (expectedExitCode !== 0) { assert.equal(output.vm_exit_code, expectedExitCode, `Test case has thrown an error code ${output.vm_exit_code} (expected ${expectedExitCode}).`); } else { if (output.vm_exit_code !== 0) { const stackTrace = extractStackTrace(output.vm_log); throw new Error(`Test case has failed with an error code ${output.vm_exit_code}.\n\n[...]\n${stackTrace}`); } } }; if (isFuzzTest) { const runs = annotations.runs ?? DEFAULT_RUNS; const parameters = []; for (const parameter of testCase.parameters) { let matches; // int if ((matches = parameter.type.match(/^(u?)int(\d*)$/))) { const signed = matches[1] !== 'u'; const bits = parseInt(matches[2] ?? 256); parameters.push((0, int_strategy_1.generateIntValues)(bits, signed, runs)); continue; } if (parameter.type === 'bool') { parameters.push((0, bool_strategy_1.generateBoolValues)(runs)); continue; } if (parameter.type === 'address') { parameters.push((0, address_strategy_1.generateAddressValues)(runs)); continue; } throw new Error(`Fuzz tests do not support type ${parameter.type}.`); } parameters.forEach((stack) => shuffleArray(stack)); let passed = 0; let failed = 0; let skipped = 0; let failures = []; for (let runIndex = 0; runIndex < runs; runIndex++) { try { await runTest(testCase.parameters.map((_, index) => parameters[index][runIndex])); passed++; } catch (error) { if (error instanceof SkipRun) { skipped++; continue; } failed++; const message = error instanceof Error ? error.message : 'Unknown error occurred.'; const formatParameter = (type, item) => { if (item.type === 'slice') { return type === 'address' ? (item.cell.asSlice().loadAddressAny()?.toString() ?? null) : item.cell.toString(); } if (item.type === 'int') { return type === 'bool' ? !!item.value : item.value; } return (0, jest_matcher_utils_1.DIM_COLOR)('unknown'); }; failures.push(`${(0, jest_matcher_utils_1.BOLD_WEIGHT)(`Run #${runIndex}`)}: ${message.substring(0, message.indexOf('\n'))}\n` + testCase.parameters .map((parameter, parameterIndex) => `${parameterIndex === testCase.parameters.length - 1 ? '└' : '├'} ${(0, jest_matcher_utils_1.BOLD_WEIGHT)(parameter.name)} = ${formatParameter(parameter.type, parameters[parameterIndex][runIndex])}`) .join('\n')); } } if (failures.length) { throw new Error(`Fuzz test failed (${passed} passed, ${failed} failed, ${skipped} skipped).\n\n${failures.join('\n\n')}`); } } else { await runTest(); } testResults.push({ duration, failureDetails: [], failureMessages: [], numPassingAsserts: 1, status: 'passed', ancestorTitles: annotations.scope ? [annotations.scope] : [], title: testCaseName, fullName: testCaseName, }); numPassingTests += 1; } catch (error) { const failureMessage = typeof error === 'string' ? error : error instanceof Error ? error.message : `${error}`; testResults.push({ duration, failureDetails: [], failureMessages: [failureMessage], numPassingAsserts: 0, status: 'failed', ancestorTitles: annotations.scope ? [annotations.scope] : [], title: testCaseName, fullName: testCaseName, }); numFailingTests += 1; } } return { ...(0, test_result_1.createEmptyTestResult)(), failureMessage: (0, jest_message_util_1.formatResultsErrors)(testResults, config, globalConfig, testPath), numFailingTests, numPassingTests, numPendingTests, numTodoTests, testResults, testFilePath: testPath, }; }; module.exports = runTest;