risei 1.0.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.
@@ -0,0 +1,140 @@
1
+ /**/
2
+
3
+ /* TestFinder is an ATestFinder that finds tests in the places identified in package.json. */
4
+
5
+ import { ATestFinder } from "./ATestFinder.js";
6
+ import { ATestSource } from "./ATestSource.js";
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { minimatch } from "minimatch";
10
+
11
+ export class TestFinder extends ATestFinder {
12
+ // region Definitions
13
+
14
+ static NO_SUCH_FILE = "ENOENT";
15
+ static METADATA_FILE = "package.json";
16
+
17
+ // These are never test locations. Others like `bin`
18
+ // probably aren't, either, but it's not 100% certain.
19
+ static ALWAYS_SKIPPED = [ ".git", "node_modules" ];
20
+
21
+ // endregion Definitions
22
+
23
+ // region Fields
24
+
25
+ #sought;
26
+
27
+ // endregion Fields
28
+
29
+ constructor() {
30
+ super();
31
+ }
32
+
33
+ async findAllTests() {
34
+ // Paths for test sources are listed in package.json, possibly with globbing.
35
+ let source = this.sourceBySearch();
36
+ this.#sought = source.tests;
37
+ let testPaths = this.pathsFromTargetingSource();
38
+
39
+ let tests = [];
40
+
41
+ for (let testPath of testPaths) {
42
+ // Contents of test sources have to be loaded.
43
+ let sources = await this.convertPathToClassInstances(testPath);
44
+
45
+ // Any test classes in each source have to be loaded.
46
+ for (let source of sources) {
47
+ let local = source.tests;
48
+ tests.push(...local);
49
+ }
50
+ }
51
+
52
+ return tests;
53
+ }
54
+
55
+ sourceBySearch() {
56
+ // App's package.json always found at process.cwd().
57
+ let packageSite = process.cwd();
58
+ let pathAndName = path.join(packageSite, TestFinder.METADATA_FILE);
59
+
60
+ // Contents of package.json are just a single JSON object.
61
+ let packageJson = fs.readFileSync(pathAndName, { encoding: "utf8" });
62
+ packageJson = JSON.parse(packageJson);
63
+
64
+ // Can't run or reasonably recover if this is not defined.
65
+ if (!packageJson.risei) {
66
+ throw new Error(
67
+ `For Risei tests to run, this app's package.json must contain `
68
+ + `a node like this: "risei": { tests: "path-or-glob-here" }. `
69
+ + `One glob or path may be used, or an array of them.`);
70
+ }
71
+
72
+ // Only Risei's own metadata is needed.
73
+ return packageJson.risei;
74
+ }
75
+
76
+ pathsFromTargetingSource() {
77
+ let paths = [];
78
+ let root = process.cwd();
79
+
80
+ // Recursively add all test
81
+ // files in tree to `paths`.
82
+ this.searchTree(root, paths);
83
+
84
+ return paths;
85
+ }
86
+
87
+ searchTree(root, paths) {
88
+ let entries = fs.readdirSync(root, { withFileTypes: true });
89
+
90
+ for (let entry of entries) {
91
+ // Working around a recent bug in fs of .path always undefined or wrong.
92
+ entry.path = path.join(root, entry.name);
93
+
94
+ // These should never contain tests, and may not even be folders.
95
+ if (TestFinder.ALWAYS_SKIPPED.includes(entry.name)) {
96
+ continue;
97
+ }
98
+
99
+ // This level: any test files.
100
+ if (entry.isFile()) {
101
+ if (minimatch(entry.path, this.#sought, { matchBase: true })) {
102
+ paths.push(entry.path);
103
+ }
104
+ }
105
+
106
+ // Next level: recursion.
107
+ if (entry.isDirectory()) {
108
+ this.searchTree(entry.path, paths);
109
+ }
110
+ }
111
+ }
112
+
113
+ async convertPathToClassInstances(classPath) {
114
+ // Each test source may have multiple test classes.
115
+ let testSources = [];
116
+
117
+ // Loading modules should load their dependencies.
118
+ try {
119
+ let module = await import(classPath);
120
+
121
+ // A module may have many contents.
122
+ for (let moduleKey in module) {
123
+ let definition = module[moduleKey];
124
+ let local = new definition.prototype.constructor();
125
+
126
+ // Non-test contents may exist in each test source.
127
+ if (local instanceof ATestSource) {
128
+ testSources.push(local);
129
+ }
130
+ }
131
+
132
+ return testSources;
133
+ }
134
+ catch (e) {
135
+ let fileName = path.basename(classPath);
136
+ console.log(`Test loading failed for ${ fileName }. ${ e }`);
137
+ return [];
138
+ }
139
+ }
140
+ }
@@ -0,0 +1,25 @@
1
+ /**/
2
+
3
+ export class TestGroup {
4
+ // region Private fields
5
+
6
+ #group;
7
+
8
+ // endregion Private fields
9
+
10
+ // region Properties
11
+
12
+ get group() {
13
+ return this.#group;
14
+ }
15
+
16
+ set group(value) {
17
+ this.#group = value;
18
+ }
19
+
20
+ // endregion Properties
21
+
22
+ constructor(group) {
23
+ this.#group = group;
24
+ }
25
+ }
@@ -0,0 +1,338 @@
1
+ /**/
2
+
3
+ export class TestResult {
4
+ // region Fields
5
+
6
+ #test;
7
+
8
+ #identityText;
9
+ #initorsText;
10
+ #inputsText;
11
+ #expectedText;
12
+ #actualText;
13
+
14
+ #didPass;
15
+ #actual;
16
+ #thrown;
17
+
18
+ // endregion Fields
19
+
20
+ // region Source properties
21
+
22
+ get test() {
23
+ return this.#test;
24
+ }
25
+
26
+ set test(value) {
27
+ this.#test = value;
28
+ }
29
+
30
+ // endregion Source properties
31
+
32
+ // region Nature properties
33
+
34
+ get identityText() {
35
+ return this.#identityText;
36
+ }
37
+
38
+ set identityText(value) {
39
+ this.#identityText = value;
40
+ }
41
+
42
+ get initorsText() {
43
+ return this.#initorsText;
44
+ }
45
+
46
+ set initorsText(value) {
47
+ this.#initorsText = value;
48
+ }
49
+
50
+ get inputsText() {
51
+ return this.#inputsText;
52
+ }
53
+
54
+ set inputsText(value) {
55
+ this.#inputsText = value;
56
+ }
57
+
58
+ get expectedText() {
59
+ return this.#expectedText;
60
+ }
61
+
62
+ set expectedText(value) {
63
+ this.#expectedText = value;
64
+ }
65
+
66
+ // endregion Nature properties
67
+
68
+ // region Outcome properties
69
+
70
+ get actualText() {
71
+ return this.#actualText;
72
+ }
73
+
74
+ set actualText(value) {
75
+ this.#actualText = value;
76
+ }
77
+
78
+ get didPass() {
79
+ return this.#didPass;
80
+ }
81
+
82
+ set didPass(value) {
83
+ this.#didPass = value;
84
+ }
85
+
86
+ get anyThrow() {
87
+ return this.#thrown;
88
+ }
89
+
90
+ set anyThrow(value) {
91
+ this.#thrown = value;
92
+ }
93
+
94
+ // endregion Outcome properties
95
+
96
+ // region Construction
97
+
98
+ constructor(test) {
99
+ this.#test = test;
100
+ }
101
+
102
+ // endregion Construction
103
+
104
+ // region Before run: setNature() and dependencies
105
+
106
+ setNature() {
107
+ this.#identityText = this.#calculateIdentityText();
108
+ this.#initorsText = this.#calculateInitorsText();
109
+ this.#inputsText = this.#calculateInputsText();
110
+ this.#expectedText = this.#calculateExpectedText();
111
+ }
112
+
113
+ #calculateIdentityText() {
114
+ let text = `${ this.test.for } `;
115
+ return text;
116
+ }
117
+
118
+ #calculateInitorsText() {
119
+ let text = this.#improveValueRowDisplay(this.test.with);
120
+ text = `Initors: ${ text }. `;
121
+ return text;
122
+ }
123
+
124
+ #calculateInputsText() {
125
+ let text = this.#improveValueRowDisplay(this.test.in);
126
+ text = `Inputs: ${ text }. `;
127
+ return text;
128
+ }
129
+
130
+ #calculateExpectedText() {
131
+ let text = this.#improveValueDisplay(this.test.out);
132
+ text = `Expected: ${ text }. `;
133
+ return text;
134
+ }
135
+
136
+ // endregion Before run: setNature() and dependencies
137
+
138
+ // region After run: setResults() and dependencies
139
+
140
+ setResults() {
141
+ // Direct results.
142
+ this.#didPass = this.test.didPass;
143
+ this.#actual = this.test.actual;
144
+ this.#thrown = this.test.anyThrow;
145
+
146
+ // Derived displayable results.
147
+ this.#actualText = this.#calculateActualText();
148
+ }
149
+
150
+ #calculateActualText() {
151
+ let text = this.#improveValueDisplay(this.test.actual);
152
+ text = `Actual: ${ text }. `;
153
+ return text;
154
+ }
155
+
156
+ // endregion After run: setResults() and dependencies
157
+
158
+ // region Improving displaying
159
+
160
+ #improveValueRowDisplay(row) {
161
+ let items = row
162
+ .map(x => this.#improveValueDisplay(x));
163
+
164
+ if (items.length === 0) {
165
+ return [ "\u2014" ];
166
+ }
167
+
168
+ return items.join(`, `);
169
+ }
170
+
171
+ #improveValueDisplay(value) {
172
+ let output = this.#addQuotesIfString(value);
173
+ output = this.#addArrayStyleIfArray(output);
174
+ output = this.#jsonifyIfMap(output);
175
+ output = this.#jsonifyIfSet(output);
176
+ output = this.#jsonifyIfClass(output);
177
+ output = this.#jsonifyIfFunction(output);
178
+ output = this.#jsonifyIfObject(output);
179
+ output = this.#stateUndefinedIfUndefined(output);
180
+ output = this.#stateNullIfNull(output);
181
+
182
+ return output;
183
+ }
184
+
185
+ // endregion Improving displaying
186
+
187
+ // region Individual display improvements
188
+
189
+ #addQuotesIfString(value) {
190
+ let output = typeof value !== "string" ? value : `"${ value }"`;
191
+ return output;
192
+ }
193
+
194
+ #addArrayStyleIfArray(value) {
195
+ // Not-an-array path.
196
+ if (!Array.isArray(value)) {
197
+ return value;
198
+ }
199
+
200
+ // Each array value may be styled.
201
+ let outputs = value
202
+ .map(x => this.#improveValueDisplay(x));
203
+
204
+ // Brackets around, commas between.
205
+ let output = `[${ outputs.join(",") }]`;
206
+
207
+ // And back to caller.
208
+ return output;
209
+ }
210
+
211
+ #jsonifyIfMap(value) {
212
+ // Not-a-map path.
213
+ if (!(value instanceof Map)) {
214
+ return value;
215
+ }
216
+
217
+ let items = [];
218
+
219
+ // Each key-value pair is handled individually.
220
+ for (let key of value.keys()) {
221
+ // Keys are not styled.
222
+ let item = value.get(key);
223
+
224
+ // Cross-recursion for styling values.
225
+ items.push(`${ key }:${ this.#improveValueDisplay(item) }`);
226
+ }
227
+
228
+ // Key-value pairs are all listed together.
229
+ let output = `Map{${ items.join(",") }}`;
230
+ return output;
231
+ }
232
+
233
+ #jsonifyIfSet(value) {
234
+ // Not-a-set path.
235
+ if (!(value instanceof Set)) {
236
+ return value;
237
+ }
238
+
239
+ let items = [];
240
+
241
+ // Each item is handled individually.
242
+ for (let item of value) {
243
+ // Cross-recursion for styling values.
244
+ let display = `${ this.#improveValueDisplay(item) }`;
245
+ items.push(display);
246
+ }
247
+
248
+ // Items are all listed together.
249
+ let output = `Set{${ items.join(",") }}`;
250
+ return output;
251
+ }
252
+
253
+ #jsonifyIfClass(value) {
254
+ let isAFunction = value instanceof Function;
255
+
256
+ // If not a Function, also not a class.
257
+ if (!isAFunction) {
258
+ return value;
259
+ }
260
+
261
+ // Definition of a class always starts with `class` and space.
262
+ let definition = value.toString();
263
+ let isAClass = definition.startsWith(`class` + ` `);
264
+
265
+ // Not-a-class path.
266
+ if (!isAClass) {
267
+ return value;
268
+ }
269
+
270
+ // Class definitions have a .name.
271
+ let output = value.name;
272
+ return output;
273
+ }
274
+
275
+ #jsonifyIfFunction(value) {
276
+ // Not-a-function path.
277
+ if (!(value instanceof Function)) {
278
+ return value;
279
+ }
280
+
281
+ // toString() lists the definition, including code.
282
+ let output = value.toString();
283
+
284
+ // Most whitespace is eliminated for readability.
285
+ output = output.replace(/(\r\n?)/g, "");
286
+ output = output.replace(/\s{2,}/g, " ");
287
+
288
+ // Back to caller.
289
+ return output;
290
+ }
291
+
292
+ #jsonifyIfObject(value) {
293
+ // Not-an-object path.
294
+ if (!(value instanceof Object)) {
295
+ return value;
296
+ }
297
+
298
+ /* Use any meaningful toString() text for the object. */
299
+ let asString = value.toString();
300
+
301
+ if (asString !== "[object Object]") {
302
+ return asString;
303
+ }
304
+
305
+ /* If no meaningful toString() value,
306
+ use a list of all public members. */
307
+ let items = [];
308
+
309
+ // Each property of the object is handled individually.
310
+ for (let p in value) {
311
+ if (value[p] instanceof Function) {
312
+ items.push(`${ p }:${ value[p].toString() }`);
313
+ continue;
314
+ }
315
+
316
+ // Cross-recursion for nested items.
317
+ let item = this.#improveValueDisplay(value[p]);
318
+ items.push(`${ p }:${ item }`);
319
+ }
320
+
321
+ // Properties are all listed together.
322
+ let output = `{ ${ items.join(", ") } }`;
323
+ return output;
324
+ }
325
+
326
+ #stateUndefinedIfUndefined(value) {
327
+ let output = value !== undefined ? value : "undefined";
328
+ return output;
329
+ }
330
+
331
+ #stateNullIfNull(value) {
332
+ let output = value !== null ? value : "null";
333
+ return output;
334
+ }
335
+
336
+ // endregion Individual display improvements
337
+
338
+ }