360 lines
14 KiB
Rust
360 lines
14 KiB
Rust
//! 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()
|
|
);
|
|
}
|
|
}
|
|
}
|