Unit testing of individual parsers
Spicy comes with tools which given a Spicy grammar can read input from stdin or
file and forward to a parser (any public
unit
in the grammar):
spicy-dump
additionally prints structured rendering of parsed unitsspicy-driver
runs parser, outputs anyprint
statements- use
-f
to read from file instead of stdin
Often spicy-dump
is less intrusive since it requires not grammar changes.
Given a grammar
module foo;
public type X = unit {
a: uint8;
b: bytes &until=b"\x00";
};
To parse the input b"\x00foo\x00"
to this parser we could feed it data with
Bash's printf
builtin.
$ printf '\x01foo\x00' | spicy-dump -d foo.spicy
foo::X {
a: 1
b: foo
}
If using e.g., BTest we can snapshots this output to make sure it stays the same, e.g.,
# @TEST-EXEC: printf '\x01foo\x00' | spicy-dump -d foo.spicy >output-foo 2>&1
# @TEST-EXEC: btest-diff output-foo
The default BTest setup generated by the package template sets the environment
variable DIST
to the root directory of the analyzer.
# ...
[environment]
DIST=%(testbase)s/..
# ...
Use this variable to access the original grammar in tests, e.g.,
# @TEST-EXEC: spicyc -d "${DIST}/analyzer/foo.spicy" -o foo.hlto
General tips
- often Spicy grammars compile faster in debug mode
-d
, default to this in tests for faster turnaround - make sure to not accidentally append unintended newlines to input, e.g., use
echo -n
instead of plainecho
- Bash's
printf
builtin can be used to create binary data - select parser by passing
-p module::Unit
if the grammar contains multiple entry points
Avoiding repeated analyzer recompilations
Since above spicy-dump
invocation needs to compiles the full parser, consider compiling once to an HLTO file and reusing it for multiple checks in the same test, e.g.,
## Compile grammar.
# @TEST-EXEC: spicyc -d "${DIST}/analyzer/foo.spicy" -o foo.hlto
#
## Run tests.
# @TEST-EXEC: printf '\x01foo\x00' | spicy-dump -d foo.hlto >>output-foo 2>&1
# @TEST-EXEC: printf '\x02bar\x00' | spicy-dump -d foo.hlto >>output-foo 2>&1
Adding additional code for test
We might want to add additional code for testing only, e.g., add additional
logging, or check state with
assert
or assert-exception
.
We can add testing-only module to the compilation during test, e.g.,
# @TEST-EXEC: spicyc -dj "${DIST}/analyzer/foo.spicy" %INPUT -o http.hlto
#
# @TEST-EXEC: printf '\x01foo\x00' | spicy-dump -d foo.hlto >>output-foo 2>&1
# @TEST-EXEC: printf '\x02bar\x00' | spicy-dump -d foo.hlto >>output-foo 2>&1
#
# @TEST-EXEC: btest-diff output
module test;
import foo;
on foo::Something { print self; }
Since one can implement hooks even for non-public
units this is pretty powerful;
e.g., we can use this technique to observe data in anonymous fields,
module foo;
public type TcpMessages = unit {
: Message[]; # Anonymous field since list is unbounded.
};
type Message = unit {
# Consume and parse input ...
};
# In test code print individual message.
module test;
import foo;
on foo::Message { print self; }