mocha-distributed 0.8.1 → 0.9.3

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/index.js CHANGED
@@ -1,49 +1,218 @@
1
1
  // -----------------------------------------------------------------------------
2
- // index.js
2
+ // Copyright (c) 2018 Pau Sanchez
3
3
  //
4
- // Copyright(c) 2019 Pau Sanchez - MIT License
4
+ // MIT Licensed
5
5
  // -----------------------------------------------------------------------------
6
+ const redis = require("redis");
7
+ const crypto = require("crypto");
6
8
 
7
- // Three scenarios for MOCHA_DISTRIBUTED:
8
- // - master (MOCHA_DISTRIBUTED = 'master[:PORT]')
9
- // Runs the server as master node, waiting for connections and waiting
10
- // others to run the tests. For simplicity, this node won't execute
11
- // any tests, but gather the result of all of them.
9
+ const GRANULARITY = {
10
+ TEST: "test",
11
+ SUITE: "suite",
12
+ };
13
+
14
+ // Initialize variables from environment
15
+ const g_redisAddress = process.env.MOCHA_DISTRIBUTED || "";
16
+ const g_testExecutionId = process.env.MOCHA_DISTRIBUTED_EXECUTION_ID || "";
17
+ const g_expirationTime =
18
+ process.env.MOCHA_DISTRIBUTED_EXPIRATION_TIME || `${24 * 3600}`;
19
+
20
+ // Generate a unique random id for this runner (with almost 100% certainty
21
+ // to be different on any machine/environment).
22
+ const _randomRunnerBuf = Buffer.alloc(16);
23
+ const _randomRunnerId = crypto.randomFillSync(_randomRunnerBuf).toString("hex");
24
+ const g_runnerId = process.env.MOCHA_DISTRIBUTED_RUNNER_ID || _randomRunnerId;
25
+ let g_granularity =
26
+ process.env.MOCHA_DISTRIBUTED_GRANULARITY || GRANULARITY.TEST;
27
+ const g_mochaVerbose = process.env.MOCHA_DISTRIBUTED_VERBOSE === "true";
28
+
29
+ if (g_granularity !== GRANULARITY.TEST) {
30
+ g_granularity = GRANULARITY.SUITE;
31
+ }
32
+
33
+ let g_redis = null;
34
+
35
+ let g_capture = { stdout: null, stderr: null };
36
+
37
+ // -----------------------------------------------------------------------------
38
+ // getTestPath
12
39
  //
13
- // - runner (MOCHA_DISTRIBUTED='<master-address>[:PORT]' IP/DNS of the master node)
14
- // IP/DNS of the master node (a.k.a runner), this node will get a unique
15
- // name from the master, and will ask, before each suite or orphaned test
16
- // if it needs to run it or not
40
+ // Returns an array with the test suites and test name from a test context
41
+ // as found in the hooks
17
42
  //
18
- // - Empty or not defined
19
- // Runs mocha normally, we don't redefine any variable at all.
20
- // Everything will run locally, if spawned on many machines or processes
21
- // tests will run on all of them as if this module did not exist.
22
- const master = require('./master.js');
23
- const runner = require('./runner.js');
24
-
25
- // Initialize mode & port from environment variable MOCHA_DISTRIBUTED
26
- const DEFAULT_PORT = 12421;
27
- let mode = (process.env.MOCHA_DISTRIBUTED || '').toLowerCase();
28
- let port = DEFAULT_PORT;
29
-
30
- if (mode.indexOf (':') >= 0) {
31
- const splitted = mode.split(':');
32
- mode = splitted[0];
33
- port = parseInt (splitted[1], 10);
34
- }
43
+ // Example:
44
+ //
45
+ // >>> getTestPath(ctxt)
46
+ //
47
+ // -----------------------------------------------------------------------------
48
+ function getTestPath(testContext) {
49
+ const path = [testContext.title];
35
50
 
36
- // let's get the party started
37
- if (!mode) {
38
- // run as normal mocha
39
- }
40
- else if (mode === 'master') {
41
- master (port);
51
+ while (!testContext.root && testContext.parent) {
52
+ testContext = testContext.parent;
53
+
54
+ if (testContext && !testContext.root) {
55
+ path.push(testContext.title);
56
+ }
57
+ }
58
+
59
+ return path.reverse();
42
60
  }
43
- else {
44
- const masterAddress = mode;
45
- runner (masterAddress, port);
61
+
62
+ // -----------------------------------------------------------------------------
63
+ // captureStream
64
+ // -----------------------------------------------------------------------------
65
+ function captureStream(stream) {
66
+ var oldWrite = stream.write;
67
+ var buf = [];
68
+
69
+ stream.write = function(chunk, encoding, callback){
70
+ buf.push (chunk.toString()); // chunk is a String or Buffer
71
+ oldWrite.apply(stream, arguments);
72
+ }
73
+
74
+ return {
75
+ unhook(){
76
+ stream.write = oldWrite;
77
+ },
78
+ captured(){
79
+ return buf;
80
+ }
81
+ };
46
82
  }
47
83
 
48
- // remove from cache, so it is always reinitialized
49
- delete require.cache[require.resolve('./index.js')];
84
+ // -----------------------------------------------------------------------------
85
+ // Initialize redis once before the tests
86
+ // -----------------------------------------------------------------------------
87
+ exports.mochaGlobalSetup = async function () {
88
+ if (g_mochaVerbose) {
89
+ const redisNoCredentials = g_redisAddress.replace(
90
+ /\/\/[^@]*@/,
91
+ "//***:***@"
92
+ );
93
+ console.log("---------------------------------------------------");
94
+ console.log(" Mocha Distributed");
95
+ console.log(" - Runner Id :", g_runnerId);
96
+ console.log(" - Redis Address :", redisNoCredentials);
97
+ console.log(" - Execution Id :", g_testExecutionId);
98
+ console.log(" - Data Expiration Time :", g_expirationTime);
99
+ console.log(" - Test Parallel Granularity:", g_granularity);
100
+ console.log("---------------------------------------------------");
101
+ }
102
+
103
+ if (!g_redisAddress || !g_testExecutionId) {
104
+ console.log (g_redisAddress, g_testExecutionId)
105
+ console.error(
106
+ "You need to set at least the following environment variables:\n" +
107
+ " - MOCHA_DISTRIBUTED\n" +
108
+ " - MOCHA_DISTRIBUTED_EXECUTION_ID\n"
109
+ );
110
+ process.exit(-1);
111
+ }
112
+
113
+ g_redis = redis.createClient({ url: g_redisAddress });
114
+ g_redis.on("error", (err) => {
115
+ console.log("Redis Client Error", err);
116
+ console.log("Closing application!");
117
+ process.exit(-1);
118
+ });
119
+ await g_redis.connect();
120
+ };
121
+
122
+ // -----------------------------------------------------------------------------
123
+ // Quit from redis
124
+ // -----------------------------------------------------------------------------
125
+ exports.mochaGlobalTeardown = async function () {
126
+ if (g_redis) {
127
+ await g_redis.quit();
128
+ }
129
+ };
130
+
131
+ // -----------------------------------------------------------------------------
132
+ // Hook tests
133
+ //
134
+ // Please note that we run skip before each test if the ownership of it has
135
+ // already been defined by another runner.
136
+ // -----------------------------------------------------------------------------
137
+ exports.mochaHooks = {
138
+ beforeEach: async function () {
139
+ const testPath = getTestPath(this.currentTest);
140
+ const testKeyFullPath = `${g_testExecutionId}:${testPath.join(":")}`;
141
+ const testKeySuite = `${g_testExecutionId}:${testPath[0]}`;
142
+
143
+ const testKey =
144
+ g_granularity === GRANULARITY.TEST ? testKeyFullPath : testKeySuite;
145
+
146
+ // Atomically set/get the runner id associated to this test. Only the first
147
+ // runner to get there will set the value to its own runner id.
148
+ const [_, assignedRunnerId] = await g_redis
149
+ .multi()
150
+ .set(testKey, g_runnerId, { EX: g_expirationTime, NX: true })
151
+ .get(testKey)
152
+ .exec();
153
+
154
+ if (assignedRunnerId !== g_runnerId) {
155
+ this.currentTest.title += " (skipped by mocha_distributted)";
156
+ this.skip();
157
+ }
158
+ else {
159
+ g_capture.stdout = captureStream(process.stdout);
160
+ g_capture.stderr = captureStream(process.stderr);
161
+ }
162
+ },
163
+ afterEach(done) {
164
+ const SKIPPED = "pending";
165
+ const FAILED = "failed";
166
+ const PASSED = "passed";
167
+
168
+ let capturedStdout = '';
169
+ let capturedStderr = '';
170
+ if (g_capture.stdout) {
171
+ const stdoutArray = g_capture.stdout.captured();
172
+ capturedStdout = stdoutArray.join('');
173
+ capturedStdout = capturedStdout.replace(/\s*\u001b\[3[12]m[^\n]*\n$/g, '');
174
+ g_capture.stdout.unhook();
175
+ g_capture.stdout = null;
176
+ }
177
+
178
+ if (g_capture.stderr) {
179
+ capturedStderr = g_capture.stderr.captured().join('');
180
+ g_capture.stderr.unhook();
181
+ g_capture.stderr = null;
182
+ }
183
+
184
+ // Save all data in redis in a way it can be retrieved and aggregated
185
+ // easily for all test by an external reporter
186
+ if (this.currentTest.state !== SKIPPED) {
187
+ const stateFixed = this.currentTest.state || (this.currentTest.timedOut ? FAILED : PASSED)
188
+ const testResult = {
189
+ id: getTestPath(this.currentTest),
190
+ type: this.currentTest.type,
191
+ title: this.currentTest.title,
192
+ timedOut: this.currentTest.timedOut,
193
+ duration: this.currentTest.duration,
194
+ startTime: Date.now() - (this.currentTest.duration || 0),
195
+ endTime: Date.now(),
196
+ file: this.currentTest.file,
197
+ state: stateFixed,
198
+ failed: stateFixed === FAILED,
199
+ speed: this.currentTest.speed,
200
+ err: this.currentTest.err || null,
201
+ stdout: capturedStdout,
202
+ stderr: capturedStderr
203
+ };
204
+
205
+ // save results as single line on purpose
206
+ const key = `${g_testExecutionId}:test_result`;
207
+ g_redis.rPush(key, JSON.stringify(testResult));
208
+ g_redis.expire(key, g_expirationTime);
209
+
210
+ // increment passed_count/failed_count & set expiry time
211
+ const countKey = `${g_testExecutionId}:${stateFixed}_count`
212
+ g_redis.incr(countKey);
213
+ g_redis.expire(countKey, g_expirationTime);
214
+ }
215
+
216
+ done();
217
+ },
218
+ };
@@ -0,0 +1,85 @@
1
+ // -----------------------------------------------------------------------------
2
+ // Copyright (c) 2018 Pau Sanchez
3
+ //
4
+ // MIT Licensed
5
+ // -----------------------------------------------------------------------------
6
+
7
+ const redis = require("redis");
8
+
9
+ async function getTestResults(redisHost = 'localhost', redisPort = 6379) {
10
+ const allResults = []
11
+ try {
12
+ const redisClient = redis.createClient({
13
+ url: `redis://${redisHost}:${redisPort}/`
14
+ });
15
+
16
+ await redisClient.connect()
17
+
18
+ // get all potential test results keys
19
+ const keyResults = await redisClient.keys('*:test_result')
20
+
21
+ // we can get all in parallel
22
+ const parallelResults = []
23
+ for(const key of keyResults) {
24
+ const rawResultList = await redisClient.lRange(key, 0, -1)
25
+
26
+ let passed = 0
27
+ let failed = 0
28
+ let aggregatedDurationMs = 0
29
+ let firstStartTime = null
30
+ let lastEndTime = null
31
+ const jsonResultList = []
32
+ for (const rawResult of rawResultList) {
33
+ const jsonResult = JSON.parse(rawResult)
34
+ jsonResultList.push(jsonResult)
35
+
36
+ // compute some extra stuff
37
+ if (firstStartTime === null || jsonResult.startTime < firstStartTime) {
38
+ firstStartTime = jsonResult.startTime
39
+ }
40
+
41
+ if (lastEndTime === null || jsonResult.endTime > lastEndTime) {
42
+ lastEndTime = jsonResult.endTime
43
+ }
44
+
45
+ passed += (jsonResult.state === "passed")
46
+ failed += (jsonResult.state === "failed")
47
+
48
+ aggregatedDurationMs += (jsonResult.duration || 0)
49
+ }
50
+
51
+ // sort all jsonResultList by the full test route, because they will
52
+ // be visualized better
53
+ jsonResultList.sort((a, b) => (a.id.join('/') > b.id.join('/')) ? 1 : -1)
54
+
55
+ allResults.push({
56
+ key: key,
57
+ start_time : firstStartTime || 0,
58
+ end_time : lastEndTime || 0,
59
+ aggregated_duration: aggregatedDurationMs,
60
+ real_duration: (firstStartTime && lastEndTime) ? lastEndTime - firstStartTime : aggregatedDurationMs,
61
+ tests_passed : passed,
62
+ tests_failed : failed,
63
+ test_results : jsonResultList
64
+ })
65
+ }
66
+
67
+ // sort results by startTime, most recent first
68
+ allResults.sort((a, b) => (a.start_time < b.start_time) ? 1 : -1)
69
+
70
+ await redisClient.quit()
71
+ return allResults
72
+ }
73
+ catch(e) {
74
+ console.error ("Test Error: ", e)
75
+ return false
76
+ }
77
+ }
78
+
79
+
80
+ // -----------------------------------------------------------------------------
81
+ // Print results as JSON output
82
+ // -----------------------------------------------------------------------------
83
+ getTestResults().then( (results) => {
84
+ console.log (JSON.stringify(results, null, 2))
85
+ })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mocha-distributed",
3
- "version": "0.8.1",
4
- "description": "Run multiple mocha suites and tests in parallel, from different processes or different machines",
3
+ "version": "0.9.3",
4
+ "description": "Run multiple mocha suites and tests in parallel, from different processes and different machines. Results available on a redis database.",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "test": "echo \"Error: no test specified\" && exit 1"
@@ -14,6 +14,7 @@
14
14
  "integration tests",
15
15
  "pipelines",
16
16
  "chai",
17
+ "expect",
17
18
  "kubernetes",
18
19
  "docker",
19
20
  "mocha-parallel-tests",
@@ -27,10 +28,10 @@
27
28
  "author": "Pau Sanchez",
28
29
  "license": "MIT",
29
30
  "dependencies": {
30
- "axios": "^0.18.0"
31
+ "redis": "^4.0.2"
31
32
  },
32
33
  "devDependencies": {
33
34
  "chai": "^4.2.0",
34
- "mocha": "^5.2.0"
35
+ "mocha": "^9.1.4"
35
36
  }
36
37
  }
package/constants.js DELETED
@@ -1,33 +0,0 @@
1
- // -----------------------------------------------------------------------------
2
- // constants.js
3
- //
4
- // Copyright(c) 2019 Pau Sanchez - MIT License
5
- // -----------------------------------------------------------------------------
6
-
7
- const RUNNER_ID_PREFIX = 'runner-';
8
-
9
- const TEST_STATUS_RUNNING = 'running';
10
- const TEST_STATUS_SUCCESS = 'success';
11
- const TEST_STATUS_FAILED = 'failed';
12
-
13
- const TEST_PATH_SEPARATOR = '>>>';
14
-
15
- const EVENT_FINISHED = 'finished';
16
-
17
- const ERRORS = {
18
- INVALID_REQUEST : 'INVALID_REQUEST',
19
- INVALID_TEST_ID : 'INVALID_TEST_ID',
20
- INVALID_REQUEST_ACTION : 'INVALID_REQUEST_ACTION',
21
- INVALID_RUNNER_OWNERSHIP : 'INVALID_RUNNER_OWNERSHIP',
22
- ALREADY_RUNNING : 'ALREADY_RUNNING',
23
- };
24
-
25
- module.exports = {
26
- RUNNER_ID_PREFIX,
27
- TEST_STATUS_RUNNING,
28
- TEST_STATUS_SUCCESS,
29
- TEST_STATUS_FAILED,
30
- TEST_PATH_SEPARATOR,
31
- EVENT_FINISHED,
32
- ERRORS
33
- };
@@ -1,159 +0,0 @@
1
- // -----------------------------------------------------------------------------
2
- // runner-mocha-bindings.js
3
- //
4
- // NOTE: mocha first does a first pass executing all describe/it/... and then
5
- // when it has gathered all information, it runs the functions associated
6
- // to each. What we do is we hook our method in the middle and call
7
- // or not, the original test method.
8
- //
9
- // Copyright(c) 2019 Pau Sanchez - MIT License
10
- // -----------------------------------------------------------------------------
11
- const constants = require('./constants.js');
12
-
13
- let g_mochaMethods = {
14
- describe : global.describe || null,
15
- it : global.it || null,
16
- before : global.before || null,
17
- beforeEach : global.beforeEach || null,
18
- after : global.after || null,
19
- afterEach : global.afterEach || null
20
- };
21
-
22
- // contains the actual test path such as suite.title > suite.title > it.title
23
- let g_testPath = [];
24
- let g_testResults = new Map();
25
- let g_testEventEmitter = null;
26
-
27
- // -----------------------------------------------------------------------------
28
- // setEventEmitter
29
- //
30
- // To communicate with the mocha bindings
31
- // -----------------------------------------------------------------------------
32
- function setEventEmitter (testEventEmitter) {
33
- g_testEventEmitter = testEventEmitter;
34
- }
35
-
36
- // -----------------------------------------------------------------------------
37
- // describe
38
- //
39
- // Custom version to describe a test, with same signature
40
- // -----------------------------------------------------------------------------
41
- async function describe (title, fn) {
42
- g_testPath.push (title);
43
-
44
- // TODO: handle timeouts!!
45
- const result = g_mochaMethods.describe (title, fn);
46
- g_testPath.pop();
47
-
48
- return result;
49
- }
50
-
51
- // TODO:
52
- // describe.skip = async function (title, fn) { }
53
- // describe.once = async function (title, fn) { }
54
-
55
- // -----------------------------------------------------------------------------
56
- // it
57
- // -----------------------------------------------------------------------------
58
- async function it (title, fn) {
59
- // store current path for when the 'it' function executes
60
- g_testPath.push (title);
61
- const testPath = g_testPath.slice(0);
62
- const testId = testPath.join(constants.TEST_PATH_SEPARATOR);
63
- const listenerId = constants.EVENT_FINISHED + ':' + testId;
64
- g_testPath.pop();
65
-
66
- // TODO: manage timeouts
67
-
68
- // Two listeners are created, once beforehand, and other in runtime.
69
- //
70
- // Runtime one will wait the master to receive the status... but since the
71
- // master node runs serially, it can also happen that another runner executes
72
- // the tests before the master can initialize the second event listener, and
73
- // we use this one to capture the result beforehand.
74
- g_testEventEmitter.once (
75
- listenerId,
76
- function (test) {
77
- g_testResults.set (test.id, test);
78
- }
79
- );
80
-
81
- // define our own function hook to wait until the test finishes
82
- return g_mochaMethods.it (title, async function () {
83
- await new Promise(function (resolve, reject) {
84
- // helper function to avoid repeating the code twice
85
- function helperProcessTestResult(test) {
86
- if (test.status === constants.TEST_STATUS_FAILED) {
87
- reject (test.error);
88
- return;
89
- }
90
-
91
- resolve();
92
- }
93
-
94
- if (g_testResults.has(testId)) {
95
- const test = g_testResults.get (testId);
96
- helperProcessTestResult (test);
97
- return;
98
- }
99
-
100
- // first event-listener has not triggered, so let's install another
101
- // event listener to resolve as quickly as possible
102
- g_testEventEmitter.once (listenerId, helperProcessTestResult);
103
- });
104
- });
105
- }
106
-
107
- // -----------------------------------------------------------------------------
108
- // execHookMochaMethod
109
- //
110
- // Method used accross all mocha hooks (before, beforeEach, after, afterEach)
111
- // in order to query the master server if it needs to be executed or not.
112
- // -----------------------------------------------------------------------------
113
- function execHookMochaMethod (title, orgMochaMethod, fn) {
114
- g_testPath.push (title);
115
- const testPath = g_testPath.slice(0);
116
- g_testPath.pop();
117
-
118
- return orgMochaMethod (async function () {
119
- // TODO: wait
120
- });
121
- }
122
-
123
- // -----------------------------------------------------------------------------
124
- // before
125
- // -----------------------------------------------------------------------------
126
- async function before (fn) {
127
- return execHookMochaMethod (':before', g_mochaMethods.before, fn);
128
- }
129
-
130
- // -----------------------------------------------------------------------------
131
- // beforeEach
132
- // -----------------------------------------------------------------------------
133
- async function beforeEach (fn) {
134
- return execHookMochaMethod (':beforeEach', g_mochaMethods.beforeEach, fn);
135
- }
136
-
137
- // -----------------------------------------------------------------------------
138
- // after
139
- // -----------------------------------------------------------------------------
140
- async function after (fn) {
141
- return execHookMochaMethod (':after', g_mochaMethods.after, fn);
142
- }
143
-
144
- // -----------------------------------------------------------------------------
145
- // afterEach
146
- // -----------------------------------------------------------------------------
147
- async function afterEach (fn) {
148
- return execHookMochaMethod (':afterEach', g_mochaMethods.after, fn);
149
- }
150
-
151
- module.exports = {
152
- setEventEmitter,
153
- describe,
154
- it,
155
- before,
156
- beforeEach,
157
- after,
158
- afterEach
159
- };