Skip to content

Commit

Permalink
test(docs): Add e2e tests for fail-{none,any} to README
Browse files Browse the repository at this point in the history
Required extending the bash interpreter with a notion of expected failure
  • Loading branch information
alexpovel committed Oct 14, 2023
1 parent 60e3e24 commit 04f2a40
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 14 deletions.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,49 @@ spaces](https://docs.rs/regex/latest/regex/#ascii-character-classes)).
> [!NOTE]
> When deleting (`-d`), for reasons of safety and sanity, a scope is *required*.
#### Explicit failure for (mis)matches

After all scopes are applied, it might turn out no matches were found. The default
behavior is to silently succeed:

```console
$ echo 'Some input...' | srgn --delete '\d'
Some input...
```

The output matches the specification: all digits are removed. There just happened to be
none. No matter how many actions are applied, **the input is returned unprocessed** once
this situation is detected. Hence, no unnecessary work is done.

One might prefer receiving explicit feedback (exit code other than zero) on failure:

```bash
echo 'Some input...' | srgn --delete --fail-none '\d' # will fail
```

The inverse scenario is also supported: **failing if anything matched**. This is useful
for checks (for example, in CI) against "undesirable" content. This works much like a
custom, ad-hoc linter.

Take for example "old-style" Python code, where type hints are not yet [surfaced to the
syntax-level](https://docs.python.org/3/library/typing.html):

```python oldtyping.py
def square(a):
"""Squares a number.
:param a: The number (type: int or float)
"""

return a**2
```

This style can be checked against and "forbidden" using:

```bash
cat oldtyping.py | srgn --python 'doc-strings' --fail-any 'param.+type' # will fail
```

#### Literal scope

This causes whatever was passed as the regex scope to be interpreted literally. Useful
Expand Down
51 changes: 37 additions & 14 deletions tests/readme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,13 @@ mod tests {

/// Multiple commands can be piped together.
#[derive(Debug, Clone, PartialEq, Eq)]
struct PipedPrograms(VecDeque<Program>);
struct PipedPrograms {
/// The program forming the pipe.
programs: VecDeque<Program>,
/// The expected outcome of the *entire* pipe. Any failure anywhere in the pipe
/// should cause overall failure (like `pipefail`).
should_fail: bool,
}

impl PipedPrograms {
/// Assembles a list of programs into a pipe.
Expand All @@ -210,10 +216,11 @@ mod tests {
chain: impl Iterator<Item = Program>,
stdout: Option<&str>,
snippets: Snippets,
should_fail: bool,
) -> Result<Self, &str> {
let mut chain = chain.collect::<VecDeque<_>>();
let mut programs = chain.collect::<VecDeque<_>>();

let first = chain
let first = programs
.pop_front()
.ok_or("Should have at least one program in pipe")?;

Expand All @@ -236,7 +243,7 @@ mod tests {
_ => return Err("First command should be able to produce stdin."),
};

match chain
match programs
.front_mut()
.expect("No second program to assemble with")
{
Expand All @@ -248,12 +255,18 @@ mod tests {
}
}

match chain.back_mut().expect("No last program to assemble with") {
match programs
.back_mut()
.expect("No last program to assemble with")
{
Program::Echo(_) | Program::Cat(_) => {
return Err("Stdin-generating program should not be at the end of a pipe")
}
Program::Self_(inv) => {
inv.stdout = if let Program::Cat(inv) = first {
inv.stdout = if should_fail {
// No stdout needed if command fails anyway
None
} else if let Program::Cat(inv) = first {
assert!(
stdout.is_none(),
"Cat output should be given as extra snippet, not inline"
Expand Down Expand Up @@ -281,7 +294,10 @@ mod tests {
}
}

Ok(Self(chain))
Ok(Self {
programs,
should_fail,
})
}
}

Expand All @@ -290,7 +306,7 @@ mod tests {
type IntoIter = std::collections::vec_deque::IntoIter<Self::Item>;

fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
self.programs.into_iter()
}
}

Expand Down Expand Up @@ -325,7 +341,8 @@ mod tests {
eprintln!("Parsed programs: {:#?}", programs);

// Advance to end; this eats optional comments and trailing whitespace.
let (input, _) = take_until("\n")(input)?;
let (input, tail) = take_until("\n")(input)?;
let should_fail = tail.contains("will fail");
let (input, _) = line_ending(input)?;

// Parse stdout; anything up to the next prompt.
Expand All @@ -334,7 +351,7 @@ mod tests {

Ok((
input,
PipedPrograms::assemble(programs.into_iter(), stdout, snippets)
PipedPrograms::assemble(programs.into_iter(), stdout, snippets, should_fail)
.expect("Should be able to assemble"),
))
}
Expand Down Expand Up @@ -572,6 +589,7 @@ mod tests {

for pipe in pipes {
let mut previous_stdin = None;
let should_fail = pipe.should_fail;
for program in pipe {
let mut cmd = Command::try_from(program.clone())
.expect("Should be able to convert invocation to cmd to run");
Expand All @@ -582,11 +600,16 @@ mod tests {

eprintln!("Running command: {:?}", cmd);

if let Some(stdout) = program.stdout().clone() {
// `success` takes ownership so can't test separately.
cmd.assert().success().stdout(stdout);
let mut assertion = cmd.assert();

assertion = if should_fail {
assertion.failure()
} else {
cmd.assert().success();
assertion.success()
};

if let Some(stdout) = program.stdout().clone() {
assertion.stdout(stdout);
}

// Pipe stdout to stdin of next run...
Expand Down

0 comments on commit 04f2a40

Please sign in to comment.