@typecad/expect 0.1.0-alpha.1
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 +21 -0
- package/README.md +345 -0
- package/dist/host/chain-collector.d.ts +48 -0
- package/dist/host/chain-collector.js +153 -0
- package/dist/host/cli.d.ts +2 -0
- package/dist/host/cli.js +185 -0
- package/dist/host/compiler.d.ts +27 -0
- package/dist/host/compiler.js +250 -0
- package/dist/host/config.d.ts +29 -0
- package/dist/host/config.js +218 -0
- package/dist/host/evaluator.d.ts +14 -0
- package/dist/host/evaluator.js +113 -0
- package/dist/host/finder.d.ts +12 -0
- package/dist/host/finder.js +70 -0
- package/dist/host/parser.d.ts +5 -0
- package/dist/host/parser.js +196 -0
- package/dist/host/preprocessor.d.ts +33 -0
- package/dist/host/preprocessor.js +115 -0
- package/dist/host/protocol-emitter.d.ts +29 -0
- package/dist/host/protocol-emitter.js +107 -0
- package/dist/host/reporter.d.ts +11 -0
- package/dist/host/reporter.js +200 -0
- package/dist/host/runner.d.ts +7 -0
- package/dist/host/runner.js +256 -0
- package/dist/host/serial.d.ts +28 -0
- package/dist/host/serial.js +129 -0
- package/dist/host/types.d.ts +121 -0
- package/dist/host/types.js +12 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/stubs.d.ts +17 -0
- package/dist/stubs.js +26 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +8 -0
- package/package.json +80 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// @typecad/expect — AST Preprocessor
|
|
3
|
+
//
|
|
4
|
+
// Transforms fluent test syntax into flat Serial.print/println protocol
|
|
5
|
+
// calls that the cuttlefish transpiler handles correctly.
|
|
6
|
+
//
|
|
7
|
+
// INPUT (user test file):
|
|
8
|
+
// describe("A0 analog read")
|
|
9
|
+
// .it("reads zero").expect(A0.readAnalog()).toBe(0);
|
|
10
|
+
// done();
|
|
11
|
+
//
|
|
12
|
+
// OUTPUT (preprocessed — fed to transpiler):
|
|
13
|
+
// Serial.begin(115200);
|
|
14
|
+
// Serial.println("[TC:SUITE_START]");
|
|
15
|
+
// Serial.println("[TC:DESCRIBE:A0 analog read]");
|
|
16
|
+
// Serial.println("[TC:IT:reads zero]");
|
|
17
|
+
// const __tc_v1: number = A0.readAnalog();
|
|
18
|
+
// Serial.print("[TC:EXPECT:toBe:0:");
|
|
19
|
+
// Serial.print(__tc_v1);
|
|
20
|
+
// Serial.println("]");
|
|
21
|
+
// Serial.println("[TC:SUITE_END]");
|
|
22
|
+
// while (true) { delay(1000); }
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
import ts from 'typescript';
|
|
25
|
+
import { isDescribeChain, collectChainSegments } from './chain-collector.js';
|
|
26
|
+
import { emitSegments } from './protocol-emitter.js';
|
|
27
|
+
/**
|
|
28
|
+
* Preprocess a test file's TypeScript source.
|
|
29
|
+
*
|
|
30
|
+
* 1. Strips `import { ... } from '@typecad/expect'`
|
|
31
|
+
* 2. Walks top-level expression-statements looking for `describe(...)...` chains
|
|
32
|
+
* 3. Replaces `done()` with the suite-end sentinel + idle loop
|
|
33
|
+
* 4. Hoists hardware expressions out of `expect()` into `const` declarations
|
|
34
|
+
* 5. Wraps everything with Serial.initialize + SUITE_START preamble
|
|
35
|
+
*/
|
|
36
|
+
export function preprocess(source, fileName = 'test.ts', options) {
|
|
37
|
+
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
38
|
+
const ctx = new PreprocessorContext(options?.isAvr ?? false);
|
|
39
|
+
for (const stmt of sf.statements) {
|
|
40
|
+
if (ts.isImportDeclaration(stmt)) {
|
|
41
|
+
const moduleSpecifier = stmt.moduleSpecifier.text;
|
|
42
|
+
if (moduleSpecifier === '@typecad/expect')
|
|
43
|
+
continue; // strip
|
|
44
|
+
ctx.emit(stmt.getText(sf));
|
|
45
|
+
}
|
|
46
|
+
else if (ts.isExpressionStatement(stmt)) {
|
|
47
|
+
processExpressionStatement(stmt, sf, ctx);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
ctx.emit(stmt.getText(sf));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return ctx.build();
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// PreprocessorContext — shared state threaded through sub-modules
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
export class PreprocessorContext {
|
|
59
|
+
constructor(isAvr) {
|
|
60
|
+
this.lines = [];
|
|
61
|
+
this.varCounter = 0;
|
|
62
|
+
this.fnCounter = 0;
|
|
63
|
+
this.preambleEmitted = false;
|
|
64
|
+
this.baudRate = 115200;
|
|
65
|
+
this.isAvr = isAvr;
|
|
66
|
+
}
|
|
67
|
+
/** Wrap a string literal in F() on AVR to keep it in flash. */
|
|
68
|
+
flash(s) {
|
|
69
|
+
return this.isAvr ? `F("${s}")` : `"${s}"`;
|
|
70
|
+
}
|
|
71
|
+
/** Emit a line of TypeScript output. */
|
|
72
|
+
emit(line) {
|
|
73
|
+
if (!this.preambleEmitted)
|
|
74
|
+
this.emitPreamble();
|
|
75
|
+
this.lines.push(line);
|
|
76
|
+
}
|
|
77
|
+
/** Generate a unique temporary variable name. */
|
|
78
|
+
nextVar() {
|
|
79
|
+
return `__tc_v${++this.varCounter}`;
|
|
80
|
+
}
|
|
81
|
+
/** Generate a unique extracted function name. */
|
|
82
|
+
nextFn() {
|
|
83
|
+
return `__tc_fn${++this.fnCounter}`;
|
|
84
|
+
}
|
|
85
|
+
emitPreamble() {
|
|
86
|
+
this.preambleEmitted = true;
|
|
87
|
+
this.lines.push(`Serial.begin(${this.baudRate});`);
|
|
88
|
+
this.lines.push(`Serial.println(${this.flash('[TC:SUITE_START]')});`);
|
|
89
|
+
}
|
|
90
|
+
build() {
|
|
91
|
+
return this.lines.join('\n') + '\n';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Statement processing
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
function processExpressionStatement(stmt, sf, ctx) {
|
|
98
|
+
const expr = stmt.expression;
|
|
99
|
+
if (isDoneCall(expr)) {
|
|
100
|
+
ctx.emit(`Serial.println(${ctx.flash('[TC:SUITE_END]')});`);
|
|
101
|
+
ctx.emit(`while (true) { delay(1000); }`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (isDescribeChain(expr)) {
|
|
105
|
+
const segments = collectChainSegments(expr, sf, ctx);
|
|
106
|
+
emitSegments(segments, ctx);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
ctx.emit(stmt.getText(sf));
|
|
110
|
+
}
|
|
111
|
+
function isDoneCall(expr) {
|
|
112
|
+
return (ts.isCallExpression(expr) &&
|
|
113
|
+
ts.isIdentifier(expr.expression) &&
|
|
114
|
+
expr.expression.text === 'done');
|
|
115
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PreprocessorContext } from './preprocessor.js';
|
|
2
|
+
import type { ChainSegment } from './chain-collector.js';
|
|
3
|
+
/**
|
|
4
|
+
* Emit all Serial protocol lines for a collected chain.
|
|
5
|
+
*/
|
|
6
|
+
export declare function emitSegments(segments: ChainSegment[], ctx: PreprocessorContext): void;
|
|
7
|
+
/**
|
|
8
|
+
* Emit the Serial.print sequence for one assertion.
|
|
9
|
+
*
|
|
10
|
+
* Numeric: Serial.print("[TC:EXPECT:matcher:expected:");
|
|
11
|
+
* Serial.print(actual);
|
|
12
|
+
* Serial.println("]");
|
|
13
|
+
*
|
|
14
|
+
* String: Serial.print("[TC:EXPECT:matcher:");
|
|
15
|
+
* Serial.print("expected");
|
|
16
|
+
* Serial.print(":");
|
|
17
|
+
* Serial.print(actual);
|
|
18
|
+
* Serial.println("]");
|
|
19
|
+
*/
|
|
20
|
+
export declare function emitExpectProtocol(actualVar: string, matcher: string, matcherArgs: string[], ctx: PreprocessorContext, isString?: boolean): void;
|
|
21
|
+
/**
|
|
22
|
+
* Is the expression "simple" enough to inline without hoisting?
|
|
23
|
+
* Simple: identifiers, literals. Complex: anything with method calls.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isSimpleExpression(expr: string): boolean;
|
|
26
|
+
/** Heuristic: does this expression produce a string? */
|
|
27
|
+
export declare function isStringExpression(expr: string): boolean;
|
|
28
|
+
/** Escape characters that could break the protocol line format. */
|
|
29
|
+
export declare function escapeProtocol(s: string): string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// @typecad/expect — Protocol Emitter
|
|
3
|
+
//
|
|
4
|
+
// Converts collected chain segments into Serial.print/println protocol lines.
|
|
5
|
+
// All logic for the [TC:DESCRIBE:...], [TC:IT:...], [TC:EXPECT:...] wire
|
|
6
|
+
// format lives here.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Segment emission
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Emit all Serial protocol lines for a collected chain.
|
|
13
|
+
*/
|
|
14
|
+
export function emitSegments(segments, ctx) {
|
|
15
|
+
// First pass: emit any extracted function definitions before the protocol
|
|
16
|
+
for (const seg of segments) {
|
|
17
|
+
if (seg.kind === 'expect' && seg.extractedFn) {
|
|
18
|
+
ctx.emit(seg.extractedFn);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// Second pass: emit protocol lines
|
|
22
|
+
for (const seg of segments) {
|
|
23
|
+
switch (seg.kind) {
|
|
24
|
+
case 'describe':
|
|
25
|
+
ctx.emit(`Serial.println(${ctx.flash(`[TC:DESCRIBE:${escapeProtocol(seg.name ?? '')}]`)});`);
|
|
26
|
+
break;
|
|
27
|
+
case 'it':
|
|
28
|
+
ctx.emit(`Serial.println(${ctx.flash(`[TC:IT:${escapeProtocol(seg.name ?? '')}]`)});`);
|
|
29
|
+
break;
|
|
30
|
+
case 'expect': {
|
|
31
|
+
if (!seg.matcher)
|
|
32
|
+
break; // expect() without matcher — skip
|
|
33
|
+
const actualExpr = seg.actualExpr ?? '0';
|
|
34
|
+
const isString = seg.isStringExpect || seg.matcher === 'toContain' || seg.matcher === 'toHaveLength'
|
|
35
|
+
|| (seg.matcher === 'toBe' && isStringExpression(actualExpr));
|
|
36
|
+
const typeAnnotation = isString ? 'string' : 'number';
|
|
37
|
+
if (isSimpleExpression(actualExpr)) {
|
|
38
|
+
emitExpectProtocol(actualExpr, seg.matcher, seg.matcherArgs ?? [], ctx, isString);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Hoist: const __tc_v1: number = A0.readAnalog();
|
|
42
|
+
const tmpVar = ctx.nextVar();
|
|
43
|
+
ctx.emit(`const ${tmpVar}: ${typeAnnotation} = ${actualExpr};`);
|
|
44
|
+
emitExpectProtocol(tmpVar, seg.matcher, seg.matcherArgs ?? [], ctx, isString);
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Protocol line builder
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
/**
|
|
55
|
+
* Emit the Serial.print sequence for one assertion.
|
|
56
|
+
*
|
|
57
|
+
* Numeric: Serial.print("[TC:EXPECT:matcher:expected:");
|
|
58
|
+
* Serial.print(actual);
|
|
59
|
+
* Serial.println("]");
|
|
60
|
+
*
|
|
61
|
+
* String: Serial.print("[TC:EXPECT:matcher:");
|
|
62
|
+
* Serial.print("expected");
|
|
63
|
+
* Serial.print(":");
|
|
64
|
+
* Serial.print(actual);
|
|
65
|
+
* Serial.println("]");
|
|
66
|
+
*/
|
|
67
|
+
export function emitExpectProtocol(actualVar, matcher, matcherArgs, ctx, isString = false) {
|
|
68
|
+
if (isString && matcher === 'toBe') {
|
|
69
|
+
const rawExpected = (matcherArgs[0] ?? '').replace(/^["']|["']$/g, '');
|
|
70
|
+
ctx.emit(`Serial.print(${ctx.flash(`[TC:EXPECT:${matcher}:`)});`);
|
|
71
|
+
ctx.emit(`Serial.print("${escapeProtocol(rawExpected)}");`);
|
|
72
|
+
ctx.emit(`Serial.print(${ctx.flash(':')});`);
|
|
73
|
+
ctx.emit(`Serial.print(${actualVar});`);
|
|
74
|
+
ctx.emit(`Serial.println(${ctx.flash(']')});`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const expectedPart = matcherArgs.join(',');
|
|
78
|
+
ctx.emit(`Serial.print(${ctx.flash(`[TC:EXPECT:${matcher}:${expectedPart}:`)});`);
|
|
79
|
+
ctx.emit(`Serial.print(${actualVar});`);
|
|
80
|
+
ctx.emit(`Serial.println(${ctx.flash(']')});`);
|
|
81
|
+
}
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Expression classification helpers
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
/**
|
|
86
|
+
* Is the expression "simple" enough to inline without hoisting?
|
|
87
|
+
* Simple: identifiers, literals. Complex: anything with method calls.
|
|
88
|
+
*/
|
|
89
|
+
export function isSimpleExpression(expr) {
|
|
90
|
+
return !/\.\w+\s*\(/.test(expr);
|
|
91
|
+
}
|
|
92
|
+
/** Heuristic: does this expression produce a string? */
|
|
93
|
+
export function isStringExpression(expr) {
|
|
94
|
+
if (/^["']/.test(expr))
|
|
95
|
+
return true;
|
|
96
|
+
if (/\.readString|\.readLine/.test(expr))
|
|
97
|
+
return true;
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
/** Escape characters that could break the protocol line format. */
|
|
101
|
+
export function escapeProtocol(s) {
|
|
102
|
+
return s
|
|
103
|
+
.replace(/\\/g, '\\\\')
|
|
104
|
+
.replace(/"/g, '\\"')
|
|
105
|
+
.replace(/\n/g, '\\n')
|
|
106
|
+
.replace(/\r/g, '\\r');
|
|
107
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RunResult, FileResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Print a single test file's results immediately.
|
|
4
|
+
*/
|
|
5
|
+
export declare function reportFileResult(file: FileResult, options?: {
|
|
6
|
+
verbose?: boolean;
|
|
7
|
+
}): void;
|
|
8
|
+
export declare function reportSummary(result: RunResult, options?: {
|
|
9
|
+
board?: string;
|
|
10
|
+
port?: string;
|
|
11
|
+
}): void;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// @typecad/expect — Vitest-style console reporter
|
|
3
|
+
//
|
|
4
|
+
// Renders test results to the console with ANSI colors, mimicking vitest's
|
|
5
|
+
// output format.
|
|
6
|
+
//
|
|
7
|
+
// Example output:
|
|
8
|
+
//
|
|
9
|
+
// cuttlefish-test v0.1.0
|
|
10
|
+
//
|
|
11
|
+
// ✓ A0 analog read (2 tests)
|
|
12
|
+
// ✓ reads zero when grounded 2ms
|
|
13
|
+
// ✓ reads less than 100 1ms
|
|
14
|
+
//
|
|
15
|
+
// ✗ Temperature sensor (1 of 2 failing)
|
|
16
|
+
// ✓ reads above freezing 3ms
|
|
17
|
+
// ✗ reads room temperature 2ms
|
|
18
|
+
//
|
|
19
|
+
// expect(actual).toBeWithinRange(20, 25)
|
|
20
|
+
// Actual: 31
|
|
21
|
+
// Expected: 20–25
|
|
22
|
+
//
|
|
23
|
+
// Tests 3 passed | 1 failed (4)
|
|
24
|
+
// Board Arduino Uno @ COM3
|
|
25
|
+
// Time 8.42s
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
import { describeExpected } from './evaluator.js';
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// ANSI color codes
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
const RESET = '\x1b[0m';
|
|
32
|
+
const BOLD = '\x1b[1m';
|
|
33
|
+
const DIM = '\x1b[2m';
|
|
34
|
+
const RED = '\x1b[31m';
|
|
35
|
+
const GREEN = '\x1b[32m';
|
|
36
|
+
const YELLOW = '\x1b[33m';
|
|
37
|
+
const CYAN = '\x1b[36m';
|
|
38
|
+
const WHITE = '\x1b[37m';
|
|
39
|
+
const RED_BG = '\x1b[41m';
|
|
40
|
+
const GREEN_BG = '\x1b[42m';
|
|
41
|
+
const PASS_ICON = `${GREEN}✓${RESET}`;
|
|
42
|
+
const FAIL_ICON = `${RED}✗${RESET}`;
|
|
43
|
+
const SKIP_ICON = `${YELLOW}↓${RESET}`;
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Public API
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
/**
|
|
48
|
+
* Print a single test file's results immediately.
|
|
49
|
+
*/
|
|
50
|
+
export function reportFileResult(file, options = {}) {
|
|
51
|
+
const verbose = options.verbose ?? false;
|
|
52
|
+
reportFile(file, verbose);
|
|
53
|
+
const failures = collectFailuresFromFile(file);
|
|
54
|
+
if (failures.length > 0) {
|
|
55
|
+
console.log(`${BOLD}${RED} FAILURES${RESET}`);
|
|
56
|
+
console.log();
|
|
57
|
+
for (const f of failures) {
|
|
58
|
+
reportFailure(f);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
console.log();
|
|
62
|
+
}
|
|
63
|
+
export function reportSummary(result, options = {}) {
|
|
64
|
+
const { board, port } = options;
|
|
65
|
+
reportSummarySection(result, board, port);
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Internal — File reporting
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
function reportFile(file, verbose) {
|
|
71
|
+
if (file.skipped) {
|
|
72
|
+
console.log(` ${SKIP_ICON} ${BOLD}${file.filePath}${RESET} ${YELLOW}(skipped)${RESET}`);
|
|
73
|
+
if (verbose && file.skipReason) {
|
|
74
|
+
console.log(` ${DIM}${file.skipReason}${RESET}`);
|
|
75
|
+
}
|
|
76
|
+
console.log();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (file.error) {
|
|
80
|
+
console.log(` ${FAIL_ICON} ${BOLD}${file.filePath}${RESET} ${RED}(error)${RESET}`);
|
|
81
|
+
console.log(` ${DIM}${file.error}${RESET}`);
|
|
82
|
+
console.log();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const desc of file.describes) {
|
|
86
|
+
reportDescribe(desc, verbose);
|
|
87
|
+
}
|
|
88
|
+
// Debug output
|
|
89
|
+
if (verbose && file.debugOutput.length > 0) {
|
|
90
|
+
console.log(` ${DIM}── serial debug ──${RESET}`);
|
|
91
|
+
for (const line of file.debugOutput) {
|
|
92
|
+
console.log(` ${DIM}${line}${RESET}`);
|
|
93
|
+
}
|
|
94
|
+
console.log();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function reportDescribe(desc, verbose) {
|
|
98
|
+
const totalTests = desc.tests.length;
|
|
99
|
+
const passedTests = desc.tests.filter(t => t.passed).length;
|
|
100
|
+
const failedTests = totalTests - passedTests;
|
|
101
|
+
const icon = desc.passed ? PASS_ICON : FAIL_ICON;
|
|
102
|
+
const countText = desc.passed
|
|
103
|
+
? `${DIM}(${totalTests} test${totalTests !== 1 ? 's' : ''})${RESET}`
|
|
104
|
+
: `${RED}(${failedTests} of ${totalTests} failing)${RESET}`;
|
|
105
|
+
console.log(` ${icon} ${BOLD}${desc.name}${RESET} ${countText}`);
|
|
106
|
+
for (const test of desc.tests) {
|
|
107
|
+
reportTest(test, verbose);
|
|
108
|
+
}
|
|
109
|
+
console.log();
|
|
110
|
+
}
|
|
111
|
+
function reportTest(test, verbose) {
|
|
112
|
+
const icon = test.passed ? PASS_ICON : FAIL_ICON;
|
|
113
|
+
const duration = test.durationMs > 0 ? `${DIM}${test.durationMs}ms${RESET}` : '';
|
|
114
|
+
console.log(` ${icon} ${test.name} ${duration}`);
|
|
115
|
+
// In verbose mode, show passing assertion details too
|
|
116
|
+
if (verbose && test.passed) {
|
|
117
|
+
for (const a of test.assertions) {
|
|
118
|
+
console.log(` ${DIM}${a.matcher}(${a.expected}) → ${a.actual} ✓${RESET}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function collectFailuresFromFile(file) {
|
|
123
|
+
const failures = [];
|
|
124
|
+
if (file.error) {
|
|
125
|
+
return failures;
|
|
126
|
+
}
|
|
127
|
+
for (const desc of file.describes) {
|
|
128
|
+
for (const test of desc.tests) {
|
|
129
|
+
for (const assertion of test.assertions) {
|
|
130
|
+
if (!assertion.passed) {
|
|
131
|
+
failures.push({
|
|
132
|
+
describeName: desc.name,
|
|
133
|
+
testName: test.name,
|
|
134
|
+
assertion,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return failures;
|
|
141
|
+
}
|
|
142
|
+
function reportFailure(f) {
|
|
143
|
+
const a = f.assertion;
|
|
144
|
+
console.log(` ${RED}${BOLD}${f.describeName} > ${f.testName}${RESET}`);
|
|
145
|
+
console.log();
|
|
146
|
+
console.log(` ${DIM}expect(actual).${a.matcher}(${a.expected})${RESET}`);
|
|
147
|
+
console.log(` ${RED}Actual: ${BOLD}${a.actual}${RESET}`);
|
|
148
|
+
console.log(` ${GREEN}Expected: ${BOLD}${describeExpected(a.matcher, a.expected)}${RESET}`);
|
|
149
|
+
console.log();
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Internal — Summary
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
function reportSummarySection(result, board, port) {
|
|
155
|
+
// Tests line
|
|
156
|
+
const passedText = result.totalPassed > 0
|
|
157
|
+
? `${GREEN}${BOLD}${result.totalPassed} passed${RESET}`
|
|
158
|
+
: '';
|
|
159
|
+
const failedText = result.totalFailed > 0
|
|
160
|
+
? `${RED}${BOLD}${result.totalFailed} failed${RESET}`
|
|
161
|
+
: '';
|
|
162
|
+
const skippedText = result.totalSkipped > 0
|
|
163
|
+
? `${YELLOW}${BOLD}${result.totalSkipped} skipped${RESET}`
|
|
164
|
+
: '';
|
|
165
|
+
const parts = [passedText, failedText, skippedText].filter(Boolean).join(`${DIM} | ${RESET}`);
|
|
166
|
+
const total = result.totalTests + result.totalSkipped;
|
|
167
|
+
console.log(` ${BOLD}Tests${RESET} ${parts} ${DIM}(${total})${RESET}`);
|
|
168
|
+
if (result.totalSkipped > 0) {
|
|
169
|
+
const skippedFiles = result.files.filter(file => file.skipped).length;
|
|
170
|
+
const totalFiles = result.files.length;
|
|
171
|
+
console.log(` ${BOLD}Test Files${RESET} ${YELLOW}${BOLD}${skippedFiles} skipped${RESET} ${DIM}(${totalFiles})${RESET}`);
|
|
172
|
+
}
|
|
173
|
+
// Errors line (compile/upload failures)
|
|
174
|
+
if (result.totalErrors > 0) {
|
|
175
|
+
console.log(` ${BOLD}Errors${RESET} ${RED}${BOLD}${result.totalErrors} file${result.totalErrors !== 1 ? 's' : ''} failed to build${RESET}`);
|
|
176
|
+
}
|
|
177
|
+
// Board line
|
|
178
|
+
if (board || port) {
|
|
179
|
+
const boardPart = board ?? 'unknown';
|
|
180
|
+
const portPart = port ? ` @ ${port}` : '';
|
|
181
|
+
console.log(` ${BOLD}Board${RESET} ${boardPart}${portPart}`);
|
|
182
|
+
}
|
|
183
|
+
// Time line
|
|
184
|
+
const seconds = (result.durationMs / 1000).toFixed(2);
|
|
185
|
+
console.log(` ${BOLD}Time${RESET} ${seconds}s`);
|
|
186
|
+
// Final status bar
|
|
187
|
+
if (result.totalFailed > 0 || result.totalErrors > 0) {
|
|
188
|
+
console.log();
|
|
189
|
+
const failParts = [];
|
|
190
|
+
if (result.totalFailed > 0)
|
|
191
|
+
failParts.push(`${result.totalFailed} test${result.totalFailed !== 1 ? 's' : ''} failed`);
|
|
192
|
+
if (result.totalErrors > 0)
|
|
193
|
+
failParts.push(`${result.totalErrors} build error${result.totalErrors !== 1 ? 's' : ''}`);
|
|
194
|
+
console.log(`${RED_BG}${WHITE}${BOLD} FAIL ${RESET} ${RED}${failParts.join(', ')}${RESET}`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(`${GREEN_BG}${WHITE}${BOLD} PASS ${RESET} ${GREEN}All tests passed${RESET}`);
|
|
199
|
+
}
|
|
200
|
+
}
|