Skip to content

Commit fb19b1b

Browse files
committed
tests: WIP introduce performance tests
Signed-off-by: Matej Hrica <mhrica@redhat.com>
1 parent 6106034 commit fb19b1b

File tree

14 files changed

+850
-91
lines changed

14 files changed

+850
-91
lines changed

tests/Cargo.lock

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,12 @@ make test
4343
```
4444

4545
## Adding tests
46-
To add a test you need to add a new rust module in the `test_cases` directory, implement the required host and guest side methods (see existing tests) and register the test in the `test_cases/src/lib.rs` to be ran.
46+
To add a test you need to add a new rust module in the `test_cases` directory, implement the required host and guest side methods (see existing tests) and register the test in the `test_cases/src/lib.rs` to be ran.
47+
48+
## Rootfs images
49+
50+
Some tests (e.g. the iperf3 performance tests) need a full Linux rootfs with extra packages installed. These are built automatically via podman and stored in podman's local image store (tagged as `libkrun-test-<name>`). Podman's layer cache handles rebuild efficiency.
51+
52+
Container image definitions are registered in the `rootfs_image()` function in `test_cases/src/lib.rs`. Tests refer to images by name only. Tests that need a rootfs will be skipped if podman is not installed.
53+
54+
To clean up images: `podman rmi $(podman images --filter reference='libkrun-test-*' -q)`

tests/run.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ if [ -n "${KRUN_TEST_BASE_DIR}" ]; then
6565
RUNNER_ARGS="${RUNNER_ARGS} --base-dir ${KRUN_TEST_BASE_DIR}"
6666
fi
6767

68+
# Build rootfs images before entering the network namespace (needs internet + podman)
69+
target/debug/runner build-images
70+
6871
if [ "$OS" != "Darwin" ] && [ -z "${KRUN_NO_UNSHARE}" ] && which unshare 2>&1 >/dev/null; then
6972
unshare --user --map-root-user --net -- /bin/sh -c "ifconfig lo 127.0.0.1 && exec target/debug/runner ${RUNNER_ARGS}"
7073
else

tests/runner/src/main.rs

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,7 @@ use std::panic::catch_unwind;
88
use std::path::{Path, PathBuf};
99
use std::process::{Command, Stdio};
1010
use tempdir::TempDir;
11-
use test_cases::{test_cases, ShouldRun, Test, TestCase, TestSetup};
12-
13-
#[derive(Clone)]
14-
enum TestOutcome {
15-
Pass,
16-
Fail,
17-
Skip(&'static str),
18-
}
11+
use test_cases::{test_cases, rootfs_images, Report, ShouldRun, Test, TestCase, TestOutcome, TestSetup};
1912

2013
struct TestResult {
2114
name: String,
@@ -88,21 +81,32 @@ fn run_single_test(
8881
.context("Failed to start subprocess for test")?;
8982

9083
let test_name = test_case.name.to_string();
91-
let result = catch_unwind(|| {
84+
let outcome = match catch_unwind(|| {
9285
let test = get_test(&test_name).unwrap();
93-
test.check(child);
94-
});
86+
test.check(child)
87+
}) {
88+
Ok(outcome) => outcome,
89+
Err(_) => TestOutcome::Fail,
90+
};
9591

96-
let outcome = if result.is_ok() {
97-
eprintln!("OK");
98-
if !keep_all {
99-
let _ = fs::remove_dir_all(&test_dir);
92+
match &outcome {
93+
TestOutcome::Pass => {
94+
eprintln!("OK");
95+
if !keep_all {
96+
let _ = fs::remove_dir_all(&test_dir);
97+
}
10098
}
101-
TestOutcome::Pass
102-
} else {
103-
eprintln!("FAIL");
104-
TestOutcome::Fail
105-
};
99+
TestOutcome::Fail => {
100+
eprintln!("FAIL");
101+
}
102+
TestOutcome::Skip(reason) => {
103+
eprintln!("SKIP ({})", reason);
104+
}
105+
TestOutcome::Report(report) => {
106+
eprintln!("REPORT");
107+
eprintln!("{:2}", report.text());
108+
}
109+
}
106110

107111
Ok(TestResult {
108112
name: test_case.name.to_string(),
@@ -116,6 +120,7 @@ fn write_github_summary(
116120
num_pass: usize,
117121
num_fail: usize,
118122
num_skip: usize,
123+
num_report: usize,
119124
) -> anyhow::Result<()> {
120125
let summary_path = env::var("GITHUB_STEP_SUMMARY")
121126
.context("GITHUB_STEP_SUMMARY environment variable not set")?;
@@ -128,22 +133,30 @@ fn write_github_summary(
128133

129134
let num_ran = num_pass + num_fail;
130135
let status = if num_fail == 0 { "✅" } else { "❌" };
131-
let skip_msg = if num_skip > 0 {
132-
format!(" ({num_skip} skipped)")
133-
} else {
136+
let mut extra = Vec::new();
137+
if num_skip > 0 {
138+
extra.push(format!("{num_skip} skipped"));
139+
}
140+
if num_report > 0 {
141+
extra.push(format!("{num_report} reports"));
142+
}
143+
let extra_msg = if extra.is_empty() {
134144
String::new()
145+
} else {
146+
format!(" ({})", extra.join(", "))
135147
};
136148

137149
writeln!(
138150
file,
139-
"## {status} Integration Tests - {num_pass}/{num_ran} passed{skip_msg}\n"
151+
"## {status} Integration Tests - {num_pass}/{num_ran} passed{extra_msg}\n"
140152
)?;
141153

142154
for result in results {
143155
let (icon, status_text) = match &result.outcome {
144156
TestOutcome::Pass => ("✅", String::new()),
145157
TestOutcome::Fail => ("❌", String::new()),
146158
TestOutcome::Skip(reason) => ("⏭️", format!(" - {}", reason)),
159+
TestOutcome::Report(_) => ("📊", String::new()),
147160
};
148161

149162
writeln!(file, "<details>")?;
@@ -153,7 +166,9 @@ fn write_github_summary(
153166
result.name, status_text
154167
)?;
155168

156-
if let Some(log_path) = &result.log_path {
169+
if let TestOutcome::Report(report) = &result.outcome {
170+
writeln!(file, "{}", report.gh_markdown())?;
171+
} else if let Some(log_path) = &result.log_path {
157172
let log_content = fs::read_to_string(log_path).unwrap_or_default();
158173
writeln!(file, "```")?;
159174
// Limit log size to avoid huge summaries (2 MiB limit)
@@ -227,28 +242,39 @@ fn run_tests(
227242
.iter()
228243
.filter(|r| matches!(r.outcome, TestOutcome::Skip(_)))
229244
.count();
245+
let num_report = results
246+
.iter()
247+
.filter(|r| matches!(r.outcome, TestOutcome::Report(_)))
248+
.count();
230249
let num_ran = num_pass + num_fail;
231250

232251
// Write GitHub Actions summary if requested
233252
if github_summary {
234-
write_github_summary(&results, num_pass, num_fail, num_skip)?;
253+
write_github_summary(&results, num_pass, num_fail, num_skip, num_report)?;
235254
}
236255

237-
let skip_msg = if num_skip > 0 {
238-
format!(" ({num_skip} skipped)")
239-
} else {
256+
let mut extra = Vec::new();
257+
if num_skip > 0 {
258+
extra.push(format!("{num_skip} skipped"));
259+
}
260+
if num_report > 0 {
261+
extra.push(format!("{num_report} reports"));
262+
}
263+
let extra_msg = if extra.is_empty() {
240264
String::new()
265+
} else {
266+
format!(" ({})", extra.join(", "))
241267
};
242268

243269
if num_fail > 0 {
244270
eprintln!("(See test artifacts at: {})", base_dir.display());
245-
println!("\nFAIL - {num_pass}/{num_ran} passed{skip_msg}");
271+
println!("\nFAIL - {num_pass}/{num_ran} passed{extra_msg}");
246272
anyhow::bail!("")
247273
} else {
248274
if keep_all {
249275
eprintln!("(See test artifacts at: {})", base_dir.display());
250276
}
251-
eprintln!("\nOK - {num_pass}/{num_ran} passed{skip_msg}");
277+
eprintln!("\nOK - {num_pass}/{num_ran} passed{extra_msg}");
252278
}
253279

254280
Ok(())
@@ -276,6 +302,8 @@ enum CliCommand {
276302
#[arg(long)]
277303
tmp_dir: PathBuf,
278304
},
305+
/// Build all registered rootfs images (requires network; run before unshare)
306+
BuildImages,
279307
}
280308

281309
impl Default for CliCommand {
@@ -295,6 +323,17 @@ struct Cli {
295323
command: Option<CliCommand>,
296324
}
297325

326+
fn build_images() -> anyhow::Result<()> {
327+
use test_cases::rootfs;
328+
329+
for (name, _) in rootfs_images() {
330+
eprint!("Building rootfs image {name}...");
331+
rootfs::build_rootfs(name)?;
332+
eprintln!(" done");
333+
}
334+
Ok(())
335+
}
336+
298337
fn main() -> anyhow::Result<()> {
299338
let cli = Cli::parse();
300339
let command = cli.command.unwrap_or_default();
@@ -307,5 +346,6 @@ fn main() -> anyhow::Result<()> {
307346
keep_all,
308347
github_summary,
309348
} => run_tests(&test_case, base_dir, keep_all, github_summary),
349+
CliCommand::BuildImages => build_images(),
310350
}
311351
}

tests/test_cases/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "test_cases"
33
edition = "2021"
44

55
[features]
6-
host = ["krun-sys"]
6+
host = ["krun-sys", "serde", "serde_json"]
77
guest = []
88

99
[lib]
@@ -14,4 +14,6 @@ krun-sys = { path = "../../krun-sys", optional = true }
1414
macros = { path = "../macros" }
1515
nix = { version = "0.29.0", features = ["socket", "ioctl"] }
1616
anyhow = "1.0.95"
17-
tempdir = "0.3.7"
17+
serde = { version = "1", features = ["derive"], optional = true }
18+
serde_json = { version = "1", optional = true }
19+
tempdir = "0.3.7"

tests/test_cases/build.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
println!("cargo:rerun-if-env-changed=IPERF_DURATION");
3+
}

tests/test_cases/src/common.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,30 @@ pub fn setup_fs_and_enter(ctx: u32, test_setup: TestSetup) -> anyhow::Result<()>
5050
}
5151
unreachable!()
5252
}
53+
54+
/// Like setup_fs_and_enter, but uses an existing rootfs directory (e.g. a Fedora rootfs with
55+
/// extra packages installed). Copies the guest-agent into it and enters the VM.
56+
pub fn setup_existing_rootfs_and_enter(
57+
ctx: u32,
58+
test_setup: TestSetup,
59+
rootfs_dir: &Path,
60+
) -> anyhow::Result<()> {
61+
anyhow::ensure!(rootfs_dir.is_dir(), "rootfs directory not found: {}", rootfs_dir.display());
62+
let path_str = CString::new(rootfs_dir.as_os_str().as_bytes()).context("CString::new")?;
63+
copy_guest_agent(rootfs_dir)?;
64+
unsafe {
65+
krun_call!(krun_set_root(ctx, path_str.as_ptr()))?;
66+
krun_call!(krun_set_workdir(ctx, c"/".as_ptr()))?;
67+
let test_case_cstr = CString::new(test_setup.test_case).context("CString::new")?;
68+
let argv = [test_case_cstr.as_ptr(), null()];
69+
let envp = [null()];
70+
krun_call!(krun_set_exec(
71+
ctx,
72+
c"/guest-agent".as_ptr(),
73+
argv.as_ptr(),
74+
envp.as_ptr(),
75+
))?;
76+
krun_call!(krun_start_enter(ctx))?;
77+
}
78+
unreachable!()
79+
}

0 commit comments

Comments
 (0)