as-test 1.5.2 → 1.6.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 +23 -0
- package/as-test.config.schema.json +40 -0
- package/bin/build-cache.js +278 -0
- package/bin/commands/build-core.js +80 -15
- package/bin/commands/clean-core.js +4 -0
- package/bin/commands/run-core.js +182 -2
- package/bin/commands/test.js +2 -0
- package/bin/index.js +253 -68
- package/bin/reporters/default.js +56 -5
- package/bin/types.js +18 -0
- package/bin/util.js +94 -0
- package/package.json +2 -2
- package/transform/lib/index.js +2 -2
package/bin/reporters/default.js
CHANGED
|
@@ -116,14 +116,28 @@ class DefaultReporter {
|
|
|
116
116
|
renderFileResult(event) {
|
|
117
117
|
const verdict = event.verdict ?? "none";
|
|
118
118
|
const time = event.time ? ` ${chalk.dim(event.time)}` : "";
|
|
119
|
-
const
|
|
119
|
+
const name = formatSpecDisplayPath(event.file);
|
|
120
|
+
// A replayed (cached) result is de-emphasized: badge, filename, and tag are
|
|
121
|
+
// all dimmed so freshly-run specs stand out from unchanged ones.
|
|
122
|
+
if (event.cached) {
|
|
123
|
+
// Replayed-from-cache: keep the coloured verdict badge (white text) so it
|
|
124
|
+
// stays scannable, but dim the filename and show "(cache)" in place of the
|
|
125
|
+
// timing so freshly-run specs still stand out.
|
|
126
|
+
const badge =
|
|
127
|
+
verdict == "fail"
|
|
128
|
+
? chalk.bgRed.white(" FAIL ")
|
|
129
|
+
: verdict == "ok"
|
|
130
|
+
? chalk.bgGreenBright.white(" PASS ")
|
|
131
|
+
: chalk.bgBlackBright.white(" SKIP ");
|
|
132
|
+
return `${badge} ${chalk.dim(name)} ${chalk.dim("(cache)")}`;
|
|
133
|
+
}
|
|
120
134
|
if (verdict == "fail")
|
|
121
|
-
return `${chalk.bgRed.white(" FAIL ")} ${
|
|
135
|
+
return `${chalk.bgRed.white(" FAIL ")} ${name}${time}`;
|
|
122
136
|
if (this.fileHasWarning)
|
|
123
|
-
return `${chalk.bgYellow.black(" WARN ")} ${
|
|
137
|
+
return `${chalk.bgYellow.black(" WARN ")} ${name}${time}`;
|
|
124
138
|
if (verdict == "ok")
|
|
125
|
-
return `${chalk.bgGreenBright.black(" PASS ")} ${
|
|
126
|
-
return `${chalk.bgBlackBright.white(" SKIP ")} ${
|
|
139
|
+
return `${chalk.bgGreenBright.black(" PASS ")} ${name}${time}`;
|
|
140
|
+
return `${chalk.bgBlackBright.white(" SKIP ")} ${name}${time}`;
|
|
127
141
|
}
|
|
128
142
|
onRunStart(event) {
|
|
129
143
|
this.verboseMode = Boolean(event.verbose);
|
|
@@ -805,8 +819,18 @@ function renderTotals(stats, event) {
|
|
|
805
819
|
skipped: stats.skippedTests,
|
|
806
820
|
total: stats.failedTests + stats.passedTests + stats.skippedTests,
|
|
807
821
|
};
|
|
822
|
+
const cacheSummary = computeCacheSummary(event.reports);
|
|
808
823
|
const layout = createSummaryLayout([
|
|
809
824
|
event.fuzzSummary,
|
|
825
|
+
// "cached" and "failed" are the same length, so the cache summary aligns in
|
|
826
|
+
// the shared first column.
|
|
827
|
+
cacheSummary
|
|
828
|
+
? {
|
|
829
|
+
failed: cacheSummary.cached,
|
|
830
|
+
skipped: cacheSummary.skipped,
|
|
831
|
+
total: cacheSummary.total,
|
|
832
|
+
}
|
|
833
|
+
: undefined,
|
|
810
834
|
filesSummary,
|
|
811
835
|
suitesSummary,
|
|
812
836
|
testsSummary,
|
|
@@ -821,6 +845,9 @@ function renderTotals(stats, event) {
|
|
|
821
845
|
if (event.modeSummary) {
|
|
822
846
|
renderModeSummary(event.modeSummary, layout);
|
|
823
847
|
}
|
|
848
|
+
if (cacheSummary) {
|
|
849
|
+
renderCacheSummary(cacheSummary, layout);
|
|
850
|
+
}
|
|
824
851
|
process.stdout.write(
|
|
825
852
|
chalk.bold("Time:".padEnd(9)) +
|
|
826
853
|
formatTime(stats.time) +
|
|
@@ -828,6 +855,30 @@ function renderTotals(stats, event) {
|
|
|
828
855
|
"\n",
|
|
829
856
|
);
|
|
830
857
|
}
|
|
858
|
+
// When the cache is active, every report carries a `cached` flag (true =
|
|
859
|
+
// replayed from cache, false = freshly run). Returns the hit/miss split, or
|
|
860
|
+
// undefined when the cache is off (no report sets the flag) so no line shows.
|
|
861
|
+
function computeCacheSummary(reports) {
|
|
862
|
+
const flagged = reports.filter((r) => typeof r?.cached === "boolean");
|
|
863
|
+
if (!flagged.length) return undefined;
|
|
864
|
+
const cached = flagged.filter((r) => r.cached).length;
|
|
865
|
+
return { cached, skipped: flagged.length - cached, total: flagged.length };
|
|
866
|
+
}
|
|
867
|
+
// Renders the "Cache:" line in the shared three-column layout (cached / skipped
|
|
868
|
+
// / total) so it lines up with Files/Suites/Tests/Modes.
|
|
869
|
+
function renderCacheSummary(summary, layout) {
|
|
870
|
+
const cachedText = `${summary.cached} cached`;
|
|
871
|
+
const skippedText = `${summary.skipped} skipped`;
|
|
872
|
+
const totalText = `${summary.total} total`;
|
|
873
|
+
process.stdout.write(chalk.bold("Cache:".padEnd(9)));
|
|
874
|
+
process.stdout.write(
|
|
875
|
+
chalk.bold.greenBright(cachedText.padStart(layout.failedWidth)),
|
|
876
|
+
);
|
|
877
|
+
process.stdout.write(", ");
|
|
878
|
+
process.stdout.write(chalk.gray(skippedText.padStart(layout.skippedWidth)));
|
|
879
|
+
process.stdout.write(", ");
|
|
880
|
+
process.stdout.write(totalText.padStart(layout.totalWidth) + "\n");
|
|
881
|
+
}
|
|
831
882
|
function renderModeSummary(summary, layout) {
|
|
832
883
|
renderSummaryLine("Modes:", summary, layout);
|
|
833
884
|
}
|
package/bin/types.js
CHANGED
|
@@ -7,6 +7,13 @@ export class Config {
|
|
|
7
7
|
this.coverageDir = "./.as-test/coverage";
|
|
8
8
|
this.snapshotDir = "./.as-test/snapshots";
|
|
9
9
|
this.config = "none";
|
|
10
|
+
// Incremental test cache (opt-in). false = off; "build" = skip recompiling
|
|
11
|
+
// unchanged specs; "full"/true = also replay passing run results. The object
|
|
12
|
+
// form adds `maxTime` (e.g. "1h", "30m", "7d") to expire entries older than
|
|
13
|
+
// that, forcing a rebuild+rerun. ("reachable" is accepted as a deprecated
|
|
14
|
+
// alias for "full" — reachability-based dep pruning was unsound for
|
|
15
|
+
// AssemblyScript's compile-time inlining.) Enable per run with --cache / --no-cache.
|
|
16
|
+
this.cache = false;
|
|
10
17
|
this.coverage = false;
|
|
11
18
|
this.features = [];
|
|
12
19
|
this.env = {};
|
|
@@ -16,6 +23,17 @@ export class Config {
|
|
|
16
23
|
this.modes = {};
|
|
17
24
|
}
|
|
18
25
|
}
|
|
26
|
+
export class CacheOptions {
|
|
27
|
+
constructor() {
|
|
28
|
+
// Cache tier: "build" (skip rebuilds) or "full" (also replay runs).
|
|
29
|
+
// "reachable" is accepted as a deprecated alias for "full".
|
|
30
|
+
this.type = "full";
|
|
31
|
+
// Optional expiry: a duration string (e.g. "1h", "30m", "90s", "7d"). Entries
|
|
32
|
+
// built longer ago than this are treated as stale and rebuilt+rerun. Empty =
|
|
33
|
+
// no expiry.
|
|
34
|
+
this.maxTime = "";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
19
37
|
export const INTERNAL_FEATURE_NAMES = new Set(["try-as"]);
|
|
20
38
|
export function normalizeFeatureName(value) {
|
|
21
39
|
const trimmed = value.trim().toLowerCase();
|
package/bin/util.js
CHANGED
|
@@ -217,6 +217,7 @@ const TOP_LEVEL_KEYS = new Set([
|
|
|
217
217
|
"coverageDir",
|
|
218
218
|
"snapshotDir",
|
|
219
219
|
"config",
|
|
220
|
+
"cache",
|
|
220
221
|
"coverage",
|
|
221
222
|
"features",
|
|
222
223
|
"env",
|
|
@@ -253,6 +254,7 @@ function validateConfig(raw, configPath) {
|
|
|
253
254
|
validateStringField(raw, "coverageDir", "$", issues);
|
|
254
255
|
validateStringField(raw, "snapshotDir", "$", issues);
|
|
255
256
|
validateStringField(raw, "config", "$", issues);
|
|
257
|
+
validateCacheField(raw, "cache", "$", issues);
|
|
256
258
|
validateCoverageField(raw, "coverage", "$", issues);
|
|
257
259
|
validateFeaturesField(raw, "features", "$", issues);
|
|
258
260
|
validateEnvField(raw, "env", "$", issues);
|
|
@@ -380,6 +382,46 @@ function validateCoverageField(raw, key, pathPrefix, issues) {
|
|
|
380
382
|
if (!(key in raw) || raw[key] == undefined) return;
|
|
381
383
|
validateCoverageValue(raw[key], `${pathPrefix}.${key}`, issues);
|
|
382
384
|
}
|
|
385
|
+
function validateCacheField(raw, key, pathPrefix, issues) {
|
|
386
|
+
if (!(key in raw) || raw[key] == undefined) return;
|
|
387
|
+
const value = raw[key];
|
|
388
|
+
const path = `${pathPrefix}.${key}`;
|
|
389
|
+
if (typeof value == "boolean") return;
|
|
390
|
+
if (value === "build" || value === "full" || value === "reachable") return;
|
|
391
|
+
if (value && typeof value == "object" && !Array.isArray(value)) {
|
|
392
|
+
const obj = value;
|
|
393
|
+
if (
|
|
394
|
+
"type" in obj &&
|
|
395
|
+
obj.type !== "build" &&
|
|
396
|
+
obj.type !== "full" &&
|
|
397
|
+
obj.type !== "reachable"
|
|
398
|
+
) {
|
|
399
|
+
issues.push({
|
|
400
|
+
path: `${path}.type`,
|
|
401
|
+
message: 'must be "build" or "full"',
|
|
402
|
+
fix: 'set "type" to "build" or "full"',
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if ("maxTime" in obj && obj.maxTime != undefined) {
|
|
406
|
+
if (
|
|
407
|
+
typeof obj.maxTime != "string" ||
|
|
408
|
+
parseDurationMs(obj.maxTime) == null
|
|
409
|
+
) {
|
|
410
|
+
issues.push({
|
|
411
|
+
path: `${path}.maxTime`,
|
|
412
|
+
message: 'must be a duration like "1h", "30m", "90s", or "7d"',
|
|
413
|
+
fix: 'use a number followed by ms/s/m/h/d, e.g. "1h"',
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
issues.push({
|
|
420
|
+
path,
|
|
421
|
+
message: 'must be a boolean, "build"/"full", or { type, maxTime }',
|
|
422
|
+
fix: 'use false, true, "build", "full", or { "type": "full", "maxTime": "1h" }',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
383
425
|
function validateCoverageValue(value, path, issues) {
|
|
384
426
|
if (typeof value == "boolean") return;
|
|
385
427
|
if (!value || typeof value != "object" || Array.isArray(value)) {
|
|
@@ -758,6 +800,7 @@ function validateModesField(raw, key, pathPrefix, issues) {
|
|
|
758
800
|
validateStringField(modeObj, "coverageDir", modePath, issues);
|
|
759
801
|
validateStringField(modeObj, "snapshotDir", modePath, issues);
|
|
760
802
|
validateStringField(modeObj, "config", modePath, issues);
|
|
803
|
+
validateCacheField(modeObj, "cache", modePath, issues);
|
|
761
804
|
validateCoverageField(modeObj, "coverage", modePath, issues);
|
|
762
805
|
validateFeaturesField(modeObj, "features", modePath, issues);
|
|
763
806
|
validateFuzzField(modeObj, "fuzz", modePath, issues);
|
|
@@ -1244,6 +1287,7 @@ function mergeRootConfig(base, override) {
|
|
|
1244
1287
|
merged.snapshotDir = override.snapshotDir;
|
|
1245
1288
|
}
|
|
1246
1289
|
if ("config" in raw) merged.config = override.config;
|
|
1290
|
+
if ("cache" in raw) merged.cache = override.cache;
|
|
1247
1291
|
if ("coverage" in raw) {
|
|
1248
1292
|
merged.coverage = mergeCoverageConfig(
|
|
1249
1293
|
merged.coverage,
|
|
@@ -1392,6 +1436,56 @@ export function applyMode(config, modeName) {
|
|
|
1392
1436
|
function appendPathSegment(basePath, segment) {
|
|
1393
1437
|
return join(basePath, segment);
|
|
1394
1438
|
}
|
|
1439
|
+
// Parses a duration string ("500ms", "90s", "30m", "1h", "7d", "1.5h") to
|
|
1440
|
+
// milliseconds. Returns null when the format is unrecognized.
|
|
1441
|
+
export function parseDurationMs(value) {
|
|
1442
|
+
const match = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)\s*$/.exec(value);
|
|
1443
|
+
if (!match) return null;
|
|
1444
|
+
const amount = Number(match[1]);
|
|
1445
|
+
const unit = match[2];
|
|
1446
|
+
const mult =
|
|
1447
|
+
unit === "ms"
|
|
1448
|
+
? 1
|
|
1449
|
+
: unit === "s"
|
|
1450
|
+
? 1000
|
|
1451
|
+
: unit === "m"
|
|
1452
|
+
? 60000
|
|
1453
|
+
: unit === "h"
|
|
1454
|
+
? 3600000
|
|
1455
|
+
: 86400000;
|
|
1456
|
+
return amount * mult;
|
|
1457
|
+
}
|
|
1458
|
+
// Resolves the effective cache mode + expiry for a run. CLI flags win over
|
|
1459
|
+
// config: --no-cache forces off, --cache forces full. The config value may be a
|
|
1460
|
+
// boolean, a mode string, or an object { type, maxTime }. Default is off
|
|
1461
|
+
// (opt-in). maxTime (entry expiry) comes from the object form regardless of the
|
|
1462
|
+
// resolved mode.
|
|
1463
|
+
export function resolveCacheSettings(configCache, flags) {
|
|
1464
|
+
if (flags.noCache) return { mode: "off", maxTimeMs: null };
|
|
1465
|
+
// "reachable" is accepted for back-compat but treated as "full": reachability
|
|
1466
|
+
// pruning was unsound for AssemblyScript (compile-time-inlined consts, static
|
|
1467
|
+
// fields, @inline bodies, and re-export barrels can change output without
|
|
1468
|
+
// being "reachable"), so it could serve stale results. The full dependency
|
|
1469
|
+
// set is always correct.
|
|
1470
|
+
let mode = "off";
|
|
1471
|
+
let maxTimeMs = null;
|
|
1472
|
+
if (configCache && typeof configCache === "object") {
|
|
1473
|
+
mode = configCache.type === "build" ? "build" : "full";
|
|
1474
|
+
maxTimeMs = configCache.maxTime
|
|
1475
|
+
? parseDurationMs(configCache.maxTime)
|
|
1476
|
+
: null;
|
|
1477
|
+
} else if (
|
|
1478
|
+
configCache === true ||
|
|
1479
|
+
configCache === "full" ||
|
|
1480
|
+
configCache === "reachable"
|
|
1481
|
+
) {
|
|
1482
|
+
mode = "full";
|
|
1483
|
+
} else if (configCache === "build") {
|
|
1484
|
+
mode = "build";
|
|
1485
|
+
}
|
|
1486
|
+
if (flags.cache) mode = "full"; // --cache forces full; maxTime (if any) kept
|
|
1487
|
+
return { mode, maxTimeMs };
|
|
1488
|
+
}
|
|
1395
1489
|
export function getCliVersion() {
|
|
1396
1490
|
const candidates = [
|
|
1397
1491
|
join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "as-test",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"author": "Jairus Tanaka",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"husky": "^9.1.7",
|
|
25
25
|
"playwright": "^1.60.0",
|
|
26
26
|
"prettier": "3.8.3",
|
|
27
|
-
"try-as": "
|
|
27
|
+
"try-as": "1.1.0",
|
|
28
28
|
"typescript": "^6.0.3",
|
|
29
29
|
"typescript-eslint": "^8.59.4",
|
|
30
30
|
"vitepress": "^1.6.4"
|
package/transform/lib/index.js
CHANGED
|
@@ -140,7 +140,7 @@ function analyzeSourceText(sourceText) {
|
|
|
140
140
|
? new RegExp(`\\b${escapeRegex(runAlias)}\\s*\\(`).test(text)
|
|
141
141
|
: false;
|
|
142
142
|
return {
|
|
143
|
-
hasSuiteCalls: /\b(?:describe|test|it|only|
|
|
143
|
+
hasSuiteCalls: /\b(?:x?describe|x?test|x?it|x?only|todo|x?fuzz)\s*\(/.test(text),
|
|
144
144
|
hasRunCall,
|
|
145
145
|
runImportPath,
|
|
146
146
|
hasMockCalls: /\b(?:mockFn|unmockFn|mockImport|unmockImport)\s*\(/.test(text),
|
|
@@ -174,7 +174,7 @@ function detectRunAlias(text) {
|
|
|
174
174
|
return null;
|
|
175
175
|
}
|
|
176
176
|
function looksLikeAsTestImport(specifiers) {
|
|
177
|
-
return /\b(?:describe|test|it|only|
|
|
177
|
+
return /\b(?:x?describe|x?test|x?it|x?only|todo|x?fuzz|expect|beforeAll|afterAll|beforeEach|afterEach|mockFn|unmockFn|mockImport|unmockImport|snapshotFn|log|run)\b/.test(specifiers);
|
|
178
178
|
}
|
|
179
179
|
function stripComments(sourceText) {
|
|
180
180
|
return sourceText.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
|