use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use anyhow::{Context, anyhow, bail};
use cargo_metadata::Package;
use clap::{Args, Subcommand};
use ostool::{board::RunBoardOptions, build::config::Cargo, run::qemu::QemuConfig};
use crate::{
axvisor,
context::{
AppContext, ResolvedAxvisorRequest, ResolvedStarryRequest, resolve_axvisor_arch_and_target,
resolve_starry_arch_and_target,
},
starry,
};
#[cfg(test)]
mod tests;
const AXTEST_RUSTFLAGS: &[&str] = &["--cfg", "axtest", "--check-cfg", "cfg(axtest)"];
const AXTEST_FEATURE: &str = "axtest";
const AXTEST_SUITE_OK: &str = "AXTEST_SUITE_OK";
const AXTEST_SUITE_FAIL: &str = "AXTEST_SUITE_FAIL";
const AXTEST_CASE_FAIL: &str = "AXTEST_CASE .* status=fail";
const PANIC_FAIL: &str = "panicked at";
#[derive(Args, Debug, Clone)]
pub(crate) struct ArgsKtest {
#[command(subcommand)]
pub(crate) command: Command,
}
#[derive(Subcommand, Debug, Clone)]
pub(crate) enum Command {
Qemu(ArgsKtestQemu),
Board(ArgsKtestBoard),
}
#[derive(Args, Debug, Clone)]
pub(crate) struct ArgsKtestQemu {
#[arg(short = 'p', long = "package", value_name = "PACKAGE")]
pub(crate) package: String,
#[arg(long = "test", value_name = "TARGET")]
pub(crate) test: Option<String>,
#[arg(long, value_name = "ARCH", conflicts_with = "target")]
pub(crate) arch: Option<String>,
#[arg(short = 't', long, value_name = "TRIPLE", conflicts_with = "arch")]
pub(crate) target: Option<String>,
#[arg(long = "config", value_name = "BUILD_TOML")]
pub(crate) config: Option<PathBuf>,
#[arg(long = "qemu-config", value_name = "QEMU_TOML")]
pub(crate) qemu_config: Option<PathBuf>,
#[arg(long)]
pub(crate) coverage: bool,
}
#[derive(Args, Debug, Clone)]
pub(crate) struct ArgsKtestBoard {
#[arg(short = 'p', long = "package", value_name = "PACKAGE")]
pub(crate) package: String,
#[arg(long = "test", value_name = "TARGET")]
pub(crate) test: String,
#[arg(short = 'b', long = "board", value_name = "BOARD")]
pub(crate) board: String,
#[arg(long = "config", value_name = "BUILD_TOML")]
pub(crate) config: Option<PathBuf>,
#[arg(long = "board-config", value_name = "BOARD_TOML")]
pub(crate) board_config: Option<PathBuf>,
#[arg(long = "board-type", value_name = "TYPE")]
pub(crate) board_type: Option<String>,
#[arg(long)]
pub(crate) server: Option<String>,
#[arg(long)]
pub(crate) port: Option<u16>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct KtestPackage {
pub(crate) name: String,
pub(crate) targets: Vec<KtestTarget>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct KtestTarget {
pub(crate) name: String,
pub(crate) kind: KtestTargetKind,
pub(crate) harness: bool,
pub(crate) required_features: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum KtestTargetKind {
Lib,
Test,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum KtestRuntime {
Starry,
Axvisor,
}
pub(crate) async fn run(args: ArgsKtest) -> anyhow::Result<()> {
match args.command {
Command::Qemu(args) => run_qemu(args).await,
Command::Board(args) => run_board(args).await,
}
}
async fn run_qemu(args: ArgsKtestQemu) -> anyhow::Result<()> {
let mut app = AppContext::new()?;
let package = load_ktest_package(&args.package)?;
let target = select_ktest_target(&package, args.test.as_deref())?;
let runtime = runtime_for_package(app.workspace_root(), &args.package)?;
let (arch, triple) = resolve_arch_and_target(runtime, args.arch, args.target)?;
let build_config = args
.config
.unwrap_or_else(|| default_qemu_build_config(app.workspace_root(), &args.package, &arch));
let qemu_config = args
.qemu_config
.unwrap_or_else(|| default_qemu_run_config(app.workspace_root(), &args.package, &arch));
let mut cargo = load_runtime_cargo(
runtime,
app.workspace_root(),
&args.package,
&arch,
&triple,
&build_config,
)?;
prepare_ktest_cargo(&mut cargo, target, args.coverage);
app.set_debug_mode(false)?;
let output = app.build(cargo.clone(), build_config.clone()).await?;
maybe_postprocess_starry_artifact(
KtestBuildContext {
runtime,
workspace_root: app.workspace_root(),
package: &args.package,
arch: &arch,
target: &triple,
build_config: &build_config,
},
&cargo,
&output,
)?;
let rootfs = ensure_runtime_qemu_assets(runtime, app.workspace_root(), &arch, &triple).await?;
let mut qemu = app
.read_qemu_config_from_path_for_cargo(&cargo, &qemu_config)
.await
.with_context(|| format!("failed to read QEMU config {}", qemu_config.display()))?;
if let Some(rootfs) = rootfs {
crate::rootfs::qemu::patch_rootfs(
&mut qemu,
&rootfs,
crate::rootfs::qemu::RootfsPatchMode::EnsureDiskBootNet,
);
}
crate::test::qemu::apply_dynamic_platform_qemu_boot(&mut qemu, &cargo);
apply_axtest_qemu_markers(&mut qemu);
app.run_qemu_with_axtest_coverage(&cargo, qemu, None).await
}
#[derive(Debug, Clone, Copy)]
struct KtestBuildContext<'a> {
runtime: KtestRuntime,
workspace_root: &'a Path,
package: &'a str,
arch: &'a str,
target: &'a str,
build_config: &'a Path,
}
async fn ensure_runtime_qemu_assets(
runtime: KtestRuntime,
workspace_root: &Path,
arch: &str,
target: &str,
) -> anyhow::Result<Option<PathBuf>> {
if runtime == KtestRuntime::Starry {
let rootfs = starry::rootfs::ensure_rootfs_in_tmp_dir(workspace_root, arch, target).await?;
return Ok(Some(rootfs));
}
if runtime == KtestRuntime::Axvisor {
let rootfs = crate::image::storage::ensure_rootfs_for_arch(workspace_root, arch).await?;
return Ok(Some(rootfs));
}
Ok(None)
}
async fn run_board(args: ArgsKtestBoard) -> anyhow::Result<()> {
let mut app = AppContext::new()?;
let package = load_ktest_package(&args.package)?;
let target = select_ktest_target(&package, Some(&args.test))?;
let runtime = runtime_for_package(app.workspace_root(), &args.package)?;
let board = args.board;
let build_config = args
.config
.unwrap_or_else(|| default_board_build_config(app.workspace_root(), &args.package, &board));
let board_config_path = args
.board_config
.unwrap_or_else(|| default_board_run_config(app.workspace_root(), &args.package, &board));
let target_from_config =
load_target_from_build_config(runtime, &build_config).with_context(|| {
format!(
"failed to read build target from {}",
build_config.display()
)
})?;
let triple = target_from_config
.ok_or_else(|| anyhow!("board ktest requires target in {}", build_config.display()))?;
let arch = crate::context::arch_for_target_checked(&triple)?;
let mut cargo = load_runtime_cargo(
runtime,
app.workspace_root(),
&args.package,
arch,
&triple,
&build_config,
)?;
prepare_ktest_cargo(&mut cargo, target, false);
let board_config = app
.read_board_run_config_from_path_for_cargo(&cargo, &board_config_path)
.await
.with_context(|| {
format!(
"failed to read board config {}",
board_config_path.display()
)
})?;
let output = app.build(cargo.clone(), build_config.clone()).await?;
maybe_postprocess_starry_artifact(
KtestBuildContext {
runtime,
workspace_root: app.workspace_root(),
package: &args.package,
arch,
target: &triple,
build_config: &build_config,
},
&cargo,
&output,
)?;
app.board_prepared_elf(
output.elf_path().to_path_buf(),
cargo.to_bin,
build_config,
board_config,
RunBoardOptions {
board_type: args.board_type,
server: args.server,
port: args.port,
},
)
.await
}
fn load_ktest_package(package: &str) -> anyhow::Result<KtestPackage> {
let metadata = crate::build::cached_workspace_metadata()?;
let package = metadata
.packages
.iter()
.find(|candidate| candidate.name.as_str() == package)
.ok_or_else(|| anyhow!("workspace package `{package}` not found"))?;
ktest_package_from_metadata(package)
}
fn ktest_package_from_metadata(package: &Package) -> anyhow::Result<KtestPackage> {
let harness = test_harness_flags(package.manifest_path.as_std_path())?;
let targets = package
.targets
.iter()
.map(|target| {
let kind = if target.is_test() {
KtestTargetKind::Test
} else if target.is_lib() {
KtestTargetKind::Lib
} else {
KtestTargetKind::Other
};
KtestTarget {
name: target.name.clone(),
kind,
harness: harness.get(&target.name).copied().unwrap_or(true),
required_features: target.required_features.clone(),
}
})
.collect();
Ok(KtestPackage {
name: package.name.to_string(),
targets,
})
}
fn test_harness_flags(manifest_path: &Path) -> anyhow::Result<HashMap<String, bool>> {
let content = fs::read_to_string(manifest_path)
.with_context(|| format!("failed to read {}", manifest_path.display()))?;
let value: toml::Value = toml::from_str(&content)
.with_context(|| format!("failed to parse {}", manifest_path.display()))?;
let mut flags = HashMap::new();
if let Some(tests) = value.get("test").and_then(toml::Value::as_array) {
for entry in tests {
let Some(table) = entry.as_table() else {
continue;
};
let Some(name) = table.get("name").and_then(toml::Value::as_str) else {
continue;
};
let harness = table
.get("harness")
.and_then(toml::Value::as_bool)
.unwrap_or(true);
flags.insert(name.to_string(), harness);
}
}
Ok(flags)
}
pub(crate) fn select_ktest_target<'a>(
package: &'a KtestPackage,
explicit: Option<&str>,
) -> anyhow::Result<&'a KtestTarget> {
if let Some(name) = explicit {
let target = package
.targets
.iter()
.find(|target| target.name == name)
.ok_or_else(|| {
anyhow!(
"test target `{name}` not found in package `{}`",
package.name
)
})?;
if target.kind != KtestTargetKind::Test || target.harness {
bail!(
"test target `{}` in package `{}` must be a harness=false [[test]] target",
target.name,
package.name
);
}
return Ok(target);
}
let candidates = package
.targets
.iter()
.filter(|target| target.kind == KtestTargetKind::Test && !target.harness)
.collect::<Vec<_>>();
match candidates.as_slice() {
[target] => Ok(*target),
[] => bail!(
"package `{}` has no harness=false [[test]] target; pass --test after adding one",
package.name
),
many => {
let names = many
.iter()
.map(|target| target.name.as_str())
.collect::<Vec<_>>()
.join(", ");
bail!(
"package `{}` has multiple harness=false [[test]] targets ({names}); pass --test",
package.name
)
}
}
}
fn prepare_ktest_cargo(cargo: &mut Cargo, target: &KtestTarget, coverage: bool) {
cargo.bin = None;
cargo.test = Some(target.name.clone());
remove_cargo_target_selector_args(&mut cargo.args);
cargo.env.remove("AXBUILD_STARRY_BIN");
ensure_feature(cargo, AXTEST_FEATURE);
for feature in &target.required_features {
ensure_feature(cargo, feature);
}
crate::build::append_encoded_rustflags(cargo, AXTEST_RUSTFLAGS);
if coverage {
cargo
.env
.insert("AXTEST_COVERAGE".to_string(), "y".to_string());
crate::support::axtest_coverage::prepare_cargo(cargo);
}
}
fn ensure_feature(cargo: &mut Cargo, feature: &str) {
if !cargo.features.iter().any(|candidate| candidate == feature) {
cargo.features.push(feature.to_string());
}
}
fn remove_cargo_target_selector_args(args: &mut Vec<String>) {
let mut filtered = Vec::with_capacity(args.len());
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if matches!(arg.as_str(), "--bin" | "--test") {
let _ = iter.next();
continue;
}
if arg.starts_with("--bin=") || arg.starts_with("--test=") {
continue;
}
filtered.push(arg.clone());
}
*args = filtered;
}
fn apply_axtest_qemu_markers(qemu: &mut QemuConfig) {
ensure_regex(&mut qemu.success_regex, AXTEST_SUITE_OK);
ensure_regex(&mut qemu.fail_regex, PANIC_FAIL);
ensure_regex(&mut qemu.fail_regex, AXTEST_SUITE_FAIL);
ensure_regex(&mut qemu.fail_regex, AXTEST_CASE_FAIL);
}
fn ensure_regex(regexes: &mut Vec<String>, regex: &str) {
if !regexes.iter().any(|candidate| candidate == regex) {
regexes.push(regex.to_string());
}
}
fn runtime_for_package(workspace_root: &Path, package: &str) -> anyhow::Result<KtestRuntime> {
if package == axvisor::build::AXVISOR_PACKAGE {
return Ok(KtestRuntime::Axvisor);
}
let metadata = crate::build::cached_workspace_metadata()?;
let package = metadata
.packages
.iter()
.find(|candidate| candidate.name.as_str() == package)
.ok_or_else(|| anyhow!("workspace package `{package}` not found"))?;
let manifest = package.manifest_path.as_std_path();
if manifest.starts_with(workspace_root.join("os/StarryOS")) {
Ok(KtestRuntime::Starry)
} else {
bail!(
"ktest currently supports StarryOS and Axvisor packages, got `{}`",
package.name
)
}
}
fn resolve_arch_and_target(
runtime: KtestRuntime,
arch: Option<String>,
target: Option<String>,
) -> anyhow::Result<(String, String)> {
match runtime {
KtestRuntime::Starry => resolve_starry_arch_and_target(arch, target),
KtestRuntime::Axvisor => resolve_axvisor_arch_and_target(arch, target),
}
}
fn load_runtime_cargo(
runtime: KtestRuntime,
workspace_root: &Path,
package: &str,
arch: &str,
target: &str,
build_config: &Path,
) -> anyhow::Result<Cargo> {
match runtime {
KtestRuntime::Starry => {
let request = starry_request(package, arch, target, build_config);
starry::build::load_cargo_config(&request)
}
KtestRuntime::Axvisor => {
let request = axvisor_request(workspace_root, package, arch, target, build_config);
axvisor::build::load_cargo_config(&request)
}
}
}
fn load_target_from_build_config(
runtime: KtestRuntime,
build_config: &Path,
) -> anyhow::Result<Option<String>> {
match runtime {
KtestRuntime::Starry => starry::build::load_target_from_build_config(build_config),
KtestRuntime::Axvisor => axvisor::build::load_target_from_build_config(build_config),
}
}
fn maybe_postprocess_starry_artifact(
ctx: KtestBuildContext<'_>,
cargo: &Cargo,
output: &ostool::build::CargoBuildOutput,
) -> anyhow::Result<()> {
if ctx.runtime != KtestRuntime::Starry {
return Ok(());
}
let request = starry_request(ctx.package, ctx.arch, ctx.target, ctx.build_config);
starry::build::postprocess_starry_artifact(ctx.workspace_root, &request, cargo, output)
}
fn starry_request(
package: &str,
arch: &str,
target: &str,
build_config: &Path,
) -> ResolvedStarryRequest {
ResolvedStarryRequest {
package: package.to_string(),
arch: arch.to_string(),
target: target.to_string(),
smp: None,
debug: false,
build_info_path: build_config.to_path_buf(),
build_info_override: None,
qemu_config: None,
uboot_config: None,
}
}
fn axvisor_request(
workspace_root: &Path,
package: &str,
arch: &str,
target: &str,
build_config: &Path,
) -> ResolvedAxvisorRequest {
ResolvedAxvisorRequest {
package: package.to_string(),
axvisor_dir: workspace_root.join("os/axvisor"),
arch: arch.to_string(),
target: target.to_string(),
smp: None,
debug: false,
build_info_path: build_config.to_path_buf(),
qemu_config: None,
uboot_config: None,
vmconfigs: Vec::new(),
}
}
pub(crate) fn default_qemu_build_config(
workspace_root: &Path,
package: &str,
arch: &str,
) -> PathBuf {
runtime_config_root(workspace_root, package).join(format!("configs/board/qemu-{arch}.toml"))
}
fn default_qemu_run_config(workspace_root: &Path, package: &str, arch: &str) -> PathBuf {
runtime_config_root(workspace_root, package).join(format!("configs/qemu/qemu-{arch}.toml"))
}
fn default_board_build_config(workspace_root: &Path, package: &str, board: &str) -> PathBuf {
runtime_config_root(workspace_root, package).join(format!("configs/board/{board}.toml"))
}
fn default_board_run_config(workspace_root: &Path, package: &str, board: &str) -> PathBuf {
let root = runtime_config_root(workspace_root, package);
let starry_board_config = root.join(format!("configs/board/{board}-board.toml"));
if package != axvisor::build::AXVISOR_PACKAGE && starry_board_config.exists() {
starry_board_config
} else {
root.join(format!("configs/board/{board}.toml"))
}
}
fn runtime_config_root(workspace_root: &Path, package: &str) -> PathBuf {
if package == axvisor::build::AXVISOR_PACKAGE {
workspace_root.join("os/axvisor")
} else {
workspace_root.join("os/StarryOS")
}
}