as-test 1.2.0 → 1.4.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/CHANGELOG.md +60 -0
- package/README.md +1 -4
- package/as-test.config.schema.json +15 -0
- package/assembly/coverage.ts +22 -26
- package/assembly/index.ts +68 -47
- package/assembly/src/expectation.ts +154 -123
- package/assembly/src/fuzz.ts +10 -10
- package/assembly/src/log.ts +3 -3
- package/assembly/src/mode.ts +55 -0
- package/assembly/src/reflect.ts +122 -0
- package/assembly/src/stringify.ts +240 -0
- package/assembly/src/suite.ts +48 -27
- package/assembly/src/tests.ts +7 -7
- package/assembly/util/wipc.ts +2 -2
- package/bin/build-worker-pool.js +9 -0
- package/bin/build-worker.js +27 -3
- package/bin/commands/build-core.js +293 -86
- package/bin/commands/build.js +3 -1
- package/bin/commands/init-core.js +253 -8
- package/bin/commands/run-core.js +165 -41
- package/bin/commands/run.js +2 -1
- package/bin/commands/test.js +3 -2
- package/bin/dependency-graph.js +0 -0
- package/bin/index.js +592 -97
- package/bin/reporters/default.js +34 -0
- package/bin/types.js +7 -0
- package/bin/util.js +52 -0
- package/package.json +4 -8
- package/transform/lib/equals.js +388 -0
- package/transform/lib/index.js +28 -0
- package/transform/lib/log.js +3 -7
- package/transform/lib/types.js +4 -2
- package/transform/lib/transform.js +0 -502
package/bin/reporters/default.js
CHANGED
|
@@ -17,6 +17,7 @@ class DefaultReporter {
|
|
|
17
17
|
this.fileHasWarning = false;
|
|
18
18
|
this.verboseMode = false;
|
|
19
19
|
this.cleanMode = false;
|
|
20
|
+
this.showLogsMode = false;
|
|
20
21
|
this.hasRenderedTestFiles = false;
|
|
21
22
|
this.hasRenderedFuzzFiles = false;
|
|
22
23
|
}
|
|
@@ -127,6 +128,7 @@ class DefaultReporter {
|
|
|
127
128
|
onRunStart(event) {
|
|
128
129
|
this.verboseMode = Boolean(event.verbose);
|
|
129
130
|
this.cleanMode = Boolean(event.clean);
|
|
131
|
+
this.showLogsMode = Boolean(event.showLogs);
|
|
130
132
|
this.hasRenderedTestFiles = false;
|
|
131
133
|
this.hasRenderedFuzzFiles = false;
|
|
132
134
|
}
|
|
@@ -270,6 +272,9 @@ class DefaultReporter {
|
|
|
270
272
|
}
|
|
271
273
|
onLog(event) {
|
|
272
274
|
if (this.cleanMode) return;
|
|
275
|
+
// With --show-logs we print one clean grouped block at the end instead of
|
|
276
|
+
// streaming inline, so suppress the inline emit here.
|
|
277
|
+
if (this.showLogsMode) return;
|
|
273
278
|
if (this.verboseMode || !this.canRewriteLine()) {
|
|
274
279
|
const depth = Math.max(event.depth, 0);
|
|
275
280
|
this.context.stdout.write(
|
|
@@ -295,6 +300,35 @@ class DefaultReporter {
|
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
302
|
renderTotals(event.stats, event);
|
|
303
|
+
this.renderLogs(event);
|
|
304
|
+
}
|
|
305
|
+
// After the totals: either point the user at the aggregated log file (default)
|
|
306
|
+
// or, with --show-logs, print the captured logs (the same cross-mode-deduped
|
|
307
|
+
// body that was written to latest.log). Skipped in clean mode. When logs were
|
|
308
|
+
// already streamed inline (verbose or a non-TTY stream), we only re-point at
|
|
309
|
+
// the file rather than printing them twice.
|
|
310
|
+
renderLogs(event) {
|
|
311
|
+
if (this.cleanMode) return;
|
|
312
|
+
const summary = event.logSummary;
|
|
313
|
+
if (!summary || summary.count <= 0) return;
|
|
314
|
+
const out = this.context.stdout;
|
|
315
|
+
const plural = summary.count === 1 ? "" : "s";
|
|
316
|
+
// --show-logs: print the clean, cross-mode-deduped block (inline streaming
|
|
317
|
+
// was suppressed in onLog). Otherwise just point at the aggregated file —
|
|
318
|
+
// unless logs were already streamed inline (verbose / non-TTY).
|
|
319
|
+
if (event.showLogs && summary.text) {
|
|
320
|
+
out.write(`\n${chalk.bold(`Logs (${summary.count})`)}\n\n`);
|
|
321
|
+
out.write(
|
|
322
|
+
summary.text.endsWith("\n") ? summary.text : `${summary.text}\n`,
|
|
323
|
+
);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const shownInline = this.verboseMode || !this.canRewriteLine();
|
|
327
|
+
const where = summary.file ? ` → ${chalk.cyan(summary.file)}` : "";
|
|
328
|
+
out.write(`\n${summary.count} log${plural} captured${where}\n`);
|
|
329
|
+
if (!shownInline && summary.file) {
|
|
330
|
+
out.write(`${chalk.dim(" run with --show-logs to print them")}\n`);
|
|
331
|
+
}
|
|
298
332
|
}
|
|
299
333
|
onFuzzComplete(event) {
|
|
300
334
|
renderFuzzSummary(this.context, event, this.hasRenderedTestFiles);
|
package/bin/types.js
CHANGED
|
@@ -8,6 +8,7 @@ export class Config {
|
|
|
8
8
|
this.snapshotDir = "./.as-test/snapshots";
|
|
9
9
|
this.config = "none";
|
|
10
10
|
this.coverage = false;
|
|
11
|
+
this.features = [];
|
|
11
12
|
this.env = {};
|
|
12
13
|
this.buildOptions = new BuildOptions();
|
|
13
14
|
this.runOptions = new RunOptions();
|
|
@@ -15,6 +16,12 @@ export class Config {
|
|
|
15
16
|
this.modes = {};
|
|
16
17
|
}
|
|
17
18
|
}
|
|
19
|
+
export const INTERNAL_FEATURE_NAMES = new Set(["try-as"]);
|
|
20
|
+
export function normalizeFeatureName(value) {
|
|
21
|
+
const trimmed = value.trim().toLowerCase();
|
|
22
|
+
if (trimmed == "try_as" || trimmed == "tryas") return "try-as";
|
|
23
|
+
return trimmed;
|
|
24
|
+
}
|
|
18
25
|
export class CoverageOptions {
|
|
19
26
|
constructor() {
|
|
20
27
|
this.enabled = false;
|
package/bin/util.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CoverageIgnoreOptions,
|
|
7
7
|
FuzzConfig,
|
|
8
8
|
ModeConfig,
|
|
9
|
+
normalizeFeatureName,
|
|
9
10
|
ReporterConfig,
|
|
10
11
|
RunOptions,
|
|
11
12
|
Runtime,
|
|
@@ -186,6 +187,7 @@ function parseConfigRaw(raw, configPath) {
|
|
|
186
187
|
typeof config.fuzz.crashDir == "string" && config.fuzz.crashDir.length
|
|
187
188
|
? config.fuzz.crashDir
|
|
188
189
|
: "./.as-test/crashes";
|
|
190
|
+
config.features = parseFeaturesField(raw.features);
|
|
189
191
|
config.modes = parseModes(raw.modes, configDir);
|
|
190
192
|
CONFIG_META.set(config, {
|
|
191
193
|
sourcePath: configPath,
|
|
@@ -193,6 +195,19 @@ function parseConfigRaw(raw, configPath) {
|
|
|
193
195
|
});
|
|
194
196
|
return config;
|
|
195
197
|
}
|
|
198
|
+
function parseFeaturesField(raw) {
|
|
199
|
+
if (!Array.isArray(raw)) return [];
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
const out = [];
|
|
202
|
+
for (const value of raw) {
|
|
203
|
+
if (typeof value != "string") continue;
|
|
204
|
+
const name = normalizeFeatureName(value);
|
|
205
|
+
if (!name.length || seen.has(name)) continue;
|
|
206
|
+
seen.add(name);
|
|
207
|
+
out.push(name);
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
196
211
|
const TOP_LEVEL_KEYS = new Set([
|
|
197
212
|
"$schema",
|
|
198
213
|
"input",
|
|
@@ -203,6 +218,7 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
203
218
|
"snapshotDir",
|
|
204
219
|
"config",
|
|
205
220
|
"coverage",
|
|
221
|
+
"features",
|
|
206
222
|
"env",
|
|
207
223
|
"buildOptions",
|
|
208
224
|
"fuzz",
|
|
@@ -238,6 +254,7 @@ function validateConfig(raw, configPath) {
|
|
|
238
254
|
validateStringField(raw, "snapshotDir", "$", issues);
|
|
239
255
|
validateStringField(raw, "config", "$", issues);
|
|
240
256
|
validateCoverageField(raw, "coverage", "$", issues);
|
|
257
|
+
validateFeaturesField(raw, "features", "$", issues);
|
|
241
258
|
validateEnvField(raw, "env", "$", issues);
|
|
242
259
|
validateBuildOptionsField(raw, "buildOptions", "$", issues);
|
|
243
260
|
validateFuzzField(raw, "fuzz", "$", issues);
|
|
@@ -742,12 +759,34 @@ function validateModesField(raw, key, pathPrefix, issues) {
|
|
|
742
759
|
validateStringField(modeObj, "snapshotDir", modePath, issues);
|
|
743
760
|
validateStringField(modeObj, "config", modePath, issues);
|
|
744
761
|
validateCoverageField(modeObj, "coverage", modePath, issues);
|
|
762
|
+
validateFeaturesField(modeObj, "features", modePath, issues);
|
|
745
763
|
validateFuzzField(modeObj, "fuzz", modePath, issues);
|
|
746
764
|
validateEnvField(modeObj, "env", modePath, issues);
|
|
747
765
|
validateBuildOptionsField(modeObj, "buildOptions", modePath, issues);
|
|
748
766
|
validateRunOptionsField(modeObj, "runOptions", modePath, issues);
|
|
749
767
|
}
|
|
750
768
|
}
|
|
769
|
+
function validateFeaturesField(raw, key, pathPrefix, issues) {
|
|
770
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
771
|
+
const value = raw[key];
|
|
772
|
+
if (!Array.isArray(value)) {
|
|
773
|
+
issues.push({
|
|
774
|
+
path: `${pathPrefix}.${key}`,
|
|
775
|
+
message: "must be an array of feature name strings",
|
|
776
|
+
fix: 'example: "features": ["try-as", "simd"]',
|
|
777
|
+
});
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
for (let i = 0; i < value.length; i++) {
|
|
781
|
+
if (typeof value[i] != "string" || !value[i].trim().length) {
|
|
782
|
+
issues.push({
|
|
783
|
+
path: `${pathPrefix}.${key}[${i}]`,
|
|
784
|
+
message: "must be a non-empty string",
|
|
785
|
+
fix: 'feature names look like "try-as" or "simd"',
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
751
790
|
function isStringArray(value) {
|
|
752
791
|
return Array.isArray(value) && value.every((item) => typeof item == "string");
|
|
753
792
|
}
|
|
@@ -1051,6 +1090,7 @@ function cloneConfig(config) {
|
|
|
1051
1090
|
cloned.runOptions = cloneRunOptions(config.runOptions);
|
|
1052
1091
|
cloned.fuzz = cloneFuzzConfig(config.fuzz);
|
|
1053
1092
|
cloned.coverage = cloneCoverageOptions(config.coverage);
|
|
1093
|
+
cloned.features = [...config.features];
|
|
1054
1094
|
cloned.modes = Object.fromEntries(
|
|
1055
1095
|
Object.entries(config.modes).map(([name, mode]) => [
|
|
1056
1096
|
name,
|
|
@@ -1211,6 +1251,9 @@ function mergeRootConfig(base, override) {
|
|
|
1211
1251
|
raw.coverage,
|
|
1212
1252
|
);
|
|
1213
1253
|
}
|
|
1254
|
+
if (Array.isArray(raw.features)) {
|
|
1255
|
+
merged.features = [...override.features];
|
|
1256
|
+
}
|
|
1214
1257
|
if ("env" in raw) {
|
|
1215
1258
|
merged.env = { ...override.env };
|
|
1216
1259
|
}
|
|
@@ -1511,6 +1554,15 @@ export function resolveArtifactPath(file, inputPatterns) {
|
|
|
1511
1554
|
const rel = resolveSpecRelativePath(file, inputPatterns);
|
|
1512
1555
|
return rel.replace(/\.ts$/i, ".wasm");
|
|
1513
1556
|
}
|
|
1557
|
+
// Absolute path to the `.snap` file owned by a given spec.
|
|
1558
|
+
// assembly/__tests__/array.spec.ts -> <cwd>/<snapshotDir>/array.spec.snap
|
|
1559
|
+
export function resolveSnapshotPath(specFile, snapshotDir, inputPatterns) {
|
|
1560
|
+
const rel = resolveSpecRelativePath(specFile, inputPatterns).replace(
|
|
1561
|
+
/\.ts$/i,
|
|
1562
|
+
".snap",
|
|
1563
|
+
);
|
|
1564
|
+
return resolve(process.cwd(), snapshotDir, rel);
|
|
1565
|
+
}
|
|
1514
1566
|
function toComponents(absPath) {
|
|
1515
1567
|
return absPath
|
|
1516
1568
|
.split(/[\\/]+/)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "as-test",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"author": "Jairus Tanaka",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -12,9 +12,6 @@
|
|
|
12
12
|
"typer-diff": "^1.1.1",
|
|
13
13
|
"wipc-js": "^0.1.1"
|
|
14
14
|
},
|
|
15
|
-
"peerDependencies": {
|
|
16
|
-
"json-as": ">=1.3.5"
|
|
17
|
-
},
|
|
18
15
|
"devDependencies": {
|
|
19
16
|
"@assemblyscript/wasi-shim": "^0.1.0",
|
|
20
17
|
"@eslint/js": "^10.0.1",
|
|
@@ -24,7 +21,7 @@
|
|
|
24
21
|
"assemblyscript": "^0.28.17",
|
|
25
22
|
"assemblyscript-prettier": "^3.0.4",
|
|
26
23
|
"husky": "^9.1.7",
|
|
27
|
-
"
|
|
24
|
+
"playwright": "^1.60.0",
|
|
28
25
|
"prettier": "3.8.3",
|
|
29
26
|
"try-as": "^1.1.0",
|
|
30
27
|
"typescript": "^6.0.3",
|
|
@@ -91,8 +88,8 @@
|
|
|
91
88
|
"typecheck": "tsc -p cli --noEmit && tsc -p tsconfig.lib.json --noEmit && tsc -p transform --noEmit",
|
|
92
89
|
"lint": "eslint transform/src/**/*.ts tools/**/*.js eslint.config.js",
|
|
93
90
|
"build:lib": "tsc -p ./tsconfig.lib.json",
|
|
94
|
-
"build:transform": "tsc -p ./transform",
|
|
95
|
-
"build:cli": "tsc -p cli",
|
|
91
|
+
"build:transform": "rm -rf ./transform/lib && tsc -p ./transform && prettier -w ./transform/",
|
|
92
|
+
"build:cli": "rm -rf ./bin/ && tsc -p cli && prettier -w ./cli/ && chmod +x ./bin/index.js",
|
|
96
93
|
"build:run": "npm run build:cli",
|
|
97
94
|
"docs:dev": "vitepress dev docs",
|
|
98
95
|
"docs:build": "vitepress build docs",
|
|
@@ -102,7 +99,6 @@
|
|
|
102
99
|
"prepublishOnly": "npm run build:cli && npm run build:lib && npm run build:transform && npm run test && npm run format",
|
|
103
100
|
"commitmsg:verify": "bash ./scripts/commit-msg.sh",
|
|
104
101
|
"precommit:verify": "bash ./scripts/pre-commit.sh",
|
|
105
|
-
"prepush:verify": "bash ./scripts/pre-push.sh",
|
|
106
102
|
"prepare": "husky"
|
|
107
103
|
},
|
|
108
104
|
"type": "module"
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { Source, Tokenizer, } from "assemblyscript/dist/assemblyscript.js";
|
|
2
|
+
import { NodeKind } from "./types.js";
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { SimpleParser, isStdlib } from "./util.js";
|
|
6
|
+
const EQUALS_METHOD = "__AS_TEST_EQUALS";
|
|
7
|
+
const TOJSON_METHOD = "toJSON";
|
|
8
|
+
const REFLECT_LOCAL = "__AS_TEST_REFLECT_EQUALS_INTERNAL";
|
|
9
|
+
const STRINGIFY_LOCAL = "__AS_TEST_STRINGIFY_INTERNAL";
|
|
10
|
+
const ALREADY_INJECTED_EQUALS = new WeakSet();
|
|
11
|
+
const ALREADY_INJECTED_TOJSON = new WeakSet();
|
|
12
|
+
const JSON_DECORATORS = ["json", "serializable"];
|
|
13
|
+
export class EqualsTransform {
|
|
14
|
+
parser;
|
|
15
|
+
touchedSources = new Set();
|
|
16
|
+
classesByName = new Map();
|
|
17
|
+
constructor(parser) {
|
|
18
|
+
this.parser = parser;
|
|
19
|
+
}
|
|
20
|
+
apply(sources) {
|
|
21
|
+
for (const source of sources) {
|
|
22
|
+
if (isStdlib(source))
|
|
23
|
+
continue;
|
|
24
|
+
if (!isUserSource(source))
|
|
25
|
+
continue;
|
|
26
|
+
if (isAsTestInternal(source))
|
|
27
|
+
continue;
|
|
28
|
+
this.indexClasses(source.statements);
|
|
29
|
+
}
|
|
30
|
+
for (const source of this.parser.sources) {
|
|
31
|
+
if (isStdlib(source))
|
|
32
|
+
continue;
|
|
33
|
+
if (!isUserSource(source))
|
|
34
|
+
continue;
|
|
35
|
+
if (isAsTestInternal(source))
|
|
36
|
+
continue;
|
|
37
|
+
this.traverseStatements(source, source.statements);
|
|
38
|
+
}
|
|
39
|
+
for (const source of this.touchedSources) {
|
|
40
|
+
this.injectRuntimeImports(source);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
indexClasses(statements) {
|
|
44
|
+
for (const stmt of statements) {
|
|
45
|
+
if (!stmt)
|
|
46
|
+
continue;
|
|
47
|
+
if (stmt.kind === NodeKind.ClassDeclaration) {
|
|
48
|
+
const klass = stmt;
|
|
49
|
+
const list = this.classesByName.get(klass.name.text);
|
|
50
|
+
if (list)
|
|
51
|
+
list.push(klass);
|
|
52
|
+
else
|
|
53
|
+
this.classesByName.set(klass.name.text, [klass]);
|
|
54
|
+
}
|
|
55
|
+
else if (stmt.kind === NodeKind.NamespaceDeclaration) {
|
|
56
|
+
const members = stmt.members;
|
|
57
|
+
if (members)
|
|
58
|
+
this.indexClasses(members);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
traverseStatements(source, statements) {
|
|
63
|
+
for (const stmt of statements) {
|
|
64
|
+
if (!stmt)
|
|
65
|
+
continue;
|
|
66
|
+
if (stmt.kind === NodeKind.ClassDeclaration) {
|
|
67
|
+
const klass = stmt;
|
|
68
|
+
const injectedEquals = this.injectEqualsMethod(klass);
|
|
69
|
+
const injectedToJSON = this.injectToJSONMethod(klass);
|
|
70
|
+
if (injectedEquals || injectedToJSON) {
|
|
71
|
+
this.touchedSources.add(source);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (stmt.kind === NodeKind.NamespaceDeclaration) {
|
|
75
|
+
const members = stmt.members;
|
|
76
|
+
if (members)
|
|
77
|
+
this.traverseStatements(source, members);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
injectEqualsMethod(klass) {
|
|
82
|
+
if (ALREADY_INJECTED_EQUALS.has(klass))
|
|
83
|
+
return false;
|
|
84
|
+
if (declaresMethod(klass, EQUALS_METHOD))
|
|
85
|
+
return false;
|
|
86
|
+
if (klass.typeParameters && klass.typeParameters.length)
|
|
87
|
+
return false;
|
|
88
|
+
const fieldNames = [];
|
|
89
|
+
for (const member of klass.members) {
|
|
90
|
+
if (member.kind !== NodeKind.FieldDeclaration)
|
|
91
|
+
continue;
|
|
92
|
+
const field = member;
|
|
93
|
+
if (!field.is(262144))
|
|
94
|
+
continue;
|
|
95
|
+
if ((field.flags & 32) !== 0)
|
|
96
|
+
continue;
|
|
97
|
+
if (!field.name || !field.name.text)
|
|
98
|
+
continue;
|
|
99
|
+
fieldNames.push(field.name.text);
|
|
100
|
+
}
|
|
101
|
+
const fieldHashes = fieldNames.map((n) => djb2Hash(n).toString());
|
|
102
|
+
const className = klass.name.text;
|
|
103
|
+
const otherType = this.pinnedOtherType(klass);
|
|
104
|
+
const lines = [];
|
|
105
|
+
lines.push(`const __o = changetype<${className}>(other);`);
|
|
106
|
+
for (let i = 0; i < fieldNames.length; i++) {
|
|
107
|
+
const name = fieldNames[i];
|
|
108
|
+
const hash = fieldHashes[i];
|
|
109
|
+
lines.push(`if (!ignore.includes(${hash}) && ` +
|
|
110
|
+
`!${REFLECT_LOCAL}(this.${name}, __o.${name}, stack, strict)) return false;`);
|
|
111
|
+
}
|
|
112
|
+
const ignoreLiteral = fieldHashes.length
|
|
113
|
+
? `[${fieldHashes.join(", ")}] as StaticArray<i64>`
|
|
114
|
+
: `[] as StaticArray<i64>`;
|
|
115
|
+
lines.push(`if (isDefined(super.__AS_TEST_EQUALS)) {` +
|
|
116
|
+
` if (!super.__AS_TEST_EQUALS(other, stack, ` +
|
|
117
|
+
`StaticArray.concat<i64>(ignore, ${ignoreLiteral}), strict)) return false;` +
|
|
118
|
+
` }`);
|
|
119
|
+
lines.push(`return true;`);
|
|
120
|
+
const code = `${EQUALS_METHOD}(` +
|
|
121
|
+
`other: ${otherType}, ` +
|
|
122
|
+
`stack: usize[], ` +
|
|
123
|
+
`ignore: StaticArray<i64>, ` +
|
|
124
|
+
`strict: bool` +
|
|
125
|
+
`): bool { ${lines.join(" ")} }`;
|
|
126
|
+
try {
|
|
127
|
+
const method = SimpleParser.parseClassMember(code, klass);
|
|
128
|
+
klass.members.push(method);
|
|
129
|
+
ALREADY_INJECTED_EQUALS.add(klass);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
pinnedOtherType(klass) {
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
let current = klass;
|
|
139
|
+
let rootName = klass.name.text;
|
|
140
|
+
while (current && !seen.has(current)) {
|
|
141
|
+
seen.add(current);
|
|
142
|
+
const userType = userDeclaredEqualsOtherType(current);
|
|
143
|
+
if (userType)
|
|
144
|
+
return userType;
|
|
145
|
+
rootName = current.name.text;
|
|
146
|
+
const parentName = extendsName(current);
|
|
147
|
+
if (!parentName)
|
|
148
|
+
return rootName;
|
|
149
|
+
const parents = this.classesByName.get(parentName);
|
|
150
|
+
if (!parents || parents.length === 0)
|
|
151
|
+
return rootName;
|
|
152
|
+
current = parents[0];
|
|
153
|
+
}
|
|
154
|
+
return rootName;
|
|
155
|
+
}
|
|
156
|
+
injectRuntimeImports(source) {
|
|
157
|
+
const asTestPath = detectAsTestImportPath(source.text) ?? "as-test";
|
|
158
|
+
const importLine = `import { reflectEquals as ${REFLECT_LOCAL}, ` +
|
|
159
|
+
`__as_test_stringify as ${STRINGIFY_LOCAL} } from "${asTestPath}";`;
|
|
160
|
+
const tokenizer = new Tokenizer(new Source(0, source.normalizedPath, importLine));
|
|
161
|
+
this.parser.currentSource = tokenizer.source;
|
|
162
|
+
source.statements.unshift(this.parser.parseTopLevelStatement(tokenizer));
|
|
163
|
+
this.parser.currentSource = source;
|
|
164
|
+
}
|
|
165
|
+
injectToJSONMethod(klass) {
|
|
166
|
+
if (ALREADY_INJECTED_TOJSON.has(klass))
|
|
167
|
+
return false;
|
|
168
|
+
if (declaresMethod(klass, TOJSON_METHOD))
|
|
169
|
+
return false;
|
|
170
|
+
if (hasAnyDecorator(klass, JSON_DECORATORS))
|
|
171
|
+
return false;
|
|
172
|
+
if (klass.typeParameters && klass.typeParameters.length)
|
|
173
|
+
return false;
|
|
174
|
+
const fieldNames = this.collectChainFieldNames(klass);
|
|
175
|
+
const chainNames = this.collectChainClassNames(klass);
|
|
176
|
+
const renderable = [];
|
|
177
|
+
for (const name of fieldNames) {
|
|
178
|
+
const fieldType = this.fieldTypeName(klass, name);
|
|
179
|
+
if (fieldType && chainNames.has(fieldType))
|
|
180
|
+
continue;
|
|
181
|
+
renderable.push(name);
|
|
182
|
+
}
|
|
183
|
+
const parts = [];
|
|
184
|
+
for (let i = 0; i < renderable.length; i++) {
|
|
185
|
+
const name = renderable[i];
|
|
186
|
+
const prefix = i === 0 ? "" : ",";
|
|
187
|
+
parts.push(`"${prefix}\\"${name}\\":" + ${STRINGIFY_LOCAL}(this.${name})`);
|
|
188
|
+
}
|
|
189
|
+
const body = parts.length
|
|
190
|
+
? `return "{" + ${parts.join(" + ")} + "}";`
|
|
191
|
+
: `return "{}";`;
|
|
192
|
+
const code = `toJSON(): string { ${body} }`;
|
|
193
|
+
try {
|
|
194
|
+
const method = SimpleParser.parseClassMember(code, klass);
|
|
195
|
+
klass.members.push(method);
|
|
196
|
+
ALREADY_INJECTED_TOJSON.add(klass);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
collectChainFieldNames(klass) {
|
|
204
|
+
const chain = [];
|
|
205
|
+
const seen = new Set();
|
|
206
|
+
let current = klass;
|
|
207
|
+
while (current && !seen.has(current)) {
|
|
208
|
+
seen.add(current);
|
|
209
|
+
chain.unshift(current);
|
|
210
|
+
const parentName = extendsName(current);
|
|
211
|
+
if (!parentName)
|
|
212
|
+
break;
|
|
213
|
+
const parents = this.classesByName.get(parentName);
|
|
214
|
+
if (!parents || parents.length === 0)
|
|
215
|
+
break;
|
|
216
|
+
current = parents[0];
|
|
217
|
+
}
|
|
218
|
+
const ordered = [];
|
|
219
|
+
const known = new Set();
|
|
220
|
+
for (const cls of chain) {
|
|
221
|
+
for (const member of cls.members) {
|
|
222
|
+
if (member.kind !== NodeKind.FieldDeclaration)
|
|
223
|
+
continue;
|
|
224
|
+
const field = member;
|
|
225
|
+
if (!field.is(262144))
|
|
226
|
+
continue;
|
|
227
|
+
if ((field.flags & 32) !== 0)
|
|
228
|
+
continue;
|
|
229
|
+
if (!field.name || !field.name.text)
|
|
230
|
+
continue;
|
|
231
|
+
const name = field.name.text;
|
|
232
|
+
if (known.has(name))
|
|
233
|
+
continue;
|
|
234
|
+
known.add(name);
|
|
235
|
+
ordered.push(name);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return ordered;
|
|
239
|
+
}
|
|
240
|
+
collectChainClassNames(klass) {
|
|
241
|
+
const out = new Set();
|
|
242
|
+
const seen = new Set();
|
|
243
|
+
let current = klass;
|
|
244
|
+
while (current && !seen.has(current)) {
|
|
245
|
+
seen.add(current);
|
|
246
|
+
out.add(current.name.text);
|
|
247
|
+
const parentName = extendsName(current);
|
|
248
|
+
if (!parentName)
|
|
249
|
+
break;
|
|
250
|
+
const parents = this.classesByName.get(parentName);
|
|
251
|
+
if (!parents || parents.length === 0)
|
|
252
|
+
break;
|
|
253
|
+
current = parents[0];
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
fieldTypeName(klass, fieldName) {
|
|
258
|
+
const seen = new Set();
|
|
259
|
+
let current = klass;
|
|
260
|
+
while (current && !seen.has(current)) {
|
|
261
|
+
seen.add(current);
|
|
262
|
+
for (const member of current.members) {
|
|
263
|
+
if (member.kind !== NodeKind.FieldDeclaration)
|
|
264
|
+
continue;
|
|
265
|
+
const field = member;
|
|
266
|
+
if (!field.name || field.name.text !== fieldName)
|
|
267
|
+
continue;
|
|
268
|
+
return namedTypeText(field.type);
|
|
269
|
+
}
|
|
270
|
+
const parentName = extendsName(current);
|
|
271
|
+
if (!parentName)
|
|
272
|
+
break;
|
|
273
|
+
const parents = this.classesByName.get(parentName);
|
|
274
|
+
if (!parents || parents.length === 0)
|
|
275
|
+
break;
|
|
276
|
+
current = parents[0];
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function declaresMethod(klass, name) {
|
|
282
|
+
for (const member of klass.members) {
|
|
283
|
+
if (member.kind !== NodeKind.MethodDeclaration)
|
|
284
|
+
continue;
|
|
285
|
+
const method = member;
|
|
286
|
+
if (method.name && method.name.text === name)
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
function hasAnyDecorator(klass, names) {
|
|
292
|
+
if (!klass.decorators)
|
|
293
|
+
return false;
|
|
294
|
+
for (const dec of klass.decorators) {
|
|
295
|
+
const decName = dec.name.text;
|
|
296
|
+
if (decName && names.indexOf(decName) !== -1)
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
function userDeclaredEqualsOtherType(klass) {
|
|
302
|
+
for (const member of klass.members) {
|
|
303
|
+
if (member.kind !== NodeKind.MethodDeclaration)
|
|
304
|
+
continue;
|
|
305
|
+
const method = member;
|
|
306
|
+
if (!method.name || method.name.text !== EQUALS_METHOD)
|
|
307
|
+
continue;
|
|
308
|
+
const params = method.signature.parameters;
|
|
309
|
+
if (!params || params.length === 0)
|
|
310
|
+
return null;
|
|
311
|
+
return namedTypeText(params[0].type);
|
|
312
|
+
}
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
function extendsName(klass) {
|
|
316
|
+
const extendsType = klass
|
|
317
|
+
.extendsType;
|
|
318
|
+
if (!extendsType)
|
|
319
|
+
return null;
|
|
320
|
+
return namedTypeText(extendsType);
|
|
321
|
+
}
|
|
322
|
+
function namedTypeText(type) {
|
|
323
|
+
if (!type)
|
|
324
|
+
return null;
|
|
325
|
+
if (type.kind !== NodeKind.NamedType)
|
|
326
|
+
return null;
|
|
327
|
+
const named = type;
|
|
328
|
+
if (!named.name)
|
|
329
|
+
return null;
|
|
330
|
+
const ident = named.name
|
|
331
|
+
.identifier;
|
|
332
|
+
if (ident && typeof ident.text === "string")
|
|
333
|
+
return ident.text;
|
|
334
|
+
const text = named.name.text;
|
|
335
|
+
return text ?? null;
|
|
336
|
+
}
|
|
337
|
+
function isUserSource(source) {
|
|
338
|
+
return (source.sourceKind === 0 ||
|
|
339
|
+
source.sourceKind === 1);
|
|
340
|
+
}
|
|
341
|
+
let cachedIsAsTestCwd = null;
|
|
342
|
+
function isAsTestOwnCwd() {
|
|
343
|
+
if (cachedIsAsTestCwd !== null)
|
|
344
|
+
return cachedIsAsTestCwd;
|
|
345
|
+
try {
|
|
346
|
+
const raw = readFileSync(join(process.cwd(), "package.json"), "utf8");
|
|
347
|
+
const pkg = JSON.parse(raw);
|
|
348
|
+
cachedIsAsTestCwd = pkg.name === "as-test";
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
cachedIsAsTestCwd = false;
|
|
352
|
+
}
|
|
353
|
+
return cachedIsAsTestCwd;
|
|
354
|
+
}
|
|
355
|
+
function isAsTestInternal(source) {
|
|
356
|
+
const p = source.normalizedPath;
|
|
357
|
+
if (/(?:^|\/)as-test\/assembly\/(?!__tests__\/)/.test(p))
|
|
358
|
+
return true;
|
|
359
|
+
if (isAsTestOwnCwd() && /^assembly\/(?!__tests__\/)/.test(p))
|
|
360
|
+
return true;
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
function detectAsTestImportPath(sourceText) {
|
|
364
|
+
const text = sourceText
|
|
365
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
366
|
+
.replace(/\/\/.*$/gm, "");
|
|
367
|
+
const imports = text.matchAll(/import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']/g);
|
|
368
|
+
for (const match of imports) {
|
|
369
|
+
const specifiers = match[1] ?? "";
|
|
370
|
+
const modulePath = (match[2] ?? "").trim();
|
|
371
|
+
if (!modulePath.length)
|
|
372
|
+
continue;
|
|
373
|
+
if (modulePath === "as-test" || modulePath.endsWith("/as-test")) {
|
|
374
|
+
return modulePath;
|
|
375
|
+
}
|
|
376
|
+
if (/\b(?:describe|test|it|expect|beforeAll|afterAll|beforeEach|afterEach|mockFn|unmockFn|mockImport|unmockImport|snapshotFn|log|run)\b/.test(specifiers)) {
|
|
377
|
+
return modulePath;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
function djb2Hash(s) {
|
|
383
|
+
let h = 5381;
|
|
384
|
+
for (let i = 0; i < s.length; i++) {
|
|
385
|
+
h = ((h << 5) + h + s.charCodeAt(i)) | 0;
|
|
386
|
+
}
|
|
387
|
+
return h >>> 0;
|
|
388
|
+
}
|
package/transform/lib/index.js
CHANGED
|
@@ -4,9 +4,12 @@ import { CoverageTransform } from "./coverage.js";
|
|
|
4
4
|
import { MockTransform } from "./mock.js";
|
|
5
5
|
import { LocationTransform } from "./location.js";
|
|
6
6
|
import { LogTransform } from "./log.js";
|
|
7
|
+
import { EqualsTransform } from "./equals.js";
|
|
7
8
|
import { isStdlib } from "./util.js";
|
|
9
|
+
import { NodeKind } from "./types.js";
|
|
8
10
|
export default class Transformer extends Transform {
|
|
9
11
|
afterParse(parser) {
|
|
12
|
+
patchModeName(parser, process.env.AS_TEST_MODE_NAME ?? "default");
|
|
10
13
|
const mock = new MockTransform();
|
|
11
14
|
const location = new LocationTransform();
|
|
12
15
|
const log = new LogTransform(parser);
|
|
@@ -79,6 +82,31 @@ export default class Transformer extends Transform {
|
|
|
79
82
|
if (coverage) {
|
|
80
83
|
coverage.globalStatements = [];
|
|
81
84
|
}
|
|
85
|
+
new EqualsTransform(parser).apply(parser.sources);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function patchModeName(parser, modeName) {
|
|
89
|
+
for (const source of parser.sources) {
|
|
90
|
+
if (!source.normalizedPath.endsWith("assembly/src/mode.ts"))
|
|
91
|
+
continue;
|
|
92
|
+
for (const stmt of source.statements) {
|
|
93
|
+
if (stmt.kind !== NodeKind.Variable)
|
|
94
|
+
continue;
|
|
95
|
+
const decls = stmt.declarations;
|
|
96
|
+
for (const decl of decls) {
|
|
97
|
+
if (decl.name.text !== "AS_TEST_MODE_NAME")
|
|
98
|
+
continue;
|
|
99
|
+
if (!decl.initializer)
|
|
100
|
+
continue;
|
|
101
|
+
if (decl.initializer.kind !== NodeKind.Literal)
|
|
102
|
+
continue;
|
|
103
|
+
const literal = decl.initializer;
|
|
104
|
+
if (literal.literalKind !== 2)
|
|
105
|
+
continue;
|
|
106
|
+
literal.value = modeName;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
82
110
|
}
|
|
83
111
|
}
|
|
84
112
|
function collectMockImportTargets(sources) {
|