Skip to content

Commit 56f4b33

Browse files
authored
Merge pull request #41 from ian-h-chamberlain/testing/custom-test-framework
Use custom test framework to `cargo test` !
2 parents b835695 + 91c2c58 commit 56f4b33

File tree

3 files changed

+185
-0
lines changed

3 files changed

+185
-0
lines changed

ctru-rs/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#![crate_type = "rlib"]
22
#![crate_name = "ctru"]
3+
#![feature(test)]
4+
#![feature(custom_test_frameworks)]
5+
#![test_runner(test_runner::run)]
36

47
/// Call this somewhere to force Rust to link some required crates
58
/// This is also a setup for some crate integration only available at runtime
@@ -61,6 +64,9 @@ cfg_if::cfg_if! {
6164
}
6265
}
6366

67+
#[cfg(test)]
68+
mod test_runner;
69+
6470
pub use crate::error::{Error, Result};
6571

6672
pub use crate::gfx::Gfx;

ctru-rs/src/services/ps.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,59 @@ impl Drop for Ps {
8383
}
8484
}
8585
}
86+
87+
#[cfg(test)]
88+
mod tests {
89+
use std::collections::HashMap;
90+
91+
use super::*;
92+
93+
#[test]
94+
fn construct_hash_map() {
95+
let _ps = Ps::init().unwrap();
96+
97+
let mut input = vec![
98+
(1_i32, String::from("123")),
99+
(2, String::from("2")),
100+
(6, String::from("six")),
101+
];
102+
103+
let map: HashMap<i32, String> = HashMap::from_iter(input.clone());
104+
105+
let mut actual: Vec<_> = map.into_iter().collect();
106+
input.sort();
107+
actual.sort();
108+
109+
assert_eq!(input, actual);
110+
}
111+
112+
#[test]
113+
fn construct_hash_map_no_rand() {
114+
// Without initializing PS, we can't use `libc::getrandom` and constructing
115+
// a HashMap panics at runtime.
116+
//
117+
// If any test case successfully creates a HashMap before this test,
118+
// the thread-local RandomState in std will be initialized. We spawn
119+
// a new thread to actually create the hash map, since even in multi-threaded
120+
// test environment there's a chance this test wouldn't panic because
121+
// some other test case ran before it.
122+
//
123+
// One downside of this approach is that the panic handler for the panicking
124+
// thread prints to the console, which is not captured by the default test
125+
// harness and prints even when the test passes.
126+
crate::thread::Builder::new()
127+
.stack_size(0x20_0000)
128+
.spawn(|| {
129+
let map: HashMap<i32, String> = HashMap::from_iter([
130+
(1_i32, String::from("123")),
131+
(2, String::from("2")),
132+
(6, String::from("six")),
133+
]);
134+
135+
dbg!(map);
136+
})
137+
.unwrap()
138+
.join()
139+
.expect_err("should have panicked");
140+
}
141+
}

ctru-rs/src/test_runner.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//! Custom test runner for building/running unit tests on the 3DS.
2+
3+
extern crate test;
4+
5+
use std::io;
6+
7+
use test::{ColorConfig, Options, OutputFormat, RunIgnored, TestDescAndFn, TestFn, TestOpts};
8+
9+
use crate::console::Console;
10+
use crate::gfx::Gfx;
11+
use crate::services::hid::{Hid, KeyPad};
12+
use crate::services::Apt;
13+
14+
/// A custom runner to be used with `#[test_runner]`. This simple implementation
15+
/// runs all tests in series, "failing" on the first one to panic (really, the
16+
/// panic is just treated the same as any normal application panic).
17+
pub(crate) fn run(tests: &[&TestDescAndFn]) {
18+
crate::init();
19+
20+
let gfx = Gfx::default();
21+
let hid = Hid::init().unwrap();
22+
let apt = Apt::init().unwrap();
23+
24+
let mut top_screen = gfx.top_screen.borrow_mut();
25+
top_screen.set_wide_mode(true);
26+
let _console = Console::init(top_screen);
27+
28+
// TODO: it would be nice to have a way of specifying argv to make these
29+
// configurable at runtime, but I can't figure out how to do it easily,
30+
// so for now, just hardcode everything.
31+
let opts = TestOpts {
32+
list: false,
33+
filters: Vec::new(),
34+
filter_exact: false,
35+
// Forking is not supported
36+
force_run_in_process: true,
37+
exclude_should_panic: false,
38+
run_ignored: RunIgnored::No,
39+
run_tests: true,
40+
// Don't run benchmarks. We may want to create a separate runner for them in the future
41+
bench_benchmarks: false,
42+
logfile: None,
43+
nocapture: false,
44+
// TODO: color doesn't work because of TERM/TERMINFO.
45+
// With RomFS we might be able to fake this out nicely...
46+
color: ColorConfig::AutoColor,
47+
format: OutputFormat::Pretty,
48+
shuffle: false,
49+
shuffle_seed: None,
50+
test_threads: None,
51+
skip: Vec::new(),
52+
time_options: None,
53+
options: Options::new(),
54+
};
55+
56+
// Use the default test implementation with our hardcoded options
57+
let _success = run_static_tests(&opts, tests).unwrap();
58+
59+
// Make sure the user can actually see the results before we exit
60+
println!("Press START to exit.");
61+
62+
while apt.main_loop() {
63+
gfx.flush_buffers();
64+
gfx.swap_buffers();
65+
gfx.wait_for_vblank();
66+
67+
hid.scan_input();
68+
if hid.keys_down().contains(KeyPad::KEY_START) {
69+
break;
70+
}
71+
}
72+
}
73+
74+
/// Adapted from [`test::test_main_static`] and [`test::make_owned_test`].
75+
fn run_static_tests(opts: &TestOpts, tests: &[&TestDescAndFn]) -> io::Result<bool> {
76+
let tests = tests.iter().map(make_owned_test).collect();
77+
test::run_tests_console(opts, tests)
78+
}
79+
80+
/// Clones static values for putting into a dynamic vector, which test_main()
81+
/// needs to hand out ownership of tests to parallel test runners.
82+
///
83+
/// This will panic when fed any dynamic tests, because they cannot be cloned.
84+
fn make_owned_test(test: &&TestDescAndFn) -> TestDescAndFn {
85+
match test.testfn {
86+
TestFn::StaticTestFn(f) => TestDescAndFn {
87+
testfn: TestFn::StaticTestFn(f),
88+
desc: test.desc.clone(),
89+
},
90+
TestFn::StaticBenchFn(f) => TestDescAndFn {
91+
testfn: TestFn::StaticBenchFn(f),
92+
desc: test.desc.clone(),
93+
},
94+
_ => panic!("non-static tests passed to test::test_main_static"),
95+
}
96+
}
97+
98+
/// The following functions are stubs needed to link the test library,
99+
/// but do nothing because we don't actually need them for the runner to work.
100+
mod link_fix {
101+
#[no_mangle]
102+
extern "C" fn execvp(
103+
_argc: *const libc::c_char,
104+
_argv: *mut *const libc::c_char,
105+
) -> libc::c_int {
106+
-1
107+
}
108+
109+
#[no_mangle]
110+
extern "C" fn pipe(_fildes: *mut libc::c_int) -> libc::c_int {
111+
-1
112+
}
113+
114+
#[no_mangle]
115+
extern "C" fn sigemptyset(_arg1: *mut libc::sigset_t) -> ::libc::c_int {
116+
-1
117+
}
118+
119+
#[no_mangle]
120+
extern "C" fn sysconf(_name: libc::c_int) -> libc::c_long {
121+
-1
122+
}
123+
}

0 commit comments

Comments
 (0)