use super::{discovery::qemu_configs_in_dir, *};
pub(crate) fn normalize_qemu_test_commands<I, S>(
qemu_config_path: &Path,
commands: I,
suite_name: &str,
) -> anyhow::Result<Vec<String>>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut test_commands = Vec::new();
for command in commands {
let command = command.as_ref().trim().to_string();
if command.is_empty() {
bail!(
"{suite_name} grouped qemu case `{}` contains an empty test command",
qemu_config_path.display()
);
}
test_commands.push(command);
}
Ok(test_commands)
}
pub(crate) fn load_test_qemu_case_fields(
display_name: String,
name: String,
case_dir: PathBuf,
qemu_config_path: PathBuf,
suite_name: &str,
discover_subcases: bool,
) -> anyhow::Result<TestQemuCase> {
let (test_case, write_policy) = load_qemu_case_fields_with_write_policy(
display_name,
name,
case_dir,
qemu_config_path,
suite_name,
discover_subcases,
)?;
ensure_test_rootfs_write_policy(write_policy, &test_case.qemu_config_path, suite_name)?;
Ok(test_case)
}
pub(crate) fn validate_test_qemu_rootfs_write_policy(
qemu_config_path: &Path,
suite_name: &str,
) -> anyhow::Result<()> {
let config = load_qemu_case_extra_config(qemu_config_path)?;
ensure_test_rootfs_write_policy(config.rootfs_write_policy, qemu_config_path, suite_name)
}
fn ensure_test_rootfs_write_policy(
write_policy: crate::rootfs::qemu::RootfsWritePolicy,
qemu_config_path: &Path,
suite_name: &str,
) -> anyhow::Result<()> {
if write_policy == crate::rootfs::qemu::RootfsWritePolicy::Persist {
bail!(
"{suite_name} qemu test case `{}` cannot use `rootfs_write_policy = \"persist\"`; \
test rootfs writes must be discarded",
qemu_config_path.display()
);
}
Ok(())
}
pub(crate) fn load_qemu_case_fields_with_write_policy(
display_name: String,
name: String,
case_dir: PathBuf,
qemu_config_path: PathBuf,
suite_name: &str,
discover_subcases: bool,
) -> anyhow::Result<(TestQemuCase, crate::rootfs::qemu::RootfsWritePolicy)> {
let config = load_qemu_case_extra_config(&qemu_config_path)?;
let write_policy = config.rootfs_write_policy;
let test_commands =
normalize_qemu_test_commands(&qemu_config_path, config.test_commands, suite_name)?;
let subcases = if discover_subcases && !test_commands.is_empty() {
let arch = qemu_config_path
.file_stem()
.and_then(|stem| stem.to_str())
.and_then(|stem| stem.strip_prefix("qemu-"));
discover_qemu_subcases(&case_dir, arch)?
} else {
Vec::new()
};
let test_case = TestQemuCase {
display_name,
name,
case_dir,
qemu_config_path,
test_commands,
host_symbolize_success_regex: config.host_symbolize_success_regex,
host_http_server: config.host_http_server,
subcases,
grouped_subcase_filter: None,
};
Ok((test_case, write_policy))
}
pub(crate) fn load_qemu_case_extra_config(
qemu_config_path: &Path,
) -> anyhow::Result<QemuCaseExtraConfig> {
let content = fs::read_to_string(qemu_config_path)
.with_context(|| format!("failed to read {}", qemu_config_path.display()))?;
let config: QemuCaseExtraConfig = toml::from_str(&content)
.with_context(|| format!("failed to parse {}", qemu_config_path.display()))?;
if config.legacy_snapshot.is_some() {
bail!(
"QEMU config `{}` uses removed field `snapshot`; replace it with `rootfs_write_policy \
= \"discard\"` or `rootfs_write_policy = \"persist\"`",
qemu_config_path.display()
);
}
Ok(config)
}
pub(crate) fn load_qemu_case_host_http_server(
qemu_config_path: &Path,
) -> anyhow::Result<Option<HostHttpServerConfig>> {
Ok(load_qemu_case_extra_config(qemu_config_path)?.host_http_server)
}
pub(super) fn discover_qemu_subcases(
case_dir: &Path,
arch: Option<&str>,
) -> anyhow::Result<Vec<TestQemuSubcase>> {
let mut subcases = Vec::new();
for entry in
fs::read_dir(case_dir).with_context(|| format!("failed to read {}", case_dir.display()))?
{
let entry = entry?;
let path = entry.path();
if !path.is_dir() {
continue;
}
if let Some(arch) = arch
&& let Some(configs) = qemu_configs_in_dir(&path)?
&& !configs.contains_key(arch)
{
continue;
}
let Ok(name) = entry.file_name().into_string() else {
continue;
};
let kind = if path.join("c").is_dir() || path.join("CMakeLists.txt").is_file() {
Some(TestQemuSubcaseKind::C)
} else if path.join("rust").is_dir() {
Some(TestQemuSubcaseKind::Rust)
} else {
None
};
if let Some(kind) = kind {
subcases.push(TestQemuSubcase {
name,
case_dir: path,
kind,
});
}
}
subcases.sort_by(|left, right| left.name.cmp(&right.name));
Ok(subcases)
}
pub(crate) fn validate_grouped_qemu_commands(
qemu: &QemuConfig,
case: &TestQemuCase,
suite_name: &str,
) -> anyhow::Result<()> {
let shell_init_cmd_set = qemu
.shell_init_cmd
.as_deref()
.map(str::trim)
.is_some_and(|value| !value.is_empty());
if shell_init_cmd_set && !case.test_commands.is_empty() {
bail!(
"{suite_name} grouped qemu case `{}` cannot define both `shell_init_cmd` and \
`test_commands`",
case.qemu_config_path.display()
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
use crate::rootfs::qemu::RootfsWritePolicy;
fn write_qemu_config(content: &str) -> (tempfile::TempDir, PathBuf) {
let root = tempdir().unwrap();
let path = root.path().join("qemu-x86_64.toml");
fs::write(&path, content).unwrap();
(root, path)
}
#[test]
fn rootfs_write_policy_defaults_to_discard() {
let (_root, path) = write_qemu_config("args = []\n");
let config = load_qemu_case_extra_config(&path).unwrap();
assert_eq!(config.rootfs_write_policy, RootfsWritePolicy::Discard);
}
#[test]
fn rootfs_write_policy_parses_persist() {
let (_root, path) = write_qemu_config("args = []\nrootfs_write_policy = \"persist\"\n");
let config = load_qemu_case_extra_config(&path).unwrap();
assert_eq!(config.rootfs_write_policy, RootfsWritePolicy::Persist);
}
#[test]
fn legacy_snapshot_field_reports_migration_error() {
let (_root, path) = write_qemu_config("args = []\nsnapshot = false\n");
let error = load_qemu_case_extra_config(&path).unwrap_err().to_string();
assert!(error.contains("removed field `snapshot`"), "{error}");
assert!(error.contains("rootfs_write_policy"), "{error}");
}
#[test]
fn qemu_test_case_rejects_persistent_rootfs_policy() {
let (root, path) = write_qemu_config("args = []\nrootfs_write_policy = \"persist\"\n");
let error = load_test_qemu_case_fields(
"case".to_string(),
"case".to_string(),
root.path().to_path_buf(),
path,
"Starry",
false,
)
.unwrap_err()
.to_string();
assert!(
error.contains("test rootfs writes must be discarded"),
"{error}"
);
}
}