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-dumpadditionally prints structured rendering of parsed unitsspicy-driverruns parser, outputs anyprintstatements- use
-fto 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 -ninstead of plainecho - Bash's
printfbuiltin can be used to create binary data - select parser by passing
-p module::Unitif 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; }