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 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 too many lines of code, nor having to decide
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
- Hopefully you will only need to include a single line of code on each of your
11
- mocha files in order for them to run in parallel.
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
- Your test files will still be 100% compatible with mocha, so you can run them
14
- without any side-effects locally, using mocha, as if nothing was changed.
25
+ ## Quick start
15
26
 
16
- A idea for the future is to create a mocha-compatible runner so that you don't
17
- even have to change source files in any way. But let's go one step at a time.
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
- ## How it works
31
+ ```bash
32
+ $ npm install -s mocha-distributed
33
+ ```
20
34
 
21
- ### Brief example
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
- First, add the following line in each of your test files you want to distribute.
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
- ```javascript
28
- require('mocha-distributed');
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
- Then, if you want to run mocha in one computer (e.g your dev computer):
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
- ```bash
34
- $ mocha test/**/*.js
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
- To run mocha distributed:
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
- - Execute as master in only one machine (imagine it is running on 1.2.3.4 IP address):
57
+ ## Environment Variables
40
58
 
41
- ```bash
42
- $ MOCHA_DISTRIBUTED="master" mocha test/**/*.js
43
- ```
59
+ - **MOCHA_DISTRIBUTED** (required)
44
60
 
45
- - Execute runners on one or a thousand machines/processes as:
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
- ```bash
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
- ### Conceptual overview
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
- The concept is very simple, this module hooks all mocha calls and does some magic
56
- to allow running tests across machines without you having to decide what runs
57
- where, or splitting tests beforehand, etc...
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
- Runners connect to the master and for each suite they ask whether they are
64
- the 'owners' to run the tests on that suite or not. If they are, they run it.
65
- If they are not, they just skip the tests and continue running the next suite.
116
+ - **MOCHA_DISTRIBUTED_VERBOSE** = false
117
+ - false (default)
118
+ Avoid printing verbose information
66
119
 
67
- For simplicity this is all granularity you'll get for now. If you need two
68
- suites to run one after another on the same machine, then create a suite
69
- that encloses those.
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
- All test results (with error information) are sent to the master, and the master
72
- will display the output of all tests.
124
+ ## Reading test results from Redis
73
125
 
74
- If you like to save test results, etc... run the master with the right mocha
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
- ### How to run in practice
128
+ The list is basically the execution ID from the variable
129
+ MOCHA_DISTRIBUTED_EXECUTION_ID concatenated to ':test_result'
78
130
 
79
- There is a magic environment variable called MOCHA_DISTRIBUTED.
131
+ For example, if you are using: MOCHA_DISTRIBUTED_EXECUTION_ID="abcdefg"
80
132
 
81
- When unset or empty, this module does nothing at all. Mocha runs normally,
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
- When set to the special keyword 'master', the mocha will automatically create
85
- an HTTP server and listen to other processes or machines to connect to it and
86
- ask/inform about running the tests.
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
- For the runners, you would need to set MOCHA_DISTRIBUTED variable with the IP
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
- You can also append the port to both the master and the runners, but in
95
- that case, the port must match.
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
- ### Run tests in one machine and one process
164
+ ### Environment-agnostic
100
165
 
101
- Just don't use the MOCHA_DISTRIBUTED variable, or set it to empty string.
166
+ Make sure at least the following variables are set:
102
167
 
103
168
  ```bash
104
- $ mocha test/**/*.js
169
+ MOCHA_DISTRIBUTED="redis://1.2.3.4"
170
+ MOCHA_DISTRIBUTED_EXECUTION_ID="a5ce4d8a-5b06-4ec8-aea2-37d7e4b2ffe1"
105
171
  ```
106
172
 
107
- ### Run tests in one machine, multiple processes
173
+ Again, execution ID should be a different random number each time you want to
174
+ launch tests in parallel.
108
175
 
109
- To keep things simple, do something like this:
176
+ Example:
110
177
 
111
178
  ```bash
112
- $ MOCHA_DISTRIBUTED="master" mocha test/**/*.js
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
- Run as many processes as you'd like
182
+ Of course, this assumes you have already installed mocha-distributed.
120
183
 
121
- ### Run tests in several processes across several machines
184
+ ### Run tests in parallel in the same machine
122
185
 
123
- On one machine do:
186
+ To keep things simple, do something like this:
124
187
 
125
188
  ```bash
126
- $ MOCHA_DISTRIBUTED="master" mocha test/**/*.js
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
- You can also run some runners in that machine if you wish (see previous example).
198
+ Run as many processes as you'd like.
130
199
 
131
- Figure out the IP address of the master. For this example let's say the master
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
- ```bash
135
- $ MOCHA_DISTRIBUTED="1.2.3.4" mocha test/**/*.js > /dev/null &
136
- $ MOCHA_DISTRIBUTED="1.2.3.4" mocha test/**/*.js > /dev/null &
137
- ...
138
- $ MOCHA_DISTRIBUTED="1.2.3.4" mocha test/**/*.js > /dev/null &
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
- Again, spawn as many processes as you'd like.
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, only one runner should
148
- be defined as the master. The master never runs tests, only waits.
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 should create more processes or launch more docker or kubernetes instances
151
- or spread test on several nodes... do it as you wish, but for each of the
152
- runners that you create, make sure they have visibility to the master (e.g
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
- In case you run multiple masters on the same machine, make sure you setup
156
- a different port each time, otherwise runner's from different projects will
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
- // index.js
31
+ // getTestPath
3
32
  //
4
- // Copyright(c) 2019 Pau Sanchez - MIT License
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
- // - runner (MOCHA_DISTRIBUTED='<master-address>[:PORT]' IP/DNS of the master node)
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
- // - Empty or not defined
19
- // Runs mocha normally, we don't redefine any variable at all.
20
- // Everything will run locally, if spawned on many machines or processes
21
- // tests will run on all of them as if this module did not exist.
22
- const master = require('./master.js');
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
- // let's get the party started
37
- if (!mode) {
38
- // run as normal mocha
39
- }
40
- else if (mode === 'master') {
41
- master (port);
42
- }
43
- else {
44
- const masterAddress = mode;
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
- // remove from cache, so it is always reinitialized
49
- delete require.cache[require.resolve('./index.js')];
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.8.1",
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
- "axios": "^0.18.0"
30
+ "redis": "^4.0.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "chai": "^4.2.0",
34
- "mocha": "^5.2.0"
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
- };
@@ -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;
@@ -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;