reifydb_testing/testscript/mod.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4// This file includes portions of code from https://github.com/erikgrinaker/goldenscript (Apache 2 License).
5// Original Apache 2 License Copyright (c) erikgrinaker 2024.
6
7//! This crate provides the testscript testing framework, loosely based on
8//! Cockroach Labs' [`datadriven`](https://github.com/cockroachdb/datadriven)
9//! framework for Go. It combines several testing techniques that make it easy
10//! and efficient to write and update test cases:
11//!
12//! * [Golden master testing](https://en.wikipedia.org/wiki/Characterization_test) (aka characterization testing or
13//! historical oracle)
14//! * [Data-driven testing](https://en.wikipedia.org/wiki/Data-driven_testing) (aka table-driven testing or
15//! parameterized testing)
16//! * [Keyword-driven testing](https://en.wikipedia.org/wiki/Keyword-driven_testing)
17//!
18//! A testscript is a plain text file that contains a set of arbitrary input
19//! commands and their expected text output, separated by `---`:
20//!
21//! ```text
22//! command
23//! ---
24//! output
25//!
26//! command argument
27//! command key=value
28//! ---
29//! output
30//! ```
31//!
32//! The commands are executed by a provided [`Runner`]. The expected output is
33//! usually not written by hand, but instead generated by running tests with the
34//! environment variable `UPDATE_TESTFILES=1`:
35//!
36//! ```sh
37//! $ UPDATE_TESTFILES=1 cargo test
38//! ```
39//!
40//! The files are then verified by inspection and checked in to version control.
41//! Tests will fail with a diff if they don't match the expected output.
42//!
43//! This approach is particularly useful when testing complex
44//! systems, such as database operations, network protocols, or language
45//! parsing. It can be tedious and labor-intensive to write and assert such
46//! cases by hand, so scripting and recording these interactions often yields
47//! much better test coverage at a fraction of the cost.
48//!
49//! Internally, the
50//! [`testfile`](https://docs.rs/testfile/latest/testfile/) crate is used
51//! to manage golden files.
52//!
53//! # Examples
54//!
55//! For real-world examples, see e.g.:
56//!
57//! * [toyDB Raft](https://github.com/erikgrinaker/toydb/tree/master/src/raft/testscripts/node): distributed consensus
58//! cluster.
59//! * [toyDB MVCC](https://github.com/erikgrinaker/toydb/tree/master/src/store/testscripts/mvcc): ACID transactions.
60//! * [testscript parser](https://github.com/erikgrinaker/testscript/tree/main/tests/scripts): testscript uses itself to
61//! test its parser and runner.
62//!
63//! Below is a basic example, testing the Rust transaction library's
64//! [`BTreeMap`](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html).
65//!
66//! ```yaml
67//! # Tests the Rust transaction library BTreeMap.
68//!
69//! # Get and range returns nothing for an empty map.
70//! get foo
71//! range
72//! ---
73//! get -> None
74//!
75//! # Inserting keys out of order will return them in order. Silence the insert
76//! # output with ().
77//! (insert b=2 a=1 c=3)
78//! range
79//! ---
80//! a=1
81//! b=2
82//! c=3
83//!
84//! # Getting a key returns its value.
85//! get b
86//! ---
87//! get -> Some("2")
88//!
89//! # Bounded scans, where the end is exclusive.
90//! range b
91//! ---
92//! b=2
93//! c=3
94//!
95//! range a c
96//! ---
97//! a=1
98//! b=2
99//!
100//! # An end bound less than the start bound panics. Expect the failure with !.
101//! !range b a
102//! ---
103//! Panic: range start is greater than range end in BTreeMap
104//!
105//! # Replacing a key updates the value and returns the old one.
106//! insert b=foo
107//! get b
108//! ---
109//! insert -> Some("2")
110//! get -> Some("foo")
111//! ```
112//!
113//! # Syntax
114//!
115//! ## Blocks
116//!
117//! A testscript consists of one or more input/output blocks. Each block has a
118//! set of one or more input commands on individual lines (empty or comment
119//! lines are ignored), a `---` separator, and arbitrary output terminated by an
120//! empty line. A minimal testscript with two blocks might be:
121//!
122//! ```text
123//! command
124//! ---
125//! output
126//!
127//! command 1
128//! command 2
129//! ---
130//! output 1
131//! output 2
132//! ```
133//!
134//! ## Commands
135//!
136//! A [`Command`] must have a command name, which can be any arbitrary
137//! [string](#strings), e.g.:
138//!
139//! ```text
140//! command
141//! "command with space and š"
142//! ---
143//! ```
144//!
145//! It may additionally have:
146//!
147//! * [**Arguments:**](Argument) any number of space-separated arguments. These have a string [value](Argument::value),
148//! and optionally also a string [key](Argument::key) as `key=value`. Keys and values can be empty, and duplicate keys
149//! are allowed by the parser (the runner can handle this as desired).
150//!
151//! ```text
152//! command argument key=value
153//! command "argument with space" "key with space"="value with space"
154//! command "" key= # Empty argument values.
155//! ---
156//! ```
157//!
158//! * [**Prefix:**](Command::prefix) an optional :-terminated string prefix before the command. The command's output
159//! will be given the same prefix. The prefix can be used by the test runner, e.g. to signify two different clients.
160//!
161//! ```text
162//! client1: put key=value
163//! client2: get key
164//! ---
165//! client1: put ok
166//! client2: get key=value
167//! ```
168//!
169//! * [**Silencing:**](Command::silent) a command wrapped in `()` will have its output suppressed. This can be useful
170//! e.g. for setup commands whose output are not of interest in the current test case and would only add noise.
171//!
172//! ```text
173//! echo foo
174//! (echo bar)
175//! ---
176//! foo
177//! ```
178//!
179//! * [**Failure:**](Command::fail) if `!` precedes the command, it is expected to fail with an error or panic, and the
180//! failure message is used as output. If the command unexpectedly succeeds, the test fails. If the line contains
181//! other symbols before the command name (e.g. a prefix or silencing), the `!` must be used immediately before the
182//! command name.
183//!
184//! ```text
185//! ! command error=foo
186//! prefix: ! command panic=bar
187//! (!command error=foo)
188//! ---
189//! Error: foo
190//! prefix: Panic: bar
191//! ```
192//!
193//! * [**Tags:**](Command::tags) an optional comma- or space-separated list of tags (strings) enclosed in [] before or
194//! after the command and arguments. This can be used by the runner e.g. to modify the execution of a command.
195//!
196//! ```text
197//! command [tag]
198//! command arg key=value [a,b c]
199//! [tag] command
200//! prefix:[tag]!> command arg
201//! ---
202//! ```
203//!
204//! * **Literal:** if `>` precedes the command, the entire rest of the line is taken to be the command name (except
205//! leading whitespace). Arguments, tags, comments, and any other special characters are ignored and used as-is. As a
206//! special case (currently only with `>`), lines can fragment multiple lines by ending the line with \.
207//!
208//! ```text
209//! > a long command name including key=value, [tags], # a comment and
210//! > exclamation!
211//! prefix: [tag] ! > a long, failing command with tags and a prefix
212//! ---
213//!
214//! > a very \
215//! long line \
216//! with line \
217//! continuation
218//! ---
219//! ```
220//!
221//! ## Output
222//!
223//! The command output following a `---` separator can contain any arbitrary
224//! Unicode string until an empty line (or end of file). If the command output
225//! contains empty lines, the entire output will automatically be prefixed with
226//! `> `. If no commands in a block yield any output, it defaults to "ok".
227//!
228//! ```text
229//! echo "output 1"
230//! echo "output 2"
231//! ---
232//! output 1
233//! output 2
234//!
235//! echo "Paragraph 1.\n\nParagraph 2."
236//! ---
237//! > Paragraph 1.
238//! >
239//! > Paragraph 2.
240//!
241//! echo "č¾åŗ\n# Comment\nš"
242//! ---
243//! č¾åŗ
244//! # Comment
245//! š
246//! ```
247//!
248//! ## Comments
249//!
250//! Comments begin with `#` or `//` and run to the end of the line.
251//!
252//! ```text
253//! # This is a comment.
254//! // As is this.
255//! command argument # Comments can follow commands too.
256//! ---
257//! ```
258//!
259//! ## Strings
260//!
261//! Unquoted strings can only contain alphanumeric ASCII characters
262//! `[a-zA-Z0-9]` and a handful of special characters: `_ - . / @`
263//! (only `_` at the start of a string).
264//!
265//! Strings can be quoted using `"` or `'`, in which case they can contain
266//! arbitrary Unicode characters. `\` is used as an escape character, both to
267//! escape quotes `\"` and `\'` as well as itself `\\`, and also `\0` (null),
268//! `\n` (newline), `\r` (carriage return), and `\t` (tab). `\x` can be used to
269//! represent arbitrary hexadecimal bytes (e.g. `\x7a`) and `\u{}` can be used
270//! to represent arbitrary Unicode characters (e.g. `\u{1f44b}`)
271//! ## Managing State
272//!
273//! The runner is free to manage internal state as desired. If it is stateful,
274//! it is recommended to persist state within a single testscript (across
275//! commands and blocks), but not across testscripts since this can be hard to
276//! reason about and depend on the execution order of scripts. This is most
277//! easily done by instantiating a new runner for each script.
278//!
279//! Initial state setup should generally be done via explicit setup commands, to
280//! make it more discoverable.
281//!
282//! ## Running All Scripts in a Directory
283//!
284//! External crates can be used to automatically generate and run individual
285//! tests for each testscript in a directory. For example, the
286//! [`test_each_file`](https://docs.rs/test_each_file/latest/test_each_file/)
287//!
288//! Runners have various hooks that will be called during script execution:
289//! [`Runner::start_script`], [`Runner::end_script`], [`Runner::start_block`],
290//! [`Runner::end_block`], [`Runner::start_command`], and
291//! [`Runner::end_command`]. These can be used e.g. for initial setup, invariant
292//! assertions, or to output the current state.
293
294pub mod command;
295pub mod parser;
296pub mod runner;