diff --git a/ctru-rs/src/lib.rs b/ctru-rs/src/lib.rs index 5f487a9..c66ff31 100644 --- a/ctru-rs/src/lib.rs +++ b/ctru-rs/src/lib.rs @@ -1,5 +1,8 @@ #![crate_type = "rlib"] #![crate_name = "ctru"] +#![feature(test)] +#![feature(custom_test_frameworks)] +#![test_runner(test_runner::run)] /// Call this somewhere to force Rust to link some required crates /// This is also a setup for some crate integration only available at runtime @@ -61,6 +64,9 @@ cfg_if::cfg_if! { } } +#[cfg(test)] +mod test_runner; + pub use crate::error::{Error, Result}; pub use crate::gfx::Gfx; diff --git a/ctru-rs/src/services/ps.rs b/ctru-rs/src/services/ps.rs index 5b1ca27..b6f1702 100644 --- a/ctru-rs/src/services/ps.rs +++ b/ctru-rs/src/services/ps.rs @@ -83,3 +83,59 @@ impl Drop for Ps { } } } + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + #[test] + fn construct_hash_map() { + let _ps = Ps::init().unwrap(); + + let mut input = vec![ + (1_i32, String::from("123")), + (2, String::from("2")), + (6, String::from("six")), + ]; + + let map: HashMap = HashMap::from_iter(input.clone()); + + let mut actual: Vec<_> = map.into_iter().collect(); + input.sort(); + actual.sort(); + + assert_eq!(input, actual); + } + + #[test] + fn construct_hash_map_no_rand() { + // Without initializing PS, we can't use `libc::getrandom` and constructing + // a HashMap panics at runtime. + // + // If any test case successfully creates a HashMap before this test, + // the thread-local RandomState in std will be initialized. We spawn + // a new thread to actually create the hash map, since even in multi-threaded + // test environment there's a chance this test wouldn't panic because + // some other test case ran before it. + // + // One downside of this approach is that the panic handler for the panicking + // thread prints to the console, which is not captured by the default test + // harness and prints even when the test passes. + crate::thread::Builder::new() + .stack_size(0x20_0000) + .spawn(|| { + let map: HashMap = HashMap::from_iter([ + (1_i32, String::from("123")), + (2, String::from("2")), + (6, String::from("six")), + ]); + + dbg!(map); + }) + .unwrap() + .join() + .expect_err("should have panicked"); + } +} diff --git a/ctru-rs/src/test_runner.rs b/ctru-rs/src/test_runner.rs new file mode 100644 index 0000000..ff3a459 --- /dev/null +++ b/ctru-rs/src/test_runner.rs @@ -0,0 +1,123 @@ +//! Custom test runner for building/running unit tests on the 3DS. + +extern crate test; + +use std::io; + +use test::{ColorConfig, Options, OutputFormat, RunIgnored, TestDescAndFn, TestFn, TestOpts}; + +use crate::console::Console; +use crate::gfx::Gfx; +use crate::services::hid::{Hid, KeyPad}; +use crate::services::Apt; + +/// A custom runner to be used with `#[test_runner]`. This simple implementation +/// runs all tests in series, "failing" on the first one to panic (really, the +/// panic is just treated the same as any normal application panic). +pub(crate) fn run(tests: &[&TestDescAndFn]) { + crate::init(); + + let gfx = Gfx::default(); + let hid = Hid::init().unwrap(); + let apt = Apt::init().unwrap(); + + let mut top_screen = gfx.top_screen.borrow_mut(); + top_screen.set_wide_mode(true); + let _console = Console::init(top_screen); + + // TODO: it would be nice to have a way of specifying argv to make these + // configurable at runtime, but I can't figure out how to do it easily, + // so for now, just hardcode everything. + let opts = TestOpts { + list: false, + filters: Vec::new(), + filter_exact: false, + // Forking is not supported + force_run_in_process: true, + exclude_should_panic: false, + run_ignored: RunIgnored::No, + run_tests: true, + // Don't run benchmarks. We may want to create a separate runner for them in the future + bench_benchmarks: false, + logfile: None, + nocapture: false, + // TODO: color doesn't work because of TERM/TERMINFO. + // With RomFS we might be able to fake this out nicely... + color: ColorConfig::AutoColor, + format: OutputFormat::Pretty, + shuffle: false, + shuffle_seed: None, + test_threads: None, + skip: Vec::new(), + time_options: None, + options: Options::new(), + }; + + // Use the default test implementation with our hardcoded options + let _success = run_static_tests(&opts, tests).unwrap(); + + // Make sure the user can actually see the results before we exit + println!("Press START to exit."); + + while apt.main_loop() { + gfx.flush_buffers(); + gfx.swap_buffers(); + gfx.wait_for_vblank(); + + hid.scan_input(); + if hid.keys_down().contains(KeyPad::KEY_START) { + break; + } + } +} + +/// Adapted from [`test::test_main_static`] and [`test::make_owned_test`]. +fn run_static_tests(opts: &TestOpts, tests: &[&TestDescAndFn]) -> io::Result { + let tests = tests.iter().map(make_owned_test).collect(); + test::run_tests_console(opts, tests) +} + +/// Clones static values for putting into a dynamic vector, which test_main() +/// needs to hand out ownership of tests to parallel test runners. +/// +/// This will panic when fed any dynamic tests, because they cannot be cloned. +fn make_owned_test(test: &&TestDescAndFn) -> TestDescAndFn { + match test.testfn { + TestFn::StaticTestFn(f) => TestDescAndFn { + testfn: TestFn::StaticTestFn(f), + desc: test.desc.clone(), + }, + TestFn::StaticBenchFn(f) => TestDescAndFn { + testfn: TestFn::StaticBenchFn(f), + desc: test.desc.clone(), + }, + _ => panic!("non-static tests passed to test::test_main_static"), + } +} + +/// The following functions are stubs needed to link the test library, +/// but do nothing because we don't actually need them for the runner to work. +mod link_fix { + #[no_mangle] + extern "C" fn execvp( + _argc: *const libc::c_char, + _argv: *mut *const libc::c_char, + ) -> libc::c_int { + -1 + } + + #[no_mangle] + extern "C" fn pipe(_fildes: *mut libc::c_int) -> libc::c_int { + -1 + } + + #[no_mangle] + extern "C" fn sigemptyset(_arg1: *mut libc::sigset_t) -> ::libc::c_int { + -1 + } + + #[no_mangle] + extern "C" fn sysconf(_name: libc::c_int) -> libc::c_long { + -1 + } +}