Skip to content

Commit 54772c1

Browse files
Merge pull request #9 from ian-h-chamberlain/feature/run-doctests
2 parents 22522b4 + 7fd9946 commit 54772c1

File tree

8 files changed

+175
-67
lines changed

8 files changed

+175
-67
lines changed

.github/workflows/ci.yml

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,17 @@ jobs:
6363
working-directory: test-runner
6464
args: -- -v
6565

66-
# TODO(#4): run these suckers
67-
# - name: Build and run doc tests
68-
# # Let's still run doc tests even if lib/integration tests fail:
69-
# if: ${{ !cancelled() }}
70-
# env:
71-
# # This ensures the citra logs and video output gets put in a directory
72-
# # where we can upload as artifacts
73-
# RUSTDOCFLAGS: " --persist-doctests ${{ env.GITHUB_WORKSPACE }}/target/armv6k-nintendo-3ds/debug/doctests"
74-
# uses: ./run-tests
75-
# with:
76-
# working-directory: test-runner
77-
# args: --doc -- -v
66+
- name: Build and run doc tests
67+
# Still run doc tests even if lib/integration tests fail:
68+
if: ${{ !cancelled() }}
69+
env:
70+
# This ensures the citra logs and video output get persisted to a
71+
# directory where the artifact upload can find them.
72+
RUSTDOCFLAGS: " --persist-doctests target/armv6k-nintendo-3ds/debug/doctests"
73+
uses: ./run-tests
74+
with:
75+
working-directory: test-runner
76+
args: --doc -- -v
7877

7978
- name: Upload citra logs and capture videos
8079
uses: actions/upload-artifact@v3
@@ -83,5 +82,5 @@ jobs:
8382
with:
8483
name: citra-logs-${{ matrix.toolchain }}
8584
path: |
86-
target/armv6k-nintendo-3ds/debug/**/*.txt
87-
target/armv6k-nintendo-3ds/debug/**/*.webm
85+
test-runner/target/armv6k-nintendo-3ds/debug/**/*.txt
86+
test-runner/target/armv6k-nintendo-3ds/debug/**/*.webm

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ In `lib.rs` and any integration test files:
2626
```
2727

2828
Then use the `setup` and `run-tests` actions in your github workflow. This
29-
example shows the default value for each of the inputs:
29+
example shows the default value for each of the inputs.
3030

3131
```yml
3232
jobs:
@@ -37,8 +37,6 @@ jobs:
3737
volumes:
3838
# This is required so the test action can `docker run` the runner:
3939
- '/var/run/docker.sock:/var/run/docker.sock'
40-
# This is required so doctest artifacts are accessible to the action:
41-
- '/tmp:/tmp'
4240

4341
steps:
4442
- name: Checkout branch
@@ -63,3 +61,6 @@ jobs:
6361
# https://github.com/actions/runner/issues/2058
6462
working-directory: ${GITHUB_WORKSPACE}
6563
```
64+
65+
See [`ci.yml`](.github/workflows/ci.yml) to see a full lint and test workflow
66+
using these actions (including uploading output artifacts from the tests).

run-tests/action.yml

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
name: Cargo 3DS Test
22
description: >
33
Run `cargo 3ds test` executables using Citra. Note that to use this action,
4-
you must mount `/var/run/docker.sock:/var/run/docker.sock` and `/tmp:/tmp` into
5-
the container so that the runner image can be built and doctest artifacts can
6-
be found, respectively.
4+
you must use a container image of `devkitpro/devkitarm` and mount
5+
`/var/run/docker.sock:/var/run/docker.sock` into the container so that the
6+
runner image can be built by the action.
77
88
inputs:
99
args:
@@ -34,6 +34,8 @@ runs:
3434
tags: ${{ inputs.runner-image }}:latest
3535
push: false
3636
load: true
37+
cache-from: type=gha
38+
cache-to: type=gha,mode=max
3739

3840
- name: Ensure docker is installed in the container
3941
shell: bash
@@ -42,20 +44,24 @@ runs:
4244
- name: Run cargo 3ds test
4345
shell: bash
4446
# Set a custom runner for `cargo test` commands to use.
45-
# Use ${GITHUB_WORKSPACE} due to
47+
# Use ${PWD} and ${RUNNER_TEMP} due to
4648
# https://github.com/actions/runner/issues/2058, which also means
47-
# we have to export this instead of using the env: key
49+
# we have to export this in `run` instead of using the `env` key
4850
run: |
4951
cd ${{ inputs.working-directory }}
52+
53+
# Hopefully this still works if the input is an absolute path:
54+
mounted_pwd="${{ github.workspace }}/${{ inputs.working-directory }}"
55+
5056
export CARGO_TARGET_ARMV6K_NINTENDO_3DS_RUNNER="
5157
docker run --rm
52-
-v ${{ runner.temp }}:${{ runner.temp }}
53-
-v ${{ github.workspace }}/target:/app/target
5458
-v ${{ github.workspace }}:${GITHUB_WORKSPACE}
59+
-v ${mounted_pwd}/target:/app/target
60+
-v ${{ runner.temp }}:${RUNNER_TEMP}
5561
${{ inputs.runner-image }}:latest"
5662
env
5763
cargo 3ds -v test ${{ inputs.args }}
5864
env:
59-
# Ensure that doctests get built into a path which is mounted on the host
60-
# as well as in this container (via the bind mount in the RUNNER command)
61-
TMPDIR: ${{ runner.temp }}
65+
# Make sure doctests are built into the shared tempdir instead of the
66+
# container's /tmp which will be immediately removed
67+
TMPDIR: ${{ env.RUNNER_TEMP }}

test-runner/src/console.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::process::Termination;
2+
13
use ctru::prelude::*;
24
use ctru::services::gfx::{Flush, Swap};
35

@@ -28,11 +30,10 @@ impl TestRunner for ConsoleRunner {
2830
Console::new(self.gfx.top_screen.borrow_mut())
2931
}
3032

31-
fn cleanup(mut self, _test_result: std::io::Result<bool>) {
32-
// We don't actually care about the test result, either way we'll stop
33-
// and show the results to the user
33+
fn cleanup<T: Termination>(mut self, result: T) -> T {
34+
// We don't actually care about the output of the test result, either
35+
// way we'll stop and show the results to the user.
3436

35-
// Wait to make sure the user can actually see the results before we exit
3637
println!("Press START to exit.");
3738

3839
while self.apt.main_loop() {
@@ -47,5 +48,7 @@ impl TestRunner for ConsoleRunner {
4748
break;
4849
}
4950
}
51+
52+
result
5053
}
5154
}

test-runner/src/gdb.rs

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::process::Termination;
2+
13
use ctru::error::ResultCode;
24

35
use super::TestRunner;
@@ -27,22 +29,10 @@ impl TestRunner for GdbRunner {
2729
.expect("failed to redirect I/O streams to GDB");
2830
}
2931

30-
fn cleanup(self, test_result: std::io::Result<bool>) {
32+
fn cleanup<T: Termination>(self, test_result: T) -> T {
3133
// GDB actually has the opportunity to inspect the exit code,
3234
// unlike other runners, so let's follow the default behavior of the
3335
// stdlib test runner.
34-
match test_result {
35-
Ok(success) => {
36-
if success {
37-
std::process::exit(0);
38-
} else {
39-
std::process::exit(101);
40-
}
41-
}
42-
Err(err) => {
43-
eprintln!("Error: {err}");
44-
std::process::exit(101);
45-
}
46-
}
36+
test_result.report().exit_process()
4737
}
4838
}

test-runner/src/lib.rs

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,37 @@
66
77
#![feature(test)]
88
#![feature(custom_test_frameworks)]
9+
#![feature(exitcode_exit_method)]
910
#![test_runner(run_gdb)]
1011

1112
extern crate test;
1213

1314
mod console;
1415
mod gdb;
16+
mod macros;
1517
mod socket;
1618

17-
use console::ConsoleRunner;
18-
use gdb::GdbRunner;
19-
use socket::SocketRunner;
19+
use std::process::{ExitCode, Termination};
2020

21+
pub use console::ConsoleRunner;
22+
pub use gdb::GdbRunner;
23+
pub use socket::SocketRunner;
2124
use test::{ColorConfig, OutputFormat, TestDescAndFn, TestFn, TestOpts};
2225

2326
/// Show test output in GDB, using the [File I/O Protocol] (called HIO in some 3DS
2427
/// homebrew resources). Both stdout and stderr will be printed to the GDB console.
2528
///
2629
/// [File I/O Protocol]: https://sourceware.org/gdb/onlinedocs/gdb/File_002dI_002fO-Overview.html#File_002dI_002fO-Overview
2730
pub fn run_gdb(tests: &[&TestDescAndFn]) {
28-
run::<GdbRunner>(tests)
31+
run::<GdbRunner>(tests);
2932
}
3033

3134
/// Run tests using the `ctru` [`Console`] (print results to the 3DS screen).
3235
/// This is mostly useful for running tests manually, especially on real hardware.
3336
///
3437
/// [`Console`]: ctru::console::Console
3538
pub fn run_console(tests: &[&TestDescAndFn]) {
36-
run::<ConsoleRunner>(tests)
39+
run::<ConsoleRunner>(tests);
3740
}
3841

3942
/// Show test output via a network socket to `3dslink`. This runner is only useful
@@ -43,7 +46,7 @@ pub fn run_console(tests: &[&TestDescAndFn]) {
4346
///
4447
/// [`Soc::redirect_to_3dslink`]: ctru::services::soc::Soc::redirect_to_3dslink
4548
pub fn run_socket(tests: &[&TestDescAndFn]) {
46-
run::<SocketRunner>(tests)
49+
run::<SocketRunner>(tests);
4750
}
4851

4952
fn run<Runner: TestRunner>(tests: &[&TestDescAndFn]) {
@@ -71,7 +74,13 @@ fn run<Runner: TestRunner>(tests: &[&TestDescAndFn]) {
7174

7275
drop(ctx);
7376

74-
runner.cleanup(result);
77+
let reportable_result = match result {
78+
Ok(true) => Ok(()),
79+
// Try to match stdlib console test runner behavior as best we can
80+
_ => Err(ExitCode::from(101)),
81+
};
82+
83+
let _ = runner.cleanup(reportable_result);
7584
}
7685

7786
/// Adapted from [`test::make_owned_test`].
@@ -92,8 +101,16 @@ fn make_owned_test(test: &TestDescAndFn) -> TestDescAndFn {
92101
}
93102
}
94103

104+
mod private {
105+
pub trait Sealed {}
106+
107+
impl Sealed for super::ConsoleRunner {}
108+
impl Sealed for super::GdbRunner {}
109+
impl Sealed for super::SocketRunner {}
110+
}
111+
95112
/// A helper trait to make the behavior of test runners consistent.
96-
trait TestRunner: Sized + Default {
113+
pub trait TestRunner: private::Sealed + Sized + Default {
97114
/// Any context the test runner needs to remain alive for the duration of
98115
/// the test. This can be used for things that need to borrow the test runner
99116
/// itself.
@@ -107,7 +124,11 @@ trait TestRunner: Sized + Default {
107124

108125
/// Handle the results of the test and perform any necessary cleanup.
109126
/// The [`Context`](Self::Context) will be dropped just before this is called.
110-
fn cleanup(self, test_result: std::io::Result<bool>);
127+
///
128+
/// This returns `T` so that the result can be used in doctests.
129+
fn cleanup<T: Termination>(self, test_result: T) -> T {
130+
test_result
131+
}
111132
}
112133

113134
/// This module has stubs needed to link the test library, but they do nothing
@@ -132,17 +153,6 @@ mod link_fix {
132153
}
133154
}
134155

135-
/// Verify that doctests work as expected
136-
/// ```
137-
/// assert_eq!(2 + 2, 4);
138-
/// ```
139-
///
140-
/// ```should_panic
141-
/// assert_eq!(2 + 2, 5);
142-
/// ```
143-
#[cfg(doctest)]
144-
struct Dummy;
145-
146156
#[cfg(test)]
147157
mod tests {
148158
#[test]

test-runner/src/macros.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//! Macros for working with test runners.
2+
3+
// Use a neat little trick with cfg(doctest) to make code fences appear in
4+
// rustdoc output, but still compile normally when doctesting. This raises warnings
5+
// for invalid code though, so we also silence that lint here.
6+
#[cfg_attr(not(doctest), allow(rustdoc::invalid_rust_codeblocks))]
7+
/// Helper macro for writing doctests using this runner. Wrap this macro around
8+
/// your normal doctest to enable running it with the test runners in this crate.
9+
///
10+
/// You may optionally specify a runner before the test body, and may use any of
11+
/// the various [`fn main()`](https://doc.rust-lang.org/rustdoc/write-documentation/documentation-tests.html#using--in-doc-tests)
12+
/// signatures allowed by documentation tests.
13+
///
14+
/// # Examples
15+
///
16+
/// ## Basic usage
17+
///
18+
#[cfg_attr(not(doctest), doc = "````")]
19+
/// ```
20+
/// test_runner::doctest! {
21+
/// assert_eq!(2 + 2, 4);
22+
/// }
23+
/// ```
24+
#[cfg_attr(not(doctest), doc = "````")]
25+
///
26+
/// ## Custom runner
27+
///
28+
#[cfg_attr(not(doctest), doc = "````")]
29+
/// ```no_run
30+
/// test_runner::doctest! { SocketRunner,
31+
/// assert_eq!(2 + 2, 4);
32+
/// }
33+
/// ```
34+
#[cfg_attr(not(doctest), doc = "````")]
35+
///
36+
/// ## `should_panic`
37+
///
38+
#[cfg_attr(not(doctest), doc = "````")]
39+
/// ```should_panic
40+
/// test_runner::doctest! {
41+
/// assert_eq!(2 + 2, 5);
42+
/// }
43+
/// ```
44+
#[cfg_attr(not(doctest), doc = "````")]
45+
///
46+
/// ## Custom `fn main`
47+
///
48+
#[cfg_attr(not(doctest), doc = "````")]
49+
/// ```
50+
/// test_runner::doctest! {
51+
/// fn main() {
52+
/// assert_eq!(2 + 2, 4);
53+
/// }
54+
/// }
55+
/// ```
56+
#[cfg_attr(not(doctest), doc = "````")]
57+
///
58+
#[cfg_attr(not(doctest), doc = "````")]
59+
/// ```
60+
/// test_runner::doctest! {
61+
/// fn main() -> Result<(), Box<dyn std::error::Error>> {
62+
/// assert_eq!(2 + 2, 4);
63+
/// Ok(())
64+
/// }
65+
/// }
66+
/// ```
67+
#[cfg_attr(not(doctest), doc = "````")]
68+
///
69+
/// ## Implicit return type
70+
///
71+
/// Note that for the rustdoc preprocessor to understand the return type, the
72+
/// `Ok(())` expression must be written _outside_ the `doctest!` invocation.
73+
///
74+
#[cfg_attr(not(doctest), doc = "````")]
75+
/// ```
76+
/// test_runner::doctest! {
77+
/// assert_eq!(2 + 2, 4);
78+
/// }
79+
/// Ok::<(), std::io::Error>(())
80+
/// ```
81+
#[cfg_attr(not(doctest), doc = "````")]
82+
#[macro_export]
83+
macro_rules! doctest {
84+
($runner:ident, fn main() $(-> $ret:ty)? { $($body:tt)* } ) => {
85+
fn main() $(-> $ret)? {
86+
$crate::doctest!{ $runner, $($body)* }
87+
}
88+
};
89+
($runner:ident, $($body:tt)*) => {
90+
use $crate::TestRunner as _;
91+
let mut _runner = $crate::$runner::default();
92+
_runner.setup();
93+
let _result = { $($body)* };
94+
_runner.cleanup(_result)
95+
};
96+
($($body:tt)*) => {
97+
$crate::doctest!{ GdbRunner,
98+
$($body)*
99+
}
100+
};
101+
}

0 commit comments

Comments
 (0)