mocha-distributed 0.9.4 → 0.9.6

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
@@ -1,6 +1,6 @@
1
1
  # mocha-distributed
2
2
 
3
- Run mocha tests faster.
3
+ Run mocha tests in parallel.
4
4
 
5
5
  Speed up your mocha tests by running them in parallel in multiple machines all
6
6
  at once without changing a single line of code. You only need a redis server.
@@ -12,15 +12,16 @@ tests without having to change any line of code, nor having to decide
12
12
  what to run where. Tests spread automatically according to the nodes you have.
13
13
 
14
14
  The concept is very simple, basically you spawn as many runners as you wish
15
- on as many nodes as you wish, and each node decides whether they should run
15
+ on as many nodes as you wish, and each process decides whether they should run
16
16
  a test or the test has already been executed or is being executed somewhere
17
17
  else.
18
18
 
19
- It does not matter if you run the tests in one machine as subprocesses or in
20
- many machines with multiple processes each.
19
+ It does not matter if you run the tests in one machine in multiple processes or
20
+ in multiple machines with multiple processes each. It will just work.
21
21
 
22
- Because you don't need to change a single line of code, which means that you
23
- can still run mocha locally as usual.
22
+ You don't need to change a single line of code, thus, this library it allows you
23
+ to continue developing tests as usual and launch them in parallel whenever you
24
+ want. No strings attached.
24
25
 
25
26
  ## Quick start
26
27
 
@@ -38,7 +39,7 @@ or machines where you want to run the tests on.
38
39
  Finally, on each of the runners just run:
39
40
 
40
41
  ```bash
41
- $ export MOCHA_DISTRIBUTED_EXECUTION_ID="execution__2021-01-01__20:10"
42
+ $ export MOCHA_DISTRIBUTED_EXECUTION_ID="execution__2024-01-01__20:10"
42
43
  $ export MOCHA_DISTRIBUTED="redis://redis.address"
43
44
  $ mocha --require mocha-distributed test/**/*.js
44
45
  ```
@@ -102,11 +103,11 @@ whether a test has already been executed or not by other of their peers.
102
103
  string in each machine, BUT you can override this in case you need it for
103
104
  whatever reason, although in theory you probably shouldn't.
104
105
 
105
- - **MOCHA_DISTRIBUTED_EXPIRATION_TIME** = 86400
106
+ - **MOCHA_DISTRIBUTED_EXPIRATION_TIME** = 604800
106
107
 
107
108
  Configures to how long the data is kept in redis before it expires (in
108
- seconds). The amount of data in redis is minimal, so you probably don't want
109
- to play with it.
109
+ seconds). 7 days is the default. The amount of data in redis is minimal,
110
+ so you probably don't want to play with it.
110
111
 
111
112
  It might be helpful to increase it though, if you want to build some sort of
112
113
  reporting on top of it, because you can directly explore test results in
@@ -172,6 +173,65 @@ Keep in mind that:
172
173
  You might have a look at list-tests-from-redis.js for an example on how to
173
174
  query redis and list all tests.
174
175
 
176
+ ### Mark tests to run serially
177
+
178
+ If you'd like some of your tests to run serially you can use a magic string with
179
+ this framework.
180
+
181
+ Simply add "[serial]" or "[serial-<ID OF YOUR CHOICE>]" to the title of your
182
+ test or test suite and all those tests will execute serially by the same runner.
183
+
184
+ The important part is that the test title contains "[serial" and ends with "]"
185
+
186
+ It's easier to explain with a couple of examples:
187
+
188
+ The following tests, regardless of whether they are on the same file or spreaded
189
+ in multiple files, will be executed all by the same runner one after another.
190
+
191
+ Might run in parallel to other tests that don't contain the "[serial]" word,
192
+ but will run sequentially for this group.
193
+
194
+ ```javascript
195
+ it('Test id 1 [serial]', function() { /* ... */})
196
+ it('Test id 2 [serial]', function() { /* ... */})
197
+ it('Test id 3 [serial]', function() { /* ... */})
198
+ it('Test id 4 [serial]', function() { /* ... */})
199
+ ```
200
+
201
+ See this other example below. Again, regardless of whether the tests are on the
202
+ same file or spreaded in multiple files, will be executed by two sets of
203
+ runners.
204
+
205
+ ```javascript
206
+ it('Test id 1 [serial-worker]', function() { /* ... */})
207
+ it('Test id 2 [serial-worker]', function() { /* ... */})
208
+ it('Test id 3 [serial-another worker]', function() { /* ... */})
209
+ it('Test id 4 [serial-another worker]', function() { /* ... */})
210
+ ```
211
+
212
+ Test 1 and 2 will be executed by one runner, whereas test 3 and 4 will be
213
+ executed by another. In both cases 1 and 2 will be executed sequentially and 3
214
+ and 4 also sequentially, but since they have different serial IDs, those two
215
+ subgroups of tests can run in parallel (e.g 1 and 2 in parallel with 3 and 4).
216
+
217
+ And now last example below:
218
+
219
+ ```javascript
220
+ describe('[serial-my test id] test multiple things sequentially', function () {
221
+ it('Test id 1', function() { /* ... */})
222
+ it('Test id 2', function() { /* ... */})
223
+ it('Test id 3', function() { /* ... */})
224
+ it('Test id 4', function() { /* ... */})
225
+ })
226
+ ```
227
+
228
+ The suite contains "[serial-my test id]", but the tests don't contain any serial
229
+ magic id. In this case, ALL those tests will run sequentially because the suite
230
+ contains the magic word.
231
+
232
+ Long story short. Add "[serial-whatever you want]" on the title but make sure
233
+ that "whatever you want" is the same for the stuff you want to run sequentially.
234
+
175
235
  ## Examples
176
236
 
177
237
  ### Environment-agnostic
@@ -0,0 +1,34 @@
1
+ const util = require('./util.js');
2
+
3
+ // require('mocha-distributed')
4
+ require('../index.js');
5
+
6
+ describe ('suite-5.serial', async function () {
7
+ beforeEach (function () {
8
+ // console.log ('before each');
9
+ });
10
+
11
+ it ('[serial] test-5.0', async function () {
12
+ await util.serialTestConcurrency('serial5')
13
+ });
14
+
15
+ it ('[serial:odd-worker] test-5.1', async function () {
16
+ await util.serialTestConcurrency('odd-worker')
17
+ });
18
+
19
+ it ('[serial:even-worker] test-5.2', async function () {
20
+ await util.serialTestConcurrency('even-worker')
21
+ });
22
+
23
+ it ('[serial:odd-worker] test-5.3', async function () {
24
+ await util.serialTestConcurrency('odd-worker')
25
+ });
26
+
27
+ it ('[serial:even-worker] test-5.4', async function () {
28
+ await util.serialTestConcurrency('even-worker')
29
+ });
30
+
31
+ it ('[serial] test-5.5', async function () {
32
+ await util.serialTestConcurrency('serial5')
33
+ });
34
+ });
@@ -0,0 +1,35 @@
1
+ const util = require('./util.js');
2
+
3
+ // require('mocha-distributed')
4
+ require('../index.js');
5
+
6
+ // NOTE: since "describe" has [serial] on it, all will be executed with that ID
7
+ describe ('[serial] suite-6.serial', async function () {
8
+ beforeEach (function () {
9
+ // console.log ('before each');
10
+ });
11
+
12
+ it ('[serial] test-6.0', async function () {
13
+ await util.serialTestConcurrency('serial6')
14
+ });
15
+
16
+ it ('[serial:odd-worker] test-6.1', async function () {
17
+ await util.serialTestConcurrency('serial6')
18
+ });
19
+
20
+ it ('[serial:even-worker] test-6.2', async function () {
21
+ await util.serialTestConcurrency('serial6')
22
+ });
23
+
24
+ it ('[serial:odd-worker] test-6.3', async function () {
25
+ await util.serialTestConcurrency('serial6')
26
+ });
27
+
28
+ it ('[serial:even-worker] test-6.4', async function () {
29
+ await util.serialTestConcurrency('serial6')
30
+ });
31
+
32
+ it ('[serial] test-6.5', async function () {
33
+ await util.serialTestConcurrency('serial6')
34
+ });
35
+ });
package/example/util.js CHANGED
@@ -1,3 +1,4 @@
1
+ const fs = require('fs');
1
2
  const util = require('./util.js');
2
3
 
3
4
  async function sleep (seconds) {
@@ -6,6 +7,22 @@ async function sleep (seconds) {
6
7
  });
7
8
  }
8
9
 
10
+ // -----------------------------------------------------------------------------
11
+ // Append data to given file name, wait for some time, and then continue
12
+ // adding end of data.
13
+ //
14
+ // This will easily make visible on the file whether two tests have been
15
+ // executed concurrently or not, since it will mess up the lines on the file
16
+ // -----------------------------------------------------------------------------
17
+ async function serialTestConcurrency(name) {
18
+ const fname = `tmp-${name}.tmp`
19
+
20
+ fs.appendFileSync(fname, `${name}: [Start: ${Date.now()} -> `)
21
+ await sleep(1);
22
+ fs.appendFileSync(fname, ` End:${Date.now()}]\n`)
23
+ }
24
+
9
25
  module.exports = {
10
- sleep
26
+ sleep,
27
+ serialTestConcurrency
11
28
  }
package/index.js CHANGED
@@ -15,7 +15,7 @@ const GRANULARITY = {
15
15
  const g_redisAddress = process.env.MOCHA_DISTRIBUTED || "";
16
16
  const g_testExecutionId = process.env.MOCHA_DISTRIBUTED_EXECUTION_ID || "";
17
17
  const g_expirationTime =
18
- process.env.MOCHA_DISTRIBUTED_EXPIRATION_TIME || `${24 * 3600}`;
18
+ process.env.MOCHA_DISTRIBUTED_EXPIRATION_TIME || `${7 * 24 * 3600}`;
19
19
 
20
20
  // Generate a unique random id for this runner (with almost 100% certainty
21
21
  // to be different on any machine/environment).
@@ -59,6 +59,29 @@ function getTestPath(testContext) {
59
59
  return path.reverse();
60
60
  }
61
61
 
62
+ // -----------------------------------------------------------------------------
63
+ // getSerialGranularity
64
+ //
65
+ // Returns the full string or the "serial string" which is whatever finds that
66
+ // follows this regex "[serial.*]" on the string. Only first instance is
67
+ // returned.
68
+ //
69
+ // This will allow serializing tests with given serial name.
70
+ // -----------------------------------------------------------------------------
71
+ function getSerialGranularity(testKey) {
72
+ // NOTE: a regular expression might be trickier to get right, since you can
73
+ // have multiple instances of [serialxxxx] on the same string
74
+ let index = testKey.indexOf('[serial')
75
+ if (index === -1)
76
+ return testKey
77
+
78
+ let index2 = testKey.indexOf(']', index)
79
+ if (index2 === -1)
80
+ return testKey
81
+
82
+ return testKey.substring(index, index2+1)
83
+ }
84
+
62
85
  // -----------------------------------------------------------------------------
63
86
  // captureStream
64
87
  // -----------------------------------------------------------------------------
@@ -66,18 +89,18 @@ function captureStream(stream) {
66
89
  var oldWrite = stream.write;
67
90
  var buf = [];
68
91
 
69
- stream.write = function(chunk, encoding, callback){
70
- buf.push (chunk.toString()); // chunk is a String or Buffer
92
+ stream.write = function (chunk, encoding, callback) {
93
+ buf.push(chunk.toString()); // chunk is a String or Buffer
71
94
  oldWrite.apply(stream, arguments);
72
- }
95
+ };
73
96
 
74
97
  return {
75
- unhook(){
76
- stream.write = oldWrite;
98
+ unhook() {
99
+ stream.write = oldWrite;
77
100
  },
78
- captured(){
101
+ captured() {
79
102
  return buf;
80
- }
103
+ },
81
104
  };
82
105
  }
83
106
 
@@ -101,7 +124,7 @@ exports.mochaGlobalSetup = async function () {
101
124
  }
102
125
 
103
126
  if (!g_redisAddress || !g_testExecutionId) {
104
- console.log (g_redisAddress, g_testExecutionId)
127
+ console.log(g_redisAddress, g_testExecutionId);
105
128
  console.error(
106
129
  "You need to set at least the following environment variables:\n" +
107
130
  " - MOCHA_DISTRIBUTED\n" +
@@ -137,8 +160,8 @@ exports.mochaGlobalTeardown = async function () {
137
160
  exports.mochaHooks = {
138
161
  beforeEach: async function () {
139
162
  const testPath = getTestPath(this.currentTest);
140
- const testKeyFullPath = `${g_testExecutionId}:${testPath.join(":")}`;
141
- const testKeySuite = `${g_testExecutionId}:${testPath[0]}`;
163
+ const testKeyFullPath = `${g_testExecutionId}:${getSerialGranularity(testPath.join(":"))}`;
164
+ const testKeySuite = `${g_testExecutionId}:${getSerialGranularity(testPath[0])}`;
142
165
 
143
166
  const testKey =
144
167
  g_granularity === GRANULARITY.TEST ? testKeyFullPath : testKeySuite;
@@ -154,29 +177,32 @@ exports.mochaHooks = {
154
177
  if (assignedRunnerId !== g_runnerId) {
155
178
  this.currentTest.title += " (skipped by mocha_distributted)";
156
179
  this.skip();
157
- }
158
- else {
180
+ } else {
159
181
  g_capture.stdout = captureStream(process.stdout);
160
182
  g_capture.stderr = captureStream(process.stderr);
161
183
  }
162
184
  },
185
+
163
186
  afterEach(done) {
164
187
  const SKIPPED = "pending";
165
188
  const FAILED = "failed";
166
189
  const PASSED = "passed";
167
190
 
168
- let capturedStdout = '';
169
- let capturedStderr = '';
191
+ let capturedStdout = "";
192
+ let capturedStderr = "";
170
193
  if (g_capture.stdout) {
171
194
  const stdoutArray = g_capture.stdout.captured();
172
- capturedStdout = stdoutArray.join('');
173
- capturedStdout = capturedStdout.replace(/\s*\u001b\[3[12]m[^\n]*\n$/g, '');
195
+ capturedStdout = stdoutArray.join("");
196
+ capturedStdout = capturedStdout.replace(
197
+ /\s*\u001b\[3[12]m[^\n]*\n$/g,
198
+ ""
199
+ );
174
200
  g_capture.stdout.unhook();
175
201
  g_capture.stdout = null;
176
202
  }
177
203
 
178
204
  if (g_capture.stderr) {
179
- capturedStderr = g_capture.stderr.captured().join('');
205
+ capturedStderr = g_capture.stderr.captured().join("");
180
206
  g_capture.stderr.unhook();
181
207
  g_capture.stderr = null;
182
208
  }
@@ -184,7 +210,28 @@ exports.mochaHooks = {
184
210
  // Save all data in redis in a way it can be retrieved and aggregated
185
211
  // easily for all test by an external reporter
186
212
  if (this.currentTest.state !== SKIPPED) {
187
- const stateFixed = this.currentTest.state || (this.currentTest.timedOut ? FAILED : PASSED)
213
+ const retryAttempt = this.currentTest._currentRetry || 0;
214
+ const retryTotal = this.currentTest._retries || 1;
215
+
216
+ // adjust state value accounting for exceptions, timeouts & retries
217
+ let stateFixed = PASSED;
218
+ if (
219
+ this.currentTest.state === FAILED ||
220
+ this.currentTest.timedOut ||
221
+ (typeof this.currentTest.state === "undefined" &&
222
+ retryAttempt < retryTotal)
223
+ ) {
224
+ stateFixed = FAILED;
225
+ }
226
+
227
+ // Error objects cannot be properly serialized with stringify, thus
228
+ // we need to use this hack to make it look like a normal object.
229
+ // Hopefully this should work as well with other sort of objects
230
+ const err = this.currentTest.err || null;
231
+ const errObj = JSON.parse(
232
+ JSON.stringify(err, Object.getOwnPropertyNames(err || {}))
233
+ );
234
+
188
235
  const testResult = {
189
236
  id: getTestPath(this.currentTest),
190
237
  type: this.currentTest.type,
@@ -193,15 +240,15 @@ exports.mochaHooks = {
193
240
  duration: this.currentTest.duration,
194
241
  startTime: Date.now() - (this.currentTest.duration || 0),
195
242
  endTime: Date.now(),
196
- retryAttempt: this.currentTest._currentRetry || 0,
197
- retryTotal: this.currentTest._retries || 1,
243
+ retryAttempt: retryAttempt,
244
+ retryTotal: retryTotal,
198
245
  file: this.currentTest.file,
199
246
  state: stateFixed,
200
247
  failed: stateFixed === FAILED,
201
248
  speed: this.currentTest.speed,
202
- err: this.currentTest.err || null,
249
+ err: errObj,
203
250
  stdout: capturedStdout,
204
- stderr: capturedStderr
251
+ stderr: capturedStderr,
205
252
  };
206
253
 
207
254
  // save results as single line on purpose
@@ -210,7 +257,7 @@ exports.mochaHooks = {
210
257
  g_redis.expire(key, g_expirationTime);
211
258
 
212
259
  // increment passed_count/failed_count & set expiry time
213
- const countKey = `${g_testExecutionId}:${stateFixed}_count`
260
+ const countKey = `${g_testExecutionId}:${stateFixed}_count`;
214
261
  g_redis.incr(countKey);
215
262
  g_redis.expire(countKey, g_expirationTime);
216
263
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocha-distributed",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
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": {
@@ -23,12 +23,12 @@
23
23
  ],
24
24
  "repository": {
25
25
  "type": "git",
26
- "url": "https://github.com/pausan/mocha-distributed.git"
26
+ "url": "git+https://github.com/pausan/mocha-distributed.git"
27
27
  },
28
28
  "author": "Pau Sanchez",
29
29
  "license": "MIT",
30
30
  "dependencies": {
31
- "redis": "^4.0.2"
31
+ "redis": "^4.0.3"
32
32
  },
33
33
  "devDependencies": {
34
34
  "chai": "^4.2.0",
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+ export MOCHA_DISTRIBUTED_EXECUTION_ID="eid_$(date +%s)"
3
+ export MOCHA_DISTRIBUTED="redis://127.0.0.1"
4
+
5
+ npm install > /dev/null 2>&1
6
+
7
+ N=$1
8
+ COMMAND="mocha --require ./index.js example/**/*.js"
9
+
10
+ # cleanup tmp files
11
+ rm tmp-*
12
+
13
+ echo "Spawning $N commands in parallel with Execution ID: $MOCHA_DISTRIBUTED_EXECUTION_ID"
14
+
15
+ # Run the command N times in the background, saving stdout/stderr
16
+ for ((i = 1; i <= N; i++)); do
17
+ $COMMAND >"tmp-output-$i.log" 2>&1 &
18
+ done
19
+
20
+ wait
21
+
22
+ echo "All tests finished!"