diff --git a/.github/actions/citra/action.yml b/.github/actions/citra/action.yml deleted file mode 100644 index 18441f0..0000000 --- a/.github/actions/citra/action.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: 'Run 3DS Executable' -description: 'Run a given 3DS executable with citra and GDB' -inputs: - executable: - description: > - The 3DS executable(s) to run. Globs and space separated lists allowed. - required: true -runs: - using: docker - image: ../../../Dockerfile - args: - - ${{ inputs.executable }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04d0614..f117c0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,6 @@ on: - main workflow_dispatch: -env: - # actions-rust-lang/setup-rust-toolchain sets some default RUSTFLAGS, which we don't want to use - RUSTFLAGS: "" - jobs: lint: strategy: @@ -24,18 +20,19 @@ jobs: container: devkitpro/devkitarm steps: - name: Checkout branch - uses: actions/checkout@v2 + uses: actions/checkout@v4 - - uses: ./.github/actions/setup + - uses: ./setup with: toolchain: ${{ matrix.toolchain }} - name: Check formatting + working-directory: test-runner 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 + working-directory: test-runner + run: cargo 3ds clippy --color=always --verbose --all-targets test: strategy: @@ -47,38 +44,44 @@ jobs: continue-on-error: ${{ matrix.toolchain == 'nightly' }} runs-on: ubuntu-latest - container: devkitpro/devkitarm + container: + image: devkitpro/devkitarm + volumes: + # So the test action can `docker run` the runner: + - '/var/run/docker.sock:/var/run/docker.sock' steps: - name: Checkout branch - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - uses: ./.github/actions/setup + - uses: ./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 - run: cargo 3ds test --no-run --doc - - - name: Run lib + integration tests - uses: ./.github/actions/citra + - name: Build and run tests (unit + integration) + uses: ./run-tests with: - executable: ./target/armv6k-nintendo-3ds/debug/deps/*.elf - - # TODO: run doc tests. We might be able to do something with e.g. - # cargo's "runner" configuration, but it seems we also need a test - # runtime and stuff for that to work. + 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: Upload citra logs and capture videos uses: actions/upload-artifact@v3 - if: success() || failure() + # We always want to upload artifacts regardless of previous success/failure + if: ${{ !cancelled() }} with: name: citra-logs-${{ matrix.toolchain }} path: | - target/armv6k-nintendo-3ds/debug/deps/*.txt - target/armv6k-nintendo-3ds/debug/deps/*.webm + target/armv6k-nintendo-3ds/debug/**/*.txt + target/armv6k-nintendo-3ds/debug/**/*.webm diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index f183e67..0000000 --- a/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM buildpack-deps:latest as builder - -ARG CITRA_CHANNEL=nightly -ARG CITRA_RELEASE=1962 - -WORKDIR /tmp -COPY ./docker/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 devkitpro/devkitarm:latest as devkitarm - -FROM ubuntu:latest - -RUN --mount=type=cache,sharing=locked,target=/var/cache/apt \ - apt-get update -y && \ - apt-get install -y \ - libswscale5 \ - libsdl2-2.0-0 \ - libavformat58 \ - libavfilter7 \ - xvfb - -COPY --from=devkitarm /opt/devkitpro /opt/devkitpro -ENV PATH=/opt/devkitpro/devkitARM/bin:${PATH} - -COPY --from=builder /tmp/citra.AppImage /usr/local/bin/citra -COPY ./docker/sdl2-config.ini /app/ -COPY ./docker/test-runner.gdb /app/ -COPY ./docker/entrypoint.sh /app/ - -WORKDIR /app - -ENTRYPOINT [ "/app/entrypoint.sh" ] diff --git a/README.md b/README.md index 6141473..67018b1 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,60 @@ A set of tools for running automated Rust tests against Citra (3DS emulator). ## Components * `test-runner`: a Rust crate for writing tests for 3DS homebrew -* `Dockerfile`: builds a container for running test executables with Citra. * GitHub Actions: - * `.github/actions/setup`: action for setting up the Rust 3DS toolchain in - workflows - * `.github/actions/citra`: 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/ian-h-chamberlain/test-runner-3ds +``` + +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: + +```yml +jobs: + test: + runs-on: ubuntu-latest + container: + image: devkitpro/devkitarm + 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 + uses: actions/checkout@v4 + + - name: Setup Rust3DS toolchain + uses: ian-h-chamberlain/test-runner-3ds/setup@v1 + with: + # Optionally use a more specific nightly toolchain here if desired + toolchain: nightly + + - name: Build and run tests + uses: ian-h-chamberlain/test-runner-3ds/run-tests@v1 + with: + # Optionally add arguments to pass to `cargo 3ds test` + args: '' + # Optionally set the name of the built test-runner docker image + runner-image: test-runner-3ds + # Optionally change to a given directory before running tests. Note + # that this should use the environment variable ${GITHUB_WORKSPACE} + # rather than ${{ github.workspace }} to avoid the issue described in + # https://github.com/actions/runner/issues/2058 + working-directory: ${GITHUB_WORKSPACE} +``` diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100755 index a64e91d..0000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash - -# Clean up child processes on exit: https://stackoverflow.com/a/2173421/14436105 -trap "pkill -P $$" EXIT INT TERM - -mkdir -p ~/.config/citra-emu -cp /app/sdl2-config.ini ~/.config/citra-emu -# For some reason, log file is only written when this dir already exists, -# but it is only created after the first run of citra (our only run, in the container) -mkdir -p ~/.local/share/citra-emu/ - -ERRS=0 -# shellcheck disable=SC2068 -for EXE in $@; do - VIDEO_OUT="$(dirname "$EXE")/$(basename "$EXE" .elf)_capture.webm" - - # colored logs would be nice, but we can always just grab the plaintext log file - xvfb-run citra \ - --appimage-extract-and-run \ - --dump-video="$VIDEO_OUT" \ - "$EXE" \ - &>/dev/null & - PID=$! - - # Citra takes a little while to start up, so wait a little before we try to connect - sleep 3 - - arm-none-eabi-gdb --silent --batch-silent --command /app/test-runner.gdb "$EXE" - STATUS=$? - if [ $STATUS -ne 0 ]; then - echo >&2 "FAILED (exit status $STATUS): $EXE" - ERRS=$(( ERRS + 1 )) - fi - - kill -INT $PID &>/dev/null - sleep 1 - if kill -0 $PID &>/dev/null; then - kill -KILL $PID &>/dev/null - fi - - CITRA_LOG=~/.local/share/citra-emu/log/citra_log.txt - CITRA_LOG_OUT="$(dirname "$EXE")/$(basename "$EXE" .elf)_citra_log.txt" - if test -f "$CITRA_LOG"; then - cp "$CITRA_LOG" "$CITRA_LOG_OUT" - else - echo "WARNING: citra log not found" - fi -done - -exit $ERRS diff --git a/.dockerignore b/run-tests/.dockerignore similarity index 100% rename from .dockerignore rename to run-tests/.dockerignore diff --git a/run-tests/Dockerfile b/run-tests/Dockerfile new file mode 100644 index 0000000..aa95b1d --- /dev/null +++ b/run-tests/Dockerfile @@ -0,0 +1,54 @@ +FROM buildpack-deps:latest as builder + +WORKDIR /tmp +COPY ./docker/download_citra.sh /usr/local/bin/download_citra +RUN apt-get update -y && apt-get install -y jq + +ARG CITRA_CHANNEL=nightly +ARG CITRA_RELEASE=1995 +RUN download_citra ${CITRA_CHANNEL} ${CITRA_RELEASE} + +FROM devkitpro/devkitarm:latest as devkitarm + +# 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 +RUN cd /opt/devkitpro/examples/3ds/graphics/printing/hello-world && \ + echo 'int main(int, char**) {}' > source/main.c && \ + make && \ + mv hello-world.3dsx /tmp/ + +FROM ubuntu:latest + +RUN --mount=type=cache,sharing=locked,target=/var/cache/apt \ + apt-get update -y && \ + apt-get install -y \ + libswscale5 \ + libsdl2-2.0-0 \ + libavformat58 \ + libavfilter7 \ + xvfb + +COPY --from=devkitarm /opt/devkitpro /opt/devkitpro +# There's no way to copy ENV values from other stages properly: +# https://github.com/moby/moby/issues/37345 +# Luckily in this case we know exactly what the values should be: +ENV DEVKITPRO=/opt/devkitpro +ENV DEVKITARM=${DEVKITPRO}/devkitARM +ENV PATH=${DEVKITARM}/bin:${PATH} + +COPY --from=builder /tmp/citra.AppImage /usr/local/bin/citra +COPY --from=devkitarm /tmp/hello-world.3dsx /tmp/ +# We run citra once before copying our config file, so it should create its +# necessary directory structure and run once with defaults +RUN xvfb-run citra --appimage-extract-and-run /tmp/hello-world.3dsx; \ + rm -f /tmp/hello-world.3dsx +# Initial run seems to miss this one directory so just make it manually +RUN mkdir -p /root/.local/share/citra-emu/log + +COPY ./docker/sdl2-config.ini /root/.config/citra-emu/ +COPY ./docker/test-runner.gdb /app/ +COPY ./docker/entrypoint.sh /app/ + +WORKDIR /app + +ENTRYPOINT [ "/app/entrypoint.sh" ] diff --git a/run-tests/action.yml b/run-tests/action.yml new file mode 100644 index 0000000..c9255dd --- /dev/null +++ b/run-tests/action.yml @@ -0,0 +1,61 @@ +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. + +inputs: + args: + description: Extra arguments to pass to `cargo 3ds test` + required: false + default: '' + + runner-image: + description: The name of the container image to build for running tests in + required: false + default: test-runner-3ds + + working-directory: + description: Change to this directory before running tests. Defaults to $GITHUB_WORKSPACE + required: false + default: ${GITHUB_WORKSPACE} + +runs: + using: composite + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build test-runner image + uses: docker/build-push-action@v4 + with: + context: ${{ github.action_path }} + tags: ${{ inputs.runner-image }}:latest + push: false + load: true + + - name: Ensure docker is installed in the container + shell: bash + run: apt-get update -y && apt-get install docker.io -y + + - name: Run cargo 3ds test + shell: bash + # Set a custom runner for `cargo test` commands to use. + # Use ${GITHUB_WORKSPACE} due to + # https://github.com/actions/runner/issues/2058, which also means + # we have to export this instead of using the env: key + run: | + cd ${{ inputs.working-directory }} + export CARGO_TARGET_ARMV6K_NINTENDO_3DS_RUNNER=" + docker run --rm + -v ${{ runner.temp }}:${{ runner.temp }} + -v ${{ github.workspace }}/target:/app/target + -v ${{ github.workspace }}:${GITHUB_WORKSPACE} + ${{ inputs.runner-image }}:latest" + env + cargo 3ds -v test ${{ inputs.args }} + env: + # Ensure that doctests get built into a path which is mounted on the host + # as well as in this container (via the bind mount in the RUNNER command) + TMPDIR: ${{ runner.temp }} diff --git a/docker/download_citra.sh b/run-tests/docker/download_citra.sh similarity index 89% rename from docker/download_citra.sh rename to run-tests/docker/download_citra.sh index ec6699c..fd12cea 100755 --- a/docker/download_citra.sh +++ b/run-tests/docker/download_citra.sh @@ -9,7 +9,7 @@ RELEASE_API="https://api.github.com/repos/citra-emu/citra-${CITRA_CHANNEL}/relea curl "${RELEASE_API}" | jq --raw-output '.assets[].browser_download_url' | - grep -E 'citra-linux-.*.tar.gz' | + grep -E 'citra-linux-.*[.]tar.gz' | xargs wget -O citra-linux.tar.gz tar --strip-components 1 -xvf citra-linux.tar.gz diff --git a/run-tests/docker/entrypoint.sh b/run-tests/docker/entrypoint.sh new file mode 100755 index 0000000..3e2ecbf --- /dev/null +++ b/run-tests/docker/entrypoint.sh @@ -0,0 +1,68 @@ +#!/bin/bash + +# Uncomment for debugging the action itself. Maybe consider a job summary or +# grouping the output, to keep this stuff visible but make it simpler to use: +# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions + +# set -x + +function cleanup_jobs() { + # shellcheck disable=SC2317 # Unreachable because it's only used in trap + if [ -n "$(jobs -p)" ]; then + sleep 5 & + wait -n + # shellcheck disable=SC2046 # We want to expand jobs here and for `wait` + kill -9 $(jobs -p) + # shellcheck disable=SC2046 + wait $(jobs -p) &>/dev/null + fi +} + +trap cleanup_jobs EXIT + +EXE_ELF=$1 +EXE_3DSX="$(dirname "$EXE")/$(basename "$EXE" .elf).3dsx" + +EXE_TO_RUN="$EXE_ELF" +if [ -f "$EXE_3DSX" ]; then + echo >&2 "Found $(basename "$EXE_3DSX"), it will be run instead of $(basename "$EXE_ELF")" + EXE_TO_RUN="$EXE_3DSX" +fi + +VIDEO_OUT="$(dirname "$EXE_ELF")/$(basename "$EXE_ELF" .elf)_capture.webm" + +CITRA_LOG_DIR=~/.local/share/citra-emu/log +CITRA_OUT="$CITRA_LOG_DIR/citra_output.txt" + +xvfb-run --auto-servernum \ + citra \ + --appimage-extract-and-run \ + --dump-video="$VIDEO_OUT" \ + "$EXE_TO_RUN" \ + &>"$CITRA_OUT" & +CITRA_PID=$! + +# Citra takes a little while to start up, so wait a little before we try to connect +sleep 5 + +arm-none-eabi-gdb --silent --batch-silent --command /app/test-runner.gdb "$EXE_ELF" +STATUS=$? + +kill $CITRA_PID +cleanup_jobs + +CITRA_LOG="$CITRA_LOG_DIR/citra_log.txt" + +for f in "$CITRA_LOG" "$CITRA_OUT"; do + OUT="$(dirname "$EXE_ELF")/$(basename "$EXE_ELF" .elf)_$(basename "$f")" + if test -f "$f"; then + cp "$f" "$OUT" + if [ $STATUS -ne 0 ]; then + echo >&2 "$(basename $f) copied to $OUT" + fi + else + echo >&2 "WARNING: $(basename "$f") not found" + fi +done + +exit $STATUS diff --git a/docker/sdl2-config.ini b/run-tests/docker/sdl2-config.ini similarity index 100% rename from docker/sdl2-config.ini rename to run-tests/docker/sdl2-config.ini diff --git a/docker/test-runner.gdb b/run-tests/docker/test-runner.gdb similarity index 100% rename from docker/test-runner.gdb rename to run-tests/docker/test-runner.gdb diff --git a/.github/actions/setup/action.yml b/setup/action.yml similarity index 72% rename from .github/actions/setup/action.yml rename to setup/action.yml index 95629df..60167b7 100644 --- a/.github/actions/setup/action.yml +++ b/setup/action.yml @@ -10,14 +10,6 @@ inputs: 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 @@ -25,11 +17,13 @@ runs: with: components: clippy, rustfmt, rust-src toolchain: ${{ inputs.toolchain }} + cache: false # We set up our own cache manually in the next step + rustflags: "" - name: Set up Rust cache uses: Swatinem/rust-cache@v2 with: - shared-key: rust3ds + cache-on-failure: true - name: Install build tools for host shell: bash @@ -38,7 +32,7 @@ runs: - name: Install cargo-3ds shell: bash # TODO: replace with crates.io version once published - run: cargo install --git https://github.com/rust3ds/cargo-3ds --branch feature/verbose-flag + run: cargo install --locked --git https://github.com/rust3ds/cargo-3ds - name: Set PATH to include devkitARM shell: bash diff --git a/Cargo.toml b/test-runner/Cargo.toml similarity index 54% rename from Cargo.toml rename to test-runner/Cargo.toml index 6d49699..eddd1c3 100644 --- a/Cargo.toml +++ b/test-runner/Cargo.toml @@ -12,3 +12,10 @@ socket = [] ctru-rs = { git = "https://github.com/rust3ds/ctru-rs" } ctru-sys = { git = "https://github.com/rust3ds/ctru-rs" } libc = "0.2.147" + +[patch."https://github.com/ian-h-chamberlain/test-runner-3ds"] +test-runner = { path = "." } + +# Future-proofing for a rename + org move: +[patch."https://github.com/rust3ds/test-runner"] +test-runner = { path = "." } diff --git a/src/console.rs b/test-runner/src/console.rs similarity index 100% rename from src/console.rs rename to test-runner/src/console.rs diff --git a/src/gdb.rs b/test-runner/src/gdb.rs similarity index 100% rename from src/gdb.rs rename to test-runner/src/gdb.rs diff --git a/src/lib.rs b/test-runner/src/lib.rs similarity index 100% rename from src/lib.rs rename to test-runner/src/lib.rs diff --git a/src/socket.rs b/test-runner/src/socket.rs similarity index 100% rename from src/socket.rs rename to test-runner/src/socket.rs diff --git a/tests/integration.rs b/test-runner/tests/integration.rs similarity index 100% rename from tests/integration.rs rename to test-runner/tests/integration.rs