espforge-core 0.6.0

a scaffolding++ tool for creating esp32 no_std esphal without the hassle
//! Wiring asserted on the emitted text: what the template-era substring tests
//! covered, driven by real ESPF sources through the core pipeline.
//!
//! These are about generated bytes, not about a terminal, so they belong next
//! to the pipeline. The layout is `prettyplease`'s, so they assert on
//! identifiers and calls rather than on indentation.

use std::collections::BTreeMap;

use espforge_core::answers::Overrides;
use espforge_core::pipeline;

fn generated_for(src: &str) -> BTreeMap<String, String> {
    let checked = pipeline::validate(src, &Overrides::default())
        .ok()
        .unwrap_or_else(|| panic!("the test source validates:\n{src}"));
    let board = pipeline::board(&checked)
        .ok()
        .unwrap_or_else(|| panic!("the test source lowers:\n{src}"));
    let generated = espforge_emit::generate_from_board(
        &board,
        &checked.meta,
        &checked.resolved,
        &espforge_emit::RuntimeSource::Published,
        &[],
    )
    .unwrap();
    generated
        .artifacts
        .into_iter()
        .map(|a| (a.path, a.content))
        .collect()
}

fn project(runtime: &str, body: &str) -> String {
    format!(
        "# espforge\n  <project name=\"t\" />\n  <chip type=\"esp32c3\" />\n  <runtime type=\"{runtime}\" />\n{body}"
    )
}

const LED_BUTTON_BODY: &str = "# peripherals\n  <gpio id=\"g18\" pin=\"18\" direction=\"output\" />\n  <gpio id=\"g9\" pin=\"9\" direction=\"input\" />\n# components\n  <led id=\"red_led\" gpio=\"$g18\" />\n  <button id=\"btn\" gpio=\"$g9\" pull_up=\"true\" />\n# devices\n# services\n";

const I2C_BODY: &str = "# peripherals\n  <i2c id=\"i2c0\" sda=\"4\" scl=\"5\" />\n# components\n  <i2c_component id=\"oled\" i2c=\"$i2c0\" address=\"0x48\" />\n# devices\n# services\n";

const SPI_WRITE_ONLY_BODY: &str = "# peripherals\n  <spi id=\"spi2\" sclk=\"8\" mosi=\"7\" />\n# components\n  <spi_component id=\"display\" spi=\"$spi2\" cs=\"9\" />\n# devices\n# services\n";

const UART_BODY: &str = "# peripherals\n  <uart id=\"uart1\" tx=\"20\" rx=\"21\" />\n# components\n  <uart_component id=\"serial\" uart=\"$uart1\" baud_rate=\"9600\" parity=\"even\" stop_bits=\"2\" />\n# devices\n# services\n";

const WIFI_BODY: &str = "# peripherals\n# components\n# devices\n# services\n  <wifi id=\"wifi0\" network1.ssid=\"Wokwi-GUEST\" />\n";

#[test]
fn leds_and_buttons_reach_generated_rs() {
    let files = generated_for(&project("blocking", LED_BUTTON_BODY));
    let generated = &files["src/generated.rs"];
    assert!(
        generated.contains("pub red_led: espforge_runtime::Led,"),
        "{generated}"
    );
    assert!(
        generated.contains("GPIO18.clone_unchecked()"),
        "{generated}"
    );
    assert!(
        generated.contains("pub btn: espforge_runtime::Button,"),
        "{generated}"
    );
    assert!(generated.contains("Button::new("), "{generated}");
}

#[test]
fn the_runtime_picks_the_blocking_or_async_bus_type() {
    let blocking = generated_for(&project("blocking", I2C_BODY));
    assert!(
        blocking["src/generated.rs"].contains("I2cBus<esp_hal::Blocking>"),
        "{}",
        blocking["src/generated.rs"]
    );

    let embassy = generated_for(&project("embassy", I2C_BODY));
    let generated = &embassy["src/generated.rs"];
    assert!(generated.contains("I2cBus<esp_hal::Async>"), "{generated}");
    assert!(!generated.contains("esp_hal::Blocking"), "{generated}");
}

#[test]
fn a_spi_bus_without_miso_is_write_only() {
    let files = generated_for(&project("blocking", SPI_WRITE_ONLY_BODY));
    let generated = &files["src/generated.rs"];
    assert!(
        generated.contains("SpiBus::build_write_only("),
        "{generated}"
    );
    assert!(!generated.contains("SpiBus::build("), "{generated}");
}

#[test]
fn uart_framing_reaches_generated_rs() {
    let files = generated_for(&project("blocking", UART_BODY));
    let generated = &files["src/generated.rs"];
    assert!(generated.contains("baudrate: 9600"), "{generated}");
    assert!(
        generated.contains("parity: esp_hal::uart::Parity::Even"),
        "{generated}"
    );
    assert!(
        generated.contains("stop_bits: esp_hal::uart::StopBits::_2"),
        "{generated}"
    );
}

#[test]
fn the_wifi_service_reaches_main_and_cargo_toml() {
    let files = generated_for(&project("embassy", WIFI_BODY));
    let main = &files["src/bin/main.rs"];
    assert!(
        main.contains("Services::new(registry.peripherals.WIFI, &spawner)"),
        "{main}"
    );
    assert!(main.contains("esp_alloc::heap_allocator!"), "{main}");
    let cargo = &files["Cargo.toml"];
    assert!(cargo.contains("esp-radio"), "{cargo}");
    assert!(cargo.contains("\"wifi\""), "{cargo}");
    assert!(files["src/generated.rs"].contains("pub wifi: espforge_runtime::Wifi,"));
}