//! The Linux target, built and run. //! //! A graph compiled for Linux is a Cargo project (`codegen::linux::to_linux_project`) whose //! program drives `flowrs_device::linux_io::LinuxBoard`. These tests build that project and //! run it on this machine with the board's simulated GPIO chip (`FLOWRS_GPIO_CHIP=sim`) and //! sysfs trees laid out in a temp directory, so what the generated program does to its pins //! is read back from the board's own record. The hardware test it stands in for (a Pi 5 //! toggling a line that gpiocdev reads back 1,000 times) and the kernel's `gpio-sim` run //! need a Linux machine; `tools/linux-gpio-loopback.sh` is that run. //! //! The cross-builds for both GNU/Linux triples under `-D warnings` are `#[ignore]`d: they //! need a linker for each triple (a GNU cross gcc, or cargo-zigbuild and zig; see //! `CrossLinker`), and minutes. `cargo test -p flowrs-core --test linux_target -- --ignored` //! runs them. //! //! Needs cargo and the crates flowrs-device uses in the local cache (`--offline`), as the //! MCU target's compile tests do. use std::path::{Path, PathBuf}; use std::process::Command; use flowrs_connect::scaffold::{CrossLinker, DeviceSource, LINUX_TRIPLES}; use flowrs_core::codegen::link::{LinkChannel, LinkEnd, LinkPlan, WireTy}; use flowrs_core::codegen::linux::{to_linux_project, to_linux_project_linked}; use flowrs_core::graph::WireMode; use flowrs_core::graph::ir::{Graph, NodeId}; fn device() -> DeviceSource { let p = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../flowrs-device") .canonicalize() .expect("the flowrs-device crate beside this one"); DeviceSource::Path(p.display().to_string()) } fn set(g: &mut Graph, id: NodeId, k: &str, v: &str) { g.get_node_mut(id) .unwrap() .properties .insert(k.into(), v.into()); } /// A held toggle drives GPIO 17; GPIO 27, jumpered to 17 on the bench, is read and mirrored /// to GPIO 22. Each pass of the step loop is one toggle. fn loopback() -> Graph { let mut g = Graph::new(); let not = g.add_node("logic.not", 0.0, 0.0); let held = g .add_wire(not, "result", not, "value", WireMode::Copy) .unwrap(); g.wires.get_mut(&held).unwrap().feedback = true; let out = g.add_node("embedded.gpio.digital_out", 0.0, 0.0); set(&mut g, out, "pin", "17"); g.add_wire(not, "result", out, "state", WireMode::Copy) .unwrap(); let din = g.add_node("embedded.gpio.digital_in", 0.0, 0.0); set(&mut g, din, "pin", "27"); let mirror = g.add_node("embedded.gpio.digital_out", 0.0, 0.0); set(&mut g, mirror, "pin", "22"); g.add_wire(din, "state", mirror, "state", WireMode::Copy) .unwrap(); g } /// An ADC reading sets a PWM duty: the analog half of the surface. fn adc_to_pwm() -> Graph { let mut g = Graph::new(); let adc = g.add_node("embedded.adc", 0.0, 0.0); set(&mut g, adc, "channel", "0"); let pwm = g.add_node("embedded.pwm", 0.0, 0.0); set(&mut g, pwm, "pin", "1"); set(&mut g, pwm, "frequency", "1000"); g.add_wire(adc, "voltage", pwm, "duty", WireMode::Copy) .unwrap(); g } /// Every embedded node once, nothing left unread: the program the cross-builds check. fn every_embedded_node() -> Graph { let mut g = loopback(); g.add_node("embedded.pwm", 0.0, 0.0); let rx = g.add_node("embedded.uart.read", 0.0, 0.0); let tx = g.add_node("embedded.uart.write", 0.0, 0.0); g.add_wire(rx, "data", tx, "data", WireMode::Copy).unwrap(); let i2c = g.add_node("embedded.i2c.read", 0.0, 0.0); set(&mut g, i2c, "length", "2"); let spi = g.add_node("embedded.spi.transfer", 0.0, 0.0); set(&mut g, spi, "cs_pin", "0"); g.add_wire(i2c, "data", spi, "tx_data", WireMode::Copy) .unwrap(); let w = g.add_node("embedded.i2c.write", 0.0, 0.0); g.add_wire(spi, "rx_data", w, "data", WireMode::Copy) .unwrap(); g.add_node("embedded.adc", 0.0, 0.0); g.add_node("embedded.delay", 0.0, 0.0); g.add_node("embedded.timer.interval", 0.0, 0.0); g } /// The project for `graph` written under the temp dir, every build of it sharing one /// target dir so flowrs-device compiles once per run. fn write_project(name: &str, graph: &Graph, steps: usize) -> PathBuf { let dir = std::env::temp_dir().join("flowrs-linux-target").join(name); let _ = std::fs::remove_dir_all(&dir); let project = to_linux_project(graph, steps, name, &device()).expect("a Linux project"); for f in &project.files { let p = dir.join(&f.path); std::fs::create_dir_all(p.parent().unwrap()).unwrap(); std::fs::write(p, &f.contents).unwrap(); } dir } fn target_dir() -> PathBuf { std::env::temp_dir() .join("flowrs-linux-target") .join("target") } /// Build the project for this machine and return the binary. fn build_native(dir: &Path, name: &str) -> PathBuf { let out = Command::new("cargo") .current_dir(dir) .env("CARGO_TARGET_DIR", target_dir()) .env("RUSTFLAGS", "-D warnings") .args(["build", "--offline", "--quiet"]) .output() .expect("cargo"); assert!( out.status.success(), "{name} did not build:\n{}\n{}", std::fs::read_to_string(dir.join("src/main.rs")).unwrap_or_default(), String::from_utf8_lossy(&out.stderr) ); target_dir().join("debug").join(name) } /// The 1,000-toggle loopback, on the board's simulated GPIO chip. /// /// Line 17 is jumpered to 27 on the simulated chip, and the board reads 27 back after every /// write to 17 (`FLOWRS_GPIO_READBACK=17:27`). The program must finish cleanly and the chip's /// record must show 1,000 writes to 17, alternating from high, each followed at once by a /// read of 27 at the same level. With the jumper pulled, the first high write fails the /// run, naming both lines: the check can fail. #[test] fn the_generated_program_toggles_a_line_that_reads_back_1000_times() { let dir = write_project("gpio-loopback", &loopback(), 1000); let bin = build_native(&dir, "gpio-loopback"); let log = dir.join("gpio.log"); let run = |wired: bool| { let mut c = Command::new(&bin); c.env("FLOWRS_GPIO_CHIP", "sim") .env("FLOWRS_GPIO_SIM_LINES", "32") .env("FLOWRS_GPIO_READBACK", "17:27") .env("FLOWRS_GPIO_SIM_LOG", &log); if wired { c.env("FLOWRS_GPIO_SIM_WIRES", "17:27"); } c.output().expect("run the generated program") }; let out = run(true); let stderr = String::from_utf8_lossy(&out.stderr); assert!(out.status.success(), "the loopback failed:\n{stderr}"); assert!( stderr.contains("1000 of 1000 checked GPIO writes read back"), "{stderr}" ); let record = std::fs::read_to_string(&log).expect("the simulated chip's record"); let events: Vec<&str> = record.lines().collect(); let mut writes = 0; for (i, e) in events.iter().enumerate() { let level = match *e { "claim 17 output high" | "set 17 high" => "high", "set 17 low" => "low", _ => continue, }; writes += 1; let want = if writes % 2 == 1 { "high" } else { "low" }; assert_eq!(level, want, "write {writes} drove 17 {level}"); // The next read, past the request for line 27 that the first readback makes. let next = events[i + 1..].iter().find(|e| !e.starts_with("claim ")); assert_eq!( next.copied(), Some(format!("get 27 {level}").as_str()), "write {writes} was not read back on 27 at once" ); } assert_eq!(writes, 1000, "{record}"); // The graph's own read of 27 saw the same level and mirrored it to 22. assert_eq!( events.iter().filter(|e| e.starts_with("set 22 ")).count(), 999 ); let out = run(false); let stderr = String::from_utf8_lossy(&out.stderr); assert!(!out.status.success(), "a pulled jumper must fail the run"); assert!( stderr.contains("line 17 high") && stderr.contains("line 27 reads low"), "{stderr}" ); } /// The analog half through the sysfs classes, laid out in a temp directory: an ADC count /// of 2000 at the kernel's 0.25 mV per count is 0.5 V, which as a duty is half of a 1 ms /// period. #[test] fn the_generated_program_drives_pwm_from_an_iio_reading() { let dir = write_project("adc-to-pwm", &adc_to_pwm(), 1); let bin = build_native(&dir, "adc-to-pwm"); let sys = dir.join("sys"); let pwm = sys.join("pwmchip0/pwm1"); std::fs::create_dir_all(&pwm).unwrap(); for f in ["period", "duty_cycle", "enable"] { std::fs::write(pwm.join(f), "0\n").unwrap(); } let iio = sys.join("iio:device0"); std::fs::create_dir_all(&iio).unwrap(); std::fs::write(iio.join("in_voltage0_raw"), "2000\n").unwrap(); std::fs::write(iio.join("in_voltage_scale"), "0.25\n").unwrap(); let out = Command::new(&bin) .env("FLOWRS_PWM_CHIP", sys.join("pwmchip0")) .env("FLOWRS_IIO_DEVICE", &iio) .output() .expect("run the generated program"); assert!( out.status.success(), "{}", String::from_utf8_lossy(&out.stderr) ); let read = |f: &str| std::fs::read_to_string(pwm.join(f)).unwrap(); assert_eq!(read("period"), "1000000"); assert_eq!(read("duty_cycle"), "500000"); assert_eq!(read("enable"), "1"); // No IIO device: the program stops with an error naming it, and drives nothing. let out = Command::new(&bin) .env("FLOWRS_IIO_DEVICE", sys.join("absent")) .output() .unwrap(); assert!(!out.status.success()); assert!(String::from_utf8_lossy(&out.stderr).contains("absent")); } /// The Linux half of a graph cut at a UART, speaking the typed link: it builds under /// `-D warnings`, opens its board's UART for the link before anything else, and stops /// with an error naming the port when there is none. (The link itself, end to end over a /// tty, is flowrs-device's `link` tests and the MCU apps' host run in `codegen::mcu`; /// macOS refuses serialport's baud-rate ioctl on a pseudo-terminal opened by path, so the /// program cannot be pointed at one here.) #[test] fn a_linked_program_builds_and_opens_its_link_first() { let mut g = Graph::new(); let pwm = g.add_node("embedded.pwm", 0.0, 0.0); let button = g.add_node("embedded.gpio.digital_in", 0.0, 0.0); set(&mut g, button, "pin", "5"); let plan = LinkPlan { uart: 3, baud: 1_000_000, timeout_ms: 500, cdc: false, order: None, channels: vec![ LinkChannel { id: 1, name: "voltage".into(), ty: WireTy::F32, end: LinkEnd::Receive { node: pwm, port: "duty".into(), }, iface: None, fragments: 1, stamped: false, }, LinkChannel { id: 2, name: "led".into(), ty: WireTy::Bool, end: LinkEnd::Send { node: button, port: "state".into(), }, iface: None, fragments: 1, stamped: false, }, ], }; let name = "linked"; let dir = std::env::temp_dir().join("flowrs-linux-target").join(name); let _ = std::fs::remove_dir_all(&dir); let project = to_linux_project_linked(&g, 1, name, &device(), &plan).expect("a project"); for f in &project.files { let p = dir.join(&f.path); std::fs::create_dir_all(p.parent().unwrap()).unwrap(); std::fs::write(p, &f.contents).unwrap(); } let bin = build_native(&dir, name); let out = Command::new(&bin) .env("FLOWRS_GPIO_CHIP", "sim") .env("FLOWRS_UART", "/dev/flowrs-no-such-tty{uart}") .output() .expect("run the generated program"); let stderr = String::from_utf8_lossy(&out.stderr); assert!(!out.status.success(), "no port, no run:\n{stderr}"); assert!(stderr.contains("/dev/flowrs-no-such-tty3"), "{stderr}"); } /// Acceptance: the generated programs build for both GNU/Linux triples with `-D warnings`, /// each linked by what this host has for it ([`CrossLinker::detect`]): on the build host a /// GNU cross gcc for aarch64 and cargo-zigbuild for x86_64. #[test] #[ignore = "cross-builds two triples (GNU cross gcc or cargo-zigbuild); minutes. Run with --ignored"] fn generated_programs_cross_build_for_both_linux_triples_without_warnings() { let graphs = [ ("every-embedded-node", every_embedded_node(), 3), ("gpio-loopback", loopback(), 1000), ("adc-to-pwm", adc_to_pwm(), 1), ]; for (name, graph, steps) in graphs { let dir = write_project(&format!("cross-{name}"), &graph, steps); for triple in LINUX_TRIPLES { let linker = CrossLinker::detect(triple); let out = linker .release_build(&dir, triple) .arg("--offline") .env("CARGO_TARGET_DIR", target_dir()) .env("RUSTFLAGS", "-D warnings") .output() .expect("cargo"); assert!( out.status.success(), "{name} for {triple} ({linker:?}):\n{}", String::from_utf8_lossy(&out.stderr) ); let bin = target_dir() .join(triple) .join("release") .join(format!("cross-{name}")); let head = std::fs::read(&bin).unwrap_or_default(); // ELF, 64-bit, and the machine: EM_AARCH64 (183) or EM_X86_64 (62). assert_eq!(&head[..4], b"\x7fELF", "{} is not an ELF", bin.display()); assert_eq!(head[4], 2, "64-bit"); let machine = u16::from_le_bytes([head[18], head[19]]); let want = if triple.starts_with("aarch64") { 183 } else { 62 }; assert_eq!(machine, want, "{name} for {triple}: e_machine {machine}"); eprintln!( "built {name} for {triple} with {linker:?} under -D warnings: {} ({} bytes)", bin.display(), head.len() ); } } }