blob: 08475dfe04bd7ff632d6d2953342c66d94825e47 [file] [log] [blame] [edit]
//! This crate provides a simple language testing framework designed to help when you are testing
//! things like compilers and virtual machines. It allows users to express simple tests for process
//! success/failure and for stderr/stdout, including embedding those tests directly in the source
//! file. It is loosely based on the [`compiletest_rs`](https://crates.io/crates/compiletest_rs)
//! crate, but is much simpler (and hence sometimes less powerful), and designed to be used for
//! testing non-Rust languages too.
//!
//! For example, a Rust language tester, loosely in the spirit of
//! [`compiletest_rs`](https://crates.io/crates/compiletest_rs), looks as follows:
//!
//! ```rust,ignore
//! use std::{fs::read_to_string, path::PathBuf, process::Command};
//!
//! use lang_tester::LangTester;
//! use tempfile::TempDir;
//!
//! static COMMENT_PREFIX: &str = "//";
//!
//! fn main() {
//! // We use rustc to compile files into a binary: we store those binary files
//! // into `tempdir`. This may not be necessary for other languages.
//! let tempdir = TempDir::new().unwrap();
//! LangTester::new()
//! .test_dir("examples/rust_lang_tester/lang_tests")
//! // Only use files named `*.rs` as test files.
//! .test_path_filter(|p| p.extension().and_then(|x| x.to_str()) == Some("rs"))
//! // Treat lines beginning with "#" as comments.
//! .comment_prefix("#")
//! // Extract the first sequence of commented line(s) as the tests.
//! .test_extract(|p| {
//! read_to_string(p)
//! .unwrap()
//! .lines()
//! // Skip non-commented lines at the start of the file.
//! .skip_while(|l| !l.starts_with(COMMENT_PREFIX))
//! // Extract consecutive commented lines.
//! .take_while(|l| l.starts_with(COMMENT_PREFIX))
//! .map(|l| &l[COMMENT_PREFIX.len()..])
//! .collect::<Vec<_>>()
//! .join("\n")
//! })
//! // We have two test commands:
//! // * `Compiler`: runs rustc.
//! // * `Run-time`: if rustc does not error, and the `Compiler` tests
//! // succeed, then the output binary is run.
//! .test_cmds(move |p| {
//! // Test command 1: Compile `x.rs` into `tempdir/x`.
//! let mut exe = PathBuf::new();
//! exe.push(&tempdir);
//! exe.push(p.file_stem().unwrap());
//! let mut compiler = Command::new("rustc");
//! compiler.args(&["-o", exe.to_str().unwrap(), p.to_str().unwrap()]);
//! // Test command 2: run `tempdir/x`.
//! let runtime = Command::new(exe);
//! vec![("Compiler", compiler), ("Run-time", runtime)]
//! })
//! .run();
//! }
//! ```
//!
//! This defines a lang tester that uses all `*.rs` files in a given directory as test files,
//! running two test commands against them: `Compiler` (i.e. `rustc`); and `Run-time` (the compiled
//! binary).
//!
//! Users can then write test files such as the following:
//!
//! ```rust,ignore
//! // Compiler:
//! // stderr:
//! // warning: unused variable: `x`
//! // ...unused_var.rs:12:9
//! // ...
//! //
//! // Run-time:
//! // stdout: Hello world
//! fn main() {
//! let x = 0;
//! println!("Hello world");
//! }
//! ```
//!
//! `lang_tester` is entirely ignorant of the language being tested, leaving it entirely to the
//! user to determine what the test data in/for a file is. In this case, since we are embedding the
//! test data as a Rust comment at the start of the file, the `test_extract` function we specified
//! returns the following string:
//!
//! ```text
//! Compiler:
//! stderr:
//! warning: unused variable: `x`
//! ...unused_var.rs:12:9
//! ...
//!
//! Run-time:
//! stdout: Hello world
//! ```
//!
//! Test data is specified with a two-level indentation syntax: the outer most level of indentation
//! defines a test command (multiple command names can be specified, as in the above); the inner
//! most level of indentation defines alterations to the general command or sub-tests. Multi-line
//! values are stripped of their common indentation, such that:
//!
//! ```text
//! x:
//! a
//! b
//! c
//! ```
//!
//! defines a test command `x` with a value `a\n b\nc`. Trailing whitespace is preserved.
//!
//! String matching is performed by the [fm crate](https://crates.io/crates/fm), which provides
//! support for `...` operators and so on. Unless `lang_tester` is explicitly instructed otherwise,
//! it uses `fm`'s defaults. In particular, even though `lang_tester` preserves (some) leading and
//! (all) trailing whitespace, `fm` ignores leading and trailing whitespace by default (though this
//! can be changed).
//!
//! Each test command must define at least one sub-test:
//!
//! * `status: <success|error|signal|<int>>`, where `success` and `error` map to platform
//! specific notions of a command completing successfully or unsuccessfully respectively.
//! `signal` checks for termination due to a signal on Unix platforms; on non-Unix platforms,
//! the test will be ignored. `<int>` is a signed integer checking for a specific exit code on
//! platforms that support it. If not specified, defaults to `success`.
//! * `stderr: [<string>]`, `stdout: [<string>]` match `<string>` against a command's `stderr` or
//! `stdout`. The special string `...` can be used as a simple wildcard: if a line consists
//! solely of `...`, it means "match zero or more lines"; if a line begins with `...`, it means
//! "match the remainder of the line only"; if a line ends with `...`, it means "match the
//! start of the line only". A line may start and end with `...`. Note that `stderr`/`stdout`
//! matches ignore leading/trailing whitespace and newlines, but are case sensitive. If not
//! specified, defaults to `...` (i.e. match anything). Note that the empty string matches only
//! the empty string so e.g. `stderr:` on its own means that a command's `stderr` muct not
//! contain any output.
//!
//! Test commands can alter the general command by specifying zero or more of the following:
//!
//! * `env-var: <key>=<string>` will set (or override if it is already present) the environment
//! variable `<key>` to the value `<string>`. `env-var` can be specified multiple times, each
//! setting an additional (or overriding an existing) environment variable.
//! * `exec-arg: <string>` specifies a string which will be passed as an additional command-line
//! argument to the command (in addition to those specified by the `test_cmds` function).
//! Multiple `exec-arg`s can be specified, each adding an additional command-line argument.
//! * `stdin: <string>` specifies text to be passed to the command's `stdin`. If the command
//! exits without consuming all of `<string>`, an error will be raised. Note, though, that
//! operating system file buffers can mean that the command *appears* to have consumed all of
//! `<string>` without it actually having done so.
//!
//! Test commands can specify that a test should be rerun if one of the following (optional) is
//! specified and it matches the test's output:
//!
//! * `rerun-if-status` follows the same format as the `status`.
//! * `rerun-if-stderr` and `rerun-if-stdout` follow the same format as `stderr` and `stdout`.
//!
//! These can be useful if tests are subject to intermittent errors (e.g. network failure) that
//! should not be considered as a failure of the test itself. Test commands are rerun at most *n*
//! times, which by default is specified as 3. If no `rerun-if-` is specified, then the first time
//! a test fails, it will be reported to the user.
//!
//! The above file thus contains 4 meaningful tests, two specified by the user and two implied by
//! defaults: the `Compiler` should succeed (e.g. return a `0` exit code when run on Unix), and
//! its `stderr` output should warn about an unused variable on line 12; and the resulting binary
//! should succeed produce `Hello world` on `stdout`.
//!
//! A file's tests can be ignored entirely with:
//!
//! * `ignore-if: <cmd>` defines a shell command that will be run to determine whether to ignore
//! this test or not. If `<cmd>` returns 0 the test will be ignored, otherwise it will be run.
//!
//! `lang_tester`'s output is deliberately similar to Rust's normal testing output. Running the
//! example `rust_lang_tester` in this crate produces the following output:
//!
//! ```text
//! $ cargo run --example=rust_lang_tester
//! Compiling lang_tester v0.1.0 (/home/ltratt/scratch/softdev/lang_tester)
//! Finished dev [unoptimized + debuginfo] target(s) in 3.49s
//! Running `target/debug/examples/rust_lang_tester`
//!
//! running 4 tests
//! test lang_tests::no_main ... ok
//! test lang_tests::unknown_var ... ok
//! test lang_tests::unused_var ... ok
//! test lang_tests::exit_code ... ok
//!
//! test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
//! ```
//!
//! If you want to run a subset of tests, you can specify simple filters which use substring match
//! to run a subset of tests:
//!
//! ```text
//! $ cargo run --example=rust_lang_tester var
//! Compiling lang_tester v0.1.0 (/home/ltratt/scratch/softdev/lang_tester)
//! Finished dev [unoptimized + debuginfo] target(s) in 3.37s
//! Running `target/debug/examples/rust_lang_tester var`
//!
//! running 2 tests
//! test lang_tests::unknown_var ... ok
//! test lang_tests::unused_var ... ok
//!
//! test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
//! ```
//!
//! ## Integration with Cargo.
//!
//! Tests created with lang_tester can be used as part of an existing test suite and can be run
//! with the `cargo test` command. For example, if the Rust source file that runs your lang tests
//! is `lang_tests/run.rs` then add the following to your Cargo.toml:
//!
//! ```text
//! [[test]]
//! name = "lang_tests"
//! path = "lang_tests/run_tests.rs"
//! harness = false
//! ```
#![allow(clippy::needless_doctest_main)]
#![allow(clippy::new_without_default)]
#![allow(clippy::redundant_closure)]
#![allow(clippy::type_complexity)]
mod parser;
mod tester;
pub use tester::LangTester;
pub(crate) fn fatal(msg: &str) -> ! {
eprintln!("\nFatal exception:\n {}", msg);
std::process::exit(1);
}