UNPKG

@touca/node

Version:

Touca SDK for JavaScript

759 lines (758 loc) 32.9 kB
// Copyright 2023 Touca, Inc. Subject to Apache-2.0 License. var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; 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 __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; import { mkdirSync, writeFileSync } from 'node:fs'; import { dirname } from 'node:path'; import { format } from 'node:util'; import { Builder } from 'flatbuffers'; import { Case } from './case.js'; import { assignOptions, ToucaError, updateCoreOptions } from './options.js'; import { run } from './runner.js'; import * as schema from './schema.js'; import { Transport } from './transport.js'; import { TypeHandler } from './types.js'; var NodeClient = /** @class */ (function () { function NodeClient() { this._cases = new Map(); this._configured = false; this._options = {}; this._transport = new Transport(); this._type_handler = new TypeHandler(); this._workflows = []; } NodeClient.prototype.isConfigured = function (v) { return this._configured; }; NodeClient.prototype._serialize = function (cases) { var e_1, _a; var builder = new Builder(1024); var msg_buf = []; try { for (var _b = __values(cases.reverse()), _c = _b.next(); !_c.done; _c = _b.next()) { var item = _c.value; var content = item.serialize(); var buf = schema.MessageBuffer.createBufVector(builder, content); schema.MessageBuffer.startMessageBuffer(builder); schema.MessageBuffer.addBuf(builder, buf); msg_buf.push(schema.MessageBuffer.endMessageBuffer(builder)); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (_c && !_c.done && (_a = _b.return)) _a.call(_b); } finally { if (e_1) throw e_1.error; } } var fbs_msg_buf = schema.Messages.createMessagesVector(builder, msg_buf); schema.Messages.startMessages(builder); schema.Messages.addMessages(builder, fbs_msg_buf); var fbs_messages = schema.Messages.endMessages(builder); builder.finish(fbs_messages); return builder.asUint8Array(); }; NodeClient.prototype._save = function (path, cases) { if (dirname(path).length !== 0) { mkdirSync(dirname(path), { recursive: true }); } if (cases.length !== 0) { return Array.from(this._cases.entries()) .filter(function (k) { return cases.includes(k[0]); }) .map(function (k) { return k[1]; }); } return Array.from(this._cases.values()); }; NodeClient.prototype._post = function (path, content, headers) { if (headers === void 0) { headers = {}; } return __awaiter(this, void 0, void 0, function () { var response, reason; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, this._transport.request('POST', path, content, 'application/octet-stream', headers)]; case 1: response = _a.sent(); if (response.status === 204) { return [2 /*return*/]; } if (response.status === 200) { return [2 /*return*/, response.body]; } reason = ''; if (response.status === 400) { if (response.body.includes('batch is sealed')) { reason = ' This version is already submitted and sealed'; } if (response.body.includes('team not found')) { reason = ' This team does not exist'; } throw new ToucaError('transport_post', reason); } return [2 /*return*/]; } }); }); }; /** * Configures the Touca client. Must be called before declaring test cases * and adding results to the client. Should be regarded as a potentially * expensive operation. Should be called only from your test environment. * * {@link configure} takes a variety of optional configuration parameters. * Calling this * function without any parameters is possible: the client can capture * the behavior and performance data and store them on a local filesystem * but it will not be able to post them to the Touca server. * * In most cases, You will need to pass API Key and API URL during the * configuration. The code below shows the common pattern in which API URL * is given in long format (it includes the team slug and the suite slug) * and API Key as well as the version of the code under test are specified * as environment variables `TOUCA_API_KEY` and `TOUCA_TEST_VERSION`, * respectively: * * ```js * touca.configure({api_url: 'https://api.touca.io/@/acme/students'}) * ``` * * As long as the API Key and API URL to the Touca server are known to * the client, it attempts to authenticate with the Touca server. You * can explicitly disable this communication in rare cases by setting * configuration option `offline` to `false`. * * You can call {@link configure} any number of times. The client * preserves the configuration parameters specified in previous calls to * this function. */ NodeClient.prototype.configure = function (options) { if (options === void 0) { options = {}; } return __awaiter(this, void 0, void 0, function () { var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: assignOptions(this._options, options); _a = this; return [4 /*yield*/, updateCoreOptions(this._options, this._transport)]; case 1: _a._configured = _b.sent(); return [2 /*return*/]; } }); }); }; /** * Declares name of the test case to which all subsequent results will be * submitted until a new test case is declared. * * If configuration parameter `concurrency` is set to `"enabled"`, when * a thread calls `declare_testcase` all other threads also have their most * recent testcase changed to the newly declared one. Otherwise, each * thread will submit to its own testcase. * * @param name name of the testcase to be declared */ NodeClient.prototype.declare_testcase = function (name) { if (!this.isConfigured(this._options)) { return; } if (!this._cases.has(name)) { var testcase = new Case({ name: name, team: this._options.team, suite: this._options.suite, version: this._options.version }); this._cases.set(name, testcase); } this._active_case = name; }; /** * Removes all logged information associated with a given test case. * * This information is removed from memory, such that switching back to * an already-declared or already-submitted test case would behave similar * to when that test case was first declared. This information is removed, * for all threads, regardless of the configuration option `concurrency`. * Information already submitted to the server will not be removed from * the server. * * This operation is useful in long-running regression test frameworks, * after submission of test case to the server, if memory consumed by * the client library is a concern or if there is a risk that a future * test case with a similar name may be executed. * * @param name name of the testcase to be removed from memory * * @throws when called with the name of a test case that was never declared */ NodeClient.prototype.forget_testcase = function (name) { if (!this._cases.has(name)) { throw new ToucaError('capture_forget', name); } this._cases.delete(name); }; /** * Captures the value of a given variable as a data point for the declared * test case and associates it with the specified key. * * @param key name to be associated with the captured data point * @param value value to be captured as a test result * @param options comparison rule for this test result */ NodeClient.prototype.check = function (key, value, options) { var _a; if (this._active_case) { var touca_value = this._type_handler.transform(value); (_a = this._cases.get(this._active_case)) === null || _a === void 0 ? void 0 : _a.check(key, touca_value, options); } }; /** * Captures an external file as a data point for the declared * test case and associates it with the specified key. * * @param key name to be associated with the captured file * @param path path to the external file to be captured */ NodeClient.prototype.checkFile = function (key, path) { var _a; if (this._active_case) { (_a = this._cases.get(this._active_case)) === null || _a === void 0 ? void 0 : _a.checkFile(key, path); } }; /** * Logs a given value as an assertion for the declared test case * and associates it with the specified key. * * @param key name to be associated with the logged test result * @param value value to be logged as a test result */ NodeClient.prototype.assume = function (key, value) { var _a; if (this._active_case) { var touca_value = this._type_handler.transform(value); (_a = this._cases.get(this._active_case)) === null || _a === void 0 ? void 0 : _a.assume(key, touca_value); } }; /** * Adds a given value to a list of results for the declared * test case which is associated with the specified key. * Could be considered as a helper utility function. * This method is particularly helpful to log a list of items as they * are found: * * ```js * for (const number of numbers) { * if (is_prime(number)) { * touca.add_array_element("prime numbers", number); * touca.add_hit_count("number of primes"); * } * } * ``` * * This pattern can be considered as a syntactic sugar for the following * alternative: * * ```js * const primes = []; * for (const number of numbers) { * if (is_prime(number)) { * primes.push(number); * } * } * if (primes.length !== 0) { * touca.check("prime numbers", primes); * touca.check("number of primes", primes.length); * } * ``` * * The items added to the list are not required to be of the same type. * The following code is acceptable: * * ```js * touca.check("prime numbers", 42); * touca.check("prime numbers", "forty three"); * ``` * * @throws if specified key is already associated with a test result which * was not iterable * @param key name to be associated with the logged test result * @param value element to be appended to the array * @see {@link check} */ NodeClient.prototype.add_array_element = function (key, value) { var _a; if (this._active_case) { var touca_value = this._type_handler.transform(value); (_a = this._cases.get(this._active_case)) === null || _a === void 0 ? void 0 : _a.add_array_element(key, touca_value); } }; /** * Increments value of key every time it is executed. * creates the key with initial value of one if it does not exist. * * Could be considered as a helper utility function. * This method is particularly helpful to track variables whose values * are determined in loops with indeterminate execution cycles: * * ```js * for (const number of numbers) { * if (is_prime(number)) { * touca.add_array_element("prime numbers", number); * touca.add_hit_count("number of primes"); * } * } * ``` * * This pattern can be considered as a syntactic sugar for the following * alternative: * * ```js * const primes = [] * for (const number of numbers) { * if (is_prime(number)) { * primes.push(number); * } * } * if (primes.length !== 0) { * touca.check("prime numbers", primes); * touca.check("number of primes", primes.length); * } * ``` * * @throws if specified key is already associated with a test result * which was not an integer * * @param key name to be associated with the logged test result * @see {@link check} */ NodeClient.prototype.add_hit_count = function (key) { var _a; if (this._active_case) { (_a = this._cases.get(this._active_case)) === null || _a === void 0 ? void 0 : _a.add_hit_count(key); } }; /** * Adds an already obtained measurements to the list of captured * performance benchmarks. * * Useful for logging a metric that is measured without using this SDK. * * @param key name to be associated with this performance benchmark * @param milliseconds duration of this measurement in milliseconds */ NodeClient.prototype.add_metric = function (key, milliseconds) { var _a; if (this._active_case) { (_a = this._cases.get(this._active_case)) === null || _a === void 0 ? void 0 : _a.add_metric(key, milliseconds); } }; /** * Starts timing an event with the specified name. * * Measurement of the event is only complete when function * {@link stop_timer} is later called for the specified name. * * @param key name to be associated with the performance metric */ NodeClient.prototype.start_timer = function (key) { var _a; if (this._active_case) { (_a = this._cases.get(this._active_case)) === null || _a === void 0 ? void 0 : _a.start_timer(key); } }; /** * Stops timing an event with the specified name. * * Expects function {@link stop_timer} to have been called previously * with the specified name. * * @param key name to be associated with the performance metric */ NodeClient.prototype.stop_timer = function (key) { var _a; if (this._active_case) { (_a = this._cases.get(this._active_case)) === null || _a === void 0 ? void 0 : _a.stop_timer(key); } }; NodeClient.prototype.scoped_timer = function (key, callback) { return __awaiter(this, void 0, void 0, function () { var v; return __generator(this, function (_a) { switch (_a.label) { case 0: this.start_timer(key); return [4 /*yield*/, callback()]; case 1: v = _a.sent(); this.stop_timer(key); return [2 /*return*/, v]; } }); }); }; /** * Registers custom serialization logic for a given custom data type. * * Calling this function is rarely needed. The library already handles * all custom data types by serializing all their properties. Custom * serializers allow you to exclude a subset of an object properties * during serialization. */ NodeClient.prototype.add_serializer = function (type, // eslint-disable-next-line @typescript-eslint/no-explicit-any serializer) { this._type_handler.add_serializer(type, serializer); }; /** * Stores test results and performance benchmarks in binary format * in a file of specified path. * * Touca binary files can be submitted at a later time to the Touca * server. * * We do not recommend as a general practice for regression test tools * to locally store their test results. This feature may be helpful for * special cases such as when regression test tools have to be run in * environments that have no access to the Touca server (e.g. running * with no network access). * * @param path path to file in which test results and performance * benchmarks should be stored. * @param cases names of test cases whose results should be stored. * If a set is not specified or is set as empty, all * test cases will be stored in the specified file. */ NodeClient.prototype.save_binary = function (path, cases) { if (cases === void 0) { cases = []; } return __awaiter(this, void 0, void 0, function () { var items, content; return __generator(this, function (_a) { items = this._save(path, cases); content = this._serialize(items); writeFileSync(path, content, { flag: 'w+' }); return [2 /*return*/]; }); }); }; /** * Stores test results and performance benchmarks in JSON format * in a file of specified path. * * This feature may be helpful during development of regression tests * tools for quick inspection of the test results and performance metrics * being captured. * * @param path path to file in which test results and performance * benchmarks should be stored. * @param cases names of test cases whose results should be stored. * If a set is not specified or is set as empty, all * test cases will be stored in the specified file. */ NodeClient.prototype.save_json = function (path, cases) { if (cases === void 0) { cases = []; } return __awaiter(this, void 0, void 0, function () { var items, content; return __generator(this, function (_a) { items = this._save(path, cases); content = JSON.stringify(items.map(function (item) { return item.json(); })); writeFileSync(path, content, { flag: 'w+' }); return [2 /*return*/]; }); }); }; /** @param cmp comparison result for a single test case */ NodeClient.prototype.parseComparisonResult = function (result) { try { var cmp = JSON.parse(result)[0]; return cmp['body'] && cmp['body']['src']['version'] == cmp['body']['dst']['version'] ? 'Sent' : cmp['overview'] && cmp['overview']['keysScore'] == 1 ? 'Pass' : 'Diff'; } catch (err) { return 'Sent'; } }; /** * Submits all test results recorded so far to Touca server. * * It is possible to call {@link post} multiple times during runtime * of the regression test tool. Test cases already submitted to the server * whose test results have not changed, will not be resubmitted. * It is also possible to add test results to a testcase after it is * submitted to the server. Any subsequent call to {@link post} will * resubmit the modified test case. * * @throws if called before calling `configure` or when called on a client * that is configured not to communicate with the Touca server or * if operation fails for any reason. * * @returns a promise that is resolved when all test results are submitted. */ NodeClient.prototype.post = function (options) { if (options === void 0) { options = { submit_async: false }; } return __awaiter(this, void 0, void 0, function () { var content, result, _a, _b, _c, name_1, testcase, _d, _e, _f, key, value, e_2_1, e_3_1; var e_3, _g, e_2, _h; return __generator(this, function (_j) { switch (_j.label) { case 0: if (!this.isConfigured(this._options) || this._options.offline) { throw new ToucaError('capture_not_configured'); } content = this._serialize(Array.from(this._cases.values())); return [4 /*yield*/, this._post('/client/submit', content, { 'X-Touca-Submission-Mode': options.submit_async ? 'async' : 'sync' })]; case 1: result = _j.sent(); _j.label = 2; case 2: _j.trys.push([2, 13, 14, 15]); _a = __values(this._cases.entries()), _b = _a.next(); _j.label = 3; case 3: if (!!_b.done) return [3 /*break*/, 12]; _c = __read(_b.value, 2), name_1 = _c[0], testcase = _c[1]; _j.label = 4; case 4: _j.trys.push([4, 9, 10, 11]); _d = (e_2 = void 0, __values(testcase.blobs())), _e = _d.next(); _j.label = 5; case 5: if (!!_e.done) return [3 /*break*/, 8]; _f = __read(_e.value, 2), key = _f[0], value = _f[1]; return [4 /*yield*/, this._post("/client/submit/artifact/".concat(this._options.team, "/").concat(this._options.suite, "/").concat(this._options.version, "/").concat(name_1, "/").concat(key), value.binary())]; case 6: _j.sent(); _j.label = 7; case 7: _e = _d.next(); return [3 /*break*/, 5]; case 8: return [3 /*break*/, 11]; case 9: e_2_1 = _j.sent(); e_2 = { error: e_2_1 }; return [3 /*break*/, 11]; case 10: try { if (_e && !_e.done && (_h = _d.return)) _h.call(_d); } finally { if (e_2) throw e_2.error; } return [7 /*endfinally*/]; case 11: _b = _a.next(); return [3 /*break*/, 3]; case 12: return [3 /*break*/, 15]; case 13: e_3_1 = _j.sent(); e_3 = { error: e_3_1 }; return [3 /*break*/, 15]; case 14: try { if (_b && !_b.done && (_g = _a.return)) _g.call(_a); } finally { if (e_3) throw e_3.error; } return [7 /*endfinally*/]; case 15: return [2 /*return*/, result ? this.parseComparisonResult(result) : 'Sent']; } }); }); }; /** * Notifies the Touca server that all test cases were executed for this * version and no further test result is expected to be submitted. * Expected to be called by the test tool once all test cases are executed * and all test results are posted. * * Sealing the version is optional. The Touca server automatically * performs this operation once a certain amount of time has passed since * the last test case was submitted. This duration is configurable from * the "Settings" tab in "Suite" Page. * * @throws if called before calling `configure` or when called on a client * that is configured not to communicate with the Touca server or * if operation fails for any reason. * * @returns a promise that is resolved when all test results are submitted. */ NodeClient.prototype.seal = function () { return __awaiter(this, void 0, void 0, function () { var response; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.isConfigured(this._options) || this._options.offline) { throw new ToucaError('capture_not_configured'); } return [4 /*yield*/, this._transport.request('POST', "/batch/".concat(this._options.team, "/").concat(this._options.suite, "/").concat(this._options.version, "/seal"))]; case 1: response = _a.sent(); if (response.status == 403) { throw new ToucaError('auth_invalid_key'); } if (response.status !== 204) { throw new ToucaError('transport_seal'); } return [2 /*return*/]; } }); }); }; /** * High-level API designed to make writing regression test workflows easy * and straightforward. It abstracts away many of the common expected * features such as logging, error handling and progress reporting. * The following example demonstrates how to use this API. * * ```js * import { touca } from '@touca/node'; * import { find_student, calculate_gpa } from './code_under_test'; * * touca.workflow('test_students', (testcase: string) => { * const student = find_student(testcase); * touca.assume('username', student.username); * touca.check('fullname', student.fullname); * touca.check('birth_date', student.dob); * touca.check('gpa', calculate_gpa(student.courses)); * }); * * touca.run(); * ``` * * @param suite name of the workflow * @param callback test code to run for each test case * @param options options to pass to the test runner for this workflow */ NodeClient.prototype.workflow = function (suite, callback, options) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { this._workflows.push(__assign({ suite: suite, callback: callback }, options)); return [2 /*return*/]; }); }); }; /** * Runs the registered workflows. * * @param options configuration options to start with for all * registered workflows. */ NodeClient.prototype.run = function (options) { if (options === void 0) { options = {}; } return __awaiter(this, void 0, void 0, function () { var err_1, error; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: if (!options.workflows) { options.workflows = []; } (_a = options.workflows).push.apply(_a, __spreadArray([], __read(this._workflows), false)); _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); return [4 /*yield*/, run(options, this._transport, this)]; case 2: _b.sent(); process.exit(0); return [3 /*break*/, 4]; case 3: err_1 = _b.sent(); error = err_1 instanceof Error ? err_1.message : 'Unknown Error'; process.stderr.write(format('\nTest failed:\n%s\n', error)); process.exit(1); return [3 /*break*/, 4]; case 4: return [2 /*return*/]; } }); }); }; return NodeClient; }()); export { NodeClient }; //# sourceMappingURL=client.js.map