From 8fd71e850780b7d333ae34b0522419de5411e13b Mon Sep 17 00:00:00 2001 From: Niu Zhihong Date: Sat, 16 May 2026 14:27:35 +0800 Subject: [PATCH] =?UTF-8?q?test(ostool):=20=E8=A1=A5=E5=85=85=20build/run?= =?UTF-8?q?=20=E8=B0=83=E7=94=A8=E5=A5=91=E7=BA=A6=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐 build/run 路径上的测试护栏,并根据 review 收敛测试边界。 - 使用显式 virtio-net QEMU fixture 稳定 byte-stream 集成测试,避免默认 NIC ROM 依赖导致串口不可用。 - 使用测试主动输出的 U-Boot marker 验证真实 TCP serial 字节流匹配,避免依赖 arch、FDT 或 bootflow 日志。 - 用 apply_cargo_selector 行为测试替换低价值 clap 字段映射断言。 - 澄清 CargoBuilder 测试覆盖 resolved artifact 写入 runtime state,而非 serde 配置解析。 --- ostool-server/tests/session_ws_lifecycle.rs | 49 ++++- ostool/src/bin/cargo-osrun.rs | 4 + ostool/src/build/cargo_builder.rs | 187 ++++++++++++++++++++ ostool/src/main.rs | 93 +++++++++- ostool/src/tool.rs | 71 ++++++++ ostool/tests/qemu_byte_stream.rs | 109 +++++++++--- uboot-shell/tests/test.rs | 5 + 7 files changed, 491 insertions(+), 27 deletions(-) diff --git a/ostool-server/tests/session_ws_lifecycle.rs b/ostool-server/tests/session_ws_lifecycle.rs index 1d01289d..d6026a5e 100644 --- a/ostool-server/tests/session_ws_lifecycle.rs +++ b/ostool-server/tests/session_ws_lifecycle.rs @@ -1,3 +1,4 @@ +//! Integration tests for serial WebSocket session lifecycle and virtual power state. #![cfg(unix)] use std::{ @@ -99,6 +100,7 @@ fn sample_virtual_board(serial_port: String) -> BoardConfig { } } +/// Starts an in-process ostool-server with one virtual board and PTY serial port. fn spawn_test_server(root: &Path, serial_port: String) -> Result { let config_path = root.join("config.toml"); let data_dir = root.join("data"); @@ -108,12 +110,15 @@ fn spawn_test_server(root: &Path, serial_port: String) -> Result Result<()> { shutdown_result } +/// Drives one client session through power-on, serial I/O, and release assertions. async fn run_client_flow( base_url: &str, mode: ClientShutdownMode, @@ -291,10 +297,7 @@ async fn run_client_flow( .send(Message::Text(r#"{"type":"close"}"#.to_string().into())) .await .context("failed to send websocket close control message")?; - websocket - .send(Message::Close(None)) - .await - .context("failed to send websocket close frame")?; + wait_for_closed(&mut websocket).await?; } ClientShutdownMode::AbruptDrop => { drop(websocket); @@ -432,6 +435,7 @@ where } } +/// Reads the first binary payload from the serial WebSocket. async fn read_binary_payload(websocket: &mut S) -> Result> where S: futures_util::Stream< @@ -458,6 +462,41 @@ where } } +/// Waits until the serial WebSocket reports closed or the connection closes. +async fn wait_for_closed(websocket: &mut S) -> Result<()> +where + S: futures_util::Stream< + Item = std::result::Result, + > + Unpin, +{ + let deadline = Instant::now() + Duration::from_secs(2); + let mut saw_closed_control = false; + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + let message = + tokio::time::timeout(remaining.max(Duration::from_millis(10)), websocket.next()) + .await + .context("timed out waiting for websocket close")?; + let Some(message) = message else { + return if saw_closed_control { + Ok(()) + } else { + Err(anyhow!("websocket closed before closed control message")) + }; + }; + match message.context("failed to read websocket close message")? { + Message::Text(text) if text.contains(r#""type":"closed""#) => { + saw_closed_control = true; + } + Message::Text(text) if text.contains(r#""type":"error""#) => { + bail!("received websocket error while waiting for close: {text}"); + } + Message::Close(_) => return Ok(()), + _ => {} + } + } +} + async fn wait_for_session_release( client: &reqwest::Client, base_url: &str, diff --git a/ostool/src/bin/cargo-osrun.rs b/ostool/src/bin/cargo-osrun.rs index eb1bcf6a..1f584d9a 100644 --- a/ostool/src/bin/cargo-osrun.rs +++ b/ostool/src/bin/cargo-osrun.rs @@ -1,3 +1,5 @@ +//! Cargo runner entrypoint that dispatches built ELF artifacts to QEMU or U-Boot. + use std::{ env, path::PathBuf, @@ -91,6 +93,7 @@ async fn main() -> ExitCode { } } +/// Parses Cargo runner arguments and starts the selected runtime backend. async fn try_main() -> anyhow::Result<()> { let args = RunnerArgs::parse(); if env::var("CARGO").is_err() { @@ -170,6 +173,7 @@ async fn try_main() -> anyhow::Result<()> { Ok(()) } +/// Reports runner errors to logs, terminal output, and the file-log hint. fn report_error(err: &anyhow::Error) { log::error!("{err:#}"); log::error!("Trace:\n{err:?}"); diff --git a/ostool/src/build/cargo_builder.rs b/ostool/src/build/cargo_builder.rs index 074278d4..7039dd41 100644 --- a/ostool/src/build/cargo_builder.rs +++ b/ostool/src/build/cargo_builder.rs @@ -290,6 +290,7 @@ impl<'a> CargoBuilder<'a> { Ok(cmd) } + /// Applies the resolved Cargo artifact to the legacy tool runtime state. async fn handle_output(&mut self) -> anyhow::Result<()> { let resolved = self.resolved_artifact.clone().ok_or_else(|| { anyhow!( @@ -377,6 +378,7 @@ impl<'a> CargoBuilder<'a> { } } + /// Resolves an optional extra Cargo config from a local path or URL. async fn cargo_extra_config(&self) -> anyhow::Result> { let s = match self.config.extra_config.as_ref() { Some(s) => s, @@ -560,3 +562,188 @@ fn select_executable_artifact( "package `{package}` has multiple binary targets ({bins}); pass system.Cargo.bin or --bin" ) } + +#[cfg(test)] +mod tests { + use std::{ + collections::HashMap, + fs, + path::{Path, PathBuf}, + }; + + use super::{CargoBuilder, ResolvedCargoArtifact, select_executable_artifact}; + use crate::{ + Tool, ToolConfig, + build::config::{Cargo, CargoBuildProfile}, + }; + + fn artifact(name: &str) -> ResolvedCargoArtifact { + let cargo_artifact_dir = PathBuf::from("/tmp/ostool-target/debug"); + ResolvedCargoArtifact { + elf_path: cargo_artifact_dir.join(name), + cargo_artifact_dir, + } + } + + fn select( + artifacts: &[(String, ResolvedCargoArtifact)], + explicit_bin: Option<&str>, + default_run: Option<&str>, + package: &str, + ) -> anyhow::Result { + select_executable_artifact(artifacts, explicit_bin, default_run, package) + } + + #[test] + fn select_executable_artifact_uses_explicit_bin_first() { + let artifacts = vec![ + ("kernel".to_string(), artifact("kernel")), + ("kernel-qemu".to_string(), artifact("kernel-qemu")), + ]; + + let selected = select(&artifacts, Some("kernel-qemu"), None, "kernel").unwrap(); + + assert_eq!( + selected.elf_path, + Path::new("/tmp/ostool-target/debug/kernel-qemu") + ); + } + + #[test] + fn select_executable_artifact_errors_when_explicit_bin_was_not_built() { + let artifacts = vec![("kernel".to_string(), artifact("kernel"))]; + + let err = select(&artifacts, Some("missing-bin"), None, "kernel").unwrap_err(); + + assert!( + err.to_string() + .contains("binary target `missing-bin` was not built") + ); + } + + #[test] + fn select_executable_artifact_prefers_package_name_before_default_run() { + let artifacts = vec![ + ("helper".to_string(), artifact("helper")), + ("kernel".to_string(), artifact("kernel")), + ]; + + let selected = select(&artifacts, None, Some("helper"), "kernel").unwrap(); + + assert_eq!( + selected.elf_path, + Path::new("/tmp/ostool-target/debug/kernel") + ); + } + + #[test] + fn select_executable_artifact_uses_default_run_without_package_name_binary() { + let artifacts = vec![ + ("helper".to_string(), artifact("helper")), + ("boot-test".to_string(), artifact("boot-test")), + ]; + + let selected = select(&artifacts, None, Some("boot-test"), "kernel").unwrap(); + + assert_eq!( + selected.elf_path, + Path::new("/tmp/ostool-target/debug/boot-test") + ); + } + + #[test] + fn select_executable_artifact_uses_single_binary_as_fallback() { + let artifacts = vec![("helper".to_string(), artifact("helper"))]; + + let selected = select(&artifacts, None, None, "kernel").unwrap(); + + assert_eq!( + selected.elf_path, + Path::new("/tmp/ostool-target/debug/helper") + ); + } + + #[test] + fn select_executable_artifact_errors_on_empty_cargo_output() { + let err = select(&[], None, None, "kernel").unwrap_err(); + + assert!(err.to_string().contains("no executable bin artifact found")); + } + + #[test] + fn select_executable_artifact_errors_on_ambiguous_multiple_binaries() { + let artifacts = vec![ + ("kernel-qemu".to_string(), artifact("kernel-qemu")), + ("kernel-uboot".to_string(), artifact("kernel-uboot")), + ]; + + let err = select(&artifacts, None, None, "kernel").unwrap_err(); + + let rendered = err.to_string(); + assert!(rendered.contains("multiple binary targets")); + assert!(rendered.contains("kernel-qemu")); + assert!(rendered.contains("kernel-uboot")); + } + + /// Verifies resolved Cargo artifacts are recorded into runtime state. + /// + /// This covers post-resolution Tool state, not serde/config loading. + #[tokio::test] + async fn handle_output_records_runtime_artifact_state_without_objcopy() { + let temp = tempfile::tempdir().unwrap(); + fs::write( + temp.path().join("Cargo.toml"), + "[package]\nname = \"kernel\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ) + .unwrap(); + fs::create_dir_all(temp.path().join("src")).unwrap(); + fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap(); + + let cargo_artifact_dir = temp.path().join("target/aarch64/debug"); + fs::create_dir_all(&cargo_artifact_dir).unwrap(); + let elf_path = cargo_artifact_dir.join("kernel"); + fs::copy(std::env::current_exe().unwrap(), &elf_path).unwrap(); + + let config = Cargo { + env: HashMap::new(), + target: "aarch64-unknown-none".into(), + package: "kernel".into(), + bin: None, + features: vec![], + log: None, + extra_config: None, + profile: Some(CargoBuildProfile::Debug), + args: vec![], + pre_build_cmds: vec![], + post_build_cmds: vec![], + to_bin: true, + }; + + let mut tool = Tool::new(ToolConfig { + manifest: Some(temp.path().to_path_buf()), + ..Default::default() + }) + .unwrap(); + + let mut builder = CargoBuilder::build(&mut tool, &config, None).skip_objcopy(true); + builder.resolved_artifact = Some(ResolvedCargoArtifact { + elf_path: elf_path.clone(), + cargo_artifact_dir: cargo_artifact_dir.clone(), + }); + builder.handle_output().await.unwrap(); + drop(builder); + + let expected_elf = elf_path.canonicalize().unwrap(); + assert_eq!(tool.ctx.artifacts.elf.as_ref(), Some(&expected_elf)); + assert!(tool.ctx.artifacts.bin.is_none()); + assert_eq!( + tool.ctx.artifacts.cargo_artifact_dir.as_ref(), + Some(&cargo_artifact_dir) + ); + assert_eq!( + tool.ctx.artifacts.runtime_artifact_dir.as_ref(), + Some(&cargo_artifact_dir) + ); + assert!(tool.ctx.arch.is_some()); + } +} diff --git a/ostool/src/main.rs b/ostool/src/main.rs index ff8ff740..e2bb58ff 100644 --- a/ostool/src/main.rs +++ b/ostool/src/main.rs @@ -1,3 +1,5 @@ +//! Main ostool CLI argument parsing and command dispatch. + use std::{path::PathBuf, process::ExitCode}; use anyhow::Result; @@ -177,6 +179,7 @@ async fn main() -> ExitCode { } } +/// Parses the CLI and dispatches the selected ostool subcommand. async fn try_main() -> Result<()> { env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); @@ -335,6 +338,7 @@ async fn try_main() -> Result<()> { Ok(()) } +/// Creates the legacy tool facade from an optional manifest argument. fn init_tool(manifest_arg: Option) -> Result<(Tool, ManifestContext)> { let manifest = resolve_manifest_context(manifest_arg.clone())?; info!("Using manifest {}", manifest.manifest_path.display()); @@ -346,6 +350,7 @@ fn init_tool(manifest_arg: Option) -> Result<(Tool, ManifestContext)> { Ok((tool, manifest)) } +/// Loads the build config from an explicit path or workspace default. async fn load_build_config( tool: &mut Tool, manifest: &ManifestContext, @@ -360,6 +365,7 @@ async fn load_build_config( } } +/// Applies `--package` and `--bin` overrides to Cargo build configs. fn apply_cargo_selector( tool: &mut Tool, build_config: &mut build::config::BuildConfig, @@ -384,6 +390,7 @@ fn apply_cargo_selector( Ok(()) } +/// Loads QEMU config from an explicit path or workspace default. async fn load_qemu_config( tool: &mut Tool, manifest: &ManifestContext, @@ -398,6 +405,7 @@ async fn load_qemu_config( } } +/// Loads U-Boot config from an explicit path or workspace default. async fn load_uboot_config( tool: &mut Tool, manifest: &ManifestContext, @@ -412,6 +420,7 @@ async fn load_uboot_config( } } +/// Loads board-run config from an explicit path or workspace default. async fn load_board_config( tool: &mut Tool, manifest: &ManifestContext, @@ -426,6 +435,7 @@ async fn load_board_config( } } +/// Prints CLI errors with a structured trace. fn report_error(err: &anyhow::Error) { log::error!("{err:#}"); log::error!("Trace:\n{err:?}"); @@ -436,9 +446,15 @@ fn report_error(err: &anyhow::Error) { #[cfg(test)] mod tests { + use std::fs; + use clap::Parser; + use ostool::{Tool, ToolConfig}; - use super::{BoardArgs, BoardSubCommands, Cli, SubCommands}; + use super::{ + BoardArgs, BoardSubCommands, CargoSelectorArgs, Cli, SubCommands, apply_cargo_selector, + build, + }; #[test] fn parse_board_ls_with_server_args() { @@ -565,6 +581,81 @@ mod tests { } } + #[test] + fn apply_cargo_selector_overrides_cargo_build_config() { + let (_temp, mut tool) = test_tool(); + let mut build_config = build::config::BuildConfig { + system: build::config::BuildSystem::Cargo(build::config::Cargo { + package: "default-package".into(), + bin: None, + ..Default::default() + }), + }; + + apply_cargo_selector( + &mut tool, + &mut build_config, + &CargoSelectorArgs { + package: Some("kernel".into()), + bin: Some("kernel-qemu".into()), + }, + ) + .unwrap(); + + match &build_config.system { + build::config::BuildSystem::Cargo(cargo) => { + assert_eq!(cargo.package, "kernel"); + assert_eq!(cargo.bin.as_deref(), Some("kernel-qemu")); + } + other => panic!("unexpected build system: {other:?}"), + } + assert_eq!(tool.ctx().build_config.as_ref(), Some(&build_config)); + } + + #[test] + fn apply_cargo_selector_rejects_custom_build_config() { + let (_temp, mut tool) = test_tool(); + let mut build_config = build::config::BuildConfig { + system: build::config::BuildSystem::Custom(build::config::Custom { + build_cmd: "make".into(), + elf_path: "target/kernel.elf".into(), + to_bin: true, + }), + }; + + let err = apply_cargo_selector( + &mut tool, + &mut build_config, + &CargoSelectorArgs { + package: Some("kernel".into()), + bin: None, + }, + ) + .unwrap_err(); + + assert!( + err.to_string() + .contains("--package/--bin can only be used with system.Cargo") + ); + } + + fn test_tool() -> (tempfile::TempDir, Tool) { + let temp = tempfile::tempdir().unwrap(); + fs::write( + temp.path().join("Cargo.toml"), + "[package]\nname = \"kernel\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ) + .unwrap(); + fs::create_dir_all(temp.path().join("src")).unwrap(); + fs::write(temp.path().join("src/lib.rs"), "").unwrap(); + let tool = Tool::new(ToolConfig { + manifest: Some(temp.path().to_path_buf()), + ..Default::default() + }) + .unwrap(); + (temp, tool) + } + #[test] fn parse_board_config_command() { let cli = Cli::try_parse_from(["ostool", "board", "config"]).unwrap(); diff --git a/ostool/src/tool.rs b/ostool/src/tool.rs index 6b0b69a1..3f433719 100644 --- a/ostool/src/tool.rs +++ b/ostool/src/tool.rs @@ -1,3 +1,5 @@ +//! Legacy tool facade for workspace configuration, build, and run workflows. + use std::{ env::current_dir, ffi::OsStr, @@ -1082,6 +1084,29 @@ mod tests { assert_eq!(replaced, std::env::temp_dir().display().to_string()); } + /// Verifies that missing environment placeholders expand to an empty string. + #[test] + fn replace_string_uses_empty_string_for_missing_env() { + let temp = tempfile::tempdir().unwrap(); + write_single_package(temp.path(), "sample"); + + let tool = Tool::new(ToolConfig { + manifest: Some(temp.path().to_path_buf()), + ..Default::default() + }) + .unwrap(); + + let missing = format!( + "__OSTOOL_TEST_ENV_SHOULD_NOT_EXIST_{}__", + std::process::id() + ); + + let replaced = tool + .replace_string(&format!("before-${{env:{missing}}}-after")) + .unwrap(); + assert_eq!(replaced, "before--after"); + } + #[test] fn replace_string_uses_package_dir_from_build_config() { let temp = tempfile::tempdir().unwrap(); @@ -1196,6 +1221,41 @@ mod tests { envs.iter() .any(|(k, v)| k == "PKG_DIR" && v == &temp.path().display().to_string()) ); + assert!( + envs.iter() + .any(|(k, v)| k == "WORKSPACE_FOLDER" && v == &temp.path().display().to_string()) + ); + } + + /// Verifies shell hooks receive the runtime kernel ELF path. + #[cfg(unix)] + #[tokio::test] + async fn shell_run_cmd_injects_kernel_elf_when_runtime_elf_exists() { + let temp = tempfile::tempdir().unwrap(); + write_single_package(temp.path(), "sample"); + + let source = std::env::current_exe().unwrap(); + let copied = temp.path().join("sample-elf"); + std::fs::copy(&source, &copied).unwrap(); + + let mut tool = Tool::new(ToolConfig { + manifest: Some(temp.path().to_path_buf()), + ..Default::default() + }) + .unwrap(); + tool.set_elf_artifact_path(copied.clone()).await.unwrap(); + + let output = temp.path().join("kernel-env.txt"); + tool.shell_run_cmd(&format!( + "printf '%s' \"$KERNEL_ELF\" > {}", + output.display() + )) + .unwrap(); + + assert_eq!( + fs::read_to_string(output).unwrap(), + copied.canonicalize().unwrap().display().to_string() + ); } #[test] @@ -1385,4 +1445,15 @@ targets = "aarch64-unknown-none" fs::write(package_dir.join("src/lib.rs"), "").unwrap(); root.join("Cargo.toml") } + + /// Writes a minimal single-package Cargo project for tool tests. + fn write_single_package(root: &Path, package: &str) { + fs::write( + root.join("Cargo.toml"), + format!("[package]\nname = \"{package}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n"), + ) + .unwrap(); + fs::create_dir_all(root.join("src")).unwrap(); + fs::write(root.join("src/lib.rs"), "").unwrap(); + } } diff --git a/ostool/tests/qemu_byte_stream.rs b/ostool/tests/qemu_byte_stream.rs index 206f55c2..b862c6ca 100644 --- a/ostool/tests/qemu_byte_stream.rs +++ b/ostool/tests/qemu_byte_stream.rs @@ -1,5 +1,7 @@ +//! QEMU-backed byte-stream matcher integration tests. + use std::{ - io::{ErrorKind, Read}, + io::{ErrorKind, Read, Write}, net::TcpStream, process::{Child, Command, Stdio}, sync::atomic::{AtomicU32, Ordering}, @@ -12,6 +14,12 @@ use ostool::run::{ByteStreamMatcher, StreamMatchKind}; use regex::Regex; static PORT: AtomicU32 = AtomicU32::new(11000); +const SUCCESS_MARKER: &str = "__OSTOOL_QEMU_SUCCESS_MARKER__"; +const FAIL_MARKER: &str = "__OSTOOL_QEMU_FAIL_MARKER__"; +const BOTH_MARKER: &str = "__OSTOOL_QEMU_BOTH_MARKER__"; +const NEVER_MATCH_REGEX: &str = r"__ostool_never_match__"; +const MARKER_COMMAND_DELAY: Duration = Duration::from_secs(2); +const MARKER_COMMAND_INTERVAL: Duration = Duration::from_millis(300); struct QemuGuard(Option); @@ -41,6 +49,7 @@ fn uboot_bin() -> std::path::PathBuf { std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../assets/u-boot.bin") } +/// Starts QEMU with U-Boot and returns a TCP serial stream. fn spawn_uboot_qemu() -> Result<(QemuGuard, TcpStream)> { let port = PORT.fetch_add(1, Ordering::SeqCst); let bin = uboot_bin(); @@ -54,6 +63,12 @@ fn spawn_uboot_qemu() -> Result<(QemuGuard, TcpStream)> { "-cpu", "cortex-a57", "-nographic", + // Avoid QEMU's default NIC path, which can require efi-virtio.rom + // before the TCP serial endpoint becomes available in slim images. + "-netdev", + "user,id=net0", + "-device", + "virtio-net-device,netdev=net0", "-bios", bin.to_str() .context("u-boot.bin path contains invalid UTF-8")?, @@ -96,7 +111,41 @@ struct MatchOutcome { tail_bytes: usize, } -fn run_case(success_patterns: &[&str], fail_patterns: &[&str]) -> Result> { +fn marker_regex(marker: &str) -> String { + format!(r"(?m)^{}", regex::escape(marker)) +} + +fn drive_uboot_marker_command( + stream: &mut TcpStream, + started_at: Instant, + last_write: &mut Option, + marker: &str, +) -> Result<()> { + let now = Instant::now(); + if last_write + .as_ref() + .is_some_and(|last| now.duration_since(*last) < MARKER_COMMAND_INTERVAL) + { + return Ok(()); + } + + if now.duration_since(started_at) < MARKER_COMMAND_DELAY { + stream + .write_all(b"\x03\r") + .context("failed to interrupt U-Boot autoboot")?; + } else { + write!(stream, "echo {marker}\r").context("failed to write U-Boot marker command")?; + } + + *last_write = Some(now); + Ok(()) +} + +fn run_case( + success_patterns: &[&str], + fail_patterns: &[&str], + marker: &str, +) -> Result> { let (guard, mut stream) = match spawn_uboot_qemu() { Ok(pair) => pair, Err(err) if err.to_string().contains("not installed") => { @@ -118,6 +167,8 @@ fn run_case(success_patterns: &[&str], fail_patterns: &[&str]) -> Result Result Result Result<()> { + // Drive U-Boot to print a marker line controlled by the test fixture. The + // line-anchor keeps the matcher from accepting the echoed command itself. + let success_marker_regex = marker_regex(SUCCESS_MARKER); let Some(outcome) = run_case( - &[r"Hit any key to stop autoboot:"], - &[r"__ostool_never_fail__"], + &[success_marker_regex.as_str()], + &[NEVER_MATCH_REGEX], + SUCCESS_MARKER, )? else { return Ok(()); }; assert_eq!(outcome.kind, StreamMatchKind::Success); - assert_eq!(outcome.matched_regex, r"Hit any key to stop autoboot:"); - assert!( - outcome - .matched_text - .contains("Hit any key to stop autoboot") - ); + assert_eq!(outcome.matched_regex, success_marker_regex); + assert!(outcome.matched_text.contains(SUCCESS_MARKER)); assert!( outcome.tail_bytes > 0, "expected tail drain bytes after success" @@ -209,15 +270,22 @@ fn qemu_byte_stream_success_matches_before_newline() -> Result<()> { Ok(()) } +/// Verifies a fail regex can match before the newline is drained. #[test] fn qemu_byte_stream_fail_matches_before_newline() -> Result<()> { - let Some(outcome) = run_case(&[r"__ostool_never_success__"], &[r"Net:\s+eth0:"])? else { + let fail_marker_regex = marker_regex(FAIL_MARKER); + let Some(outcome) = run_case( + &[NEVER_MATCH_REGEX], + &[fail_marker_regex.as_str()], + FAIL_MARKER, + )? + else { return Ok(()); }; assert_eq!(outcome.kind, StreamMatchKind::Fail); - assert_eq!(outcome.matched_regex, r"Net:\s+eth0:"); - assert!(outcome.matched_text.contains("Net:")); + assert_eq!(outcome.matched_regex, fail_marker_regex); + assert!(outcome.matched_text.contains(FAIL_MARKER)); assert!( outcome.tail_bytes > 0, "expected tail drain bytes after fail" @@ -225,23 +293,22 @@ fn qemu_byte_stream_fail_matches_before_newline() -> Result<()> { Ok(()) } +/// Verifies fail matches take precedence when both regex sets match. #[test] fn qemu_byte_stream_fail_wins_when_both_match() -> Result<()> { + let both_marker_regex = marker_regex(BOTH_MARKER); let Some(outcome) = run_case( - &[r"Hit any key to stop autoboot:"], - &[r"Hit any key to stop autoboot:"], + &[both_marker_regex.as_str()], + &[both_marker_regex.as_str()], + BOTH_MARKER, )? else { return Ok(()); }; assert_eq!(outcome.kind, StreamMatchKind::Fail); - assert_eq!(outcome.matched_regex, r"Hit any key to stop autoboot:"); - assert!( - outcome - .matched_text - .contains("Hit any key to stop autoboot") - ); + assert_eq!(outcome.matched_regex, both_marker_regex); + assert!(outcome.matched_text.contains(BOTH_MARKER)); assert!( outcome.tail_bytes > 0, "expected tail drain bytes after fail" diff --git a/uboot-shell/tests/test.rs b/uboot-shell/tests/test.rs index d37039a2..10ab65e2 100644 --- a/uboot-shell/tests/test.rs +++ b/uboot-shell/tests/test.rs @@ -1,3 +1,5 @@ +//! QEMU-backed smoke tests for the asynchronous U-Boot shell. + use std::{ process::{Child, Command}, sync::atomic::AtomicU32, @@ -14,6 +16,7 @@ use uboot_shell::UbootShell; static PORT: AtomicU32 = AtomicU32::new(10000); +/// Starts QEMU with the bundled U-Boot image and returns an attached shell. async fn new_uboot() -> (Child, UbootShell) { let port = PORT.fetch_add(1, std::sync::atomic::Ordering::SeqCst); @@ -27,6 +30,8 @@ async fn new_uboot() -> (Child, UbootShell) { "-cpu", "cortex-a57", "-nographic", + "-net", + "none", "-bios", "../assets/u-boot.bin", ])