Skip to content
This repository has been archived by the owner on Jun 3, 2021. It is now read-only.

Sh/stdout #176

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ scripts:

dev:
clear ; printf "\e[3J"
cargo test --all --color=always --features 'dev test' -- --test-threads=1 --quiet
cargo test --all --color=always --features 'dev test' -- --test-threads=1

unit_test:
clear ; printf "\e[3J"
cargo build --color=always --features=dev
cargo test --lib --all --color=always --features 'dev test' -- --test-threads=1 --quiet

run_bigger:
cargo run -- tests/examples/bigger/script
Expand Down
191 changes: 191 additions & 0 deletions src/executable_mock.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
use crate::context::Context;
use crate::tracer::tracee_memory;
use crate::utils::short_temp_files::ShortTempFile;
use crate::{ExitCode, R};
use bincode::{deserialize, serialize};
use libc::user_regs_struct;
use nix::unistd::Pid;
use std::fs;
use std::io::Write;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};
use std::process::Command;

#[derive(Debug, Serialize, Deserialize)]
pub enum Config {
Config { stdout: Vec<u8>, exitcode: i32 },
Wrapper { executable: PathBuf },
}

#[derive(Debug)]
pub struct ExecutableMock {
temp_file: ShortTempFile,
}

impl ExecutableMock {
pub fn new(context: &Context, mock_config: Config) -> R<ExecutableMock> {
let mut contents = b"#!".to_vec();
contents.append(
&mut context
.scriptkeeper_executable()
.as_os_str()
.as_bytes()
.to_vec(),
);
contents.append(&mut b" --executable-mock\n".to_vec());
contents.append(&mut serialize(&mock_config)?);
let temp_file = ShortTempFile::new(&contents)?;
Ok(ExecutableMock { temp_file })
}

pub fn wrapper(context: &Context, executable: &Path) -> R<ExecutableMock> {
ExecutableMock::new(
context,
Config::Wrapper {
executable: executable.to_owned(),
},
)
}

pub fn path(&self) -> PathBuf {
self.temp_file.path()
}

pub fn poke_for_execve_syscall(
pid: Pid,
registers: &user_regs_struct,
executable_mock_path: PathBuf,
) -> R<()> {
tracee_memory::poke_single_word_string(
pid,
registers.rdi,
&executable_mock_path.as_os_str().as_bytes(),
)
}

pub fn run(context: &Context, executable_mock_path: &Path) -> R<ExitCode> {
let config: Config = deserialize(&ExecutableMock::skip_hashbang_line(fs::read(
executable_mock_path,
)?))?;
match config {
Config::Config { stdout, exitcode } => {
context.stdout().write_all(&stdout)?;
Ok(ExitCode(exitcode))
}
Config::Wrapper { executable } => {
Command::new(&executable).status()?;
Ok(ExitCode(0))
}
}
}

fn skip_hashbang_line(input: Vec<u8>) -> Vec<u8> {
input
.clone()
.into_iter()
.skip_while(|char: &u8| *char != b'\n')
.skip(1)
.collect()
}
}

#[cfg(test)]
mod test {
use super::*;
use std::process::Command;
use test_utils::{trim_margin, TempFile};

mod new {
use super::*;

#[test]
fn creates_an_executable_that_outputs_the_given_stdout() -> R<()> {
let executable_mock = ExecutableMock::new(
&Context::new_mock(),
Config::Config {
stdout: b"foo".to_vec(),
exitcode: 0,
},
)?;
let output = Command::new(&executable_mock.path()).output();
assert_eq!(output?.stdout, b"foo");
Ok(())
}

#[test]
fn creates_an_executable_that_exits_with_the_given_exitcode() -> R<()> {
let executable_mock = ExecutableMock::new(
&Context::new_mock(),
Config::Config {
stdout: b"foo".to_vec(),
exitcode: 42,
},
)?;
let output = Command::new(executable_mock.path()).output()?;
assert_eq!(output.status.code(), Some(42));
Ok(())
}
}

mod wrapper {
use super::*;
use crate::utils::path_to_string;
use tempdir::TempDir;

#[test]
fn executes_the_given_command() -> R<()> {
let temp_dir = TempDir::new("test")?;
let path = temp_dir.path().join("foo");
let script = TempFile::write_temp_script(
trim_margin(&format!(
"
|#!/usr/bin/env bash
|touch {}
",
path_to_string(&path)?
))?
.as_bytes(),
)?;
let executable_mock = ExecutableMock::wrapper(&Context::new_mock(), &script.path())?;
Command::new(executable_mock.path()).status()?;
assert!(path.exists());
Ok(())
}

#[test]
fn relays_stdout() -> R<()> {
let script = TempFile::write_temp_script(
trim_margin(
"
|#!/usr/bin/env bash
|echo foo
",
)?
.as_bytes(),
)?;
let executable_mock = ExecutableMock::wrapper(&Context::new_mock(), &script.path())?;
let output = Command::new(executable_mock.path()).output()?;
assert_eq!(String::from_utf8(output.stdout)?, "foo\n");
Ok(())
}

#[test]
fn relays_the_process_environment() -> R<()> {
let script = TempFile::write_temp_script(
trim_margin(
"
|#!/usr/bin/env bash
|echo $FOO
",
)?
.as_bytes(),
)?;
let executable_mock = ExecutableMock::wrapper(&Context::new_mock(), &script.path())?;
let output = Command::new(executable_mock.path())
.env("FOO", "bar")
.output()?;
assert_eq!(String::from_utf8(output.stdout)?, "bar\n");
Ok(())
}
}
}
14 changes: 7 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ extern crate memoffset;

pub mod cli;
pub mod context;
mod executable_mock;
mod recorder;
mod test_checker;
mod test_spec;
mod tracer;
pub mod utils;

use crate::context::Context;
use crate::executable_mock::ExecutableMock;
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;
Expand Down Expand Up @@ -80,7 +81,7 @@ pub fn run_main(context: &Context, args: &cli::Args) -> R<ExitCode> {
Ok(match args {
cli::Args::ExecutableMock {
executable_mock_path,
} => executable_mock::run(context, &executable_mock_path)?,
} => ExecutableMock::run(context, &executable_mock_path)?,
cli::Args::Scriptkeeper {
script_path,
record,
Expand All @@ -97,20 +98,19 @@ pub fn run_main(context: &Context, args: &cli::Args) -> R<ExitCode> {
#[cfg(test)]
mod run_main {
use super::*;
use executable_mock::create_mock_executable;
use crate::executable_mock;
use test_utils::TempFile;

#[test]
fn when_passed_executable_mock_flag_behaves_like_executable_mock() -> R<()> {
let context = Context::new_mock();
let executable_contents = create_mock_executable(
let executable_mock = ExecutableMock::new(
&context,
executable_mock::Config {
executable_mock::Config::Config {
stdout: b"foo".to_vec(),
exitcode: 0,
},
)?;
let executable_mock = TempFile::write_temp_script(&executable_contents)?;
run_main(
&context,
&cli::Args::ExecutableMock {
Expand Down Expand Up @@ -141,7 +141,7 @@ fn print_recorded_test(context: &Context, program: &Path) -> R<ExitCode> {
vec![],
HashMap::new(),
CaptureStderr::NoCapture,
Recorder::empty(),
Recorder::empty(context),
)?;
write_yaml(&mut *context.stdout(), &Tests::new(vec![test]).serialize()?)?;
Ok(ExitCode(0))
Expand Down
4 changes: 3 additions & 1 deletion src/recorder/hole_recorder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ impl SyscallMock for HoleRecorder {
CheckerResult::Pass => {
*self = HoleRecorder::Recorder {
recorder: Recorder::new(
&checker.context,
original_test.clone(),
&checker.unmocked_commands,
),
Expand Down Expand Up @@ -86,7 +87,8 @@ impl SyscallMock for HoleRecorder {
} => match checker.result {
CheckerResult::Pass => {
original_test.ends_with_hole = false;
let recorder = Recorder::new(original_test, &checker.unmocked_commands);
let recorder =
Recorder::new(&checker.context, original_test, &checker.unmocked_commands);
RecorderResult::Recorded(recorder.handle_end(exitcode, redirector)?)
}
failure @ CheckerResult::Failure(_) => {
Expand Down
22 changes: 18 additions & 4 deletions src/recorder/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
pub mod hole_recorder;
mod result;

use crate::context::Context;
use crate::executable_mock::ExecutableMock;
use crate::test_spec::command::Command;
use crate::test_spec::command_matcher::CommandMatcher;
use crate::test_spec::{compare_executables, Step, Test};
Expand All @@ -13,25 +15,32 @@ use std::ffi::OsString;
use std::path::PathBuf;

pub struct Recorder {
context: Context,
test: Test,
command: Option<Command>,
unmocked_commands: Vec<PathBuf>,
temporary_executables: Vec<ExecutableMock>,
}

impl Recorder {
pub fn empty() -> Recorder {
pub fn empty(context: &Context) -> Recorder {
// fixme: use new
Recorder {
context: context.clone(),
test: Test::new(vec![]),
command: None,
unmocked_commands: vec![],
temporary_executables: vec![],
}
}

pub fn new(test: Test, unmocked_commands: &[PathBuf]) -> Recorder {
pub fn new(context: &Context, test: Test, unmocked_commands: &[PathBuf]) -> Recorder {
Recorder {
context: context.clone(),
test,
command: None,
unmocked_commands: unmocked_commands.to_vec(),
temporary_executables: vec![],
}
}
}
Expand All @@ -41,8 +50,8 @@ impl SyscallMock for Recorder {

fn handle_execve_enter(
&mut self,
_pid: Pid,
_registers: &user_regs_struct,
pid: Pid,
registers: &user_regs_struct,
executable: PathBuf,
arguments: Vec<OsString>,
) -> R<()> {
Expand All @@ -51,6 +60,11 @@ impl SyscallMock for Recorder {
.iter()
.any(|unmocked_command| compare_executables(unmocked_command, &executable));
if !is_unmocked_command {
let executable_mock_path = ExecutableMock::wrapper(&self.context, &executable)?;
let path = executable_mock_path.path();
self.temporary_executables.push(executable_mock_path);
ExecutableMock::poke_for_execve_syscall(pid, registers, path)?;

self.command = Some(Command {
executable,
arguments,
Expand Down
Loading