agb/mgba-test-runner/src/main.rs
2021-07-03 22:19:10 +01:00

161 lines
4.8 KiB
Rust

#![allow(clippy::all)]
mod runner;
use anyhow::{anyhow, Error};
use image::io::Reader;
use io::Write;
use regex::Regex;
use runner::VideoBuffer;
use std::io;
use std::path::Path;
#[derive(PartialEq, Eq, Debug, Clone)]
enum Status {
Running,
Failed,
Sucess,
}
enum Timing {
None,
WaitFor(i32),
Difference(i32),
}
const TEST_RUNNER_TAG: u16 = 785;
fn test_file(file_to_run: &str) -> Status {
let mut finished = Status::Running;
let debug_reader_mutex = Regex::new(r"(?s)^\[(.*)\] GBA Debug: (.*)$").unwrap();
let tagged_cycles_reader = Regex::new(r"Cycles: (\d*) Tag: (\d*)").unwrap();
let mut mgba = runner::MGBA::new(file_to_run).unwrap();
let video_buffer = mgba.get_video_buffer();
let mut number_of_cycles = Timing::None;
mgba.set_logger(|message| {
if let Some(captures) = debug_reader_mutex.captures(message) {
let log_level = &captures[1];
let out = &captures[2];
if out.starts_with("image:") {
let image_path = out.strip_prefix("image:").unwrap();
match check_image_match(image_path, &video_buffer) {
Err(e) => {
println!("[failed]");
println!("{}", e);
finished = Status::Failed;
}
Ok(_) => {}
}
} else if out.ends_with("...") {
print!("{}", out);
io::stdout().flush().expect("can't flush stdout");
} else if out.starts_with("Cycles: ") {
if let Some(captures) = tagged_cycles_reader.captures(out) {
let num_cycles: i32 = captures[1].parse().unwrap();
let tag: u16 = captures[2].parse().unwrap();
if tag == TEST_RUNNER_TAG {
number_of_cycles = match number_of_cycles {
Timing::WaitFor(n) => Timing::Difference(num_cycles - n),
Timing::None => Timing::WaitFor(num_cycles),
Timing::Difference(_) => Timing::WaitFor(num_cycles),
};
}
}
} else if out == "[ok]" {
if let Timing::Difference(cycles) = number_of_cycles {
println!(
"[ok: {} c ≈ {} s]",
cycles,
((cycles as f64 / (16.78 * 1_000_000.0)) * 100.0).round() / 100.0
);
} else {
println!("{}", out);
}
} else {
println!("{}", out);
}
if log_level == "FATAL" {
finished = Status::Failed;
}
if out == "Tests finished successfully" {
finished = Status::Sucess;
}
}
});
loop {
mgba.advance_frame();
if finished != Status::Running {
break;
}
}
return finished;
}
fn main() -> Result<(), Error> {
let args: Vec<String> = std::env::args().collect();
let file_to_run = args.get(1).expect("you should provide file to run");
if !Path::new(file_to_run).exists() {
return Err(anyhow!("File to run should exist!"));
}
let output = test_file(file_to_run);
match output {
Status::Failed => Err(anyhow!("Tests failed!")),
Status::Sucess => Ok(()),
_ => {
unreachable!("very bad thing happened");
}
}
}
fn gba_colour_to_rgba(colour: u32) -> [u8; 4] {
[
((colour >> 0) & 0xFF) as u8,
((colour >> 8) & 0xFF) as u8,
((colour >> 16) & 0xFF) as u8,
255,
]
}
fn rgba_to_gba_to_rgba(c: [u8; 4]) -> [u8; 4] {
let mut n = c.clone();
n.iter_mut()
.for_each(|a| *a = ((((*a as u32 >> 3) << 3) * 0x21) >> 5) as u8);
n
}
fn check_image_match(image_path: &str, video_buffer: &VideoBuffer) -> Result<(), Error> {
let expected_image = Reader::open(image_path)?.decode()?;
let expected = expected_image
.as_rgba8()
.ok_or(anyhow!("cannot convert to rgba8"))?;
let (buf_dim_x, buf_dim_y) = video_buffer.get_size();
let (exp_dim_x, exp_dim_y) = expected.dimensions();
if (buf_dim_x != exp_dim_x) || (buf_dim_y != exp_dim_y) {
return Err(anyhow!("image sizes do not match"));
}
for y in 0..buf_dim_y {
for x in 0..buf_dim_x {
let video_pixel = video_buffer.get_pixel(x, y);
let image_pixel = expected.get_pixel(x, y);
let video_pixel = gba_colour_to_rgba(video_pixel);
let image_pixel = rgba_to_gba_to_rgba(image_pixel.0);
if image_pixel != video_pixel {
return Err(anyhow!("images do not match"));
}
}
}
Ok(())
}