risei 2.0.1 → 3.1.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.
@@ -6,26 +6,29 @@ import PropertySpoofer from "./PropertySpoofer.js";
6
6
  import TypeAnalyzer from "./TypeAnalyzer.js";
7
7
  import NameAnalyzer from "./NameAnalyzer.js";
8
8
  import TotalComparer from "./TotalComparer.js";
9
+ import CallTypes from "./CallTypes.js";
9
10
 
10
11
  export default class TestStages {
11
- /* &cruft, ? factor these to more classes */
12
-
13
- /* These methods hold operations carried out in the test frame,
14
- or before / after all test frames, in the test runner.
12
+ /* These methods hold reentrant operations carried
13
+ out in the test frame in the test runner.
15
14
  Not necessarily all test steps are found here. */
16
15
 
16
+ // region Components
17
+
17
18
  static #methodSpoofer = new MethodSpoofer();
18
19
  static #propertySpoofer = new PropertySpoofer();
19
20
  static #comparer = new TotalComparer();
20
21
 
21
- anyPreTargetingGroundwork(test) /* passed */ {
22
+ // endregion Components
23
+
24
+ async anyPreTargetingGroundwork(test) /* passed */ {
22
25
  // Spoofing methods; properties spoofed later.
23
26
  TestStages.#methodSpoofer.spoof(test);
24
27
 
25
28
  // Arbitrary user-defined actions.
26
29
  // Only spoofed methods available.
27
30
  if (test.doesHaveDoEarly) {
28
- test.do.early(test);
31
+ await test.do.early(test);
29
32
  }
30
33
  }
31
34
 
@@ -35,7 +38,7 @@ export default class TestStages {
35
38
  static or instance properties. */
36
39
 
37
40
  if (test.isConstructorTest) {
38
- test.target = new test.on.prototype.constructor(...test.in);
41
+ test.target = test.on.prototype;
39
42
  return;
40
43
  }
41
44
 
@@ -48,36 +51,49 @@ export default class TestStages {
48
51
 
49
52
  supplyLocalTarget(test) /* passed */ {
50
53
  /* Methods are called on instance / type / prototype directly.
51
- Properties are called on instance / type in a nonce. */
54
+ Properties are called on instance / type in a nonce.
55
+ Methods or properties are poly-called in a nonce. */
52
56
 
53
57
  let callable = NameAnalyzer.plainNameOf(test.method);
54
58
 
55
- let localTarget = test.isMethodTest
56
- ? test.target
57
- : { nonce: () => { return test.target[callable]; } };
59
+ switch (test.callType) {
60
+ case CallTypes.methodMono:
61
+ return test.target;
62
+ case CallTypes.propMono:
63
+ return { nonce: () => { return test.target[callable]; } };
64
+ case CallTypes.conMono:
65
+ return { nonce: (...args) => { return new test.target[callable](...args); } };
66
+ case CallTypes.methodPoly:
67
+ return { nonce: this.supplyPolyMethodNonce(test, callable) };
68
+ case CallTypes.propPoly:
69
+ return { nonce: this.supplyPolyPropNonce(test, callable) };
70
+ case CallTypes.conPoly:
71
+ return { nonce: this.supplyPolyConstructorNonce(test, callable) };
72
+ }
58
73
 
59
74
  return localTarget;
60
75
  }
61
76
 
62
77
  supplyCallableName(test) /* passed */ {
63
- /* Method under test is a method named in test,
64
- or a nonce method for a property test. */
78
+ /* Name is of the method named in test,
79
+ or of a nonce method for prop, con,
80
+ or poly-call test (of any target). */
65
81
 
66
- let name = test.isMethodTest
82
+ let name = test.isMethodTest && test.isMonoCallTest
67
83
  ? NameAnalyzer.plainNameOf(test.method)
68
84
  : TestDef.nonceLocalCallableName;
69
85
 
70
86
  return name;
71
87
  }
72
88
 
73
- anyPostTargetingGroundwork(test) /* passed */ {
89
+ async anyPostTargetingGroundwork(test) /* passed */ {
74
90
  // Spoofing properties; methods spoofed earlier.
75
91
  TestStages.#propertySpoofer.spoof(test);
76
92
 
77
93
  // Arbitrary user-defined actions.
78
94
  // All spoofs normally available.
79
95
  if (test.doesHaveDoLate) {
80
- test.do.late(test);
96
+ await test.do.late(test);
81
97
  }
82
98
  }
83
99
 
@@ -99,7 +115,7 @@ export default class TestStages {
99
115
  return TestStages.#comparer.compare(expected, actual);
100
116
  }
101
117
 
102
- anyGroundworkReversal(test) /* passed */ {
118
+ async anyGroundworkReversal(test) /* passed */ {
103
119
  // Unspoofing methods and properties.
104
120
  TestStages.#methodSpoofer.unspoof(test);
105
121
  TestStages.#propertySpoofer.unspoof(test);
@@ -107,12 +123,57 @@ export default class TestStages {
107
123
  // Arbitrary user-defined operations.
108
124
  // No spoofs are available here.
109
125
  if (test.doesHaveUndo) {
110
- test.undo(test);
126
+ await test.undo(test);
111
127
  }
112
128
  }
113
129
 
114
130
  // region Dependencies of test stages
115
131
 
132
+ supplyPolyMethodNonce(test, callable) /* verified */ {
133
+ let nonce = (...inputs) => {
134
+ let actuals = [];
135
+
136
+ for (let args of inputs) {
137
+ let local = test.target[callable](...args);
138
+ actuals.push(local);
139
+ }
140
+
141
+ return actuals;
142
+ };
143
+
144
+ return nonce;
145
+ }
146
+
147
+ supplyPolyPropNonce(test, callable) /* verified */ {
148
+ let nonce = (...inputs) => {
149
+ let actuals = [];
150
+
151
+ for (let args of inputs) {
152
+ let local = test.target[callable];
153
+ actuals.push(local);
154
+ }
155
+
156
+ return actuals;
157
+ };
158
+
159
+ return nonce;
160
+ }
161
+
162
+ supplyPolyConstructorNonce(test, callable) /* verified */ {
163
+ let nonce = (...inputs) => {
164
+ let actuals = [];
165
+
166
+ for (let args of inputs) {
167
+ let local = new test.target[callable](...args);
168
+ actuals.push(local);
169
+ }
170
+
171
+ return actuals;
172
+ };
173
+
174
+ return nonce;
175
+ }
176
+
116
177
  supplyNonReturnActual(test) /* passed */ {
117
178
  // When .from is a string, the actual
118
179
  // is the named target or type member.
@@ -125,28 +186,26 @@ export default class TestStages {
125
186
  ? test.target
126
187
  : test.type;
127
188
 
189
+ // Constructors are different.
190
+ host = test.isConstructorTest
191
+ ? test.actual
192
+ : host;
193
+
128
194
  // Actually supplying value.
129
195
  return host[name];
130
196
  }
131
197
 
132
- /* &cruft, possibly change all .from syntax
133
- to ? (actual, test), or ? (test) */
134
198
  // When there is a .from that's a function,
135
199
  // the actual is the result of calling it,
136
200
  // given everything that might be needed.
137
201
  if (test.from instanceof Function) {
138
202
  // Actually supplying value.
139
- return test.from(test.target, test);
203
+ return test.from(test, test.actual);
140
204
  }
141
205
 
142
206
  // Only property names and functions make sense to support.
143
207
  throw new Error("The test.from value was not a usable type. It must be either a property name or a function to work.");
144
208
  }
145
209
 
146
- #compare(expected, actual) /* verified */ {
147
- return this.#comparer.compare(expected, actual);
148
- }
149
-
150
210
  // endregion Dependencies of test stages
151
-
152
211
  }
@@ -1,37 +1,38 @@
1
- /**/
2
-
3
- export default 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
- }
1
+ /**/
2
+
3
+ export default class TestSummary {
4
+ #summary;
5
+ #allDidPass;
6
+ #anyWereRun;
7
+
8
+ #anyThrows;
9
+ #anyTroubles;
10
+
11
+ get summary() {
12
+ return this.#summary;
13
+ }
14
+
15
+ get allDidPass() {
16
+ return this.#allDidPass;
17
+ }
18
+
19
+ get anyWereRun() {
20
+ return this.#anyWereRun;
21
+ }
22
+
23
+ get anyThrows() {
24
+ return this.#anyThrows;
25
+ }
26
+
27
+ get anyTroubles() {
28
+ return this.#anyTroubles;
29
+ }
30
+
31
+ constructor(summary, allDidPass, anyWereRun, anyThrows, anyTroubles) {
32
+ this.#summary = summary;
33
+ this.#allDidPass = allDidPass;
34
+ this.#anyWereRun = anyWereRun;
35
+ this.#anyThrows = anyThrows;
36
+ this.#anyTroubles = anyTroubles;
37
+ }
38
+ }
@@ -1,6 +1,7 @@
1
1
  /**/
2
2
 
3
3
  import ATestSource from "./ATestSource.js";
4
+ import ObjectAnalyzer from "./ObjectAnalyzer.js";
4
5
 
5
6
  export default class TotalComparer {
6
7
  constructor() {
@@ -65,26 +66,31 @@ export default class TotalComparer {
65
66
  if (!(actual instanceof Object)) {
66
67
  return false;
67
68
  }
68
-
69
- // There must not be any extra members
70
- // of `actual`, its own or inherited.
71
- for (let propertyName in actual) {
72
- if (!(propertyName in expected)) {
73
- return false;
74
- }
69
+
70
+ let expectedNames = ObjectAnalyzer.allProperties(expected);
71
+ let actualNames = ObjectAnalyzer.allProperties(actual);
72
+
73
+ // Objects with different numbers of properties can't be the same.
74
+ if (expectedNames.length !== actualNames.length) {
75
+ return false;
75
76
  }
76
77
 
77
- // All members of `expected`, its own or inherited,
78
- // must exist in `actual` and match it recursively.
79
- for (let propertyName in expected) {
80
- // Members present must match.
81
- if (!(propertyName in actual)) {
78
+ // O(n) algorithm covers all other cases, including recursion.
79
+ for (let at = 0; at < expectedNames.length; at++) {
80
+ // Objects with differing property names can't be the same.
81
+ if (expectedNames[at] !== actualNames[at]) {
82
82
  return false;
83
83
  }
84
-
84
+
85
+ // Same property name in both objects.
86
+ let name = expectedNames[at];
87
+
88
+ // Individual props, or their throws if getters that fail.
89
+ let paired = this.#retrieveExpectedAndActualProps(name, expected, actual);
90
+
85
91
  // Cross-recursion.
86
- let areEqual = this.#recurse(expected[propertyName], actual[propertyName]);
87
-
92
+ let areEqual = this.#recurse(paired.expected, paired.actual);
93
+
88
94
  // Exit only at first false.
89
95
  switch (areEqual) {
90
96
  case true: continue;
@@ -95,7 +101,32 @@ export default class TotalComparer {
95
101
  // No mismatched or missing found.
96
102
  return true;
97
103
  }
98
-
104
+
105
+ /* Dependency of #recurseOverObjects(). */
106
+ #retrieveExpectedAndActualProps(name, expected, actual) {
107
+ /* Expected and actual prop values are retrieved in individual
108
+ try-catches because getters may fail, and this way,
109
+ any fails can be compared as indicators of overall state. */
110
+
111
+ let paired = { };
112
+
113
+ try {
114
+ paired.expected = expected[name];
115
+ }
116
+ catch (thrown) {
117
+ paired.expected = thrown;
118
+ }
119
+
120
+ try {
121
+ paired.actual = actual[name];
122
+ }
123
+ catch (thrown) {
124
+ paired.actual = thrown;
125
+ }
126
+
127
+ return paired;
128
+ }
129
+
99
130
  #recurseOverMaps(expected, actual) /* verified */ {
100
131
  // Both must be Maps.
101
132
  if (!(actual instanceof Map)) {
@@ -229,7 +260,7 @@ export default class TotalComparer {
229
260
  }
230
261
 
231
262
  #compareFunctions(expected, actual) /* verified */ {
232
- // `expected` is already known to be a function / method.
263
+ // 'expected' is already known to be a function / method.
233
264
  if (!(actual instanceof Function)) {
234
265
  return false;
235
266
  }
@@ -253,6 +284,7 @@ export default class TotalComparer {
253
284
  }
254
285
 
255
286
  #compareDates(expected, actual) /* verified */ {
287
+ // 'expected' is already known to be a Date.
256
288
  if (!(actual instanceof Date)) {
257
289
  return false;
258
290
  }
@@ -261,6 +293,11 @@ export default class TotalComparer {
261
293
  }
262
294
 
263
295
  #compareErrors(expected, actual) /* verified */ {
296
+ // 'expected' is already known to be an Error.
297
+ if (!(actual instanceof Error)) {
298
+ return false;
299
+ }
300
+
264
301
  /* Compares Errors and subclasses accurately apart from .stack,
265
302
  which is ignored because it rarely makes sense to compare it. */
266
303
 
@@ -3,23 +3,27 @@
3
3
  import TypeIdentifier from "./TypeIdentifier.js";
4
4
  import Types from "./Types.js";
5
5
  import ATestSource from "./ATestSource.js";
6
+ import ObjectAnalyzer from "./ObjectAnalyzer.js";
6
7
 
7
8
  export default class TotalDisplayer {
8
- display(value) /* good */ {
9
+ static defaultToString = "[object Object]";
10
+
11
+ display(value) /* passed */ {
9
12
  let typeId = TypeIdentifier.identify(value);
10
13
 
11
14
  switch (typeId) {
15
+ case Types.isUndefined: return this.displayUndefined(value);
12
16
  case Types.isValue: return this.displayValue(value);
13
17
  case Types.isString: return this.displayString(value);
14
18
  case Types.isArray: return this.displayArray(value);
15
19
  case Types.isMap: return this.displayMap(value);
16
20
  case Types.isSet: return this.displaySet(value);
21
+ case Types.isFile: return this.displayFile(value);
17
22
  case Types.isError: return this.displayError(value);
18
23
  case Types.isClass: return this.displayClass(value);
19
24
  case Types.isFunction: return this.displayFunction(value);
20
25
  case Types.isObject: return this.displayObject(value);
21
26
  case Types.isDate: return this.displayValue(value);
22
- case Types.isUndefined: return this.displayUndefined(value);
23
27
  case Types.isUndefSymbol: return this.displayUndefined(value);
24
28
  case Types.isNull: return this.displayNull(value);
25
29
  }
@@ -82,6 +86,10 @@ export default class TotalDisplayer {
82
86
  return output;
83
87
  }
84
88
 
89
+ displayFile(value) /* passed */ {
90
+ return `File{name:"${ value.name }",size:${ value.size }}`;
91
+ }
92
+
85
93
  displayError(value) /* passed */ {
86
94
  // Subtype can't be found with typeof.
87
95
  let type = value.name;
@@ -125,13 +133,28 @@ export default class TotalDisplayer {
125
133
  }
126
134
 
127
135
  displayObject(value) /* passed */ {
136
+ // If a custom toString() exists, use its output.
137
+ let asString = value.toString();
138
+
139
+ if (asString !== TotalDisplayer.defaultToString) {
140
+ return asString;
141
+ }
142
+
128
143
  // Each property of the object is handled individually.
129
144
  let items = [ ];
130
145
 
131
- for (let p in value) {
132
- // Cross-recursion.
133
- let item = this.display(value[p]);
134
- items.push(`${ p }:${ item }`);
146
+ let props = ObjectAnalyzer.allProperties(value);
147
+
148
+ for (let prop of props) {
149
+ // Cross-recursion. Try-catch because getters can fail.
150
+ try {
151
+ let item = this.display(value[prop]);
152
+ items.push(`${ prop }:${ item }`);
153
+ }
154
+ catch (e)
155
+ {
156
+ items.push(`${ prop }:(threw)`);
157
+ }
135
158
  }
136
159
 
137
160
  // Properties are all listed together.
@@ -45,40 +45,131 @@ export default class TypeAnalyzer {
45
45
  /* Returns true if instance or static member is a method.
46
46
  Returns false if member is any kind of property. */
47
47
  static isMethodMember(type, name) /* passed */ {
48
- /* A method is always defined either on a type or
49
- its prototype, even if declared on a superclass. */
48
+ return !TypeAnalyzer.isPropertyMember(type, name);
49
+ }
50
50
 
51
- try {
52
- let anyInstanceType = TypeIdentifier.identify(type.prototype[name]);
53
- let anyStaticType = TypeIdentifier.identify(type[name]);
51
+ /* Returns true if instance or static member is a property
52
+ (value or accessor). Returns false if member is a method. */
53
+ static isPropertyMember(type, name) /* passed */ {
54
+ /* Type and supertypes are tested directly for static props,
55
+ and their definition texts are read for instance props. */
54
56
 
55
- /* Excludes class "functions", which are not methods. */
56
- let isInstanceMethod = anyInstanceType === Types.isFunction;
57
- let isStaticMethod = anyStaticType === Types.isFunction;
57
+ let isProperty = this.#typeChainIncludesAsProperty(type, name);
58
58
 
59
- /* Returns net method-ness; if not found at all, a property
60
- is assumed (true as long as the member exists at all.) */
61
- return isInstanceMethod || isStaticMethod;
62
- }
63
- catch (thrown) {
64
- /* If private fields are used behind public accessor properties,
65
- there's a characteristic message which shows it's not a method.
66
- It may be a property returning a function, but not a method. */
67
- if (thrown.message.startsWith(TypeAnalyzer.accessorOfPrivate)) {
68
- return false;
59
+ return isProperty;
60
+ }
61
+
62
+ // region Dependencies of isPropertyMember()
63
+
64
+ static #typeChainIncludesAsProperty(type, name) /* verified */ {
65
+ let prototype = type.prototype;
66
+
67
+ // Rootward traversal of the prototype chain.
68
+ while (prototype !== null) {
69
+ // Type is needed, rather than prototype.
70
+ type = prototype.constructor;
71
+
72
+ // First line finds any static props; second, any instance props.
73
+ let isProperty
74
+ = TypeAnalyzer.#typeDefinitionIncludesAsProperty(type, name)
75
+ || TypeAnalyzer.#typeTextContainsNameAsProperty(type, name);
76
+
77
+ // Was found as static or instance.
78
+ if (isProperty) {
79
+ return true;
69
80
  }
70
81
 
71
- /* Other throws are unpredictable and mean there's a problem. */
72
- throw thrown;
82
+ // Parent class, or null at root.
83
+ prototype = Object.getPrototypeOf(prototype);
73
84
  }
74
85
 
75
- /* Probably never reached. No previous throw, but some unknown problem. */
76
- throw new Error("Unable to determine method vs. property state.");
86
+ // Never found.
87
+ return false;
77
88
  }
78
89
 
79
- /* Returns true if instance or static member is a property
80
- (value or accessor). Returns false if member is a method. */
81
- static isPropertyMember(type, name) /* passed */ {
82
- return !TypeAnalyzer.isMethodMember(type, name);
90
+ static #typeDefinitionIncludesAsProperty(type, name) /* verified */ {
91
+ let named = Object.getOwnPropertyDescriptor(type, name);
92
+
93
+ // If not found, try parent.
94
+ if (named === undefined) {
95
+ return false;
96
+ }
97
+
98
+ // Members with getters are properties.
99
+ if (named.get !== undefined) {
100
+ return true;
101
+ }
102
+
103
+ // Members with function values are methods.
104
+ if (named.value instanceof Function) {
105
+ return false;
106
+ }
107
+
108
+ // Members with any other values are properties.
109
+ if (named.value !== undefined) {
110
+ return true;
111
+ }
112
+ }
113
+
114
+ static #typeTextContainsNameAsProperty(type, name) /* verified */ {
115
+ let text = type.toString();
116
+
117
+ // Instance properties are always defined as 'name =',
118
+ // 'name;', or 'get name()' at the start of a line.
119
+ let valueRegex = new RegExp(`^\\s*${ name }\\s*[=;]`, "m");
120
+ let accessorRegex = new RegExp(`^\\s*get ${ name }\\(\\)`, "m");
121
+
122
+ // Conditional execution of the second test() call.
123
+ let doesContain = valueRegex.test(text) || accessorRegex.test(text);
124
+ return doesContain;
125
+ }
126
+
127
+ // endregion Dependencies of isPropertyMember()
128
+
129
+ static allGetters(type) /* passed */ {
130
+ return TypeAnalyzer.#allTypeChainGetters(type);
131
+ }
132
+
133
+ // region Dependencies of allGetters()
134
+
135
+ static #allTypeChainGetters(type) /* verified */ {
136
+ let output = [];
137
+
138
+ let prototype = type.prototype;
139
+
140
+ // Rootward traversal of the prototype chain.
141
+ while (prototype !== null) {
142
+ // Type is needed, rather than prototype.
143
+ type = prototype.constructor;
144
+
145
+ // Any getters at this level.
146
+ let local = TypeAnalyzer.#gettersInTypeText(type);
147
+ output.push(...local);
148
+
149
+ // Parent class, or null at root.
150
+ prototype = Object.getPrototypeOf(prototype);
151
+ }
152
+
153
+ return output;
154
+ }
155
+
156
+ static #gettersInTypeText(type) /* verified */ {
157
+ let text = type.toString();
158
+
159
+ // At start of line's text, get + name + ().
160
+ let getterRegex = /^\s*get\s+(\w+)\(\)/gm;
161
+
162
+ // All getters and their names
163
+ // as an array of arrays.
164
+ let raw = text.matchAll(getterRegex);
165
+ raw = [ ...raw ];
166
+
167
+ // First capturing group is name.
168
+ let names = raw.map(x => x[1]);
169
+
170
+ return names;
83
171
  }
172
+
173
+ // endregion Dependencies of allGetters()
174
+
84
175
  }
@@ -14,27 +14,31 @@ export default class TypeIdentifier {
14
14
  if (Array.isArray(value)) {
15
15
  return Types.isArray;
16
16
  }
17
-
17
+
18
18
  if (value instanceof Map) {
19
19
  return Types.isMap;
20
20
  }
21
-
21
+
22
22
  if (value instanceof Set) {
23
23
  return Types.isSet;
24
24
  }
25
-
25
+
26
+ if (value instanceof File) {
27
+ return Types.isFile;
28
+ }
29
+
26
30
  if (value instanceof Date) {
27
31
  return Types.isDate;
28
32
  }
29
-
33
+
30
34
  if (value instanceof Error) {
31
35
  return Types.isError;
32
36
  }
33
-
37
+
34
38
  if (value === null) {
35
39
  return Types.isNull;
36
40
  }
37
-
41
+
38
42
  return Types.isObject;
39
43
  }
40
44
 
package/system/Types.js CHANGED
@@ -9,6 +9,7 @@ export default class Types {
9
9
  static isObject = "object";
10
10
  static isMap = "map";
11
11
  static isSet = "set";
12
+ static isFile = "file";
12
13
  static isFunction = "function";
13
14
  static isClass = "class";
14
15
  static isDate = "date";