@testream/mocha-reporter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ /**
2
+ * "Buffered" reporter used internally by a worker process when running in parallel mode.
3
+ * @module nodejs/reporters/parallel-buffered
4
+ * @public
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ /**
10
+ * Module dependencies.
11
+ */
12
+
13
+ const {
14
+ EVENT_SUITE_BEGIN,
15
+ EVENT_SUITE_END,
16
+ EVENT_TEST_FAIL,
17
+ EVENT_TEST_PASS,
18
+ EVENT_TEST_PENDING,
19
+ EVENT_TEST_BEGIN,
20
+ EVENT_TEST_END,
21
+ EVENT_TEST_RETRY,
22
+ EVENT_DELAY_BEGIN,
23
+ EVENT_DELAY_END,
24
+ EVENT_HOOK_BEGIN,
25
+ EVENT_HOOK_END,
26
+ EVENT_RUN_END
27
+ } = require('../../runner').constants;
28
+ const {SerializableEvent, SerializableWorkerResult} = require('../serializer');
29
+ const debug = require('debug')('mocha:reporters:buffered');
30
+ const Base = require('../../reporters/base');
31
+
32
+ /**
33
+ * List of events to listen to; these will be buffered and sent
34
+ * when `Mocha#run` is complete (via {@link ParallelBuffered#done}).
35
+ */
36
+ const EVENT_NAMES = [
37
+ EVENT_SUITE_BEGIN,
38
+ EVENT_SUITE_END,
39
+ EVENT_TEST_BEGIN,
40
+ EVENT_TEST_PENDING,
41
+ EVENT_TEST_FAIL,
42
+ EVENT_TEST_PASS,
43
+ EVENT_TEST_RETRY,
44
+ EVENT_TEST_END,
45
+ EVENT_HOOK_BEGIN,
46
+ EVENT_HOOK_END
47
+ ];
48
+
49
+ /**
50
+ * Like {@link EVENT_NAMES}, except we expect these events to only be emitted
51
+ * by the `Runner` once.
52
+ */
53
+ const ONCE_EVENT_NAMES = [EVENT_DELAY_BEGIN, EVENT_DELAY_END];
54
+
55
+ /**
56
+ * The `ParallelBuffered` reporter is used by each worker process in "parallel"
57
+ * mode, by default. Instead of reporting to `STDOUT`, etc., it retains a
58
+ * list of events it receives and hands these off to the callback passed into
59
+ * {@link Mocha#run}. That callback will then return the data to the main
60
+ * process.
61
+ * @public
62
+ */
63
+ class ParallelBuffered extends Base {
64
+ /**
65
+ * Calls {@link ParallelBuffered#createListeners}
66
+ * @param {Runner} runner
67
+ */
68
+ constructor(runner, opts) {
69
+ super(runner, opts);
70
+
71
+ /**
72
+ * Retained list of events emitted from the {@link Runner} instance.
73
+ * @type {BufferedEvent[]}
74
+ * @public
75
+ */
76
+ this.events = [];
77
+
78
+ /**
79
+ * Map of `Runner` event names to listeners (for later teardown)
80
+ * @public
81
+ * @type {Map<string,EventListener>}
82
+ */
83
+ this.listeners = new Map();
84
+
85
+ this.createListeners(runner);
86
+ }
87
+
88
+ /**
89
+ * Returns a new listener which saves event data in memory to
90
+ * {@link ParallelBuffered#events}. Listeners are indexed by `eventName` and stored
91
+ * in {@link ParallelBuffered#listeners}. This is a defensive measure, so that we
92
+ * don't a) leak memory or b) remove _other_ listeners that may not be
93
+ * associated with this reporter.
94
+ *
95
+ * Subclasses could override this behavior.
96
+ *
97
+ * @public
98
+ * @param {string} eventName - Name of event to create listener for
99
+ * @returns {EventListener}
100
+ */
101
+ createListener(eventName) {
102
+ const listener = (runnable, err) => {
103
+ this.events.push(SerializableEvent.create(eventName, runnable, err));
104
+ };
105
+ return this.listeners.set(eventName, listener).get(eventName);
106
+ }
107
+
108
+ /**
109
+ * Creates event listeners (using {@link ParallelBuffered#createListener}) for each
110
+ * reporter-relevant event emitted by a {@link Runner}. This array is drained when
111
+ * {@link ParallelBuffered#done} is called by {@link Runner#run}.
112
+ *
113
+ * Subclasses could override this behavior.
114
+ * @public
115
+ * @param {Runner} runner - Runner instance
116
+ * @returns {ParallelBuffered}
117
+ * @chainable
118
+ */
119
+ createListeners(runner) {
120
+ EVENT_NAMES.forEach(evt => {
121
+ runner.on(evt, this.createListener(evt));
122
+ });
123
+ ONCE_EVENT_NAMES.forEach(evt => {
124
+ runner.once(evt, this.createListener(evt));
125
+ });
126
+
127
+ runner.once(EVENT_RUN_END, () => {
128
+ debug('received EVENT_RUN_END');
129
+ this.listeners.forEach((listener, evt) => {
130
+ runner.removeListener(evt, listener);
131
+ this.listeners.delete(evt);
132
+ });
133
+ });
134
+
135
+ return this;
136
+ }
137
+
138
+ /**
139
+ * Calls the {@link Mocha#run} callback (`callback`) with the test failure
140
+ * count and the array of {@link BufferedEvent} objects. Resets the array.
141
+ *
142
+ * This is called directly by `Runner#run` and should not be called by any other consumer.
143
+ *
144
+ * Subclasses could override this.
145
+ *
146
+ * @param {number} failures - Number of failed tests
147
+ * @param {Function} callback - The callback passed to {@link Mocha#run}.
148
+ * @public
149
+ */
150
+ done(failures, callback) {
151
+ callback(SerializableWorkerResult.create(this.events, failures));
152
+ this.events = []; // defensive
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Serializable event data from a `Runner`. Keys of the `data` property
158
+ * beginning with `__` will be converted into a function which returns the value
159
+ * upon deserialization.
160
+ * @typedef {Object} BufferedEvent
161
+ * @property {string} name - Event name
162
+ * @property {object} data - Event parameters
163
+ */
164
+
165
+ module.exports = ParallelBuffered;
@@ -0,0 +1,29 @@
1
+ import * as Mocha from 'mocha';
2
+ /**
3
+ * Testream Mocha Reporter
4
+ * Wraps mocha-ctrf-json-reporter to generate CTRF reports and upload them to Testream
5
+ */
6
+ export declare class TestreamMochaReporter extends Mocha.reporters.Base {
7
+ private config;
8
+ private readonly outputDir;
9
+ private readonly outputFile;
10
+ constructor(runner: Mocha.Runner, options: Mocha.MochaOptions);
11
+ /**
12
+ * Mocha calls done(failures, fn) when a reporter defines it — Mocha will
13
+ * wait for fn() before exiting, giving us time to finish the async upload.
14
+ */
15
+ done(failures: number, fn?: (failures: number) => void): void;
16
+ /**
17
+ * Called when entire test run completes (after mocha-ctrf-json-reporter has written its file)
18
+ */
19
+ private handleRunEnd;
20
+ /**
21
+ * Read CTRF report from file
22
+ */
23
+ private readCTRFReport;
24
+ /**
25
+ * Upload report to Testream API
26
+ */
27
+ private uploadReport;
28
+ }
29
+ export default TestreamMochaReporter;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Re-export CTRF types from shared-types package
3
+ */
4
+ export type { CTRFReport, CTRFTest, CTRFSummary, CTRFResults, CTRFAttachment, CTRFStep, } from '@jira-test-manager/shared-types';
5
+ /**
6
+ * Re-export API types from shared-types package
7
+ */
8
+ export type { IngestResponse, ApiErrorResponse, UploadResult, CIContext, } from '@jira-test-manager/shared-types';
9
+ /**
10
+ * Reporter Configuration Options
11
+ */
12
+ export interface MochaReporterConfig {
13
+ /**
14
+ * API key for authentication
15
+ * @required
16
+ */
17
+ apiKey: string;
18
+ /**
19
+ * Git branch name (auto-detected from CI environment if not provided)
20
+ */
21
+ branch?: string;
22
+ /**
23
+ * Git commit SHA (auto-detected from CI environment if not provided)
24
+ */
25
+ commitSha?: string;
26
+ /**
27
+ * Git repository URL (auto-detected from CI environment if not provided)
28
+ */
29
+ repositoryUrl?: string;
30
+ /**
31
+ * Enable/disable automatic upload to backend
32
+ * @default true
33
+ */
34
+ uploadEnabled?: boolean;
35
+ /**
36
+ * Fail the process if upload fails
37
+ * @default false
38
+ */
39
+ failOnUploadError?: boolean;
40
+ /**
41
+ * Test type identifier (e.g., 'unit', 'integration', 'e2e')
42
+ */
43
+ testType?: string;
44
+ /**
45
+ * Application name under test
46
+ */
47
+ appName?: string;
48
+ /**
49
+ * Application version under test
50
+ */
51
+ appVersion?: string;
52
+ /**
53
+ * Build name/identifier
54
+ */
55
+ buildName?: string;
56
+ /**
57
+ * Build number
58
+ */
59
+ buildNumber?: string;
60
+ /**
61
+ * Build URL (CI pipeline URL)
62
+ */
63
+ buildUrl?: string;
64
+ /**
65
+ * Test environment (e.g., 'staging', 'production', 'ci')
66
+ */
67
+ testEnvironment?: string;
68
+ }
@@ -0,0 +1,6 @@
1
+ import { UploadResult } from './types';
2
+ import { UploadTestRunOptions } from '@jira-test-manager/shared-types';
3
+ /**
4
+ * Upload CTRF report to Testream API
5
+ */
6
+ export declare function uploadToApi(options: UploadTestRunOptions): Promise<UploadResult>;
package/dist/worker.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * A worker process. Consumes {@link module:reporters/parallel-buffered} reporter.
3
+ * @module worker
4
+ * @private
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const {
10
+ createInvalidArgumentTypeError,
11
+ createInvalidArgumentValueError
12
+ } = require('../errors');
13
+ const workerpool = require('workerpool');
14
+ const Mocha = require('../mocha');
15
+ const {handleRequires, validateLegacyPlugin} = require('../cli/run-helpers');
16
+ const d = require('debug');
17
+ const debug = d.debug(`mocha:parallel:worker:${process.pid}`);
18
+ const isDebugEnabled = d.enabled(`mocha:parallel:worker:${process.pid}`);
19
+ const {serialize} = require('./serializer');
20
+ const {setInterval, clearInterval} = global;
21
+
22
+ let rootHooks;
23
+
24
+ if (workerpool.isMainThread) {
25
+ throw new Error(
26
+ 'This script is intended to be run as a worker (by the `workerpool` package).'
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Initializes some stuff on the first call to {@link run}.
32
+ *
33
+ * Handles `--require` and `--ui`. Does _not_ handle `--reporter`,
34
+ * as only the `Buffered` reporter is used.
35
+ *
36
+ * **This function only runs once per worker**; it overwrites itself with a no-op
37
+ * before returning.
38
+ *
39
+ * @param {Options} argv - Command-line options
40
+ */
41
+ let bootstrap = async argv => {
42
+ // globalSetup and globalTeardown do not run in workers
43
+ const plugins = await handleRequires(argv.require, {
44
+ ignoredPlugins: ['mochaGlobalSetup', 'mochaGlobalTeardown']
45
+ });
46
+ validateLegacyPlugin(argv, 'ui', Mocha.interfaces);
47
+
48
+ rootHooks = plugins.rootHooks;
49
+ bootstrap = () => {};
50
+ debug('bootstrap(): finished with args: %O', argv);
51
+ };
52
+
53
+ /**
54
+ * Runs a single test file in a worker thread.
55
+ * @param {string} filepath - Filepath of test file
56
+ * @param {string} [serializedOptions] - **Serialized** options. This string will be eval'd!
57
+ * @see https://npm.im/serialize-javascript
58
+ * @returns {Promise<{failures: number, events: BufferedEvent[]}>} - Test
59
+ * failure count and list of events.
60
+ */
61
+ async function run(filepath, serializedOptions = '{}') {
62
+ if (!filepath) {
63
+ throw createInvalidArgumentTypeError(
64
+ 'Expected a non-empty "filepath" argument',
65
+ 'file',
66
+ 'string'
67
+ );
68
+ }
69
+
70
+ debug('run(): running test file %s', filepath);
71
+
72
+ if (typeof serializedOptions !== 'string') {
73
+ throw createInvalidArgumentTypeError(
74
+ 'run() expects second parameter to be a string which was serialized by the `serialize-javascript` module',
75
+ 'serializedOptions',
76
+ 'string'
77
+ );
78
+ }
79
+ let argv;
80
+ try {
81
+ // eslint-disable-next-line no-eval
82
+ argv = eval('(' + serializedOptions + ')');
83
+ } catch (err) {
84
+ throw createInvalidArgumentValueError(
85
+ 'run() was unable to deserialize the options',
86
+ 'serializedOptions',
87
+ serializedOptions
88
+ );
89
+ }
90
+
91
+ const opts = Object.assign({ui: 'bdd'}, argv, {
92
+ // if this was true, it would cause infinite recursion.
93
+ parallel: false,
94
+ // this doesn't work in parallel mode
95
+ forbidOnly: true,
96
+ // it's useful for a Mocha instance to know if it's running in a worker process.
97
+ isWorker: true
98
+ });
99
+
100
+ await bootstrap(opts);
101
+
102
+ opts.rootHooks = rootHooks;
103
+
104
+ const mocha = new Mocha(opts).addFile(filepath);
105
+
106
+ try {
107
+ await mocha.loadFilesAsync();
108
+ } catch (err) {
109
+ debug('run(): could not load file %s: %s', filepath, err);
110
+ throw err;
111
+ }
112
+
113
+ return new Promise((resolve, reject) => {
114
+ let debugInterval;
115
+ /* istanbul ignore next */
116
+ if (isDebugEnabled) {
117
+ debugInterval = setInterval(() => {
118
+ debug('run(): still running %s...', filepath);
119
+ }, 5000).unref();
120
+ }
121
+ mocha.run(result => {
122
+ // Runner adds these; if we don't remove them, we'll get a leak.
123
+ process.removeAllListeners('uncaughtException');
124
+ process.removeAllListeners('unhandledRejection');
125
+
126
+ try {
127
+ const serialized = serialize(result);
128
+ debug(
129
+ 'run(): completed run with %d test failures; returning to main process',
130
+ typeof result.failures === 'number' ? result.failures : 0
131
+ );
132
+ resolve(serialized);
133
+ } catch (err) {
134
+ // TODO: figure out exactly what the sad path looks like here.
135
+ // rejection should only happen if an error is "unrecoverable"
136
+ debug('run(): serialization failed; rejecting: %O', err);
137
+ reject(err);
138
+ } finally {
139
+ clearInterval(debugInterval);
140
+ }
141
+ });
142
+ });
143
+ }
144
+
145
+ // this registers the `run` function.
146
+ workerpool.worker({run});
147
+
148
+ debug('started worker process');
149
+
150
+ // for testing
151
+ exports.run = run;
@@ -0,0 +1,263 @@
1
+ /**
2
+ * worker must be started as a child process or a web worker.
3
+ * It listens for RPC messages from the parent process.
4
+ */
5
+ var Transfer = require('./transfer');
6
+
7
+ // source of inspiration: https://github.com/sindresorhus/require-fool-webpack
8
+ var requireFoolWebpack = eval(
9
+ 'typeof require !== \'undefined\'' +
10
+ ' ? require' +
11
+ ' : function (module) { throw new Error(\'Module " + module + " not found.\') }'
12
+ );
13
+
14
+ /**
15
+ * Special message sent by parent which causes the worker to terminate itself.
16
+ * Not a "message object"; this string is the entire message.
17
+ */
18
+ var TERMINATE_METHOD_ID = '__workerpool-terminate__';
19
+
20
+ // var nodeOSPlatform = require('./environment').nodeOSPlatform;
21
+
22
+ // create a worker API for sending and receiving messages which works both on
23
+ // node.js and in the browser
24
+ var worker = {
25
+ exit: function() {}
26
+ };
27
+ if (typeof self !== 'undefined' && typeof postMessage === 'function' && typeof addEventListener === 'function') {
28
+ // worker in the browser
29
+ worker.on = function (event, callback) {
30
+ addEventListener(event, function (message) {
31
+ callback(message.data);
32
+ })
33
+ };
34
+ worker.send = function (message) {
35
+ postMessage(message);
36
+ };
37
+ }
38
+ else if (typeof process !== 'undefined') {
39
+ // node.js
40
+
41
+ var WorkerThreads;
42
+ try {
43
+ WorkerThreads = requireFoolWebpack('worker_threads');
44
+ } catch(error) {
45
+ if (typeof error === 'object' && error !== null && error.code === 'MODULE_NOT_FOUND') {
46
+ // no worker_threads, fallback to sub-process based workers
47
+ } else {
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ if (WorkerThreads &&
53
+ /* if there is a parentPort, we are in a WorkerThread */
54
+ WorkerThreads.parentPort !== null) {
55
+ var parentPort = WorkerThreads.parentPort;
56
+ worker.send = parentPort.postMessage.bind(parentPort);
57
+ worker.on = parentPort.on.bind(parentPort);
58
+ worker.exit = process.exit.bind(process);
59
+ } else {
60
+ worker.on = process.on.bind(process);
61
+ // ignore transfer argument since it is not supported by process
62
+ worker.send = function (message) {
63
+ process.send(message);
64
+ };
65
+ // register disconnect handler only for subprocess worker to exit when parent is killed unexpectedly
66
+ worker.on('disconnect', function () {
67
+ process.exit(1);
68
+ });
69
+ worker.exit = process.exit.bind(process);
70
+ }
71
+ }
72
+ else {
73
+ throw new Error('Script must be executed as a worker');
74
+ }
75
+
76
+ function convertError(error) {
77
+ return Object.getOwnPropertyNames(error).reduce(function(product, name) {
78
+ return Object.defineProperty(product, name, {
79
+ value: error[name],
80
+ enumerable: true
81
+ });
82
+ }, {});
83
+ }
84
+
85
+ /**
86
+ * Test whether a value is a Promise via duck typing.
87
+ * @param {*} value
88
+ * @returns {boolean} Returns true when given value is an object
89
+ * having functions `then` and `catch`.
90
+ */
91
+ function isPromise(value) {
92
+ return value && (typeof value.then === 'function') && (typeof value.catch === 'function');
93
+ }
94
+
95
+ // functions available externally
96
+ worker.methods = {};
97
+
98
+ /**
99
+ * Execute a function with provided arguments
100
+ * @param {String} fn Stringified function
101
+ * @param {Array} [args] Function arguments
102
+ * @returns {*}
103
+ */
104
+ worker.methods.run = function run(fn, args) {
105
+ var f = new Function('return (' + fn + ').apply(null, arguments);');
106
+ return f.apply(f, args);
107
+ };
108
+
109
+ /**
110
+ * Get a list with methods available on this worker
111
+ * @return {String[]} methods
112
+ */
113
+ worker.methods.methods = function methods() {
114
+ return Object.keys(worker.methods);
115
+ };
116
+
117
+ /**
118
+ * Custom handler for when the worker is terminated.
119
+ */
120
+ worker.terminationHandler = undefined;
121
+
122
+ /**
123
+ * Cleanup and exit the worker.
124
+ * @param {Number} code
125
+ * @returns
126
+ */
127
+ worker.cleanupAndExit = function(code) {
128
+ var _exit = function() {
129
+ worker.exit(code);
130
+ }
131
+
132
+ if(!worker.terminationHandler) {
133
+ return _exit();
134
+ }
135
+
136
+ var result = worker.terminationHandler(code);
137
+ if (isPromise(result)) {
138
+ result.then(_exit, _exit);
139
+ } else {
140
+ _exit();
141
+ }
142
+ }
143
+
144
+ var currentRequestId = null;
145
+
146
+ worker.on('message', function (request) {
147
+ if (request === TERMINATE_METHOD_ID) {
148
+ return worker.cleanupAndExit(0);
149
+ }
150
+ try {
151
+ var method = worker.methods[request.method];
152
+
153
+ if (method) {
154
+ currentRequestId = request.id;
155
+
156
+ // execute the function
157
+ var result = method.apply(method, request.params);
158
+
159
+ if (isPromise(result)) {
160
+ // promise returned, resolve this and then return
161
+ result
162
+ .then(function (result) {
163
+ if (result instanceof Transfer) {
164
+ worker.send({
165
+ id: request.id,
166
+ result: result.message,
167
+ error: null
168
+ }, result.transfer);
169
+ } else {
170
+ worker.send({
171
+ id: request.id,
172
+ result: result,
173
+ error: null
174
+ });
175
+ }
176
+ currentRequestId = null;
177
+ })
178
+ .catch(function (err) {
179
+ worker.send({
180
+ id: request.id,
181
+ result: null,
182
+ error: convertError(err)
183
+ });
184
+ currentRequestId = null;
185
+ });
186
+ }
187
+ else {
188
+ // immediate result
189
+ if (result instanceof Transfer) {
190
+ worker.send({
191
+ id: request.id,
192
+ result: result.message,
193
+ error: null
194
+ }, result.transfer);
195
+ } else {
196
+ worker.send({
197
+ id: request.id,
198
+ result: result,
199
+ error: null
200
+ });
201
+ }
202
+
203
+ currentRequestId = null;
204
+ }
205
+ }
206
+ else {
207
+ throw new Error('Unknown method "' + request.method + '"');
208
+ }
209
+ }
210
+ catch (err) {
211
+ worker.send({
212
+ id: request.id,
213
+ result: null,
214
+ error: convertError(err)
215
+ });
216
+ }
217
+ });
218
+
219
+ /**
220
+ * Register methods to the worker
221
+ * @param {Object} [methods]
222
+ * @param {WorkerRegisterOptions} [options]
223
+ */
224
+ worker.register = function (methods, options) {
225
+
226
+ if (methods) {
227
+ for (var name in methods) {
228
+ if (methods.hasOwnProperty(name)) {
229
+ worker.methods[name] = methods[name];
230
+ }
231
+ }
232
+ }
233
+
234
+ if (options) {
235
+ worker.terminationHandler = options.onTerminate;
236
+ }
237
+
238
+ worker.send('ready');
239
+ };
240
+
241
+ worker.emit = function (payload) {
242
+ if (currentRequestId) {
243
+ if (payload instanceof Transfer) {
244
+ worker.send({
245
+ id: currentRequestId,
246
+ isEvent: true,
247
+ payload: payload.message
248
+ }, payload.transfer);
249
+ return;
250
+ }
251
+
252
+ worker.send({
253
+ id: currentRequestId,
254
+ isEvent: true,
255
+ payload
256
+ });
257
+ }
258
+ };
259
+
260
+ if (typeof exports !== 'undefined') {
261
+ exports.add = worker.register;
262
+ exports.emit = worker.emit;
263
+ }