diff --git a/src/lib.rs b/src/lib.rs index 9d3f103..f4d9fc1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,7 +145,7 @@ fn print_recorded_protocol(context: &Context, program: &Path) -> R { )?; write_yaml( &mut *context.stdout(), - &Protocols::new(vec![protocol]).serialize()?, + &Protocols::new(vec![protocol]).serialize(&[])?, )?; Ok(ExitCode(0)) } diff --git a/src/protocol/command.rs b/src/protocol/command.rs index aa93446..634b1b5 100644 --- a/src/protocol/command.rs +++ b/src/protocol/command.rs @@ -20,9 +20,12 @@ impl Command { } } - pub fn compare(&self, other: &Command) -> bool { - executable_path::compare_executables(&self.executable, &other.executable) - && self.arguments == other.arguments + pub fn compare(&self, mocked_executables: &[PathBuf], other: &Command) -> bool { + executable_path::compare_executables( + mocked_executables, + &self.executable, + &other.executable, + ) && self.arguments == other.arguments } fn escape(word: String) -> String { @@ -46,8 +49,8 @@ impl Command { .join(" ") } - pub fn format(&self) -> String { - let executable = executable_path::canonicalize(&self.executable) + pub fn format(&self, mocked_executables: &[PathBuf]) -> String { + let executable = executable_path::canonicalize(mocked_executables, &self.executable) .to_string_lossy() .into_owned(); if self.arguments.is_empty() { @@ -251,7 +254,7 @@ mod command { ($name:ident, $string:expr) => { #[test] fn $name() -> R<()> { - assert_eq!(Command::new($string)?.format(), $string); + assert_eq!(Command::new($string)?.format(&[]), $string); Ok(()) } }; @@ -261,7 +264,7 @@ mod command { ($name:ident, $input:expr, $normalized:expr) => { #[test] fn $name() -> R<()> { - assert_eq!(Command::new($input)?.format(), $normalized); + assert_eq!(Command::new($input)?.format(&[]), $normalized); Ok(()) } }; diff --git a/src/protocol/command_matcher.rs b/src/protocol/command_matcher.rs index ee54ca7..e469685 100644 --- a/src/protocol/command_matcher.rs +++ b/src/protocol/command_matcher.rs @@ -1,6 +1,7 @@ use super::command::Command; use crate::R; use regex::Regex; +use std::path::PathBuf; use std::str; #[derive(Debug, Clone, Eq, PartialEq)] @@ -10,16 +11,16 @@ pub enum CommandMatcher { } impl CommandMatcher { - pub fn matches(&self, other: &Command) -> bool { + pub fn matches(&self, mocked_executables: &[PathBuf], other: &Command) -> bool { match self { - CommandMatcher::ExactMatch(command) => command.compare(other), - CommandMatcher::RegexMatch(regex) => regex.is_match(&other.format()), + CommandMatcher::ExactMatch(command) => command.compare(mocked_executables, other), + CommandMatcher::RegexMatch(regex) => regex.is_match(&other.format(mocked_executables)), } } - pub fn format(&self) -> String { + pub fn format(&self, mocked_executables: &[PathBuf]) -> String { match self { - CommandMatcher::ExactMatch(command) => command.format(), + CommandMatcher::ExactMatch(command) => command.format(mocked_executables), CommandMatcher::RegexMatch(AnchoredRegex { original_string, .. }) => original_string.clone(), @@ -63,44 +64,43 @@ mod command_matcher { #[test] fn matches_command_executable() -> R<()> { - assert!( - CommandMatcher::ExactMatch(Command::new("true")?).matches(&Command::new("true")?) - ); + assert!(CommandMatcher::ExactMatch(Command::new("true")?) + .matches(&[], &Command::new("true")?)); Ok(()) } #[test] fn matches_command_with_arguments() -> R<()> { assert!(CommandMatcher::ExactMatch(Command::new("echo 1")?) - .matches(&Command::new("echo 1")?)); + .matches(&[], &Command::new("echo 1")?)); Ok(()) } #[test] fn matches_command_even_if_it_doesnt_exist() -> R<()> { - assert!(CommandMatcher::ExactMatch(Command::new("foo")?).matches(&Command::new("foo")?)); + assert!(CommandMatcher::ExactMatch(Command::new("foo")?) + .matches(&[], &Command::new("foo")?)); Ok(()) } #[test] fn matches_command_with_full_path() -> R<()> { assert!(CommandMatcher::ExactMatch(Command::new("/bin/true")?) - .matches(&Command::new("/bin/true")?)); + .matches(&[], &Command::new("/bin/true")?)); Ok(()) } #[test] fn doesnt_match_a_different_command() -> R<()> { - assert!( - !CommandMatcher::ExactMatch(Command::new("foo")?).matches(&Command::new("bar")?) - ); + assert!(!CommandMatcher::ExactMatch(Command::new("foo")?) + .matches(&[], &Command::new("bar")?)); Ok(()) } #[test] fn doesnt_match_with_the_same_executable_but_different_arguments() -> R<()> { assert!(!CommandMatcher::ExactMatch(Command::new("foo 1")?) - .matches(&Command::new("foo 2")?)); + .matches(&[], &Command::new("foo 2")?)); Ok(()) } } @@ -110,7 +110,7 @@ mod command_matcher { fn test_regex_matches_command(regex: &str, command: &str) -> R { let result = CommandMatcher::RegexMatch(AnchoredRegex::new(regex)?) - .matches(&Command::new(command)?); + .matches(&[], &Command::new(command)?); Ok(result) } diff --git a/src/protocol/executable_path.rs b/src/protocol/executable_path.rs index b344c20..cdfc455 100644 --- a/src/protocol/executable_path.rs +++ b/src/protocol/executable_path.rs @@ -1,8 +1,8 @@ use quale::which; use std::path::{Path, PathBuf}; -pub fn compare_executables(a: &Path, b: &Path) -> bool { - canonicalize(a) == canonicalize(b) +pub fn compare_executables(mocked_executables: &[PathBuf], a: &Path, b: &Path) -> bool { + canonicalize(mocked_executables, a) == canonicalize(mocked_executables, b) } #[cfg(test)] @@ -13,7 +13,7 @@ mod compare_executables { #[test] fn returns_true_if_executables_are_identical() -> R<()> { let executable = Path::new("./bin/myexec"); - assert!(compare_executables(executable, executable)); + assert!(compare_executables(&[], executable, executable)); Ok(()) } @@ -21,7 +21,7 @@ mod compare_executables { fn returns_false_if_executables_are_distinct() -> R<()> { let a = Path::new("./bin/myexec"); let b = Path::new("./bin/myotherexec"); - assert!(!compare_executables(a, b)); + assert!(!compare_executables(&[], a, b)); Ok(()) } @@ -30,17 +30,25 @@ mod compare_executables { let path = which("cp").unwrap(); let cp_long = path; let cp_short = Path::new("cp"); - assert!(compare_executables(&cp_long, cp_short)); + assert!(compare_executables(&[], &cp_long, cp_short)); Ok(()) } } -pub fn canonicalize(executable: &Path) -> PathBuf { +fn foo_which(mocked_executables: &[PathBuf], name: &Path) -> Option { + if mocked_executables.contains(&PathBuf::from(name)) { + Some(PathBuf::from("/bin").join(name)) + } else { + which(name) + } +} + +pub fn canonicalize(mocked_executables: &[PathBuf], executable: &Path) -> PathBuf { let file_name = match executable.file_name() { None => return executable.into(), Some(f) => f, }; - match which(file_name) { + match foo_which(mocked_executables, &PathBuf::from(file_name)) { Some(resolved) => { if resolved == executable { PathBuf::from(file_name) @@ -62,7 +70,7 @@ mod canonicalize { fn shortens_absolute_executable_paths_if_found_in_path() -> R<()> { let executable = "cp"; let resolved = which(executable).unwrap(); - let file_name = canonicalize(&resolved); + let file_name = canonicalize(&[], &resolved); assert_eq!(file_name, PathBuf::from("cp")); Ok(()) } @@ -70,7 +78,7 @@ mod canonicalize { #[test] fn does_not_shorten_executable_that_is_not_in_path() -> R<()> { let executable = Path::new("/foo/doesnotexist"); - let file_name = canonicalize(executable); + let file_name = canonicalize(&[], executable); assert_eq!(file_name, PathBuf::from("/foo/doesnotexist")); Ok(()) } @@ -78,7 +86,7 @@ mod canonicalize { #[test] fn does_not_shorten_executable_that_is_not_in_path_but_has_same_name_as_one_that_is() -> R<()> { let executable = Path::new("/not/in/path/ls"); - let file_name = canonicalize(executable); + let file_name = canonicalize(&[], executable); assert_eq!(file_name, PathBuf::from("/not/in/path/ls")); Ok(()) } @@ -86,7 +94,7 @@ mod canonicalize { #[test] fn does_not_shorten_relative_path() -> R<()> { let executable = Path::new("./foo"); - let file_name = canonicalize(executable); + let file_name = canonicalize(&[], executable); assert_eq!(file_name, PathBuf::from("./foo")); Ok(()) } @@ -94,8 +102,18 @@ mod canonicalize { #[test] fn does_not_modify_short_forms_if_found_in_path() -> R<()> { let executable = Path::new("ls"); - let file_name = canonicalize(executable); + let file_name = canonicalize(&[], executable); assert_eq!(file_name, PathBuf::from("ls")); Ok(()) } + + #[test] + fn shortens_mocked_executables() -> R<()> { + let file_name = canonicalize( + &[PathBuf::from("does_not_exist")], + &PathBuf::from("/bin/does_not_exist"), + ); + assert_eq!(file_name, PathBuf::from("does_not_exist")); + Ok(()) + } } diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index d447d3e..b48ca8f 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -75,8 +75,8 @@ impl Step { } } - fn serialize(&self) -> Yaml { - let command = Yaml::String(self.command_matcher.format()); + fn serialize(&self, mocked_executables: &[PathBuf]) -> Yaml { + let command = Yaml::String(self.command_matcher.format(mocked_executables)); if self.exitcode == 0 { command } else { @@ -337,7 +337,7 @@ impl Protocol { } } - fn serialize(&self) -> Yaml { + fn serialize(&self, mocked_executables: &[PathBuf]) -> Yaml { let mut protocol = LinkedHashMap::new(); if !self.arguments.is_empty() { let arguments = self.arguments.iter().map(OsString::from).collect(); @@ -350,7 +350,7 @@ impl Protocol { { let mut steps = vec![]; for step in &self.steps { - steps.push(step.serialize()); + steps.push(step.serialize(mocked_executables)); } protocol.insert(Yaml::from_str("protocol"), Yaml::Array(steps)); } @@ -369,6 +369,7 @@ pub struct Protocols { pub protocols: Vec, pub unmocked_commands: Vec, pub interpreter: Option, + pub mocked_executables: Vec, } impl Protocols { @@ -377,6 +378,7 @@ impl Protocols { protocols, unmocked_commands: vec![], interpreter: None, + mocked_executables: vec![], } } @@ -405,6 +407,16 @@ impl Protocols { Ok(()) } + fn add_mocked_executables(&mut self, object: &Hash) -> R<()> { + if let Ok(mocked_executables) = object.expect_field("mockedExecutables") { + for mocked_executable in mocked_executables.expect_array()? { + self.mocked_executables + .push(PathBuf::from(mocked_executable.expect_str()?)); + } + } + Ok(()) + } + fn parse(yaml: Yaml) -> R { Ok(match &yaml { Yaml::Array(array) => Protocols::from_array(&array)?, @@ -413,10 +425,19 @@ impl Protocols { object.expect_field("protocol"), ) { (Ok(protocols), _) => { - check_keys(&["protocols", "interpreter", "unmockedCommands"], object)?; + check_keys( + &[ + "protocols", + "interpreter", + "unmockedCommands", + "mockedExecutables", + ], + object, + )?; let mut protocols = Protocols::from_array(protocols.expect_array()?)?; protocols.add_unmocked_commands(object)?; protocols.add_interpreter(object)?; + protocols.add_mocked_executables(object)?; protocols } (Err(_), Ok(_)) => Protocols::new(vec![Protocol::from_object(&object)?]), @@ -478,13 +499,13 @@ impl Protocols { Ok(()) } - pub fn serialize(&self) -> R { + pub fn serialize(&self, mocked_executables: &[PathBuf]) -> R { let mut object = LinkedHashMap::new(); self.serialize_unmocked_commands(&mut object)?; { let mut protocols = vec![]; for protocol in self.protocols.iter() { - protocols.push(protocol.serialize()); + protocols.push(protocol.serialize(mocked_executables)); } object.insert(Yaml::from_str("protocols"), Yaml::Array(protocols)); } @@ -559,7 +580,8 @@ mod load { format!( "error in {}.protocols.yaml: \ unexpected field 'foo', \ - possible values: 'protocols', 'interpreter', 'unmockedCommands'", + possible values: 'protocols', 'interpreter', 'unmockedCommands', \ + 'mockedExecutables'", path_to_string(&tempfile.path())? ) ); @@ -999,7 +1021,27 @@ mod load { )? .mocked_files .map(|path| path.to_string_lossy().to_string()), - vec![("/foo")] + vec!["/foo"] + ); + Ok(()) + } + + #[test] + fn allows_to_specify_mocked_executables() -> R<()> { + let tempfile = TempFile::new()?; + assert_eq!( + test_parse( + &tempfile, + r" + |protocols: + | - protocol: [] + |mockedExecutables: + | - foo + " + )? + .mocked_executables + .map(|path| path.to_string_lossy().to_string()), + vec!["foo"] ); Ok(()) } @@ -1171,7 +1213,7 @@ mod serialize { use pretty_assertions::assert_eq; fn roundtrip(protocols: Protocols) -> R<()> { - let yaml = protocols.serialize()?; + let yaml = protocols.serialize(&[])?; let result = Protocols::parse(yaml)?; assert_eq!(result, protocols); Ok(()) diff --git a/src/protocol_checker/mod.rs b/src/protocol_checker/mod.rs index ec4ba33..171f7e3 100644 --- a/src/protocol_checker/mod.rs +++ b/src/protocol_checker/mod.rs @@ -21,6 +21,7 @@ pub struct ProtocolChecker { context: Context, pub protocol: Protocol, pub unmocked_commands: Vec, + pub mocked_executables: Vec, pub result: CheckerResult, temporary_executables: Vec, } @@ -30,11 +31,13 @@ impl ProtocolChecker { context: &Context, protocol: Protocol, unmocked_commands: &[PathBuf], + mocked_executables: &[PathBuf], ) -> ProtocolChecker { ProtocolChecker { context: context.clone(), protocol, unmocked_commands: unmocked_commands.to_vec(), + mocked_executables: mocked_executables.to_vec(), result: CheckerResult::Pass, temporary_executables: vec![], } @@ -50,10 +53,15 @@ impl ProtocolChecker { fn handle_step(&mut self, received: protocol::Command) -> R { let mock_config = match self.protocol.steps.pop_front() { Some(next_protocol_step) => { - if !next_protocol_step.command_matcher.matches(&received) { + if !next_protocol_step + .command_matcher + .matches(&self.mocked_executables, &received) + { self.register_step_error( - &next_protocol_step.command_matcher.format(), - &received.format(), + &next_protocol_step + .command_matcher + .format(&self.mocked_executables), + &received.format(&self.mocked_executables), ); } executable_mock::Config { @@ -62,7 +70,10 @@ impl ProtocolChecker { } } None => { - self.register_step_error("", &received.format()); + self.register_step_error( + "", + &received.format(&self.mocked_executables), + ); ProtocolChecker::allow_failing_scripts_to_continue() } }; @@ -101,10 +112,9 @@ impl SyscallMock for ProtocolChecker { executable: PathBuf, arguments: Vec, ) -> R<()> { - let is_unmocked_command = self - .unmocked_commands - .iter() - .any(|unmocked_command| protocol::compare_executables(unmocked_command, &executable)); + let is_unmocked_command = self.unmocked_commands.iter().any(|unmocked_command| { + protocol::compare_executables(&self.mocked_executables, unmocked_command, &executable) + }); if !is_unmocked_command { let mock_executable_path = self.handle_step(protocol::Command { executable, @@ -149,6 +159,22 @@ impl SyscallMock for ProtocolChecker { let mut registers = *registers; registers.rax = 0; ptrace::setregs(pid, registers)?; + } else if self + .mocked_executables + .iter() + .any(|mocked_executable| PathBuf::from("/bin").join(mocked_executable) == filename) + { + let statbuf_ptr = registers.rsi; + let mock_mode = 0; + #[allow(clippy::forget_copy)] + tracee_memory::poke_four_bytes( + pid, + statbuf_ptr + (offset_of!(libc::stat, st_mode) as u64), + mock_mode as u32, + )?; + let mut registers = *registers; + registers.rax = 0; + ptrace::setregs(pid, registers)?; } Ok(()) } @@ -156,7 +182,9 @@ impl SyscallMock for ProtocolChecker { fn handle_end(mut self, exitcode: i32, redirector: &Redirector) -> R { if let Some(expected_step) = self.protocol.steps.pop_front() { self.register_step_error( - &expected_step.command_matcher.format(), + &expected_step + .command_matcher + .format(&self.mocked_executables), "