Browse Source

Copy test runner and actions from ctru-rs

pull/1/head
Ian Chamberlain 9 months ago
parent
commit
98fc093c43
No known key found for this signature in database
GPG Key ID: AE5484D09405AA60
  1. 1
      .github/CODEOWNERS
  2. 40
      .github/actions/setup/action.yml
  3. 67
      .github/workflows/ci.yml
  4. 14
      .gitignore
  5. 15
      Cargo.toml
  6. 29
      docker/citra.dockerfile
  7. 0
      docker/download_citra.sh
  8. 0
      docker/sdl2-config.ini
  9. 16
      docker/test-runner.gdb
  10. 51
      src/console.rs
  11. 48
      src/gdb.rs
  12. 158
      src/lib.rs
  13. 27
      src/socket.rs
  14. 1
      test-crate/.gitignore
  15. 119
      test-crate/Cargo.lock
  16. 9
      test-crate/Cargo.toml
  17. 53
      test-crate/src/main.rs
  18. 13
      tests/integration.rs

1
.github/CODEOWNERS

@ -0,0 +1 @@ @@ -0,0 +1 @@
* @rust3ds/active

40
.github/actions/setup/action.yml

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
name: Setup Rust3DS
description: Set up CI environment for Rust + 3DS development
inputs:
toolchain:
description: The Rust toolchain to use for the steps
required: true
default: nightly
runs:
using: composite
steps:
# https://github.com/nektos/act/issues/917#issuecomment-1074421318
- if: ${{ env.ACT }}
shell: bash
name: Hack container for local development
run: |
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
- name: Setup default Rust toolchain
# Use this helper action so we get matcher support
# https://github.com/actions-rust-lang/setup-rust-toolchain/pull/15
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: clippy, rustfmt, rust-src
toolchain: ${{ inputs.toolchain }}
- name: Install build tools for host
shell: bash
run: sudo apt-get update && sudo apt-get install -y build-essential
- name: Install cargo-3ds
shell: bash
run: cargo install cargo-3ds
- name: Set PATH to include devkitARM
shell: bash
# For some reason devkitARM/bin is not part of the default PATH in the container
run: echo "${DEVKITARM}/bin" >> $GITHUB_PATH

67
.github/workflows/ci.yml

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
name: CI
on:
push:
branches:
- master
pull_request:
branches:
- master
workflow_dispatch:
env:
# actions-rust-lang/setup-rust-toolchain sets some default RUSTFLAGS, which we don't want to use
RUSTFLAGS: ""
jobs:
lint:
strategy:
matrix:
toolchain:
- nightly-2023-06-01
runs-on: ubuntu-latest
container: devkitpro/devkitarm
steps:
- name: Checkout branch
uses: actions/checkout@v2
- uses: ./.github/actions/setup
with:
toolchain: ${{ matrix.toolchain }}
- name: Check formatting
run: cargo fmt --all --verbose -- --check
- name: Run clippy
# We have to build the test crate here since it's not included in build-std by default
run: cargo 3ds clippy -Zbuild-std=std,test --color=always --verbose --all-targets
test:
strategy:
matrix:
toolchain:
# Oldest supported nightly
- nightly-2023-06-01
- nightly
continue-on-error: ${{ matrix.toolchain == 'nightly' }}
runs-on: ubuntu-latest
container: devkitpro/devkitarm
steps:
- name: Checkout branch
uses: actions/checkout@v3
- uses: ./.github/actions/setup
with:
toolchain: ${{ matrix.toolchain }}
- name: Build lib tests
run: cargo 3ds test --no-run --lib
- name: Build integration tests
run: cargo 3ds test --no-run --test integration
- name: Build doc tests
# need build-std=test until https://github.com/rust3ds/cargo-3ds/pull/42
run: cargo 3ds test --doc -Zbuild-std=std,test

14
.gitignore vendored

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

15
Cargo.toml

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
[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"
shim-3ds = { git = "https://github.com/rust3ds/shim-3ds", version = "0.1.0" }

29
docker/citra.dockerfile

@ -1,29 +0,0 @@ @@ -1,29 +0,0 @@
FROM buildpack-deps:latest as builder
# ARG CITRA_RELEASE=nightly-1783
# ARG CITRA_RELEASE_FILE=citra-linux-20220902-746609f.tar.xz
ARG CITRA_CHANNEL=nightly
ARG CITRA_RELEASE=1816
WORKDIR /tmp
COPY ./citra/download_citra.sh /usr/local/bin/download_citra
RUN apt-get update -y && apt-get install -y jq
RUN download_citra ${CITRA_CHANNEL} ${CITRA_RELEASE}
FROM ubuntu:latest
RUN apt-get update -y && \
apt-get install -y \
libswscale5 \
libsdl2-2.0-0 \
libavformat58 \
libavfilter7 \
xvfb
COPY --from=builder /tmp/citra /usr/local/bin
COPY ./citra/sdl2-config.ini /root/.config/citra-emu/
WORKDIR /app
CMD [ "citra", "--version" ]

0
docker/citra/download_citra.sh → docker/download_citra.sh

0
docker/citra/sdl2-config.ini → docker/sdl2-config.ini

16
docker/test-runner.gdb

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
# https://github.com/devkitPro/libctru/blob/master/libctru/source/system/stack_adjust.s#LL28C23-L28C23
# or should this be `_exit` ?
break __ctru_exit
commands
# TODO: needed?
continue
# ARM calling convention will put the exit code in r0 when __ctru_exit is called.
# Just tell GDB to exit with the same code, since it doesn't get passed back when
# the program exits
quit $r0
end
# TODO: parametrize or pass as command line arg instead
target extended-remote 192.168.0.167:4003
# target extended-remote :4000
continue

51
src/console.rs

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
use ctru::prelude::*;
use ctru::services::gfx::{Flush, Swap};
use super::TestRunner;
pub struct ConsoleRunner {
gfx: Gfx,
hid: Hid,
apt: Apt,
}
impl Default for ConsoleRunner {
fn default() -> 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 }
}
}
impl TestRunner for ConsoleRunner {
type Context<'this> = Console<'this>;
fn setup(&mut self) -> Self::Context<'_> {
Console::new(self.gfx.top_screen.borrow_mut())
}
fn cleanup(mut self, _test_result: std::io::Result<bool>) {
// We don't actually care about the test result, either way we'll stop
// and show the results to the user
// Wait to make sure the user can actually see the results before we exit
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;
}
}
}
}

48
src/gdb.rs

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
use ctru::error::ResultCode;
use super::TestRunner;
#[derive(Default)]
pub struct GdbRunner;
impl Drop for GdbRunner {
fn drop(&mut self) {
unsafe { ctru_sys::gdbHioDevExit() }
}
}
impl TestRunner for GdbRunner {
type Context<'this> = ();
fn setup(&mut self) -> Self::Context<'_> {
// TODO: `ctru` expose safe API to do this and call that instead
|| -> ctru::Result<()> {
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");
}
fn cleanup(self, test_result: std::io::Result<bool>) {
// 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);
}
}
}
}

158
src/lib.rs

@ -0,0 +1,158 @@ @@ -0,0 +1,158 @@
//! 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)]
#![test_runner(run_gdb)]
extern crate test;
mod console;
mod gdb;
mod socket;
use console::ConsoleRunner;
use gdb::GdbRunner;
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
/// homebrew resources). Both stdout and stderr will be printed to the GDB console.
///
/// [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::<GdbRunner>(tests)
}
/// Run tests using the `ctru` [`Console`] (print results to the 3DS screen).
/// This is mostly useful for running tests manually, especially on real hardware.
///
/// [`Console`]: ctru::console::Console
pub fn run_console(tests: &[&TestDescAndFn]) {
run::<ConsoleRunner>(tests)
}
/// 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 fn run_socket(tests: &[&TestDescAndFn]) {
run::<SocketRunner>(tests)
}
fn run<Runner: TestRunner>(tests: &[&TestDescAndFn]) {
std::env::set_var("RUST_BACKTRACE", "1");
let mut runner = Runner::default();
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);
runner.cleanup(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 + 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.
// TODO: with associated type defaults this could be `= ();`
type Context<'this>
where
Self: 'this;
/// 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.
fn cleanup(self, test_result: std::io::Result<bool>);
}
/// 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
}
}
/// Verify that doctests work as expected
/// ```
/// assert_eq!(2 + 2, 4);
/// ```
///
/// ```should_panic
/// assert_eq!(2 + 2, 5);
/// ```
#[cfg(doctest)]
struct Dummy;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
#[should_panic]
fn it_fails() {
assert_eq!(2 + 2, 5);
}
}

27
src/socket.rs

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
use ctru::prelude::*;
use super::TestRunner;
pub struct SocketRunner {
soc: Soc,
}
impl Default for SocketRunner {
fn default() -> Self {
Self {
soc: Soc::new().expect("failed to initialize network service"),
}
}
}
impl TestRunner for SocketRunner {
type Context<'this> = ();
fn setup(&mut self) -> Self::Context<'_> {
self.soc
.redirect_to_3dslink(true, true)
.expect("failed to redirect to socket");
}
fn cleanup(self, _test_result: std::io::Result<bool>) {}
}

1
test-crate/.gitignore vendored

@ -1 +0,0 @@ @@ -1 +0,0 @@
/target

119
test-crate/Cargo.lock generated

@ -1,119 +0,0 @@ @@ -1,119 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "const-zero"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d3d68618a1f2c2d86e0bd2adec82e31960ca11aaeb5f353ff01746a4e08f36"
[[package]]
name = "ctru-rs"
version = "0.7.1"
source = "git+https://github.com/Meziu/ctru-rs.git#ac6c81e7819185be46576af3441f5260d39a2320"
dependencies = [
"bitflags",
"cfg-if",
"const-zero",
"ctru-sys",
"libc",
"linker-fix-3ds",
"once_cell",
"pthread-3ds",
"toml",
"widestring",
]
[[package]]
name = "ctru-sys"
version = "0.4.1"
source = "git+https://github.com/Meziu/ctru-rs.git#ac6c81e7819185be46576af3441f5260d39a2320"
dependencies = [
"libc",
]
[[package]]
name = "libc"
version = "0.2.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
[[package]]
name = "linker-fix-3ds"
version = "0.1.0"
source = "git+https://github.com/Meziu/rust-linker-fix-3ds.git#d5d3be4a0da876df6d6ac55cc8b48488713e149a"
dependencies = [
"ctru-sys",
"libc",
]
[[package]]
name = "once_cell"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
[[package]]
name = "pthread-3ds"
version = "0.1.0"
source = "git+https://github.com/Meziu/pthread-3ds.git#42a80c0e816251138df535648258671d93e047a6"
dependencies = [
"ctru-sys",
"libc",
"spin",
"static_assertions",
]
[[package]]
name = "serde"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
[[package]]
name = "spin"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "test-crate"
version = "0.1.0"
dependencies = [
"ctru-rs",
"ctru-sys",
]
[[package]]
name = "toml"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
dependencies = [
"serde",
]
[[package]]
name = "widestring"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7157704c2e12e3d2189c507b7482c52820a16dfa4465ba91add92f266667cadb"

9
test-crate/Cargo.toml

@ -1,9 +0,0 @@ @@ -1,9 +0,0 @@
[package]
name = "test-crate"
version = "0.1.0"
edition = "2021"
authors = [""]
[dependencies]
ctru-rs = { git = "https://github.com/Meziu/ctru-rs.git" }
ctru-sys = { git = "https://github.com/Meziu/ctru-rs.git" }

53
test-crate/src/main.rs

@ -1,53 +0,0 @@ @@ -1,53 +0,0 @@
use std::time::Duration;
use ctru::console::Console;
use ctru::gfx::Gfx;
use ctru::services::apt::Apt;
use ctru::services::hid::{Hid, KeyPad};
fn main() {
ctru::init();
let gfx = Gfx::init().expect("Couldn't obtain GFX controller");
let hid = Hid::init().expect("Couldn't obtain HID controller");
let apt = Apt::init().expect("Couldn't obtain APT controller");
// let _console = Console::init(gfx.top_screen.borrow_mut());
let _ = unsafe { ctru_sys::consoleDebugInit(ctru_sys::debugDevice_SVC) };
std::env::set_var("RUST_BACKTRACE", "full");
let res = unsafe { ctru_sys::gdbHioDevInit() };
if res != 0 {
eprintln!("failed to init gdbHIO: {res}");
} else {
eprintln!("init gdb hio");
}
// let res = unsafe { ctru_sys::gdbHioDevRedirectStdStreams(false, true, true) };
// if res != 0 {
// eprintln!("failed to redirect gdbHIO: {res}");
// } else {
// eprintln!("redirected gdb hio");
// }
println!("hey stdout");
eprintln!("hey stderr");
// Main loop
while apt.main_loop() {
//Scan all the inputs. This should be done once for each frame
hid.scan_input();
if hid.keys_down().contains(KeyPad::KEY_START) {
break;
}
// Flush and swap framebuffers
gfx.flush_buffers();
gfx.swap_buffers();
//Wait for VBlank
gfx.wait_for_vblank();
}
unsafe { ctru_sys::gdbHioDevExit() };
}

13
tests/integration.rs

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
// Workaround for https://github.com/rust-lang/rust/issues/94348
extern crate shim_3ds;
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
#[should_panic]
fn it_fails() {
assert_eq!(2 + 2, 5);
}
Loading…
Cancel
Save