mocha-distributed 0.9.6 → 0.9.7

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/README.md CHANGED
@@ -173,7 +173,7 @@ Keep in mind that:
173
173
  You might have a look at list-tests-from-redis.js for an example on how to
174
174
  query redis and list all tests.
175
175
 
176
- ### Mark tests to run serially
176
+ ## Run tests serially
177
177
 
178
178
  If you'd like some of your tests to run serially you can use a magic string with
179
179
  this framework.
package/index.js CHANGED
@@ -34,6 +34,11 @@ let g_redis = null;
34
34
 
35
35
  let g_capture = { stdout: null, stderr: null };
36
36
 
37
+ // Cache errors from intermediate retry attempts. Mocha only sets test.err via
38
+ // the reporter on the final EVENT_TEST_FAIL; for non-final retries it emits
39
+ // EVENT_TEST_RETRY instead and never stores the error on the test object.
40
+ const g_retryErrors = new Map();
41
+
37
42
  // -----------------------------------------------------------------------------
38
43
  // getTestPath
39
44
  //
@@ -108,6 +113,11 @@ function captureStream(stream) {
108
113
  // Initialize redis once before the tests
109
114
  // -----------------------------------------------------------------------------
110
115
  exports.mochaGlobalSetup = async function () {
116
+ // `this` is the Mocha Runner — store errors from non-final retry attempts
117
+ // so afterEach can record them (Mocha never sets test.err for those).
118
+ this.on('retry', (test, err) => {
119
+ g_retryErrors.set(test.fullTitle(), err);
120
+ });
111
121
  if (g_mochaVerbose) {
112
122
  const redisNoCredentials = g_redisAddress.replace(
113
123
  /\/\/[^@]*@/,
@@ -183,7 +193,7 @@ exports.mochaHooks = {
183
193
  }
184
194
  },
185
195
 
186
- afterEach(done) {
196
+ afterEach: async function () {
187
197
  const SKIPPED = "pending";
188
198
  const FAILED = "failed";
189
199
  const PASSED = "passed";
@@ -227,7 +237,10 @@ exports.mochaHooks = {
227
237
  // Error objects cannot be properly serialized with stringify, thus
228
238
  // we need to use this hack to make it look like a normal object.
229
239
  // Hopefully this should work as well with other sort of objects
230
- const err = this.currentTest.err || null;
240
+ const err = this.currentTest.err
241
+ || g_retryErrors.get(this.currentTest.fullTitle())
242
+ || null;
243
+ g_retryErrors.delete(this.currentTest.fullTitle());
231
244
  const errObj = JSON.parse(
232
245
  JSON.stringify(err, Object.getOwnPropertyNames(err || {}))
233
246
  );
@@ -252,16 +265,16 @@ exports.mochaHooks = {
252
265
  };
253
266
 
254
267
  // save results as single line on purpose
255
- const key = `${g_testExecutionId}:test_result`;
256
- g_redis.rPush(key, JSON.stringify(testResult));
257
- g_redis.expire(key, g_expirationTime);
258
-
259
- // increment passed_count/failed_count & set expiry time
268
+ const resultKey = `${g_testExecutionId}:test_result`;
260
269
  const countKey = `${g_testExecutionId}:${stateFixed}_count`;
261
- g_redis.incr(countKey);
262
- g_redis.expire(countKey, g_expirationTime);
263
- }
264
270
 
265
- done();
271
+ await g_redis
272
+ .multi()
273
+ .rPush(resultKey, JSON.stringify(testResult))
274
+ .expire(resultKey, g_expirationTime)
275
+ .incr(countKey)
276
+ .expire(countKey, g_expirationTime)
277
+ .exec();
278
+ }
266
279
  },
267
280
  };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "mocha-distributed",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
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
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "test": "mocha test/test-retry.js"
8
8
  },
9
9
  "keywords": [
10
10
  "mocha",
@@ -0,0 +1,142 @@
1
+ // -----------------------------------------------------------------------------
2
+ // test/test-retry.js
3
+ //
4
+ // Mocha test suite verifying that mocha-distributed records err, stdout and
5
+ // stderr on ALL retry attempts, not just the last one.
6
+ //
7
+ // Run with: mocha test/test-retry.js
8
+ // -----------------------------------------------------------------------------
9
+ 'use strict';
10
+
11
+ const assert = require('assert');
12
+ const Mocha = require('mocha/lib/mocha');
13
+ const Suite = require('mocha/lib/suite');
14
+ const Test = require('mocha/lib/test');
15
+
16
+ // -----------------------------------------------------------------------------
17
+ // Mock redis helpers
18
+ // -----------------------------------------------------------------------------
19
+ const redisResolved = require.resolve('redis');
20
+
21
+ const mockMulti = (writtenResults) => () => {
22
+ const cmds = [];
23
+ const chain = {
24
+ set: (...a) => { cmds.push(['set', ...a]); return chain; },
25
+ get: (...a) => { cmds.push(['get', ...a]); return chain; },
26
+ rPush: (...a) => {
27
+ cmds.push(['rPush', ...a]);
28
+ writtenResults.push(JSON.parse(a[1]));
29
+ return chain;
30
+ },
31
+ expire: (...a) => { cmds.push(['expire', ...a]); return chain; },
32
+ incr: (...a) => { cmds.push(['incr', ...a]); return chain; },
33
+ exec: async () => {
34
+ // beforeEach pipeline: SET NX + GET — this runner always wins ownership
35
+ if (cmds[0] && cmds[0][0] === 'set') {
36
+ return [null, process.env.MOCHA_DISTRIBUTED_RUNNER_ID];
37
+ }
38
+ // afterEach pipeline: rPush + expire + incr + expire
39
+ return [1, 1, 1, 1];
40
+ }
41
+ };
42
+ return chain;
43
+ };
44
+
45
+ function injectMockRedis(writtenResults) {
46
+ require.cache[redisResolved] = {
47
+ id: redisResolved,
48
+ filename: redisResolved,
49
+ loaded: true,
50
+ exports: {
51
+ createClient: () => ({
52
+ on: () => {},
53
+ connect: async () => {},
54
+ quit: async () => {},
55
+ multi: mockMulti(writtenResults),
56
+ })
57
+ },
58
+ };
59
+ }
60
+
61
+ function restoreRedis() {
62
+ delete require.cache[redisResolved];
63
+ }
64
+
65
+ function loadFreshLib() {
66
+ const libPath = require.resolve('../index.js');
67
+ delete require.cache[libPath];
68
+ return require('../index.js');
69
+ }
70
+
71
+ // -----------------------------------------------------------------------------
72
+ // Tests
73
+ // -----------------------------------------------------------------------------
74
+ describe('mocha-distributed', function () {
75
+
76
+ describe('retry attempt recording', function () {
77
+ let writtenResults;
78
+ let lib;
79
+
80
+ before(function () {
81
+ writtenResults = [];
82
+ injectMockRedis(writtenResults);
83
+
84
+ process.env.MOCHA_DISTRIBUTED = 'redis://mock';
85
+ process.env.MOCHA_DISTRIBUTED_EXECUTION_ID = 'test-exec-retry';
86
+ process.env.MOCHA_DISTRIBUTED_RUNNER_ID = 'runner-test';
87
+
88
+ lib = loadFreshLib();
89
+ });
90
+
91
+ after(function () {
92
+ restoreRedis();
93
+ delete require.cache[require.resolve('../index.js')];
94
+ });
95
+
96
+ it('records err, stdout and stderr on every retry attempt', async function () {
97
+ this.timeout(10000);
98
+
99
+ // Build the inner mocha instance with a flaky test that fails on
100
+ // attempts 0 and 1, and passes on attempt 2
101
+ const m = new Mocha({ reporter: 'min' });
102
+ m.rootHooks(lib.mochaHooks);
103
+ m.globalSetup([lib.mochaGlobalSetup]);
104
+ m.globalTeardown([lib.mochaGlobalTeardown]);
105
+
106
+ const suite = Suite.create(m.suite, 'retry-suite');
107
+ suite.retries(2);
108
+
109
+ let attempt = 0;
110
+ suite.addTest(new Test('flaky-test', function () {
111
+ attempt++;
112
+ console.log('stdout from attempt ' + attempt);
113
+ console.error('stderr from attempt ' + attempt);
114
+ if (attempt < 3) throw new Error('intentional failure on attempt ' + attempt);
115
+ }));
116
+
117
+ await new Promise(resolve => m.run(resolve));
118
+
119
+ // Sort by retryAttempt for stable assertions
120
+ const results = writtenResults.slice().sort((a, b) => a.retryAttempt - b.retryAttempt);
121
+
122
+ assert.strictEqual(results.length, 3, '3 results written to Redis (one per attempt)');
123
+
124
+ // Attempts 0 and 1: failed, err populated, stdout and stderr present
125
+ for (const i of [0, 1]) {
126
+ const r = results[i];
127
+ assert.strictEqual(r.retryAttempt, i, `attempt ${i}: retryAttempt`);
128
+ assert.strictEqual(r.state, 'failed', `attempt ${i}: state`);
129
+ assert.ok(r.err && r.err.message, `attempt ${i}: err.message should be set`);
130
+ assert.ok(r.stdout.includes(`attempt ${i + 1}`), `attempt ${i}: stdout`);
131
+ assert.ok(r.stderr.includes(`attempt ${i + 1}`), `attempt ${i}: stderr`);
132
+ }
133
+
134
+ // Attempt 2: passed, stdout and stderr present
135
+ const r2 = results[2];
136
+ assert.strictEqual(r2.retryAttempt, 2, 'attempt 2: retryAttempt');
137
+ assert.strictEqual(r2.state, 'passed', 'attempt 2: state');
138
+ assert.ok(r2.stdout.includes('attempt 3'), 'attempt 2: stdout');
139
+ assert.ok(r2.stderr.includes('attempt 3'), 'attempt 2: stderr');
140
+ });
141
+ });
142
+ });