|
| 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(¶ms) |
| 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