@typecad/expect 0.1.0-alpha.2 → 1.0.0-alpha.6
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/README.md +6 -6
- package/dist/host/compiler.js +24 -0
- package/dist/host/config.js +1 -0
- package/dist/host/preprocessor.d.ts +26 -3
- package/dist/host/preprocessor.js +26 -8
- package/dist/host/protocol-emitter.js +10 -10
- package/dist/host/runner.js +2 -1
- package/dist/host/types.d.ts +2 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ Hardware test runner for [TypeCAD](../../README.md). Write vitest-style assertio
|
|
|
62
62
|
npm install --save-dev @typecad/expect @typecad/cuttlefish
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
`@typecad/expect` ships the `cuttlefish-test` CLI. It pairs with [`@typecad/cuttlefish`](https://
|
|
65
|
+
`@typecad/expect` ships the `cuttlefish-test` CLI. It pairs with [`@typecad/cuttlefish`](https://cuttlefish.typecad.net), which transpiles your TypeScript test files to C++ for upload to hardware.
|
|
66
66
|
|
|
67
67
|
**Prerequisites:**
|
|
68
68
|
|
|
@@ -152,13 +152,13 @@ All numeric matchers return the parent `Suite`, so you can continue the chain wi
|
|
|
152
152
|
|
|
153
153
|
```bash
|
|
154
154
|
# Run a specific file
|
|
155
|
-
npx cuttlefish-test examples/my-sensor.test.ts
|
|
155
|
+
npx --package=@typecad/expect cuttlefish-test examples/my-sensor.test.ts
|
|
156
156
|
|
|
157
157
|
# Run all test files matched by config include patterns
|
|
158
|
-
npx cuttlefish-test
|
|
158
|
+
npx --package=@typecad/expect cuttlefish-test
|
|
159
159
|
|
|
160
160
|
# Override the port at run-time
|
|
161
|
-
npx cuttlefish-test --port /dev/ttyACM0 examples/my-sensor.test.ts
|
|
161
|
+
npx --package=@typecad/expect cuttlefish-test --port /dev/ttyACM0 examples/my-sensor.test.ts
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
Or via the npm script defined in the root `package.json`:
|
|
@@ -253,10 +253,10 @@ Example flow:
|
|
|
253
253
|
|
|
254
254
|
```bash
|
|
255
255
|
# 1. Compile and upload the serial-output showcase
|
|
256
|
-
npx cuttlefish src/23-transpiler-showcase.ts --compile --upload --port COM4
|
|
256
|
+
npx @typecad/cuttlefish src/23-transpiler-showcase.ts --compile --upload --port COM4
|
|
257
257
|
|
|
258
258
|
# 2. Run the on-hardware expect test against the connected Uno
|
|
259
|
-
npx cuttlefish-test examples/24-uno-validation.test.ts --port COM4
|
|
259
|
+
npx --package=@typecad/expect cuttlefish-test examples/24-uno-validation.test.ts --port COM4
|
|
260
260
|
```
|
|
261
261
|
|
|
262
262
|
This hybrid workflow is the recommended way to confirm that simple variables, arithmetic, arrays, enums, functions, GPIO, and analog input are behaving correctly on real Uno hardware.
|
package/dist/host/compiler.js
CHANGED
|
@@ -8,6 +8,14 @@ import path from 'node:path';
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
10
|
import { parseConfigAST } from './config.js';
|
|
11
|
+
import { checkArduinoEnv } from '@typecad/arduino-cli';
|
|
12
|
+
/** Format a check failure into the `error` field used by CompileResult/UploadResult. */
|
|
13
|
+
function formatEnvFailure(failure) {
|
|
14
|
+
const lines = [...failure.messages];
|
|
15
|
+
if (failure.fixCommand)
|
|
16
|
+
lines.push(` Fix: ${failure.fixCommand}`);
|
|
17
|
+
return lines.join('\n');
|
|
18
|
+
}
|
|
11
19
|
/**
|
|
12
20
|
* Transpile preprocessed TypeScript source to a C++ Arduino sketch.
|
|
13
21
|
*
|
|
@@ -77,6 +85,14 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
|
|
|
77
85
|
* Compile the Arduino sketch using arduino-cli.
|
|
78
86
|
*/
|
|
79
87
|
export function compileSketch(sketchDir, buildTarget) {
|
|
88
|
+
// Hard gate: verify arduino-cli + core before spawning.
|
|
89
|
+
{
|
|
90
|
+
const gate = checkArduinoEnv(buildTarget);
|
|
91
|
+
if (!gate.ok) {
|
|
92
|
+
const message = formatEnvFailure(gate);
|
|
93
|
+
return { success: false, sketchDir, sketchPath: '', output: message, error: message };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
80
96
|
const result = spawnSync('arduino-cli', ['compile', '--fqbn', buildTarget, sketchDir], { encoding: 'utf8', timeout: 120000 });
|
|
81
97
|
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
82
98
|
return {
|
|
@@ -91,6 +107,14 @@ export function compileSketch(sketchDir, buildTarget) {
|
|
|
91
107
|
* Upload the compiled sketch to the board.
|
|
92
108
|
*/
|
|
93
109
|
export function uploadSketch(sketchDir, buildTarget, port) {
|
|
110
|
+
// Hard gate: verify arduino-cli + core before spawning.
|
|
111
|
+
{
|
|
112
|
+
const gate = checkArduinoEnv(buildTarget);
|
|
113
|
+
if (!gate.ok) {
|
|
114
|
+
const message = formatEnvFailure(gate);
|
|
115
|
+
return { success: false, output: message, error: message };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
94
118
|
const result = spawnSync('arduino-cli', ['upload', '--fqbn', buildTarget, '--port', port, sketchDir], { encoding: 'utf8', timeout: 60000 });
|
|
95
119
|
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
96
120
|
return {
|
package/dist/host/config.js
CHANGED
|
@@ -55,6 +55,7 @@ export function loadConfig(projectRoot, overrides = {}) {
|
|
|
55
55
|
buildTarget: test.buildTarget ?? raw.frameworkData?.buildTarget ?? 'arduino:avr:uno',
|
|
56
56
|
board: test.board ?? raw.board ?? '@typecad/board-arduino-uno',
|
|
57
57
|
target: raw.target ?? 'avr',
|
|
58
|
+
framework: raw.framework,
|
|
58
59
|
projectRoot,
|
|
59
60
|
};
|
|
60
61
|
}
|
|
@@ -1,6 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Describes how the test protocol emits output for a given framework.
|
|
3
|
+
* Each framework provides its own shim so the preprocessor doesn't hardcode
|
|
4
|
+
* Serial.* (which would force the Arduino core to be linked).
|
|
5
|
+
*/
|
|
6
|
+
export interface OutputShim {
|
|
7
|
+
/** The init call emitted in the preamble, e.g. "Serial.begin(115200)". */
|
|
8
|
+
begin: string;
|
|
9
|
+
/** Print without newline — receives a fully-formed argument expression. */
|
|
10
|
+
print: (expr: string) => string;
|
|
11
|
+
/** Print with newline — receives a fully-formed argument expression. */
|
|
12
|
+
println: (expr: string) => string;
|
|
13
|
+
/** The idle-loop delay call after SUITE_END, e.g. "delay(1000)". */
|
|
14
|
+
delay: string;
|
|
15
|
+
}
|
|
16
|
+
/** Default shim: Arduino HardwareSerial. */
|
|
17
|
+
export declare const serialShim: OutputShim;
|
|
18
|
+
/** AVR native UART shim: routes through framework-avr's _uart_* helpers. */
|
|
19
|
+
export declare const avrUartShim: OutputShim;
|
|
1
20
|
export interface PreprocessorOptions {
|
|
2
21
|
/** Wrap string literals in Arduino F() macro to save SRAM on AVR. */
|
|
3
22
|
isAvr?: boolean;
|
|
23
|
+
/** Output shim — defaults to serialShim (Arduino HardwareSerial). */
|
|
24
|
+
shim?: OutputShim;
|
|
4
25
|
}
|
|
5
26
|
/**
|
|
6
27
|
* Preprocess a test file's TypeScript source.
|
|
@@ -17,10 +38,12 @@ export declare class PreprocessorContext {
|
|
|
17
38
|
private varCounter;
|
|
18
39
|
private fnCounter;
|
|
19
40
|
private preambleEmitted;
|
|
20
|
-
private readonly baudRate;
|
|
21
41
|
readonly isAvr: boolean;
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
readonly shim: OutputShim;
|
|
43
|
+
constructor(isAvr: boolean, shim?: OutputShim);
|
|
44
|
+
/** Wrap a string literal in F() on AVR to keep it in flash.
|
|
45
|
+
* Only applies when using the serialShim (Arduino core provides F()).
|
|
46
|
+
* The avrUartShim runs without the core, so F() is undefined — plain strings. */
|
|
24
47
|
flash(s: string): string;
|
|
25
48
|
/** Emit a line of TypeScript output. */
|
|
26
49
|
emit(line: string): void;
|
|
@@ -24,6 +24,20 @@
|
|
|
24
24
|
import ts from 'typescript';
|
|
25
25
|
import { isDescribeChain, collectChainSegments } from './chain-collector.js';
|
|
26
26
|
import { emitSegments } from './protocol-emitter.js';
|
|
27
|
+
/** Default shim: Arduino HardwareSerial. */
|
|
28
|
+
export const serialShim = {
|
|
29
|
+
begin: 'Serial.begin(115200)',
|
|
30
|
+
print: (e) => `Serial.print(${e})`,
|
|
31
|
+
println: (e) => `Serial.println(${e})`,
|
|
32
|
+
delay: 'delay(1000)',
|
|
33
|
+
};
|
|
34
|
+
/** AVR native UART shim: routes through framework-avr's _uart_* helpers. */
|
|
35
|
+
export const avrUartShim = {
|
|
36
|
+
begin: '_uart_init(115200)',
|
|
37
|
+
print: (e) => `_uart_print_expr(${e})`,
|
|
38
|
+
println: (e) => `_uart_println_expr(${e})`,
|
|
39
|
+
delay: '_native_delay_ms(1000)',
|
|
40
|
+
};
|
|
27
41
|
/**
|
|
28
42
|
* Preprocess a test file's TypeScript source.
|
|
29
43
|
*
|
|
@@ -35,7 +49,7 @@ import { emitSegments } from './protocol-emitter.js';
|
|
|
35
49
|
*/
|
|
36
50
|
export function preprocess(source, fileName = 'test.ts', options) {
|
|
37
51
|
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
38
|
-
const ctx = new PreprocessorContext(options?.isAvr ?? false);
|
|
52
|
+
const ctx = new PreprocessorContext(options?.isAvr ?? false, options?.shim);
|
|
39
53
|
for (const stmt of sf.statements) {
|
|
40
54
|
if (ts.isImportDeclaration(stmt)) {
|
|
41
55
|
const moduleSpecifier = stmt.moduleSpecifier.text;
|
|
@@ -56,16 +70,20 @@ export function preprocess(source, fileName = 'test.ts', options) {
|
|
|
56
70
|
// PreprocessorContext — shared state threaded through sub-modules
|
|
57
71
|
// ---------------------------------------------------------------------------
|
|
58
72
|
export class PreprocessorContext {
|
|
59
|
-
constructor(isAvr) {
|
|
73
|
+
constructor(isAvr, shim = serialShim) {
|
|
60
74
|
this.lines = [];
|
|
61
75
|
this.varCounter = 0;
|
|
62
76
|
this.fnCounter = 0;
|
|
63
77
|
this.preambleEmitted = false;
|
|
64
|
-
this.baudRate = 115200;
|
|
65
78
|
this.isAvr = isAvr;
|
|
79
|
+
this.shim = shim;
|
|
66
80
|
}
|
|
67
|
-
/** Wrap a string literal in F() on AVR to keep it in flash.
|
|
81
|
+
/** Wrap a string literal in F() on AVR to keep it in flash.
|
|
82
|
+
* Only applies when using the serialShim (Arduino core provides F()).
|
|
83
|
+
* The avrUartShim runs without the core, so F() is undefined — plain strings. */
|
|
68
84
|
flash(s) {
|
|
85
|
+
if (this.shim === avrUartShim)
|
|
86
|
+
return `"${s}"`;
|
|
69
87
|
return this.isAvr ? `F("${s}")` : `"${s}"`;
|
|
70
88
|
}
|
|
71
89
|
/** Emit a line of TypeScript output. */
|
|
@@ -84,8 +102,8 @@ export class PreprocessorContext {
|
|
|
84
102
|
}
|
|
85
103
|
emitPreamble() {
|
|
86
104
|
this.preambleEmitted = true;
|
|
87
|
-
this.lines.push(
|
|
88
|
-
this.lines.push(
|
|
105
|
+
this.lines.push(`${this.shim.begin};`);
|
|
106
|
+
this.lines.push(`${this.shim.println(this.flash('[TC:SUITE_START]'))};`);
|
|
89
107
|
}
|
|
90
108
|
build() {
|
|
91
109
|
return this.lines.join('\n') + '\n';
|
|
@@ -97,8 +115,8 @@ export class PreprocessorContext {
|
|
|
97
115
|
function processExpressionStatement(stmt, sf, ctx) {
|
|
98
116
|
const expr = stmt.expression;
|
|
99
117
|
if (isDoneCall(expr)) {
|
|
100
|
-
ctx.emit(
|
|
101
|
-
ctx.emit(`while (true) { delay
|
|
118
|
+
ctx.emit(`${ctx.shim.println(ctx.flash('[TC:SUITE_END]'))};`);
|
|
119
|
+
ctx.emit(`while (true) { ${ctx.shim.delay}; }`);
|
|
102
120
|
return;
|
|
103
121
|
}
|
|
104
122
|
if (isDescribeChain(expr)) {
|
|
@@ -22,10 +22,10 @@ export function emitSegments(segments, ctx) {
|
|
|
22
22
|
for (const seg of segments) {
|
|
23
23
|
switch (seg.kind) {
|
|
24
24
|
case 'describe':
|
|
25
|
-
ctx.emit(
|
|
25
|
+
ctx.emit(`${ctx.shim.println(ctx.flash(`[TC:DESCRIBE:${escapeProtocol(seg.name ?? '')}]`))};`);
|
|
26
26
|
break;
|
|
27
27
|
case 'it':
|
|
28
|
-
ctx.emit(
|
|
28
|
+
ctx.emit(`${ctx.shim.println(ctx.flash(`[TC:IT:${escapeProtocol(seg.name ?? '')}]`))};`);
|
|
29
29
|
break;
|
|
30
30
|
case 'expect': {
|
|
31
31
|
if (!seg.matcher)
|
|
@@ -67,17 +67,17 @@ export function emitSegments(segments, ctx) {
|
|
|
67
67
|
export function emitExpectProtocol(actualVar, matcher, matcherArgs, ctx, isString = false) {
|
|
68
68
|
if (isString && matcher === 'toBe') {
|
|
69
69
|
const rawExpected = (matcherArgs[0] ?? '').replace(/^["']|["']$/g, '');
|
|
70
|
-
ctx.emit(
|
|
71
|
-
ctx.emit(
|
|
72
|
-
ctx.emit(
|
|
73
|
-
ctx.emit(
|
|
74
|
-
ctx.emit(
|
|
70
|
+
ctx.emit(`${ctx.shim.print(ctx.flash(`[TC:EXPECT:${matcher}:`))};`);
|
|
71
|
+
ctx.emit(`${ctx.shim.print(`"${escapeProtocol(rawExpected)}"`)};`);
|
|
72
|
+
ctx.emit(`${ctx.shim.print(ctx.flash(':'))};`);
|
|
73
|
+
ctx.emit(`${ctx.shim.print(actualVar)};`);
|
|
74
|
+
ctx.emit(`${ctx.shim.println(ctx.flash(']'))};`);
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
const expectedPart = matcherArgs.join(',');
|
|
78
|
-
ctx.emit(
|
|
79
|
-
ctx.emit(
|
|
80
|
-
ctx.emit(
|
|
78
|
+
ctx.emit(`${ctx.shim.print(ctx.flash(`[TC:EXPECT:${matcher}:${expectedPart}:`))};`);
|
|
79
|
+
ctx.emit(`${ctx.shim.print(actualVar)};`);
|
|
80
|
+
ctx.emit(`${ctx.shim.println(ctx.flash(']'))};`);
|
|
81
81
|
}
|
|
82
82
|
// ---------------------------------------------------------------------------
|
|
83
83
|
// Expression classification helpers
|
package/dist/host/runner.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { findTestFiles } from './finder.js';
|
|
10
|
-
import { preprocess } from './preprocessor.js';
|
|
10
|
+
import { preprocess, serialShim, avrUartShim } from './preprocessor.js';
|
|
11
11
|
import { transpileTestFile, compileSketch, uploadSketch } from './compiler.js';
|
|
12
12
|
import { readSerialOutput } from './serial.js';
|
|
13
13
|
import { parseProtocolLines } from './parser.js';
|
|
@@ -84,6 +84,7 @@ async function processTestFile(filePath, config) {
|
|
|
84
84
|
try {
|
|
85
85
|
preprocessed = preprocess(source, path.basename(filePath), {
|
|
86
86
|
isAvr: config.target === 'avr' || config.target === 'megaavr',
|
|
87
|
+
shim: config.framework === '@typecad/framework-avr' ? avrUartShim : serialShim,
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
catch (e) {
|
package/dist/host/types.d.ts
CHANGED
|
@@ -116,6 +116,8 @@ export interface ResolvedConfig {
|
|
|
116
116
|
buildTarget: string;
|
|
117
117
|
board: string;
|
|
118
118
|
target: string;
|
|
119
|
+
/** Framework package name, e.g. "@typecad/framework-avr". */
|
|
120
|
+
framework?: string;
|
|
119
121
|
/** Absolute path to project root. */
|
|
120
122
|
projectRoot: string;
|
|
121
123
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typecad/expect",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0-alpha.6",
|
|
4
4
|
"description": "Hardware test framework for TypeCAD — vitest-style assertions over serial",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -39,10 +39,11 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"typescript": "^5.7.3",
|
|
42
|
-
"serialport": "^12.0.0"
|
|
42
|
+
"serialport": "^12.0.0",
|
|
43
|
+
"@typecad/arduino-cli": "1.0.0-alpha.6"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"@typecad/cuttlefish": "
|
|
46
|
+
"@typecad/cuttlefish": "1.0.0-alpha.6",
|
|
46
47
|
"@types/node": "^22.10.7"
|
|
47
48
|
},
|
|
48
49
|
"license": "MIT",
|