@touca/node
Version:
Touca SDK for JavaScript
759 lines (758 loc) • 32.9 kB
JavaScript
// 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