Skip to content

Use custom test framework to cargo test ! #41

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ctru-rs/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -61,6 +64,9 @@ cfg_if::cfg_if! {
}
}

#[cfg(test)]
mod test_runner;

pub use crate::error::{Error, Result};

pub use crate::gfx::Gfx;
Expand Down
56 changes: 56 additions & 0 deletions ctru-rs/src/services/ps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<i32, String> = 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<i32, String> = 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");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit weird, hence the large comment, and relies on some implementation details of std, which I don't love. Here is the output I was referencing:

tests_Pass

Perhaps it makes more sense to remove this test case entirely, since we don't necessarily care if HashMap::new panics, but we do want the positive test case of being able to construct one when we have initialized PS. Let me know what you think or if you have a cleaner suggestion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this will be fixed once we move to std threads: rust-lang/rust#78227

I think we should keep this test regardless.

}
}
126 changes: 126 additions & 0 deletions ctru-rs/src/test_runner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//! 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);

// Start printing from the top left
print!("\x1b[1;1H");

// 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<bool> {
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
}
}