#[cfg(test)]
pub(crate) mod runner {
use std::{
io::Read as _,
process::{Command, ExitStatus},
};
pub(crate) struct StressTest {
nocapture: bool,
max_memory_bytes: u64,
max_runtime_seconds: u64,
}
impl StressTest {
pub(crate) fn new() -> Self {
let nocapture = std::env::args().any(|arg| arg == "--nocapture");
let max_memory_bytes = 1 << 30; let max_runtime_seconds = 10;
Self {
nocapture,
max_memory_bytes,
max_runtime_seconds,
}
}
pub(crate) fn with_nocapture(mut self, nocapture: bool) -> Self {
self.nocapture = nocapture;
self
}
pub(crate) fn with_max_memory(mut self, max_memory_bytes: u64) -> Self {
self.max_memory_bytes = max_memory_bytes;
self
}
pub(crate) fn with_max_runtime(mut self, max_runtime_seconds: u64) -> Self {
self.max_runtime_seconds = max_runtime_seconds;
self
}
pub(crate) fn run(&self, test_name: &str) {
self.run_with_args(test_name, &[]);
}
pub(crate) fn run_with_args(&self, test_name: &str, args: &[&str]) {
let pkg_name = "midnight-storage";
let bin_name = "stress";
println!("{test_name}: building stress-test runner ...");
let build_succeeded = Command::new("cargo")
.arg("build")
.arg("-p")
.arg(pkg_name)
.arg("--all-features")
.arg("--release")
.arg("--quiet")
.args(["--bin", bin_name])
.spawn()
.unwrap()
.wait()
.unwrap()
.success();
assert!(build_succeeded, "failed to build stress test runner");
println!("{test_name}: done building, running stress test ...");
let mut command = Command::new("cargo");
command
.arg("run")
.arg("-p")
.arg(pkg_name)
.arg("--all-features")
.arg("--release")
.arg("--quiet")
.args(["--bin", bin_name])
.args(["--", test_name])
.args(args);
let mut maybe_reader = None;
if !self.nocapture {
let (reader, writer) = os_pipe::pipe().unwrap();
maybe_reader = Some(reader);
let writer_clone = writer.try_clone().unwrap();
command.stdout(writer).stderr(writer_clone);
}
let mut child = command
.spawn()
.unwrap_or_else(|e| panic!("failed to run stress tester: {e:?}"));
drop(command);
let mut system = sysinfo::System::new_all();
let pid = sysinfo::Pid::from_u32(child.id());
enum Outcome {
Exit(ExitStatus),
OutOfMemory,
OutOfTime,
}
let outcome = loop {
std::thread::sleep(std::time::Duration::from_millis(100));
if let Some(status) = child.try_wait().unwrap() {
break Outcome::Exit(status);
}
let remove_dead = true;
system.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), remove_dead);
let process = system.process(pid).unwrap();
let memory_usage_bytes = process.memory();
if memory_usage_bytes > self.max_memory_bytes {
assert!(process.kill());
break Outcome::OutOfMemory;
}
let runtime_seconds = process.run_time();
if runtime_seconds > self.max_runtime_seconds {
assert!(process.kill());
break Outcome::OutOfTime;
}
};
let mut output = String::new();
if !self.nocapture {
maybe_reader
.as_ref()
.unwrap()
.read_to_string(&mut output)
.unwrap();
print!("{}", output.clone());
}
match outcome {
Outcome::Exit(status) => {
if !status.success() {
let msg = if self.nocapture {
String::from("stress test failed, but its output was not captured")
} else {
format!("stress test failed: {}", output)
};
panic!("{msg}")
}
}
Outcome::OutOfMemory => {
panic!("memory usage exceeded limit, killing stress test...")
}
Outcome::OutOfTime => panic!("runtime exceeded limit, killing stress test..."),
}
}
}
}
pub mod stress_tests {
pub fn run_pass() {}
pub fn run_fail_oom() {
let mut string = String::from("uh oh");
loop {
string.push_str(&string.clone());
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
pub fn run_fail_timeout() {
loop {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
pub fn run_fail_capture_interleave() {
println!("1");
eprintln!("2");
println!("3");
eprintln!("4");
panic!();
}
pub fn run_fail_match_output() {
panic!("billions of bilious blue blistering barnacles!");
}
}
#[cfg(test)]
mod tests {
use super::runner::StressTest;
#[test]
fn run_pass() {
StressTest::new().run("stress_test::stress_tests::run_pass");
}
#[test]
#[should_panic = "memory usage exceeded limit"]
fn run_fail_oom() {
let one_gb = 1 << 30;
StressTest::new()
.with_max_memory(one_gb)
.run("stress_test::stress_tests::run_fail_oom");
}
#[test]
#[should_panic = "runtime exceeded limit"]
fn run_fail_timeout() {
StressTest::new()
.with_max_runtime(5)
.run("stress_test::stress_tests::run_fail_timeout");
}
#[test]
#[ignore = "testception()"]
fn run_fail_capture_interleave() {
StressTest::new()
.with_nocapture(false)
.run("stress_test::stress_tests::run_fail_capture_interleave");
}
#[test]
fn testception() {
use std::process::Command;
let pkg_name = env!("CARGO_PKG_NAME");
let output = Command::new("cargo")
.arg("test")
.arg("-p")
.arg(pkg_name)
.args(["--features", "stress-test"])
.arg("stress_test::tests::run_fail_capture_interleave")
.args(["--", "--include-ignored"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let canary = "1\n2\n3\n4\n";
if !stdout.contains(canary) {
panic!(
"subprocess output doesn't contain the canary:\n<stdout>\n{}</stdout>\n<stderr>\n{}</stderr>",
stdout, stderr
);
}
}
#[test]
#[should_panic = "billions of bilious blue blistering barnacles!"]
fn run_fail_match_output() {
StressTest::new()
.with_nocapture(false)
.run("stress_test::stress_tests::run_fail_match_output");
}
}