UNPKG

@zombienet/orchestrator

Version:

ZombieNet aim to be a testing framework for substrate based blockchains, providing a simple cli tool that allow users to spawn and test ephemeral Substrate based networks

351 lines (350 loc) 16.5 kB
"use strict"; 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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const api_1 = require("@polkadot/api"); const utils_1 = require("@zombienet/utils"); const chai_1 = require("chai"); const execa_1 = __importDefault(require("execa")); const promises_1 = __importDefault(require("fs/promises")); const jsdom_1 = require("jsdom"); const minimatch_1 = require("minimatch"); const path_1 = __importDefault(require("path")); const typescript_1 = __importDefault(require("typescript")); const jsapi_helpers_1 = require("../jsapi-helpers"); const utilCrypto = require("@polkadot/util-crypto"); const debug = require("debug")("zombie::test-runner"); const DEFAULT_INDIVIDUAL_TEST_TIMEOUT = 10; // seconds // helper function toChaiComparator(op) { return op.charAt(0).toLocaleLowerCase() + op.slice(1); } const comparators = { Equal: chai_1.assert.equal, NotEqual: chai_1.assert.notEqual, IsAbove: chai_1.assert.isAbove, IsAtLeast: chai_1.assert.isAtLeast, IsBelow: chai_1.assert.isBelow, IsAtMost: chai_1.assert.isAtMost, }; const IsUp = ({ node_name, timeout }) => { timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; return (network) => __awaiter(void 0, void 0, void 0, function* () { const nodes = network.getNodes(node_name); const results = yield Promise.all(nodes.map((node) => node.getMetric("process_start_time_seconds", "isAtLeast", 1, timeout))); const AllNodeUps = results.every(Boolean); (0, chai_1.expect)(AllNodeUps).to.be.ok; }); }; const Report = ({ node_name, metric_name, target_value, op, timeout, }) => { const comparatorFn = comparators[op]; return (network) => __awaiter(void 0, void 0, void 0, function* () { const nodes = network.getNodes(node_name); const results = yield Promise.all(nodes.map((node) => node.getMetric(metric_name, toChaiComparator(op), target_value, timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT))); for (const value of results) { comparatorFn(value, target_value); } }); }; const CalcMetrics = ({ node_name, metric_name_a, math_ops, metric_name_b, target_value, op, timeout, }) => { const comparatorFn = comparators[op]; return (network) => __awaiter(void 0, void 0, void 0, function* () { const nodes = network.getNodes(node_name); const results = yield Promise.all(nodes.map((node) => node.getCalcMetric(metric_name_a, metric_name_b, math_ops, toChaiComparator(op), target_value, timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT))); for (const value of results) { comparatorFn(value, target_value); } }); }; const Histogram = ({ node_name, metric_name, target_value, buckets, op, timeout, }) => { const comparatorFn = comparators[op]; return (network) => __awaiter(void 0, void 0, void 0, function* () { const nodes = network.getNodes(node_name); const results = yield Promise.all(nodes.map((node) => node.getHistogramSamplesInBuckets(metric_name, buckets, target_value, timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT))); for (const value of results) { comparatorFn(value, target_value); } }); }; const Trace = ({ node_name, span_id, pattern }) => { const spanNames = pattern .split(",") .map((x) => x.replaceAll('"', "").trim()); return (network) => __awaiter(void 0, void 0, void 0, function* () { const nodes = network.getNodes(node_name); const results = yield Promise.all(nodes.map((node) => node.getSpansByTraceId(span_id, network.tracing_collator_url))); for (const value of results) { chai_1.assert.includeOrderedMembers(value, spanNames); } }); }; const LogMatch = ({ node_name, pattern, match_type, timeout }) => { const isGlob = (match_type && match_type.trim() === "glob") || false; return (network) => __awaiter(void 0, void 0, void 0, function* () { const nodes = network.getNodes(node_name); const results = yield Promise.all(nodes.map((node) => node.findPattern(pattern, isGlob, timeout))); const found = results.every(Boolean); (0, chai_1.expect)(found).to.be.ok; }); }; const CountLogMatch = ({ node_name, pattern, match_type, op, target_value, timeout, }) => { const comparatorFn = comparators[op]; const isGlob = (match_type && match_type.trim() === "glob") || false; return (network) => __awaiter(void 0, void 0, void 0, function* () { const nodes = network.getNodes(node_name); const results = yield Promise.all(nodes.map((node) => node.countPatternLines(pattern, isGlob, toChaiComparator(op), target_value, timeout))); for (const value of results) { comparatorFn(value, target_value); } }); }; const SystemEvent = ({ node_name, pattern, match_type, timeout }) => { const isGlob = (match_type && match_type.trim() === "glob") || false; return (network) => __awaiter(void 0, void 0, void 0, function* () { const node = network.node(node_name); const api = yield (0, jsapi_helpers_1.connect)(node.wsUri); const re = isGlob ? (0, minimatch_1.makeRe)(pattern) : new RegExp(pattern, "ig"); const found = yield (0, jsapi_helpers_1.findPatternInSystemEventSubscription)(api, re, timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT); api.disconnect(); (0, chai_1.expect)(found).to.be.ok; }); }; // Customs const CustomJs = ({ node_name, file_path, custom_args, op, target_value, timeout, is_ts, }) => { timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; const comparatorFn = comparators[op]; return (network, _backchannelMap, configBasePath) => __awaiter(void 0, void 0, void 0, function* () { const networkInfo = { tmpDir: network.tmpDir, chainSpecPath: network.chainSpecFullPath, relay: network.relay.map((node) => { const { name, wsUri, prometheusUri, userDefinedTypes } = node; return { name, wsUri, prometheusUri, userDefinedTypes }; }), paras: Object.keys(network.paras).reduce((memo, paraId) => { const { chainSpecPath, wasmPath, statePath } = network.paras[paraId]; memo[paraId] = { chainSpecPath, wasmPath, statePath }; memo[paraId].nodes = network.paras[paraId].nodes.map((node) => { return Object.assign({}, node); }); return memo; }, {}), nodesByName: Object.keys(network.nodesByName).reduce((memo, nodeName) => { const { name, wsUri, prometheusUri, userDefinedTypes, parachainId } = network.nodesByName[nodeName]; memo[nodeName] = { name, wsUri, prometheusUri, userDefinedTypes }; if (parachainId) memo[nodeName].parachainId = parachainId; return memo; }, {}), }; const nodes = network.getNodes(node_name); const call_args = custom_args ? custom_args === "" ? [] : custom_args.charAt(0) === "{" // allow to pass a single quoted json ? JSON.parse(custom_args) : custom_args.split(",") : []; let resolvedJsFilePath = path_1.default.resolve(configBasePath, file_path); if (is_ts) { const source = (yield promises_1.default.readFile(resolvedJsFilePath)).toString(); const result = typescript_1.default.transpileModule(source, { compilerOptions: { module: typescript_1.default.ModuleKind.CommonJS }, }); resolvedJsFilePath = path_1.default.resolve(configBasePath, path_1.default.parse(file_path).name + ".js"); yield promises_1.default.writeFile(resolvedJsFilePath, result.outputText); } // shim with jsdom const dom = new jsdom_1.JSDOM("<!doctype html><html><head><meta charset='utf-8'></head><body></body></html>"); global.window = dom.window; global.document = dom.window.document; global.zombie = { ApiPromise: api_1.ApiPromise, Keyring: api_1.Keyring, WsProvider: api_1.WsProvider, util: utilCrypto, connect: jsapi_helpers_1.connect, registerParachain: jsapi_helpers_1.registerParachain, }; let values; try { // import user defined code const jsScript = yield Promise.resolve(`${resolvedJsFilePath}`).then(s => __importStar(require(s))); const resp = yield Promise.race([ Promise.all(nodes.map((node) => jsScript.run(node.name, networkInfo, call_args))), new Promise((resolve) => setTimeout(() => { const err = new Error(`Timeout(${timeout}), "custom-js ${file_path} within ${timeout} secs" didn't complete on time.`); return resolve(err); }, timeout * 1000)), ]); if (resp instanceof Error) throw new Error(resp); else values = resp; } catch (err) { console.log(`\n ${utils_1.decorators.red(`Error running script: ${file_path}`)} \t ${utils_1.decorators.bright(err.message)}\n`); debug(err.stack || "can't print the stack"); throw new Error(err); } // remove shim if (is_ts) { yield execa_1.default.command(`rm -rf ${resolvedJsFilePath}`); } global.window = undefined; global.document = undefined; global.zombie = undefined; if (target_value) { for (const value of values) { comparatorFn(value, target_value); } } else { // test don't have matching output (0, chai_1.expect)(true).to.be.ok; } }); }; const CustomSh = ({ node_name, file_path, custom_args, op, target_value, timeout, }) => { timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; const comparatorFn = comparators[op]; return (network, _backchannelMap, configBasePath) => __awaiter(void 0, void 0, void 0, function* () { try { const resolvedShFilePath = path_1.default.resolve(configBasePath, file_path); const nodes = network.getNodes(node_name); const call_args = custom_args ? custom_args === "" ? [] : custom_args.split(",") : []; const results = yield Promise.all(nodes.map((node) => node.run(resolvedShFilePath, call_args, timeout))); if (comparatorFn && target_value !== undefined) { for (const value of results) { comparatorFn(value, target_value); } } // all the commands run successfully (0, chai_1.expect)(true).to.be.ok; } catch (err) { console.log(`\n ${utils_1.decorators.red(`Error running script: ${file_path}`)} \t ${utils_1.decorators.bright(err.message)}\n`); throw new Error(err); } }); }; // Paras const ParaIsRegistered = ({ node_name, para_id, timeout }) => { return (network) => __awaiter(void 0, void 0, void 0, function* () { const nodes = network.getNodes(node_name); const results = yield Promise.all(nodes.map((node) => node.parachainIsRegistered(para_id, timeout))); const parachainIsRegistered = results.every(Boolean); (0, chai_1.expect)(parachainIsRegistered).to.be.ok; }); }; const ParaBlockHeight = ({ node_name, para_id, target_value, op, timeout, }) => { timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; return (network) => __awaiter(void 0, void 0, void 0, function* () { const nodes = network.getNodes(node_name); const comparatorFn = comparators[op]; const results = yield Promise.all(nodes.map((node) => node.parachainBlockHeight(para_id, target_value, timeout))); for (const value of results) { comparatorFn(value, target_value); } }); }; const ParaRuntimeUpgrade = ({ node_name, para_id, file_or_uri, timeout, }) => { timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; return (network, _backchannelMap, configBasePath) => __awaiter(void 0, void 0, void 0, function* () { const node = network.node(node_name); let api = yield (0, jsapi_helpers_1.connect)(node.wsUri); let hash; if ((0, utils_1.isValidHttpUrl)(file_or_uri)) { hash = yield (0, jsapi_helpers_1.chainUpgradeFromUrl)(api, file_or_uri); } else { const resolvedJsFilePath = path_1.default.resolve(configBasePath, file_or_uri); hash = yield (0, jsapi_helpers_1.chainUpgradeFromLocalFile)(api, resolvedJsFilePath); } // validate in a node of the relay chain api.disconnect(); const { wsUri, userDefinedTypes } = network.relay[0]; api = yield (0, jsapi_helpers_1.connect)(wsUri, userDefinedTypes); const valid = yield (0, jsapi_helpers_1.validateRuntimeCode)(api, para_id, hash, timeout); api.disconnect(); (0, chai_1.expect)(valid).to.be.ok; }); }; const ParaRuntimeDummyUpgrade = ({ node_name, para_id, timeout }) => { timeout = timeout || DEFAULT_INDIVIDUAL_TEST_TIMEOUT; return (network) => __awaiter(void 0, void 0, void 0, function* () { const collator = network.paras[para_id].nodes[0]; let node = network.node(collator.name); let api = yield (0, jsapi_helpers_1.connect)(node.wsUri); const hash = yield (0, jsapi_helpers_1.chainCustomSectionUpgrade)(api); // validate in the <node>: of the relay chain node = network.node(node_name); api = yield (0, jsapi_helpers_1.connect)(node.wsUri); const valid = yield (0, jsapi_helpers_1.validateRuntimeCode)(api, para_id, hash, timeout); api.disconnect(); (0, chai_1.expect)(valid).to.be.ok; }); }; exports.default = { IsUp, Report, Histogram, Trace, LogMatch, CountLogMatch, SystemEvent, CustomJs, CustomSh, CalcMetrics, ParaBlockHeight, ParaIsRegistered, ParaRuntimeUpgrade, ParaRuntimeDummyUpgrade, };