diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dab688..92a318d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,30 +10,6 @@ on: workflow_dispatch: jobs: - lint: - strategy: - matrix: - toolchain: - - nightly-2024-02-18 - - runs-on: ubuntu-latest - container: devkitpro/devkitarm - steps: - - name: Checkout branch - uses: actions/checkout@v4 - - - uses: ./setup - with: - toolchain: ${{ matrix.toolchain }} - - - name: Check formatting - working-directory: test-runner - run: cargo fmt --all --verbose -- --check - - - name: Run clippy - working-directory: test-runner - run: cargo 3ds clippy --color=always --verbose --all-targets - test: strategy: matrix: @@ -41,8 +17,12 @@ jobs: # Oldest supported nightly - nightly-2024-02-18 - nightly + ctru-rs-ref: + # "known good" version of `test-runner` crate + - 3247af67d + - master - continue-on-error: ${{ matrix.toolchain == 'nightly' }} + continue-on-error: ${{ matrix.toolchain == 'nightly' || matrix.ctru-rs-ref == 'master' }} runs-on: ubuntu-latest container: image: devkitpro/devkitarm @@ -57,11 +37,32 @@ jobs: with: toolchain: ${{ matrix.toolchain }} + - name: Resolve inputs.ref to full SHA + # https://github.com/actions/checkout/issues/265#issuecomment-1936792528 + id: resolve-ref + run: | + apt-get update -y && apt-get install -y jq + + ref=${{ matrix.ctru-rs-ref }} + sha=$(curl -L "https://api.github.com/repos/rust3ds/ctru-rs/commits/$ref" | jq -r .sha) + if [ -z "$sha" ]; then + echo "Failed to resolve ref $ref (possibly missing GH_TOKEN env var?)" >&2 + exit 1 + fi + + echo "sha=$sha" >> $GITHUB_OUTPUT + + - uses: actions/checkout@v4 + with: + repository: 'rust3ds/ctru-rs' + path: 'ctru-rs' + ref: ${{ steps.resolve-ref.outputs.sha }} + - name: Build and run tests (unit + integration) uses: ./run-tests with: - working-directory: test-runner - args: -- -v + working-directory: ctru-rs + args: --package test-runner -- -v - name: Build and run doc tests # Still run doc tests even if lib/integration tests fail: @@ -72,15 +73,15 @@ jobs: RUSTDOCFLAGS: " --persist-doctests target/armv6k-nintendo-3ds/debug/doctests" uses: ./run-tests with: - working-directory: test-runner - args: --doc -- -v + working-directory: ctru-rs + args: --doc --package test-runner -v - name: Upload citra logs and capture videos uses: actions/upload-artifact@v3 # We always want to upload artifacts regardless of previous success/failure if: ${{ !cancelled() }} with: - name: citra-logs-${{ matrix.toolchain }} + name: citra-logs-${{ matrix.toolchain }}-${{ matrix.ctru-rs-ref }} path: | - test-runner/target/armv6k-nintendo-3ds/debug/**/*.txt - test-runner/target/armv6k-nintendo-3ds/debug/**/*.webm + ctru-rs/target/armv6k-nintendo-3ds/debug/**/*.txt + ctru-rs/target/armv6k-nintendo-3ds/debug/**/*.webm diff --git a/README.md b/README.md index a403ca4..780a2db 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,18 @@ # test-runner -A set of tools for running automated Rust tests against Citra (3DS emulator). +A set of Github Actions for working with 3DS applications and the `rust3ds` toolchain. + +It's recommended to use the `test-runner` crate from [ctru-rs](https://github.com/rust3ds/ctru-rs) +when working with these actions, in order to get useful test output on failures. ## Components -* `test-runner`: a Rust crate for writing tests for 3DS homebrew -* GitHub Actions: - * `setup`: action for setting up the Rust 3DS toolchain in workflows - * `run-tests`: action for running test executables with Citra in workflows +* `setup`: action for setting up the Rust 3DS toolchain in workflows +* `run-tests`: action for running test executables with Citra in workflows ## Usage -First the test runner to your crate: - -```sh -cargo add --dev test-runner --git https://github.com/rust3ds/test-runner -``` - -In `lib.rs` and any integration test files: - -```rs -#![feature(custom_test_frameworks)] -#![test_runner(test_runner::run_gdb)] -``` - -Then use the `setup` and `run-tests` actions in your github workflow. This -example shows the default value for each of the inputs. +This example shows the default value for each of the inputs. ```yml jobs: @@ -66,25 +53,9 @@ using these actions (including uploading output artifacts from the tests). ## Caveats -* GDB doesn't seem to support separate output streams for `stdout` and `stderr`, - so all test output to `stderr` will end up combined with `stdout` and both will be - printed to the runner's `stdout`. If you know a workaround for this that doesn't - require patching + building GDB itself please open an issue about it! - * Since the custom test runner runs as part of `cargo test`, it won't be able to find a `3dsx` that hasn't built yet. `cargo-3ds` doesn't build `3dsx` executables until _after_ the cargo command it runs internally, so this means that tests can't depend on any features of the `3dsx` (like embedded romFS). A workaround for this is to simply build the tests as a separate step before running them, after which the runner will be able to find the `3dsx`. - -* Doctests require a bit of extra setup to work with the runner, since they don't - use the crate's `#![test_runner]`. To write doctests, add the following to the - beginning of the doctest (or `fn main()` if the test defines it): - - ```rust - let _runner = test_runner::GdbRunner::default(); - ``` - - The runner must remain in scope for the duration of the test in order for - the test output to be printed. diff --git a/test-runner/Cargo.toml b/test-runner/Cargo.toml deleted file mode 100644 index 4955033..0000000 --- a/test-runner/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "test-runner" -version = "0.1.0" -edition = "2021" - -[features] -console = [] -gdb = [] -socket = [] - -[dependencies] -ctru-rs = { git = "https://github.com/rust3ds/ctru-rs" } -ctru-sys = { git = "https://github.com/rust3ds/ctru-rs" } -libc = "0.2.147" - -# Future-proofing for a rename + org move: -[patch."https://github.com/rust3ds/test-runner"] -test-runner = { path = "." } diff --git a/test-runner/src/console.rs b/test-runner/src/console.rs deleted file mode 100644 index 90ad76f..0000000 --- a/test-runner/src/console.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::process::Termination; - -use ctru::prelude::*; -use ctru::services::gfx::{Flush, Swap}; - -use super::TestRunner; - -/// Run tests using the [`ctru::console::Console`] (print results to the 3DS screen). -/// This is mostly useful for running tests manually, especially on real hardware. -pub struct ConsoleRunner { - gfx: Gfx, - hid: Hid, - apt: Apt, -} - -impl TestRunner for ConsoleRunner { - type Context<'this> = Console<'this>; - - fn new() -> Self { - let gfx = Gfx::new().unwrap(); - let hid = Hid::new().unwrap(); - let apt = Apt::new().unwrap(); - - gfx.top_screen.borrow_mut().set_wide_mode(true); - - Self { gfx, hid, apt } - } - - fn setup(&mut self) -> Self::Context<'_> { - Console::new(self.gfx.top_screen.borrow_mut()) - } - - fn cleanup(mut self, result: T) -> T { - // We don't actually care about the output of the test result, either - // way we'll stop and show the results to the user. - - println!("Press START to exit."); - - while self.apt.main_loop() { - let mut screen = self.gfx.top_screen.borrow_mut(); - screen.flush_buffers(); - screen.swap_buffers(); - - self.gfx.wait_for_vblank(); - - self.hid.scan_input(); - if self.hid.keys_down().contains(KeyPad::START) { - break; - } - } - - result - } -} diff --git a/test-runner/src/gdb.rs b/test-runner/src/gdb.rs deleted file mode 100644 index 03a84ad..0000000 --- a/test-runner/src/gdb.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::process::Termination; - -use ctru::error::ResultCode; - -use super::TestRunner; - -// We use a little trick with cfg(doctest) to make code fences appear in -// rustdoc output, but compile without them when doctesting. This raises warnings -// for invalid code, though, so silence that lint here. -#[cfg_attr(not(doctest), allow(rustdoc::invalid_rust_codeblocks))] -/// Show test output in GDB, using the [File I/O Protocol] (called HIO in some 3DS -/// homebrew resources). Both stdout and stderr will be printed to the GDB console. -/// -/// Creating this runner at the beginning of a doctest enables output from failing -/// tests. Without `GdbRunner`, tests will still fail on panic, but they won't display -/// anything written to `stdout` or `stderr`. -/// -/// The runner should remain in scope for the remainder of the test. -/// -/// [File I/O Protocol]: https://sourceware.org/gdb/onlinedocs/gdb/File_002dI_002fO-Overview.html#File_002dI_002fO-Overview -/// -/// # Examples -/// -#[cfg_attr(not(doctest), doc = "````")] -/// ``` -/// let _runner = test_runner::GdbRunner::default(); -/// assert_eq!(2 + 2, 4); -/// ``` -#[cfg_attr(not(doctest), doc = "````")] -/// -#[cfg_attr(not(doctest), doc = "````")] -/// ```should_panic -/// let _runner = test_runner::GdbRunner::default(); -/// assert_eq!(2 + 2, 5); -/// ``` -#[cfg_attr(not(doctest), doc = "````")] -pub struct GdbRunner(()); - -impl Default for GdbRunner { - fn default() -> Self { - || -> ctru::Result<()> { - // TODO: `ctru` expose safe API to do this and call that instead - unsafe { - ResultCode(ctru_sys::gdbHioDevInit())?; - // TODO: should we actually redirect stdin or nah? - ResultCode(ctru_sys::gdbHioDevRedirectStdStreams(true, true, true))?; - } - Ok(()) - }() - .expect("failed to redirect I/O streams to GDB"); - - Self(()) - } -} - -impl Drop for GdbRunner { - fn drop(&mut self) { - unsafe { ctru_sys::gdbHioDevExit() } - } -} - -impl TestRunner for GdbRunner { - type Context<'this> = (); - - fn new() -> Self { - Self::default() - } - - fn setup(&mut self) -> Self::Context<'_> {} - - fn cleanup(self, test_result: T) -> T { - // GDB actually has the opportunity to inspect the exit code, - // unlike other runners, so let's follow the default behavior of the - // stdlib test runner. - test_result.report().exit_process() - } -} diff --git a/test-runner/src/lib.rs b/test-runner/src/lib.rs deleted file mode 100644 index b4a3fcf..0000000 --- a/test-runner/src/lib.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! Custom test runner for building/running tests on the 3DS. -//! -//! This library can be used with -//! [`custom_test_frameworks`](https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html) -//! to enable normal Rust testing workflows for 3DS homebrew. - -#![feature(test)] -#![feature(custom_test_frameworks)] -#![feature(exitcode_exit_method)] -#![test_runner(run_gdb)] - -extern crate test; - -mod console; -mod gdb; -mod socket; - -use std::process::{ExitCode, Termination}; - -pub use console::ConsoleRunner; -pub use gdb::GdbRunner; -pub use socket::SocketRunner; -use test::{ColorConfig, OutputFormat, TestDescAndFn, TestFn, TestOpts}; - -/// Run tests using the [`GdbRunner`]. -/// This function can be used with the `#[test_runner]` attribute. -pub fn run_gdb(tests: &[&TestDescAndFn]) { - run::(tests); -} - -/// Run tests using the [`ConsoleRunner`]. -/// This function can be used with the `#[test_runner]` attribute. -pub fn run_console(tests: &[&TestDescAndFn]) { - run::(tests); -} - -/// Run tests using the [`SocketRunner`]. -/// This function can be used with the `#[test_runner]` attribute. -pub fn run_socket(tests: &[&TestDescAndFn]) { - run::(tests); -} - -fn run(tests: &[&TestDescAndFn]) { - std::env::set_var("RUST_BACKTRACE", "1"); - - let mut runner = Runner::new(); - let ctx = runner.setup(); - - let opts = TestOpts { - force_run_in_process: true, - run_tests: true, - // TODO: color doesn't work because of TERM/TERMINFO. - // With RomFS we might be able to fake this out nicely... - color: ColorConfig::AlwaysColor, - format: OutputFormat::Pretty, - test_threads: Some(1), - // Hopefully this interface is more stable vs specifying individual options, - // and parsing the empty list of args should always work, I think. - // TODO Ideally we could pass actual std::env::args() here too - ..test::test::parse_opts(&[]).unwrap().unwrap() - }; - - let tests = tests.iter().map(|t| make_owned_test(t)).collect(); - let result = test::run_tests_console(&opts, tests); - - drop(ctx); - - let reportable_result = match result { - Ok(true) => Ok(()), - // Try to match stdlib console test runner behavior as best we can - _ => Err(ExitCode::from(101)), - }; - - let _ = runner.cleanup(reportable_result); -} - -/// Adapted from [`test::make_owned_test`]. -/// 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 { - let testfn = match test.testfn { - TestFn::StaticTestFn(f) => TestFn::StaticTestFn(f), - TestFn::StaticBenchFn(f) => TestFn::StaticBenchFn(f), - _ => panic!("non-static tests passed to test::test_main_static"), - }; - - TestDescAndFn { - testfn, - desc: test.desc.clone(), - } -} - -/// A helper trait to make the behavior of test runners consistent. -trait TestRunner: Sized { - /// Any context the test runner needs to remain alive for the duration of - /// the test. This can be used for things that need to borrow the test runner - /// itself. - // TODO: with associated type defaults this could be `= ();` - type Context<'this> - where - Self: 'this; - - /// Initialize the test runner. - fn new() -> Self; - - /// Create the [`Context`](Self::Context), if any. - fn setup(&mut self) -> Self::Context<'_>; - - /// Handle the results of the test and perform any necessary cleanup. - /// The [`Context`](Self::Context) will be dropped just before this is called. - /// - /// This returns `T` so that the result can be used in doctests. - fn cleanup(self, test_result: T) -> T { - test_result - } -} - -/// This module has stubs needed to link the test library, but they 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 - } -} - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } - - #[test] - #[should_panic] - fn it_fails() { - assert_eq!(2 + 2, 5); - } -} diff --git a/test-runner/src/socket.rs b/test-runner/src/socket.rs deleted file mode 100644 index cf24a00..0000000 --- a/test-runner/src/socket.rs +++ /dev/null @@ -1,28 +0,0 @@ -use ctru::prelude::*; - -use super::TestRunner; - -/// Show test output via a network socket to `3dslink`. This runner is only useful -/// on real hardware, since `3dslink` doesn't work with emulators. -/// -/// See [`Soc::redirect_to_3dslink`] for more details. -/// -/// [`Soc::redirect_to_3dslink`]: ctru::services::soc::Soc::redirect_to_3dslink -pub struct SocketRunner { - soc: Soc, -} - -impl TestRunner for SocketRunner { - type Context<'this> = &'this Soc; - - fn new() -> Self { - let mut soc = Soc::new().expect("failed to initialize network service"); - soc.redirect_to_3dslink(true, true) - .expect("failed to redirect to socket"); - Self { soc } - } - - fn setup(&mut self) -> Self::Context<'_> { - &self.soc - } -} diff --git a/test-runner/tests/integration.rs b/test-runner/tests/integration.rs deleted file mode 100644 index 1cd14ac..0000000 --- a/test-runner/tests/integration.rs +++ /dev/null @@ -1,13 +0,0 @@ -#![feature(custom_test_frameworks)] -#![test_runner(test_runner::run_gdb)] - -#[test] -fn it_works() { - assert_eq!(2 + 2, 4); -} - -#[test] -#[should_panic] -fn it_panics() { - assert_eq!(2 + 2, 5); -}