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

255 lines (254 loc) 14.4 kB
"use strict"; 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 }); exports.run = run; const utils_1 = require("@zombienet/utils"); const fs_1 = __importDefault(require("fs")); const mocha_1 = __importDefault(require("mocha")); const path_1 = __importDefault(require("path")); const network_1 = require("../network"); const orchestrator_1 = require("../orchestrator"); const providers_1 = require("../providers"); const assertions_1 = __importDefault(require("./assertions")); const commands_1 = __importDefault(require("./commands")); const DEFAULT_GLOBAL_TIMEOUT = 1200; // 20 mins const debug = require("debug")("zombie::test-runner"); const { Test, Suite } = mocha_1.default; const mocha = new mocha_1.default(); function run(configBasePath_1, testName_1, testDef_1, provider_1) { return __awaiter(this, arguments, void 0, function* (configBasePath, testName, testDef, provider, inCI = false, concurrency = 1, logType = "table", runningNetworkSpecPath, dir, force = false) { var _a; const testStart = performance.now(); logType && (0, utils_1.setLogType)(logType); let network; const backchannelMap = {}; let suiteName = testName; if (testDef.description) suiteName += `( ${testDef.description} )`; // read network file const networkConfigFilePath = fs_1.default.existsSync(testDef.network) ? testDef.network : path_1.default.resolve(configBasePath, testDef.network); const config = (0, utils_1.readNetworkConfig)(networkConfigFilePath); // set the provider if (!config.settings) config.settings = { provider, timeout: DEFAULT_GLOBAL_TIMEOUT }; else config.settings.provider = provider; // find creds file const configFromEnv = process.env.KUBECONFIG || "config"; const credsFile = inCI ? configFromEnv : ((_a = testDef.creds) !== null && _a !== void 0 ? _a : "config"); let creds; if (fs_1.default.existsSync(credsFile)) creds = credsFile; else { const possiblePaths = [ ".", "..", `${process.env.HOME}/.kube`, "/etc/zombie-net", ]; const credsFileExistInPath = possiblePaths.find((path) => { const t = `${path}/${credsFile}`; return fs_1.default.existsSync(t); }); if (credsFileExistInPath) creds = credsFileExistInPath + "/" + credsFile; } if (!creds && config.settings.provider === "kubernetes") throw new Error(`Invalid credential file path: ${credsFile}`); // create suite const suite = Suite.create(mocha.suite, suiteName); suite.beforeAll("launching", function () { return __awaiter(this, void 0, void 0, function* () { var _a; const launchTimeout = ((_a = config.settings) === null || _a === void 0 ? void 0 : _a.timeout) || 500; this.timeout(launchTimeout * 1000); try { if (!runningNetworkSpecPath) { console.log(`\t Launching network... this can take a while.`); network = yield (0, orchestrator_1.start)(creds, config, { spawnConcurrency: concurrency, inCI, logType, dir, force, }); } else { const runningNetworkSpec = require(runningNetworkSpecPath); if (provider !== runningNetworkSpec.client.providerName) throw new Error(`Invalid provider, the provider set doesn't match with the running network definition`); const { client, namespace, tmpDir } = runningNetworkSpec; // initialize the Client const initClient = providers_1.Providers.get(runningNetworkSpec.client.providerName).initClient(client.configPath, namespace, tmpDir); // initialize the network network = (0, network_1.rebuildNetwork)(initClient, runningNetworkSpec); } network.showNetworkInfo(config.settings.provider); yield (0, utils_1.sleep)(5 * 1000); return; } catch (err) { console.log(`\n${utils_1.decorators.red("Error launching the network!")} \t ${utils_1.decorators.bright(err)}`); exitMocha(100); } }); }); suite.afterAll("teardown", function () { return __awaiter(this, void 0, void 0, function* () { const timeout = 180 * 1000; // 3 mins this.timeout(timeout + 10 * 1000); // just in case use mocha timeout after 10 secs of the teardown timeout. const innerTearDown = () => __awaiter(this, void 0, void 0, function* () { var _a, _b; // report metric const testEnd = performance.now(); const elapsedSecs = Math.round((testEnd - testStart) / 1000); debug(`\t 🕰 [Test] elapsed time: ${elapsedSecs} secs`); let success = false; if (network && !network.wasRunning) { let logsPath; try { logsPath = yield network.dumpLogs(false); } catch (e) { console.log(`${utils_1.decorators.red("❌ Error dumping logs!")}`); console.log(`err: ${e}`); } const tests = (_b = (_a = this.test) === null || _a === void 0 ? void 0 : _a.parent) === null || _b === void 0 ? void 0 : _b.tests; if (tests) { const failed = tests.filter((test) => { return test.state !== "passed"; }); if (failed.length) { console.log(`\n\n\t${utils_1.decorators.red("❌ One or more of your test failed...")}`); // Show network-wide error logs link for kubernetes in CI when tests fail if (network.client.providerName === "kubernetes" && inCI) { const networkEndtime = new Date().getTime(); const networkLokiUrl = (0, utils_1.getLokiUrlForNetworkErrors)(network.namespace, network.networkStartTime, networkEndtime); console.log(`\n\t${utils_1.decorators.red("🔍 View error logs for all nodes in the network:")}`); console.log(`\t${utils_1.decorators.bright(utils_1.decorators.red(networkLokiUrl))}`); } } else { success = true; } // All test passed, just remove the network console.log(`\n\t ${utils_1.decorators.green("Deleting network")}`); try { yield network.stop(); } catch (e) { console.log(`${utils_1.decorators.yellow("⚠️ Error deleting network")}`); console.log(`err: ${e}`); } // show logs console.log(`\n\n\t${utils_1.decorators.magenta("📓 To see the full logs of the nodes please go to:")}`); switch (network.client.providerName) { case "podman": case "native": console.log(`\n\t${utils_1.decorators.magenta(logsPath)}`); break; case "kubernetes": if (inCI) { // show links to grafana and also we need to move the logs to artifacts const networkEndtime = new Date().getTime(); for (const node of network.relay) { const loki_url = (0, utils_1.getLokiUrl)(network.namespace, node.name, network.networkStartTime, networkEndtime); console.log(`\t${utils_1.decorators.magenta(node.name)}: ${utils_1.decorators.green(loki_url)}`); } for (const [paraId, parachain] of Object.entries(network.paras)) { console.log(`\n\tParaId: ${utils_1.decorators.magenta(paraId)}`); for (const node of parachain.nodes) { const loki_url = (0, utils_1.getLokiUrl)(network.namespace, node.name, network.networkStartTime, networkEndtime); console.log(`\t\t${utils_1.decorators.magenta(node.name)}: ${utils_1.decorators.green(loki_url)}`); } } // Add network-wide errors logs link const networkLokiUrl = (0, utils_1.getLokiUrlForNetworkErrors)(network.namespace, network.networkStartTime, networkEndtime); console.log(`\n\t${utils_1.decorators.cyan("🌐 All nodes (relaychain + parachains) error logs:")} ${utils_1.decorators.bright(networkLokiUrl)}`); // logs are also collected as artifacts console.log(`\n\n\t ${utils_1.decorators.yellow("📓 Logs are also available in the artifacts' pipeline in gitlab")}`); } else { console.log(`\n\t${utils_1.decorators.magenta(logsPath)}`); } break; } } } // submit metric if (inCI) yield (0, utils_1.registerTotalElapsedTimeSecs)(elapsedSecs, success); }); const resp = yield Promise.race([ innerTearDown(), new Promise((resolve) => setTimeout(() => { const err = new Error(`Timeout(${timeout}), in teardown process... continuing reporting the tests`); return resolve(err); }, timeout)), ]); console.log(resp); if (resp instanceof Error) { console.log(`${utils_1.decorators.yellow("⚠️ Error in teardown process!")}`); console.log(`err: ${resp}`); } // always return since we don't want to report errors in teardown return; }); }); for (const assertion of testDef.assertions) { const generator = fns[assertion.parsed.fn]; debug(generator); if (!generator) { console.log(`\n ${utils_1.decorators.red("Invalid fn generator:")} \t ${utils_1.decorators.bright(assertion.parsed.fn)}`); process.exit(1); } const testFn = generator(assertion.parsed.args); const test = new Test(assertion.original_line, () => __awaiter(this, void 0, void 0, function* () { return yield testFn(network, backchannelMap, configBasePath); })); suite.addTest(test); test.timeout(0); } // pass the file path, don't load the reporter as a module const resolvedReporterPath = path_1.default.resolve(__dirname, "./testReporter"); mocha.reporter(resolvedReporterPath); // run mocha.run(exitMocha); }); } // extracted from mocha test runner helper. const exitMocha = (code) => { console.log("exit code", code); const clampedCode = Math.min(code, 255); let draining = 0; // Eagerly set the process's exit code in case stream.write doesn't // execute its callback before the process terminates. process.exitCode = clampedCode; // flush output for Node.js Windows pipe bug // https://github.com/joyent/node/issues/6247 is just one bug example // https://github.com/visionmedia/mocha/issues/333 has a good discussion const done = () => { if (!draining--) { process.exit(clampedCode); } }; const streams = [process.stdout, process.stderr]; streams.forEach((stream) => { // submit empty write request and wait for completion draining += 1; stream.write("", done); }); done(); }; const fns = Object.assign(Object.assign({}, assertions_1.default), commands_1.default);