diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 20cae4c2..bcb45db4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ defaults: shell: bash env: - CACHE_KEY: 2 + CACHE_KEY: 3 jobs: all: @@ -72,7 +72,10 @@ jobs: - name: Install just run: | - if ! which just; then + if ! which just + then + TEMPDIR=$(mktemp -d) + cd $TEMPDIR git clone https://github.com/casey/just cd just git checkout v0.9.3 diff --git a/.gitignore b/.gitignore index ca98cd96..e9e21997 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ /target/ -Cargo.lock +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml index 137bdfa8..2b88e432 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,9 @@ homepage = "https://github.com/soenkehahn/cradle" keywords = ["child", "child-process", "command", "process", "shell"] categories = ["filesystem", "os"] +[workspace] +members = [".", "memory-test"] + [dependencies] rustversion = "1.0.4" diff --git a/justfile b/justfile index 0db31358..1d50a554 100644 --- a/justfile +++ b/justfile @@ -1,10 +1,10 @@ ci: test build doc clippy fmt context-integration-tests run-examples forbidden-words render-readme-check build: - cargo build --all-targets --all-features + cargo build --all-targets --all-features --workspace test +pattern="": - cargo test --all {{ pattern }} + cargo test {{ pattern }} test-lib-fast +pattern="": cargo test --lib {{ pattern }} @@ -13,10 +13,10 @@ context-integration-tests: cargo run --features "test_executables" --bin context_integration_tests doc +args="": - cargo doc --all {{args}} + cargo doc --workspace {{args}} clippy: - cargo clippy --all-targets --all-features + cargo clippy --all-targets --all-features --workspace fmt: cargo fmt --all -- --check diff --git a/memory-test/Cargo.lock b/memory-test/Cargo.lock new file mode 100644 index 00000000..918f6c05 --- /dev/null +++ b/memory-test/Cargo.lock @@ -0,0 +1,31 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486" + +[[package]] +name = "cradle" +version = "0.0.17" +dependencies = [ + "rustversion", +] + +[[package]] +name = "memory-test" +version = "0.0.0" +dependencies = [ + "anyhow", + "cradle", + "rustversion", +] + +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" diff --git a/memory-test/Cargo.toml b/memory-test/Cargo.toml new file mode 100644 index 00000000..7b10f4ac --- /dev/null +++ b/memory-test/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "memory-test" +version = "0.0.0" +edition = "2018" + +[dependencies] +anyhow = "1.0.42" +cradle = { path = ".." } +rustversion = "1.0.5" diff --git a/memory-test/src/bin/cradle_user.rs b/memory-test/src/bin/cradle_user.rs new file mode 100644 index 00000000..33259980 --- /dev/null +++ b/memory-test/src/bin/cradle_user.rs @@ -0,0 +1,8 @@ +use cradle::prelude::*; + +fn main() { + let mut args = std::env::args(); + let bytes: usize = args.nth(1).unwrap().parse().unwrap(); + eprintln!("consuming {} KiB", bytes / 2_usize.pow(10)); + cmd_unit!("./target/release/produce_bytes", bytes.to_string()); +} diff --git a/memory-test/src/bin/produce_bytes.rs b/memory-test/src/bin/produce_bytes.rs new file mode 100644 index 00000000..60c9ad6f --- /dev/null +++ b/memory-test/src/bin/produce_bytes.rs @@ -0,0 +1,17 @@ +use anyhow::Result; +use std::io::{stdout, Write}; + +fn main() -> Result<()> { + let mut args = std::env::args(); + let mut bytes: usize = args.nth(1).unwrap().parse()?; + eprintln!("producing {} KiB", bytes / 2_usize.pow(10)); + let buffer = &[b'x'; 1024]; + let mut stdout = stdout(); + while bytes > 0 { + let chunk_size = bytes.min(1024); + stdout.write_all(&buffer[..chunk_size])?; + bytes -= chunk_size; + } + stdout.flush()?; + Ok(()) +} diff --git a/memory-test/src/bin/run_test.rs b/memory-test/src/bin/run_test.rs new file mode 100644 index 00000000..cc7a51a2 --- /dev/null +++ b/memory-test/src/bin/run_test.rs @@ -0,0 +1,56 @@ +use anyhow::Result; +use cradle::prelude::*; +use std::process::{Command, Stdio}; + +fn from_mib(mebibytes: usize) -> usize { + mebibytes * 2_usize.pow(20) +} + +fn main() -> Result<()> { + Split("cargo build --release").run_unit(); + let bytes = from_mib(64); + let memory_consumption = measure_memory_consumption(bytes)?; + let allowed_memory_consumption = from_mib(16); + assert!( + memory_consumption < allowed_memory_consumption, + "Maximum resident set size: {}, allowed upper limit: {}", + memory_consumption, + allowed_memory_consumption + ); + Ok(()) +} + +fn measure_memory_consumption(bytes: usize) -> Result { + let output = Command::new("/usr/bin/time") + .arg("-v") + .arg("./target/release/cradle_user") + .arg(bytes.to_string()) + .stdout(Stdio::null()) + .output()?; + let stderr = String::from_utf8(output.stderr)?; + eprintln!("{}", stderr); + if !output.status.success() { + panic!("running 'cradle_user' failed"); + } + let memory_size_prefix = "Maximum resident set size (kbytes): "; + let kibibytes: usize = strip_prefix( + stderr + .lines() + .map(|line| line.trim()) + .find(|line| line.starts_with(memory_size_prefix)) + .unwrap(), + memory_size_prefix, + ) + .parse()?; + let bytes = kibibytes * 1024; + Ok(bytes) +} + +#[rustversion::attr(since(1.48), allow(clippy::manual_strip))] +fn strip_prefix<'a>(string: &'a str, prefix: &'a str) -> &'a str { + if string.starts_with(prefix) { + &string[prefix.len()..] + } else { + panic!("{} doesn't start with {}", string, prefix); + } +} diff --git a/src/collected_output.rs b/src/collected_output.rs index e93a5d9f..0a8cf876 100644 --- a/src/collected_output.rs +++ b/src/collected_output.rs @@ -8,7 +8,7 @@ use std::{ #[derive(Debug)] pub(crate) struct Waiter { stdin: JoinHandle>, - stdout: JoinHandle>>, + stdout: JoinHandle>>>, stderr: JoinHandle>>, } @@ -30,24 +30,30 @@ impl Waiter { Ok(()) }); let mut context_clone = context.clone(); - let relay_stdout = config.relay_stdout; - let stdout_join_handle = thread::spawn(move || -> io::Result> { - let mut collected_stdout = Vec::new(); + let capture_stdout = config.capture_stdout; + let stdout_join_handle = thread::spawn(move || -> io::Result>> { + let mut collected_stdout = if capture_stdout { + Some(Vec::new()) + } else { + None + }; let buffer = &mut [0; 256]; loop { let length = child_stdout.read(buffer)?; if (length) == 0 { break; } - if relay_stdout { + if let Some(collected_stdout) = &mut collected_stdout { + collected_stdout.extend(&buffer[..length]); + } + if !capture_stdout { context_clone.stdout.write_all(&buffer[..length])?; } - collected_stdout.extend(&buffer[..length]); } Ok(collected_stdout) }); let mut context_clone = context.clone(); - let relay_stderr = config.relay_stderr; + let capture_stderr = config.capture_stderr; let stderr_join_handle = thread::spawn(move || -> io::Result> { let mut collected_stderr = Vec::new(); let buffer = &mut [0; 256]; @@ -56,10 +62,10 @@ impl Waiter { if (length) == 0 { break; } - if relay_stderr { + collected_stderr.extend(&buffer[..length]); + if !capture_stderr { context_clone.stderr.write_all(&buffer[..length])?; } - collected_stderr.extend(&buffer[..length]); } Ok(collected_stderr) }); @@ -89,6 +95,6 @@ impl Waiter { #[derive(Debug)] pub(crate) struct CollectedOutput { - pub(crate) stdout: Vec, + pub(crate) stdout: Option>, pub(crate) stderr: Vec, } diff --git a/src/config.rs b/src/config.rs index 8bc7c184..46b3dff3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,15 +4,15 @@ use std::{ffi::OsString, path::PathBuf, sync::Arc}; #[doc(hidden)] #[rustversion::attr(since(1.48), allow(clippy::rc_buffer))] -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Config { pub(crate) arguments: Vec, pub(crate) log_command: bool, pub(crate) working_directory: Option, pub(crate) added_environment_variables: Vec<(OsString, OsString)>, pub(crate) stdin: Arc>, - pub(crate) relay_stdout: bool, - pub(crate) relay_stderr: bool, + pub(crate) capture_stdout: bool, + pub(crate) capture_stderr: bool, pub(crate) error_on_non_zero_exit_code: bool, } @@ -45,8 +45,8 @@ impl Default for Config { working_directory: None, added_environment_variables: Vec::new(), stdin: Arc::new(Vec::new()), - relay_stdout: true, - relay_stderr: true, + capture_stdout: false, + capture_stderr: false, error_on_non_zero_exit_code: true, } } diff --git a/src/error.rs b/src/error.rs index be242c2e..a3edb442 100644 --- a/src/error.rs +++ b/src/error.rs @@ -26,6 +26,10 @@ pub enum Error { full_command: String, source: Arc, }, + Internal { + full_command: String, + config: Config, + }, } impl Error { @@ -35,6 +39,13 @@ impl Error { source: Arc::new(error), } } + + pub(crate) fn internal(config: &Config) -> Error { + Error::Internal { + full_command: config.full_command(), + config: config.clone(), + } + } } #[doc(hidden)] @@ -48,15 +59,16 @@ pub fn panic_on_error(result: Result) -> T { impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Error::*; match self { - Error::NoArgumentsGiven => write!(f, "no arguments given"), - Error::FileNotFoundWhenExecuting { executable, .. } => write!( + NoArgumentsGiven => write!(f, "no arguments given"), + FileNotFoundWhenExecuting { executable, .. } => write!( f, "File not found error when executing '{}'", executable.to_string_lossy() ), - Error::CommandIoError { message, .. } => write!(f, "{}", message), - Error::NonZeroExitCode { + CommandIoError { message, .. } => write!(f, "{}", message), + NonZeroExitCode { full_command, exit_status, } => { @@ -70,24 +82,35 @@ impl Display for Error { write!(f, "{}:\n exited with {}", full_command, exit_status) } } - Error::InvalidUtf8ToStdout { full_command, .. } => { + InvalidUtf8ToStdout { full_command, .. } => { write!(f, "{}:\n invalid utf-8 written to stdout", full_command) } - Error::InvalidUtf8ToStderr { full_command, .. } => { + InvalidUtf8ToStderr { full_command, .. } => { write!(f, "{}:\n invalid utf-8 written to stderr", full_command) } + Internal { .. } => { + let snippets = vec![ + "Congratulations, you've found a bug in cradle! :/", + "Please, open an issue on https://github.com/soenkehahn/cradle/issues", + "with the following information:", + ]; + writeln!(f, "{}\n{:#?}", snippets.join(" "), self) + } } } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use Error::*; match self { - Error::FileNotFoundWhenExecuting { source, .. } - | Error::CommandIoError { source, .. } => Some(&**source), - Error::InvalidUtf8ToStdout { source, .. } - | Error::InvalidUtf8ToStderr { source, .. } => Some(&**source), - Error::NoArgumentsGiven | Error::NonZeroExitCode { .. } => None, + FileNotFoundWhenExecuting { source, .. } | CommandIoError { source, .. } => { + Some(&**source) + } + InvalidUtf8ToStdout { source, .. } | InvalidUtf8ToStderr { source, .. } => { + Some(&**source) + } + NoArgumentsGiven | NonZeroExitCode { .. } | Internal { .. } => None, } } } diff --git a/src/lib.rs b/src/lib.rs index 8956f0d7..d2728d80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -322,7 +322,7 @@ where #[doc(hidden)] #[derive(Clone, Debug)] pub struct RunResult { - stdout: Vec, + stdout: Option>, stderr: Vec, exit_status: ExitStatus, } diff --git a/src/output.rs b/src/output.rs index efa25255..3ceb0e18 100644 --- a/src/output.rs +++ b/src/output.rs @@ -165,13 +165,14 @@ pub struct StdoutUntrimmed(pub String); impl Output for StdoutUntrimmed { #[doc(hidden)] fn configure(config: &mut Config) { - config.relay_stdout = false; + config.capture_stdout = true; } #[doc(hidden)] fn from_run_result(config: &Config, result: Result) -> Result { let result = result?; - Ok(StdoutUntrimmed(String::from_utf8(result.stdout).map_err( + let stdout = result.stdout.ok_or_else(|| Error::internal(config))?; + Ok(StdoutUntrimmed(String::from_utf8(stdout).map_err( |source| Error::InvalidUtf8ToStdout { full_command: config.full_command(), source: Arc::new(source), @@ -203,7 +204,7 @@ pub struct Stderr(pub String); impl Output for Stderr { #[doc(hidden)] fn configure(config: &mut Config) { - config.relay_stderr = false; + config.capture_stderr = true; } #[doc(hidden)] diff --git a/tests/integration.rs b/tests/integration.rs index 316872e1..6ac20f33 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -313,3 +313,11 @@ mod run_interface { ); } } + +#[cfg(target_os = "linux")] +#[test] +fn memory_test() { + use cradle::prelude::*; + cmd_unit!(%"cargo build -p memory-test --release"); + cmd_unit!(%"cargo run -p memory-test --bin run_test"); +}