use std::fmt::Write as _;
use std::io::Read as _;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use std::thread;
use std::time::{Duration, Instant};
use crate::config::{HilConfig, UartBridge, RENODE_ARM64_IMAGE};
use crate::output::HilOutput;
use crate::platform::{CpuInit, ReplSource};
pub const RUNTIME_ENV: &str = "HILT_CONTAINER_RUNTIME";
const CONTAINER_WORK: &str = "/hil";
pub const RENODE_ARM64_VERSION: &str = "1.16.1";
fn is_managed_arm64_image(image: &str) -> bool {
image == RENODE_ARM64_IMAGE
}
fn renode_arm64_containerfile() -> String {
let v = RENODE_ARM64_VERSION;
format!(
r#"FROM docker.io/library/debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends libicu72 ca-certificates curl \
&& curl -fsSL -o /tmp/renode.tar.gz \
https://github.com/renode/renode/releases/download/v{v}/renode-{v}.linux-arm64-portable-dotnet.tar.gz \
&& mkdir -p /opt && tar xzf /tmp/renode.tar.gz -C /opt && rm /tmp/renode.tar.gz \
&& ln -s /opt/renode_{v}-dotnet_portable /opt/renode \
&& apt-get purge -y curl && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/opt/renode/renode"]
"#
)
}
fn image_present(runtime: &str, image: &str) -> bool {
command_succeeds(runtime, &["image", "inspect", image])
}
fn ensure_renode_image(runtime: &str, image: &str) {
if !is_managed_arm64_image(image) {
return;
}
static BUILD: std::sync::Once = std::sync::Once::new();
BUILD.call_once(|| {
if image_present(runtime, image) {
return;
}
eprintln!("hilt: building native arm64 Renode image `{image}` (first run, one-time)…");
let dir = std::env::temp_dir().join(format!("hilt-renode-build-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("failed to create Renode image build dir");
std::fs::write(dir.join("Containerfile"), renode_arm64_containerfile())
.expect("failed to write Containerfile");
let status = Command::new(runtime)
.args(["build", "--platform", "linux/arm64", "-t", image, "-f"])
.arg(dir.join("Containerfile"))
.arg(&dir)
.status()
.expect("failed to invoke container build");
let _ = std::fs::remove_dir_all(&dir);
assert!(
status.success(),
"failed to build native arm64 Renode image `{image}`"
);
});
}
fn command_succeeds(cmd: &str, args: &[&str]) -> bool {
Command::new(cmd)
.args(args)
.output()
.is_ok_and(|o| o.status.success())
}
fn find_container_runtime() -> Option<String> {
if let Ok(rt) = std::env::var(RUNTIME_ENV) {
return Some(rt);
}
["podman", "docker"]
.iter()
.find(|c| command_succeeds(c, &["--version"]))
.map(ToString::to_string)
}
#[must_use]
pub fn detect_container_runtime() -> String {
find_container_runtime().expect("no container runtime found -- install podman or docker")
}
#[must_use]
pub fn container_runtime_available() -> bool {
find_container_runtime().is_some()
}
#[must_use]
pub fn renode_bin() -> Option<PathBuf> {
if let Some(p) = std::env::var_os("HILT_RENODE_BIN") {
let p = PathBuf::from(p);
if p.exists() {
return Some(p);
}
}
if command_succeeds("renode", &["--version"]) {
return Some(PathBuf::from("renode"));
}
let opt = PathBuf::from("/opt/renode/renode");
opt.exists().then_some(opt)
}
#[must_use]
pub fn renode_available() -> bool {
renode_bin().is_some()
}
fn cleanup_container(runtime: &str, cidfile: &Path) {
let Ok(cid) = std::fs::read_to_string(cidfile) else {
return;
};
let cid = cid.trim();
if cid.is_empty() {
return;
}
let _ = Command::new(runtime).args(["rm", "-f", cid]).status();
}
pub fn setup_vcan(iface: &str) {
Command::new("modprobe").arg("vcan").status().ok();
Command::new("ip")
.args(["link", "del", iface])
.status()
.ok();
let status = Command::new("ip")
.args(["link", "add", "dev", iface, "type", "vcan"])
.status()
.expect("failed to create vcan interface");
assert!(
status.success(),
"failed to create {iface} -- try with sudo or in a container"
);
Command::new("ip")
.args(["link", "set", iface, "up", "mtu", "72"])
.status()
.expect("failed to bring up vcan interface");
}
#[must_use]
pub fn interface_available(iface: &str) -> bool {
command_succeeds("ip", &["link", "show", iface])
}
pub fn run_with_timeout_and_cleanup<F>(
mut command: Command,
timeout_secs: u32,
cleanup: F,
) -> Output
where
F: FnOnce(),
{
command.stdout(Stdio::piped()).stderr(Stdio::piped());
let mut child = command
.spawn()
.unwrap_or_else(|e| panic!("failed to spawn container runtime: {e}"));
let mut stdout = child.stdout.take().expect("child stdout not captured");
let mut stderr = child.stderr.take().expect("child stderr not captured");
let stdout_thread = thread::spawn(move || {
let mut buf = Vec::new();
stdout
.read_to_end(&mut buf)
.unwrap_or_else(|e| panic!("failed to read child stdout: {e}"));
buf
});
let stderr_thread = thread::spawn(move || {
let mut buf = Vec::new();
stderr
.read_to_end(&mut buf)
.unwrap_or_else(|e| panic!("failed to read child stderr: {e}"));
buf
});
let deadline = Instant::now() + Duration::from_secs(timeout_secs.into());
let status = loop {
match child
.try_wait()
.unwrap_or_else(|e| panic!("failed to poll container runtime: {e}"))
{
Some(status) => break status,
None if Instant::now() >= deadline => {
let _ = child.kill();
break child
.wait()
.unwrap_or_else(|e| panic!("failed to reap timed-out container runtime: {e}"));
}
None => thread::sleep(Duration::from_millis(100)),
}
};
cleanup();
let stdout = stdout_thread
.join()
.unwrap_or_else(|_| panic!("failed to join stdout reader"));
let stderr = stderr_thread
.join()
.unwrap_or_else(|_| panic!("failed to join stderr reader"));
Output {
status,
stdout,
stderr,
}
}
pub fn run_renode_container<F>(command: Command, timeout_secs: u32, cleanup: F) -> Output
where
F: FnOnce(),
{
run_with_timeout_and_cleanup(command, timeout_secs.saturating_add(50), cleanup)
}
#[derive(Debug, Clone, Copy)]
pub struct ElfInfo {
pub vtor: u32,
pub sp: u32,
pub pc: u32,
pub hook_addr: Option<u32>,
}
#[must_use]
pub fn extract_elf_info(firmware_elf: &Path, marker: &str) -> ElfInfo {
let vt_output = Command::new("rust-objdump")
.args(["-s", "-j", ".vector_table"])
.arg(firmware_elf)
.output()
.expect("rust-objdump not found -- install via `rustup component add llvm-tools`");
let stdout = String::from_utf8_lossy(&vt_output.stdout);
let mut vtor = 0u32;
let mut sp = 0u32;
let mut pc = 0u32;
for line in stdout.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 3 {
continue;
}
let Ok(addr) = u32::from_str_radix(parts[0], 16) else {
continue;
};
vtor = addr;
sp = swap_endian_u32(parts[1]);
pc = swap_endian_u32(parts[2]) & !1;
break;
}
assert!(
pc != 0,
"failed to extract vector table from {}",
firmware_elf.display()
);
ElfInfo {
vtor,
sp,
pc,
hook_addr: find_symbol_address(firmware_elf, marker),
}
}
#[must_use]
pub fn find_symbol_address(firmware_elf: &Path, symbol: &str) -> Option<u32> {
let output = Command::new("rust-nm")
.arg("--defined-only")
.arg(firmware_elf)
.output()
.ok()?;
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
let mut fields = line.split_whitespace();
let addr_str = fields.next()?;
let _type = fields.next();
if fields.next() == Some(symbol) {
return u32::from_str_radix(addr_str, 16).ok();
}
}
None
}
fn swap_endian_u32(hex: &str) -> u32 {
u32::from_le_bytes(u32::from_str_radix(hex, 16).unwrap_or(0).to_be_bytes())
}
struct ReadyMachine {
name: String,
elf_ref: String,
repl_ref: String,
mips: Option<u32>,
can_connector: Option<&'static str>,
init: CpuInit,
info: Option<ElfInfo>,
socketcan_bridge: bool,
uart_bridge: Option<UartBridge>,
}
fn render_resc(
machines: &[ReadyMachine],
socketcan_iface: &str,
stubs_ref: Option<&str>,
timeout_secs: u32,
) -> String {
let any_bridge = machines.iter().any(|m| m.socketcan_bridge);
let hub = machines.len() > 1 || any_bridge;
let mut s = String::from("using sysbus\n\n");
if hub {
s.push_str("emulation CreateCANHub \"canHub\" False\n\n");
}
for m in machines {
let _ = writeln!(s, "mach create \"{}\"", m.name);
let _ = writeln!(s, "machine LoadPlatformDescription @{}", m.repl_ref);
if let Some(stubs) = stubs_ref {
let _ = writeln!(s, "machine LoadPlatformDescription @{stubs}");
}
if let Some(mips) = m.mips {
let _ = writeln!(s, "cpu PerformanceInMips {mips}");
}
let _ = writeln!(s, "sysbus LoadELF @{}", m.elf_ref);
if m.init == CpuInit::VectorTable {
if let Some(info) = m.info {
let _ = writeln!(s, "cpu VectorTableOffset {:#010X}", info.vtor);
let _ = writeln!(s, "cpu SP {:#010X}", info.sp);
let _ = writeln!(s, "cpu PC {:#010X}", info.pc);
if let Some(addr) = info.hook_addr {
let _ = writeln!(
s,
"cpu AddHook {addr:#010X} \"self.Log(LogLevel.Warning, 'HIL OK')\""
);
}
}
}
if hub {
if let Some(connector) = m.can_connector {
let _ = writeln!(s, "connector Connect {connector} canHub");
}
if m.socketcan_bridge {
let _ = writeln!(s, "machine CreateSocketCANBridge \"{socketcan_iface}\"");
let _ = writeln!(s, "connector Connect {socketcan_iface} canHub");
}
}
if let Some(bridge) = &m.uart_bridge {
let term = format!("{}_uart", m.name);
let _ = writeln!(
s,
"emulation CreateServerSocketTerminal {} \"{term}\" false",
bridge.host_port
);
let _ = writeln!(s, "connector Connect {} {term}", bridge.uart);
}
s.push_str("mach clear\n\n");
}
s.push_str("start\n");
let _ = writeln!(
s,
"emulation RunFor \"00:{:02}:{:02}\"",
timeout_secs / 60,
timeout_secs % 60
);
s.push_str("quit\n");
s
}
pub struct RenodeRunner {
config: HilConfig,
}
struct CleanupGuard(PathBuf);
impl Drop for CleanupGuard {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
impl RenodeRunner {
#[must_use]
pub fn new(config: HilConfig) -> Self {
Self { config }
}
#[must_use]
pub fn config(&self) -> &HilConfig {
&self.config
}
#[must_use]
pub fn run(&self) -> HilOutput {
let work_dir = work_dir_for(&self.config);
std::fs::create_dir_all(&work_dir).expect("failed to create HIL work dir");
let _guard = CleanupGuard(work_dir.clone());
let runtime = find_container_runtime();
let use_container = runtime.is_some();
let renode = if use_container { None } else { renode_bin() };
assert!(
use_container || renode.is_some(),
"no HIL backend: install podman/docker, or a native renode \
(HILT_RENODE_BIN, PATH, or /opt/renode/renode)"
);
let repl_base = if use_container {
CONTAINER_WORK.to_string()
} else {
work_dir.display().to_string()
};
let mut ready = Vec::with_capacity(self.config.machines.len());
let mut elf_paths = Vec::new();
for m in &self.config.machines {
let elf = m.firmware_elf.canonicalize().unwrap_or_else(|e| {
panic!(
"firmware ELF not found at {}: {e}",
m.firmware_elf.display()
)
});
elf_paths.push(elf.clone());
let repl_ref = match m.platform.repl {
ReplSource::ImagePath(p) => p.to_string(),
ReplSource::Embedded(body) => {
let file = format!("{}.repl", m.name);
std::fs::write(work_dir.join(&file), body).expect("failed to write .repl");
format!("{repl_base}/{file}")
}
};
let info = (m.platform.init == CpuInit::VectorTable)
.then(|| extract_elf_info(&elf, &self.config.marker));
ready.push(ReadyMachine {
name: m.name.clone(),
elf_ref: elf.display().to_string(),
repl_ref,
mips: m.platform.mips,
can_connector: m.platform.can_connector,
init: m.platform.init,
info,
socketcan_bridge: m.socketcan_bridge,
uart_bridge: m.uart_bridge.clone(),
});
}
let stubs = self.config.stubs_repl.as_ref().map(|p| {
p.canonicalize()
.unwrap_or_else(|e| panic!("stubs REPL not found at {}: {e}", p.display()))
});
let resc = render_resc(
&ready,
&self.config.socketcan_iface,
stubs.as_ref().map(|p| p.display().to_string()).as_deref(),
self.config.timeout_secs,
);
std::fs::write(work_dir.join("test.resc"), &resc).expect("failed to write .resc");
let mut uart_ports: Vec<u16> = self
.config
.machines
.iter()
.filter_map(|m| m.uart_bridge.as_ref().map(|b| b.host_port))
.collect();
uart_ports.sort_unstable();
let unique = uart_ports.windows(2).all(|w| w[0] != w[1]);
assert!(unique, "duplicate UART bridge host_port in config");
let timeout = self.config.timeout_secs + 15;
let output = if use_container {
let runtime = runtime.expect("container backend resolved above");
ensure_renode_image(&runtime, &self.config.image);
let cidfile = work_dir.join("renode.cid");
let relabel = if cfg!(target_os = "linux") { ",Z" } else { "" };
let mut command = Command::new(&runtime);
command.args(["run", "--rm"]);
if is_managed_arm64_image(&self.config.image) {
command.args(["--platform", "linux/arm64"]);
} else if cfg!(target_arch = "aarch64") {
command.args(["-e", "DOTNET_EnableWriteXorExecute=0"]);
}
if self.config.needs_host_network() {
command.arg("--network=host");
} else {
for port in &uart_ports {
command.args(["-p".to_string(), format!("127.0.0.1:{port}:{port}")]);
}
}
command.args([
"--cidfile".to_string(),
cidfile
.to_str()
.expect("cidfile path not UTF-8")
.to_string(),
"--volume".to_string(),
format!("{}:{CONTAINER_WORK}:ro{relabel}", work_dir.display()),
]);
for elf in &elf_paths {
command.arg("--volume");
command.arg(format!("{}:{}:ro{relabel}", elf.display(), elf.display()));
}
if let Some(stubs) = &stubs {
command.arg("--volume");
command.arg(format!(
"{}:{}:ro{relabel}",
stubs.display(),
stubs.display()
));
}
command.args([
"--entrypoint".to_string(),
"/opt/renode/renode".to_string(),
self.config.image.clone(),
"--disable-xwt".to_string(),
"--plain".to_string(),
"-e".to_string(),
format!("include @{CONTAINER_WORK}/test.resc"),
]);
run_with_timeout_and_cleanup(command, timeout, || cleanup_container(&runtime, &cidfile))
} else {
let renode = renode.expect("native renode backend resolved above");
let mut command = Command::new(renode);
command
.args(["--disable-xwt", "--plain"])
.arg(work_dir.join("test.resc"));
run_with_timeout_and_cleanup(command, timeout, || {})
};
HilOutput::new(
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
output.status.code(),
)
}
}
fn work_dir_for(config: &HilConfig) -> PathBuf {
static RUN_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let seq = RUN_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let base = std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| {
std::env::current_dir()
.expect("failed to read current dir")
.join("target")
});
let tag = config.machines.first().map_or("hil", |m| m.platform.name);
base.join(format!("hil-{tag}-{}-{seq}", std::process::id()))
}
#[derive(Debug, Clone)]
pub enum Artifact {
Bin(String),
Example(String),
}
#[derive(Debug, Clone)]
pub struct GuestBuild {
pub package: String,
pub artifact: Artifact,
pub target: String,
pub features: Vec<String>,
pub no_default_features: bool,
pub release: bool,
}
impl GuestBuild {
#[must_use]
pub fn bin(package: impl Into<String>, name: impl Into<String>) -> Self {
Self::new(package, Artifact::Bin(name.into()))
}
#[must_use]
pub fn example(package: impl Into<String>, name: impl Into<String>) -> Self {
Self::new(package, Artifact::Example(name.into()))
}
fn new(package: impl Into<String>, artifact: Artifact) -> Self {
Self {
package: package.into(),
artifact,
target: String::new(),
features: Vec::new(),
no_default_features: false,
release: true,
}
}
#[must_use]
pub fn target(mut self, target: impl Into<String>) -> Self {
self.target = target.into();
self
}
#[must_use]
pub fn features<I, S>(mut self, features: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.features = features.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn no_default_features(mut self) -> Self {
self.no_default_features = true;
self
}
#[must_use]
pub fn debug(mut self) -> Self {
self.release = false;
self
}
#[must_use]
pub fn output_path(&self) -> PathBuf {
let target_root = std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("target"));
let profile = if self.release { "release" } else { "debug" };
let mut dir = target_root.join(&self.target).join(profile);
let name = match &self.artifact {
Artifact::Bin(n) => n,
Artifact::Example(n) => {
dir = dir.join("examples");
n
}
};
dir.join(name)
}
#[must_use]
pub fn build(&self) -> PathBuf {
let output = self.output_path();
if output.exists() {
return output;
}
let mut cmd = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()));
cmd.args(["build", "-p", &self.package]);
match &self.artifact {
Artifact::Bin(n) => cmd.args(["--bin", n]),
Artifact::Example(n) => cmd.args(["--example", n]),
};
if self.no_default_features {
cmd.arg("--no-default-features");
}
if !self.features.is_empty() {
cmd.args(["--features", &self.features.join(",")]);
}
if self.release {
cmd.arg("--release");
}
if !self.target.is_empty() {
cmd.args(["--target", &self.target]);
}
let status = cmd.status().expect("failed to invoke cargo");
assert!(status.success(), "failed to build guest artifact");
output
}
}
#[cfg(test)]
mod tests {
use super::*;
fn vt_info() -> ElfInfo {
ElfInfo {
vtor: 0x1000_0000,
sp: 0x2004_2000,
pc: 0x1000_0100,
hook_addr: Some(0x1000_0200),
}
}
fn vector_table_machine(name: &str) -> ReadyMachine {
ReadyMachine {
name: name.to_string(),
elf_ref: "/abs/fw.elf".to_string(),
repl_ref: format!("/hil/{name}.repl"),
mips: None,
can_connector: None,
init: CpuInit::VectorTable,
info: Some(vt_info()),
socketcan_bridge: false,
uart_bridge: None,
}
}
fn board_machine(name: &str, bridge: bool) -> ReadyMachine {
ReadyMachine {
name: name.to_string(),
elf_ref: format!("/abs/{name}.elf"),
repl_ref: "platforms/boards/nucleo_h753zi.repl".to_string(),
mips: Some(125),
can_connector: Some("fdcan1"),
init: CpuInit::Board,
info: None,
socketcan_bridge: bridge,
uart_bridge: None,
}
}
#[test]
fn single_vector_table_resc_has_cpu_state_and_hook_no_hub() {
let resc = render_resc(&[vector_table_machine("hil")], "vcan0", None, 10);
assert!(resc.contains("mach create \"hil\""));
assert!(resc.contains("LoadPlatformDescription @/hil/hil.repl"));
assert!(resc.contains("cpu VectorTableOffset 0x10000000"));
assert!(resc.contains("cpu SP 0x20042000"));
assert!(resc.contains("cpu PC 0x10000100"));
assert!(resc.contains("cpu AddHook 0x10000200"));
assert!(resc.contains("HIL OK"));
assert!(resc.contains("RunFor \"00:00:10\""));
assert!(!resc.contains("canHub"));
}
#[test]
fn multi_board_resc_builds_hub_and_connects_fdcan() {
let machines = [
board_machine("controller", true),
board_machine("sensor", false),
];
let resc = render_resc(&machines, "vcan0", None, 15);
assert!(resc.contains("emulation CreateCANHub \"canHub\""));
assert!(resc.contains("cpu PerformanceInMips 125"));
assert!(resc.contains("connector Connect fdcan1 canHub"));
assert!(resc.contains("machine CreateSocketCANBridge \"vcan0\""));
assert!(resc.contains("connector Connect vcan0 canHub"));
assert!(resc.contains("mach create \"controller\""));
assert!(resc.contains("mach create \"sensor\""));
assert_eq!(resc.matches("CreateSocketCANBridge").count(), 1);
}
#[test]
fn single_bridged_machine_still_gets_a_hub() {
let resc = render_resc(&[board_machine("solo", true)], "vcan1", None, 20);
assert!(resc.contains("CreateCANHub"));
assert!(resc.contains("CreateSocketCANBridge \"vcan1\""));
assert!(resc.contains("RunFor \"00:00:20\""));
}
#[test]
fn uart_bridge_renders_server_terminal_and_connect() {
let mut m = vector_table_machine("hil");
m.uart_bridge = Some(UartBridge {
host_port: 3456,
uart: "sysbus.uart".to_string(),
});
let resc = render_resc(&[m], "vcan0", None, 10);
assert!(resc.contains("emulation CreateServerSocketTerminal 3456 \"hil_uart\" false"));
assert!(resc.contains("connector Connect sysbus.uart hil_uart"));
assert!(!resc.contains("canHub"));
}
#[test]
fn stubs_overlay_is_loaded_per_machine() {
let resc = render_resc(
&[vector_table_machine("hil")],
"vcan0",
Some("/abs/stubs.repl"),
5,
);
assert!(resc.contains("LoadPlatformDescription @/abs/stubs.repl"));
}
#[test]
fn timeout_minutes_and_seconds_format() {
let resc = render_resc(&[vector_table_machine("hil")], "vcan0", None, 125);
assert!(resc.contains("RunFor \"00:02:05\""));
}
#[test]
fn guest_build_output_paths() {
let _guard = crate::test_env_lock();
let saved = std::env::var("CARGO_TARGET_DIR").ok();
std::env::remove_var("CARGO_TARGET_DIR");
let bin = GuestBuild::bin("buspass", "hil_controller")
.target("thumbv7em-none-eabihf")
.output_path();
assert!(bin.ends_with("target/thumbv7em-none-eabihf/release/hil_controller"));
let ex = GuestBuild::example("tecla-rp2040", "hil_keyboard")
.target("thumbv6m-none-eabi")
.output_path();
assert!(ex.ends_with("target/thumbv6m-none-eabi/release/examples/hil_keyboard"));
let dbg = GuestBuild::bin("p", "b").target("t").debug().output_path();
assert!(dbg.ends_with("target/t/debug/b"));
if let Some(v) = saved {
std::env::set_var("CARGO_TARGET_DIR", v);
}
}
#[test]
fn detect_runtime_honors_env() {
let _guard = crate::test_env_lock();
let saved = std::env::var(RUNTIME_ENV).ok();
std::env::set_var(RUNTIME_ENV, "fake-runtime");
assert_eq!(detect_container_runtime(), "fake-runtime");
assert!(container_runtime_available());
match saved {
Some(v) => std::env::set_var(RUNTIME_ENV, v),
None => std::env::remove_var(RUNTIME_ENV),
}
}
#[test]
fn interface_available_loopback_yes_bogus_no() {
if Command::new("ip").arg("-V").output().is_ok() {
assert!(interface_available("lo"));
}
assert!(!interface_available("hilt_fake_iface_xyz"));
}
}