@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.
- package/LICENSE +30 -0
- package/README.md +22 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +14 -0
- package/dist/parallel-buffered.js +165 -0
- package/dist/reporter.d.ts +29 -0
- package/dist/types.d.ts +68 -0
- package/dist/uploader.d.ts +6 -0
- package/dist/worker.js +151 -0
- package/dist/worker1.js +263 -0
- package/package.json +63 -0
|
@@ -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;
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|
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;
|
package/dist/worker1.js
ADDED
|
@@ -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
|
+
}
|