@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 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://github.com/justind000/typecode/tree/main/packages/cuttlefish), which transpiles your TypeScript test files to C++ for upload to hardware.
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.
@@ -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 {
@@ -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
- constructor(isAvr: boolean);
23
- /** Wrap a string literal in F() on AVR to keep it in flash. */
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(`Serial.begin(${this.baudRate});`);
88
- this.lines.push(`Serial.println(${this.flash('[TC:SUITE_START]')});`);
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(`Serial.println(${ctx.flash('[TC:SUITE_END]')});`);
101
- ctx.emit(`while (true) { delay(1000); }`);
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(`Serial.println(${ctx.flash(`[TC:DESCRIBE:${escapeProtocol(seg.name ?? '')}]`)});`);
25
+ ctx.emit(`${ctx.shim.println(ctx.flash(`[TC:DESCRIBE:${escapeProtocol(seg.name ?? '')}]`))};`);
26
26
  break;
27
27
  case 'it':
28
- ctx.emit(`Serial.println(${ctx.flash(`[TC:IT:${escapeProtocol(seg.name ?? '')}]`)});`);
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(`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(']')});`);
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(`Serial.print(${ctx.flash(`[TC:EXPECT:${matcher}:${expectedPart}:`)});`);
79
- ctx.emit(`Serial.print(${actualVar});`);
80
- ctx.emit(`Serial.println(${ctx.flash(']')});`);
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
@@ -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) {
@@ -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": "0.1.0-alpha.2",
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": "0.1.0-alpha.2",
46
+ "@typecad/cuttlefish": "1.0.0-alpha.6",
46
47
  "@types/node": "^22.10.7"
47
48
  },
48
49
  "license": "MIT",