@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
JavaScript
;
/**
* 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;