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,476 @@
1
+ /**/
2
+
3
+ /* Runs tests one by one as an ATestCaller requests them. */
4
+
5
+ import { TestTuple } from "./TestTuple.js";
6
+ import { TestGroup } from "./TestGroup.js";
7
+ import { ClassTestGroup } from "./ClassTestGroup.js";
8
+ import { MethodTestGroup } from "./MethodTestGroup.js";
9
+ import { TestResult } from "./TestResult.js";
10
+ import { TestSummary } from "./TestSummary.js";
11
+
12
+ import { SpoofClassMethodsFixture } from "./SpoofClassMethodsFixture.js";
13
+ import { SpoofObjectMethodsFixture } from "./SpoofObjectMethodsFixture.js";
14
+ import { SpoofTuple } from "./SpoofTuple.js";
15
+
16
+ import { AComparer } from "./AComparer.js";
17
+ import { TotalComparer } from "./TotalComparer.js";
18
+
19
+ export class TestRunner {
20
+ // region Static definition fields
21
+
22
+ // Used for a special test structural case;
23
+ // classes have one constructor by this name.
24
+ static #constructorName = "constructor";
25
+
26
+ // endregion Static definition fields
27
+
28
+ // region Private fields
29
+
30
+ // Components and state.
31
+ #tests;
32
+
33
+ #classSpoofer;
34
+ #instanceSpoofer;
35
+
36
+ #comparer;
37
+
38
+ // Results summary.
39
+ #numberRun;
40
+ #numberPassed;
41
+ #numberFailed;
42
+ #allDidPass;
43
+
44
+ // endregion Private fields
45
+
46
+ // region Properties
47
+
48
+ get tests() {
49
+ return this.#tests;
50
+ }
51
+
52
+ set tests(value) {
53
+ this.#tests = value;
54
+ }
55
+
56
+ // region Spoofing
57
+
58
+ get classSpoofer() {
59
+ return this.#classSpoofer;
60
+ }
61
+
62
+ set classSpoofer(value) {
63
+ this.#classSpoofer = value;
64
+ }
65
+
66
+ get instanceSpoofer() {
67
+ return this.#instanceSpoofer;
68
+ }
69
+
70
+ set instanceSpoofer(value) {
71
+ this.#instanceSpoofer = value;
72
+ }
73
+
74
+ // endregion Spoofing
75
+
76
+ get comparer() {
77
+ return this.#comparer;
78
+ }
79
+
80
+ set comparer(value) {
81
+ this.#comparer = value;
82
+ }
83
+
84
+ get numberRun() {
85
+ return this.#numberRun;
86
+ }
87
+
88
+ set numberRun(value) {
89
+ this.#numberRun = value;
90
+ }
91
+
92
+ get numberPassed() {
93
+ return this.#numberPassed;
94
+ }
95
+
96
+ set numberPassed(value) {
97
+ this.#numberPassed = value;
98
+ }
99
+
100
+ get numberFailed() {
101
+ return this.#numberFailed;
102
+ }
103
+
104
+ set numberFailed(value) {
105
+ this.#numberFailed = value;
106
+ }
107
+
108
+ get allDidPass() {
109
+ return this.#allDidPass;
110
+ }
111
+
112
+ set allDidPass(value) {
113
+ this.#allDidPass = value;
114
+ }
115
+
116
+ // endregion Properties
117
+
118
+ constructor() {
119
+ this.#tests = [];
120
+ this.#classSpoofer = new SpoofClassMethodsFixture();
121
+ this.#instanceSpoofer = new SpoofObjectMethodsFixture();
122
+
123
+ this.#numberRun = 0;
124
+ this.#numberPassed = 0;
125
+ this.#numberFailed = 0;
126
+
127
+ this.#allDidPass = true;
128
+
129
+ // Comparer is inited once for all tests, since compare() is reentrant.
130
+ this.#comparer = new TotalComparer();
131
+ }
132
+
133
+ // region Tests to run
134
+
135
+ useTests(tests) {
136
+ this.#tests = tests;
137
+ }
138
+
139
+ // endregion Tests to run
140
+
141
+ // region Running tests
142
+
143
+ // Generator for all test results,
144
+ // including groups and summary.
145
+ * [Symbol.iterator]() {
146
+ if (!this.#tests) {
147
+ throw new Error("No tests available to run. TestRunner's .tests must be set before an attempt to run is made.");
148
+ }
149
+
150
+ // Converting so that property names can be relied on.
151
+ this.#tests = TestTuple.fromNonceTuples(this.tests);
152
+
153
+ // Needed for displaying classes and methods as groups.
154
+ let classGroup = new ClassTestGroup();
155
+ let methodGroup = new MethodTestGroup();
156
+ let atFirstClassMethod = false;
157
+
158
+ // Iterative running of all tests in current order.
159
+ for (let test of this.#tests) {
160
+ // Each new class should be a group, and its start
161
+ // should be retained for grouping of methods.
162
+ if (test.on.name !== classGroup.group) {
163
+ classGroup.group = test.on.name;
164
+ atFirstClassMethod = true;
165
+ yield classGroup;
166
+ }
167
+
168
+ // Each new method name should be a group,
169
+ // and so should each method for a class.
170
+ if (test.of !== methodGroup.group || atFirstClassMethod) {
171
+ methodGroup.group = test.of;
172
+ atFirstClassMethod = false;
173
+ yield methodGroup;
174
+ }
175
+
176
+ let result = this.runOneTest(test);
177
+ yield result;
178
+ }
179
+
180
+ // Iterative returning of final results summary.
181
+ yield this.summarize();
182
+ }
183
+
184
+ runOneTest(test) {
185
+ // Default outputs. Never left undefined.
186
+ test.didPass = false;
187
+ test.anyThrow = null;
188
+
189
+ // Spoofing based on any spoof
190
+ // definitions in .with and .of.
191
+ this.#spoofObjects(test);
192
+
193
+ // Gathering facts defining the nature of the test,
194
+ // some of which might change after the test is run.
195
+ let result = new TestResult(test);
196
+ result.setNature();
197
+
198
+ // Choosing the right test frame for the definition.
199
+ let testFrame = this.#supplyTestFrame(test);
200
+ testFrame = testFrame.bind(this);
201
+
202
+ // Actually running the test.
203
+ try {
204
+ testFrame(test);
205
+ }
206
+ catch (thrown) {
207
+ test.anyThrow = thrown;
208
+ }
209
+
210
+ // Gathering facts based on the test run.
211
+ result.setResults();
212
+
213
+ // For later summarizing.
214
+ this.#retainTestResults(test);
215
+
216
+ // Results and test back to the caller.
217
+ return result;
218
+ }
219
+
220
+ // region Dependencies of runOneTest()
221
+
222
+ // Spoofs objects as defined in TestTuple's
223
+ // .with and .in, which can contain spoofs.
224
+ #spoofObjects(test) {
225
+ this.#instanceSpoofer.spoof(test);
226
+ }
227
+
228
+ #supplyTestFrame(test) {
229
+ // If `.and` includes "static", a call of a static member
230
+ // is made, and the actual is its return value.
231
+ if (typeof test.and === "string") {
232
+ if (this.#doesAddressStatics(test)) {
233
+ // If .from, a static call is made, then the actual is
234
+ // retrieved from a named code element or custom code.
235
+ if (test.from !== undefined) {
236
+ return this.testCodeElementAfterStaticCall;
237
+ }
238
+
239
+ // Otherwise, the return value
240
+ // of the static call is used.
241
+ return this.testReturnValueOfStaticCall;
242
+ }
243
+ }
244
+
245
+ // Constructors are a special case; no second call should
246
+ // be made, and actual must be retrieved from property.
247
+ if (test.of === TestRunner.#constructorName) {
248
+ return this.testCodeElementAfterConstruction;
249
+ }
250
+
251
+ // If .from, a call of the target is made, then the actual
252
+ // is retrieved from a named code element or custom code.
253
+ if (this.#isRetrievalTest(test.from)) {
254
+ return this.testCodeElementAfterCall;
255
+ }
256
+
257
+ // The most common case: the target is called,
258
+ // and the actual is its return value.
259
+ return this.testReturnValueOfCall;
260
+ }
261
+
262
+ #isRetrievalTest(testFrom) {
263
+ // Falsy code elements never are a retrieval.
264
+ if (!testFrom) {
265
+ return false;
266
+ }
267
+
268
+ // If a (non-falsy) string, must be a retrieval.
269
+ if (typeof testFrom === "string") {
270
+ return true;
271
+ }
272
+
273
+ // If a function, is a retrieval.
274
+ if (testFrom instanceof Function) {
275
+ return true;
276
+ }
277
+
278
+ // Anything else is not a retrieval.
279
+ return false;
280
+ }
281
+
282
+ #retainTestResults(test) {
283
+ this.#numberRun++;
284
+
285
+ if (test.didPass) {
286
+ this.#numberPassed++;
287
+ }
288
+ else {
289
+ this.#numberFailed++;
290
+ }
291
+
292
+ this.#allDidPass &= test.didPass;
293
+ }
294
+
295
+ // endregion Dependencies of runOneTest()
296
+
297
+ summarize() {
298
+ let summary = `Ran ${ this.#numberRun } test${ this.#numberRun !== 1 ? "s" : "" }. `
299
+ + `${ this.#allDidPass ? "All tests passed. " : "" }`
300
+ + `${ this.#numberPassed } passed. `
301
+ + `${ this.#numberFailed } failed.`;
302
+
303
+ let anyWereRun = this.#numberRun > 0;
304
+
305
+ return new TestSummary(summary, this.#allDidPass, anyWereRun);
306
+ }
307
+
308
+ // endregion Running tests
309
+
310
+ // region Test frames
311
+
312
+ // The most basic test: whether a call of
313
+ // a method returns the expected value.
314
+ testReturnValueOfCall(test) {
315
+ /* Groundwork. */
316
+ this.#anyMethodSpoofing(test);
317
+
318
+ let prototype = test.on.prototype;
319
+ let target = new prototype.constructor(...test.with); // Must use `...`.
320
+
321
+ /* Exercising the code. */
322
+ test.actual = target[test.of](...test.in); // Must use `...`.
323
+
324
+ /* Restoring. */
325
+ this.#anyMethodRestoring(test);
326
+
327
+ /* Comparing. */
328
+ test.didPass = this.#compare(test.out, test.actual);
329
+ }
330
+
331
+ testReturnValueOfStaticCall(test) {
332
+ /* Groundwork. */
333
+ this.#anyMethodSpoofing(test);
334
+
335
+ /* No instance is constructed. */
336
+
337
+ /* Exercising the code. */
338
+ test.actual = test.on[test.of](...test.in); // Must use `...`.
339
+
340
+ /* Restoring. */
341
+ this.#anyMethodRestoring(test);
342
+
343
+ /* Comparing. */
344
+ test.didPass = this.#compare(test.out, test.actual);
345
+ }
346
+
347
+ // The second-most basic test: whether a property
348
+ // has the expected value after a method call.
349
+ testCodeElementAfterCall(test) {
350
+ /* Groundwork. */
351
+ this.#anyMethodSpoofing(test);
352
+
353
+ let prototype = test.on.prototype;
354
+ let target = new prototype.constructor(...test.with); // Must use `...`.
355
+
356
+ /* Exercising the code. */
357
+ target[test.of](...test.in); // Must use `...`.
358
+
359
+ /* Retrieving. */
360
+ // The `actual` is a property or other code element somewhere.
361
+ test.actual = this.#supplyNonReturnActual(test, target);
362
+
363
+ /* Restoring. */
364
+ this.#anyMethodRestoring(test);
365
+
366
+ /* Comparing. */
367
+ test.didPass = this.#compare(test.out, test.actual);
368
+ }
369
+
370
+ testCodeElementAfterStaticCall(test) {
371
+ /* Groundwork. */
372
+ this.#anyMethodSpoofing(test);
373
+
374
+ /* No instance is constructed. */
375
+
376
+ /* Exercising the code. */
377
+ test.on[test.of](...test.in); // Must use `...`.
378
+
379
+ /* Retrieving. */
380
+ test.actual = this.#supplyNonReturnActual(test); // No `target` here.
381
+
382
+ /* Restoring. */
383
+ this.#anyMethodRestoring(test);
384
+
385
+ /* Comparing. */
386
+ test.didPass = this.#compare(test.out, test.actual);
387
+ }
388
+
389
+ // The third-most basic test: whether a property has
390
+ // the expected value after an object is constructed.
391
+ testCodeElementAfterConstruction(test) {
392
+ /* Groundwork. */
393
+ this.#anyMethodSpoofing(test);
394
+
395
+ /* Exercising the code. */
396
+ // For definition consistency, initing with test.in, not test.with.
397
+ let prototype = test.on.prototype;
398
+ let target = new prototype.constructor(...test.in); // Must use `...`.
399
+
400
+ /* Retrieving. */
401
+ // The `actual` is a property or other code element somewhere.
402
+ test.actual = this.#supplyNonReturnActual(test, target);
403
+
404
+ /* Restoring. */
405
+ this.#anyMethodRestoring(test);
406
+
407
+ /* Comparing. */
408
+ test.didPass = this.#compare(test.out, test.actual);
409
+ }
410
+
411
+ // endregion Test frames
412
+
413
+ // region Dependencies of test frames
414
+
415
+ #anyMethodSpoofing(test) {
416
+ if (test.plus === undefined) {
417
+ return;
418
+ }
419
+
420
+ this.#classSpoofer.spoof(test);
421
+ }
422
+
423
+ #compare(expected, actual) {
424
+ return this.#comparer.compare(expected, actual);
425
+ }
426
+
427
+ #supplyNonReturnActual(test, target) {
428
+ // When there is a .from that's a string,
429
+ // the actual is the named target member.
430
+ if (typeof test.from === "string") {
431
+ // Common member host;
432
+ // undefined if static.
433
+ let host = target;
434
+
435
+ // When .and defines static.
436
+ if (this.#doesAddressStatics(test)) {
437
+ host = test.on;
438
+ }
439
+
440
+ return host[test.from];
441
+ }
442
+
443
+ // When there is a .from that's a function,
444
+ // the actual is the result of calling it,
445
+ // given everything that might be needed.
446
+ if (test.from instanceof Function) {
447
+ return test.from(target, test);
448
+ }
449
+
450
+ // When there is any other .from, the actual is the
451
+ // value of the code element provided as .from.
452
+ return test.from;
453
+ }
454
+
455
+ #anyMethodRestoring(test) {
456
+ this.#classSpoofer.unspoof();
457
+ this.#classSpoofer.removeDefinitions();
458
+ }
459
+
460
+ // endregion Dependencies of test frames
461
+
462
+ // region Cross-region dependencies
463
+
464
+ #doesAddressStatics(test) {
465
+ if (typeof test.and === "string") {
466
+ if (test.and.includes("static")) { /* &cruft, abstract */
467
+ return true;
468
+ }
469
+ }
470
+
471
+ return false;
472
+ }
473
+
474
+ // endregion Cross-region dependencies
475
+
476
+ }
@@ -0,0 +1,37 @@
1
+ /**/
2
+
3
+ export class TestSummary {
4
+ #summary;
5
+ #allDidPass;
6
+ #anyWereRun;
7
+
8
+ get summary() {
9
+ return this.#summary;
10
+ }
11
+
12
+ set summary(value) {
13
+ this.#summary = value;
14
+ }
15
+
16
+ get allDidPass() {
17
+ return this.#allDidPass;
18
+ }
19
+
20
+ set allDidPass(value) {
21
+ this.#allDidPass = value;
22
+ }
23
+
24
+ get anyWereRun() {
25
+ return this.#anyWereRun;
26
+ }
27
+
28
+ set anyWereRun(value) {
29
+ this.#anyWereRun = value;
30
+ }
31
+
32
+ constructor(summary, allDidPass, anyWereRun) {
33
+ this.summary = summary;
34
+ this.allDidPass = allDidPass;
35
+ this.anyWereRun = anyWereRun;
36
+ }
37
+ }