Skip to content
Merged
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
101 changes: 101 additions & 0 deletions src/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,35 @@ impl File {
.map_err(|e| format!("Failed to write {}: {}", path.display(), e))
}

pub(crate) fn replace_lines(
&mut self,
line_nums: std::ops::Range<usize>,
text: &str,
) -> Result<(), String> {
let mut output_lines = String::new();

let s = self
.as_str()
.ok_or("Binary file can't have lines replaced")?;
for (line_num, line) in crate::lines::LinesWithTerminator::new(s)
.enumerate()
.map(|(i, l)| (i + 1, l))
{
if line_num == line_nums.start {
output_lines.push_str(text);
if !text.is_empty() && !text.ends_with('\n') {
output_lines.push('\n');
}
}
if !line_nums.contains(&line_num) {
output_lines.push_str(line);
}
}

*self = Self::Text(output_lines);
Ok(())
}

pub(crate) fn map_text(self, op: impl FnOnce(&str) -> String) -> Self {
match self {
Self::Binary(data) => Self::Binary(data),
Expand Down Expand Up @@ -70,6 +99,13 @@ impl File {
}
}

pub(crate) fn as_str(&self) -> Option<&str> {
match self {
Self::Binary(_) => None,
Self::Text(data) => Some(data.as_str()),
}
}

pub(crate) fn as_bytes(&self) -> &[u8] {
match self {
Self::Binary(data) => data,
Expand Down Expand Up @@ -249,3 +285,68 @@ fn symlink_to_file(link: &std::path::Path, target: &std::path::Path) -> Result<(
fn symlink_to_file(link: &std::path::Path, target: &std::path::Path) -> Result<(), std::io::Error> {
std::os::unix::fs::symlink(target, link)
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn replace_lines_same_line_count() {
let input = "One\nTwo\nThree";
let line_nums = 2..3;
let replacement = "World\n";
let expected = File::Text("One\nWorld\nThree".into());

let mut actual = File::Text(input.into());
actual.replace_lines(line_nums, replacement).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn replace_lines_grow() {
let input = "One\nTwo\nThree";
let line_nums = 2..3;
let replacement = "World\nTrees\n";
let expected = File::Text("One\nWorld\nTrees\nThree".into());

let mut actual = File::Text(input.into());
actual.replace_lines(line_nums, replacement).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn replace_lines_shrink() {
let input = "One\nTwo\nThree";
let line_nums = 2..3;
let replacement = "";
let expected = File::Text("One\nThree".into());

let mut actual = File::Text(input.into());
actual.replace_lines(line_nums, replacement).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn replace_lines_no_trailing() {
let input = "One\nTwo\nThree";
let line_nums = 2..3;
let replacement = "World";
let expected = File::Text("One\nWorld\nThree".into());

let mut actual = File::Text(input.into());
actual.replace_lines(line_nums, replacement).unwrap();
assert_eq!(expected, actual);
}

#[test]
fn replace_lines_empty_range() {
let input = "One\nTwo\nThree";
let line_nums = 2..2;
let replacement = "World\n";
let expected = File::Text("One\nWorld\nTwo\nThree".into());

let mut actual = File::Text(input.into());
actual.replace_lines(line_nums, replacement).unwrap();
assert_eq!(expected, actual);
}
}
194 changes: 105 additions & 89 deletions src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,46 @@ impl Case {
step.expected_status = Some(crate::schema::CommandStatus::Skipped);
}

let step_status = self.run_step(step, cwd.as_deref(), mode, bins);
if step_status.is_err() {
let step_status = self.run_step(step, cwd.as_deref(), bins);
if step_status.is_err() && *mode == Mode::Fail {
prior_step_failed = true;
}
outputs.push(step_status);
}
match mode {
Mode::Dump(root) => {
for output in &mut outputs {
let output = match output {
Ok(output) => output,
Err(output) => output,
};
output.stdout =
match self.dump_stream(root, output.id.as_deref(), output.stdout.take()) {
Ok(stream) => stream,
Err(stream) => stream,
};
output.stderr =
match self.dump_stream(root, output.id.as_deref(), output.stderr.take()) {
Ok(stream) => stream,
Err(stream) => stream,
};
}
}
Mode::Overwrite => {
// `rev()` to ensure we don't mess up our line number info
for output in outputs.iter().rev() {
if let Err(output) = output {
let _ = sequence.overwrite(
&self.path,
output.id.as_deref(),
output.stdout.as_ref().map(|s| &s.content),
output.stderr.as_ref().map(|s| &s.content),
);
}
}
}
Mode::Fail => {}
}

if sequence.fs.sandbox() {
let mut ok = true;
Expand Down Expand Up @@ -232,7 +266,6 @@ impl Case {
&self,
step: &mut crate::schema::Step,
cwd: Option<&std::path::Path>,
mode: &Mode,
bins: &crate::BinRegistry,
) -> Result<Output, Output> {
let output = if let Some(id) = step.id.clone() {
Expand Down Expand Up @@ -266,58 +299,18 @@ impl Case {
let cmd_output = step.to_output(cwd).map_err(|e| output.clone().error(e))?;
let output = output.output(cmd_output);

// For dump mode's sake, allow running all
let mut ok = output.is_ok();
let mut output = match self.validate_spawn(output, step.expected_status()) {
Ok(output) => output,
Err(output) => {
ok = false;
output
}
};
if let Some(mut stdout) = output.stdout {
if !step.binary {
stdout = stdout.utf8();
}
if stdout.is_ok() {
stdout = match self.validate_stream(stdout, step.expected_stdout.as_ref(), mode) {
Ok(stdout) => stdout,
Err(stdout) => {
ok = false;
stdout
}
};
}
output.stdout = Some(stdout);
}
if let Some(mut stderr) = output.stderr {
if !step.binary {
stderr = stderr.utf8();
}
if stderr.is_ok() {
stderr = match self.validate_stream(stderr, step.expected_stderr.as_ref(), mode) {
Ok(stderr) => stderr,
Err(stderr) => {
ok = false;
stderr
}
};
}
output.stderr = Some(stderr);
}
// For Mode::Dump's sake, allow running all
let output = self.validate_spawn(output, step.expected_status());
let output = self.validate_streams(output, step);

if ok {
if output.is_ok() {
Ok(output)
} else {
Err(output)
}
}

fn validate_spawn(
&self,
mut output: Output,
expected: crate::schema::CommandStatus,
) -> Result<Output, Output> {
fn validate_spawn(&self, mut output: Output, expected: crate::schema::CommandStatus) -> Output {
let status = output.spawn.exit.expect("bale out before now");
match expected {
crate::schema::CommandStatus::Success => {
Expand All @@ -343,59 +336,82 @@ impl Case {
}
}

Ok(output)
output
}

fn validate_streams(&self, mut output: Output, step: &crate::schema::Step) -> Output {
output.stdout =
self.validate_stream(output.stdout, step.expected_stdout.as_ref(), step.binary);
output.stderr =
self.validate_stream(output.stderr, step.expected_stderr.as_ref(), step.binary);

output
}

fn validate_stream(
&self,
mut stream: Stream,
stream: Option<Stream>,
expected_content: Option<&crate::File>,
mode: &Mode,
) -> Result<Stream, Stream> {
if let Mode::Dump(root) = mode {
let stdout_path = root.join(
self.path
.with_extension(stream.stream.as_str())
.file_name()
.unwrap(),
);
stream.content.write_to(&stdout_path).map_err(|e| {
let mut stream = stream.clone();
stream.status = StreamStatus::Failure(e);
stream
})?;
} else if let Some(expected_content) = expected_content {
binary: bool,
) -> Option<Stream> {
let mut stream = stream?;

if !binary {
stream = stream.utf8();
if !stream.is_ok() {
return Some(stream);
}
}

if let Some(expected_content) = expected_content {
if let crate::File::Text(e) = &expected_content {
stream.content = stream.content.map_text(|t| crate::elide::normalize(t, e));
}
if stream.content != *expected_content {
match mode {
Mode::Fail => {
stream.status = StreamStatus::Expected(expected_content.clone());
return Err(stream);
}
Mode::Overwrite => {
let stdout_path = self.path.with_extension(stream.stream.as_str());
if stdout_path.exists() {
stream.content.write_to(&stdout_path).map_err(|e| {
let mut stream = stream.clone();
stream.status = StreamStatus::Failure(e);
stream
})?;
stream.status = StreamStatus::Expected(expected_content.clone());
return Ok(stream);
} else {
// `.trycmd` files do not support overwrite, see issue #23
stream.status = StreamStatus::Expected(expected_content.clone());
return Err(stream);
}
}
Mode::Dump(_) => unreachable!("handled earlier"),
}
stream.status = StreamStatus::Expected(expected_content.clone());
return Some(stream);
}
}

Ok(stream)
Some(stream)
}

fn dump_stream(
&self,
root: &std::path::Path,
id: Option<&str>,
stream: Option<Stream>,
) -> Result<Option<Stream>, Option<Stream>> {
if let Some(stream) = stream {
let file_name = match id {
Some(id) => {
format!(
"{}-{}.{}",
self.path.file_stem().unwrap().to_string_lossy(),
id,
stream.stream.as_str(),
)
}
None => {
format!(
"{}.{}",
self.path.file_stem().unwrap().to_string_lossy(),
stream.stream.as_str(),
)
}
};
let stream_path = root.join(file_name);
stream.content.write_to(&stream_path).map_err(|e| {
let mut stream = stream.clone();
if stream.is_ok() {
stream.status = StreamStatus::Failure(e);
}
stream
})?;
Ok(Some(stream))
} else {
Ok(None)
}
}

fn validate_fs(
Expand Down
Loading