Browse Source

Add doctest macro to setup doctests

pull/9/head
Ian Chamberlain 1 year ago
parent
commit
1b0f5fc7e1
No known key found for this signature in database
GPG Key ID: AE5484D09405AA60
  1. 23
      .github/workflows/ci.yml
  2. 2
      README.md
  3. 4
      run-tests/Dockerfile
  4. 8
      run-tests/action.yml
  5. 5
      test-runner/src/console.rs
  6. 17
      test-runner/src/gdb.rs
  7. 157
      test-runner/src/lib.rs
  8. 2
      test-runner/src/socket.rs

23
.github/workflows/ci.yml

@ -63,18 +63,17 @@ jobs:
working-directory: test-runner working-directory: test-runner
args: -- -v args: -- -v
# TODO(#4): run these suckers - name: Build and run doc tests
# - name: Build and run doc tests # Still run doc tests even if lib/integration tests fail:
# # Let's still run doc tests even if lib/integration tests fail: if: ${{ !cancelled() }}
# if: ${{ !cancelled() }} env:
# env: # This ensures the citra logs and video output get put in a directory where the
# # This ensures the citra logs and video output gets put in a directory # artifact upload can find them (instead of being removed from the tmpdir)
# # where we can upload as artifacts RUSTDOCFLAGS: " --persist-doctests ${{ env.GITHUB_WORKSPACE }}/target/armv6k-nintendo-3ds/debug/doctests"
# RUSTDOCFLAGS: " --persist-doctests ${{ env.GITHUB_WORKSPACE }}/target/armv6k-nintendo-3ds/debug/doctests" uses: ./run-tests
# uses: ./run-tests with:
# with: working-directory: test-runner
# working-directory: test-runner args: --doc -- -v
# args: --doc -- -v
- name: Upload citra logs and capture videos - name: Upload citra logs and capture videos
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3

2
README.md

@ -37,8 +37,6 @@ jobs:
volumes: volumes:
# This is required so the test action can `docker run` the runner: # This is required so the test action can `docker run` the runner:
- '/var/run/docker.sock:/var/run/docker.sock' - '/var/run/docker.sock:/var/run/docker.sock'
# This is required so doctest artifacts are accessible to the action:
- '/tmp:/tmp'
steps: steps:
- name: Checkout branch - name: Checkout branch

4
run-tests/Dockerfile

@ -8,7 +8,11 @@ ARG CITRA_CHANNEL=nightly
ARG CITRA_RELEASE=1995 ARG CITRA_RELEASE=1995
RUN download_citra ${CITRA_CHANNEL} ${CITRA_RELEASE} RUN download_citra ${CITRA_CHANNEL} ${CITRA_RELEASE}
<<<<<<< Updated upstream:run-tests/Dockerfile
FROM devkitpro/devkitarm:latest as devkitarm 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, # 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 # so we build a simple dummy program to force it to create its directory structure

8
run-tests/action.yml

@ -1,9 +1,9 @@
name: Cargo 3DS Test name: Cargo 3DS Test
description: > description: >
Run `cargo 3ds test` executables using Citra. Note that to use this action, 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 you must use a container image of `devkitpro/devkitarm` and mount
the container so that the runner image can be built and doctest artifacts can `/var/run/docker.sock:/var/run/docker.sock` into the container so that the
be found, respectively. runner image can be built by the action.
inputs: inputs:
args: args:
@ -34,6 +34,8 @@ runs:
tags: ${{ inputs.runner-image }}:latest tags: ${{ inputs.runner-image }}:latest
push: false push: false
load: true load: true
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Ensure docker is installed in the container - name: Ensure docker is installed in the container
shell: bash shell: bash

5
test-runner/src/console.rs

@ -2,6 +2,7 @@ use ctru::prelude::*;
use ctru::services::gfx::{Flush, Swap}; use ctru::services::gfx::{Flush, Swap};
use super::TestRunner; use super::TestRunner;
use crate::TestResult;
pub struct ConsoleRunner { pub struct ConsoleRunner {
gfx: Gfx, gfx: Gfx,
@ -28,7 +29,7 @@ impl TestRunner for ConsoleRunner {
Console::new(self.gfx.top_screen.borrow_mut()) Console::new(self.gfx.top_screen.borrow_mut())
} }
fn cleanup(mut self, _test_result: std::io::Result<bool>) { fn cleanup<T: TestResult>(mut self, result: T) -> T {
// We don't actually care about the test result, either way we'll stop // We don't actually care about the test result, either way we'll stop
// and show the results to the user // and show the results to the user
@ -47,5 +48,7 @@ impl TestRunner for ConsoleRunner {
break; break;
} }
} }
result
} }
} }

17
test-runner/src/gdb.rs

@ -1,6 +1,7 @@
use ctru::error::ResultCode; use ctru::error::ResultCode;
use super::TestRunner; use super::TestRunner;
use crate::TestResult;
#[derive(Default)] #[derive(Default)]
pub struct GdbRunner; pub struct GdbRunner;
@ -27,22 +28,10 @@ impl TestRunner for GdbRunner {
.expect("failed to redirect I/O streams to GDB"); .expect("failed to redirect I/O streams to GDB");
} }
fn cleanup(self, test_result: std::io::Result<bool>) { fn cleanup<T: TestResult>(self, test_result: T) -> T {
// GDB actually has the opportunity to inspect the exit code, // GDB actually has the opportunity to inspect the exit code,
// unlike other runners, so let's follow the default behavior of the // unlike other runners, so let's follow the default behavior of the
// stdlib test runner. // stdlib test runner.
match test_result { std::process::exit(if test_result.succeeded() { 0 } else { 101 })
Ok(success) => {
if success {
std::process::exit(0);
} else {
std::process::exit(101);
}
}
Err(err) => {
eprintln!("Error: {err}");
std::process::exit(101);
}
}
} }
} }

157
test-runner/src/lib.rs

@ -6,6 +6,7 @@
#![feature(test)] #![feature(test)]
#![feature(custom_test_frameworks)] #![feature(custom_test_frameworks)]
#![feature(exitcode_exit_method)]
#![test_runner(run_gdb)] #![test_runner(run_gdb)]
extern crate test; extern crate test;
@ -14,10 +15,13 @@ mod console;
mod gdb; mod gdb;
mod socket; mod socket;
use console::ConsoleRunner; use std::any::Any;
use gdb::GdbRunner; use std::error::Error;
use socket::SocketRunner; use std::fmt::Display;
pub use console::ConsoleRunner;
pub use gdb::GdbRunner;
pub use socket::SocketRunner;
use test::{ColorConfig, OutputFormat, TestDescAndFn, TestFn, TestOpts}; use test::{ColorConfig, OutputFormat, TestDescAndFn, TestFn, TestOpts};
/// Show test output in GDB, using the [File I/O Protocol] (called HIO in some 3DS /// 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 /// [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]) { pub fn run_gdb(tests: &[&TestDescAndFn]) {
run::<GdbRunner>(tests) run::<GdbRunner>(tests);
} }
/// Run tests using the `ctru` [`Console`] (print results to the 3DS screen). /// 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 /// [`Console`]: ctru::console::Console
pub fn run_console(tests: &[&TestDescAndFn]) { pub fn run_console(tests: &[&TestDescAndFn]) {
run::<ConsoleRunner>(tests) run::<ConsoleRunner>(tests);
} }
/// Show test output via a network socket to `3dslink`. This runner is only useful /// 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 /// [`Soc::redirect_to_3dslink`]: ctru::services::soc::Soc::redirect_to_3dslink
pub fn run_socket(tests: &[&TestDescAndFn]) { pub fn run_socket(tests: &[&TestDescAndFn]) {
run::<SocketRunner>(tests) run::<SocketRunner>(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<dyn std::error::Error>> {
/// 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<Runner: TestRunner>(tests: &[&TestDescAndFn]) { fn run<Runner: TestRunner>(tests: &[&TestDescAndFn]) {
std::env::set_var("RUST_BACKTRACE", "1"); std::env::set_var("RUST_BACKTRACE", "1");
@ -71,7 +162,7 @@ fn run<Runner: TestRunner>(tests: &[&TestDescAndFn]) {
drop(ctx); drop(ctx);
runner.cleanup(result); let _ = runner.cleanup(result);
} }
/// Adapted from [`test::make_owned_test`]. /// 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<T, E> Sealed for Result<T, E> {}
}
/// A helper trait to make the behavior of test runners consistent. /// 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 /// 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 /// the test. This can be used for things that need to borrow the test runner
/// itself. /// itself.
@ -107,7 +209,36 @@ trait TestRunner: Sized + Default {
/// Handle the results of the test and perform any necessary cleanup. /// Handle the results of the test and perform any necessary cleanup.
/// The [`Context`](Self::Context) will be dropped just before this is called. /// The [`Context`](Self::Context) will be dropped just before this is called.
fn cleanup(self, test_result: std::io::Result<bool>); fn cleanup<T: TestResult>(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<T: Any, E> TestResult for Result<T, E> {
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::<bool>())
{
Ok(Some(&result)) => result,
other => other.is_ok(),
}
}
} }
/// This module has stubs needed to link the test library, but they do nothing /// 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)] #[cfg(doctest)]
struct Dummy; struct Dummy;

2
test-runner/src/socket.rs

@ -22,6 +22,4 @@ impl TestRunner for SocketRunner {
.redirect_to_3dslink(true, true) .redirect_to_3dslink(true, true)
.expect("failed to redirect to socket"); .expect("failed to redirect to socket");
} }
fn cleanup(self, _test_result: std::io::Result<bool>) {}
} }

Loading…
Cancel
Save