5etools-utils 0.1.24 → 0.2.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/lib/TestJson.js CHANGED
@@ -1,11 +1,17 @@
1
+ import * as os from "os";
1
2
  import * as path from "path";
3
+ import * as url from "url";
4
+ import {Worker} from "worker_threads";
2
5
 
3
6
  import Ajv2020 from "ajv/dist/2020.js";
4
- import addFormats from "ajv-formats"
7
+ import addFormats from "ajv-formats";
5
8
  import * as jsonSourceMap from "json-source-map";
6
9
 
7
10
  import * as uf from "./UtilFs.js";
8
11
  import * as um from "./UtilMisc.js";
12
+ import {WorkerList, Deferred} from "./WorkerList.js";
13
+
14
+ const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
9
15
 
10
16
  const _IS_SORT_RESULTS = !process.env.VET_TEST_JSON_RESULTS_UNSORTED;
11
17
  const _IS_TRIM_RESULTS = !process.env.VET_TEST_JSON_RESULTS_UNTRIMMED;
@@ -65,8 +71,10 @@ class JsonTester {
65
71
 
66
72
  _getFileErrors ({filePath}) { return `${filePath}\n${JSON.stringify(this._ajv.errors, null, 2)}`; }
67
73
 
68
- _handleError ({filePath, errors, errorsFull, data}) {
69
- if (!process.env.CI) errorsFull.push(this._getFileErrors({filePath}));
74
+ _getErrors ({filePath, data}) {
75
+ const out = {errors: [], errorsFull: []};
76
+
77
+ if (!process.env.CI) out.errorsFull.push(this._getFileErrors({filePath}));
70
78
 
71
79
  // Sort the deepest errors to the bottom, as these are the ones we're most likely to be the ones we care about
72
80
  // manually checking.
@@ -91,7 +99,9 @@ class JsonTester {
91
99
 
92
100
  const error = this._getFileErrors({filePath});
93
101
  um.error(this._tagLog, error);
94
- errors.push(error);
102
+ out.errors.push(error);
103
+
104
+ return out;
95
105
  }
96
106
 
97
107
  _doLoadSchema () {
@@ -111,18 +121,26 @@ class JsonTester {
111
121
  this._isSchemaLoaded = true;
112
122
  }
113
123
 
114
- _doRunOnDir ({isFailFast, dir, errors, errorsFull, ...opts} = {}) {
115
- for (const filePath of uf.listJsonFiles(dir, opts)) {
116
- um.info(this._tagLog, `\tValidating "${filePath}"...`);
124
+ getFileErrors ({filePath} = {}) {
125
+ this._doLoadSchema();
117
126
 
118
- const data = uf.readJSON(filePath);
119
- this._addImplicits(data);
120
- const valid = this._ajv.validate(this._fnGetSchemaId(filePath), data);
127
+ um.info(this._tagLog, `\tValidating "${filePath}"...`);
121
128
 
122
- if (!valid) {
123
- this._handleError({filePath, errors, errorsFull, data});
124
- if (isFailFast) break;
125
- }
129
+ const data = uf.readJSON(filePath);
130
+ this._addImplicits(data);
131
+
132
+ const isValid = this._ajv.validate(this._fnGetSchemaId(filePath), data);
133
+ if (isValid) return {};
134
+
135
+ return this._getErrors({filePath, data});
136
+ }
137
+
138
+ _doRunOnDir ({isFailFast, dir, errors, errorsFull, ...opts} = {}) {
139
+ for (const filePath of uf.listJsonFiles(dir, opts)) {
140
+ const {errors: errorsFile = [], errorsFull: errorsFullFile = []} = this.getFileErrors({filePath});
141
+ errors.push(...errorsFile);
142
+ errorsFull.push(...errorsFullFile);
143
+ if (isFailFast && (errorsFile.length || errorsFullFile.length)) break;
126
144
  }
127
145
  }
128
146
 
@@ -134,8 +152,6 @@ class JsonTester {
134
152
  getErrors (dir, opts = {}) {
135
153
  um.info(this._tagLog, `Validating JSON against schema`);
136
154
 
137
- this._doLoadSchema();
138
-
139
155
  const errors = [];
140
156
  const errorsFull = [];
141
157
 
@@ -147,8 +163,6 @@ class JsonTester {
147
163
  getErrorsOnDirs ({isFailFast = false} = {}) {
148
164
  um.info(this._tagLog, `Validating JSON against schema`);
149
165
 
150
- this._doLoadSchema();
151
-
152
166
  const errors = [];
153
167
  const errorsFull = [];
154
168
 
@@ -159,6 +173,81 @@ class JsonTester {
159
173
 
160
174
  return {errors, errorsFull};
161
175
  }
176
+
177
+ async pGetErrorsOnDirsWorkers ({isFailFast = false} = {}) {
178
+ um.info(this._tagLog, `Validating JSON against schema`);
179
+
180
+ const cntWorkers = Math.max(1, os.cpus().length - 1);
181
+
182
+ const errors = [];
183
+ const errorsFull = [];
184
+
185
+ const fileQueue = [];
186
+
187
+ uf.runOnDirs((dir) => {
188
+ fileQueue.push(...uf.listJsonFiles(dir));
189
+ });
190
+
191
+ const workerList = new WorkerList();
192
+
193
+ const workers = [...new Array(cntWorkers)]
194
+ .map(() => {
195
+ // Relative `Worker` paths do not function in packages, so give an exact path
196
+ const worker = new Worker(path.join(__dirname, "TestJsonWorker.js"));
197
+
198
+ worker.on("message", (msg) => {
199
+ switch (msg.type) {
200
+ case "ready":
201
+ case "done": {
202
+ if (msg.payload.isError) {
203
+ errors.push(...msg.payload.errors);
204
+ errorsFull.push(...msg.payload.errorsFull);
205
+
206
+ if (isFailFast) workers.forEach(worker => worker.postMessage({type: "cancel"}));
207
+ }
208
+
209
+ if (worker.dIsActive) worker.dIsActive.resolve();
210
+ workerList.add(worker);
211
+
212
+ break;
213
+ }
214
+ }
215
+ });
216
+
217
+ worker.on("error", e => console.error(e));
218
+
219
+ worker.postMessage({
220
+ type: "init",
221
+ payload: {
222
+ dirSchema: this._dirSchema,
223
+ tagLog: this._tagLog,
224
+ strFnGetSchemaId: this._fnGetSchemaId.toString(),
225
+ },
226
+ });
227
+
228
+ return worker;
229
+ });
230
+
231
+ while (fileQueue.length) {
232
+ if (isFailFast && errors.length) break;
233
+
234
+ const file = fileQueue.shift();
235
+ const worker = await workerList.get();
236
+
237
+ worker.dIsActive = new Deferred();
238
+ worker.postMessage({
239
+ type: "work",
240
+ payload: {
241
+ file,
242
+ },
243
+ });
244
+ }
245
+
246
+ await Promise.all(workers.map(it => it.dIsActive.promise));
247
+ await Promise.all(workers.map(it => it.terminate()));
248
+
249
+ return {errors, errorsFull};
250
+ }
162
251
  }
163
252
 
164
253
  export {JsonTester, LOG_TAG};
@@ -0,0 +1,57 @@
1
+ import {isMainThread, parentPort} from "worker_threads";
2
+ import {JsonTester} from "./TestJson.js";
3
+
4
+ if (isMainThread) throw new Error(`Worker must not be started in main thread!`);
5
+
6
+ let jsonTester;
7
+ let isCancelled = false;
8
+
9
+ parentPort
10
+ .on("message", msg => {
11
+ switch (msg.type) {
12
+ case "init": {
13
+ const {dirSchema, tagLog, strFnGetSchemaId} = msg.payload;
14
+
15
+ jsonTester = new JsonTester({
16
+ dirSchema,
17
+ tagLog,
18
+ fnGetSchemaId: eval(strFnGetSchemaId), // eslint-disable-line no-eval
19
+ });
20
+
21
+ parentPort.postMessage({
22
+ type: "ready",
23
+ payload: {},
24
+ });
25
+
26
+ break;
27
+ }
28
+
29
+ case "cancel": {
30
+ isCancelled = true;
31
+ break;
32
+ }
33
+
34
+ case "work": {
35
+ if (isCancelled) {
36
+ parentPort.postMessage({
37
+ type: "done",
38
+ payload: {},
39
+ });
40
+ return;
41
+ }
42
+
43
+ const {errors = [], errorsFull = []} = jsonTester.getFileErrors({filePath: msg.payload.file});
44
+
45
+ parentPort.postMessage({
46
+ type: "done",
47
+ payload: {
48
+ isError: !!(errors.length || errorsFull.length),
49
+ errors,
50
+ errorsFull,
51
+ },
52
+ });
53
+
54
+ break;
55
+ }
56
+ }
57
+ });
@@ -0,0 +1,47 @@
1
+ class Deferred {
2
+ constructor () {
3
+ this._resolve = null;
4
+ this._reject = null;
5
+ this._promise = new Promise((resolve, reject) => {
6
+ this._resolve = resolve;
7
+ this._reject = reject;
8
+ });
9
+ }
10
+
11
+ get resolve () { return this._resolve; }
12
+ get reject () { return this._reject; }
13
+ get promise () { return this._promise; }
14
+ }
15
+
16
+ class WorkerList {
17
+ /**
18
+ * @param {Array} workers
19
+ */
20
+ constructor ({workers = null} = {}) {
21
+ this._workers = workers || [];
22
+ this._deferredQueue = [];
23
+ }
24
+
25
+ add (worker) {
26
+ this._workers.push(worker);
27
+
28
+ while (this._deferredQueue.length && this._workers.length) {
29
+ const d = this._deferredQueue.shift();
30
+ d.resolve(this._workers.shift());
31
+ }
32
+ }
33
+
34
+ get () {
35
+ if (this._workers.length) {
36
+ return Promise.resolve(this._workers.shift());
37
+ }
38
+
39
+ const d = new Deferred();
40
+ this._deferredQueue.push(d);
41
+ return d.promise;
42
+ }
43
+
44
+ get cntAvailableWorkers () { return this._workers.length; }
45
+ }
46
+
47
+ export {Deferred, WorkerList};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "5etools-utils",
3
- "version": "0.1.24",
3
+ "version": "0.2.0",
4
4
  "description": "Shared utilities for the 5etools ecosystem.",
5
5
  "type": "module",
6
6
  "main": "lib/Api.js",
@@ -11,9 +11,9 @@
11
11
  "lib": "./lib"
12
12
  },
13
13
  "scripts": {
14
- "test": "npm run test:js",
15
- "test:js": "eslint lib",
16
- "lint:js": "eslint lib --fix"
14
+ "test": "npm run lint:js && npm run test:js",
15
+ "test:js": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
16
+ "lint:js": "eslint lib test --fix"
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",
@@ -27,10 +27,11 @@
27
27
  "homepage": "https://github.com/TheGiddyLimit/5etools-utils#readme",
28
28
  "dependencies": {
29
29
  "ajv": "^8.11.2",
30
- "json-source-map": "^0.6.1",
31
- "ajv-formats": "^2.1.1"
30
+ "ajv-formats": "^2.1.1",
31
+ "json-source-map": "^0.6.1"
32
32
  },
33
33
  "devDependencies": {
34
- "eslint": "^8.11.0"
34
+ "eslint": "^8.11.0",
35
+ "jest": "^29.3.1"
35
36
  }
36
37
  }