@typecad/expect 1.0.0-alpha.7 → 1.0.0-alpha.8
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/dist/host/cli.js +16 -1
- package/dist/host/compiler.d.ts +9 -16
- package/dist/host/compiler.js +172 -131
- package/dist/host/config.d.ts +6 -1
- package/dist/host/config.js +51 -2
- package/dist/host/preprocessor.d.ts +7 -11
- package/dist/host/preprocessor.js +11 -22
- package/dist/host/reporter.js +5 -0
- package/dist/host/runner.js +22 -11
- package/dist/host/types.d.ts +12 -0
- package/package.json +3 -3
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,24 +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 sketch
|
|
22
|
-
* - framework-esp32 → idf.py build (via framework-esp32's compileEspIdf)
|
|
23
|
-
* - everything else → arduino-cli compile
|
|
21
|
+
* Compile the sketch/project via the configured toolchain (arduino-cli or west).
|
|
24
22
|
*/
|
|
25
|
-
export declare function compileSketch(sketchDir: string, buildTarget: string, framework?: string): CompileResult;
|
|
23
|
+
export declare function compileSketch(sketchDir: string, buildTarget: string, framework?: string, toolchainType?: 'arduino-cli' | 'west', zephyrConfig?: Record<string, unknown>): CompileResult;
|
|
26
24
|
/**
|
|
27
|
-
* Upload the compiled sketch to the board.
|
|
25
|
+
* Upload the compiled sketch/project to the board via the configured toolchain.
|
|
28
26
|
*/
|
|
29
|
-
|
|
30
|
-
* Upload firmware. Detects the toolchain from the framework:
|
|
31
|
-
* - framework-esp32 → idf.py flash
|
|
32
|
-
* - everything else → arduino-cli upload
|
|
33
|
-
*/
|
|
34
|
-
export declare function uploadSketch(sketchDir: string, buildTarget: string, port: string, framework?: 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
|
@@ -10,9 +10,9 @@ import { spawnSync } from 'node:child_process';
|
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
11
|
import { parseConfigAST } from './config.js';
|
|
12
12
|
import { checkArduinoEnv } from '@typecad/arduino-cli';
|
|
13
|
-
// createRequire lets us
|
|
14
|
-
//
|
|
15
|
-
const
|
|
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
16
|
/** Format a check failure into the `error` field used by CompileResult/UploadResult. */
|
|
17
17
|
function formatEnvFailure(failure) {
|
|
18
18
|
const lines = [...failure.messages];
|
|
@@ -21,41 +21,20 @@ function formatEnvFailure(failure) {
|
|
|
21
21
|
return lines.join('\n');
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
* Transpile preprocessed TypeScript source to a C++ Arduino sketch
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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.
|
|
28
28
|
*/
|
|
29
|
-
export function transpileTestFile(preprocessedSource, originalFilePath, projectRoot, buildTarget) {
|
|
30
|
-
// Create a build directory for this test file.
|
|
31
|
-
//
|
|
32
|
-
// only main/main.cc changes between files, so idf.py can do incremental
|
|
33
|
-
// builds (ninja detects only the changed .cc and relinks). This turns the
|
|
34
|
-
// 2nd+ file from a ~130s full rebuild into a ~10-15s incremental build.
|
|
35
|
-
// For arduino-cli targets, use per-file directories (no incremental benefit).
|
|
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.
|
|
36
32
|
const baseName = path.basename(originalFilePath, '.test.ts').replace(/[^a-zA-Z0-9_]/g, '_');
|
|
37
|
-
const
|
|
38
|
-
const buildDir = isEsp32
|
|
39
|
-
? path.join(projectRoot, '.build', 'expect', 'esp32_shared')
|
|
40
|
-
: path.join(projectRoot, '.build', 'expect', baseName);
|
|
33
|
+
const buildDir = path.join(projectRoot, '.build', 'expect', baseName);
|
|
41
34
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Only remove the old .ts file so the transpiler doesn't pick up stale ones.
|
|
46
|
-
const oldTsFiles = fs.readdirSync(buildDir).filter(f => f.endsWith('.ts'));
|
|
47
|
-
for (const f of oldTsFiles) {
|
|
48
|
-
try {
|
|
49
|
-
fs.unlinkSync(path.join(buildDir, f));
|
|
50
|
-
}
|
|
51
|
-
catch { }
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
// Fresh dir (first run, or arduino-cli target).
|
|
56
|
-
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
57
|
-
fs.mkdirSync(buildDir, { recursive: true });
|
|
58
|
-
}
|
|
35
|
+
// Fresh dir for every transpile.
|
|
36
|
+
fs.rmSync(buildDir, { recursive: true, force: true });
|
|
37
|
+
fs.mkdirSync(buildDir, { recursive: true });
|
|
59
38
|
}
|
|
60
39
|
catch {
|
|
61
40
|
return { success: false, sketchDir: buildDir, sketchPath: '', output: '', error: `Failed to create build dir: ${buildDir}` };
|
|
@@ -67,9 +46,18 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
|
|
|
67
46
|
// Invoke the cuttlefish transpiler
|
|
68
47
|
// We call it as a CLI command rather than importing to avoid coupling
|
|
69
48
|
const cuttlefishCmd = resolveCuttlefishCmd(projectRoot);
|
|
70
|
-
|
|
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;
|
|
71
59
|
if (useBuildMode) {
|
|
72
|
-
writeBuildConfig(buildDir, projectRoot, path.basename(tsPath), buildTarget);
|
|
60
|
+
writeBuildConfig(buildDir, projectRoot, path.basename(tsPath), buildTarget, configPath);
|
|
73
61
|
}
|
|
74
62
|
const result = spawnSync(process.execPath, useBuildMode
|
|
75
63
|
? [cuttlefishCmd, 'build', '--skip-type-check', '--force']
|
|
@@ -89,46 +77,36 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
|
|
|
89
77
|
error: `Transpilation failed:\n${transpileOutput}`,
|
|
90
78
|
};
|
|
91
79
|
}
|
|
92
|
-
// Find the generated .ino
|
|
93
|
-
const outDir = findOutputDir(buildDir, baseName, projectRoot);
|
|
94
|
-
const
|
|
95
|
-
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';
|
|
96
85
|
return {
|
|
97
86
|
success: false,
|
|
98
87
|
sketchDir: outDir,
|
|
99
88
|
sketchPath: '',
|
|
100
89
|
output: transpileOutput,
|
|
101
|
-
error: `No
|
|
90
|
+
error: `No ${ext} file found in ${outDir} after transpilation`,
|
|
102
91
|
};
|
|
103
92
|
}
|
|
104
93
|
return {
|
|
105
94
|
success: true,
|
|
106
|
-
sketchDir: path.dirname(
|
|
107
|
-
sketchPath:
|
|
95
|
+
sketchDir: path.dirname(entryPath),
|
|
96
|
+
sketchPath: entryPath,
|
|
108
97
|
output: transpileOutput,
|
|
109
98
|
};
|
|
110
99
|
}
|
|
111
100
|
/**
|
|
112
|
-
* Compile the sketch
|
|
113
|
-
* - framework-esp32 → idf.py build (via framework-esp32's compileEspIdf)
|
|
114
|
-
* - everything else → arduino-cli compile
|
|
101
|
+
* Compile the sketch/project via the configured toolchain (arduino-cli or west).
|
|
115
102
|
*/
|
|
116
|
-
export function compileSketch(sketchDir, buildTarget, framework) {
|
|
117
|
-
if (
|
|
118
|
-
return
|
|
103
|
+
export function compileSketch(sketchDir, buildTarget, framework, toolchainType = 'arduino-cli', zephyrConfig) {
|
|
104
|
+
if (toolchainType === 'west') {
|
|
105
|
+
return compileWestProject(sketchDir, buildTarget, zephyrConfig);
|
|
119
106
|
}
|
|
120
107
|
return compileArduinoSketch(sketchDir, buildTarget);
|
|
121
108
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Uses the main export entry point (dist/index.js) to find the dist dir,
|
|
124
|
-
* avoiding the package's exports field restriction on subpaths. */
|
|
125
|
-
function resolveFrameworkEsp32Dist() {
|
|
126
|
-
const indexPath = require_.resolve('@typecad/framework-esp32');
|
|
127
|
-
// indexPath = .../packages/framework-esp32/dist/index.js
|
|
128
|
-
// toolchain dir = .../packages/framework-esp32/dist/toolchain
|
|
129
|
-
return path.join(path.dirname(indexPath), 'toolchain');
|
|
130
|
-
}
|
|
131
|
-
/** Compile via arduino-cli (framework-arduino / framework-avr). */
|
|
109
|
+
/** Compile via arduino-cli. */
|
|
132
110
|
function compileArduinoSketch(sketchDir, buildTarget) {
|
|
133
111
|
// Hard gate: verify arduino-cli + core before spawning.
|
|
134
112
|
{
|
|
@@ -149,85 +127,109 @@ function compileArduinoSketch(sketchDir, buildTarget) {
|
|
|
149
127
|
};
|
|
150
128
|
}
|
|
151
129
|
/**
|
|
152
|
-
* Upload the compiled sketch to the board.
|
|
130
|
+
* Upload the compiled sketch/project to the board via the configured toolchain.
|
|
153
131
|
*/
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
* - everything else → arduino-cli upload
|
|
158
|
-
*/
|
|
159
|
-
export function uploadSketch(sketchDir, buildTarget, port, framework) {
|
|
160
|
-
if (framework?.includes('framework-esp32')) {
|
|
161
|
-
return uploadEspIdfSketch(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);
|
|
162
135
|
}
|
|
163
136
|
return uploadArduinoSketch(sketchDir, buildTarget, port);
|
|
164
137
|
}
|
|
165
|
-
/**
|
|
166
|
-
function
|
|
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
|
+
}
|
|
148
|
+
const result = spawnSync('arduino-cli', ['upload', '--fqbn', buildTarget, '--port', port, sketchDir], { encoding: 'utf8', timeout: 60000 });
|
|
149
|
+
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
150
|
+
return {
|
|
151
|
+
success: result.status === 0,
|
|
152
|
+
output,
|
|
153
|
+
error: result.status !== 0 ? `Upload failed:\n${output}` : undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
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;
|
|
167
174
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
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,
|
|
174
187
|
});
|
|
175
188
|
return {
|
|
176
189
|
success: result.success,
|
|
177
|
-
sketchDir:
|
|
178
|
-
sketchPath:
|
|
190
|
+
sketchDir: projectRoot,
|
|
191
|
+
sketchPath: sourcePath,
|
|
179
192
|
output: result.output,
|
|
180
|
-
error: result.
|
|
193
|
+
error: result.success ? undefined : `west build failed:\n${result.output}`,
|
|
181
194
|
};
|
|
182
195
|
}
|
|
183
196
|
catch (e) {
|
|
184
|
-
return {
|
|
185
|
-
success: false,
|
|
186
|
-
sketchDir,
|
|
187
|
-
sketchPath: '',
|
|
188
|
-
output: '',
|
|
189
|
-
error: `idf.py compile failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
190
|
-
};
|
|
197
|
+
return { success: false, sketchDir, sketchPath: '', output: '', error: `Failed to compile via west: ${e.message}` };
|
|
191
198
|
}
|
|
192
199
|
}
|
|
193
|
-
/**
|
|
194
|
-
|
|
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;
|
|
195
211
|
try {
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
});
|
|
200
224
|
return {
|
|
201
225
|
success: result.success,
|
|
202
226
|
output: result.output,
|
|
203
|
-
error: result.
|
|
227
|
+
error: result.success ? undefined : `west flash failed:\n${result.output}`,
|
|
204
228
|
};
|
|
205
229
|
}
|
|
206
230
|
catch (e) {
|
|
207
|
-
return {
|
|
208
|
-
success: false,
|
|
209
|
-
output: '',
|
|
210
|
-
error: `idf.py flash failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
/** Upload via arduino-cli (framework-arduino / framework-avr). */
|
|
215
|
-
function uploadArduinoSketch(sketchDir, buildTarget, port) {
|
|
216
|
-
// Hard gate: verify arduino-cli + core before spawning.
|
|
217
|
-
{
|
|
218
|
-
const gate = checkArduinoEnv(buildTarget);
|
|
219
|
-
if (!gate.ok) {
|
|
220
|
-
const message = formatEnvFailure(gate);
|
|
221
|
-
return { success: false, output: message, error: message };
|
|
222
|
-
}
|
|
231
|
+
return { success: false, output: '', error: `Failed to flash via west: ${e.message}` };
|
|
223
232
|
}
|
|
224
|
-
const result = spawnSync('arduino-cli', ['upload', '--fqbn', buildTarget, '--port', port, sketchDir], { encoding: 'utf8', timeout: 60000 });
|
|
225
|
-
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
226
|
-
return {
|
|
227
|
-
success: result.status === 0,
|
|
228
|
-
output,
|
|
229
|
-
error: result.status !== 0 ? `Upload failed:\n${output}` : undefined,
|
|
230
|
-
};
|
|
231
233
|
}
|
|
232
234
|
// ---------------------------------------------------------------------------
|
|
233
235
|
// Internal
|
|
@@ -256,7 +258,7 @@ function resolveCuttlefishCmd(projectRoot) {
|
|
|
256
258
|
// Fallback: assume it's on PATH
|
|
257
259
|
return 'cuttlefish';
|
|
258
260
|
}
|
|
259
|
-
function findOutputDir(buildDir, baseName, projectRoot) {
|
|
261
|
+
function findOutputDir(buildDir, baseName, projectRoot, toolchainType = 'arduino-cli') {
|
|
260
262
|
// The cuttlefish transpiler writes output next to the source by default,
|
|
261
263
|
// or to the configured outDir. Check common locations.
|
|
262
264
|
const candidates = [
|
|
@@ -268,40 +270,59 @@ function findOutputDir(buildDir, baseName, projectRoot) {
|
|
|
268
270
|
path.join(projectRoot, '.build', 'expect', baseName, baseName),
|
|
269
271
|
];
|
|
270
272
|
for (const c of candidates) {
|
|
271
|
-
if (fs.existsSync(c) &&
|
|
273
|
+
if (fs.existsSync(c) && hasEntryFile(c, toolchainType))
|
|
272
274
|
return c;
|
|
273
275
|
}
|
|
274
|
-
// Last resort: walk the buildDir tree recursively to find any
|
|
275
|
-
const found =
|
|
276
|
+
// Last resort: walk the buildDir tree recursively to find any entry file
|
|
277
|
+
const found = findEntryFileRecursive(buildDir, toolchainType);
|
|
276
278
|
if (found)
|
|
277
279
|
return path.dirname(found);
|
|
278
280
|
return buildDir;
|
|
279
281
|
}
|
|
280
|
-
|
|
282
|
+
/** Check for a .ino (Arduino) or .cpp (Zephyr) entry file in a directory. */
|
|
283
|
+
function hasEntryFile(dir, toolchainType = 'arduino-cli') {
|
|
281
284
|
if (!fs.existsSync(dir))
|
|
282
285
|
return false;
|
|
283
|
-
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');
|
|
284
294
|
}
|
|
285
|
-
|
|
295
|
+
/** Find the entry file (.ino for Arduino, .cpp for Zephyr) in a directory. */
|
|
296
|
+
function findEntryFile(dir, toolchainType = 'arduino-cli') {
|
|
286
297
|
if (!fs.existsSync(dir))
|
|
287
298
|
return undefined;
|
|
288
299
|
for (const entry of fs.readdirSync(dir)) {
|
|
289
|
-
|
|
290
|
-
if (entry.endsWith('.ino') || entry.endsWith('.cc')) {
|
|
300
|
+
if (isEntryFileName(entry, toolchainType)) {
|
|
291
301
|
return path.join(dir, entry);
|
|
292
302
|
}
|
|
293
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
|
+
}
|
|
294
315
|
return undefined;
|
|
295
316
|
}
|
|
296
|
-
function
|
|
317
|
+
function findEntryFileRecursive(dir, toolchainType = 'arduino-cli') {
|
|
297
318
|
if (!fs.existsSync(dir))
|
|
298
319
|
return undefined;
|
|
299
320
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
300
|
-
if (entry.name
|
|
321
|
+
if (isEntryFileName(entry.name, toolchainType)) {
|
|
301
322
|
return path.join(dir, entry.name);
|
|
302
323
|
}
|
|
303
324
|
if (entry.isDirectory()) {
|
|
304
|
-
const result =
|
|
325
|
+
const result = findEntryFileRecursive(path.join(dir, entry.name), toolchainType);
|
|
305
326
|
if (result)
|
|
306
327
|
return result;
|
|
307
328
|
}
|
|
@@ -322,8 +343,14 @@ function rewriteRelativeImports(source, originalFilePath, buildDir) {
|
|
|
322
343
|
return `from ${quote}${relativePath}${quote}`;
|
|
323
344
|
});
|
|
324
345
|
}
|
|
325
|
-
function writeBuildConfig(buildDir, projectRoot, entryFileName, buildTarget) {
|
|
326
|
-
|
|
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');
|
|
327
354
|
const buildConfigPath = path.join(buildDir, 'cuttlefish.config.ts');
|
|
328
355
|
if (fs.existsSync(baseConfigPath)) {
|
|
329
356
|
// Parse base config via AST to extract scalar values, then inline them.
|
|
@@ -339,10 +366,14 @@ function writeBuildConfig(buildDir, projectRoot, entryFileName, buildTarget) {
|
|
|
339
366
|
lines.push(` target: '${baseValues.target}',`);
|
|
340
367
|
if (baseValues.board)
|
|
341
368
|
lines.push(` board: '${baseValues.board}',`);
|
|
369
|
+
if (baseValues.mcu)
|
|
370
|
+
lines.push(` mcu: '${baseValues.mcu}',`);
|
|
342
371
|
if (baseValues.framework)
|
|
343
372
|
lines.push(` framework: '${baseValues.framework}',`);
|
|
344
373
|
if (baseValues.frameworkData?.buildTarget)
|
|
345
374
|
lines.push(` frameworkData: { buildTarget: '${baseValues.frameworkData.buildTarget}' },`);
|
|
375
|
+
if (baseValues.toolchain?.type)
|
|
376
|
+
lines.push(` toolchain: { type: '${baseValues.toolchain.type}' },`);
|
|
346
377
|
lines.push(' output: {');
|
|
347
378
|
if (baseValues.output?.framework)
|
|
348
379
|
lines.push(` framework: '${baseValues.output?.framework}',`);
|
|
@@ -355,6 +386,16 @@ function writeBuildConfig(buildDir, projectRoot, entryFileName, buildTarget) {
|
|
|
355
386
|
lines.push(` baudRate: ${baseValues.console?.baudRate},`);
|
|
356
387
|
lines.push(' },');
|
|
357
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
|
+
}
|
|
358
399
|
lines.push('};');
|
|
359
400
|
lines.push('');
|
|
360
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.`);
|
|
@@ -56,7 +60,13 @@ export function loadConfig(projectRoot, overrides = {}) {
|
|
|
56
60
|
board: test.board ?? raw.board ?? '@typecad/board-arduino-uno',
|
|
57
61
|
target: raw.target ?? 'avr',
|
|
58
62
|
framework: raw.framework,
|
|
63
|
+
toolchainType: raw.toolchain?.type === 'west' ? 'west' : 'arduino-cli',
|
|
64
|
+
zephyrConfig: raw.zephyr,
|
|
59
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,
|
|
60
70
|
};
|
|
61
71
|
}
|
|
62
72
|
// ---------------------------------------------------------------------------
|
|
@@ -103,6 +113,10 @@ function extractConfigProperties(obj, out) {
|
|
|
103
113
|
if (ts.isStringLiteral(prop.initializer))
|
|
104
114
|
out.board = prop.initializer.text;
|
|
105
115
|
break;
|
|
116
|
+
case 'mcu':
|
|
117
|
+
if (ts.isStringLiteral(prop.initializer))
|
|
118
|
+
out.mcu = prop.initializer.text;
|
|
119
|
+
break;
|
|
106
120
|
case 'frameworkData':
|
|
107
121
|
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
108
122
|
out.frameworkData = {};
|
|
@@ -118,6 +132,41 @@ function extractConfigProperties(obj, out) {
|
|
|
118
132
|
if (ts.isStringLiteral(prop.initializer))
|
|
119
133
|
out.framework = prop.initializer.text;
|
|
120
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;
|
|
121
170
|
case 'test':
|
|
122
171
|
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
123
172
|
out.test = extractTestConfig(prop.initializer);
|
|
@@ -15,15 +15,12 @@ export interface OutputShim {
|
|
|
15
15
|
}
|
|
16
16
|
/** Default shim: Arduino HardwareSerial. */
|
|
17
17
|
export declare const serialShim: OutputShim;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* (println/log) — critical for the [TC:EXPECT:...] protocol format which
|
|
25
|
-
* spans multiple print calls on a single line. */
|
|
26
|
-
export declare const espIdfShim: 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;
|
|
27
24
|
export interface PreprocessorOptions {
|
|
28
25
|
/** Wrap string literals in Arduino F() macro to save SRAM on AVR. */
|
|
29
26
|
isAvr?: boolean;
|
|
@@ -49,8 +46,7 @@ export declare class PreprocessorContext {
|
|
|
49
46
|
readonly shim: OutputShim;
|
|
50
47
|
constructor(isAvr: boolean, shim?: OutputShim);
|
|
51
48
|
/** Wrap a string literal in F() on AVR to keep it in flash.
|
|
52
|
-
* Only applies when using the serialShim (Arduino core provides F()).
|
|
53
|
-
* The avrUartShim runs without the core, so F() is undefined — plain strings. */
|
|
49
|
+
* Only applies when using the serialShim (Arduino core provides F()). */
|
|
54
50
|
flash(s: string): string;
|
|
55
51
|
/** Emit a line of TypeScript output. */
|
|
56
52
|
emit(line: string): void;
|
|
@@ -31,24 +31,16 @@ export const serialShim = {
|
|
|
31
31
|
println: (e) => `Serial.println(${e})`,
|
|
32
32
|
delay: 'delay(1000)',
|
|
33
33
|
};
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
* the lowering distinguish "partial line" (print/debug) from "end of line"
|
|
45
|
-
* (println/log) — critical for the [TC:EXPECT:...] protocol format which
|
|
46
|
-
* spans multiple print calls on a single line. */
|
|
47
|
-
export const espIdfShim = {
|
|
48
|
-
begin: '',
|
|
49
|
-
print: (e) => `console.debug(${e})`,
|
|
50
|
-
println: (e) => `console.log(${e})`,
|
|
51
|
-
delay: 'Timing.delay(1000)',
|
|
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)',
|
|
52
44
|
};
|
|
53
45
|
/**
|
|
54
46
|
* Preprocess a test file's TypeScript source.
|
|
@@ -91,11 +83,8 @@ export class PreprocessorContext {
|
|
|
91
83
|
this.shim = shim;
|
|
92
84
|
}
|
|
93
85
|
/** Wrap a string literal in F() on AVR to keep it in flash.
|
|
94
|
-
* Only applies when using the serialShim (Arduino core provides F()).
|
|
95
|
-
* The avrUartShim runs without the core, so F() is undefined — plain strings. */
|
|
86
|
+
* Only applies when using the serialShim (Arduino core provides F()). */
|
|
96
87
|
flash(s) {
|
|
97
|
-
if (this.shim === avrUartShim)
|
|
98
|
-
return `"${s}"`;
|
|
99
88
|
return this.isAvr ? `F("${s}")` : `"${s}"`;
|
|
100
89
|
}
|
|
101
90
|
/** Emit a line of TypeScript output. */
|
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, serialShim,
|
|
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,9 +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',
|
|
87
|
-
shim: config.
|
|
88
|
-
: config.framework?.includes('framework-esp32') ? espIdfShim
|
|
89
|
-
: serialShim,
|
|
93
|
+
shim: config.toolchainType === 'west' ? zephyrShim : serialShim,
|
|
90
94
|
});
|
|
91
95
|
}
|
|
92
96
|
catch (e) {
|
|
@@ -94,19 +98,26 @@ async function processTestFile(filePath, config) {
|
|
|
94
98
|
}
|
|
95
99
|
// Step 2: Transpile to C++
|
|
96
100
|
console.log(` ${DIM}transpiling...${RESET}`);
|
|
97
|
-
const transpileResult = transpileTestFile(preprocessed, filePath, config.projectRoot, config.buildTarget);
|
|
101
|
+
const transpileResult = transpileTestFile(preprocessed, filePath, config.projectRoot, config.buildTarget, config.toolchainType, config.configPath);
|
|
98
102
|
if (!transpileResult.success) {
|
|
99
103
|
return errorResult(filePath, transpileResult.error ?? 'Transpilation failed', startTime);
|
|
100
104
|
}
|
|
101
|
-
// Step 3: Compile (arduino-cli or
|
|
105
|
+
// Step 3: Compile via the configured toolchain (arduino-cli or west)
|
|
102
106
|
console.log(` ${DIM}compiling...${RESET}`);
|
|
103
|
-
const compileResult = compileSketch(transpileResult.sketchDir, config.buildTarget, config.framework);
|
|
107
|
+
const compileResult = compileSketch(transpileResult.sketchDir, config.buildTarget, config.framework, config.toolchainType, config.zephyrConfig);
|
|
104
108
|
if (!compileResult.success) {
|
|
105
109
|
return errorResult(filePath, compileResult.error ?? 'Compilation failed', startTime);
|
|
106
110
|
}
|
|
107
|
-
//
|
|
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
|
|
108
119
|
console.log(` ${DIM}uploading to ${config.test.port}...${RESET}`);
|
|
109
|
-
const uploadResult = uploadSketch(transpileResult.sketchDir, config.buildTarget, config.test.port, config.framework);
|
|
120
|
+
const uploadResult = uploadSketch(transpileResult.sketchDir, config.buildTarget, config.test.port, config.framework, config.toolchainType, config.zephyrConfig);
|
|
110
121
|
if (!uploadResult.success) {
|
|
111
122
|
return errorResult(filePath, uploadResult.error ?? 'Upload failed', startTime);
|
|
112
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.
|
|
@@ -118,6 +120,16 @@ export interface ResolvedConfig {
|
|
|
118
120
|
target: string;
|
|
119
121
|
/** Framework package name, e.g. "@typecad/framework-avr". */
|
|
120
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>;
|
|
121
127
|
/** Absolute path to project root. */
|
|
122
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;
|
|
123
135
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typecad/expect",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.8",
|
|
4
4
|
"description": "Hardware test framework for TypeCAD — vitest-style assertions over serial",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"typescript": "^5.7.3",
|
|
42
42
|
"serialport": "^12.0.0",
|
|
43
|
-
"@typecad/arduino-cli": "1.0.0-alpha.
|
|
43
|
+
"@typecad/arduino-cli": "1.0.0-alpha.8"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@typecad/cuttlefish": "1.0.0-alpha.
|
|
46
|
+
"@typecad/cuttlefish": "1.0.0-alpha.8",
|
|
47
47
|
"@types/node": "^22.10.7"
|
|
48
48
|
},
|
|
49
49
|
"license": "MIT",
|