mocha-distributed 0.9.1 → 0.9.5
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 +18 -5
- package/example/suite-1.js +25 -1
- package/index.js +92 -5
- package/list-tests-from-redis.js +85 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -148,16 +148,29 @@ You will see something like this on each of the items of the list:
|
|
|
148
148
|
"type": "test",
|
|
149
149
|
"title": "test-1.1-async",
|
|
150
150
|
"timedOut": false,
|
|
151
|
+
"startTime": 1642705594300,
|
|
152
|
+
"endTime": 1642705594802,
|
|
151
153
|
"duration": 502,
|
|
152
154
|
"file": "/home/psanchez/github/mocha-distributed/example/suite-1.js",
|
|
153
155
|
"state": "passed",
|
|
156
|
+
"failed": false,
|
|
154
157
|
"speed": "slow",
|
|
155
158
|
"err": 0
|
|
156
|
-
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
157
161
|
|
|
158
162
|
The JSON formatting will differ since it is saved in a single line.
|
|
159
163
|
|
|
160
|
-
|
|
164
|
+
Keep in mind that:
|
|
165
|
+
|
|
166
|
+
* Duration and start/end times are in milliseconds.
|
|
167
|
+
* Some fields are duplicated in a way, like "state" and "failed" by design
|
|
168
|
+
because sometimes is handy to have this when reading results back.
|
|
169
|
+
* You can access test_result, passed_count and failed_count in redis
|
|
170
|
+
* Skipped tests are never saved in redis by design, unfortunately
|
|
171
|
+
|
|
172
|
+
You might have a look at list-tests-from-redis.js for an example on how to
|
|
173
|
+
query redis and list all tests.
|
|
161
174
|
|
|
162
175
|
## Examples
|
|
163
176
|
|
|
@@ -243,9 +256,9 @@ or have been skipped will return 0.
|
|
|
243
256
|
If you use jenkins, bamboo or any other build system, make sure
|
|
244
257
|
one redis is installed somewhere and all runners can access to it.
|
|
245
258
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
259
|
+
Create as many processes, nodes, dockers, kubernetes pods as you wish,
|
|
260
|
+
but for each of the runners that you create, make sure each of them can connect
|
|
261
|
+
to the redis instance (e.g are in the same network).
|
|
249
262
|
|
|
250
263
|
You can use the project name and build ID or job id as the execution ID for
|
|
251
264
|
mocha-distributed. Use something unique among the builds of all your projects.
|
package/example/suite-1.js
CHANGED
|
@@ -24,7 +24,7 @@ describe ('suite-1-async', async function () {
|
|
|
24
24
|
|
|
25
25
|
describe ('suite-1-sync', function () {
|
|
26
26
|
it ('test-1.1-async', async function () {
|
|
27
|
-
await util.sleep(
|
|
27
|
+
await util.sleep(1.5);
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
it ('test-1.2-sync', function () {
|
|
@@ -39,3 +39,27 @@ describe ('suite-1.2-sync', function () {
|
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
describe ('suite-1.3-io', function () {
|
|
44
|
+
it ('console.log', function () {
|
|
45
|
+
console.log ("Writing from console.log\nAnother line")
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it ('console.error', function () {
|
|
49
|
+
console.error ("Writing from console.error\nAnother line")
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it ('process.stdout', function () {
|
|
53
|
+
process.stdout.write("Writing from process.stdout. No newline.")
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it ('process.stderr', function () {
|
|
57
|
+
process.stderr.write("Writing from process.stderr. No newline.")
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it ('process.stdout & process.stderr', function () {
|
|
61
|
+
process.stdout.write("stdout output\nanother line")
|
|
62
|
+
process.stderr.write("stderr output\nanother line\nand yet another one.")
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
});
|
package/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// -----------------------------------------------------------------------------
|
|
2
|
+
// Copyright (c) 2018 Pau Sanchez
|
|
3
|
+
//
|
|
4
|
+
// MIT Licensed
|
|
5
|
+
// -----------------------------------------------------------------------------
|
|
1
6
|
const redis = require("redis");
|
|
2
7
|
const crypto = require("crypto");
|
|
3
8
|
|
|
@@ -27,6 +32,8 @@ if (g_granularity !== GRANULARITY.TEST) {
|
|
|
27
32
|
|
|
28
33
|
let g_redis = null;
|
|
29
34
|
|
|
35
|
+
let g_capture = { stdout: null, stderr: null };
|
|
36
|
+
|
|
30
37
|
// -----------------------------------------------------------------------------
|
|
31
38
|
// getTestPath
|
|
32
39
|
//
|
|
@@ -52,6 +59,28 @@ function getTestPath(testContext) {
|
|
|
52
59
|
return path.reverse();
|
|
53
60
|
}
|
|
54
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
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
// -----------------------------------------------------------------------------
|
|
56
85
|
// Initialize redis once before the tests
|
|
57
86
|
// -----------------------------------------------------------------------------
|
|
@@ -72,7 +101,7 @@ exports.mochaGlobalSetup = async function () {
|
|
|
72
101
|
}
|
|
73
102
|
|
|
74
103
|
if (!g_redisAddress || !g_testExecutionId) {
|
|
75
|
-
console.log
|
|
104
|
+
console.log(g_redisAddress, g_testExecutionId);
|
|
76
105
|
console.error(
|
|
77
106
|
"You need to set at least the following environment variables:\n" +
|
|
78
107
|
" - MOCHA_DISTRIBUTED\n" +
|
|
@@ -125,31 +154,89 @@ exports.mochaHooks = {
|
|
|
125
154
|
if (assignedRunnerId !== g_runnerId) {
|
|
126
155
|
this.currentTest.title += " (skipped by mocha_distributted)";
|
|
127
156
|
this.skip();
|
|
157
|
+
} else {
|
|
158
|
+
g_capture.stdout = captureStream(process.stdout);
|
|
159
|
+
g_capture.stderr = captureStream(process.stderr);
|
|
128
160
|
}
|
|
129
161
|
},
|
|
162
|
+
|
|
130
163
|
afterEach(done) {
|
|
131
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(
|
|
174
|
+
/\s*\u001b\[3[12]m[^\n]*\n$/g,
|
|
175
|
+
""
|
|
176
|
+
);
|
|
177
|
+
g_capture.stdout.unhook();
|
|
178
|
+
g_capture.stdout = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (g_capture.stderr) {
|
|
182
|
+
capturedStderr = g_capture.stderr.captured().join("");
|
|
183
|
+
g_capture.stderr.unhook();
|
|
184
|
+
g_capture.stderr = null;
|
|
185
|
+
}
|
|
132
186
|
|
|
133
187
|
// Save all data in redis in a way it can be retrieved and aggregated
|
|
134
188
|
// easily for all test by an external reporter
|
|
135
189
|
if (this.currentTest.state !== SKIPPED) {
|
|
190
|
+
const retryAttempt = this.currentTest._currentRetry || 0;
|
|
191
|
+
const retryTotal = this.currentTest._retries || 1;
|
|
192
|
+
|
|
193
|
+
// adjust state value accounting for exceptions, timeouts & retries
|
|
194
|
+
let stateFixed = PASSED;
|
|
195
|
+
if (
|
|
196
|
+
this.currentTest.state === FAILED ||
|
|
197
|
+
this.currentTest.timedOut ||
|
|
198
|
+
(typeof this.currentTest.state === "undefined" &&
|
|
199
|
+
retryAttempt < retryTotal)
|
|
200
|
+
) {
|
|
201
|
+
stateFixed = FAILED;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Error objects cannot be properly serialized with stringify, thus
|
|
205
|
+
// we need to use this hack to make it look like a normal object.
|
|
206
|
+
// Hopefully this should work as well with other sort of objects
|
|
207
|
+
const err = this.currentTest.err || null;
|
|
208
|
+
const errObj = JSON.parse(
|
|
209
|
+
JSON.stringify(err, Object.getOwnPropertyNames(err || {}))
|
|
210
|
+
);
|
|
211
|
+
|
|
136
212
|
const testResult = {
|
|
137
213
|
id: getTestPath(this.currentTest),
|
|
138
214
|
type: this.currentTest.type,
|
|
139
215
|
title: this.currentTest.title,
|
|
140
216
|
timedOut: this.currentTest.timedOut,
|
|
141
217
|
duration: this.currentTest.duration,
|
|
218
|
+
startTime: Date.now() - (this.currentTest.duration || 0),
|
|
219
|
+
endTime: Date.now(),
|
|
220
|
+
retryAttempt: retryAttempt,
|
|
221
|
+
retryTotal: retryTotal,
|
|
142
222
|
file: this.currentTest.file,
|
|
143
|
-
state:
|
|
223
|
+
state: stateFixed,
|
|
224
|
+
failed: stateFixed === FAILED,
|
|
144
225
|
speed: this.currentTest.speed,
|
|
145
|
-
err:
|
|
226
|
+
err: errObj,
|
|
227
|
+
stdout: capturedStdout,
|
|
228
|
+
stderr: capturedStderr,
|
|
146
229
|
};
|
|
147
230
|
|
|
148
|
-
// save as single line on purpose
|
|
231
|
+
// save results as single line on purpose
|
|
149
232
|
const key = `${g_testExecutionId}:test_result`;
|
|
150
233
|
g_redis.rPush(key, JSON.stringify(testResult));
|
|
151
234
|
g_redis.expire(key, g_expirationTime);
|
|
152
|
-
|
|
235
|
+
|
|
236
|
+
// increment passed_count/failed_count & set expiry time
|
|
237
|
+
const countKey = `${g_testExecutionId}:${stateFixed}_count`;
|
|
238
|
+
g_redis.incr(countKey);
|
|
239
|
+
g_redis.expire(countKey, g_expirationTime);
|
|
153
240
|
}
|
|
154
241
|
|
|
155
242
|
done();
|
|
@@ -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.9.
|
|
4
|
-
"description": "Run multiple mocha suites and tests in parallel, from different processes
|
|
3
|
+
"version": "0.9.5",
|
|
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
|
-
"redis": "^4.0.
|
|
31
|
+
"redis": "^4.0.3"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"chai": "^4.2.0",
|
|
34
|
-
"mocha": "^9.1.
|
|
35
|
+
"mocha": "^9.1.4"
|
|
35
36
|
}
|
|
36
37
|
}
|