From 1b0f5fc7e158dbbec81183291ee7fb24ab4d862c Mon Sep 17 00:00:00 2001 From: Ian Chamberlain Date: Sun, 24 Sep 2023 14:57:38 -0400 Subject: [PATCH] Add doctest macro to setup doctests --- .github/workflows/ci.yml | 23 +++--- README.md | 2 - run-tests/Dockerfile | 4 + run-tests/action.yml | 8 +- test-runner/src/console.rs | 5 +- test-runner/src/gdb.rs | 17 +--- test-runner/src/lib.rs | 157 +++++++++++++++++++++++++++++++++---- test-runner/src/socket.rs | 2 - 8 files changed, 167 insertions(+), 51 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f117c0c..298cbb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,18 +63,17 @@ jobs: working-directory: test-runner args: -- -v - # TODO(#4): run these suckers - # - name: Build and run doc tests - # # Let's still run doc tests even if lib/integration tests fail: - # if: ${{ !cancelled() }} - # env: - # # This ensures the citra logs and video output gets put in a directory - # # where we can upload as artifacts - # RUSTDOCFLAGS: " --persist-doctests ${{ env.GITHUB_WORKSPACE }}/target/armv6k-nintendo-3ds/debug/doctests" - # uses: ./run-tests - # with: - # working-directory: test-runner - # args: --doc -- -v + - name: Build and run doc tests + # Still run doc tests even if lib/integration tests fail: + if: ${{ !cancelled() }} + env: + # This ensures the citra logs and video output get put in a directory where the + # artifact upload can find them (instead of being removed from the tmpdir) + RUSTDOCFLAGS: " --persist-doctests ${{ env.GITHUB_WORKSPACE }}/target/armv6k-nintendo-3ds/debug/doctests" + uses: ./run-tests + with: + working-directory: test-runner + args: --doc -- -v - name: Upload citra logs and capture videos uses: actions/upload-artifact@v3 diff --git a/README.md b/README.md index 67018b1..e321dc3 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,6 @@ jobs: volumes: # This is required so the test action can `docker run` the runner: - '/var/run/docker.sock:/var/run/docker.sock' - # This is required so doctest artifacts are accessible to the action: - - '/tmp:/tmp' steps: - name: Checkout branch diff --git a/run-tests/Dockerfile b/run-tests/Dockerfile index aa95b1d..655195b 100644 --- a/run-tests/Dockerfile +++ b/run-tests/Dockerfile @@ -8,7 +8,11 @@ ARG CITRA_CHANNEL=nightly ARG CITRA_RELEASE=1995 RUN download_citra ${CITRA_CHANNEL} ${CITRA_RELEASE} +<<<<<<< Updated upstream:run-tests/Dockerfile FROM devkitpro/devkitarm:latest as devkitarm +======= +FROM devkitpro/devkitarm as devkitarm +>>>>>>> Stashed changes:Dockerfile # For some reason, citra isn't always happy when you try to run it for the first time, # so we build a simple dummy program to force it to create its directory structure diff --git a/run-tests/action.yml b/run-tests/action.yml index c9255dd..5f70f30 100644 --- a/run-tests/action.yml +++ b/run-tests/action.yml @@ -1,9 +1,9 @@ name: Cargo 3DS Test description: > Run `cargo 3ds test` executables using Citra. Note that to use this action, - you must mount `/var/run/docker.sock:/var/run/docker.sock` and `/tmp:/tmp` into - the container so that the runner image can be built and doctest artifacts can - be found, respectively. + you must use a container image of `devkitpro/devkitarm` and mount + `/var/run/docker.sock:/var/run/docker.sock` into the container so that the + runner image can be built by the action. inputs: args: @@ -34,6 +34,8 @@ runs: tags: ${{ inputs.runner-image }}:latest push: false load: true + cache-from: type=gha + cache-to: type=gha,mode=max - name: Ensure docker is installed in the container shell: bash diff --git a/test-runner/src/console.rs b/test-runner/src/console.rs index 5e61951..c3e53ed 100644 --- a/test-runner/src/console.rs +++ b/test-runner/src/console.rs @@ -2,6 +2,7 @@ use ctru::prelude::*; use ctru::services::gfx::{Flush, Swap}; use super::TestRunner; +use crate::TestResult; pub struct ConsoleRunner { gfx: Gfx, @@ -28,7 +29,7 @@ impl TestRunner for ConsoleRunner { Console::new(self.gfx.top_screen.borrow_mut()) } - fn cleanup(mut self, _test_result: std::io::Result) { + fn cleanup(mut self, result: T) -> T { // We don't actually care about the test result, either way we'll stop // and show the results to the user @@ -47,5 +48,7 @@ impl TestRunner for ConsoleRunner { break; } } + + result } } diff --git a/test-runner/src/gdb.rs b/test-runner/src/gdb.rs index d8a7c46..e62aa7b 100644 --- a/test-runner/src/gdb.rs +++ b/test-runner/src/gdb.rs @@ -1,6 +1,7 @@ use ctru::error::ResultCode; use super::TestRunner; +use crate::TestResult; #[derive(Default)] pub struct GdbRunner; @@ -27,22 +28,10 @@ impl TestRunner for GdbRunner { .expect("failed to redirect I/O streams to GDB"); } - fn cleanup(self, test_result: std::io::Result) { + 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. - match test_result { - Ok(success) => { - if success { - std::process::exit(0); - } else { - std::process::exit(101); - } - } - Err(err) => { - eprintln!("Error: {err}"); - std::process::exit(101); - } - } + std::process::exit(if test_result.succeeded() { 0 } else { 101 }) } } diff --git a/test-runner/src/lib.rs b/test-runner/src/lib.rs index 3f9597c..a9ba89c 100644 --- a/test-runner/src/lib.rs +++ b/test-runner/src/lib.rs @@ -6,6 +6,7 @@ #![feature(test)] #![feature(custom_test_frameworks)] +#![feature(exitcode_exit_method)] #![test_runner(run_gdb)] extern crate test; @@ -14,10 +15,13 @@ mod console; mod gdb; mod socket; -use console::ConsoleRunner; -use gdb::GdbRunner; -use socket::SocketRunner; +use std::any::Any; +use std::error::Error; +use std::fmt::Display; +pub use console::ConsoleRunner; +pub use gdb::GdbRunner; +pub use socket::SocketRunner; use test::{ColorConfig, OutputFormat, TestDescAndFn, TestFn, TestOpts}; /// Show test output in GDB, using the [File I/O Protocol] (called HIO in some 3DS @@ -25,7 +29,7 @@ use test::{ColorConfig, OutputFormat, TestDescAndFn, TestFn, TestOpts}; /// /// [File I/O Protocol]: https://sourceware.org/gdb/onlinedocs/gdb/File_002dI_002fO-Overview.html#File_002dI_002fO-Overview pub fn run_gdb(tests: &[&TestDescAndFn]) { - run::(tests) + run::(tests); } /// Run tests using the `ctru` [`Console`] (print results to the 3DS screen). @@ -33,7 +37,7 @@ pub fn run_gdb(tests: &[&TestDescAndFn]) { /// /// [`Console`]: ctru::console::Console pub fn run_console(tests: &[&TestDescAndFn]) { - run::(tests) + run::(tests); } /// Show test output via a network socket to `3dslink`. This runner is only useful @@ -43,9 +47,96 @@ pub fn run_console(tests: &[&TestDescAndFn]) { /// /// [`Soc::redirect_to_3dslink`]: ctru::services::soc::Soc::redirect_to_3dslink pub fn run_socket(tests: &[&TestDescAndFn]) { - run::(tests) + run::(tests); } +/// Helper macro for writing doctests using this runner. Wrap this macro around +/// your normal doctest to enable running it with the test runners in this crate. +/// +/// You may optionally specify a runner before the test body, and may use any of +/// the various [`fn main()`](https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html#using--in-doc-tests) +/// signatures allowed by documentation tests. +/// +/// # Examples +/// +/// ## Custom runner +/// +/// ```no_run +/// test_runner::doctest! { SocketRunner, +/// assert_eq!(2 + 2, 4); +/// } +/// ``` +/// +/// ## `should_panic` +/// +/// ```should_panic +/// test_runner::doctest! { +/// assert_eq!(2 + 2, 5); +/// } +/// ``` +/// +/// ## Custom `fn main` +/// +/// ``` +/// test_runner::doctest! { +/// fn main() { +/// assert_eq!(2 + 2, 4); +/// } +/// } +/// ``` +/// +/// ``` +/// test_runner::doctest! { +/// fn main() -> Result<(), Box> { +/// assert_eq!(2 + 2, 4); +/// Ok(()) +/// } +/// } +/// ``` +/// +/// ## Implicit return type +/// +/// Note that for the rustdoc preprocessor to understand the return type, the +/// `Ok(())` expression must be written _outside_ the `doctest!` invocation. +/// +/// ``` +/// test_runner::doctest! { +/// assert_eq!(2 + 2, 4); +/// } +/// Ok::<(), std::io::Error>(()) +/// ``` +#[macro_export] +macro_rules! doctest { + ($runner:ident, fn main() $(-> $ret:ty)? { $($body:tt)* } ) => { + fn main() $(-> $ret)? { + $crate::doctest!{ $runner, $($body)* } + } + }; + ($runner:ident, $($body:tt)*) => { + use $crate::TestRunner as _; + let mut _runner = $crate::$runner::default(); + _runner.setup(); + let _result = { $($body)* }; + _runner.cleanup(_result) + }; + ($($body:tt)*) => { + $crate::doctest!{ GdbRunner, + $($body)* + } + }; +} + +#[derive(Debug)] +struct TestsFailed; + +impl Display for TestsFailed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "some tests failed!") + } +} + +impl Error for TestsFailed {} + fn run(tests: &[&TestDescAndFn]) { std::env::set_var("RUST_BACKTRACE", "1"); @@ -71,7 +162,7 @@ fn run(tests: &[&TestDescAndFn]) { drop(ctx); - runner.cleanup(result); + let _ = runner.cleanup(result); } /// Adapted from [`test::make_owned_test`]. @@ -92,8 +183,19 @@ fn make_owned_test(test: &TestDescAndFn) -> TestDescAndFn { } } +mod private { + pub trait Sealed {} + + impl Sealed for super::ConsoleRunner {} + impl Sealed for super::GdbRunner {} + impl Sealed for super::SocketRunner {} + + impl Sealed for () {} + impl Sealed for Result {} +} + /// A helper trait to make the behavior of test runners consistent. -trait TestRunner: Sized + Default { +pub trait TestRunner: private::Sealed + Sized + Default { /// 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. @@ -107,7 +209,36 @@ trait TestRunner: Sized + Default { /// Handle the results of the test and perform any necessary cleanup. /// The [`Context`](Self::Context) will be dropped just before this is called. - fn cleanup(self, test_result: std::io::Result); + fn cleanup(self, test_result: T) -> T { + test_result + } +} + +// A helper trait to determine whether tests succeeded. This trait is implemented +// for +pub trait TestResult: private::Sealed { + fn succeeded(&self) -> bool; +} + +impl TestResult for () { + fn succeeded(&self) -> bool { + true + } +} + +impl TestResult for Result { + fn succeeded(&self) -> bool { + // This is sort of a hack workaround for lack of specialized trait impls. + // Basically, check if T is a boolean and use it if so. Otherwise default + // to mapping Ok to success and Err to failure. + match self + .as_ref() + .map(|val| (val as &dyn Any).downcast_ref::()) + { + Ok(Some(&result)) => result, + other => other.is_ok(), + } + } } /// This module has stubs needed to link the test library, but they do nothing @@ -132,14 +263,6 @@ mod link_fix { } } -/// Verify that doctests work as expected -/// ``` -/// assert_eq!(2 + 2, 4); -/// ``` -/// -/// ```should_panic -/// assert_eq!(2 + 2, 5); -/// ``` #[cfg(doctest)] struct Dummy; diff --git a/test-runner/src/socket.rs b/test-runner/src/socket.rs index dab54d1..1a78202 100644 --- a/test-runner/src/socket.rs +++ b/test-runner/src/socket.rs @@ -22,6 +22,4 @@ impl TestRunner for SocketRunner { .redirect_to_3dslink(true, true) .expect("failed to redirect to socket"); } - - fn cleanup(self, _test_result: std::io::Result) {} }