From 45724c91418cc7a277fe8a5675471987b866aecb Mon Sep 17 00:00:00 2001 From: Sibi Prabakaran Date: Sat, 17 Feb 2024 19:53:57 +0530 Subject: [PATCH] Integration testing for pid1-rs --- CHANGELOG.md | 5 + Cargo.lock | 56 +++++++- Development.md | 3 +- pid1/Cargo.toml | 3 + pid1/etc/Dockerfile | 2 + pid1/examples/dumb_shell.rs | 29 ++++ pid1/examples/simple.rs | 6 +- pid1/justfile | 3 +- pid1/tests/sanity.rs | 269 ++++++++++++++++++++++++++++++++++++ 9 files changed, 371 insertions(+), 5 deletions(-) create mode 100644 pid1/examples/dumb_shell.rs create mode 100644 pid1/tests/sanity.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index fe311c7..5f926ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# Unreleased + +- Add integration testing +- Add examples/dumb_shell.rs code for easy testing. + # v0.1.3 - Support options for running with specific user id and group id. diff --git a/Cargo.lock b/Cargo.lock index d26641b..d093eb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,17 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "heck" version = "0.4.1" @@ -86,19 +97,26 @@ name = "pid1" version = "0.1.1" dependencies = [ "nix", + "rand", "signal-hook", "thiserror", ] [[package]] name = "pid1-exe" -version = "0.1.2" +version = "0.1.3" dependencies = [ "clap", "pid1", "signal-hook", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.67" @@ -117,6 +135,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "signal-hook" version = "0.3.17" @@ -172,3 +220,9 @@ name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/Development.md b/Development.md index 755503c..d640817 100644 --- a/Development.md +++ b/Development.md @@ -10,7 +10,8 @@ used for testing this library. - zombie.rs: Creates zombie process - sigterm_handler.rs: Program which has SIGTERM handler and exits on receiving it. - sigterm_loop.rs: A buggy program which doesn't exit on SIGTERM - +- dumb_shell.rs: Alternative shell that you can use to test your + program since bash does reaping of process. ## Environment setup diff --git a/pid1/Cargo.toml b/pid1/Cargo.toml index d4f3900..a2537f0 100644 --- a/pid1/Cargo.toml +++ b/pid1/Cargo.toml @@ -16,3 +16,6 @@ categories = ["command-line-utilities"] nix = { version = "0.27.1", features = ["process", "signal"] } signal-hook = "0.3.17" thiserror = "1" + +[dev-dependencies] +rand = "0.8.5" diff --git a/pid1/etc/Dockerfile b/pid1/etc/Dockerfile index c360814..d65b6b5 100644 --- a/pid1/etc/Dockerfile +++ b/pid1/etc/Dockerfile @@ -8,4 +8,6 @@ COPY sigterm_handler /usr/bin/sigterm_handler COPY sigterm_loop /usr/bin/sigterm_loop +COPY dumb_shell /usr/bin/dumb_shell + CMD ["/simple"] diff --git a/pid1/examples/dumb_shell.rs b/pid1/examples/dumb_shell.rs new file mode 100644 index 0000000..ae80b73 --- /dev/null +++ b/pid1/examples/dumb_shell.rs @@ -0,0 +1,29 @@ +use std::{ + ffi::OsString, + io::{self, BufRead, Write}, + process::Command, + str::FromStr, +}; + +fn main() -> Result<(), Box> { + let stdin = io::stdin(); + loop { + print!("$ "); + io::stdout().flush().unwrap(); + for line in stdin.lock().lines() { + let args: Vec = line + .unwrap() + .split(' ') + .map(|item| OsString::from_str(item).unwrap()) + .collect(); + let exe = args.get(0).unwrap(); + if exe == "exit" { + std::process::exit(0); + } + let output = Command::new(exe).args(&args[1..]).output().unwrap(); + print!("{}", String::from_utf8(output.stdout).unwrap()); + print!("$ "); + io::stdout().flush().unwrap(); + } + } +} diff --git a/pid1/examples/simple.rs b/pid1/examples/simple.rs index a012e95..9724cdf 100644 --- a/pid1/examples/simple.rs +++ b/pid1/examples/simple.rs @@ -18,8 +18,10 @@ fn main() -> Result<(), Box> { } if args.len() > 1 { - println!("Going to sleep 500 seconds"); - std::thread::sleep(std::time::Duration::from_secs(500)); + let duration = &args[2]; + let duration = duration.parse().expect("Expected int value"); + println!("Going to sleep {duration} seconds"); + std::thread::sleep(std::time::Duration::from_secs(duration)); } Ok(()) diff --git a/pid1/justfile b/pid1/justfile index 7a20c5a..f57c5f9 100644 --- a/pid1/justfile +++ b/pid1/justfile @@ -18,7 +18,7 @@ build-image: # Run test image run-image: docker rm pid1rs || exit 0 - docker run --name pid1rs -t pid1rstest /simple --sleep + docker run --name pid1rs -t pid1rstest /simple --sleep 20 # Run zombie in the container run-zombie: @@ -28,6 +28,7 @@ run-zombie: test: build-image docker rm pid1rs || exit 0 docker run --name pid1rs -t pid1rstest + cargo test # Run SIGTERM test sigterm-test: diff --git a/pid1/tests/sanity.rs b/pid1/tests/sanity.rs new file mode 100644 index 0000000..570820e --- /dev/null +++ b/pid1/tests/sanity.rs @@ -0,0 +1,269 @@ +use rand::distributions::{Alphanumeric, DistString}; +use std::{process::Command, time::Duration}; + +#[derive(Clone)] +struct Container { + name: String, + image: String, +} + +impl Container { + pub fn new(image: String) -> Self { + let name = Alphanumeric.sample_string(&mut rand::thread_rng(), 5); + Container { name, image } + } + + pub fn run(&self) -> Result { + Command::new("docker") + .args([ + "run", + "--name", + self.name.as_str(), + "-t", + self.image.as_str(), + ]) + .output() + } + + pub fn plain_run(&self, args: &[&str]) -> Result { + Command::new("docker").args(args).output() + } +} + +impl Drop for Container { + fn drop(&mut self) { + let status = Command::new("docker") + .args(["rm", self.name.as_str()]) + .spawn(); + if let Err(status) = status { + eprintln!("Error dropping container {status}"); + } + } +} + +#[test] +fn sanity_test() { + let container = Container::new("pid1rstest".to_owned()); + let output = container.run().unwrap(); + assert!(output.status.success(), "Process exited successfully"); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains(&"pid1-rs: Process running as PID 1"), + "One process runs as pid1", + ); + assert!( + stdout.contains(&"pid1-rs: Process not running as Pid 1"), + "Child process not running as pid1", + ); +} + +#[test] +fn reaps_zombie_process() { + let container = Container::new("pid1rstest".to_owned()); + let (output, zombie_output) = std::thread::scope(|s| { + let result = s.spawn(|| { + let container = container.clone(); + let output = container.plain_run(&[ + "run", + "--name", + container.name.as_str(), + "-t", + container.image.as_str(), + "/simple", + "--sleep", + "20", + ]); + output.unwrap() + }); + + let zombie_result = s.spawn(|| { + std::thread::sleep(Duration::from_secs(2)); + let container = container.clone(); + let zombie_output = container + .plain_run(&["exec", "-t", container.name.as_str(), "zombie"]) + .unwrap(); + zombie_output + }); + + (result.join().unwrap(), zombie_result.join().unwrap()) + }); + + let stdout = String::from_utf8(output.stdout).unwrap(); + println!("foo: {stdout}"); + + assert!(output.status.success(), "Process exited successfully"); + + assert!( + zombie_output.status.success(), + "Process exited successfully" + ); + + assert!( + stdout.contains(&"pid1-rs: Reaped PID"), + "Successfully Reaped process", + ); +} + +#[test] +fn child_process_status_code() { + let container = Container::new("pid1rstest".to_owned()); + let (output, exec_process) = std::thread::scope(|s| { + let result = s.spawn(|| { + let container = container.clone(); + let output = container.plain_run(&[ + "run", + "--name", + container.name.as_str(), + "-t", + container.image.as_str(), + "/simple", + "--sleep", + "20", + ]); + output.unwrap() + }); + + let kill_result = s.spawn(|| { + std::thread::sleep(Duration::from_secs(2)); + let container = container.clone(); + let ps_output = container + .plain_run(&[ + "exec", + "-t", + container.name.as_str(), + "ps", + "-o", + "pid", + "a", + ]) + .unwrap(); + let ps_output = String::from_utf8(ps_output.stdout).unwrap(); + let ps_output = ps_output.lines().skip(2).next().unwrap().trim(); + + println!("Child process: {ps_output}"); + + container + .plain_run(&[ + "exec", + "-t", + container.name.as_str(), + "kill", + "-12", + ps_output, + ]) + .unwrap() + }); + + (result.join().unwrap(), kill_result.join().unwrap()) + }); + + assert!(!output.status.success(), "Pid1 process exited"); + assert_eq!( + output.status.code().unwrap(), + 140, + "Exit code is 140 (128 + 12)" + ); + assert!(exec_process.status.success(), "Killed process successfully"); +} + +#[test] +fn sigterm_handling() { + let container = Container::new("pid1rstest".to_owned()); + let (output, exec_process) = std::thread::scope(|s| { + let result = s.spawn(|| { + let container = container.clone(); + let output = container.plain_run(&[ + "run", + "--name", + container.name.as_str(), + "-t", + container.image.as_str(), + "sigterm_handler", + ]); + output.unwrap() + }); + + let kill_result = s.spawn(|| { + std::thread::sleep(Duration::from_secs(2)); + let container = container.clone(); + let ps_output = container + .plain_run(&[ + "exec", + "-t", + container.name.as_str(), + "ps", + "-o", + "pid", + "a", + ]) + .unwrap(); + let ps_output = String::from_utf8(ps_output.stdout).unwrap(); + let ps_output = ps_output.lines().skip(2).next().unwrap().trim(); + + println!("Child process: {ps_output}"); + + container + .plain_run(&["exec", "-t", container.name.as_str(), "kill", "1"]) + .unwrap() + }); + + (result.join().unwrap(), kill_result.join().unwrap()) + }); + + assert!(output.status.success(), "Pid1 exited successfully"); + assert!(exec_process.status.success(), "Killed process successfully"); + + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("App got SIGTERM 15, going to exit"), + "Application got SIGTERM from pid1" + ); +} + +#[test] +fn sigterm_ignore() { + let container = Container::new("pid1rstest".to_owned()); + let (output, exec_process) = std::thread::scope(|s| { + let result = s.spawn(|| { + let container = container.clone(); + let output = container.plain_run(&[ + "run", + "--name", + container.name.as_str(), + "-t", + container.image.as_str(), + "sigterm_loop", + ]); + output.unwrap() + }); + + let kill_result = s.spawn(|| { + std::thread::sleep(Duration::from_secs(2)); + let container = container.clone(); + container + .plain_run(&["exec", "-t", container.name.as_str(), "kill", "1"]) + .unwrap() + }); + + (result.join().unwrap(), kill_result.join().unwrap()) + }); + + assert!(!output.status.success(), "Pid1 exited unsuccessfully"); + assert_eq!( + output.status.code().unwrap(), + 137, + "pid1 exited with 9 (137 - 128) status code" + ); + assert!(exec_process.status.success(), "Killed process successfully"); + + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("This APP cannot be killed by SIGTERM (15)"), + "Application ignores SIGTERM" + ); + + assert!( + stdout.contains("App got SIGTERM 15, but *NOT* going to exit"), + "Application got SIGTERM" + ); +}