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 +70 -10
- package/example/suite-5-serial.js +34 -0
- package/example/suite-6-serial.js +35 -0
- package/example/util.js +18 -1
- package/index.js +71 -24
- package/package.json +3 -3
- package/test-example-nworkers.sh +22 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# mocha-distributed
|
|
2
2
|
|
|
3
|
-
Run mocha tests
|
|
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
|
|
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
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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="
|
|
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** =
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
197
|
-
retryTotal:
|
|
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:
|
|
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.
|
|
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.
|
|
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!"
|