A testspec engine for CLI E2E testing, inspired by Go's cmd/internal/script and roguepeppe/go-internal/testscript.
emx-testspec provides a declarative scripting DSL for testing CLI tools. Tests are written as txtar archives where the comment section contains script commands and the file section provides test fixtures.
- ✅ Declarative syntax - Simple, readable test scripts
- ✅ Txtar integration - Test fixtures in the same file
- ✅ Rich command set - exec, stdout, stderr, cmp, grep, etc.
- ✅ Platform conditions - Conditional execution per OS/arch
- ✅ Virtual files -
stdoutandstderras file-like objects - ✅ Background processes - Run commands asynchronously
- ✅ Flexible matching - Regex patterns with multi-line support
- ✅ MIT License - Free to use in any project
Add to your Cargo.toml:
[dependencies]
emx-testspec = "0.1"Or use via Git:
[dependencies]
emx-testspec = { git = "https://github.com/coreseekdev/emx-testspec" }Requires emx-txtar as a dependency.
Create tests/mytool.txtar:
# Test basic functionality
exec mytool input.txt
stdout 'success'
-- input.txt --
test data
Run tests:
use emx_testspec::run_and_assert;#[test]fntest_mytool(){run_and_assert("tests");}Install the CLI:
cargo install emx-testspec --git https://github.com/coreseekdev/emx-testspecRun tests:
emx-testspec tests/ # Run all tests
emx-testspec tests/test.txtar # Run single test
emx-testspec tests/ -v # Verbose output
emx-testspec tests/ -f "basic"# Filter by name
emx-testspec tests/ --keep # Preserve work directories| Command | Description | Example |
|---|---|---|
exec | Execute a command | exec mytool arg1 arg2 |
stdout | Match stdout with regex | stdout 'expected' |
stderr | Match stderr with regex | stderr 'error message' |
cmp | Compare files | cmp file1 file2 |
cmpenv | Compare with env expansion | cmpenv file1 file2 |
grep | Search in file | grep 'pattern' file |
cat | Print file contents | cat file |
cd | Change directory | cd subdir |
cp | Copy files | cp src dst |
mv | Move/rename files | mv old new |
rm | Remove files | rm file |
mkdir | Create directories | mkdir dir |
exists | Check file existence | exists file |
env | Set/get environment | env KEY=value |
echo | Print to stdout buffer | echo text |
sleep | Wait for duration | sleep 1s |
stop | Stop test (non-error) | stop 'reason' |
skip | Skip test | skip 'reason' |
help | List commands | help |
!- Command must fail (exit code ≠ 0)?- Command may succeed or fail- No prefix - Command must succeed (exit code = 0)
Example:
exec mytool --valid # Must succeed
! exec mytool --invalid # Must fail
? exec mytool --unpredictable # Either is OK
Conditional execution based on platform:
[unix] exec unix-tool
[windows] exec windows-tool
[linux] exec linux-tool
[amd64] exec x86_64-tool
[!windows] exec not-windows-tool
[exec:python] exec python-script
$WORKor$TMPDIR- Temporary working directory$PWD- Current working directory${/}- OS path separator (/or\)${:}- Path list separator (:or;)
env KEY=value
echo $KEY
In comparison:
cmp stdout $WORK/expected.txt
stdout and stderr can be used as virtual files:
exec mytool
cmp stdout expected.txt
cp stderr debug.log
exec server &
sleep 2s
exec client --request
wait
exec mytool
stdout 'line1.*line2.*line3'
exec mytool input.txt
cmp stdout output.txt
If files differ, unified diff is shown.
env NAME=World
echo "Hello $NAME"
In cmpenv, environment variables are expanded before comparison.
use emx_testspec::{TestRunner,RunConfig};let config = RunConfig{dir:"tests".into(),filter:None,workdir_root:None,preserve_work:false,verbose:false,extensions:vec![".txtar".into()],setup:None,};let runner = TestRunner::new(config);let result = runner.run_all()?;assert!(result.all_passed());use emx_testspec::{Engine,Cmd,CmdResult};use anyhow::Result;structMyCustomCmd;implCmdforMyCustomCmd{fnusage(&self) -> (String,String){("mycmd".into(),"[args]".into())}fnrun(&self,args:&[String],state:&mutState) -> Result<CmdResult>{// Custom logic hereOk(CmdResult::Success)}}// Register custom commandletmut engine = Engine::new();
engine.register_command("mycmd",Box::new(MyCustomCmd));| Variable | Effect |
|---|---|
TESTSCRIPT_VERBOSE=1 | Enable verbose logging |
TESTSCRIPT_WORK=1 | Preserve working directories |
See spec/00-princ-11-tool-testspec.mx for the full grammar specification.
script-line = prefix? condition* command-name arg* inline-comment?
prefix = "!" | "?"
condition = "[" "!"? cond-name (":" suffix)? "]"
command = identifier
arg = quoted-arg | unquoted-arg
quoted-arg = "'" (char | "''")* "'"
MIT License - see LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
- emx-txtar - Txtar archive format
- Go testscript - Original inspiration
- roguepeppe/go-internal/testscript - Enhanced Go version