Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 44 additions & 5 deletions ostool-server/tests/session_ws_lifecycle.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Integration tests for serial WebSocket session lifecycle and virtual power state.
#![cfg(unix)]

use std::{
Expand Down Expand Up @@ -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<TestServerHandle> {
let config_path = root.join("config.toml");
let data_dir = root.join("data");
Expand All @@ -108,12 +110,15 @@ fn spawn_test_server(root: &Path, serial_port: String) -> Result<TestServerHandl

std::fs::create_dir_all(&board_dir)
.with_context(|| format!("failed to create {}", board_dir.display()))?;
let mut tftp = BuiltinTftpConfig::default_with_root(tftp_root);
tftp.enabled = false;

let config = ServerConfig {
listen_addr: "127.0.0.1:0".parse().unwrap(),
data_dir,
board_dir: board_dir.clone(),
dtb_dir,
tftp: TftpConfig::Builtin(BuiltinTftpConfig::default_with_root(tftp_root)),
tftp: TftpConfig::Builtin(tftp),
network: ostool_server::TftpNetworkConfig {
interface: "lo".into(),
},
Expand Down Expand Up @@ -239,6 +244,7 @@ fn run_ws_lifecycle_case(mode: ClientShutdownMode) -> 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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -432,6 +435,7 @@ where
}
}

/// Reads the first binary payload from the serial WebSocket.
async fn read_binary_payload<S>(websocket: &mut S) -> Result<Vec<u8>>
where
S: futures_util::Stream<
Expand All @@ -458,6 +462,41 @@ where
}
}

/// Waits until the serial WebSocket reports closed or the connection closes.
async fn wait_for_closed<S>(websocket: &mut S) -> Result<()>
where
S: futures_util::Stream<
Item = std::result::Result<Message, tokio_tungstenite::tungstenite::Error>,
> + 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,
Expand Down
4 changes: 4 additions & 0 deletions ostool/src/bin/cargo-osrun.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Cargo runner entrypoint that dispatches built ELF artifacts to QEMU or U-Boot.

use std::{
env,
path::PathBuf,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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:?}");
Expand Down
187 changes: 187 additions & 0 deletions ostool/src/build/cargo_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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<Option<PathBuf>> {
let s = match self.config.extra_config.as_ref() {
Some(s) => s,
Expand Down Expand Up @@ -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<ResolvedCargoArtifact> {
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();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

有 serde 序列化,似乎没必要做这种测试

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个主要是验证产物的,已经更新描述

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());
}
}
Loading