mocha-distributed 0.8.1 → 0.9.0
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 +186 -92
- package/docker-compose.yml +18 -0
- package/index.js +150 -42
- package/package.json +3 -3
- package/constants.js +0 -33
- package/master-mocha-bindings.js +0 -159
- package/master-server.js +0 -194
- package/master.js +0 -47
- package/runner-mocha-bindings.js +0 -212
- package/runner.js +0 -20
package/README.md
CHANGED
|
@@ -1,160 +1,254 @@
|
|
|
1
1
|
# mocha-distributed
|
|
2
2
|
|
|
3
|
+
Run mocha tests faster.
|
|
4
|
+
|
|
5
|
+
Speed up your mocha tests by running them in parallel in multiple machines all
|
|
6
|
+
at once without changing a single line of code. You only need a redis server.
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
3
10
|
The aim of this project is to provide a simple way of running distributed mocha
|
|
4
|
-
tests without having to change
|
|
5
|
-
what to run where.
|
|
11
|
+
tests without having to change any line of code, nor having to decide
|
|
12
|
+
what to run where. Tests spread automatically according to the nodes you have.
|
|
13
|
+
|
|
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
|
|
16
|
+
a test or the test has already been executed or is being executed somewhere
|
|
17
|
+
else.
|
|
6
18
|
|
|
7
19
|
It does not matter if you run the tests in one machine as subprocesses or in
|
|
8
20
|
many machines with multiple processes each.
|
|
9
21
|
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
12
24
|
|
|
13
|
-
|
|
14
|
-
without any side-effects locally, using mocha, as if nothing was changed.
|
|
25
|
+
## Quick start
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
You don't need to change a single line of code on your tests, this project uses
|
|
28
|
+
mocha hooks in order to work, so the only thing you'll need to do in preparation
|
|
29
|
+
is:
|
|
18
30
|
|
|
19
|
-
|
|
31
|
+
```bash
|
|
32
|
+
$ npm install -s mocha-distributed
|
|
33
|
+
```
|
|
20
34
|
|
|
21
|
-
|
|
35
|
+
Make sure you have a redis running somewhere with IP visibility from the machine
|
|
36
|
+
or machines where you want to run the tests on.
|
|
22
37
|
|
|
23
|
-
|
|
24
|
-
If you try to run the tests without this line, the tests will run on ALL machines,
|
|
25
|
-
including the master node.
|
|
38
|
+
Finally, on each of the runners just run:
|
|
26
39
|
|
|
27
|
-
```
|
|
28
|
-
|
|
40
|
+
```bash
|
|
41
|
+
$ export MOCHA_DISTRIBUTED_EXECUTION_ID="execution__2021-01-01__20:10"
|
|
42
|
+
$ export MOCHA_DISTRIBUTED="redis://redis.address"
|
|
43
|
+
$ mocha --require mocha-distributed test/**/*.js
|
|
29
44
|
```
|
|
30
45
|
|
|
31
|
-
|
|
46
|
+
There are several environment variables that allow you to control the behaviour
|
|
47
|
+
of distributed tests, but this is the simplest way to launch them.
|
|
32
48
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```
|
|
49
|
+
MOCHA_DISTRIBUTED is the one holding the redis address, this is the only
|
|
50
|
+
requirement to make mocha-distributed work.
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
MOCHA_DISTRIBUTED_EXECUTION_ID is the other variable you want to pay attention
|
|
53
|
+
to. Make sure you use a different value for each group of runners every time
|
|
54
|
+
you launch a test. This variable is what makes possible to make a runner know
|
|
55
|
+
whether a test has already been executed or not by other of their peers.
|
|
38
56
|
|
|
39
|
-
|
|
57
|
+
## Environment Variables
|
|
40
58
|
|
|
41
|
-
|
|
42
|
-
$ MOCHA_DISTRIBUTED="master" mocha test/**/*.js
|
|
43
|
-
```
|
|
59
|
+
- **MOCHA_DISTRIBUTED** (required)
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
Right now this variable is the one used to specify the node that will hold
|
|
62
|
+
information about tests being run. This project only supports redis right
|
|
63
|
+
now. This variable can take the form:
|
|
64
|
+
|
|
65
|
+
redis[s]://[[username][:password]@][host][:port]
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
$ MOCHA_DISTRIBUTED="1.2.3.4" mocha test/**/*.js
|
|
49
|
-
```
|
|
67
|
+
Please make sure it has visibility to the desired redis server.
|
|
50
68
|
|
|
69
|
+
- **MOCHA_DISTRIBUTED_EXECUTION_ID** (required)
|
|
51
70
|
|
|
71
|
+
Make sure this value is different every time you launch your tests. You can
|
|
72
|
+
use any string here, but it should be different across test executions or
|
|
73
|
+
your tests will just be skipped after the second execution.
|
|
52
74
|
|
|
53
|
-
|
|
75
|
+
Execution ID is used in order to differentiate different runs of the same
|
|
76
|
+
tests among parallel executions. If you launch 10 instances and you want
|
|
77
|
+
tests to be distributed among them, all need to have the same value for this
|
|
78
|
+
variable, otherwise each of them will run all the tests on its own.
|
|
54
79
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
80
|
+
Reusing this variable in different executions will cause your tests to be
|
|
81
|
+
skipped.
|
|
82
|
+
|
|
83
|
+
Use a random uuid or other random value, a kubernetes job_name, your
|
|
84
|
+
build system job id, ...
|
|
85
|
+
|
|
86
|
+
- **MOCHA_DISTRIBUTED_GRANULARITY** = test
|
|
87
|
+
|
|
88
|
+
- test (default)
|
|
89
|
+
Potentially all tests can be executed by any runner in any order. This
|
|
90
|
+
is the default, but if you have trouble running your tests in parallel
|
|
91
|
+
please use "suite" instead
|
|
92
|
+
|
|
93
|
+
- suite (safest)
|
|
94
|
+
|
|
95
|
+
Launch all tests from the same suite in the same runner. This prevents
|
|
96
|
+
some parallelization errors if your tests are not prepared for full
|
|
97
|
+
paralelization.
|
|
98
|
+
|
|
99
|
+
- **MOCHA_DISTRIBUTED_RUNNER_ID** = random-id
|
|
100
|
+
|
|
101
|
+
By default this value is initialized automatically with a different random
|
|
102
|
+
string in each machine, BUT you can override this in case you need it for
|
|
103
|
+
whatever reason, although in theory you probably shouldn't.
|
|
104
|
+
|
|
105
|
+
- **MOCHA_DISTRIBUTED_EXPIRATION_TIME** = 86400
|
|
106
|
+
|
|
107
|
+
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.
|
|
110
|
+
|
|
111
|
+
It might be helpful to increase it though, if you want to build some sort of
|
|
112
|
+
reporting on top of it, because you can directly explore test results in
|
|
113
|
+
redis. See Tests results in Redis for more info.
|
|
58
114
|
|
|
59
|
-
To distribute tests you only need to create several processess across one
|
|
60
|
-
or more machines (system won't care), and set one of them as the master,
|
|
61
|
-
and the rest as the runners. Only one master allowed.
|
|
62
115
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
116
|
+
- **MOCHA_DISTRIBUTED_VERBOSE** = false
|
|
117
|
+
- false (default)
|
|
118
|
+
Avoid printing verbose information
|
|
66
119
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
that
|
|
120
|
+
- true
|
|
121
|
+
Prints some extra information about the variables, the server, ...
|
|
122
|
+
that might be useful for debugging issues and/or informational.
|
|
70
123
|
|
|
71
|
-
|
|
72
|
-
will display the output of all tests.
|
|
124
|
+
## Reading test results from Redis
|
|
73
125
|
|
|
74
|
-
|
|
75
|
-
parameters. Using those parameters on the runners won't hurt either.
|
|
126
|
+
All runners write the test result in JSON format in a specific redis list.
|
|
76
127
|
|
|
77
|
-
|
|
128
|
+
The list is basically the execution ID from the variable
|
|
129
|
+
MOCHA_DISTRIBUTED_EXECUTION_ID concatenated to ':test_result'
|
|
78
130
|
|
|
79
|
-
|
|
131
|
+
For example, if you are using: MOCHA_DISTRIBUTED_EXECUTION_ID="abcdefg"
|
|
80
132
|
|
|
81
|
-
|
|
82
|
-
as if you would have not installed this module.
|
|
133
|
+
Then the key you should look at in redis will be "abcdefg:test_result"
|
|
83
134
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
135
|
+
You can access this list and explore the result of all tests. Each item
|
|
136
|
+
on the list will contain information about the test suite, test id, ...
|
|
137
|
+
test name, if it timed out or not, duration of the test, result of the test,
|
|
138
|
+
if there were any errors, ... all that info is extracted from mocha itself.
|
|
87
139
|
|
|
88
|
-
|
|
89
|
-
of the master computer that is running the test. If you are running all the
|
|
90
|
-
tests in multiple processes you can set it to 127.0.0.1, otherwise it should
|
|
91
|
-
be the IP of that machine in your private network, or the public IP if the
|
|
92
|
-
machines are distributed around the world.
|
|
140
|
+
You will see something like this on each of the items of the list:
|
|
93
141
|
|
|
94
|
-
|
|
95
|
-
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"id": [
|
|
145
|
+
"suite-1-async",
|
|
146
|
+
"test-1.1-async"
|
|
147
|
+
],
|
|
148
|
+
"type": "test",
|
|
149
|
+
"title": "test-1.1-async",
|
|
150
|
+
"timedOut": false,
|
|
151
|
+
"duration": 502,
|
|
152
|
+
"file": "/home/psanchez/github/mocha-distributed/example/suite-1.js",
|
|
153
|
+
"state": "passed",
|
|
154
|
+
"speed": "slow",
|
|
155
|
+
"err": 0
|
|
156
|
+
}```
|
|
157
|
+
|
|
158
|
+
The JSON formatting will differ since it is saved in a single line.
|
|
159
|
+
|
|
160
|
+
Apart of test_result you can also access passed_count and failed_count
|
|
96
161
|
|
|
97
162
|
## Examples
|
|
98
163
|
|
|
99
|
-
###
|
|
164
|
+
### Environment-agnostic
|
|
100
165
|
|
|
101
|
-
|
|
166
|
+
Make sure at least the following variables are set:
|
|
102
167
|
|
|
103
168
|
```bash
|
|
104
|
-
|
|
169
|
+
MOCHA_DISTRIBUTED="redis://1.2.3.4"
|
|
170
|
+
MOCHA_DISTRIBUTED_EXECUTION_ID="a5ce4d8a-5b06-4ec8-aea2-37d7e4b2ffe1"
|
|
105
171
|
```
|
|
106
172
|
|
|
107
|
-
|
|
173
|
+
Again, execution ID should be a different random number each time you want to
|
|
174
|
+
launch tests in parallel.
|
|
108
175
|
|
|
109
|
-
|
|
176
|
+
Example:
|
|
110
177
|
|
|
111
178
|
```bash
|
|
112
|
-
$
|
|
113
|
-
$ MOCHA_DISTRIBUTED="localhost" mocha test/**/*.js > /dev/null &
|
|
114
|
-
$ MOCHA_DISTRIBUTED="localhost" mocha test/**/*.js > /dev/null &
|
|
115
|
-
...
|
|
116
|
-
$ MOCHA_DISTRIBUTED="localhost" mocha test/**/*.js > /dev/null &
|
|
179
|
+
$ mocha --require mocha-distributed test/**/*.js
|
|
117
180
|
```
|
|
118
181
|
|
|
119
|
-
|
|
182
|
+
Of course, this assumes you have already installed mocha-distributed.
|
|
120
183
|
|
|
121
|
-
### Run tests in
|
|
184
|
+
### Run tests in parallel in the same machine
|
|
122
185
|
|
|
123
|
-
|
|
186
|
+
To keep things simple, do something like this:
|
|
124
187
|
|
|
125
188
|
```bash
|
|
126
|
-
$
|
|
189
|
+
$ MOCHA_DISTRIBUTED_EXECUTION_ID=`uuidgen`
|
|
190
|
+
$ MOCHA_DISTRIBUTED="redis://redis-server"
|
|
191
|
+
|
|
192
|
+
$ mocha --require mocha-distributed test/**/*.js > output01.txt &
|
|
193
|
+
$ mocha --require mocha-distributed test/**/*.js > output02.txt &
|
|
194
|
+
...
|
|
195
|
+
$ mocha --require mocha-distributed test/**/*.js > output0N.txt &
|
|
127
196
|
```
|
|
128
197
|
|
|
129
|
-
|
|
198
|
+
Run as many processes as you'd like.
|
|
130
199
|
|
|
131
|
-
|
|
132
|
-
IP address is 1.2.3.4. Now on the rest of machines, just do:
|
|
200
|
+
### Using kubernetes parallel jobs to launch tests
|
|
133
201
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
202
|
+
If you plan to use kubernetes to launch parallel jobs, make sure the backoff
|
|
203
|
+
limit is set to 1, so it does not retry the job after it fails, and make sure
|
|
204
|
+
you set execution ID to a different value each time (but common across all
|
|
205
|
+
parallel executions).
|
|
206
|
+
|
|
207
|
+
The easiest is to use the job ID (not the pod ID). You can do that by exposing
|
|
208
|
+
pod metadata information as environment variables.
|
|
209
|
+
|
|
210
|
+
See https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/
|
|
211
|
+
|
|
212
|
+
### Conceptual overview
|
|
213
|
+
|
|
214
|
+
The concept is very simple, this module hooks all mocha calls and does some magic
|
|
215
|
+
to allow running tests across machines without you having to decide what runs
|
|
216
|
+
where, or splitting tests beforehand, etc...
|
|
217
|
+
|
|
218
|
+
To distribute tests you only need to create several processess across one
|
|
219
|
+
or more machines (this method won't care how you spawn your runners), and either
|
|
220
|
+
set one of them as the master or use a redis database, and launch as many runners
|
|
221
|
+
as you wish.
|
|
222
|
+
|
|
223
|
+
Each runners connects to the redis instance and for each suite or test,
|
|
224
|
+
depending on the granularity, they ask whether they are the 'owners' to run the
|
|
225
|
+
tests on that suite or not. If they are, they run it. If they are not, they just
|
|
226
|
+
skip the tests and continue running the next suite/tests.
|
|
227
|
+
|
|
228
|
+
### Caveats
|
|
229
|
+
|
|
230
|
+
When running with redis, all tests are executed by independent runners, which
|
|
231
|
+
means you need to take a look at the output of all the runners and see which
|
|
232
|
+
ones were skipped and which ones were executed for you to see if some of those
|
|
233
|
+
executed failed.
|
|
140
234
|
|
|
141
|
-
|
|
235
|
+
Also the exit code of the different mocha runners will differ. The
|
|
236
|
+
ones whose tests fail, will return an error, and the ones whose tests work well
|
|
237
|
+
or have been skipped will return 0.
|
|
142
238
|
|
|
143
239
|
## Build systems
|
|
144
240
|
|
|
145
241
|
### jenkins, bamboo, circle-ci, gitlab, travis...
|
|
146
242
|
|
|
147
|
-
If you use jenkins, bamboo or any other build system,
|
|
148
|
-
|
|
243
|
+
If you use jenkins, bamboo or any other build system, make sure
|
|
244
|
+
one redis is installed somewhere and all runners can access to it.
|
|
149
245
|
|
|
150
|
-
You
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
make sure you can send a ping from all the runners to the master).
|
|
246
|
+
You can create as many processes processes or launch as docker or kubernetes
|
|
247
|
+
instances as you wish, but for each of the runners that you create, make sure
|
|
248
|
+
they have visibility to the redis.
|
|
154
249
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
inform the wrong master.
|
|
250
|
+
You can use the project name and build ID or job id as the execution ID for
|
|
251
|
+
mocha-distributed. Use something unique among the builds of all your projects.
|
|
158
252
|
|
|
159
253
|
## MIT License
|
|
160
254
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version: "3.7"
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
|
|
5
|
+
redis:
|
|
6
|
+
image: redis:latest
|
|
7
|
+
ports:
|
|
8
|
+
- 6379:6379
|
|
9
|
+
|
|
10
|
+
redis-commander:
|
|
11
|
+
image: rediscommander/redis-commander:latest
|
|
12
|
+
environment:
|
|
13
|
+
- REDIS_HOSTS=local:redis:6379
|
|
14
|
+
ports:
|
|
15
|
+
- 8081:8081
|
|
16
|
+
depends_on:
|
|
17
|
+
- redis
|
|
18
|
+
|
package/index.js
CHANGED
|
@@ -1,49 +1,157 @@
|
|
|
1
|
+
const redis = require("redis");
|
|
2
|
+
const crypto = require("crypto");
|
|
3
|
+
|
|
4
|
+
const GRANULARITY = {
|
|
5
|
+
TEST: "test",
|
|
6
|
+
SUITE: "suite",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Initialize variables from environment
|
|
10
|
+
const g_redisAddress = process.env.MOCHA_DISTRIBUTED || "";
|
|
11
|
+
const g_testExecutionId = process.env.MOCHA_DISTRIBUTED_EXECUTION_ID || "";
|
|
12
|
+
const g_expirationTime =
|
|
13
|
+
process.env.MOCHA_DISTRIBUTED_EXPIRATION_TIME || `${24 * 3600}`;
|
|
14
|
+
|
|
15
|
+
// Generate a unique random id for this runner (with almost 100% certainty
|
|
16
|
+
// to be different on any machine/environment).
|
|
17
|
+
const _randomRunnerBuf = Buffer.alloc(16);
|
|
18
|
+
const _randomRunnerId = crypto.randomFillSync(_randomRunnerBuf).toString("hex");
|
|
19
|
+
const g_runnerId = process.env.MOCHA_DISTRIBUTED_RUNNER_ID || _randomRunnerId;
|
|
20
|
+
let g_granularity =
|
|
21
|
+
process.env.MOCHA_DISTRIBUTED_GRANULARITY || GRANULARITY.TEST;
|
|
22
|
+
const g_mochaVerbose = process.env.MOCHA_DISTRIBUTED_VERBOSE === "true";
|
|
23
|
+
|
|
24
|
+
if (g_granularity !== GRANULARITY.TEST) {
|
|
25
|
+
g_granularity = GRANULARITY.SUITE;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let g_redis = null;
|
|
29
|
+
|
|
1
30
|
// -----------------------------------------------------------------------------
|
|
2
|
-
//
|
|
31
|
+
// getTestPath
|
|
3
32
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
// Three scenarios for MOCHA_DISTRIBUTED:
|
|
8
|
-
// - master (MOCHA_DISTRIBUTED = 'master[:PORT]')
|
|
9
|
-
// Runs the server as master node, waiting for connections and waiting
|
|
10
|
-
// others to run the tests. For simplicity, this node won't execute
|
|
11
|
-
// any tests, but gather the result of all of them.
|
|
33
|
+
// Returns an array with the test suites and test name from a test context
|
|
34
|
+
// as found in the hooks
|
|
12
35
|
//
|
|
13
|
-
//
|
|
14
|
-
// IP/DNS of the master node (a.k.a runner), this node will get a unique
|
|
15
|
-
// name from the master, and will ask, before each suite or orphaned test
|
|
16
|
-
// if it needs to run it or not
|
|
36
|
+
// Example:
|
|
17
37
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const runner = require('./runner.js');
|
|
24
|
-
|
|
25
|
-
// Initialize mode & port from environment variable MOCHA_DISTRIBUTED
|
|
26
|
-
const DEFAULT_PORT = 12421;
|
|
27
|
-
let mode = (process.env.MOCHA_DISTRIBUTED || '').toLowerCase();
|
|
28
|
-
let port = DEFAULT_PORT;
|
|
29
|
-
|
|
30
|
-
if (mode.indexOf (':') >= 0) {
|
|
31
|
-
const splitted = mode.split(':');
|
|
32
|
-
mode = splitted[0];
|
|
33
|
-
port = parseInt (splitted[1], 10);
|
|
34
|
-
}
|
|
38
|
+
// >>> getTestPath(ctxt)
|
|
39
|
+
//
|
|
40
|
+
// -----------------------------------------------------------------------------
|
|
41
|
+
function getTestPath(testContext) {
|
|
42
|
+
const path = [testContext.title];
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
runner (masterAddress, port);
|
|
44
|
+
while (!testContext.root && testContext.parent) {
|
|
45
|
+
testContext = testContext.parent;
|
|
46
|
+
|
|
47
|
+
if (testContext && !testContext.root) {
|
|
48
|
+
path.push(testContext.title);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return path.reverse();
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
//
|
|
49
|
-
|
|
55
|
+
// -----------------------------------------------------------------------------
|
|
56
|
+
// Initialize redis once before the tests
|
|
57
|
+
// -----------------------------------------------------------------------------
|
|
58
|
+
exports.mochaGlobalSetup = async function () {
|
|
59
|
+
if (g_mochaVerbose) {
|
|
60
|
+
const redisNoCredentials = g_redisAddress.replace(
|
|
61
|
+
/\/\/[^@]*@/,
|
|
62
|
+
"//***:***@"
|
|
63
|
+
);
|
|
64
|
+
console.log("---------------------------------------------------");
|
|
65
|
+
console.log(" Mocha Distributed");
|
|
66
|
+
console.log(" - Runner Id :", g_runnerId);
|
|
67
|
+
console.log(" - Redis Address :", redisNoCredentials);
|
|
68
|
+
console.log(" - Execution Id :", g_testExecutionId);
|
|
69
|
+
console.log(" - Data Expiration Time :", g_expirationTime);
|
|
70
|
+
console.log(" - Test Parallel Granularity:", g_granularity);
|
|
71
|
+
console.log("---------------------------------------------------");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!g_redisAddress || !g_testExecutionId) {
|
|
75
|
+
console.log (g_redisAddress, g_testExecutionId)
|
|
76
|
+
console.error(
|
|
77
|
+
"You need to set at least the following environment variables:\n" +
|
|
78
|
+
" - MOCHA_DISTRIBUTED\n" +
|
|
79
|
+
" - MOCHA_DISTRIBUTED_EXECUTION_ID\n"
|
|
80
|
+
);
|
|
81
|
+
process.exit(-1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
g_redis = redis.createClient({ url: g_redisAddress });
|
|
85
|
+
g_redis.on("error", (err) => {
|
|
86
|
+
console.log("Redis Client Error", err);
|
|
87
|
+
console.log("Closing application!");
|
|
88
|
+
process.exit(-1);
|
|
89
|
+
});
|
|
90
|
+
await g_redis.connect();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// -----------------------------------------------------------------------------
|
|
94
|
+
// Quit from redis
|
|
95
|
+
// -----------------------------------------------------------------------------
|
|
96
|
+
exports.mochaGlobalTeardown = async function () {
|
|
97
|
+
if (g_redis) {
|
|
98
|
+
await g_redis.quit();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// -----------------------------------------------------------------------------
|
|
103
|
+
// Hook tests
|
|
104
|
+
//
|
|
105
|
+
// Please note that we run skip before each test if the ownership of it has
|
|
106
|
+
// already been defined by another runner.
|
|
107
|
+
// -----------------------------------------------------------------------------
|
|
108
|
+
exports.mochaHooks = {
|
|
109
|
+
beforeEach: async function () {
|
|
110
|
+
const testPath = getTestPath(this.currentTest);
|
|
111
|
+
const testKeyFullPath = `${g_testExecutionId}:${testPath.join(":")}`;
|
|
112
|
+
const testKeySuite = `${g_testExecutionId}:${testPath[0]}`;
|
|
113
|
+
|
|
114
|
+
const testKey =
|
|
115
|
+
g_granularity === GRANULARITY.TEST ? testKeyFullPath : testKeySuite;
|
|
116
|
+
|
|
117
|
+
// Atomically set/get the runner id associated to this test. Only the first
|
|
118
|
+
// runner to get there will set the value to its own runner id.
|
|
119
|
+
const [_, assignedRunnerId] = await g_redis
|
|
120
|
+
.multi()
|
|
121
|
+
.set(testKey, g_runnerId, { EX: g_expirationTime, NX: true })
|
|
122
|
+
.get(testKey)
|
|
123
|
+
.exec();
|
|
124
|
+
|
|
125
|
+
if (assignedRunnerId !== g_runnerId) {
|
|
126
|
+
this.currentTest.title += " (skipped by mocha_distributted)";
|
|
127
|
+
this.skip();
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
afterEach(done) {
|
|
131
|
+
const SKIPPED = "pending";
|
|
132
|
+
|
|
133
|
+
// Save all data in redis in a way it can be retrieved and aggregated
|
|
134
|
+
// easily for all test by an external reporter
|
|
135
|
+
if (this.currentTest.state !== SKIPPED) {
|
|
136
|
+
const testResult = {
|
|
137
|
+
id: getTestPath(this.currentTest),
|
|
138
|
+
type: this.currentTest.type,
|
|
139
|
+
title: this.currentTest.title,
|
|
140
|
+
timedOut: this.currentTest.timedOut,
|
|
141
|
+
duration: this.currentTest.duration,
|
|
142
|
+
file: this.currentTest.file,
|
|
143
|
+
state: this.currentTest.state,
|
|
144
|
+
speed: this.currentTest.speed,
|
|
145
|
+
err: this.currentTest.err | null,
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// save as single line on purpose
|
|
149
|
+
const key = `${g_testExecutionId}:test_result`;
|
|
150
|
+
g_redis.rPush(key, JSON.stringify(testResult));
|
|
151
|
+
g_redis.expire(key, g_expirationTime);
|
|
152
|
+
g_redis.incr(`${g_testExecutionId}:${this.currentTest.state}_count`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
done();
|
|
156
|
+
},
|
|
157
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mocha-distributed",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Run multiple mocha suites and tests in parallel, from different processes or different machines",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"author": "Pau Sanchez",
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"
|
|
30
|
+
"redis": "^4.0.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"chai": "^4.2.0",
|
|
34
|
-
"mocha": "^
|
|
34
|
+
"mocha": "^9.1.3"
|
|
35
35
|
}
|
|
36
36
|
}
|
package/constants.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// -----------------------------------------------------------------------------
|
|
2
|
-
// constants.js
|
|
3
|
-
//
|
|
4
|
-
// Copyright(c) 2019 Pau Sanchez - MIT License
|
|
5
|
-
// -----------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
const RUNNER_ID_PREFIX = 'runner-';
|
|
8
|
-
|
|
9
|
-
const TEST_STATUS_RUNNING = 'running';
|
|
10
|
-
const TEST_STATUS_SUCCESS = 'success';
|
|
11
|
-
const TEST_STATUS_FAILED = 'failed';
|
|
12
|
-
|
|
13
|
-
const TEST_PATH_SEPARATOR = '>>>';
|
|
14
|
-
|
|
15
|
-
const EVENT_FINISHED = 'finished';
|
|
16
|
-
|
|
17
|
-
const ERRORS = {
|
|
18
|
-
INVALID_REQUEST : 'INVALID_REQUEST',
|
|
19
|
-
INVALID_TEST_ID : 'INVALID_TEST_ID',
|
|
20
|
-
INVALID_REQUEST_ACTION : 'INVALID_REQUEST_ACTION',
|
|
21
|
-
INVALID_RUNNER_OWNERSHIP : 'INVALID_RUNNER_OWNERSHIP',
|
|
22
|
-
ALREADY_RUNNING : 'ALREADY_RUNNING',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
module.exports = {
|
|
26
|
-
RUNNER_ID_PREFIX,
|
|
27
|
-
TEST_STATUS_RUNNING,
|
|
28
|
-
TEST_STATUS_SUCCESS,
|
|
29
|
-
TEST_STATUS_FAILED,
|
|
30
|
-
TEST_PATH_SEPARATOR,
|
|
31
|
-
EVENT_FINISHED,
|
|
32
|
-
ERRORS
|
|
33
|
-
};
|
package/master-mocha-bindings.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
// -----------------------------------------------------------------------------
|
|
2
|
-
// runner-mocha-bindings.js
|
|
3
|
-
//
|
|
4
|
-
// NOTE: mocha first does a first pass executing all describe/it/... and then
|
|
5
|
-
// when it has gathered all information, it runs the functions associated
|
|
6
|
-
// to each. What we do is we hook our method in the middle and call
|
|
7
|
-
// or not, the original test method.
|
|
8
|
-
//
|
|
9
|
-
// Copyright(c) 2019 Pau Sanchez - MIT License
|
|
10
|
-
// -----------------------------------------------------------------------------
|
|
11
|
-
const constants = require('./constants.js');
|
|
12
|
-
|
|
13
|
-
let g_mochaMethods = {
|
|
14
|
-
describe : global.describe || null,
|
|
15
|
-
it : global.it || null,
|
|
16
|
-
before : global.before || null,
|
|
17
|
-
beforeEach : global.beforeEach || null,
|
|
18
|
-
after : global.after || null,
|
|
19
|
-
afterEach : global.afterEach || null
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// contains the actual test path such as suite.title > suite.title > it.title
|
|
23
|
-
let g_testPath = [];
|
|
24
|
-
let g_testResults = new Map();
|
|
25
|
-
let g_testEventEmitter = null;
|
|
26
|
-
|
|
27
|
-
// -----------------------------------------------------------------------------
|
|
28
|
-
// setEventEmitter
|
|
29
|
-
//
|
|
30
|
-
// To communicate with the mocha bindings
|
|
31
|
-
// -----------------------------------------------------------------------------
|
|
32
|
-
function setEventEmitter (testEventEmitter) {
|
|
33
|
-
g_testEventEmitter = testEventEmitter;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// -----------------------------------------------------------------------------
|
|
37
|
-
// describe
|
|
38
|
-
//
|
|
39
|
-
// Custom version to describe a test, with same signature
|
|
40
|
-
// -----------------------------------------------------------------------------
|
|
41
|
-
async function describe (title, fn) {
|
|
42
|
-
g_testPath.push (title);
|
|
43
|
-
|
|
44
|
-
// TODO: handle timeouts!!
|
|
45
|
-
const result = g_mochaMethods.describe (title, fn);
|
|
46
|
-
g_testPath.pop();
|
|
47
|
-
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// TODO:
|
|
52
|
-
// describe.skip = async function (title, fn) { }
|
|
53
|
-
// describe.once = async function (title, fn) { }
|
|
54
|
-
|
|
55
|
-
// -----------------------------------------------------------------------------
|
|
56
|
-
// it
|
|
57
|
-
// -----------------------------------------------------------------------------
|
|
58
|
-
async function it (title, fn) {
|
|
59
|
-
// store current path for when the 'it' function executes
|
|
60
|
-
g_testPath.push (title);
|
|
61
|
-
const testPath = g_testPath.slice(0);
|
|
62
|
-
const testId = testPath.join(constants.TEST_PATH_SEPARATOR);
|
|
63
|
-
const listenerId = constants.EVENT_FINISHED + ':' + testId;
|
|
64
|
-
g_testPath.pop();
|
|
65
|
-
|
|
66
|
-
// TODO: manage timeouts
|
|
67
|
-
|
|
68
|
-
// Two listeners are created, once beforehand, and other in runtime.
|
|
69
|
-
//
|
|
70
|
-
// Runtime one will wait the master to receive the status... but since the
|
|
71
|
-
// master node runs serially, it can also happen that another runner executes
|
|
72
|
-
// the tests before the master can initialize the second event listener, and
|
|
73
|
-
// we use this one to capture the result beforehand.
|
|
74
|
-
g_testEventEmitter.once (
|
|
75
|
-
listenerId,
|
|
76
|
-
function (test) {
|
|
77
|
-
g_testResults.set (test.id, test);
|
|
78
|
-
}
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
// define our own function hook to wait until the test finishes
|
|
82
|
-
return g_mochaMethods.it (title, async function () {
|
|
83
|
-
await new Promise(function (resolve, reject) {
|
|
84
|
-
// helper function to avoid repeating the code twice
|
|
85
|
-
function helperProcessTestResult(test) {
|
|
86
|
-
if (test.status === constants.TEST_STATUS_FAILED) {
|
|
87
|
-
reject (test.error);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
resolve();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (g_testResults.has(testId)) {
|
|
95
|
-
const test = g_testResults.get (testId);
|
|
96
|
-
helperProcessTestResult (test);
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// first event-listener has not triggered, so let's install another
|
|
101
|
-
// event listener to resolve as quickly as possible
|
|
102
|
-
g_testEventEmitter.once (listenerId, helperProcessTestResult);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// -----------------------------------------------------------------------------
|
|
108
|
-
// execHookMochaMethod
|
|
109
|
-
//
|
|
110
|
-
// Method used accross all mocha hooks (before, beforeEach, after, afterEach)
|
|
111
|
-
// in order to query the master server if it needs to be executed or not.
|
|
112
|
-
// -----------------------------------------------------------------------------
|
|
113
|
-
function execHookMochaMethod (title, orgMochaMethod, fn) {
|
|
114
|
-
g_testPath.push (title);
|
|
115
|
-
const testPath = g_testPath.slice(0);
|
|
116
|
-
g_testPath.pop();
|
|
117
|
-
|
|
118
|
-
return orgMochaMethod (async function () {
|
|
119
|
-
// TODO: wait
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// -----------------------------------------------------------------------------
|
|
124
|
-
// before
|
|
125
|
-
// -----------------------------------------------------------------------------
|
|
126
|
-
async function before (fn) {
|
|
127
|
-
return execHookMochaMethod (':before', g_mochaMethods.before, fn);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// -----------------------------------------------------------------------------
|
|
131
|
-
// beforeEach
|
|
132
|
-
// -----------------------------------------------------------------------------
|
|
133
|
-
async function beforeEach (fn) {
|
|
134
|
-
return execHookMochaMethod (':beforeEach', g_mochaMethods.beforeEach, fn);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// -----------------------------------------------------------------------------
|
|
138
|
-
// after
|
|
139
|
-
// -----------------------------------------------------------------------------
|
|
140
|
-
async function after (fn) {
|
|
141
|
-
return execHookMochaMethod (':after', g_mochaMethods.after, fn);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// -----------------------------------------------------------------------------
|
|
145
|
-
// afterEach
|
|
146
|
-
// -----------------------------------------------------------------------------
|
|
147
|
-
async function afterEach (fn) {
|
|
148
|
-
return execHookMochaMethod (':afterEach', g_mochaMethods.after, fn);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
module.exports = {
|
|
152
|
-
setEventEmitter,
|
|
153
|
-
describe,
|
|
154
|
-
it,
|
|
155
|
-
before,
|
|
156
|
-
beforeEach,
|
|
157
|
-
after,
|
|
158
|
-
afterEach
|
|
159
|
-
};
|
package/master-server.js
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
// -----------------------------------------------------------------------------
|
|
2
|
-
// master-mocha-bindings.js
|
|
3
|
-
//
|
|
4
|
-
// Copyright(c) 2019 Pau Sanchez - MIT License
|
|
5
|
-
// -----------------------------------------------------------------------------
|
|
6
|
-
const url = require('url');
|
|
7
|
-
const querystring = require('querystring');
|
|
8
|
-
const constants = require('./constants.js');
|
|
9
|
-
|
|
10
|
-
let g_testsPerRunner = new Map();
|
|
11
|
-
let g_testEventEmitter = null;
|
|
12
|
-
|
|
13
|
-
// -----------------------------------------------------------------------------
|
|
14
|
-
// setEventEmitter
|
|
15
|
-
//
|
|
16
|
-
// To communicate with the mocha bindings
|
|
17
|
-
// -----------------------------------------------------------------------------
|
|
18
|
-
function setEventEmitter (testEventEmitter) {
|
|
19
|
-
g_testEventEmitter = testEventEmitter;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// -----------------------------------------------------------------------------
|
|
23
|
-
// handleRunnerShouldRun
|
|
24
|
-
//
|
|
25
|
-
// REST: /runner/<runner-id>/should-run?test=xxxxx
|
|
26
|
-
//
|
|
27
|
-
// Asks whether the client should run given test
|
|
28
|
-
// -----------------------------------------------------------------------------
|
|
29
|
-
function handleRunnerShouldRun (req, res) {
|
|
30
|
-
const queryObj = querystring.parse(req.route.query);
|
|
31
|
-
const splitted = req.route.pathname.split('/');
|
|
32
|
-
const runnerId = splitted[2];
|
|
33
|
-
const action = splitted[3];
|
|
34
|
-
|
|
35
|
-
const testId = queryObj.test || null;
|
|
36
|
-
|
|
37
|
-
// is this test already assigned to somebody else?
|
|
38
|
-
if (!testId) {
|
|
39
|
-
res.write (
|
|
40
|
-
JSON.stringify ({
|
|
41
|
-
answer : 'skip',
|
|
42
|
-
reason : constants.ERRORS.INVALID_TEST_ID
|
|
43
|
-
})
|
|
44
|
-
)
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (g_testsPerRunner.has(testId)) {
|
|
49
|
-
const runner = g_testsPerRunner.get (testId).runner;
|
|
50
|
-
|
|
51
|
-
// is it a retry? e.g. same runner...
|
|
52
|
-
if (runnerId === runner) {
|
|
53
|
-
g_testsPerRunner.get (testId).retries ++;
|
|
54
|
-
res.write (JSON.stringify ({ answer : 'run' }));
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// otherwise, it is already running :)
|
|
59
|
-
res.write (
|
|
60
|
-
JSON.stringify ({
|
|
61
|
-
answer : 'skip',
|
|
62
|
-
reason : constants.ERRORS.ALREADY_RUNNING,
|
|
63
|
-
runner : runner
|
|
64
|
-
})
|
|
65
|
-
);
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// make this runner the owner of running this test
|
|
70
|
-
g_testsPerRunner.set (testId, {
|
|
71
|
-
id : testId,
|
|
72
|
-
runner : runnerId,
|
|
73
|
-
status : constants.TEST_STATUS_RUNNING,
|
|
74
|
-
retries: 0,
|
|
75
|
-
start : Date.now(),
|
|
76
|
-
end : false,
|
|
77
|
-
error : null
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
res.write (
|
|
81
|
-
JSON.stringify ({
|
|
82
|
-
answer : 'run'
|
|
83
|
-
})
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// -----------------------------------------------------------------------------
|
|
88
|
-
// handleRunnerResult
|
|
89
|
-
//
|
|
90
|
-
// REST: /runner/<runner-id>/result?test=xxxxx&status=success|error
|
|
91
|
-
//
|
|
92
|
-
// Informs the result of running given test
|
|
93
|
-
// -----------------------------------------------------------------------------
|
|
94
|
-
function handleRunnerResult (req, res) {
|
|
95
|
-
const queryObj = querystring.parse(req.route.query);
|
|
96
|
-
const splitted = req.route.pathname.split('/');
|
|
97
|
-
const runnerId = splitted[2];
|
|
98
|
-
const action = splitted[3];
|
|
99
|
-
|
|
100
|
-
const testId = queryObj.test || null;
|
|
101
|
-
if (!testId || !g_testsPerRunner.has(testId)) {
|
|
102
|
-
res.write (
|
|
103
|
-
JSON.stringify ({
|
|
104
|
-
error : constants.ERRORS.INVALID_TEST_ID
|
|
105
|
-
})
|
|
106
|
-
)
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const test = g_testsPerRunner.get (testId);
|
|
111
|
-
if (test.runner !== runnerId) {
|
|
112
|
-
res.write (
|
|
113
|
-
JSON.stringify ({
|
|
114
|
-
error : constants.ERRORS.INVALID_RUNNER_OWNERSHIP
|
|
115
|
-
})
|
|
116
|
-
)
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
let status = queryObj.status || constants.TEST_STATUS_FAILED;
|
|
121
|
-
if (status !== constants.TEST_STATUS_SUCCESS) {
|
|
122
|
-
status = constants.TEST_STATUS_FAILED;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
test.status = status;
|
|
126
|
-
test.error = JSON.parse(querystring.unescape (queryObj.error || 'null'));
|
|
127
|
-
|
|
128
|
-
res.write ( JSON.stringify ({ 'status' : status }) );
|
|
129
|
-
|
|
130
|
-
// Emit an event-finished for given test ID, we use the ID because that
|
|
131
|
-
// should be unique.
|
|
132
|
-
g_testEventEmitter.emit (constants.EVENT_FINISHED + ':' + testId, test);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// -----------------------------------------------------------------------------
|
|
136
|
-
// handleRunners
|
|
137
|
-
//
|
|
138
|
-
// REST: /runner/<runner-id>/*
|
|
139
|
-
//
|
|
140
|
-
// Handles all requests from all runners (registration, queries, ...)
|
|
141
|
-
// -----------------------------------------------------------------------------
|
|
142
|
-
function handleRunners (req, res) {
|
|
143
|
-
const queryObj = querystring.parse(req.route.query);
|
|
144
|
-
const splitted = req.route.pathname.split('/');
|
|
145
|
-
const runnerId = splitted[2];
|
|
146
|
-
const action = splitted[3];
|
|
147
|
-
|
|
148
|
-
// TODO: check that runner is registered
|
|
149
|
-
if (!runnerId || !action) {
|
|
150
|
-
res.end();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
switch (action) {
|
|
155
|
-
case 'should-run': handleRunnerShouldRun (req, res); break;
|
|
156
|
-
case 'result': handleRunnerResult(req, res); break;
|
|
157
|
-
default:
|
|
158
|
-
console.error ("Invalid action: " + action + "(" + req.url + ")");
|
|
159
|
-
res.write (
|
|
160
|
-
JSON.stringify ({ error : constants.ERRORS.INVALID_REQUEST_ACTION })
|
|
161
|
-
)
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
res.end();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// -----------------------------------------------------------------------------
|
|
168
|
-
// mainServerHandler
|
|
169
|
-
//
|
|
170
|
-
// REST: /*
|
|
171
|
-
//
|
|
172
|
-
// Handles all requests to this master server
|
|
173
|
-
// -----------------------------------------------------------------------------
|
|
174
|
-
function mainServerHandler (req, res) {
|
|
175
|
-
res.writeHead(200, {'Content-Type': 'application/javascript'});
|
|
176
|
-
req.route = url.parse(req.url);
|
|
177
|
-
|
|
178
|
-
// REST: /runner/<runner-id>/*
|
|
179
|
-
//
|
|
180
|
-
// execute actions from given runner
|
|
181
|
-
if (req.route.pathname.startsWith (`/runner/`)) {
|
|
182
|
-
handleRunners (req, res);
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
res.write(JSON.stringify ({ error : constants.ERRORS.INVALID_REQUEST }));
|
|
186
|
-
res.end();
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
module.exports = {
|
|
192
|
-
setEventEmitter,
|
|
193
|
-
mainServerHandler
|
|
194
|
-
};
|
package/master.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// -----------------------------------------------------------------------------
|
|
2
|
-
// master.js
|
|
3
|
-
//
|
|
4
|
-
// Copyright(c) 2019 Pau Sanchez - MIT License
|
|
5
|
-
// -----------------------------------------------------------------------------
|
|
6
|
-
const http = require('http');
|
|
7
|
-
const EventEmitter = require('events');
|
|
8
|
-
const masterServer = require ('./master-server.js');
|
|
9
|
-
const masterMochaBindings = require ('./master-mocha-bindings.js');
|
|
10
|
-
|
|
11
|
-
// used to notify master that a test has been executed
|
|
12
|
-
class TestEmitter extends EventEmitter {};
|
|
13
|
-
|
|
14
|
-
let g_server = null;
|
|
15
|
-
let g_eventEmitter = null;
|
|
16
|
-
|
|
17
|
-
// -----------------------------------------------------------------------------
|
|
18
|
-
// master
|
|
19
|
-
//
|
|
20
|
-
// Initializes the master server, redefines variables, etc...
|
|
21
|
-
// -----------------------------------------------------------------------------
|
|
22
|
-
function master (port) {
|
|
23
|
-
|
|
24
|
-
// Checking server because this master function might be called once
|
|
25
|
-
// per every mocha file, and we only want to initialize once.
|
|
26
|
-
if (g_server === null) {
|
|
27
|
-
g_eventEmitter = new TestEmitter();
|
|
28
|
-
|
|
29
|
-
masterServer.setEventEmitter(g_eventEmitter);
|
|
30
|
-
masterMochaBindings.setEventEmitter (g_eventEmitter);
|
|
31
|
-
|
|
32
|
-
g_server = http.createServer(masterServer.mainServerHandler);
|
|
33
|
-
g_server.listen(port, function() {
|
|
34
|
-
console.log("# Mocha distributed master listening at port:", port, "\n");
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// hook all mocha methods
|
|
39
|
-
global.describe = masterMochaBindings.describe;
|
|
40
|
-
global.it = masterMochaBindings.it;
|
|
41
|
-
global.before = masterMochaBindings.before;
|
|
42
|
-
global.beforeEach = masterMochaBindings.beforeEach;
|
|
43
|
-
global.after = masterMochaBindings.after;
|
|
44
|
-
global.afterEach = masterMochaBindings.afterEach;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
module.exports = master;
|
package/runner-mocha-bindings.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
// -----------------------------------------------------------------------------
|
|
2
|
-
// runner-mocha-bindings.js
|
|
3
|
-
//
|
|
4
|
-
// NOTE: mocha first does a first pass executing all describe/it/... and then
|
|
5
|
-
// when it has gathered all information, it runs the functions associated
|
|
6
|
-
// to each. What we do is we hook our method in the middle and call
|
|
7
|
-
// or not, the original test method.
|
|
8
|
-
//
|
|
9
|
-
// Copyright(c) 2019 Pau Sanchez - MIT License
|
|
10
|
-
// -----------------------------------------------------------------------------
|
|
11
|
-
const axios = require('axios');
|
|
12
|
-
const querystring = require('querystring');
|
|
13
|
-
const crypto = require('crypto');
|
|
14
|
-
|
|
15
|
-
const constants = require('./constants.js');
|
|
16
|
-
|
|
17
|
-
let g_masterAddress = null;
|
|
18
|
-
|
|
19
|
-
// Generate a unique random id for this runner (with almost 100% certainty
|
|
20
|
-
// to be different on any machine/environment).
|
|
21
|
-
const g_runnerIdBuffer = Buffer.alloc(16);
|
|
22
|
-
const g_runnerId = crypto.randomFillSync(g_runnerIdBuffer).toString('hex');
|
|
23
|
-
|
|
24
|
-
let g_mochaMethods = {
|
|
25
|
-
describe : global.describe || null,
|
|
26
|
-
it : global.it || null,
|
|
27
|
-
before : global.before || null,
|
|
28
|
-
beforeEach : global.beforeEach || null,
|
|
29
|
-
after : global.after || null,
|
|
30
|
-
afterEach : global.afterEach || null
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// contains the actual test path such as suite.title > suite.title > it.title
|
|
35
|
-
let g_testPath = [];
|
|
36
|
-
|
|
37
|
-
// -----------------------------------------------------------------------------
|
|
38
|
-
// askMasterIfShouldRun
|
|
39
|
-
//
|
|
40
|
-
// Asks master server if the current runner should run given suite.
|
|
41
|
-
//
|
|
42
|
-
// Right now we only ask for suites because it seems the safest approach.
|
|
43
|
-
//
|
|
44
|
-
// Returns:
|
|
45
|
-
// - true: the caller should run the test
|
|
46
|
-
// - false: the caller should not run the test
|
|
47
|
-
// - null: connectivity issue with the master, it depends on the caller to
|
|
48
|
-
// decide what to do
|
|
49
|
-
// -----------------------------------------------------------------------------
|
|
50
|
-
async function askMasterIfShouldRun (testPath) {
|
|
51
|
-
const testSuite = querystring.escape(testPath[0]);
|
|
52
|
-
try {
|
|
53
|
-
const r = await axios.get (`http://${g_masterAddress}/runner/${g_runnerId}/should-run?test=${testSuite}`);
|
|
54
|
-
const ok = (r.data.answer === 'run');
|
|
55
|
-
|
|
56
|
-
// we ask for given test, so we can save the result properly
|
|
57
|
-
// TODO: this can be removed if the server accepts runners
|
|
58
|
-
if (ok) {
|
|
59
|
-
const path = querystring.escape(testPath.join(constants.TEST_PATH_SEPARATOR));
|
|
60
|
-
const r = await axios.get (`http://${g_masterAddress}/runner/${g_runnerId}/should-run?test=${path}`);
|
|
61
|
-
}
|
|
62
|
-
return ok;
|
|
63
|
-
}
|
|
64
|
-
catch (e) {
|
|
65
|
-
// ignore connection errors
|
|
66
|
-
// the master could have died because it already finished
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// skip (should not run) if there is any kind of error with master
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// -----------------------------------------------------------------------------
|
|
74
|
-
// sendTestResultToMaster
|
|
75
|
-
// -----------------------------------------------------------------------------
|
|
76
|
-
async function sendTestResultToMaster (testPath, status, error = null) {
|
|
77
|
-
// TODO: use POST
|
|
78
|
-
const path = querystring.escape(testPath.join(constants.TEST_PATH_SEPARATOR));
|
|
79
|
-
let serror = querystring.escape(JSON.stringify(error || null));
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const r = await axios.get (
|
|
83
|
-
`http://${g_masterAddress}/runner/${g_runnerId}/result?test=${path}&status=${status}&error=${serror}`
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
catch (e) {
|
|
87
|
-
// ignore connection errors
|
|
88
|
-
// the master could have died because it already finished
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// -----------------------------------------------------------------------------
|
|
93
|
-
// init
|
|
94
|
-
//
|
|
95
|
-
// Initialize this module
|
|
96
|
-
// -----------------------------------------------------------------------------
|
|
97
|
-
function init (masterAddress){
|
|
98
|
-
g_masterAddress = masterAddress;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// -----------------------------------------------------------------------------
|
|
102
|
-
// describe
|
|
103
|
-
//
|
|
104
|
-
// Custom version to describe a test, with same signature
|
|
105
|
-
// -----------------------------------------------------------------------------
|
|
106
|
-
async function describe (title, fn) {
|
|
107
|
-
g_testPath.push (title);
|
|
108
|
-
const result = g_mochaMethods.describe (title, fn);
|
|
109
|
-
g_testPath.pop();
|
|
110
|
-
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// TODO:
|
|
115
|
-
// describe.skip = async function (title, fn) { }
|
|
116
|
-
// describe.once = async function (title, fn) { }
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// -----------------------------------------------------------------------------
|
|
120
|
-
// it
|
|
121
|
-
// -----------------------------------------------------------------------------
|
|
122
|
-
async function it (title, fn) {
|
|
123
|
-
// store current path for when the 'it' function executes
|
|
124
|
-
g_testPath.push (title);
|
|
125
|
-
const testPath = g_testPath.slice(0);
|
|
126
|
-
g_testPath.pop();
|
|
127
|
-
|
|
128
|
-
// define our own function hook
|
|
129
|
-
return g_mochaMethods.it (title, async function () {
|
|
130
|
-
if (!await askMasterIfShouldRun (testPath)) {
|
|
131
|
-
this.skip();
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const testResult = await fn();
|
|
137
|
-
await sendTestResultToMaster (testPath, constants.TEST_STATUS_SUCCESS);
|
|
138
|
-
return testResult;
|
|
139
|
-
}
|
|
140
|
-
catch (e) {
|
|
141
|
-
await sendTestResultToMaster (testPath, constants.TEST_STATUS_FAILED, e);
|
|
142
|
-
throw e;
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// -----------------------------------------------------------------------------
|
|
148
|
-
// execHookMochaMethod
|
|
149
|
-
//
|
|
150
|
-
// Method used accross all mocha hooks (before, beforeEach, after, afterEach)
|
|
151
|
-
// in order to query the master server if it needs to be executed or not.
|
|
152
|
-
// -----------------------------------------------------------------------------
|
|
153
|
-
function execHookMochaMethod (title, orgMochaMethod, fn) {
|
|
154
|
-
g_testPath.push (title);
|
|
155
|
-
const testPath = g_testPath.slice(0);
|
|
156
|
-
g_testPath.pop();
|
|
157
|
-
|
|
158
|
-
return orgMochaMethod (async function () {
|
|
159
|
-
if (!await askMasterIfShouldRun (testPath)) {
|
|
160
|
-
this.skip();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
const testResult = await fn();
|
|
166
|
-
await sendTestResultToMaster (testPath, constants.TEST_STATUS_SUCCESS);
|
|
167
|
-
return testResult;
|
|
168
|
-
}
|
|
169
|
-
catch (e) {
|
|
170
|
-
await sendTestResultToMaster (testPath, constants.TEST_STATUS_FAILED, e);
|
|
171
|
-
throw e;
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// -----------------------------------------------------------------------------
|
|
177
|
-
// before
|
|
178
|
-
// -----------------------------------------------------------------------------
|
|
179
|
-
async function before (fn) {
|
|
180
|
-
return execHookMochaMethod (':before', g_mochaMethods.before, fn);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// -----------------------------------------------------------------------------
|
|
184
|
-
// beforeEach
|
|
185
|
-
// -----------------------------------------------------------------------------
|
|
186
|
-
async function beforeEach (fn) {
|
|
187
|
-
return execHookMochaMethod (':beforeEach', g_mochaMethods.beforeEach, fn);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// -----------------------------------------------------------------------------
|
|
191
|
-
// after
|
|
192
|
-
// -----------------------------------------------------------------------------
|
|
193
|
-
async function after (fn) {
|
|
194
|
-
return execHookMochaMethod (':after', g_mochaMethods.after, fn);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// -----------------------------------------------------------------------------
|
|
198
|
-
// afterEach
|
|
199
|
-
// -----------------------------------------------------------------------------
|
|
200
|
-
async function afterEach (fn) {
|
|
201
|
-
return execHookMochaMethod (':afterEach', g_mochaMethods.after, fn);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
module.exports = {
|
|
205
|
-
init,
|
|
206
|
-
describe,
|
|
207
|
-
it,
|
|
208
|
-
before,
|
|
209
|
-
beforeEach,
|
|
210
|
-
after,
|
|
211
|
-
afterEach
|
|
212
|
-
};
|
package/runner.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// -----------------------------------------------------------------------------
|
|
2
|
-
// runner.js
|
|
3
|
-
//
|
|
4
|
-
// Copyright(c) 2019 Pau Sanchez - MIT License
|
|
5
|
-
// -----------------------------------------------------------------------------
|
|
6
|
-
const runnerMochaBindings = require ('./runner-mocha-bindings.js');
|
|
7
|
-
|
|
8
|
-
function runner (masterAddress, port) {
|
|
9
|
-
runnerMochaBindings.init (masterAddress + ':' + port);
|
|
10
|
-
|
|
11
|
-
// hook all mocha methods
|
|
12
|
-
global.describe = runnerMochaBindings.describe;
|
|
13
|
-
global.it = runnerMochaBindings.it;
|
|
14
|
-
global.before = runnerMochaBindings.before;
|
|
15
|
-
global.beforeEach = runnerMochaBindings.beforeEach;
|
|
16
|
-
global.after = runnerMochaBindings.after;
|
|
17
|
-
global.afterEach = runnerMochaBindings.afterEach;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
module.exports = runner;
|