Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/acceptance-205.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ on:
- 'crates/fbuild-build/src/stm32/**'
- 'crates/fbuild-build/tests/teensylc_acceptance.rs'
- 'crates/fbuild-build/tests/teensy30_acceptance.rs'
- 'crates/fbuild-build/tests/teensy41_acceptance.rs'
- 'crates/fbuild-build/tests/stm32_acceptance.rs'
- '.github/workflows/acceptance-205.yml'

Expand All @@ -44,6 +45,9 @@ jobs:
- gate: stm32f103c8 SPI
test_bin: stm32_acceptance
test_fn: stm32f103c8_blink_with_spi_auto_discovers_library_205_ac4
- gate: teensy41 cold resolver
test_bin: teensy41_acceptance
test_fn: teensy41_cold_library_selection_meets_205_ac6
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v3
Expand Down
130 changes: 130 additions & 0 deletions crates/fbuild-build/tests/teensy41_acceptance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Acceptance gate for #205 AC#6: teensy41 cold library-selection budget.
//!
//! AC#6 verbatim from the #205 issue body:
//!
//! > Cold run of library selection on `teensy41` project <= 200 ms on a
//! > typical CI runner.
//!
//! This test materializes a real Teensyduino install via
//! `fbuild_packages::library::TeensyCores::ensure_installed`, enumerates its
//! ~40 framework libraries with `get_framework_libraries`, and times a SINGLE
//! call to the uncached resolver
//! `fbuild_library_select::resolve(seeds, search_paths, libraries)` against a
//! minimal teensy41 Blink fixture. The test asserts the elapsed wall-clock
//! time is <= 200 ms — the 200 ms budget is the AC#6 contract.
//!
//! "Cold" means the resolver itself, NOT the cache layer: AC#6 is about the
//! actual BFS+attribution cost of `resolve()`, the function on top of which
//! `resolve_cached` is built. Hitting the warm cache short-circuits this work
//! and is covered by AC#5 (the bench-fastled-examples warm threshold gate);
//! AC#6 ensures the cold path is itself fast enough that a cache-cold project
//! does not pay an unbounded resolution tax.
//!
//! Uses the stm32_acceptance.rs / teensy30_acceptance.rs inline-tempdir
//! pattern so the committed `tests/platform/teensy41/` fixture stays untouched
//! and no scratch artifacts land in the repo.
//!
//! Run with:
//! `uv run soldr cargo test -p fbuild-build --release --test teensy41_acceptance \
//! -- --ignored teensy41_cold_library_selection_meets_205_ac6 --nocapture`
//!
//! Marked `#[ignore]` because it downloads Teensyduino on the first run
//! (cached after) — too heavy for default `cargo test`.

use std::time::Instant;

use fbuild_library_select::resolve;
use fbuild_packages::library::TeensyCores;
use fbuild_packages::Package;

#[test]
#[ignore = "downloads Teensyduino + arm-gcc; CI-only"]
fn teensy41_cold_library_selection_meets_205_ac6() {
// Inline tempdir project — same root-cause-isolation pattern as
// stm32_acceptance.rs / teensy30_acceptance.rs. AC#6 needs only the
// sketch on disk; we don't run a full build, only the resolver.
let tmp = tempfile::TempDir::new().unwrap();
let project_dir = tmp.path();

std::fs::write(
project_dir.join("platformio.ini"),
"[env:teensy41]\n\
platform = teensy\n\
board = teensy41\n\
framework = arduino\n",
)
.unwrap();

let src_dir = project_dir.join("src");
std::fs::create_dir_all(&src_dir).unwrap();
// Minimal Blink. The resolver only walks #include directives reachable
// from this seed, so keeping the sketch tiny mirrors the AC#6 statement
// "Cold run of library selection on teensy41 project" — there's no
// additional framework-lib reference here, the resolver still has to
// load + scan every Teensyduino library to decide they're NOT needed.
std::fs::write(
src_dir.join("main.ino"),
"#include <Arduino.h>\n\
void setup() { pinMode(LED_BUILTIN, OUTPUT); }\n\
void loop() {\n\
digitalWrite(LED_BUILTIN, HIGH);\n\
delay(500);\n\
digitalWrite(LED_BUILTIN, LOW);\n\
delay(500);\n\
}\n",
)
.unwrap();

// Materialize Teensyduino. Idempotent — cached across runs on the
// CI runner once the package has been downloaded once.
let teensy_cores = TeensyCores::new(project_dir);
let framework_dir =
Package::ensure_installed(&teensy_cores).expect("Teensyduino must install for AC#6 gate");
println!(
"AC#6 teensy41 framework installed at {}",
framework_dir.display()
);

// Real teensy41 framework library set (~40 libraries: SPI, Wire,
// OctoWS2811, FNET, Snooze, RadioHead, mbedtls, ...). Listing them is
// an O(libraries-dir) directory walk; that work is NOT what AC#6
// measures, so it happens outside the timed window below.
let libraries = teensy_cores.get_framework_libraries();
assert!(
!libraries.is_empty(),
"AC#6: TeensyCores::get_framework_libraries must return at least one \
library after install — got 0, which means the Teensyduino install \
layout has changed"
);
println!(
"AC#6 teensy41 framework libraries discovered: {}",
libraries.len()
);

// Seeds + search paths: match what the teensy orchestrator passes to
// resolve() in normal operation. The orchestrator's path
// (framework_libs.rs -> resolve_framework_library_sources_from_libraries)
// walks the project's src/include/lib roots and uses every source file
// as a seed. For AC#6 we just have the single Blink .ino, and the
// project search paths are the src/ dir.
let seeds = vec![src_dir.join("main.ino")];
let search_paths = vec![src_dir.clone(), project_dir.to_path_buf()];

// Time a SINGLE uncached resolve() — this is what AC#6 measures.
let start = Instant::now();
let selection = resolve(&seeds, &search_paths, &libraries);
let elapsed = start.elapsed();
let elapsed_ms = elapsed.as_secs_f64() * 1_000.0;

println!(
"AC#6 cold resolve: {:.2} ms (budget 200.00 ms), selected {} library(ies)",
elapsed_ms,
selection.required_libraries.len()
);

assert!(
elapsed_ms <= 200.0,
"AC#6: cold resolve must finish in <= 200 ms; got {:.2} ms",
elapsed_ms
);
}
Loading