@typecad/expect 0.1.0-alpha.2 → 1.0.0-alpha.11
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/cli.js +16 -1
- package/dist/host/compiler.d.ts +9 -9
- package/dist/host/compiler.js +204 -32
- package/dist/host/config.d.ts +6 -1
- package/dist/host/config.js +52 -2
- package/dist/host/preprocessor.d.ts +29 -3
- package/dist/host/preprocessor.js +27 -8
- package/dist/host/protocol-emitter.js +10 -10
- package/dist/host/reporter.js +5 -0
- package/dist/host/runner.js +22 -8
- package/dist/host/types.d.ts +14 -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/cli.js
CHANGED
|
@@ -27,6 +27,9 @@ function parseArgs(argv) {
|
|
|
27
27
|
for (let i = 0; i < args.length; i++) {
|
|
28
28
|
const arg = args[i];
|
|
29
29
|
switch (arg) {
|
|
30
|
+
case '--config':
|
|
31
|
+
result.config = args[++i];
|
|
32
|
+
break;
|
|
30
33
|
case '--port':
|
|
31
34
|
case '-p':
|
|
32
35
|
result.port = args[++i];
|
|
@@ -75,6 +78,12 @@ function parseArgs(argv) {
|
|
|
75
78
|
case '-v':
|
|
76
79
|
result.verbose = true;
|
|
77
80
|
break;
|
|
81
|
+
case '--dry-run':
|
|
82
|
+
result.dryRun = true;
|
|
83
|
+
break;
|
|
84
|
+
case '--bail':
|
|
85
|
+
result.bail = true;
|
|
86
|
+
break;
|
|
78
87
|
case '--help':
|
|
79
88
|
case '-h':
|
|
80
89
|
result.help = true;
|
|
@@ -169,12 +178,18 @@ async function main() {
|
|
|
169
178
|
// Load config
|
|
170
179
|
let config;
|
|
171
180
|
try {
|
|
172
|
-
|
|
181
|
+
const configPath = args.config
|
|
182
|
+
? (path.isAbsolute(args.config) ? args.config : path.resolve(projectRoot, args.config))
|
|
183
|
+
: undefined;
|
|
184
|
+
config = loadConfig(projectRoot, overrides, configPath);
|
|
173
185
|
}
|
|
174
186
|
catch (e) {
|
|
175
187
|
console.error(`\x1b[31m${e.message}\x1b[0m`);
|
|
176
188
|
process.exit(2);
|
|
177
189
|
}
|
|
190
|
+
// CLI mode flags (not config-file settings).
|
|
191
|
+
config.dryRun = args.dryRun;
|
|
192
|
+
config.bail = args.bail;
|
|
178
193
|
// Run tests
|
|
179
194
|
const exitCode = await run(config);
|
|
180
195
|
process.exit(exitCode);
|
package/dist/host/compiler.d.ts
CHANGED
|
@@ -11,17 +11,17 @@ export interface UploadResult {
|
|
|
11
11
|
error?: string;
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
* Transpile preprocessed TypeScript source to a C++ Arduino sketch
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* Transpile preprocessed TypeScript source to a C++ Arduino sketch (or Zephyr
|
|
15
|
+
* project). Writes the preprocessed source to a temp file, invokes the cuttlefish
|
|
16
|
+
* transpiler, and returns the path to the generated .ino (Arduino) or .cpp
|
|
17
|
+
* (Zephyr) entry file.
|
|
18
18
|
*/
|
|
19
|
-
export declare function transpileTestFile(preprocessedSource: string, originalFilePath: string, projectRoot: string, buildTarget: string): CompileResult;
|
|
19
|
+
export declare function transpileTestFile(preprocessedSource: string, originalFilePath: string, projectRoot: string, buildTarget: string, toolchainType?: 'arduino-cli' | 'west', configPath?: string): CompileResult;
|
|
20
20
|
/**
|
|
21
|
-
* Compile the
|
|
21
|
+
* Compile the sketch/project via the configured toolchain (arduino-cli or west).
|
|
22
22
|
*/
|
|
23
|
-
export declare function compileSketch(sketchDir: string, buildTarget: string): CompileResult;
|
|
23
|
+
export declare function compileSketch(sketchDir: string, buildTarget: string, framework?: string, toolchainType?: 'arduino-cli' | 'west', zephyrConfig?: Record<string, unknown>): CompileResult;
|
|
24
24
|
/**
|
|
25
|
-
* Upload the compiled sketch to the board.
|
|
25
|
+
* Upload the compiled sketch/project to the board via the configured toolchain.
|
|
26
26
|
*/
|
|
27
|
-
export declare function uploadSketch(sketchDir: string, buildTarget: string, port: string): UploadResult;
|
|
27
|
+
export declare function uploadSketch(sketchDir: string, buildTarget: string, port: string, framework?: string, toolchainType?: 'arduino-cli' | 'west', zephyrConfig?: Record<string, unknown>): UploadResult;
|
package/dist/host/compiler.js
CHANGED
|
@@ -7,18 +7,32 @@
|
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
10
11
|
import { parseConfigAST } from './config.js';
|
|
12
|
+
import { checkArduinoEnv } from '@typecad/arduino-cli';
|
|
13
|
+
// createRequire lets us use require() in an ESM module for the optional
|
|
14
|
+
// framework-zephyr dynamic import (avoids a hard dependency for Arduino users).
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
/** Format a check failure into the `error` field used by CompileResult/UploadResult. */
|
|
17
|
+
function formatEnvFailure(failure) {
|
|
18
|
+
const lines = [...failure.messages];
|
|
19
|
+
if (failure.fixCommand)
|
|
20
|
+
lines.push(` Fix: ${failure.fixCommand}`);
|
|
21
|
+
return lines.join('\n');
|
|
22
|
+
}
|
|
11
23
|
/**
|
|
12
|
-
* Transpile preprocessed TypeScript source to a C++ Arduino sketch
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
24
|
+
* Transpile preprocessed TypeScript source to a C++ Arduino sketch (or Zephyr
|
|
25
|
+
* project). Writes the preprocessed source to a temp file, invokes the cuttlefish
|
|
26
|
+
* transpiler, and returns the path to the generated .ino (Arduino) or .cpp
|
|
27
|
+
* (Zephyr) entry file.
|
|
16
28
|
*/
|
|
17
|
-
export function transpileTestFile(preprocessedSource, originalFilePath, projectRoot, buildTarget) {
|
|
18
|
-
// Create a build directory for this test file
|
|
29
|
+
export function transpileTestFile(preprocessedSource, originalFilePath, projectRoot, buildTarget, toolchainType = 'arduino-cli', configPath) {
|
|
30
|
+
// Create a build directory for this test file. Per-file directories are
|
|
31
|
+
// used — arduino-cli compile has no incremental benefit from a shared dir.
|
|
19
32
|
const baseName = path.basename(originalFilePath, '.test.ts').replace(/[^a-zA-Z0-9_]/g, '_');
|
|
20
33
|
const buildDir = path.join(projectRoot, '.build', 'expect', baseName);
|
|
21
34
|
try {
|
|
35
|
+
// Fresh dir for every transpile.
|
|
22
36
|
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
23
37
|
fs.mkdirSync(buildDir, { recursive: true });
|
|
24
38
|
}
|
|
@@ -32,9 +46,18 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
|
|
|
32
46
|
// Invoke the cuttlefish transpiler
|
|
33
47
|
// We call it as a CLI command rather than importing to avoid coupling
|
|
34
48
|
const cuttlefishCmd = resolveCuttlefishCmd(projectRoot);
|
|
35
|
-
|
|
49
|
+
// Use build mode (run `cuttlefish build` from a build dir that holds its own
|
|
50
|
+
// generated cuttlefish.config.ts) when the test source has relative imports
|
|
51
|
+
// OR when an explicit configPath was passed. The latter matters because
|
|
52
|
+
// direct-file mode discovers cuttlefish.config.ts from cwd (projectRoot),
|
|
53
|
+
// which is the DEFAULT config — so a project with several target-specific
|
|
54
|
+
// configs (e.g. tests/hardware/cuttlefish.config.ts vs ble-demo.config.ts)
|
|
55
|
+
// would always transpile against the default. Build mode writes a config
|
|
56
|
+
// derived from the chosen configPath into the build dir, so the right
|
|
57
|
+
// board/MCU/target is used.
|
|
58
|
+
const useBuildMode = hasRelativeImports(rewrittenSource) || !!configPath;
|
|
36
59
|
if (useBuildMode) {
|
|
37
|
-
writeBuildConfig(buildDir, projectRoot, path.basename(tsPath), buildTarget);
|
|
60
|
+
writeBuildConfig(buildDir, projectRoot, path.basename(tsPath), buildTarget, configPath);
|
|
38
61
|
}
|
|
39
62
|
const result = spawnSync(process.execPath, useBuildMode
|
|
40
63
|
? [cuttlefishCmd, 'build', '--skip-type-check', '--force']
|
|
@@ -54,29 +77,45 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
|
|
|
54
77
|
error: `Transpilation failed:\n${transpileOutput}`,
|
|
55
78
|
};
|
|
56
79
|
}
|
|
57
|
-
// Find the generated .ino
|
|
58
|
-
const outDir = findOutputDir(buildDir, baseName, projectRoot);
|
|
59
|
-
const
|
|
60
|
-
if (!
|
|
80
|
+
// Find the generated entry file (.ino for Arduino, .cpp for Zephyr)
|
|
81
|
+
const outDir = findOutputDir(buildDir, baseName, projectRoot, toolchainType);
|
|
82
|
+
const entryPath = findEntryFile(outDir, toolchainType);
|
|
83
|
+
if (!entryPath) {
|
|
84
|
+
const ext = toolchainType === 'west' ? '.cpp' : '.ino';
|
|
61
85
|
return {
|
|
62
86
|
success: false,
|
|
63
87
|
sketchDir: outDir,
|
|
64
88
|
sketchPath: '',
|
|
65
89
|
output: transpileOutput,
|
|
66
|
-
error: `No
|
|
90
|
+
error: `No ${ext} file found in ${outDir} after transpilation`,
|
|
67
91
|
};
|
|
68
92
|
}
|
|
69
93
|
return {
|
|
70
94
|
success: true,
|
|
71
|
-
sketchDir: path.dirname(
|
|
72
|
-
sketchPath:
|
|
95
|
+
sketchDir: path.dirname(entryPath),
|
|
96
|
+
sketchPath: entryPath,
|
|
73
97
|
output: transpileOutput,
|
|
74
98
|
};
|
|
75
99
|
}
|
|
76
100
|
/**
|
|
77
|
-
* Compile the
|
|
101
|
+
* Compile the sketch/project via the configured toolchain (arduino-cli or west).
|
|
78
102
|
*/
|
|
79
|
-
export function compileSketch(sketchDir, buildTarget) {
|
|
103
|
+
export function compileSketch(sketchDir, buildTarget, framework, toolchainType = 'arduino-cli', zephyrConfig) {
|
|
104
|
+
if (toolchainType === 'west') {
|
|
105
|
+
return compileWestProject(sketchDir, buildTarget, zephyrConfig);
|
|
106
|
+
}
|
|
107
|
+
return compileArduinoSketch(sketchDir, buildTarget);
|
|
108
|
+
}
|
|
109
|
+
/** Compile via arduino-cli. */
|
|
110
|
+
function compileArduinoSketch(sketchDir, buildTarget) {
|
|
111
|
+
// Hard gate: verify arduino-cli + core before spawning.
|
|
112
|
+
{
|
|
113
|
+
const gate = checkArduinoEnv(buildTarget);
|
|
114
|
+
if (!gate.ok) {
|
|
115
|
+
const message = formatEnvFailure(gate);
|
|
116
|
+
return { success: false, sketchDir, sketchPath: '', output: message, error: message };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
80
119
|
const result = spawnSync('arduino-cli', ['compile', '--fqbn', buildTarget, sketchDir], { encoding: 'utf8', timeout: 120000 });
|
|
81
120
|
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
82
121
|
return {
|
|
@@ -88,9 +127,24 @@ export function compileSketch(sketchDir, buildTarget) {
|
|
|
88
127
|
};
|
|
89
128
|
}
|
|
90
129
|
/**
|
|
91
|
-
* Upload the compiled sketch to the board.
|
|
130
|
+
* Upload the compiled sketch/project to the board via the configured toolchain.
|
|
92
131
|
*/
|
|
93
|
-
export function uploadSketch(sketchDir, buildTarget, port) {
|
|
132
|
+
export function uploadSketch(sketchDir, buildTarget, port, framework, toolchainType = 'arduino-cli', zephyrConfig) {
|
|
133
|
+
if (toolchainType === 'west') {
|
|
134
|
+
return uploadWestProject(sketchDir, buildTarget, port, zephyrConfig);
|
|
135
|
+
}
|
|
136
|
+
return uploadArduinoSketch(sketchDir, buildTarget, port);
|
|
137
|
+
}
|
|
138
|
+
/** Upload via arduino-cli. */
|
|
139
|
+
function uploadArduinoSketch(sketchDir, buildTarget, port) {
|
|
140
|
+
// Hard gate: verify arduino-cli + core before spawning.
|
|
141
|
+
{
|
|
142
|
+
const gate = checkArduinoEnv(buildTarget);
|
|
143
|
+
if (!gate.ok) {
|
|
144
|
+
const message = formatEnvFailure(gate);
|
|
145
|
+
return { success: false, output: message, error: message };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
94
148
|
const result = spawnSync('arduino-cli', ['upload', '--fqbn', buildTarget, '--port', port, sketchDir], { encoding: 'utf8', timeout: 60000 });
|
|
95
149
|
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
96
150
|
return {
|
|
@@ -100,6 +154,84 @@ export function uploadSketch(sketchDir, buildTarget, port) {
|
|
|
100
154
|
};
|
|
101
155
|
}
|
|
102
156
|
// ---------------------------------------------------------------------------
|
|
157
|
+
// Zephyr (west) toolchain
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
/**
|
|
160
|
+
* Compile a Zephyr project via `west build`. The Zephyr Toolchain (from
|
|
161
|
+
* @typecad/framework-zephyr) handles west discovery, ZEPHYR_BASE, scaffolding,
|
|
162
|
+
* and the board target. We call it via dynamic import to avoid a hard
|
|
163
|
+
* dependency on framework-zephyr (the Arduino path doesn't need it).
|
|
164
|
+
*/
|
|
165
|
+
function compileWestProject(sketchDir, buildTarget, zephyrConfig) {
|
|
166
|
+
// sketchDir for Zephyr is the project root containing src/, app/, build/.
|
|
167
|
+
// The transpiler emits src/src.cpp; the west project root is the parent of src/.
|
|
168
|
+
const srcDir = path.join(sketchDir, 'src');
|
|
169
|
+
const projectRoot = fs.existsSync(srcDir) ? sketchDir : path.dirname(sketchDir);
|
|
170
|
+
const sourcePath = fs.existsSync(path.join(srcDir, 'src.cpp'))
|
|
171
|
+
? path.join(srcDir, 'src.cpp')
|
|
172
|
+
: path.join(sketchDir, 'src.cpp');
|
|
173
|
+
const outputDir = fs.existsSync(srcDir) ? srcDir : sketchDir;
|
|
174
|
+
try {
|
|
175
|
+
// Dynamic import — framework-zephyr is an optional dependency (only present
|
|
176
|
+
// for Zephyr projects). The Toolchain object has compile()/upload().
|
|
177
|
+
const mod = require('@typecad/framework-zephyr');
|
|
178
|
+
const Toolchain = mod.Toolchain;
|
|
179
|
+
if (!Toolchain || typeof Toolchain.compile !== 'function') {
|
|
180
|
+
return { success: false, sketchDir, sketchPath: '', output: '', error: '@typecad/framework-zephyr did not export a usable Toolchain.compile().' };
|
|
181
|
+
}
|
|
182
|
+
const result = Toolchain.compile({
|
|
183
|
+
outputDir,
|
|
184
|
+
sourcePath,
|
|
185
|
+
buildTarget,
|
|
186
|
+
zephyrConfig,
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
success: result.success,
|
|
190
|
+
sketchDir: projectRoot,
|
|
191
|
+
sketchPath: sourcePath,
|
|
192
|
+
output: result.output,
|
|
193
|
+
error: result.success ? undefined : `west build failed:\n${result.output}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
return { success: false, sketchDir, sketchPath: '', output: '', error: `Failed to compile via west: ${e.message}` };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Upload (flash) a Zephyr project via `west flash`. For ESP32 boards, west
|
|
202
|
+
* uses the esptool runner; for nRF boards, nrfjprog. The port is forwarded.
|
|
203
|
+
*/
|
|
204
|
+
function uploadWestProject(sketchDir, buildTarget, port, zephyrConfig) {
|
|
205
|
+
const srcDir = path.join(sketchDir, 'src');
|
|
206
|
+
const projectRoot = fs.existsSync(srcDir) ? sketchDir : path.dirname(sketchDir);
|
|
207
|
+
const sourcePath = fs.existsSync(path.join(srcDir, 'src.cpp'))
|
|
208
|
+
? path.join(srcDir, 'src.cpp')
|
|
209
|
+
: path.join(sketchDir, 'src.cpp');
|
|
210
|
+
const outputDir = fs.existsSync(srcDir) ? srcDir : sketchDir;
|
|
211
|
+
try {
|
|
212
|
+
const mod = require('@typecad/framework-zephyr');
|
|
213
|
+
const Toolchain = mod.Toolchain;
|
|
214
|
+
if (!Toolchain || typeof Toolchain.upload !== 'function') {
|
|
215
|
+
return { success: false, output: '', error: '@typecad/framework-zephyr did not export a usable Toolchain.upload().' };
|
|
216
|
+
}
|
|
217
|
+
const result = Toolchain.upload({
|
|
218
|
+
outputDir,
|
|
219
|
+
sourcePath,
|
|
220
|
+
buildTarget,
|
|
221
|
+
port,
|
|
222
|
+
zephyrConfig,
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
success: result.success,
|
|
226
|
+
output: result.output,
|
|
227
|
+
error: result.success ? undefined : `west flash failed:\n${result.output}`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (e) {
|
|
231
|
+
return { success: false, output: '', error: `Failed to flash via west: ${e.message}` };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
103
235
|
// Internal
|
|
104
236
|
// ---------------------------------------------------------------------------
|
|
105
237
|
function resolveCuttlefishCmd(projectRoot) {
|
|
@@ -126,7 +258,7 @@ function resolveCuttlefishCmd(projectRoot) {
|
|
|
126
258
|
// Fallback: assume it's on PATH
|
|
127
259
|
return 'cuttlefish';
|
|
128
260
|
}
|
|
129
|
-
function findOutputDir(buildDir, baseName, projectRoot) {
|
|
261
|
+
function findOutputDir(buildDir, baseName, projectRoot, toolchainType = 'arduino-cli') {
|
|
130
262
|
// The cuttlefish transpiler writes output next to the source by default,
|
|
131
263
|
// or to the configured outDir. Check common locations.
|
|
132
264
|
const candidates = [
|
|
@@ -138,39 +270,59 @@ function findOutputDir(buildDir, baseName, projectRoot) {
|
|
|
138
270
|
path.join(projectRoot, '.build', 'expect', baseName, baseName),
|
|
139
271
|
];
|
|
140
272
|
for (const c of candidates) {
|
|
141
|
-
if (fs.existsSync(c) &&
|
|
273
|
+
if (fs.existsSync(c) && hasEntryFile(c, toolchainType))
|
|
142
274
|
return c;
|
|
143
275
|
}
|
|
144
|
-
// Last resort: walk the buildDir tree recursively to find any
|
|
145
|
-
const found =
|
|
276
|
+
// Last resort: walk the buildDir tree recursively to find any entry file
|
|
277
|
+
const found = findEntryFileRecursive(buildDir, toolchainType);
|
|
146
278
|
if (found)
|
|
147
279
|
return path.dirname(found);
|
|
148
280
|
return buildDir;
|
|
149
281
|
}
|
|
150
|
-
|
|
282
|
+
/** Check for a .ino (Arduino) or .cpp (Zephyr) entry file in a directory. */
|
|
283
|
+
function hasEntryFile(dir, toolchainType = 'arduino-cli') {
|
|
151
284
|
if (!fs.existsSync(dir))
|
|
152
285
|
return false;
|
|
153
|
-
return fs.readdirSync(dir).some(f => f
|
|
286
|
+
return fs.readdirSync(dir).some(f => isEntryFileName(f, toolchainType));
|
|
287
|
+
}
|
|
288
|
+
/** True if the filename is a valid entry file for the toolchain. */
|
|
289
|
+
function isEntryFileName(name, toolchainType) {
|
|
290
|
+
if (toolchainType === 'west') {
|
|
291
|
+
return name.endsWith('.cpp') || name.endsWith('.cc');
|
|
292
|
+
}
|
|
293
|
+
return name.endsWith('.ino') || name.endsWith('.cc');
|
|
154
294
|
}
|
|
155
|
-
|
|
295
|
+
/** Find the entry file (.ino for Arduino, .cpp for Zephyr) in a directory. */
|
|
296
|
+
function findEntryFile(dir, toolchainType = 'arduino-cli') {
|
|
156
297
|
if (!fs.existsSync(dir))
|
|
157
298
|
return undefined;
|
|
158
299
|
for (const entry of fs.readdirSync(dir)) {
|
|
159
|
-
if (entry
|
|
300
|
+
if (isEntryFileName(entry, toolchainType)) {
|
|
160
301
|
return path.join(dir, entry);
|
|
161
302
|
}
|
|
162
303
|
}
|
|
304
|
+
// For Zephyr, the entry may be in a src/ subdirectory
|
|
305
|
+
if (toolchainType === 'west') {
|
|
306
|
+
const srcDir = path.join(dir, 'src');
|
|
307
|
+
if (fs.existsSync(srcDir)) {
|
|
308
|
+
for (const entry of fs.readdirSync(srcDir)) {
|
|
309
|
+
if (isEntryFileName(entry, toolchainType)) {
|
|
310
|
+
return path.join(srcDir, entry);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
163
315
|
return undefined;
|
|
164
316
|
}
|
|
165
|
-
function
|
|
317
|
+
function findEntryFileRecursive(dir, toolchainType = 'arduino-cli') {
|
|
166
318
|
if (!fs.existsSync(dir))
|
|
167
319
|
return undefined;
|
|
168
320
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
169
|
-
if (entry.name
|
|
321
|
+
if (isEntryFileName(entry.name, toolchainType)) {
|
|
170
322
|
return path.join(dir, entry.name);
|
|
171
323
|
}
|
|
172
324
|
if (entry.isDirectory()) {
|
|
173
|
-
const result =
|
|
325
|
+
const result = findEntryFileRecursive(path.join(dir, entry.name), toolchainType);
|
|
174
326
|
if (result)
|
|
175
327
|
return result;
|
|
176
328
|
}
|
|
@@ -191,8 +343,14 @@ function rewriteRelativeImports(source, originalFilePath, buildDir) {
|
|
|
191
343
|
return `from ${quote}${relativePath}${quote}`;
|
|
192
344
|
});
|
|
193
345
|
}
|
|
194
|
-
function writeBuildConfig(buildDir, projectRoot, entryFileName, buildTarget) {
|
|
195
|
-
|
|
346
|
+
function writeBuildConfig(buildDir, projectRoot, entryFileName, buildTarget, configPath) {
|
|
347
|
+
// Resolve the source config: an explicit --config path wins; otherwise fall
|
|
348
|
+
// back to cuttlefish.config.ts in the project root. The transpile runs from
|
|
349
|
+
// buildDir, so we inline the scalar fields the cuttlefish transpiler needs
|
|
350
|
+
// (target/board/mcu/framework/buildTarget/toolchain/zephyr) into a generated
|
|
351
|
+
// cuttlefish.config.ts there — the config loader can't evaluate spreads or
|
|
352
|
+
// relative imports, and discovery from buildDir would find no config.
|
|
353
|
+
const baseConfigPath = configPath ?? path.join(projectRoot, 'cuttlefish.config.ts');
|
|
196
354
|
const buildConfigPath = path.join(buildDir, 'cuttlefish.config.ts');
|
|
197
355
|
if (fs.existsSync(baseConfigPath)) {
|
|
198
356
|
// Parse base config via AST to extract scalar values, then inline them.
|
|
@@ -208,10 +366,14 @@ function writeBuildConfig(buildDir, projectRoot, entryFileName, buildTarget) {
|
|
|
208
366
|
lines.push(` target: '${baseValues.target}',`);
|
|
209
367
|
if (baseValues.board)
|
|
210
368
|
lines.push(` board: '${baseValues.board}',`);
|
|
369
|
+
if (baseValues.mcu)
|
|
370
|
+
lines.push(` mcu: '${baseValues.mcu}',`);
|
|
211
371
|
if (baseValues.framework)
|
|
212
372
|
lines.push(` framework: '${baseValues.framework}',`);
|
|
213
373
|
if (baseValues.frameworkData?.buildTarget)
|
|
214
374
|
lines.push(` frameworkData: { buildTarget: '${baseValues.frameworkData.buildTarget}' },`);
|
|
375
|
+
if (baseValues.toolchain?.type)
|
|
376
|
+
lines.push(` toolchain: { type: '${baseValues.toolchain.type}' },`);
|
|
215
377
|
lines.push(' output: {');
|
|
216
378
|
if (baseValues.output?.framework)
|
|
217
379
|
lines.push(` framework: '${baseValues.output?.framework}',`);
|
|
@@ -224,6 +386,16 @@ function writeBuildConfig(buildDir, projectRoot, entryFileName, buildTarget) {
|
|
|
224
386
|
lines.push(` baudRate: ${baseValues.console?.baudRate},`);
|
|
225
387
|
lines.push(' },');
|
|
226
388
|
}
|
|
389
|
+
// Pass the zephyr section (kconfig, runner) through verbatim — the Zephyr
|
|
390
|
+
// toolchain reads runner from it (e.g. zephyr.runner: 'uf2'). Serialize the
|
|
391
|
+
// parsed object as a minimal object literal (string values only; sufficient
|
|
392
|
+
// for the scalar fields the toolchain reads).
|
|
393
|
+
if (baseValues.zephyr && Object.keys(baseValues.zephyr).length > 0) {
|
|
394
|
+
const parts = Object.entries(baseValues.zephyr)
|
|
395
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
396
|
+
.join(', ');
|
|
397
|
+
lines.push(` zephyr: { ${parts} },`);
|
|
398
|
+
}
|
|
227
399
|
lines.push('};');
|
|
228
400
|
lines.push('');
|
|
229
401
|
lines.push('export default config;');
|
package/dist/host/config.d.ts
CHANGED
|
@@ -8,14 +8,19 @@ import type { ResolvedConfig, TestConfig } from './types.js';
|
|
|
8
8
|
* @param projectRoot Absolute path to the project root.
|
|
9
9
|
* @param overrides CLI flag overrides for test config.
|
|
10
10
|
*/
|
|
11
|
-
export declare function loadConfig(projectRoot: string, overrides?: Partial<TestConfig
|
|
11
|
+
export declare function loadConfig(projectRoot: string, overrides?: Partial<TestConfig>, explicitConfigPath?: string): ResolvedConfig;
|
|
12
12
|
export interface RawConfig {
|
|
13
13
|
target?: string;
|
|
14
14
|
board?: string;
|
|
15
|
+
mcu?: string;
|
|
15
16
|
frameworkData?: {
|
|
16
17
|
buildTarget?: string;
|
|
17
18
|
};
|
|
18
19
|
framework?: string;
|
|
20
|
+
toolchain?: {
|
|
21
|
+
type?: string;
|
|
22
|
+
};
|
|
23
|
+
zephyr?: Record<string, unknown>;
|
|
19
24
|
test?: Partial<TestConfig>;
|
|
20
25
|
output?: {
|
|
21
26
|
framework?: string;
|
package/dist/host/config.js
CHANGED
|
@@ -29,8 +29,12 @@ const DEFAULT_TEST_CONFIG = {
|
|
|
29
29
|
* @param projectRoot Absolute path to the project root.
|
|
30
30
|
* @param overrides CLI flag overrides for test config.
|
|
31
31
|
*/
|
|
32
|
-
export function loadConfig(projectRoot, overrides = {}) {
|
|
33
|
-
|
|
32
|
+
export function loadConfig(projectRoot, overrides = {}, explicitConfigPath) {
|
|
33
|
+
// An explicit --config path wins; otherwise discover cuttlefish.config.ts in
|
|
34
|
+
// the project root. The explicit path lets a project hold several configs
|
|
35
|
+
// (e.g. one per target board) and select one at run time instead of keeping
|
|
36
|
+
// a single cuttlefish.config.ts as the only entry.
|
|
37
|
+
const configPath = explicitConfigPath ?? findConfigFile(projectRoot);
|
|
34
38
|
if (!configPath) {
|
|
35
39
|
throw new Error(`No cuttlefish.config.ts found in ${projectRoot}. ` +
|
|
36
40
|
`Create one or specify --port and --board on the command line.`);
|
|
@@ -55,7 +59,14 @@ export function loadConfig(projectRoot, overrides = {}) {
|
|
|
55
59
|
buildTarget: test.buildTarget ?? raw.frameworkData?.buildTarget ?? 'arduino:avr:uno',
|
|
56
60
|
board: test.board ?? raw.board ?? '@typecad/board-arduino-uno',
|
|
57
61
|
target: raw.target ?? 'avr',
|
|
62
|
+
framework: raw.framework,
|
|
63
|
+
toolchainType: raw.toolchain?.type === 'west' ? 'west' : 'arduino-cli',
|
|
64
|
+
zephyrConfig: raw.zephyr,
|
|
58
65
|
projectRoot,
|
|
66
|
+
// The config file these values came from. writeBuildConfig re-reads it to
|
|
67
|
+
// extract board/MCU for the transpile, so it must point at the same file
|
|
68
|
+
// loadConfig parsed (not always the default cuttlefish.config.ts).
|
|
69
|
+
configPath,
|
|
59
70
|
};
|
|
60
71
|
}
|
|
61
72
|
// ---------------------------------------------------------------------------
|
|
@@ -102,6 +113,10 @@ function extractConfigProperties(obj, out) {
|
|
|
102
113
|
if (ts.isStringLiteral(prop.initializer))
|
|
103
114
|
out.board = prop.initializer.text;
|
|
104
115
|
break;
|
|
116
|
+
case 'mcu':
|
|
117
|
+
if (ts.isStringLiteral(prop.initializer))
|
|
118
|
+
out.mcu = prop.initializer.text;
|
|
119
|
+
break;
|
|
105
120
|
case 'frameworkData':
|
|
106
121
|
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
107
122
|
out.frameworkData = {};
|
|
@@ -117,6 +132,41 @@ function extractConfigProperties(obj, out) {
|
|
|
117
132
|
if (ts.isStringLiteral(prop.initializer))
|
|
118
133
|
out.framework = prop.initializer.text;
|
|
119
134
|
break;
|
|
135
|
+
case 'toolchain':
|
|
136
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
137
|
+
out.toolchain = {};
|
|
138
|
+
for (const tProp of prop.initializer.properties) {
|
|
139
|
+
if (ts.isPropertyAssignment(tProp) && tProp.name.getText() === 'type' && ts.isStringLiteral(tProp.initializer)) {
|
|
140
|
+
out.toolchain.type = tProp.initializer.text;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
case 'zephyr':
|
|
146
|
+
// Parse the zephyr section (kconfig, runner, cmakeArgs) as a generic
|
|
147
|
+
// object so it can be passed through to the Zephyr toolchain.
|
|
148
|
+
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
149
|
+
out.zephyr = {};
|
|
150
|
+
for (const zProp of prop.initializer.properties) {
|
|
151
|
+
if (ts.isPropertyAssignment(zProp)) {
|
|
152
|
+
const key = zProp.name.getText();
|
|
153
|
+
if (ts.isObjectLiteralExpression(zProp.initializer)) {
|
|
154
|
+
// kconfig: { 'CONFIG_X': 'y' }
|
|
155
|
+
const sub = {};
|
|
156
|
+
for (const subProp of zProp.initializer.properties) {
|
|
157
|
+
if (ts.isPropertyAssignment(subProp) && ts.isStringLiteral(subProp.initializer)) {
|
|
158
|
+
sub[subProp.name.getText().replace(/['"]/g, '')] = subProp.initializer.text;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
out.zephyr[key] = sub;
|
|
162
|
+
}
|
|
163
|
+
else if (ts.isStringLiteral(zProp.initializer)) {
|
|
164
|
+
out.zephyr[key] = zProp.initializer.text;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
120
170
|
case 'test':
|
|
121
171
|
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
122
172
|
out.test = extractTestConfig(prop.initializer);
|
|
@@ -1,6 +1,31 @@
|
|
|
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
|
+
/**
|
|
19
|
+
* Zephyr shim: uses overloaded __tc_print/__tc_println helpers that handle
|
|
20
|
+
* both string and numeric (double) output via printf. The Zephyr strategy's
|
|
21
|
+
* shimLines emits these helper definitions. k_msleep replaces delay().
|
|
22
|
+
*/
|
|
23
|
+
export declare const zephyrShim: OutputShim;
|
|
1
24
|
export interface PreprocessorOptions {
|
|
2
25
|
/** Wrap string literals in Arduino F() macro to save SRAM on AVR. */
|
|
3
26
|
isAvr?: boolean;
|
|
27
|
+
/** Output shim — defaults to serialShim (Arduino HardwareSerial). */
|
|
28
|
+
shim?: OutputShim;
|
|
4
29
|
}
|
|
5
30
|
/**
|
|
6
31
|
* Preprocess a test file's TypeScript source.
|
|
@@ -17,10 +42,11 @@ export declare class PreprocessorContext {
|
|
|
17
42
|
private varCounter;
|
|
18
43
|
private fnCounter;
|
|
19
44
|
private preambleEmitted;
|
|
20
|
-
private readonly baudRate;
|
|
21
45
|
readonly isAvr: boolean;
|
|
22
|
-
|
|
23
|
-
|
|
46
|
+
readonly shim: OutputShim;
|
|
47
|
+
constructor(isAvr: boolean, shim?: OutputShim);
|
|
48
|
+
/** Wrap a string literal in F() on AVR to keep it in flash.
|
|
49
|
+
* Only applies when using the serialShim (Arduino core provides F()). */
|
|
24
50
|
flash(s: string): string;
|
|
25
51
|
/** Emit a line of TypeScript output. */
|
|
26
52
|
emit(line: string): void;
|
|
@@ -24,6 +24,24 @@
|
|
|
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
|
+
/**
|
|
35
|
+
* Zephyr shim: uses overloaded __tc_print/__tc_println helpers that handle
|
|
36
|
+
* both string and numeric (double) output via printf. The Zephyr strategy's
|
|
37
|
+
* shimLines emits these helper definitions. k_msleep replaces delay().
|
|
38
|
+
*/
|
|
39
|
+
export const zephyrShim = {
|
|
40
|
+
begin: '', // Zephyr console auto-initializes via DT; no explicit begin needed
|
|
41
|
+
print: (e) => `__tc_print(${e})`,
|
|
42
|
+
println: (e) => `__tc_println(${e})`,
|
|
43
|
+
delay: 'k_msleep(1000)',
|
|
44
|
+
};
|
|
27
45
|
/**
|
|
28
46
|
* Preprocess a test file's TypeScript source.
|
|
29
47
|
*
|
|
@@ -35,7 +53,7 @@ import { emitSegments } from './protocol-emitter.js';
|
|
|
35
53
|
*/
|
|
36
54
|
export function preprocess(source, fileName = 'test.ts', options) {
|
|
37
55
|
const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
38
|
-
const ctx = new PreprocessorContext(options?.isAvr ?? false);
|
|
56
|
+
const ctx = new PreprocessorContext(options?.isAvr ?? false, options?.shim);
|
|
39
57
|
for (const stmt of sf.statements) {
|
|
40
58
|
if (ts.isImportDeclaration(stmt)) {
|
|
41
59
|
const moduleSpecifier = stmt.moduleSpecifier.text;
|
|
@@ -56,15 +74,16 @@ export function preprocess(source, fileName = 'test.ts', options) {
|
|
|
56
74
|
// PreprocessorContext — shared state threaded through sub-modules
|
|
57
75
|
// ---------------------------------------------------------------------------
|
|
58
76
|
export class PreprocessorContext {
|
|
59
|
-
constructor(isAvr) {
|
|
77
|
+
constructor(isAvr, shim = serialShim) {
|
|
60
78
|
this.lines = [];
|
|
61
79
|
this.varCounter = 0;
|
|
62
80
|
this.fnCounter = 0;
|
|
63
81
|
this.preambleEmitted = false;
|
|
64
|
-
this.baudRate = 115200;
|
|
65
82
|
this.isAvr = isAvr;
|
|
83
|
+
this.shim = shim;
|
|
66
84
|
}
|
|
67
|
-
/** Wrap a string literal in F() on AVR to keep it in flash.
|
|
85
|
+
/** Wrap a string literal in F() on AVR to keep it in flash.
|
|
86
|
+
* Only applies when using the serialShim (Arduino core provides F()). */
|
|
68
87
|
flash(s) {
|
|
69
88
|
return this.isAvr ? `F("${s}")` : `"${s}"`;
|
|
70
89
|
}
|
|
@@ -84,8 +103,8 @@ export class PreprocessorContext {
|
|
|
84
103
|
}
|
|
85
104
|
emitPreamble() {
|
|
86
105
|
this.preambleEmitted = true;
|
|
87
|
-
this.lines.push(
|
|
88
|
-
this.lines.push(
|
|
106
|
+
this.lines.push(`${this.shim.begin};`);
|
|
107
|
+
this.lines.push(`${this.shim.println(this.flash('[TC:SUITE_START]'))};`);
|
|
89
108
|
}
|
|
90
109
|
build() {
|
|
91
110
|
return this.lines.join('\n') + '\n';
|
|
@@ -97,8 +116,8 @@ export class PreprocessorContext {
|
|
|
97
116
|
function processExpressionStatement(stmt, sf, ctx) {
|
|
98
117
|
const expr = stmt.expression;
|
|
99
118
|
if (isDoneCall(expr)) {
|
|
100
|
-
ctx.emit(
|
|
101
|
-
ctx.emit(`while (true) { delay
|
|
119
|
+
ctx.emit(`${ctx.shim.println(ctx.flash('[TC:SUITE_END]'))};`);
|
|
120
|
+
ctx.emit(`while (true) { ${ctx.shim.delay}; }`);
|
|
102
121
|
return;
|
|
103
122
|
}
|
|
104
123
|
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/reporter.js
CHANGED
|
@@ -82,6 +82,11 @@ function reportFile(file, verbose) {
|
|
|
82
82
|
console.log();
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
|
+
if (file.compiled) {
|
|
86
|
+
console.log(` ${PASS_ICON} ${BOLD}${file.filePath}${RESET} ${GREEN}(compiled, dry-run)${RESET}`);
|
|
87
|
+
console.log();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
85
90
|
for (const desc of file.describes) {
|
|
86
91
|
reportDescribe(desc, verbose);
|
|
87
92
|
}
|
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, zephyrShim } 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';
|
|
@@ -43,6 +43,12 @@ export async function run(config) {
|
|
|
43
43
|
const result = await processTestFile(filePath, config);
|
|
44
44
|
fileResults.push(result);
|
|
45
45
|
reportFileResult(result, { verbose: config.test.verbose });
|
|
46
|
+
// --bail: stop after the first file that fails to compile/upload or has a
|
|
47
|
+
// failing test (skipped/compiled files don't count as failures).
|
|
48
|
+
if (config.bail && (result.error || (result.describes.length > 0 && !result.passed))) {
|
|
49
|
+
console.log(`${YELLOW}--bail: stopping after first failure${RESET}`);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
46
52
|
}
|
|
47
53
|
// 3. Aggregate results
|
|
48
54
|
const runResult = aggregateResults(fileResults, Date.now() - startTime);
|
|
@@ -73,8 +79,8 @@ async function processTestFile(filePath, config) {
|
|
|
73
79
|
}
|
|
74
80
|
// Validate port only for files that will actually compile/upload. This lets
|
|
75
81
|
// target-incompatible files be skipped without requiring hardware to be
|
|
76
|
-
// connected.
|
|
77
|
-
if (!config.test.port) {
|
|
82
|
+
// connected, and lets --dry-run run without any port (it stops after compile).
|
|
83
|
+
if (!config.test.port && !config.dryRun) {
|
|
78
84
|
return errorResult(filePath, 'No serial port specified. Use --port <port> or set test.port in cuttlefish.config.ts', startTime);
|
|
79
85
|
}
|
|
80
86
|
console.log(`${CYAN}●${RESET} ${relativePath}`);
|
|
@@ -84,6 +90,7 @@ async function processTestFile(filePath, config) {
|
|
|
84
90
|
try {
|
|
85
91
|
preprocessed = preprocess(source, path.basename(filePath), {
|
|
86
92
|
isAvr: config.target === 'avr' || config.target === 'megaavr',
|
|
93
|
+
shim: config.toolchainType === 'west' ? zephyrShim : serialShim,
|
|
87
94
|
});
|
|
88
95
|
}
|
|
89
96
|
catch (e) {
|
|
@@ -91,19 +98,26 @@ async function processTestFile(filePath, config) {
|
|
|
91
98
|
}
|
|
92
99
|
// Step 2: Transpile to C++
|
|
93
100
|
console.log(` ${DIM}transpiling...${RESET}`);
|
|
94
|
-
const transpileResult = transpileTestFile(preprocessed, filePath, config.projectRoot, config.buildTarget);
|
|
101
|
+
const transpileResult = transpileTestFile(preprocessed, filePath, config.projectRoot, config.buildTarget, config.toolchainType, config.configPath);
|
|
95
102
|
if (!transpileResult.success) {
|
|
96
103
|
return errorResult(filePath, transpileResult.error ?? 'Transpilation failed', startTime);
|
|
97
104
|
}
|
|
98
|
-
// Step 3: Compile
|
|
105
|
+
// Step 3: Compile via the configured toolchain (arduino-cli or west)
|
|
99
106
|
console.log(` ${DIM}compiling...${RESET}`);
|
|
100
|
-
const compileResult = compileSketch(transpileResult.sketchDir, config.buildTarget);
|
|
107
|
+
const compileResult = compileSketch(transpileResult.sketchDir, config.buildTarget, config.framework, config.toolchainType, config.zephyrConfig);
|
|
101
108
|
if (!compileResult.success) {
|
|
102
109
|
return errorResult(filePath, compileResult.error ?? 'Compilation failed', startTime);
|
|
103
110
|
}
|
|
104
|
-
//
|
|
111
|
+
// --dry-run: stop after a successful compile. Skips upload + serial read so
|
|
112
|
+
// the pipeline can be verified without hardware attached.
|
|
113
|
+
if (config.dryRun) {
|
|
114
|
+
const durationMs = Date.now() - startTime;
|
|
115
|
+
console.log(` ${DIM}compiled (dry-run, skipping upload and tests)${RESET}`);
|
|
116
|
+
return { filePath: relativePath, describes: [], passed: true, durationMs, debugOutput: [], compiled: true };
|
|
117
|
+
}
|
|
118
|
+
// Step 4: Upload via the configured toolchain
|
|
105
119
|
console.log(` ${DIM}uploading to ${config.test.port}...${RESET}`);
|
|
106
|
-
const uploadResult = uploadSketch(transpileResult.sketchDir, config.buildTarget, config.test.port);
|
|
120
|
+
const uploadResult = uploadSketch(transpileResult.sketchDir, config.buildTarget, config.test.port, config.framework, config.toolchainType, config.zephyrConfig);
|
|
107
121
|
if (!uploadResult.success) {
|
|
108
122
|
return errorResult(filePath, uploadResult.error ?? 'Upload failed', startTime);
|
|
109
123
|
}
|
package/dist/host/types.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface FileResult {
|
|
|
67
67
|
skipReason?: string;
|
|
68
68
|
/** If the file didn't compile or upload. */
|
|
69
69
|
error?: string;
|
|
70
|
+
/** True under --dry-run: the file compiled but was not uploaded/run. */
|
|
71
|
+
compiled?: boolean;
|
|
70
72
|
}
|
|
71
73
|
/**
|
|
72
74
|
* Result of the entire test run.
|
|
@@ -116,6 +118,18 @@ export interface ResolvedConfig {
|
|
|
116
118
|
buildTarget: string;
|
|
117
119
|
board: string;
|
|
118
120
|
target: string;
|
|
121
|
+
/** Framework package name, e.g. "@typecad/framework-avr". */
|
|
122
|
+
framework?: string;
|
|
123
|
+
/** Toolchain type from config: 'arduino-cli' (default) or 'west' (Zephyr). */
|
|
124
|
+
toolchainType: 'arduino-cli' | 'west';
|
|
125
|
+
/** Zephyr-specific config (kconfig, etc.) from cuttlefish.config.ts. */
|
|
126
|
+
zephyrConfig?: Record<string, unknown>;
|
|
119
127
|
/** Absolute path to project root. */
|
|
120
128
|
projectRoot: string;
|
|
129
|
+
/** Absolute path to the cuttlefish config file these values were read from. */
|
|
130
|
+
configPath: string;
|
|
131
|
+
/** --dry-run: compile every file but skip upload and test execution. */
|
|
132
|
+
dryRun?: boolean;
|
|
133
|
+
/** --bail: stop after the first failing test file. */
|
|
134
|
+
bail?: boolean;
|
|
121
135
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typecad/expect",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0-alpha.11",
|
|
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.11"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"@typecad/cuttlefish": "
|
|
46
|
+
"@typecad/cuttlefish": "1.0.0-alpha.11",
|
|
46
47
|
"@types/node": "^22.10.7"
|
|
47
48
|
},
|
|
48
49
|
"license": "MIT",
|