Skip to content
This repository was archived by the owner on Jun 3, 2021. It is now read-only.
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
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ test_dockerfile:
docker run --rm \
--cap-add=SYS_PTRACE \
-v $(pwd)/tests/examples/bigger/script:/root/script \
-v $(pwd)/tests/examples/bigger/script.protocols.yaml:/root/script.protocols.yaml \
-v $(pwd)/tests/examples/bigger/script.test.yaml:/root/script.test.yaml \
scriptkeeper \
script
77 changes: 40 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ Feel free to open (or vote on)

## Usage

`scriptkeeper` allows you to write tests -- so-called protocols -- for
scripts (or other executables) -- without the need to modify your executables.
`scriptkeeper` allows you to write tests for scripts (or other executables) --
without the need to modify your executables.

Here's an example script `./build-image.sh`:

Expand All @@ -47,38 +47,41 @@ else
fi
```

And here's a matching protocols file `./build-image.sh.protocols.yaml`:
For `scriptkeeper` to be able to test this script, you need to add a yaml file
in the same directory as the script that has the additional file extension
`.test.yaml`. So here's the matching test file
`./build-image.sh.test.yaml`:

```yaml
protocols:
tests:
# builds a docker image when git repo is clean
- protocol:
- steps:
- command: /usr/bin/git status --porcelain
stdout: ""
- command: /usr/bin/git rev-parse HEAD
stdout: "mock_commit_hash\n"
- /usr/bin/docker build --tag image_name:mock_commit_hash .
# aborts when git repo is not clean
- protocol:
- steps:
- command: /usr/bin/git status --porcelain
stdout: " M some-file"
exitcode: 1
```

Now running `scriptkeeper ./build-image.sh` will tell you whether your script
`./build-image.sh` conforms to your protocols in
`./build-image.sh.protocols.yaml`.
`./build-image.sh` conforms to your tests in
`./build-image.sh.test.yaml`.

There are more example test cases in the [tests/examples](./tests/examples)
folder.

### `.protocols.yaml` format
### `.test.yaml` format

Here's all the fields that are available in the yaml declarations for the
protocols: (`?` marks optional fields.)
tests: (`?` marks optional fields.)

``` yaml
protocols:
tests:
- arguments?: string
# List of arguments given to the tested script, seperated by spaces.
# Example: "-rf /", default: ""
Expand All @@ -99,7 +102,7 @@ protocols:
exitcode?: number
# Exitcode that the tested script is expected to exit with.
# Default: 0.
protocol:
steps:
# List of commands that your script is expected to execute.
- command|regex: string
# One of either `command` or `regex` is required
Expand Down Expand Up @@ -132,87 +135,87 @@ unmockedCommands: [string]
For convenience you can specify commands as a string directly. So this

``` yaml
protocol:
steps:
- command: git add .
- command: git push
```

can be written as

``` yaml
protocol:
steps:
- git add .
- git push
```

#### Multiple protocols
#### Multiple tests

Multiple protocols can be specified using a YAML array:
Multiple tests can be specified using a YAML array:

``` yaml
# when given the 'push' argument, it pushes to the remote
- arguments: push
protocol:
steps:
- git add .
- git push
# when given the 'pull' argument, it just pulls
- arguments: push
protocol:
steps:
- git pull
```

You can also put everything into a `protocols` field:
You can also put everything into a `tests` field:

``` yaml
protocols:
tests:
# when given the 'push' argument, it pushes to the remote
- arguments: push
protocol:
steps:
- git add .
- git push
# when given the 'pull' argument, it just pulls
- arguments: push
protocol:
steps:
- git pull
```

## Recording protocols
## Recording tests

There is **experimental** support for recording protocols. You can either record
protocols by passing in the `--record` command line flag, or you can put
so-called holes into your protocols:
There is **experimental** support for recording tests. You can either record
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it just occurred to me that this idea might need a bit more explanation. i don't believe there are other similar tools out there that can automatically generate tests for you, so the idea of "recording tests" isn't obvious IMO.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's right. This could probably use recording snapshot tests as an analogy.

Do you think that this PR makes the readme worse? If not I would defer your concern to a different issue/PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no i think the change is fine, but using a more common word made it clearer to me that this was confusing. yes, i would defer to a separate PR. i can make a ticket for it.

tests by passing in the `--record` command line flag, or you can put so-called
holes into your tests:

``` yaml
protocols:
- protocol:
tests:
- steps:
- _
```

This will actually execute the sub-commands that your script performs, without
mocking them out. And it will overwrite your protocols file with the recorded
mocking them out. And it will overwrite your tests file with the recorded
version.

You can also start with a partial protocol and have `scriptkeeper` fill in the
You can also start with a partial test and have `scriptkeeper` fill in the
specified holes:

``` yaml
protocols:
tests:
- arguments: foo
protocol:
steps:
- git add .
- _
```

This allows for an iterative process to create a protocol:
This allows for an iterative process to create a test:

1. Start with an empty protocol with a hole.
1. Start with an empty test with a hole.
2. Run `scriptkeeper`.
3. Identify the step in the recorded protocol where it deviates from the
3. Identify the step in the recorded test where it deviates from the
intended test. (If it doesn't, you're done.)
4. Refine the protocol by modifying the inputs to the tested script, i.e. the
4. Refine the test by modifying the inputs to the tested script, i.e. the
arguments, the environment, etc. This can be guided by both the recorded
script and the script's output to `stdout` and `stderr`.
5. Remove all protocol steps after the step identified in 3.
5. Remove all test steps after the step identified in 3.
6. Add a hole at the end.
7. Re-iterate from step 2.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
protocol:
steps:
- /usr/bin/docker build -t scriptkeeper .
2 changes: 1 addition & 1 deletion scriptkeeper-in-docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ fi

docker run --rm -it --cap-add=SYS_PTRACE \
--mount type=bind,source=$script,target=/root/$(basename $script) \
--mount type=bind,source=$script.protocols.yaml,target=/root/$(basename $script).protocols.yaml \
--mount type=bind,source=$script.test.yaml,target=/root/$(basename $script).test.yaml \
scriptkeeper $(basename $script)
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
unmockedCommands:
- /usr/bin/basename
protocols:
tests:
# works for relative paths
- arguments: ./script
cwd: /basedir
protocol:
steps:
- /usr/bin/docker run --rm -it
--cap-add=SYS_PTRACE
--mount type=bind,source=/basedir/./script,target=/root/script
--mount type=bind,source=/basedir/./script.protocols.yaml,target=/root/script.protocols.yaml
--mount type=bind,source=/basedir/./script.test.yaml,target=/root/script.test.yaml
scriptkeeper script
# works for absolute paths
- arguments: /test/script
protocol:
steps:
- /usr/bin/docker run --rm -it
--cap-add=SYS_PTRACE
--mount type=bind,source=/test/script,target=/root/script
--mount type=bind,source=/test/script.protocols.yaml,target=/root/script.protocols.yaml
--mount type=bind,source=/test/script.test.yaml,target=/root/script.test.yaml
scriptkeeper script
2 changes: 1 addition & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ fn parse_args_safe(args: impl Iterator<Item = String>) -> Result<Args, Error> {
let matches = App::new("scriptkeeper")
.arg(Arg::with_name("record").short("r").long("record").help(
"[EXPERIMENTAL] Runs the script (without mocking out anything), \
records a protocol and prints it to stdout",
records a test case and prints it to stdout",
))
.arg(
Arg::with_name("program")
Expand Down
27 changes: 12 additions & 15 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ extern crate memoffset;

pub mod cli;
pub mod context;
mod protocol;
mod protocol_checker;
mod recorder;
mod test_checker;
mod test_spec;
mod tracer;
pub mod utils;

use crate::context::Context;
use crate::protocol::yaml::write_yaml;
use crate::protocol::Protocols;
use crate::protocol_checker::executable_mock;
use crate::recorder::{hole_recorder::run_against_protocols, Recorder};
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::Tracer;
use std::collections::HashMap;
Expand Down Expand Up @@ -86,7 +86,7 @@ pub fn run_main(context: &Context, args: &cli::Args) -> R<ExitCode> {
record,
} => {
if *record {
print_recorded_protocol(context, script_path)?
print_recorded_test(context, script_path)?
} else {
run_scriptkeeper(context, &script_path)?
}
Expand Down Expand Up @@ -129,12 +129,12 @@ pub fn run_scriptkeeper(context: &Context, script: &Path) -> R<ExitCode> {
script.to_string_lossy()
))?
}
let (protocols_file_path, expected_protocols) = Protocols::load(script)?;
run_against_protocols(&context, script, &protocols_file_path, expected_protocols)
let (test_file_path, tests) = Tests::load(script)?;
run_against_tests(&context, script, &test_file_path, tests)
}

fn print_recorded_protocol(context: &Context, program: &Path) -> R<ExitCode> {
let protocol = Tracer::run_against_mock(
fn print_recorded_test(context: &Context, program: &Path) -> R<ExitCode> {
let test = Tracer::run_against_mock(
context,
&None,
program,
Expand All @@ -143,9 +143,6 @@ fn print_recorded_protocol(context: &Context, program: &Path) -> R<ExitCode> {
CaptureStderr::NoCapture,
Recorder::empty(),
)?;
write_yaml(
&mut *context.stdout(),
&Protocols::new(vec![protocol]).serialize()?,
)?;
write_yaml(&mut *context.stdout(), &Tests::new(vec![test]).serialize()?)?;
Ok(ExitCode(0))
}
Loading