diff --git a/src/lib.rs b/src/lib.rs index e7adb5b..6182c68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ use crate::recorder::{hole_recorder::run_against_tests, Recorder}; use crate::test_checker::executable_mock; use crate::test_spec::yaml::write_yaml; use crate::test_spec::Tests; -use crate::tracer::stdio_redirecting::CaptureStderr; +use crate::tracer::stdio_redirecting::Capture; use crate::tracer::Tracer; use std::collections::HashMap; use std::path::Path; @@ -107,6 +107,7 @@ mod run_main { &context, executable_mock::Config { stdout: b"foo".to_vec(), + stderr: vec![], exitcode: 0, }, )?; @@ -140,7 +141,10 @@ fn print_recorded_test(context: &Context, program: &Path) -> R { program, vec![], HashMap::new(), - CaptureStderr::NoCapture, + Capture { + stdout: false, + stderr: false, + }, Recorder::empty(), )?; write_yaml(&mut *context.stdout(), &Tests::new(vec![test]).serialize()?)?; diff --git a/src/recorder/mod.rs b/src/recorder/mod.rs index 602c397..db7cf88 100644 --- a/src/recorder/mod.rs +++ b/src/recorder/mod.rs @@ -65,6 +65,7 @@ impl SyscallMock for Recorder { self.test.steps.push_back(Step { command_matcher: CommandMatcher::ExactMatch(command), stdout: vec![], + stderr: vec![], exitcode, }); } diff --git a/src/recorder/result.rs b/src/recorder/result.rs index 2bdb5f6..7a932b4 100644 --- a/src/recorder/result.rs +++ b/src/recorder/result.rs @@ -5,7 +5,7 @@ use crate::test_checker::{ TestChecker, }; use crate::test_spec::{yaml::write_yaml, Test, Tests}; -use crate::tracer::stdio_redirecting::CaptureStderr; +use crate::tracer::stdio_redirecting::Capture; use crate::tracer::Tracer; use crate::{ExitCode, R}; use std::fs::OpenOptions; @@ -128,10 +128,9 @@ fn run_against_test( program, test.arguments.clone(), test.env.clone(), - if test.stderr.is_some() { - CaptureStderr::Capture - } else { - CaptureStderr::NoCapture + Capture { + stdout: test.stdout.is_some(), + stderr: test.stderr.is_some(), }, $syscall_mock, ) diff --git a/src/test_checker/executable_mock.rs b/src/test_checker/executable_mock.rs index e07b6fe..a7e315c 100644 --- a/src/test_checker/executable_mock.rs +++ b/src/test_checker/executable_mock.rs @@ -9,6 +9,7 @@ use std::path::Path; #[derive(Debug, Serialize, Deserialize)] pub struct Config { pub stdout: Vec, + pub stderr: Vec, pub exitcode: i32, } @@ -35,6 +36,7 @@ pub fn create_mock_executable(context: &Context, config: Config) -> R> { pub fn run(context: &Context, executable_mock_path: &Path) -> R { let config: Config = deserialize(&skip_hashbang_line(fs::read(executable_mock_path)?))?; context.stdout().write_all(&config.stdout)?; + context.stderr().write_all(&config.stderr)?; Ok(ExitCode(config.exitcode)) } @@ -60,6 +62,7 @@ mod create_mock_executable { &Context::new_mock(), Config { stdout: b"foo".to_vec(), + stderr: vec![], exitcode: 0, }, )?)?; @@ -68,12 +71,28 @@ mod create_mock_executable { Ok(()) } + #[test] + fn renders_an_executable_that_outputs_the_given_stderr() -> R<()> { + let mock_executable = TempFile::write_temp_script(&create_mock_executable( + &Context::new_mock(), + Config { + stdout: vec![], + stderr: b"foo".to_vec(), + exitcode: 0, + }, + )?)?; + let output = Command::new(mock_executable.path()).output()?; + assert_eq!(output.stderr, b"foo"); + Ok(()) + } + #[test] fn renders_an_executable_that_exits_with_the_given_exitcode() -> R<()> { let mock_executable = TempFile::write_temp_script(&create_mock_executable( &Context::new_mock(), Config { stdout: b"foo".to_vec(), + stderr: vec![], exitcode: 42, }, )?)?; @@ -92,6 +111,7 @@ mod create_mock_executable { &context, Config { stdout: vec![], + stderr: vec![], exitcode: 42, }, ), diff --git a/src/test_checker/mod.rs b/src/test_checker/mod.rs index 08ed203..ff20ea0 100644 --- a/src/test_checker/mod.rs +++ b/src/test_checker/mod.rs @@ -4,7 +4,7 @@ pub mod executable_mock; use crate::context::Context; use crate::test_spec; use crate::test_spec::Test; -use crate::tracer::stdio_redirecting::Redirector; +use crate::tracer::stdio_redirecting::{Redirect, Redirector}; use crate::tracer::{tracee_memory, SyscallMock}; use crate::utils::short_temp_files::ShortTempFile; use crate::R; @@ -39,6 +39,7 @@ impl TestChecker { fn allow_failing_scripts_to_continue() -> executable_mock::Config { executable_mock::Config { stdout: vec![], + stderr: vec![], exitcode: 0, } } @@ -54,6 +55,7 @@ impl TestChecker { } executable_mock::Config { stdout: next_test_step.stdout, + stderr: next_test_step.stderr, exitcode: next_test_step.exitcode, } } @@ -70,6 +72,28 @@ impl TestChecker { Ok(path) } + fn check_expected_output_stream(&mut self, redirect: &Redirect, expected: Vec) -> R<()> { + match redirect.captured()? { + None => panic!( + "scriptkeeper bug: {} expected, but not captured", + redirect.stream_type + ), + Some(captured) => { + if captured != expected { + self.register_error(format!( + " expected output to {}: {:?}\ + \n received output to {}: {:?}\n", + redirect.stream_type, + String::from_utf8_lossy(&expected).as_ref(), + redirect.stream_type, + String::from_utf8_lossy(&captured).as_ref(), + )); + } + } + } + Ok(()) + } + fn register_step_error(&mut self, expected: &str, received: &str) { self.register_error(format!( " expected: {}\n received: {}\n", @@ -163,20 +187,11 @@ impl SyscallMock for TestChecker { &format!("", exitcode), ); } + if let Some(expected_stdout) = &self.test.stdout { + self.check_expected_output_stream(&redirector.stdout, expected_stdout.clone())?; + } if let Some(expected_stderr) = &self.test.stderr { - match redirector.stderr.captured()? { - None => panic!("scriptkeeper bug: stderr expected, but not captured"), - Some(captured_stderr) => { - if &captured_stderr != expected_stderr { - self.register_error(format!( - " expected output to stderr: {:?}\ - \n received output to stderr: {:?}\n", - String::from_utf8_lossy(&expected_stderr).as_ref(), - String::from_utf8_lossy(&captured_stderr).as_ref(), - )); - } - } - } + self.check_expected_output_stream(&redirector.stderr, expected_stderr.clone())?; } Ok(self.result) } diff --git a/src/test_spec/mod.rs b/src/test_spec/mod.rs index 9f9e13f..425ad46 100644 --- a/src/test_spec/mod.rs +++ b/src/test_spec/mod.rs @@ -24,6 +24,7 @@ use yaml_rust::{yaml::Hash, Yaml, YamlLoader}; pub struct Step { pub command_matcher: CommandMatcher, pub stdout: Vec, + pub stderr: Vec, pub exitcode: i32, } @@ -32,6 +33,7 @@ impl Step { Step { command_matcher, stdout: vec![], + stderr: vec![], exitcode: 0, } } @@ -49,7 +51,14 @@ impl Step { fn add_stdout(&mut self, object: &Hash) -> R<()> { if let Ok(stdout) = object.expect_field("stdout") { - self.stdout = stdout.expect_str()?.as_bytes().to_vec(); + self.stdout = stdout.expect_bytes()?; + } + Ok(()) + } + + fn add_stderr(&mut self, object: &Hash) -> R<()> { + if let Ok(stderr) = object.expect_field("stderr") { + self.stderr = stderr.expect_bytes()?; } Ok(()) } @@ -58,7 +67,10 @@ impl Step { match yaml { Yaml::String(string) => Step::from_string(string), Yaml::Hash(object) => { - check_keys(&["command", "stdout", "exitcode", "regex"], object)?; + check_keys( + &["command", "stdout", "stderr", "exitcode", "regex"], + object, + )?; let mut step = match (object.expect_field("command"), object.expect_field("regex")) { (Ok(command_field), Err(_)) => Step::from_string(command_field.expect_str()?)?, @@ -68,6 +80,7 @@ impl Step { _ => Err("please provide either a 'command' or 'regex' field but not both")?, }; step.add_stdout(object)?; + step.add_stderr(object)?; step.add_exitcode(object)?; Ok(step) } @@ -169,6 +182,15 @@ mod parse_step { Ok(()) } + #[test] + fn allows_to_specify_stderr() -> R<()> { + assert_eq!( + test_parse_step(r#"{command: "foo", stderr: "bar"}"#)?.stderr, + b"bar".to_vec(), + ); + Ok(()) + } + mod exitcode { use super::*; @@ -196,6 +218,7 @@ pub struct Test { pub arguments: Vec, pub env: HashMap, pub cwd: Option, + pub stdout: Option>, pub stderr: Option>, pub exitcode: Option, pub mocked_files: Vec, @@ -214,9 +237,10 @@ impl Test { arguments: vec![], env: HashMap::new(), cwd: None, + stdout: None, + stderr: None, exitcode: None, mocked_files: vec![], - stderr: None, } } @@ -281,9 +305,16 @@ impl Test { Ok(()) } + fn add_stdout(&mut self, object: &Hash) -> R<()> { + if let Ok(stdout) = object.expect_field("stdout") { + self.stdout = Some(stdout.expect_bytes()?); + } + Ok(()) + } + fn add_stderr(&mut self, object: &Hash) -> R<()> { if let Ok(stderr) = object.expect_field("stderr") { - self.stderr = Some(stderr.expect_str()?.as_bytes().to_vec()); + self.stderr = Some(stderr.expect_bytes()?); } Ok(()) } @@ -312,6 +343,7 @@ impl Test { "arguments", "env", "exitcode", + "stdout", "stderr", "cwd", ], @@ -321,6 +353,7 @@ impl Test { test.add_arguments(&object)?; test.add_env(&object)?; test.add_cwd(&object)?; + test.add_stdout(&object)?; test.add_stderr(&object)?; test.add_exitcode(&object)?; test.add_mocked_files(&object)?; @@ -575,7 +608,7 @@ mod load { unexpected field 'foo', \ possible values: \ 'steps', 'mockedFiles', 'arguments', 'env', \ - 'exitcode', 'stderr', 'cwd'", + 'exitcode', 'stdout', 'stderr', 'cwd'", path_to_string(&tempfile.path())? ) ); @@ -598,7 +631,7 @@ mod load { format!( "error in {}.test.yaml: \ unexpected field 'foo', \ - possible values: 'command', 'stdout', 'exitcode', 'regex'", + possible values: 'command', 'stdout', 'stderr', 'exitcode', 'regex'", path_to_string(&tempfile.path())? ) ); @@ -995,6 +1028,41 @@ mod load { Ok(()) } + mod expected_stdout { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn allows_to_specify_the_expected_stdout() -> R<()> { + assert_eq!( + test_parse_one( + r" + |- steps: [] + | stdout: foo + " + )? + .stdout + .map(|s| String::from_utf8(s).unwrap()), + Some("foo".to_string()) + ); + Ok(()) + } + + #[test] + fn none_is_the_default() -> R<()> { + assert_eq!( + test_parse_one( + r" + |- steps: [] + " + )? + .stdout, + None + ); + Ok(()) + } + } + mod expected_stderr { use super::*; use pretty_assertions::assert_eq; @@ -1213,6 +1281,7 @@ mod serialize { let test = Test::new(vec![Step { command_matcher: CommandMatcher::ExactMatch(Command::new("cp")?), stdout: vec![], + stderr: vec![], exitcode: 42, }]); roundtrip(Tests::new(vec![test])) diff --git a/src/test_spec/yaml.rs b/src/test_spec/yaml.rs index 0e77bc6..0fe698f 100644 --- a/src/test_spec/yaml.rs +++ b/src/test_spec/yaml.rs @@ -6,6 +6,8 @@ use std::io::Cursor; use yaml_rust::{yaml::Hash, Yaml, YamlEmitter}; pub trait YamlExt { + fn expect_bytes(&self) -> R>; + fn expect_str(&self) -> R<&str>; fn expect_array(&self) -> R<&Vec>; @@ -16,6 +18,11 @@ pub trait YamlExt { } impl YamlExt for Yaml { + fn expect_bytes(&self) -> R> { + let str = self.expect_str()?; + Ok(str.as_bytes().to_vec()) + } + fn expect_str(&self) -> R<&str> { Ok(self .as_str() diff --git a/src/tracer/mod.rs b/src/tracer/mod.rs index 08eb224..0ab8e28 100644 --- a/src/tracer/mod.rs +++ b/src/tracer/mod.rs @@ -25,7 +25,7 @@ use std::os::unix::ffi::OsStringExt; use std::panic; use std::path::{Path, PathBuf}; use std::str; -use stdio_redirecting::{CaptureStderr, Redirector}; +use stdio_redirecting::{Capture, Redirector}; use syscall::Syscall; use tempdir::TempDir; @@ -147,10 +147,10 @@ impl Tracer { program: &Path, args: Vec, env: HashMap, - capture_stderr: CaptureStderr, + capture: Capture, mut syscall_mock: impl SyscallMock, ) -> R { - let redirector = Redirector::new(context, capture_stderr)?; + let redirector = Redirector::new(context, capture)?; fork_with_child_errors( || { redirector.child_redirect_streams()?; diff --git a/src/tracer/stdio_redirecting.rs b/src/tracer/stdio_redirecting.rs index 6004e64..aee75f8 100644 --- a/src/tracer/stdio_redirecting.rs +++ b/src/tracer/stdio_redirecting.rs @@ -2,6 +2,7 @@ use crate::context::Context; use crate::R; use libc::c_int; use nix::unistd::{close, dup2, pipe, read}; +use std::fmt; use std::io::{Cursor, Write}; use std::sync::{Arc, Mutex}; use std::thread; @@ -9,23 +10,20 @@ use std::thread; type RawFd = c_int; pub struct Redirector { - stdout: Redirect, + pub stdout: Redirect, pub stderr: Redirect, } -pub enum CaptureStderr { - Capture, - NoCapture, +pub struct Capture { + pub stdout: bool, + pub stderr: bool, } impl Redirector { - pub fn new(context: &Context, capture_stderr: CaptureStderr) -> R { + pub fn new(context: &Context, capture: Capture) -> R { Ok(Redirector { - stdout: Redirect::new(context, StreamType::Stdout)?, - stderr: match capture_stderr { - CaptureStderr::Capture => Redirect::new_capturing(context, StreamType::Stderr)?, - CaptureStderr::NoCapture => Redirect::new(context, StreamType::Stderr)?, - }, + stdout: Redirect::new(context, StreamType::Stdout, capture.stdout)?, + stderr: Redirect::new(context, StreamType::Stderr, capture.stderr)?, }) } @@ -47,13 +45,22 @@ impl Redirector { } #[derive(Clone, Copy)] -enum StreamType { +pub enum StreamType { Stdout, Stderr, } +impl fmt::Display for StreamType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + StreamType::Stdout => write!(f, "stdout"), + StreamType::Stderr => write!(f, "stderr"), + } + } +} + pub struct Redirect { - stream_type: StreamType, + pub stream_type: StreamType, context: Context, read_end: RawFd, write_end: RawFd, @@ -61,30 +68,18 @@ pub struct Redirect { } impl Redirect { - fn new(context: &Context, stream_type: StreamType) -> R { - Redirect::new_internal(context, stream_type, None) - } - - fn new_capturing(context: &Context, stream_type: StreamType) -> R { - Redirect::new_internal( - context, - stream_type, - Some(Arc::new(Mutex::new(Cursor::new(vec![])))), - ) - } - - fn new_internal( - context: &Context, - stream_type: StreamType, - captured: Option>>>>, - ) -> R { + fn new(context: &Context, stream_type: StreamType, capture: bool) -> R { let (read_end, write_end) = pipe()?; Ok(Redirect { stream_type, context: context.clone(), read_end, write_end, - captured, + captured: if capture { + Some(Arc::new(Mutex::new(Cursor::new(vec![])))) + } else { + None + }, }) } diff --git a/tests/files.rs b/tests/files.rs index cfc1956..00b708e 100644 --- a/tests/files.rs +++ b/tests/files.rs @@ -9,7 +9,7 @@ mod utils; use scriptkeeper::R; -use utils::test_run; +use utils::{test_run, Expect}; #[test] fn allows_to_mock_files_existence() -> R<()> { @@ -27,7 +27,7 @@ fn allows_to_mock_files_existence() -> R<()> { | mockedFiles: | - /foo ", - Ok(()), + Expect::ok(), )?; Ok(()) } @@ -48,7 +48,7 @@ fn allows_to_mock_directory_existence() -> R<()> { | mockedFiles: | - /foo/ ", - Ok(()), + Expect::ok(), )?; Ok(()) } @@ -66,7 +66,7 @@ fn does_not_mock_existence_of_unspecified_files() -> R<()> { |tests: | - steps: [] ", - Ok(()), + Expect::ok(), )?; Ok(()) } diff --git a/tests/path.rs b/tests/path.rs index 9ab193a..7b2340a 100644 --- a/tests/path.rs +++ b/tests/path.rs @@ -9,7 +9,7 @@ mod utils; use scriptkeeper::R; use test_utils::trim_margin; -use utils::test_run; +use utils::{test_run, Expect}; #[test] fn looks_up_step_executable_in_path() -> R<()> { @@ -22,7 +22,7 @@ fn looks_up_step_executable_in_path() -> R<()> { |steps: | - cp ", - Ok(()), + Expect::ok(), )?; Ok(()) } @@ -40,7 +40,7 @@ fn looks_up_unmocked_command_executable_in_path() -> R<()> { |unmockedCommands: | - ls ", - Ok(()), + Expect::ok(), )?; Ok(()) } @@ -56,7 +56,7 @@ fn shortens_received_executable_to_file_name_when_reporting_step_error() -> R<() |steps: | - cp ", - Err(&trim_margin( + Expect::err(&trim_margin( " |error: | expected: cp @@ -78,7 +78,7 @@ fn runs_step_executable_that_is_not_in_path() -> R<()> { |steps: | - /not/in/path ", - Ok(()), + Expect::ok(), )?; Ok(()) } diff --git a/tests/run.rs b/tests/run.rs index b97dc0c..49ebef9 100644 --- a/tests/run.rs +++ b/tests/run.rs @@ -14,7 +14,7 @@ use std::env; use std::fs; use std::path::PathBuf; use test_utils::{assert_error, trim_margin, TempFile}; -use utils::{prepare_script, test_run, test_run_with_tempfile}; +use utils::{prepare_script, test_run, test_run_with_tempfile, Expect}; #[test] fn simple() -> R<()> { @@ -27,7 +27,7 @@ fn simple() -> R<()> { |steps: | - cp ", - Ok(()), + Expect::ok(), )?; Ok(()) } @@ -99,7 +99,7 @@ fn can_specify_interpreter() -> R<()> { | - "true" |interpreter: /usr/bin/ruby "#, - Ok(()), + Expect::ok(), )?; Ok(()) } @@ -116,7 +116,7 @@ fn allows_to_match_command_with_regex() -> R<()> { | - steps: | - regex: cp \d "#, - Ok(()), + Expect::ok(), )?; Ok(()) } @@ -182,7 +182,7 @@ fn multiple() -> R<()> { | - cp | - ls ", - Ok(()), + Expect::ok(), )?; Ok(()) } @@ -198,7 +198,7 @@ fn failing() -> R<()> { |steps: | - cp ", - Err(&trim_margin( + Expect::err(&trim_margin( " |error: | expected: cp @@ -222,7 +222,7 @@ fn failing_later() -> R<()> { | - ls | - cp ", - Err(&trim_margin( + Expect::err(&trim_margin( " |error: | expected: cp @@ -368,7 +368,7 @@ mod arguments { |steps: | - cp foo ", - Ok(()), + Expect::ok(), )?; Ok(()) } @@ -384,7 +384,7 @@ mod arguments { |steps: | - cp foo ", - Err(&trim_margin( + Expect::err(&trim_margin( " |error: | expected: cp foo @@ -406,7 +406,7 @@ mod arguments { |steps: | - cp "foo bar" "#, - Ok(()), + Expect::ok(), )?; Ok(()) } @@ -422,7 +422,7 @@ mod arguments { |steps: | - cp "foo bar" "#, - Err(&trim_margin( + Expect::err(&trim_margin( r#" |error: | expected: cp "foo bar" @@ -447,7 +447,7 @@ fn reports_the_first_error() -> R<()> { | - cp first | - cp second ", - Err(&trim_margin( + Expect::err(&trim_margin( " |error: | expected: cp first @@ -473,7 +473,7 @@ mod mismatch_in_number_of_commands { | - ls | - cp ", - Err(&trim_margin( + Expect::err(&trim_margin( " |error: | expected: cp @@ -496,7 +496,7 @@ mod mismatch_in_number_of_commands { |steps: | - ls ", - Err(&trim_margin( + Expect::err(&trim_margin( " |error: | expected: