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.
- package/LICENSE.txt +8 -0
- package/README.md +642 -0
- package/index.js +122 -0
- package/package.json +39 -0
- package/public/javascript/AComparer.js +9 -0
- package/public/javascript/ASpoofingFixture.js +84 -0
- package/public/javascript/ATestCaller.js +61 -0
- package/public/javascript/ATestFinder.js +25 -0
- package/public/javascript/ATestFixture.js +44 -0
- package/public/javascript/ATestReporter.js +25 -0
- package/public/javascript/ATestSource.js +8 -0
- package/public/javascript/ChosenTestFinder.js +30 -0
- package/public/javascript/ClassTestGroup.js +8 -0
- package/public/javascript/LocalCaller.js +22 -0
- package/public/javascript/MethodTestGroup.js +8 -0
- package/public/javascript/Moment.js +29 -0
- package/public/javascript/Risei.js +88 -0
- package/public/javascript/SpoofClassMethodsFixture.js +165 -0
- package/public/javascript/SpoofObjectMethodsFixture.js +52 -0
- package/public/javascript/SpoofTuple.js +238 -0
- package/public/javascript/TerminalReporter.js +222 -0
- package/public/javascript/TestFinder.js +140 -0
- package/public/javascript/TestGroup.js +25 -0
- package/public/javascript/TestResult.js +338 -0
- package/public/javascript/TestRunner.js +476 -0
- package/public/javascript/TestSummary.js +37 -0
- package/public/javascript/TestTuple.js +244 -0
- package/public/javascript/TotalComparer.js +229 -0
- package/usage-examples/Output-example.png +0 -0
- package/usage-examples/Summary-example.png +0 -0
- package/usage-examples/Syntax-example.png +0 -0
|
@@ -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
|
+
}
|