Skip to content

Commit 0240d9e

Browse files
authored
feat(acceptance): #205 AC#2 teensy30 AnalogOutput .dmabuffers gate (#227)
1 parent a23d855 commit 0240d9e

2 files changed

Lines changed: 156 additions & 0 deletions

File tree

.github/workflows/acceptance-205.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ on:
1717
- 'crates/fbuild-build/src/teensy/**'
1818
- 'crates/fbuild-build/src/stm32/**'
1919
- 'crates/fbuild-build/tests/teensylc_acceptance.rs'
20+
- 'crates/fbuild-build/tests/teensy30_acceptance.rs'
2021
- 'crates/fbuild-build/tests/stm32_acceptance.rs'
2122
- '.github/workflows/acceptance-205.yml'
2223

@@ -37,6 +38,9 @@ jobs:
3738
- gate: teensyLC
3839
test_bin: teensylc_acceptance
3940
test_fn: teensylc_blink_meets_205_acceptance_criteria
41+
- gate: teensy30 AnalogOutput
42+
test_bin: teensy30_acceptance
43+
test_fn: teensy30_analog_output_meets_205_ac2
4044
- gate: stm32f103c8 SPI
4145
test_bin: stm32_acceptance
4246
test_fn: stm32f103c8_blink_with_spi_auto_discovers_library_205_ac4
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//! Acceptance gate for #205 AC#2: teensy30 AnalogOutput sketch.
2+
//!
3+
//! Runs the full TeensyOrchestrator build against an inline-tempdir
4+
//! `AnalogOutput.ino` sketch for the Teensy 3.0 (`teensy30` env) and asserts:
5+
//!
6+
//! * `.dmabuffers` section size <= 1 KB (#205 AC#2). The Teensy 3.0 has
7+
//! only 16 KB of SRAM; FNET, Snooze, and friends each pull DMAMEM-tagged
8+
//! statics (DMA descriptor pools, Ethernet frame buffers, RNG state)
9+
//! into the `.dmabuffers` section. If those libraries are linked into a
10+
//! simple Arduino `analogWrite` sketch, `.dmabuffers` balloons and the
11+
//! build blows the RAM budget. This is the AC#2 gate.
12+
//! * No `fnet_*`, `snooze_*`, `RadioHead`, or `mbedtls` symbols leaked
13+
//! into the linked ELF (#204 / #205 AC#1 regression guard — the same
14+
//! forbidden list as `teensylc_acceptance.rs`, complementing teensyLC's
15+
//! `.bss <= 3 KB` gate with the teensy30 `.dmabuffers` gate).
16+
//! * `compile_commands.json` parses and references no `FNET`, `Snooze`,
17+
//! `RadioHead`, or `mbedtls` files (#204 root-cause guard).
18+
//!
19+
//! Uses the stm32-style inline tempdir `project_dir` so the committed
20+
//! `tests/platform/teensy30/` fixture is untouched and no
21+
//! `compile_commands.json` or `.fbuild/` is ever left behind in the repo.
22+
//!
23+
//! Run with:
24+
//! `uv run soldr cargo test -p fbuild-build --test teensy30_acceptance -- --ignored`
25+
//!
26+
//! Marked `#[ignore]` because it downloads Teensyduino + arm-gcc on the
27+
//! first run (cached after) and performs a full firmware build — too
28+
//! heavy for default `cargo test`.
29+
//!
30+
//! LTO-symbol caveat: as with `teensylc_acceptance.rs` and
31+
//! `stm32_acceptance.rs` (see #223), the Release profile's
32+
//! `-flto -Os` inlines tiny functions like the sketch's `setup` and
33+
//! `loop` into their callers and `--gc-sections` strips the
34+
//! independent symbols. The meaningful signals are therefore the
35+
//! ELF section size and forbidden-symbol substring checks, not
36+
//! probes for `setup`/`loop`/`analogWrite` symbols.
37+
38+
use fbuild_build::{BuildOrchestrator, BuildParams};
39+
use fbuild_core::BuildProfile;
40+
use fbuild_test_support::{CompileDb, ElfProbe};
41+
42+
#[test]
43+
#[ignore = "downloads Teensyduino + arm-gcc; CI-only"]
44+
fn teensy30_analog_output_meets_205_ac2() {
45+
// Use a temporary project dir so the committed teensy30 fixture
46+
// at tests/platform/teensy30/ stays untouched and no scratch
47+
// build artifacts land in the repo.
48+
let tmp = tempfile::TempDir::new().unwrap();
49+
let project_dir = tmp.path();
50+
51+
std::fs::write(
52+
project_dir.join("platformio.ini"),
53+
"[env:teensy30]\n\
54+
platform = teensy\n\
55+
board = teensy30\n\
56+
framework = arduino\n",
57+
)
58+
.unwrap();
59+
60+
let src = project_dir.join("src");
61+
std::fs::create_dir_all(&src).unwrap();
62+
// WHY .ino: the AC#2 sketch is "AnalogOutput" and Teensyduino's
63+
// builder treats .ino as Arduino main; this matches the user-facing
64+
// `fbuild build teensy30 AnalogOutput` invocation in the #205 body.
65+
std::fs::write(
66+
src.join("main.ino"),
67+
"#include <Arduino.h>\n\
68+
void setup() { pinMode(LED_BUILTIN, OUTPUT); }\n\
69+
void loop() {\n\
70+
for (int v = 0; v < 256; v += 5) {\n\
71+
analogWrite(LED_BUILTIN, v);\n\
72+
delay(20);\n\
73+
}\n\
74+
}\n",
75+
)
76+
.unwrap();
77+
78+
let build_dir = project_dir.join(".fbuild/build");
79+
let params = BuildParams {
80+
project_dir: project_dir.to_path_buf(),
81+
// WHY env_name = "teensy30": must match the [env:teensy30] key
82+
// in the platformio.ini we just wrote. Same root-cause family
83+
// as #220 / #221.
84+
env_name: "teensy30".to_string(),
85+
clean: true,
86+
profile: BuildProfile::Release,
87+
build_dir,
88+
verbose: true,
89+
jobs: None,
90+
generate_compiledb: true,
91+
compiledb_only: false,
92+
log_sender: None,
93+
symbol_analysis: false,
94+
symbol_analysis_path: None,
95+
no_timestamp: false,
96+
src_dir: None,
97+
pio_env: Default::default(),
98+
extra_build_flags: Vec::new(),
99+
watch_set_cache: None,
100+
};
101+
102+
let result = fbuild_build::teensy::orchestrator::TeensyOrchestrator
103+
.build(&params)
104+
.expect("teensy30 AnalogOutput build must succeed for AC#2 gate");
105+
assert!(result.success, "build did not report success");
106+
107+
// ── ELF probes (AC#2 + #204 regression guard) ───────────────────────
108+
let elf = result
109+
.elf_path
110+
.as_ref()
111+
.expect("teensy build must produce ELF");
112+
let probe = ElfProbe::open(elf).expect("ELF parses");
113+
114+
let dmabuffers = probe
115+
.section_size(".dmabuffers")
116+
.expect("dmabuffers section query");
117+
assert!(
118+
dmabuffers <= 1024,
119+
"AC#2: .dmabuffers must be <= 1 KB; got {dmabuffers} bytes. \
120+
If this fires, the resolver linked FNET/Snooze/RadioHead/mbedtls \
121+
DMAMEM-tagged statics into a simple analogWrite sketch — see #204."
122+
);
123+
124+
for forbidden in ["fnet_", "snooze_", "RadioHead", "mbedtls"] {
125+
assert!(
126+
!probe
127+
.has_symbol_containing(forbidden)
128+
.expect("symbol query"),
129+
"AC#2 / #204: forbidden symbol substring '{forbidden}' present \
130+
in ELF — resolver regression"
131+
);
132+
}
133+
134+
// ── compile_commands.json probes (#204 root-cause guard) ────────────
135+
// WHY use result.compile_database_path: per #226, the pipeline ignores
136+
// params.build_dir for the compdb location and roots its build cache
137+
// at <project_dir>/.fbuild/build/<env>/<profile>/. The orchestrator
138+
// already reports the effective location in BuildResult — trust it
139+
// instead of walking the tempdir.
140+
let compdb_path = result
141+
.compile_database_path
142+
.as_ref()
143+
.expect("teensy build must report compile_commands.json path");
144+
let db = CompileDb::from_path(compdb_path).expect("parse compile_commands.json");
145+
let forbidden_hits = db.forbidden_present(&["FNET", "Snooze", "RadioHead", "mbedtls"]);
146+
assert!(
147+
forbidden_hits.is_empty(),
148+
"AC#2 / #204: compile_commands.json must not include any of \
149+
FNET/Snooze/RadioHead/mbedtls; found: {:?}",
150+
forbidden_hits
151+
);
152+
}

0 commit comments

Comments
 (0)