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 @@ -17,6 +17,7 @@ on:
- 'crates/fbuild-build/src/teensy/**'
- 'crates/fbuild-build/src/stm32/**'
- 'crates/fbuild-build/tests/teensylc_acceptance.rs'
- 'crates/fbuild-build/tests/teensy30_acceptance.rs'
- 'crates/fbuild-build/tests/stm32_acceptance.rs'
- '.github/workflows/acceptance-205.yml'

Expand All @@ -37,6 +38,9 @@ jobs:
- gate: teensyLC
test_bin: teensylc_acceptance
test_fn: teensylc_blink_meets_205_acceptance_criteria
- gate: teensy30 AnalogOutput
test_bin: teensy30_acceptance
test_fn: teensy30_analog_output_meets_205_ac2
- gate: stm32f103c8 SPI
test_bin: stm32_acceptance
test_fn: stm32f103c8_blink_with_spi_auto_discovers_library_205_ac4
Expand Down
152 changes: 152 additions & 0 deletions crates/fbuild-build/tests/teensy30_acceptance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! Acceptance gate for #205 AC#2: teensy30 AnalogOutput sketch.
//!
//! Runs the full TeensyOrchestrator build against an inline-tempdir
//! `AnalogOutput.ino` sketch for the Teensy 3.0 (`teensy30` env) and asserts:
//!
//! * `.dmabuffers` section size <= 1 KB (#205 AC#2). The Teensy 3.0 has
//! only 16 KB of SRAM; FNET, Snooze, and friends each pull DMAMEM-tagged
//! statics (DMA descriptor pools, Ethernet frame buffers, RNG state)
//! into the `.dmabuffers` section. If those libraries are linked into a
//! simple Arduino `analogWrite` sketch, `.dmabuffers` balloons and the
//! build blows the RAM budget. This is the AC#2 gate.
//! * No `fnet_*`, `snooze_*`, `RadioHead`, or `mbedtls` symbols leaked
//! into the linked ELF (#204 / #205 AC#1 regression guard — the same
//! forbidden list as `teensylc_acceptance.rs`, complementing teensyLC's
//! `.bss <= 3 KB` gate with the teensy30 `.dmabuffers` gate).
//! * `compile_commands.json` parses and references no `FNET`, `Snooze`,
//! `RadioHead`, or `mbedtls` files (#204 root-cause guard).
//!
//! Uses the stm32-style inline tempdir `project_dir` so the committed
//! `tests/platform/teensy30/` fixture is untouched and no
//! `compile_commands.json` or `.fbuild/` is ever left behind in the repo.
//!
//! Run with:
//! `uv run soldr cargo test -p fbuild-build --test teensy30_acceptance -- --ignored`
//!
//! Marked `#[ignore]` because it downloads Teensyduino + arm-gcc on the
//! first run (cached after) and performs a full firmware build — too
//! heavy for default `cargo test`.
//!
//! LTO-symbol caveat: as with `teensylc_acceptance.rs` and
//! `stm32_acceptance.rs` (see #223), the Release profile's
//! `-flto -Os` inlines tiny functions like the sketch's `setup` and
//! `loop` into their callers and `--gc-sections` strips the
//! independent symbols. The meaningful signals are therefore the
//! ELF section size and forbidden-symbol substring checks, not
//! probes for `setup`/`loop`/`analogWrite` symbols.

use fbuild_build::{BuildOrchestrator, BuildParams};
use fbuild_core::BuildProfile;
use fbuild_test_support::{CompileDb, ElfProbe};

#[test]
#[ignore = "downloads Teensyduino + arm-gcc; CI-only"]
fn teensy30_analog_output_meets_205_ac2() {
// Use a temporary project dir so the committed teensy30 fixture
// at tests/platform/teensy30/ stays untouched and no scratch
// build artifacts land in the repo.
let tmp = tempfile::TempDir::new().unwrap();
let project_dir = tmp.path();

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

let src = project_dir.join("src");
std::fs::create_dir_all(&src).unwrap();
// WHY .ino: the AC#2 sketch is "AnalogOutput" and Teensyduino's
// builder treats .ino as Arduino main; this matches the user-facing
// `fbuild build teensy30 AnalogOutput` invocation in the #205 body.
std::fs::write(
src.join("main.ino"),
"#include <Arduino.h>\n\
void setup() { pinMode(LED_BUILTIN, OUTPUT); }\n\
void loop() {\n\
for (int v = 0; v < 256; v += 5) {\n\
analogWrite(LED_BUILTIN, v);\n\
delay(20);\n\
}\n\
}\n",
)
.unwrap();

let build_dir = project_dir.join(".fbuild/build");
let params = BuildParams {
project_dir: project_dir.to_path_buf(),
// WHY env_name = "teensy30": must match the [env:teensy30] key
// in the platformio.ini we just wrote. Same root-cause family
// as #220 / #221.
env_name: "teensy30".to_string(),
clean: true,
profile: BuildProfile::Release,
build_dir,
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(&params)
.expect("teensy30 AnalogOutput build must succeed for AC#2 gate");
assert!(result.success, "build did not report success");

// ── ELF probes (AC#2 + #204 regression guard) ───────────────────────
let elf = result
.elf_path
.as_ref()
.expect("teensy build must produce ELF");
let probe = ElfProbe::open(elf).expect("ELF parses");

let dmabuffers = probe
.section_size(".dmabuffers")
.expect("dmabuffers section query");
assert!(
dmabuffers <= 1024,
"AC#2: .dmabuffers must be <= 1 KB; got {dmabuffers} bytes. \
If this fires, the resolver linked FNET/Snooze/RadioHead/mbedtls \
DMAMEM-tagged statics into a simple analogWrite sketch — see #204."
);

for forbidden in ["fnet_", "snooze_", "RadioHead", "mbedtls"] {
assert!(
!probe
.has_symbol_containing(forbidden)
.expect("symbol query"),
"AC#2 / #204: forbidden symbol substring '{forbidden}' present \
in ELF — resolver regression"
);
}

// ── compile_commands.json probes (#204 root-cause guard) ────────────
// WHY use result.compile_database_path: per #226, the pipeline ignores
// params.build_dir for the compdb location and roots its build cache
// at <project_dir>/.fbuild/build/<env>/<profile>/. The orchestrator
// already reports the effective location in BuildResult — trust it
// instead of walking the tempdir.
let compdb_path = result
.compile_database_path
.as_ref()
.expect("teensy build must report compile_commands.json path");
let db = CompileDb::from_path(compdb_path).expect("parse compile_commands.json");
let forbidden_hits = db.forbidden_present(&["FNET", "Snooze", "RadioHead", "mbedtls"]);
assert!(
forbidden_hits.is_empty(),
"AC#2 / #204: compile_commands.json must not include any of \
FNET/Snooze/RadioHead/mbedtls; found: {:?}",
forbidden_hits
);
}
Loading