-
Notifications
You must be signed in to change notification settings - Fork 1
feat(library-selection): #205 Phase 6 acceptance gates + Phase 8 CLI/docs #208
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| //! Acceptance gate for #205 AC#4 / closes #202: stm32f103c8 SPI auto-discovery. | ||
| //! | ||
| //! This integration test verifies that an stm32f103c8 Blink sketch which | ||
| //! `#include`s `<SPI.h>` builds with no manual library allowlist, and that | ||
| //! the bundled `Arduino_Core_STM32` SPI library is automatically discovered | ||
| //! by fbuild's library-selection layer. | ||
| //! | ||
| //! Run with: | ||
| //! `uv run soldr cargo test -p fbuild-build --test stm32_acceptance -- --ignored` | ||
| //! | ||
| //! Marked `#[ignore]` because it downloads the ARM GCC toolchain plus the | ||
| //! STM32duino cores (cached after first run) and performs a full firmware | ||
| //! build — too heavy for default `cargo test`. | ||
| //! | ||
| //! Acceptance criteria (#205 AC#4): | ||
| //! 1. The build succeeds. | ||
| //! 2. `compile_commands.json` references at least one source file under | ||
| //! the SPI library (substring `SPI`). | ||
| //! 3. The ELF contains a symbol whose mangled name contains `SPIClass`. | ||
|
|
||
| use std::path::{Path, PathBuf}; | ||
|
|
||
| use fbuild_build::{BuildOrchestrator, BuildParams}; | ||
| use fbuild_core::BuildProfile; | ||
| use fbuild_test_support::{CompileDb, ElfProbe}; | ||
|
|
||
| #[test] | ||
| #[ignore = "downloads STM32duino + builds firmware; CI-only"] | ||
| fn stm32f103c8_blink_with_spi_auto_discovers_library_205_ac4() { | ||
| // Use a temporary project dir so we can write our own SPI-using sketch | ||
| // independent of whatever ships in the fixture. | ||
| let tmp = tempfile::TempDir::new().unwrap(); | ||
| let project_dir = tmp.path(); | ||
|
|
||
| std::fs::write( | ||
| project_dir.join("platformio.ini"), | ||
| "[env:stm32f103c8]\n\ | ||
| platform = ststm32\n\ | ||
| board = bluepill_f103c8\n\ | ||
| framework = arduino\n", | ||
| ) | ||
| .unwrap(); | ||
|
|
||
| let src = project_dir.join("src"); | ||
| std::fs::create_dir_all(&src).unwrap(); | ||
| std::fs::write( | ||
| src.join("main.cpp"), | ||
| "#include <Arduino.h>\n\ | ||
| #include <SPI.h>\n\ | ||
| void setup() { SPI.begin(); }\n\ | ||
| void loop() {}\n", | ||
| ) | ||
| .unwrap(); | ||
|
|
||
| let build_dir = project_dir.join(".fbuild/build"); | ||
| let params = BuildParams { | ||
| project_dir: project_dir.to_path_buf(), | ||
| env_name: "stm32f103c8".to_string(), | ||
| clean: true, | ||
| profile: BuildProfile::Release, | ||
| build_dir: build_dir.clone(), | ||
| verbose: true, | ||
| jobs: None, | ||
| generate_compiledb: true, | ||
| compiledb_only: false, | ||
| log_sender: None, | ||
| symbol_analysis: false, | ||
| symbol_analysis_path: None, | ||
| no_timestamp: false, | ||
| src_dir: None, | ||
| pio_env: Default::default(), | ||
| extra_build_flags: Vec::new(), | ||
| watch_set_cache: None, | ||
| }; | ||
|
|
||
| let orchestrator = fbuild_build::stm32::orchestrator::Stm32Orchestrator; | ||
| let result = orchestrator | ||
| .build(¶ms) | ||
| .expect("stm32f103c8 build with SPI must succeed"); | ||
| assert!(result.success, "build did not report success"); | ||
|
|
||
| let elf = result | ||
| .elf_path | ||
| .as_ref() | ||
| .expect("stm32 build must produce ELF"); | ||
| let probe = ElfProbe::open(elf).expect("ELF parses"); | ||
| assert!( | ||
| probe | ||
| .has_symbol_containing("SPIClass") | ||
| .expect("symbol query"), | ||
| "AC#4: SPIClass symbol must be present in ELF — closes #202" | ||
| ); | ||
|
|
||
| let compdb = locate_compile_commands(&build_dir, "stm32f103c8") | ||
| .expect("compile_commands.json should land in build dir"); | ||
| let db = CompileDb::from_path(&compdb).expect("parse compile_commands.json"); | ||
| let spi_entries: Vec<_> = db.entries_matching("SPI").collect(); | ||
| assert!( | ||
| !spi_entries.is_empty(), | ||
| "AC#4: compile_commands.json must reference an SPI library entry — \ | ||
| closes #202; found {} entries with no SPI hit", | ||
| db.tu_count() | ||
| ); | ||
| } | ||
|
|
||
| fn locate_compile_commands(build_dir: &Path, env: &str) -> Option<PathBuf> { | ||
| let candidates = [ | ||
| build_dir.join(env).join("compile_commands.json"), | ||
| build_dir.join("compile_commands.json"), | ||
| ]; | ||
| for c in candidates { | ||
| if c.exists() { | ||
| return Some(c); | ||
| } | ||
| } | ||
| for entry in walkdir::WalkDir::new(build_dir).into_iter().flatten() { | ||
| if entry.file_name() == "compile_commands.json" { | ||
| return Some(entry.into_path()); | ||
| } | ||
| } | ||
| None | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| //! Phase 6.a acceptance gate for issue #205 on the teensyLC Blink target. | ||
| //! | ||
| //! Runs the full TeensyOrchestrator build against the in-repo | ||
| //! `tests/platform/teensylc` fixture and asserts: | ||
| //! | ||
| //! * `.bss` size <= 3 KB (#205 AC#1). | ||
| //! * No `fnet_*`, `snooze_*`, `RadioHead`, or `mbedtls` symbols leaked into | ||
| //! the linked ELF (#205 AC#1, #204 regression guard). | ||
| //! * The Arduino/Teensy `setup` and `loop` symbols are present (#205 A-11). | ||
| //! * `compile_commands.json` has <= 250 translation units (was 451 pre-fix | ||
| //! per the #205 issue body). | ||
| //! * `compile_commands.json` references no `FNET`, `Snooze`, `RadioHead`, or | ||
| //! `mbedtls` files (#204 root-cause guard). | ||
| //! | ||
| //! This test downloads Teensyduino + arm-gcc on the first run and is | ||
| //! therefore CI-only — it is gated behind `#[ignore]` and runs via | ||
| //! `uv run soldr cargo test -p fbuild-build --test teensylc_acceptance -- --ignored`. | ||
|
|
||
| use std::path::PathBuf; | ||
|
|
||
| use fbuild_build::{BuildOrchestrator, BuildParams}; | ||
| use fbuild_core::BuildProfile; | ||
| use fbuild_test_support::{CompileDb, ElfProbe}; | ||
|
|
||
| #[test] | ||
| #[ignore = "downloads Teensyduino + builds firmware; CI-only"] | ||
| fn teensylc_blink_meets_205_acceptance_criteria() { | ||
| let project_dir = repo_fixture("teensylc"); | ||
| let build_dir = tempfile::TempDir::new().unwrap(); | ||
|
|
||
| let params = BuildParams { | ||
| project_dir: project_dir.clone(), | ||
| env_name: "teensyLC".to_string(), | ||
| clean: true, | ||
| profile: BuildProfile::Release, | ||
| build_dir: build_dir.path().to_path_buf(), | ||
| verbose: true, | ||
| jobs: None, | ||
| generate_compiledb: true, | ||
| compiledb_only: false, | ||
| log_sender: None, | ||
| symbol_analysis: false, | ||
| symbol_analysis_path: None, | ||
| no_timestamp: false, | ||
| src_dir: None, | ||
| pio_env: Default::default(), | ||
| extra_build_flags: Vec::new(), | ||
| watch_set_cache: None, | ||
| }; | ||
|
|
||
| let result = fbuild_build::teensy::orchestrator::TeensyOrchestrator | ||
| .build(¶ms) | ||
| .expect("teensyLC build must succeed for acceptance gate"); | ||
| assert!(result.success, "build did not report success"); | ||
|
|
||
| // ── ELF probes (AC#1) ─────────────────────────────────────────────── | ||
| let elf = result | ||
| .elf_path | ||
| .as_ref() | ||
| .expect("teensy build must produce ELF"); | ||
| let probe = ElfProbe::open(elf).expect("ELF parses"); | ||
| let bss = probe.section_size(".bss").expect("bss query"); | ||
| assert!(bss <= 3 * 1024, "AC#1: .bss must be <= 3KB; got {bss}"); | ||
|
|
||
| for forbidden in ["fnet_", "snooze_", "RadioHead", "mbedtls"] { | ||
| assert!( | ||
| !probe | ||
| .has_symbol_containing(forbidden) | ||
| .expect("symbol query"), | ||
| "AC#1: forbidden symbol substring '{forbidden}' present in ELF — \ | ||
| #204 regression" | ||
| ); | ||
| } | ||
| for required in ["setup", "loop"] { | ||
| assert!( | ||
| probe.has_symbol(required).expect("symbol query") | ||
| || probe.has_symbol_containing(required).expect("symbol query"), | ||
| "A-11: required symbol '{required}' missing from ELF" | ||
| ); | ||
| } | ||
|
|
||
| // ── compile_commands.json probes (AC#1, A-20..A-22) ───────────────── | ||
| let compdb_path = locate_compile_commands(build_dir.path(), "teensyLC") | ||
| .expect("compile_commands.json should land in build dir"); | ||
| let db = CompileDb::from_path(&compdb_path).expect("parse compile_commands.json"); | ||
| assert!( | ||
| db.tu_count() <= 250, | ||
| "AC#1: TU count must be <= 250; got {} entries", | ||
| db.tu_count() | ||
| ); | ||
| let forbidden_hits = db.forbidden_present(&["FNET", "Snooze", "RadioHead", "mbedtls"]); | ||
| assert!( | ||
| forbidden_hits.is_empty(), | ||
| "AC#1 / #204: compile_commands.json must not include any of \ | ||
| FNET/Snooze/RadioHead/mbedtls; found: {:?}", | ||
| forbidden_hits | ||
| ); | ||
| } | ||
|
|
||
| fn repo_fixture(name: &str) -> PathBuf { | ||
| PathBuf::from(env!("CARGO_MANIFEST_DIR")) | ||
| .parent() | ||
| .unwrap() | ||
| .parent() | ||
| .unwrap() | ||
| .join("tests/platform") | ||
| .join(name) | ||
| } | ||
|
|
||
| fn locate_compile_commands(build_dir: &std::path::Path, env: &str) -> Option<PathBuf> { | ||
| // Per fbuild's layout the file lives at one of: | ||
| // <build_dir>/<env>/compile_commands.json | ||
| // <build_dir>/compile_commands.json | ||
| // Search both, prefer the per-env path. | ||
| let candidates = [ | ||
| build_dir.join(env).join("compile_commands.json"), | ||
| build_dir.join("compile_commands.json"), | ||
| ]; | ||
| for c in candidates { | ||
| if c.exists() { | ||
| return Some(c); | ||
| } | ||
| } | ||
| // Fallback: walk the build_dir for any compile_commands.json. | ||
| for entry in walkdir::WalkDir::new(build_dir).into_iter().flatten() { | ||
| if entry.file_name() == "compile_commands.json" { | ||
| return Some(entry.into_path()); | ||
| } | ||
| } | ||
| None | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
has_symbol_containing("loop")/"setup"is overly permissive.loopandsetupare 4–5-char tokens that appear inside many mangled C++ symbol names (e.g._ZN6Stream8setupXxx..., anything withloopin its identifier). The fallback substring match almost guarantees this assertion passes even on a build where Arduino's actualsetup/loopare missing. Since these areextern "C"frommain.cpp/.ino,has_symbol("setup")alone should be sufficient — the substring fallback can be dropped.♻️ Suggested fix
for required in ["setup", "loop"] { assert!( - probe.has_symbol(required).expect("symbol query") - || probe.has_symbol_containing(required).expect("symbol query"), + probe.has_symbol(required).expect("symbol query"), "A-11: required symbol '{required}' missing from ELF" ); }📝 Committable suggestion
🤖 Prompt for AI Agents