as-test 1.0.6 → 1.0.9

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,173 @@
1
+ import { readFileSync } from "fs";
2
+ import * as path from "path";
3
+ const sourceLineCache = new Map();
4
+ export function describeCoveragePoint(file, line, column, fallbackType) {
5
+ const context = getCoverageSourceContext(file, line, column);
6
+ if (!context) {
7
+ return {
8
+ displayType: fallbackType,
9
+ subjectName: null,
10
+ visible: "",
11
+ focus: 0,
12
+ highlightStart: 0,
13
+ highlightEnd: 0,
14
+ };
15
+ }
16
+ const declaration = detectCoverageDeclaration(context.visible);
17
+ if (declaration) {
18
+ const [highlightStart, highlightEnd] = resolveCoverageHighlightSpan(context.visible, context.focus);
19
+ return {
20
+ displayType: declaration.type,
21
+ subjectName: declaration.name,
22
+ visible: context.visible,
23
+ focus: context.focus,
24
+ highlightStart,
25
+ highlightEnd,
26
+ };
27
+ }
28
+ const call = detectCoverageCall(context.visible, context.focus);
29
+ if (call) {
30
+ return {
31
+ displayType: "Call",
32
+ subjectName: call.name,
33
+ visible: context.visible,
34
+ focus: context.focus,
35
+ highlightStart: call.start,
36
+ highlightEnd: call.end,
37
+ };
38
+ }
39
+ const [highlightStart, highlightEnd] = resolveCoverageHighlightSpan(context.visible, context.focus);
40
+ return {
41
+ displayType: fallbackType,
42
+ subjectName: null,
43
+ visible: context.visible,
44
+ focus: context.focus,
45
+ highlightStart,
46
+ highlightEnd,
47
+ };
48
+ }
49
+ export function readCoverageSourceLine(file, line) {
50
+ const resolved = path.resolve(process.cwd(), file);
51
+ let lines = sourceLineCache.get(resolved);
52
+ if (lines === undefined) {
53
+ try {
54
+ lines = readFileSync(resolved, "utf8").split(/\r?\n/);
55
+ }
56
+ catch {
57
+ lines = null;
58
+ }
59
+ sourceLineCache.set(resolved, lines);
60
+ }
61
+ if (!lines)
62
+ return "";
63
+ return lines[line - 1] ?? "";
64
+ }
65
+ export function resolveCoverageHighlightSpan(visible, focus) {
66
+ if (!visible.length)
67
+ return [0, 0];
68
+ const index = Math.max(0, Math.min(visible.length - 1, focus));
69
+ if (isCoverageBoundary(visible.charAt(index))) {
70
+ return [index, Math.min(visible.length, index + 1)];
71
+ }
72
+ let start = index;
73
+ let end = index + 1;
74
+ while (start > 0 && !isCoverageBoundary(visible.charAt(start - 1)))
75
+ start--;
76
+ while (end < visible.length && !isCoverageBoundary(visible.charAt(end)))
77
+ end++;
78
+ return [start, end];
79
+ }
80
+ function getCoverageSourceContext(file, line, column) {
81
+ const sourceLine = readCoverageSourceLine(file, line);
82
+ if (!sourceLine)
83
+ return null;
84
+ const expanded = sourceLine.replace(/\t/g, " ");
85
+ const firstNonWhitespace = expanded.search(/\S/);
86
+ if (firstNonWhitespace == -1)
87
+ return null;
88
+ const visible = expanded.slice(firstNonWhitespace).trimEnd();
89
+ if (!visible.length)
90
+ return null;
91
+ const focus = Math.max(0, Math.min(visible.length - 1, Math.max(0, column - 1 - firstNonWhitespace)));
92
+ return { visible, focus };
93
+ }
94
+ function detectCoverageDeclaration(visible) {
95
+ const trimmed = visible.trim();
96
+ if (!trimmed.length)
97
+ return null;
98
+ let match = trimmed.match(/^(?:export\s+)?function\s+([A-Za-z_]\w*)(?:<[^>]+>)?\s*\(/);
99
+ if (match)
100
+ return { type: "Function", name: match[1] ?? null };
101
+ if (trimmed.startsWith("constructor(") ||
102
+ /^(?:public\s+|private\s+|protected\s+)constructor\s*\(/.test(trimmed)) {
103
+ return { type: "Constructor", name: "constructor" };
104
+ }
105
+ match = trimmed.match(/^(?:export\s+)?(?:public\s+|private\s+|protected\s+)?(?:static\s+)?([A-Za-z_]\w*)(?:<[^>]+>)?\([^)]*\)\s*:\s*[^{=]+[{]?$/);
106
+ if (match)
107
+ return { type: "Method", name: match[1] ?? null };
108
+ match = trimmed.match(/^(?:public\s+|private\s+|protected\s+)?(?:readonly\s+)?([A-Za-z_]\w*)(?:<[^>]+>)?\s*:\s*[^=;{]+(?:=.*)?;?$/);
109
+ if (match)
110
+ return { type: "Property", name: match[1] ?? null };
111
+ if (/^(?:export\s+)?class\b/.test(trimmed)) {
112
+ match = trimmed.match(/^(?:export\s+)?class\s+([A-Za-z_]\w*)/);
113
+ return { type: "Class", name: match?.[1] ?? null };
114
+ }
115
+ if (/^(?:export\s+)?enum\b/.test(trimmed)) {
116
+ match = trimmed.match(/^(?:export\s+)?enum\s+([A-Za-z_]\w*)/);
117
+ return { type: "Enum", name: match?.[1] ?? null };
118
+ }
119
+ if (/^(?:export\s+)?interface\b/.test(trimmed)) {
120
+ match = trimmed.match(/^(?:export\s+)?interface\s+([A-Za-z_]\w*)/);
121
+ return { type: "Interface", name: match?.[1] ?? null };
122
+ }
123
+ if (/^(?:export\s+)?namespace\b/.test(trimmed)) {
124
+ match = trimmed.match(/^(?:export\s+)?namespace\s+([A-Za-z_]\w*)/);
125
+ return { type: "Namespace", name: match?.[1] ?? null };
126
+ }
127
+ if (/^(?:const|let|var)\b/.test(trimmed)) {
128
+ match = trimmed.match(/^(?:const|let|var)\s+([A-Za-z_]\w*)/);
129
+ return { type: "Variable", name: match?.[1] ?? null };
130
+ }
131
+ return null;
132
+ }
133
+ function detectCoverageCall(visible, focus) {
134
+ const matches = [...visible.matchAll(/\b([A-Za-z_]\w*)(?:<[^>()]+>)?\s*\(/g)];
135
+ if (!matches.length)
136
+ return null;
137
+ let bestDistance = Number.POSITIVE_INFINITY;
138
+ let bestMatch = null;
139
+ for (const match of matches) {
140
+ const start = match.index ?? -1;
141
+ if (start == -1)
142
+ continue;
143
+ const end = start + match[0].length;
144
+ const distance = focus < start ? start - focus : focus >= end ? focus - end + 1 : 0;
145
+ if (distance < bestDistance) {
146
+ bestDistance = distance;
147
+ bestMatch = match;
148
+ }
149
+ }
150
+ if (!bestMatch)
151
+ return null;
152
+ const name = bestMatch[1] ?? null;
153
+ if (name == "if" ||
154
+ name == "for" ||
155
+ name == "while" ||
156
+ name == "switch" ||
157
+ name == "return" ||
158
+ name == "function") {
159
+ return null;
160
+ }
161
+ if (bestDistance > Math.max(12, Math.floor(visible.length / 3))) {
162
+ return null;
163
+ }
164
+ const start = bestMatch.index ?? 0;
165
+ return {
166
+ name,
167
+ start,
168
+ end: start + (name?.length ?? 1),
169
+ };
170
+ }
171
+ function isCoverageBoundary(ch) {
172
+ return /[\s()[\]{}.,;:+\-*/%&|^!?=<>]/.test(ch);
173
+ }
package/bin/index.js CHANGED
@@ -281,7 +281,7 @@ function printCommandHelp(command) {
281
281
  process.stdout.write(" --enable <feature> Enable feature (coverage|try-as)\n");
282
282
  process.stdout.write(" --disable <feature> Disable feature (coverage|try-as)\n");
283
283
  process.stdout.write(" --fuzz Run fuzz targets after the normal test pass\n");
284
- process.stdout.write(" --fuzz-runs <n> Override fuzz iteration count for this run\n");
284
+ process.stdout.write(" --fuzz-runs <value> Override fuzz iteration count, e.g. 500, 1.5x, +10%, +100000\n");
285
285
  process.stdout.write(" --fuzz-seed <n> Override fuzz seed for this run\n");
286
286
  process.stdout.write(" --parallel Run files through an ordered worker pool using an automatic worker count\n");
287
287
  process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
@@ -302,7 +302,7 @@ function printCommandHelp(command) {
302
302
  process.stdout.write(chalk.bold("Flags:\n"));
303
303
  process.stdout.write(" --config <path> Use a specific config file\n");
304
304
  process.stdout.write(" --mode <name[,name...]> Run one or multiple named config modes\n");
305
- process.stdout.write(" --runs <n> Override fuzz iteration count\n");
305
+ process.stdout.write(" --runs <value> Override fuzz iteration count, e.g. 500, 1.5x, +10%, +100000\n");
306
306
  process.stdout.write(" --seed <n> Override fuzz seed\n");
307
307
  process.stdout.write(" --jobs <n> Run files through an ordered worker pool\n");
308
308
  process.stdout.write(" --build-jobs <n> Limit concurrent build tasks (defaults to --jobs)\n");
@@ -482,9 +482,10 @@ function resolveFuzzOverrides(rawArgs, command) {
482
482
  runs: "--fuzz-runs",
483
483
  seed: "--fuzz-seed",
484
484
  };
485
- const runs = parseNumberFlag(rawArgs, i, direct.runs);
485
+ const runs = parseFuzzRunsFlag(rawArgs, i, direct.runs);
486
486
  if (runs) {
487
- out.runs = runs.number;
487
+ out.runs = runs.absoluteRuns;
488
+ out.runsOverride = runs.override;
488
489
  if (runs.consumeNext)
489
490
  i++;
490
491
  continue;
@@ -499,6 +500,63 @@ function resolveFuzzOverrides(rawArgs, command) {
499
500
  }
500
501
  return out;
501
502
  }
503
+ function parseFuzzRunsFlag(rawArgs, index, flag) {
504
+ const arg = rawArgs[index];
505
+ let value = "";
506
+ let consumeNext = false;
507
+ if (arg == flag) {
508
+ const next = rawArgs[index + 1];
509
+ if (!next || !next.length) {
510
+ throw new Error(`${flag} requires a value such as 500, 1.5x, +10%, or +100000`);
511
+ }
512
+ value = next;
513
+ consumeNext = true;
514
+ }
515
+ else if (arg.startsWith(`${flag}=`)) {
516
+ value = arg.slice(flag.length + 1);
517
+ if (!value.length) {
518
+ throw new Error(`${flag} requires a value such as 500, 1.5x, +10%, or +100000`);
519
+ }
520
+ }
521
+ else {
522
+ return null;
523
+ }
524
+ const parsed = parseFuzzRunsValue(flag, value.trim());
525
+ return {
526
+ key: flag,
527
+ absoluteRuns: parsed.kind == "set" ? parsed.value : undefined,
528
+ override: parsed,
529
+ consumeNext,
530
+ };
531
+ }
532
+ function parseFuzzRunsValue(flag, value) {
533
+ if (/^\d+$/.test(value)) {
534
+ const parsed = parseIntegerFlag(flag, value);
535
+ return { kind: "set", value: parsed };
536
+ }
537
+ if (/^[+-]\d+$/.test(value)) {
538
+ const delta = Number(value);
539
+ if (!Number.isFinite(delta) || !Number.isInteger(delta)) {
540
+ throw new Error(`${flag} additive run override must be an integer`);
541
+ }
542
+ return { kind: "add", value: delta };
543
+ }
544
+ if (/^\d+(?:\.\d+)?x$/i.test(value)) {
545
+ const factor = Number(value.slice(0, -1));
546
+ if (!Number.isFinite(factor) || factor <= 0) {
547
+ throw new Error(`${flag} multiplier must be greater than 0`);
548
+ }
549
+ return { kind: "scale", value: factor };
550
+ }
551
+ if (/^[+-]\d+(?:\.\d+)?%$/.test(value)) {
552
+ const percent = Number(value.slice(0, -1));
553
+ if (!Number.isFinite(percent)) {
554
+ throw new Error(`${flag} percentage must be numeric`);
555
+ }
556
+ return { kind: "percent-add", value: percent };
557
+ }
558
+ throw new Error(`${flag} must be a run count or expression such as 500, 1.5x, +10%, or +100000`);
559
+ }
502
560
  function resolveListFlags(rawArgs, command) {
503
561
  const out = {
504
562
  list: false,
@@ -3,6 +3,7 @@ import { diff } from "typer-diff";
3
3
  import { readFileSync } from "fs";
4
4
  import * as path from "path";
5
5
  import { formatTime } from "../util.js";
6
+ import { describeCoveragePoint, readCoverageSourceLine, resolveCoverageHighlightSpan, } from "../coverage-points.js";
6
7
  export const createReporter = (context) => {
7
8
  return new DefaultReporter(context);
8
9
  };
@@ -633,21 +634,60 @@ function renderSummaryLine(label, summary, layout = {
633
634
  process.stdout.write(totalText.padStart(layout.totalWidth) + "\n");
634
635
  }
635
636
  function renderCoverageSummary(summary) {
637
+ console.log("");
638
+ console.log(chalk.bold("Coverage"));
639
+ if (!summary.files.length || summary.total <= 0) {
640
+ console.log(` ${chalk.dim("No eligible source files were tracked for coverage.")}`);
641
+ return;
642
+ }
636
643
  const pct = summary.total
637
644
  ? ((summary.covered * 100) / summary.total).toFixed(2)
638
645
  : "100.00";
646
+ const missingLabel = summary.uncovered == 1 ? "1 point missing" : `${summary.uncovered} points missing`;
647
+ const fileLabel = summary.files.length == 1 ? "1 file" : `${summary.files.length} files`;
639
648
  const color = Number(pct) >= 90
640
649
  ? chalk.greenBright
641
650
  : Number(pct) >= 75
642
651
  ? chalk.yellowBright
643
652
  : chalk.redBright;
644
- console.log("");
645
- console.log(`${chalk.bold("Coverage:")} ${color(pct + "%")} ${chalk.dim(`(${summary.covered}/${summary.total} points, ${summary.uncovered} uncovered)`)}`);
653
+ console.log(` ${color(pct + "%")} ${renderCoverageBar(summary.percent)} ${chalk.dim(`(${summary.covered}/${summary.total} covered, ${missingLabel}, ${fileLabel})`)}`);
654
+ const ranked = [...summary.files].sort((a, b) => {
655
+ if (a.percent != b.percent)
656
+ return a.percent - b.percent;
657
+ if (a.uncovered != b.uncovered)
658
+ return b.uncovered - a.uncovered;
659
+ return a.file.localeCompare(b.file);
660
+ });
661
+ console.log(chalk.bold(" File Breakdown"));
662
+ for (const file of ranked.slice(0, 8)) {
663
+ const filePct = file.total
664
+ ? ((file.covered * 100) / file.total).toFixed(2)
665
+ : "100.00";
666
+ const fileColor = Number(filePct) >= 90
667
+ ? chalk.greenBright
668
+ : Number(filePct) >= 75
669
+ ? chalk.yellowBright
670
+ : chalk.redBright;
671
+ const suffix = file.uncovered > 0
672
+ ? `${file.uncovered} missing`
673
+ : "fully covered";
674
+ console.log(` ${fileColor(filePct.padStart(6) + "%")} ${toRelativeResultPath(file.file).padEnd(36)} ${chalk.dim(`${file.covered}/${file.total} covered, ${suffix}`)}`);
675
+ }
676
+ if (ranked.length > 8) {
677
+ console.log(chalk.dim(` ... ${ranked.length - 8} more files`));
678
+ }
646
679
  }
647
680
  function renderCoveragePoints(files) {
648
681
  console.log("");
649
- console.log(chalk.bold("Coverage Points:"));
682
+ console.log(chalk.bold("Coverage Gaps"));
650
683
  const sortedFiles = [...files].sort((a, b) => a.file.localeCompare(b.file));
684
+ const missingPoints = sortedFiles.flatMap((file) => file.points
685
+ .filter((point) => !point.executed)
686
+ .map((point) => ({
687
+ ...point,
688
+ displayType: describeCoveragePoint(point.file, point.line, point.column, point.type).displayType,
689
+ })));
690
+ const layout = createCoverageGapLayout(missingPoints);
651
691
  for (const file of sortedFiles) {
652
692
  const points = [...file.points].sort((a, b) => {
653
693
  if (a.line != b.line)
@@ -656,10 +696,69 @@ function renderCoveragePoints(files) {
656
696
  return a.column - b.column;
657
697
  return a.type.localeCompare(b.type);
658
698
  });
699
+ const missing = points.filter((point) => !point.executed);
700
+ if (!missing.length)
701
+ continue;
702
+ console.log(` ${chalk.bold(toRelativeResultPath(file.file))} ${chalk.dim(`(${missing.length} uncovered)`)}`);
659
703
  for (const point of points) {
660
704
  if (point.executed)
661
705
  continue;
662
- console.log(`${chalk.bgRed(" MISS ")} ${chalk.dim(`${point.file}:${point.line}:${point.column}`)} ${chalk.dim(point.type)}`);
706
+ const location = `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`;
707
+ const snippet = formatCoverageSnippet(point.file, point.line, point.column);
708
+ const typeLabel = describeCoveragePoint(point.file, point.line, point.column, point.type).displayType.padEnd(layout.typeWidth + 4);
709
+ const locationLabel = location.padEnd(layout.locationWidth + 4);
710
+ console.log(` ${chalk.red("x")} ${chalk.dim(typeLabel)}${chalk.dim(locationLabel)}${snippet}`);
663
711
  }
664
712
  }
665
713
  }
714
+ function renderCoverageBar(percent) {
715
+ const slots = 12;
716
+ const filled = Math.max(0, Math.min(slots, Math.round((Math.max(0, Math.min(100, percent)) / 100) * slots)));
717
+ return `[${"=".repeat(filled)}${"-".repeat(slots - filled)}]`;
718
+ }
719
+ function createCoverageGapLayout(points) {
720
+ return {
721
+ typeWidth: Math.max(...points.map((point) => point.displayType.length), 5),
722
+ locationWidth: Math.max(...points.map((point) => `${toRelativeResultPath(point.file)}:${point.line}:${point.column}`.length), 1),
723
+ };
724
+ }
725
+ function formatCoverageSnippet(file, line, column) {
726
+ const sourceLine = readCoverageSourceLine(file, line);
727
+ if (!sourceLine)
728
+ return "";
729
+ const expanded = sourceLine.replace(/\t/g, " ");
730
+ const firstNonWhitespace = expanded.search(/\S/);
731
+ if (firstNonWhitespace == -1)
732
+ return "";
733
+ const visible = expanded.slice(firstNonWhitespace).trimEnd();
734
+ if (!visible.length)
735
+ return "";
736
+ const maxWidth = 72;
737
+ const focus = Math.max(0, Math.min(visible.length - 1, Math.max(0, column - 1 - firstNonWhitespace)));
738
+ if (visible.length <= maxWidth) {
739
+ return styleCoverageSnippetWindow(visible, 0, visible.length, focus);
740
+ }
741
+ const start = Math.max(0, Math.min(visible.length - maxWidth, focus - Math.floor(maxWidth / 2)));
742
+ const end = Math.min(visible.length, start + maxWidth);
743
+ return styleCoverageSnippetWindow(visible, start, end, focus);
744
+ }
745
+ function styleCoverageSnippetWindow(visible, start, end, focus) {
746
+ const prefix = start > 0 ? "..." : "";
747
+ const suffix = end < visible.length ? "..." : "";
748
+ const slice = visible.slice(start, end);
749
+ const localFocus = Math.max(0, Math.min(slice.length - 1, focus - start));
750
+ const [highlightStart, highlightEnd] = resolveCoverageHighlightSpan(visible, focus);
751
+ const localStart = Math.max(0, Math.min(slice.length, highlightStart - start));
752
+ const localEnd = Math.max(localStart + 1, Math.min(slice.length, highlightEnd - start));
753
+ if (!slice.length)
754
+ return "";
755
+ if (localStart >= slice.length) {
756
+ return chalk.dim(`${prefix}${slice}${suffix}`);
757
+ }
758
+ const head = slice.slice(0, localStart);
759
+ const body = slice.slice(localStart, localEnd || localStart + 1);
760
+ const tail = slice.slice(localEnd || localStart + 1);
761
+ return (chalk.dim(prefix + head) +
762
+ chalk.dim.underline(body.length ? body : slice.charAt(localFocus)) +
763
+ chalk.dim(tail + suffix));
764
+ }
package/bin/types.js CHANGED
@@ -21,6 +21,15 @@ export class CoverageOptions {
21
21
  this.includeSpecs = false;
22
22
  this.include = [];
23
23
  this.exclude = [];
24
+ this.ignore = new CoverageIgnoreOptions();
25
+ }
26
+ }
27
+ export class CoverageIgnoreOptions {
28
+ constructor() {
29
+ this.labels = [];
30
+ this.names = [];
31
+ this.locations = [];
32
+ this.snippets = [];
24
33
  }
25
34
  }
26
35
  export class Suite {
package/bin/util.js CHANGED
@@ -340,6 +340,22 @@ function validateCoverageValue(value, path, issues) {
340
340
  }
341
341
  validateStringArrayField(obj, "include", path, issues);
342
342
  validateStringArrayField(obj, "exclude", path, issues);
343
+ if ("ignore" in obj && obj.ignore != undefined) {
344
+ if (!obj.ignore || typeof obj.ignore != "object" || Array.isArray(obj.ignore)) {
345
+ issues.push({
346
+ path: `${path}.ignore`,
347
+ message: "must be an object",
348
+ fix: 'set "ignore" to an object such as { "labels": ["Call"], "names": ["panic"] }',
349
+ });
350
+ }
351
+ else {
352
+ const ignore = obj.ignore;
353
+ validateStringArrayField(ignore, "labels", `${path}.ignore`, issues);
354
+ validateStringArrayField(ignore, "names", `${path}.ignore`, issues);
355
+ validateStringArrayField(ignore, "locations", `${path}.ignore`, issues);
356
+ validateStringArrayField(ignore, "snippets", `${path}.ignore`, issues);
357
+ }
358
+ }
343
359
  }
344
360
  function validateStringArrayField(raw, key, pathPrefix, issues) {
345
361
  if (!(key in raw) || raw[key] == undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "as-test",
3
- "version": "1.0.6",
3
+ "version": "1.0.9",
4
4
  "author": "Jairus Tanaka",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,6 +39,7 @@
39
39
  "description": "Testing framework for AssemblyScript. Compatible with WASI or Bindings",
40
40
  "files": [
41
41
  "assembly/**/*.ts",
42
+ "assembly/**/*.d.ts",
42
43
  "!assembly/__tests__/**",
43
44
  "!assembly/tsconfig.json",
44
45
  "bin/**/*.js",