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 +1 -1
- package/index.js +24 -11
- package/package.json +2 -2
- package/test/test-retry.js +142 -0
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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
|
+
});
|