# evunit
This is a unit testing application for Game Boy roms.
It only contains a CPU emulator; no PPU, memory mapper, or I/O.
By using real binaries as input, you can run unit tests on your finished ROM without any need to rebuild.
The command-line tool loads test configurations from TOML files.
You can also use it as a Rust library, and configure your tests from Rust code.
[Changelog](./CHANGELOG.md)
## Installing
`cargo install evunit`
## Configuring a test
Within the test config you can create a heading for each test you want to run, and assign default and expected values for registers.
The first heading (for example, `add-one`) determines the initial state, while the "`.result`" heading (for example, `add-one.result`) describes the expected result.
If the expected result does not match the final state, the test will fail.
```toml
[add-one]
b = 1
[add-one.result]
a = 2
[add-two]
b = 2
[add-two.result]
a = 3
```
You can assign any cpu register to an integer.
In addition, 16-bit registers may be assigned a quoted label if a symfile is loaded.
To determine which function should run in each test, assign a label to `pc`.
Possible registers are:
- `a`
- `b`
- `c`
- `d`
- `e`
- `h`
- `l`
- `bc`
- `de`
- `hl`
- `pc`
- `sp`
In addition the flags can be assigned to either `true` or `false`.
Possible flags are:
- `f.z`
- `f.n`
- `f.h`
- `f.c`
Note that the flags must be *quoted* in the config file because of the dot:
```toml
"f.z" = false
```
Finally, memory can be assigned a value in the config file by surrounding a label name or address in square brackets.
You can either assign an 8-bit integer, a string*, or an array of either.
Like the flags, memory addresses must be quoted because of the square brackets:
```toml
# Writes a string to wString, followed by a 0
"[wString]" = ["Hello, world!", 0]
# Writes a series of bytes to WRAM0
"[0xC000]" = [ 0x01, 0x02, 0x03, 0x04 ]
```
\* = Note that string are converted to their ASCII representation.
Strings containing Non-ASCII characters will return errors.
## Global configurations
Sometimes you have configurations which should apply to all tests, like a global variable or the stack pointer.
Any configurations at the top of the file (before a heading) are global and apply to all tests.
```toml
sp = "wStack.end"
[my-test]
pc = "MyTest"
a = 42
[my-test.result]
b = 42
```
If the test result is absent, the test will always pass unless it crashes.
Creating an exhaustive set of tests by hand might be tedious, so remember that you an always generate tests in Rust by using `evunit` as a library:.
```rust,ignore
use evunit::prelude::*;
use std::{path::Path, process::exit};
let rom = "bin.gb";
let sym = Some(Path::new("bin.sym"));
let symfile = read_symfile(sym);
let mut tests = Vec::new();
for i in 0..8 {
let mut test = TestConfig::new(format!("my-test{i}"));
// Initial state
test.initial = Registers::new()
.with_pc(symfile["GetBitA"].0 as u16)
.with_a(i);
// Expected state
test.result = Some(Registers::new()
.with_a(i));
tests.push(test);
}
let result = run_tests(&rom, &tests, SilenceLevel::Passing);
if result.is_err() {
exit(1);
}
```
Then pipe this into evunit.
You can use `-` to read from stdin.
```sh
./config_generator | evunit -c - bin/rom.gb
```
And you can always use `cat` to add a handwritten file into the mix.
```sh
./config_generator | cat config.toml - | evunit -c - bin/rom.gb
```
## Terminating a test
A test is complete when either a crash address is reached, the test times out, or `pc` is equal to the `caller` specified in the config file (default is `0xFFFF`).
evunit pushes the `caller` value to the stack before running your test, meaning that in most scenarios a `ret` will end the test.
When the `caller` value is successfully reached, evunit checks to see if the result matches what was expected.
## Configuration options
In addition to registers, there are a few other options you can configure.
All of these can be configured globally as well as per-test.
### caller
Sets the caller address.
This address is pushed to the stack when a test begins, allowing `ret` to end the test.
```toml
caller = "Main"
```
By default, `caller` is set to `0xFFFF`.
### crash
Marks an address as a "crash", causing the test to fail if `pc` reaches it.
This is useful for crash handler functions such as `rst $38`
```toml
crash = 0x38
```
An array of values can also be used.
```toml
crash = [0x38, "crash"]
```
### enable-breakpoints
Enables or disables printing register info after executing `ld b, b` and `ld d, d`.
Enabled by default.
This configuration can only be used globally.
```toml
enable-breakpoints = true
enable-breakpoints = false
```
### exit
Marks an address as an "exit", causing the test to end if `pc` reaches it.
The results will then be verified.
```toml
exit = "SomeFunction.exit"
```
An array of values can also be used.
### timeout
Sets the maximum number of cycles before a test fails.
This is useful if you have code that tends to get stuck in infinite loops, or code which can take an extremely long time to complete.
The default value is 65536.
```toml
timeout = 65536
```
### stack
Specifies data to be pushed onto the stack prior to a test being ran (just before `caller` is pushed).
Some functions expect their arguments to be on the stack,
so this option allows to do this.
You can either assign an 8-bit integer, a string, or an array of either.
```toml
stack = [ 0x04, 0x71, 0xff, "\n" ]
```
Due note that values are pushed to the stack in reverse.
As an example, the initial values on the stack for the example above look like the following (assuming `sp` = `0xD000`):
```
| 0xCFFF | 0x0A |
| 0xCFFF | 0xFF |
| 0xCFFF | 0x71 |
| 0xCFFE | 0x04 |
| 0xCFFD | high(caller) |
| 0xCFFC | low(caller) |
```
## Diagnosing failures
When a test fails, it outputs some cpu registers depending on the failure reason to help you diagnose the issue.
However, sometimes you need to check the state of memory as well; this can be accomplished with the `--dump-dir` (`-d`) flag.
Pass a directory to this flag and when any test fails a text dump of memory will be placed in the provided directory.
```bash
evunit -c fail.toml -d dump/ rom.gb
```
The dump is simply a giant list of bytes, with headers for each memory type:
```not_rust
[WRAM 0]
[WRAM]
0xc000: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc010: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc020: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc030: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc040: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc050: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0xc060: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
...
```