Skip to content

feat(emulator): add EmulatorRunner abstraction and test-emu command#28

Merged
zackees merged 5 commits intomainfrom
feat/emulator-runner-abstraction
Apr 11, 2026
Merged

feat(emulator): add EmulatorRunner abstraction and test-emu command#28
zackees merged 5 commits intomainfrom
feat/emulator-runner-abstraction

Conversation

@zackees
Copy link
Copy Markdown
Member

@zackees zackees commented Apr 11, 2026

Summary

  • Adds EmulatorOutcome and EmulatorRunResult types to fbuild-core for shared emulator result classification (Passed, Failed, Crashed, TimedOut, Unsupported)
  • Adds EmulatorRunner async trait in fbuild-daemon with QemuRunner (ESP32-S3) and Avr8jsRunner (ATmega328P) implementations
  • Adds POST /api/test-emu daemon endpoint and fbuild test-emu CLI command that builds firmware then runs it in an emulator
  • Emulator execution is separate from the hardware flashing path; unsupported boards produce explicit errors before building

Test plan

  • All 1041 workspace tests pass (including 6 new tests)
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo fmt --all clean
  • Manual: fbuild test-emu tests/platform/uno -e uno (requires Node.js)
  • Manual: fbuild test-emu tests/platform/esp32s3 -e esp32s3 (requires QEMU)

Closes #23

Summary by CodeRabbit

  • New Features

    • Added a test-emu CLI command and server API to build-and-run firmware in emulators (QEMU, avr8js, simavr) with timeout, halt-on/expect matching, verbose/timestamp controls, backend selection, and detailed run results (outcome, stdout/stderr, exit code). CLI streams output and uses the emulation exit code on failure.
  • Enhancements

    • Broader ESP32 QEMU support and improved artifact detection/validation for emulator runs.
  • Documentation

    • Added emulator testing endpoint and handler docs.
  • Tests

    • Added tests for outcomes, runner selection, artifact handling, and request parsing.

…command (#23)

Introduce a standalone emulator execution layer with a trait-based
abstraction and a dedicated CLI command surface for post-build testing.

- Add `EmulatorOutcome` and `EmulatorRunResult` types to fbuild-core
- Add `EmulatorRunner` async trait with `QemuRunner` and `Avr8jsRunner`
  implementations in fbuild-daemon
- Add `select_runner()` to auto-detect the right backend from platform/MCU
- Add `POST /api/test-emu` daemon endpoint (build + emulator run)
- Add `fbuild test-emu` CLI command with --timeout, --expect,
  --halt-on-success, --halt-on-error, and --emulator flags
- Unsupported boards produce explicit errors before building
- Monitor semantics reused for pass/fail/crash classification

Closes #23
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 11, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds a build-then-emulate flow: new CLI subcommand and daemon endpoint POST /api/test-emu, a TestEmuRequest type and client method, core emulator result and artifact types, an EmulatorRunner abstraction with qemu/avr8js/simavr backends, runner selection and monitor-based outcome classification, and a handler that builds then runs an emulator.

Changes

Cohort / File(s) Summary
CLI Daemon Client
crates/fbuild-cli/src/daemon_client.rs
Added TestEmuRequest and DaemonClient::test_emu() to POST to /api/test-emu.
CLI Command Implementation
crates/fbuild-cli/src/main.rs
Added test-emu subcommand, argument parsing, and run_test_emu() to build request, call daemon, stream outputs, and exit with daemon-provided exit code.
Core Emulator Types
crates/fbuild-core/src/emulator.rs, crates/fbuild-core/src/lib.rs
New EmulatorArtifactBundle, RunnerKind, EmulatorOutcome, and EmulatorRunResult types with serde/display/validation/helpers and unit tests; exported emulator module.
Daemon Models & Route
crates/fbuild-daemon/src/models.rs, crates/fbuild-daemon/src/main.rs
Added TestEmuRequest model (serde defaults) and wired new POST /api/test-emu route.
Daemon Emulator Handler
crates/fbuild-daemon/src/handlers/emulator.rs
New EmulatorRunConfig, EmulatorRunner trait, QemuRunner/Avr8jsRunner/SimavrRunner, select_runner(), monitor_outcome_to_emulator() (crash heuristics), test_emu handler that validates, builds, runs emulator, and returns outcome; added tests.
Daemon Visibility & Docs
crates/fbuild-daemon/src/handlers/operations.rs, crates/fbuild-daemon/src/lib.rs, crates/fbuild-daemon/src/handlers/README.md
Made qemu_extra_build_flags() pub(crate), updated crate docs and handler README to list new endpoint.
Deploy QEMU Args
crates/fbuild-deploy/src/esp32.rs
Generalized QEMU arg builder to build_qemu_args(mcu, ...), added wrapper build_qemu_esp32s3_args, adjusted messages/tests and conditional ADC patching.
Daemon Cargo
crates/fbuild-daemon/Cargo.toml
Promoted tempfile to runtime dependency and added async-trait.

Sequence Diagram

sequenceDiagram
    actor User as CLI User
    participant CLI as fbuild CLI
    participant Daemon as fbuild Daemon
    participant Builder as Build System
    participant Runner as EmulatorRunner
    participant Monitor as Process Monitor

    User->>CLI: fbuild test-emu --project <dir> --expect <pattern>
    CLI->>Daemon: POST /api/test-emu (TestEmuRequest)
    Daemon->>Daemon: validate project & parse platformio.ini
    Daemon->>Daemon: select_runner(platform, board, emulator_override)
    Daemon->>Builder: spawn blocking build firmware
    Builder-->>Daemon: build outputs & firmware artifacts
    Daemon->>Runner: Runner::run(EmulatorRunConfig)
    Runner->>Monitor: spawn process, capture stdout/stderr
    Monitor->>Monitor: detect pass/fail/crash/timeout
    Monitor-->>Runner: MonitorOutcome
    Runner-->>Daemon: EmulatorRunResult (outcome, stdout, stderr, exit_code)
    Daemon-->>CLI: OperationResponse (success, message, exit_code, streams)
    CLI->>User: print results & exit with provided code
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #26 — Adds emulator artifact/result types and tests; this PR implements EmulatorArtifactBundle and outcome/run result types.
  • #24 — Requests SimAVR and avr8js support; this PR adds SimavrRunner and Avr8jsRunner and selection logic.
  • #21 — Seeks EmulatorRunner abstraction and fbuild test-emu; this PR introduces EmulatorRunner, runner selection, and CLI/daemon flow.
  • #25 — Requests ESP32 QEMU support; this PR adds QEMU runner and generalizes QEMU arg building for ESP32 variants.

Poem

🐰 I hopped along the build-time trail,

compiled the bits and chased the tail,
QEMU hummed and avr8js played,
outcomes stamped and neatly laid,
the daemon cheered — the tests prevailed.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly describes the main changes: adding an EmulatorRunner abstraction and test-emu command, which aligns with the core objectives.
Linked Issues check ✅ Passed The PR addresses all key requirements from issue #23: EmulatorRunner abstraction, Passed/Failed/Crashed/TimedOut/Unsupported result types, fbuild test-emu CLI command, monitor semantics reuse, CLI flag support (--expect, --halt-on-success, --halt-on-error, --timeout), emulator exit code/stdout/stderr surfacing, and explicit errors for unsupported boards.
Out of Scope Changes check ✅ Passed All changes align with the stated objectives. Updates to esp32.rs generalize QEMU argument construction to support both ESP32 and ESP32-S3, which directly supports the runner selection and artifact validation requirements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/emulator-runner-abstraction

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/fbuild-core/src/emulator.rs`:
- Around line 37-42: The display text for Self::TimedOut { expect_found } is
misleading when no --expect was provided; change the TimedOut variant to carry
whether an expect pattern was configured (e.g., TimedOut { expect_found: bool,
expect_configured: bool } or TimedOut(Option<bool>)) and update every site that
constructs TimedOut to supply the new flag; then update the Display
implementation (the match arm for Self::TimedOut) to render three distinct
messages: when expect_configured && expect_found => "timed out (expected pattern
was found)", when expect_configured && !expect_found => "timed out (expected
pattern NOT found)", and when !expect_configured => "timed out" (no expect
mention).

In `@crates/fbuild-daemon/src/handlers/emulator.rs`:
- Around line 1461-1469: The handler currently sets exit_code = None and never
threads the child process status through, causing failures to be normalized to
1; update run_qemu_process and run_avr8js_headless to return the child process
exit status (e.g. Option<ExitStatus> or similar), propagate that returned status
into monitor_outcome_to_emulator (call
monitor_outcome_to_emulator(qemu_result.outcome, qemu_result.exit_status)) and
set EmulatorRunResult.exit_code (and the OperationResponse that wraps it) to
that actual status instead of None; apply the same change to the other similar
blocks mentioned (around the other occurrences at the indicated ranges) so
EmulatorOutcome::Crashed can be produced and the CLI sees the real emulator exit
code.
- Around line 1393-1408: The QEMU DIO-only check must run in select_runner()
rather than later in QemuRunner::run() so unsupported flash modes fail fast; add
a pre-build check inside select_runner() that reads board.flash_mode (or falls
back to mcu_config.default_flash_mode()), does a case-insensitive compare
against "dio", and if not equal returns the appropriate Unsupported result (use
EmulatorOutcome::Unsupported with a clear message) instead of selecting
QemuRunner; remove or keep the existing check in QemuRunner::run() as a
redundant guard but ensure select_runner() prevents building for qio/qout
targets.
- Around line 1730-1731: The handler currently holds the project lock by
assigning let _guard = lock.lock().await which keeps the lock for the entire
emulator session; release that guard immediately after the
build/serialization-critical section so the long-running emulator phase does not
block other operations. Locate the ctx.project_lock call and the
lock.lock().await assignment (the _guard variable) in the emulator handler, and
explicitly drop or let the guard go out of scope (e.g., call drop(_guard) or
limit the guard to a narrower block) before starting the emulator/session work
while keeping the lock during the build step only.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 10789f27-f17f-49d1-8dec-66c19a8c6fbb

📥 Commits

Reviewing files that changed from the base of the PR and between 3cc444a and 7e5813c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • crates/fbuild-cli/src/daemon_client.rs
  • crates/fbuild-cli/src/main.rs
  • crates/fbuild-core/src/emulator.rs
  • crates/fbuild-core/src/lib.rs
  • crates/fbuild-daemon/Cargo.toml
  • crates/fbuild-daemon/src/handlers/README.md
  • crates/fbuild-daemon/src/handlers/emulator.rs
  • crates/fbuild-daemon/src/handlers/operations.rs
  • crates/fbuild-daemon/src/lib.rs
  • crates/fbuild-daemon/src/main.rs
  • crates/fbuild-daemon/src/models.rs

Implement native SimAVR emulator backend supporting any AVR board that
advertises simavr in its debug_tools (Uno, Leonardo, Mega, and 100+
others). SimAVR consumes firmware.elf directly and pipes UART output to
stdout for pass/fail/timeout classification via the existing monitor.

- Add SimavrRunner struct implementing EmulatorRunner trait
- Add find_simavr() with platform-specific install hints (apt/brew/MSYS2)
- Update select_runner() to support explicit --emulator simavr and
  auto-detect simavr for non-ATmega328P AVR boards
- ATmega328P still defaults to avr8js (no external binary needed)
- Add 11 tests covering process monitoring, select_runner routing, and
  board compatibility validation
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
crates/fbuild-daemon/src/handlers/emulator.rs (3)

1873-1874: ⚠️ Potential issue | 🟠 Major

Release the project lock before emulator execution.

The guard acquired at Line 1874 lives through the emulator phase, so long test-emu runs block other project operations unnecessarily.

Suggested fix
-    let lock = ctx.project_lock(&project_dir);
-    let _guard = lock.lock().await;
-
-    let build_result = {
+    let build_result = {
+        let lock = ctx.project_lock(&project_dir);
+        let _guard = lock.lock().await;
         let p = platform;
         tokio::task::spawn_blocking(move || {
             let orchestrator = fbuild_build::get_orchestrator(p)?;
             orchestrator.build(&params)
         })
         .await
     };

As per coding guidelines, "fbuild-daemon must provide Axum HTTP/WebSocket server with request processors, device lease manager, and lock manager".

Also applies to: 1911-1918, 1958-1981

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-daemon/src/handlers/emulator.rs` around lines 1873 - 1874, The
project lock acquired via ctx.project_lock(&project_dir) and held in _guard
(result of lock.lock().await) must be released before entering the emulator
phase so long-running test-emu doesn't block other operations; refactor to limit
the guard's scope or explicitly drop(_guard) immediately after the critical
section that requires the lock and before calling the emulator runner, and apply
the same change for the other occurrences around the emulator code (the blocks
that currently hold _guard through the emulator at the regions corresponding to
lines ~1911-1918 and ~1958-1981).

1461-1470: ⚠️ Potential issue | 🟠 Major

Propagate real emulator exit codes end-to-end.

Lines 1461/1538/1659 force exit_code: None, and Line 1984 rewrites failures to 1. This drops real process status and weakens Crashed vs Failed classification fidelity.

Suggested fix
 struct QemuRunResult {
     outcome: MonitorOutcome,
     stdout: String,
     stderr: String,
+    exit_code: Option<i32>,
 }
@@
 struct Avr8jsRunResult {
     outcome: MonitorOutcome,
     stdout: String,
     stderr: String,
+    exit_code: Option<i32>,
 }
@@
-    Ok(QemuRunResult {
+    let exit_code = child_exit.and_then(|s| s.code());
+    Ok(QemuRunResult {
         outcome,
         stdout: stdout_buf,
         stderr: stderr_buf,
+        exit_code,
     })
@@
-    Ok(Avr8jsRunResult {
+    let exit_code = child_exit.and_then(|s| s.code());
+    Ok(Avr8jsRunResult {
         outcome,
         stdout: stdout_buf,
         stderr: stderr_buf,
+        exit_code,
     })
@@
-        let exit_code = None;
-        let outcome = monitor_outcome_to_emulator(qemu_result.outcome, exit_code);
+        let exit_code = qemu_result.exit_code;
+        let outcome = monitor_outcome_to_emulator(qemu_result.outcome, exit_code);
@@
-        let outcome = monitor_outcome_to_emulator(avr8js_result.outcome, None);
+        let outcome = monitor_outcome_to_emulator(avr8js_result.outcome, avr8js_result.exit_code);
@@
-        let outcome = monitor_outcome_to_emulator(result.outcome, None);
+        let outcome = monitor_outcome_to_emulator(result.outcome, result.exit_code);
@@
-    let exit_code = if success { 0 } else { 1 };
+    let exit_code = emu_result.exit_code.unwrap_or(if success { 0 } else { 1 });

Also applies to: 1531-1539, 1652-1660, 1983-1999

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-daemon/src/handlers/emulator.rs` around lines 1461 - 1470, The
code is discarding the real QEMU exit status by setting exit_code = None and
later coercing failures to 1; update the run path to extract and propagate the
actual process exit code from the qemu_result returned by run_qemu_process (or
whatever field holds the child status) into the local exit_code variable, pass
that exit_code into monitor_outcome_to_emulator and into the EmulatorRunResult
struct (used at the sites around EmulatorRunResult, monitor_outcome_to_emulator,
and qemu_result), and remove any logic that overwrites real codes with a
hardcoded 1 so that Crashed vs Failed classification uses the original process
status end-to-end.

1681-1698: ⚠️ Potential issue | 🟠 Major

Fail fast on unsupported QEMU flash modes in select_runner().

select_runner() still accepts ESP32-S3 targets with non-DIO flash modes, and the unsupported check happens later in QemuRunner::run() (Line 1393+), after build cost is already paid.

Suggested fix
 pub fn select_runner(
@@
 ) -> fbuild_core::Result<Box<dyn EmulatorRunner>> {
     let board = fbuild_config::BoardConfig::from_board_id(board_id, board_overrides)?;
+    let ensure_qemu_flash_mode = |b: &fbuild_config::BoardConfig| -> fbuild_core::Result<()> {
+        let mcu_config = fbuild_build::esp32::mcu_config::get_mcu_config(&b.mcu)?;
+        let effective_flash_mode = b
+            .flash_mode
+            .as_deref()
+            .unwrap_or(mcu_config.default_flash_mode());
+        if !effective_flash_mode.eq_ignore_ascii_case("dio") {
+            return Err(fbuild_core::FbuildError::DeployFailed(format!(
+                "QEMU requires DIO flash mode; effective mode is '{}'",
+                effective_flash_mode
+            )));
+        }
+        Ok(())
+    };
@@
             "qemu" => {
@@
                 if !board.mcu.eq_ignore_ascii_case("esp32s3") {
@@
                 }
+                ensure_qemu_flash_mode(&board)?;
                 Ok(Box::new(QemuRunner::new(
                     project_dir.to_path_buf(),
                     env_name.to_string(),
                     board,
                 )))
             }
@@
         fbuild_core::Platform::Espressif32 => {
             if board.mcu.eq_ignore_ascii_case("esp32s3") {
+                ensure_qemu_flash_mode(&board)?;
                 Ok(Box::new(QemuRunner::new(
                     project_dir.to_path_buf(),
                     env_name.to_string(),
                     board,
                 )))

Also applies to: 1757-1764

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-daemon/src/handlers/emulator.rs` around lines 1681 - 1698,
select_runner() accepts ESP32-S3 targets with unsupported flash modes and defers
the error to QemuRunner::run(), causing wasted build cost; inside the "qemu"
match arm (where QemuRunner::new is returned) add a fast-fail check on the
board's flash mode (e.g., board.flash / board.flash_mode) and return
Err(fbuild_core::FbuildError::DeployFailed(...)) if it is not the supported mode
(case-insensitive compare for "dio" per the QemuRunner::run() logic), mirroring
the exact error message used in QemuRunner::run(); apply the same fast-fail
check to the other "qemu" branch referenced in the comment so unsupported flash
modes are rejected before building.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/fbuild-daemon/src/handlers/emulator.rs`:
- Around line 1635-1653: SimAVR runner reuses run_qemu_process which produces
QEMU-branded messages; update the SimavrRunner call sites so the backend
identity is passed through instead of "QEMU". Modify the call to
run_qemu_process (and/or its RunQemuOptions) from SimavrRunner to provide a
backend label "simavr" and forward that label into
monitor_outcome_to_emulator(result.outcome, ...) rather than None; reference the
SimavrRunner invocation, run_qemu_process, RunQemuOptions, and
monitor_outcome_to_emulator so logs and user-visible strings reflect "simavr"
instead of "QEMU".

---

Duplicate comments:
In `@crates/fbuild-daemon/src/handlers/emulator.rs`:
- Around line 1873-1874: The project lock acquired via
ctx.project_lock(&project_dir) and held in _guard (result of lock.lock().await)
must be released before entering the emulator phase so long-running test-emu
doesn't block other operations; refactor to limit the guard's scope or
explicitly drop(_guard) immediately after the critical section that requires the
lock and before calling the emulator runner, and apply the same change for the
other occurrences around the emulator code (the blocks that currently hold
_guard through the emulator at the regions corresponding to lines ~1911-1918 and
~1958-1981).
- Around line 1461-1470: The code is discarding the real QEMU exit status by
setting exit_code = None and later coercing failures to 1; update the run path
to extract and propagate the actual process exit code from the qemu_result
returned by run_qemu_process (or whatever field holds the child status) into the
local exit_code variable, pass that exit_code into monitor_outcome_to_emulator
and into the EmulatorRunResult struct (used at the sites around
EmulatorRunResult, monitor_outcome_to_emulator, and qemu_result), and remove any
logic that overwrites real codes with a hardcoded 1 so that Crashed vs Failed
classification uses the original process status end-to-end.
- Around line 1681-1698: select_runner() accepts ESP32-S3 targets with
unsupported flash modes and defers the error to QemuRunner::run(), causing
wasted build cost; inside the "qemu" match arm (where QemuRunner::new is
returned) add a fast-fail check on the board's flash mode (e.g., board.flash /
board.flash_mode) and return Err(fbuild_core::FbuildError::DeployFailed(...)) if
it is not the supported mode (case-insensitive compare for "dio" per the
QemuRunner::run() logic), mirroring the exact error message used in
QemuRunner::run(); apply the same fast-fail check to the other "qemu" branch
referenced in the comment so unsupported flash modes are rejected before
building.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6741c2a9-5769-41c8-80bc-61c0c12f92d9

📥 Commits

Reviewing files that changed from the base of the PR and between 7e5813c and 69d5b2a.

📒 Files selected for processing (1)
  • crates/fbuild-daemon/src/handlers/emulator.rs

zackees added 2 commits April 10, 2026 21:23
Add structured artifact bundle type for emulator consumption:
- EmulatorArtifactBundle with from_build_dir() and from_paths() constructors
- RunnerKind enum for per-backend validation (Avr8js, Simavr, QemuEsp32)
- validate_for() checks required artifacts exist on disk
- All three runners (QemuRunner, Avr8jsRunner, SimavrRunner) validate bundles
- Golden tests for outcome classification (passed/failed/crashed/timeout)
- Comprehensive bundle tests (serde, scanning, validation, edge cases)
Extend QEMU runner from ESP32-S3-only to also support base ESP32:
- Add generic build_qemu_args() that parameterizes machine type and WDT
- QemuRunner dynamically names itself based on MCU (QEMU ESP32/ESP32S3)
- select_runner() and deploy handler accept both esp32 and esp32s3
- ADC calibration patch only applied for ESP32-S3 (not base ESP32)
- Unsupported variants (ESP32-C3, ESP32-S2, etc.) still fail explicitly
- 10 new tests: runner selection, auto-detect, MCU validation, args builder
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (5)
crates/fbuild-daemon/src/handlers/emulator.rs (4)

1916-1918: ⚠️ Potential issue | 🟠 Major

Project lock held during entire emulator phase.

The _guard from lock.lock().await is held until the handler returns, blocking other operations on the same project during the (potentially long) emulator run. Only the build phase needs serialization.

Consider narrowing the lock scope to just the build:

-    let lock = ctx.project_lock(&project_dir);
-    let _guard = lock.lock().await;
-
-    let build_result = {
+    let build_result = {
+        let lock = ctx.project_lock(&project_dir);
+        let _guard = lock.lock().await;
         let p = platform;
         tokio::task::spawn_blocking(move || { ... })
         .await
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-daemon/src/handlers/emulator.rs` around lines 1916 - 1918, The
project lock from ctx.project_lock(&project_dir) is held for the whole handler
because _guard stays in scope, blocking other operations during the emulator
run; acquire the lock only for the build phase by limiting the guard's
scope—call lock.lock().await immediately before the build step (using a short
block or explicitly dropping the guard) and ensure the guard (the value bound to
_guard) is dropped before starting the emulator so the lock is released while
the emulator runs.

1487-1496: ⚠️ Potential issue | 🟠 Major

Emulator exit code is not preserved end-to-end.

exit_code is hardcoded to None and the handler rewrites failures to exit code 1. This prevents the CLI from surfacing the actual emulator exit status and makes EmulatorOutcome::Crashed classification less useful.

Consider threading the child process exit status through run_qemu_processQemuRunResultmonitor_outcome_to_emulatorEmulatorRunResult.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-daemon/src/handlers/emulator.rs` around lines 1487 - 1496, The
code hardcodes exit_code to None; update run_qemu_process to capture the child
process exit status and add that field to QemuRunResult (e.g., exit_code:
Option<i32>), then change monitor_outcome_to_emulator signature to accept the
exit_code and use it when mapping to EmulatorOutcome, and finally return
EmulatorRunResult with exit_code: qemu_result.exit_code instead of None; update
all call sites and tests accordingly so the emulator exit status is preserved
end-to-end.

1411-1427: ⚠️ Potential issue | 🟠 Major

DIO flash mode check should happen in select_runner() for fail-fast behavior.

The PR objective states "unsupported boards must fail early with explicit errors" but the DIO-only validation only happens in QemuRunner::run(), after the build completes. This means an ESP32 board with qio flash mode will incur the full build cost before failing.

Consider moving this check to select_runner() to fail before building.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-daemon/src/handlers/emulator.rs` around lines 1411 - 1427, The
DIO-only flash-mode check currently in QemuRunner::run() causes late failures
after building; move this validation into select_runner() so unsupported flash
modes fail fast. In select_runner(), read board.flash_mode
(as_deref()/unwrap_or(mcu_config.default_flash_mode())), compute
effective_flash_mode, and if it is not equal to "dio" (use
eq_ignore_ascii_case), return the same
EmulatorRunResult/EmulatorOutcome::Unsupported used in QemuRunner::run() instead
of selecting QemuRunner; then remove the duplicate check from QemuRunner::run().

1673-1691: ⚠️ Potential issue | 🟡 Minor

SimavrRunner reuses run_qemu_process which emits "QEMU exited..." messages.

The SimavrRunner delegates to run_qemu_process(), but that function produces QEMU-branded strings like "QEMU exited normally" or "QEMU exited with code X". This leaks incorrect backend identity in user-visible messages.

Consider parameterizing the process label in RunQemuOptions (e.g., process_label: &str) and passing "simavr" from SimavrRunner.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-daemon/src/handlers/emulator.rs` around lines 1673 - 1691, The
SimavrRunner is leaking QEMU-branded messages because it calls run_qemu_process
with RunQemuOptions that lack a backend label; add a new field (e.g.,
process_label: &str or String) to the RunQemuOptions struct, thread that label
through run_qemu_process and use it in all user-visible messages ("QEMU
exited..." -> "{process_label} exited..."), then update the SimavrRunner call
site (the call in emulator.rs that currently constructs RunQemuOptions) to pass
"simavr" as the process_label; ensure monitor_outcome_to_emulator and any
outcome-to-message helpers use the same label or continue to map outcomes
without embedding the hardcoded "QEMU" string.
crates/fbuild-core/src/emulator.rs (1)

183-188: ⚠️ Potential issue | 🟡 Minor

Timeout display text is misleading when no --expect pattern was configured.

When expect_found: false, the display renders "expected pattern NOT found" even if no --expect flag was ever provided. This can confuse users who didn't configure an expect pattern.

Consider either:

  1. Tracking whether an expect was configured (e.g., TimedOut { expect_configured: bool, expect_found: bool })
  2. Simplifying to just "timed out" when expect_found: false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/fbuild-core/src/emulator.rs` around lines 183 - 188, The Display
output for the TimedOut variant is misleading when no expect pattern was
configured: update the match arm handling Self::TimedOut { expect_found } in the
Display impl in emulator.rs so it only mentions the expect pattern when
expect_found is true (e.g., "timed out (expected pattern was found)"), and
otherwise print a plain "timed out" message; alternatively, if you prefer to
track configuration explicitly, extend the TimedOut variant to TimedOut {
expect_configured: bool, expect_found: bool } and use expect_configured to
decide whether to include the expect-related text.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@crates/fbuild-core/src/emulator.rs`:
- Around line 183-188: The Display output for the TimedOut variant is misleading
when no expect pattern was configured: update the match arm handling
Self::TimedOut { expect_found } in the Display impl in emulator.rs so it only
mentions the expect pattern when expect_found is true (e.g., "timed out
(expected pattern was found)"), and otherwise print a plain "timed out" message;
alternatively, if you prefer to track configuration explicitly, extend the
TimedOut variant to TimedOut { expect_configured: bool, expect_found: bool } and
use expect_configured to decide whether to include the expect-related text.

In `@crates/fbuild-daemon/src/handlers/emulator.rs`:
- Around line 1916-1918: The project lock from ctx.project_lock(&project_dir) is
held for the whole handler because _guard stays in scope, blocking other
operations during the emulator run; acquire the lock only for the build phase by
limiting the guard's scope—call lock.lock().await immediately before the build
step (using a short block or explicitly dropping the guard) and ensure the guard
(the value bound to _guard) is dropped before starting the emulator so the lock
is released while the emulator runs.
- Around line 1487-1496: The code hardcodes exit_code to None; update
run_qemu_process to capture the child process exit status and add that field to
QemuRunResult (e.g., exit_code: Option<i32>), then change
monitor_outcome_to_emulator signature to accept the exit_code and use it when
mapping to EmulatorOutcome, and finally return EmulatorRunResult with exit_code:
qemu_result.exit_code instead of None; update all call sites and tests
accordingly so the emulator exit status is preserved end-to-end.
- Around line 1411-1427: The DIO-only flash-mode check currently in
QemuRunner::run() causes late failures after building; move this validation into
select_runner() so unsupported flash modes fail fast. In select_runner(), read
board.flash_mode (as_deref()/unwrap_or(mcu_config.default_flash_mode())),
compute effective_flash_mode, and if it is not equal to "dio" (use
eq_ignore_ascii_case), return the same
EmulatorRunResult/EmulatorOutcome::Unsupported used in QemuRunner::run() instead
of selecting QemuRunner; then remove the duplicate check from QemuRunner::run().
- Around line 1673-1691: The SimavrRunner is leaking QEMU-branded messages
because it calls run_qemu_process with RunQemuOptions that lack a backend label;
add a new field (e.g., process_label: &str or String) to the RunQemuOptions
struct, thread that label through run_qemu_process and use it in all
user-visible messages ("QEMU exited..." -> "{process_label} exited..."), then
update the SimavrRunner call site (the call in emulator.rs that currently
constructs RunQemuOptions) to pass "simavr" as the process_label; ensure
monitor_outcome_to_emulator and any outcome-to-message helpers use the same
label or continue to map outcomes without embedding the hardcoded "QEMU" string.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c116e0b3-3e04-43c7-b417-0c0ca869473b

📥 Commits

Reviewing files that changed from the base of the PR and between 69d5b2a and 8ef1a75.

📒 Files selected for processing (3)
  • crates/fbuild-core/src/emulator.rs
  • crates/fbuild-daemon/src/handlers/emulator.rs
  • crates/fbuild-deploy/src/esp32.rs

- Fix TimedOut display: show "timed out" (not "expected pattern NOT
  found") when no --expect pattern was configured
- Add process_label to RunQemuOptions so SimavrRunner shows "simavr"
  instead of QEMU-branded messages in user-visible output
- Thread real child process exit code through QemuRunResult,
  Avr8jsRunResult, and into EmulatorRunResult/OperationResponse
  instead of hardcoding None/0/1
- Scope project lock to the build phase only so long-running emulator
  sessions don't block other project operations
- Add check_qemu_flash_mode() in select_runner() for fail-fast on
  non-DIO flash modes before paying the build cost
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add EmulatorRunner abstraction and fbuild test-emu command

1 participant