diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ca27d87 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +** +!docker/ diff --git a/.github/actions/citra/action.yml b/.github/actions/citra/action.yml new file mode 100644 index 0000000..18441f0 --- /dev/null +++ b/.github/actions/citra/action.yml @@ -0,0 +1,12 @@ +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/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..95629df --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,46 @@ +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: Set up Rust cache + uses: Swatinem/rust-cache@v2 + with: + shared-key: rust3ds + + - 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 + # TODO: replace with crates.io version once published + run: cargo install --git https://github.com/rust3ds/cargo-3ds --branch feature/verbose-flag + + - 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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..04d0614 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,84 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - 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: + 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 + run: cargo 3ds test --no-run --doc + + - name: Run lib + integration tests + uses: ./.github/actions/citra + 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. + + - name: Upload citra logs and capture videos + uses: actions/upload-artifact@v3 + if: success() || failure() + with: + name: citra-logs-${{ matrix.toolchain }} + path: | + target/armv6k-nintendo-3ds/debug/deps/*.txt + target/armv6k-nintendo-3ds/debug/deps/*.webm diff --git a/.gitignore b/.gitignore index 2a6b1e1..6985cf1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,14 @@ -# Ignore files generated by citra or the VNC client -citra/out -driver/out +# 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 diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6d49699 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[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" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f183e67 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +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 1901e4b..6141473 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,14 @@ A set of tools for running automated Rust tests against Citra (3DS emulator). -## Usage +## Components -`./run.sh 3DSX_FILE` +* `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 -## Goals - -* Docker container for manually running tests against Citra -* GitHub Action for running automated tests -* Rust testing framework (custom runner) for use with the 3ds -* (maybe) Acceptance testing framework or glue for one? - -## Workflow / Notes - -1. Build a test executable (type tbd) -1. `citra-emu` container: bind-mount test executable and choose it -1. `driver` container perform input / output as needed for test, via VNC - - * possible extension: `3dslink -s` to get actual stdout/stderr (return code?) - - * acceptance testing of images, hopefully via screenshot + diff --git a/citra/qt-config.ini b/citra/qt-config.ini deleted file mode 100644 index 1ad0544..0000000 --- a/citra/qt-config.ini +++ /dev/null @@ -1,496 +0,0 @@ -[Audio] -enable_audio_stretching=true -enable_audio_stretching\default=true -enable_dsp_lle=false -enable_dsp_lle\default=true -enable_dsp_lle_multithread=false -enable_dsp_lle_multithread\default=true -mic_input_device=Default -mic_input_device\default=true -mic_input_type=0 -mic_input_type\default=true -output_device=auto -output_device\default=true -output_engine=auto -output_engine\default=true -volume=@Variant(\0\0\0\x87?\x80\0\0) -volume\default=true - -[Camera] -camera_inner_config= -camera_inner_config\default=true -camera_inner_flip=0 -camera_inner_flip\default=true -camera_inner_name=blank -camera_inner_name\default=true -camera_outer_left_config= -camera_outer_left_config\default=true -camera_outer_left_flip=0 -camera_outer_left_flip\default=true -camera_outer_left_name=blank -camera_outer_left_name\default=true -camera_outer_right_config= -camera_outer_right_config\default=true -camera_outer_right_flip=0 -camera_outer_right_flip\default=true -camera_outer_right_name=blank -camera_outer_right_name\default=true - -[Controls] -profile=0 -profile\default=true -profiles\1\button_a="code:65,engine:keyboard" -profiles\1\button_a\default=true -profiles\1\button_b="code:83,engine:keyboard" -profiles\1\button_b\default=true -profiles\1\button_debug="code:79,engine:keyboard" -profiles\1\button_debug\default=true -profiles\1\button_down="code:71,engine:keyboard" -profiles\1\button_down\default=true -profiles\1\button_gpio14="code:80,engine:keyboard" -profiles\1\button_gpio14\default=true -profiles\1\button_home="code:66,engine:keyboard" -profiles\1\button_home\default=true -profiles\1\button_l="code:81,engine:keyboard" -profiles\1\button_l\default=true -profiles\1\button_left="code:70,engine:keyboard" -profiles\1\button_left\default=true -profiles\1\button_r="code:87,engine:keyboard" -profiles\1\button_r\default=true -profiles\1\button_right="code:72,engine:keyboard" -profiles\1\button_right\default=true -profiles\1\button_select="code:78,engine:keyboard" -profiles\1\button_select\default=true -profiles\1\button_start="code:77,engine:keyboard" -profiles\1\button_start\default=true -profiles\1\button_up="code:84,engine:keyboard" -profiles\1\button_up\default=true -profiles\1\button_x="code:90,engine:keyboard" -profiles\1\button_x\default=true -profiles\1\button_y="code:88,engine:keyboard" -profiles\1\button_y\default=true -profiles\1\button_zl="code:49,engine:keyboard" -profiles\1\button_zl\default=true -profiles\1\button_zr="code:50,engine:keyboard" -profiles\1\button_zr\default=true -profiles\1\c_stick="down:code$075$1engine$0keyboard,engine:analog_from_button,left:code$074$1engine$0keyboard,modifier:code$068$1engine$0keyboard,modifier_scale:0.500000,right:code$076$1engine$0keyboard,up:code$073$1engine$0keyboard" -profiles\1\c_stick\default=true -profiles\1\circle_pad="down:code$016777237$1engine$0keyboard,engine:analog_from_button,left:code$016777234$1engine$0keyboard,modifier:code$068$1engine$0keyboard,modifier_scale:0.500000,right:code$016777236$1engine$0keyboard,up:code$016777235$1engine$0keyboard" -profiles\1\circle_pad\default=true -profiles\1\motion_device="engine:motion_emu,update_period:100,sensitivity:0.01,tilt_clamp:90.0" -profiles\1\motion_device\default=true -profiles\1\name=default -profiles\1\name\default=true -profiles\1\touch_device=engine:emu_window -profiles\1\touch_device\default=true -profiles\1\touch_from_button_map=0 -profiles\1\touch_from_button_map\default=true -profiles\1\udp_input_address=127.0.0.1 -profiles\1\udp_input_address\default=true -profiles\1\udp_input_port=26760 -profiles\1\udp_input_port\default=true -profiles\1\udp_pad_index=0 -profiles\1\udp_pad_index\default=true -profiles\1\use_touch_from_button=false -profiles\1\use_touch_from_button\default=true -profiles\size=1 -touch_from_button_maps\1\entries\size=0 -touch_from_button_maps\1\name=default -touch_from_button_maps\1\name\default=true -touch_from_button_maps\size=1 - -[Core] -cpu_clock_percentage=100 -cpu_clock_percentage\default=true -use_cpu_jit=true -use_cpu_jit\default=true - -[Data%20Storage] -nand_directory=/root/.local/share/citra-emu/nand/ -nand_directory\default=true -sdmc_directory=/root/.local/share/citra-emu/sdmc/ -sdmc_directory\default=true -use_virtual_sd=true -use_virtual_sd\default=true - -[Debugging] -LLE\AC=false -LLE\AC\default=true -LLE\ACT=false -LLE\ACT\default=true -LLE\AM=false -LLE\AM\default=true -LLE\BOSS=false -LLE\BOSS\default=true -LLE\CAM=false -LLE\CAM\default=true -LLE\CDC=false -LLE\CDC\default=true -LLE\CECD=false -LLE\CECD\default=true -LLE\CFG=false -LLE\CFG\default=true -LLE\CSND=false -LLE\CSND\default=true -LLE\DLP=false -LLE\DLP\default=true -LLE\DSP=false -LLE\DSP\default=true -LLE\ERR=false -LLE\ERR\default=true -LLE\FRD=false -LLE\FRD\default=true -LLE\FS=false -LLE\FS\default=true -LLE\GPIO=false -LLE\GPIO\default=true -LLE\GSP=false -LLE\GSP\default=true -LLE\HID=false -LLE\HID\default=true -LLE\HTTP=false -LLE\HTTP\default=true -LLE\I2C=false -LLE\I2C\default=true -LLE\IR=false -LLE\IR\default=true -LLE\LDR=false -LLE\LDR\default=true -LLE\MCU=false -LLE\MCU\default=true -LLE\MIC=false -LLE\MIC\default=true -LLE\MP=false -LLE\MP\default=true -LLE\MVD=false -LLE\MVD\default=true -LLE\NDM=false -LLE\NDM\default=true -LLE\NEWS=false -LLE\NEWS\default=true -LLE\NFC=false -LLE\NFC\default=true -LLE\NIM=false -LLE\NIM\default=true -LLE\NS=false -LLE\NS\default=true -LLE\NWM=false -LLE\NWM\default=true -LLE\PDN=false -LLE\PDN\default=true -LLE\PM=false -LLE\PM\default=true -LLE\PS=false -LLE\PS\default=true -LLE\PTM=false -LLE\PTM\default=true -LLE\PXI=false -LLE\PXI\default=true -LLE\QTM=false -LLE\QTM\default=true -LLE\SOC=false -LLE\SOC\default=true -LLE\SPI=false -LLE\SPI\default=true -LLE\SSL=false -LLE\SSL\default=true -gdbstub_port=24689 -gdbstub_port\default=true -record_frame_times=false -use_gdbstub=false -use_gdbstub\default=true - -[Layout] -custom_bottom_bottom=480 -custom_bottom_bottom\default=true -custom_bottom_left=40 -custom_bottom_left\default=true -custom_bottom_right=360 -custom_bottom_right\default=true -custom_bottom_top=240 -custom_bottom_top\default=true -custom_layout=false -custom_layout\default=true -custom_top_bottom=240 -custom_top_bottom\default=true -custom_top_left=0 -custom_top_left\default=true -custom_top_right=400 -custom_top_right\default=true -custom_top_top=0 -custom_top_top\default=true -factor_3d=0 -factor_3d\default=true -filter_mode=true -filter_mode\default=true -layout_option=0 -pp_shader_name=none (builtin) -pp_shader_name\default=true -render_3d=0 -render_3d\default=true -swap_screen=false -swap_screen\default=true -upright_screen=false -upright_screen\default=true - -[Miscellaneous] -log_filter=*:Info -log_filter\default=true - -[Renderer] -bg_blue=0.545098066329956 -bg_blue\default=false -bg_green=0.545098066329956 -bg_green\default=false -bg_red=0.545098066329956 -bg_red\default=false -frame_limit=100 -frame_limit\default=true -frame_limit_alternate=200 -frame_limit_alternate\default=true -resolution_factor=1 -resolution_factor\default=true -shaders_accurate_mul=true -shaders_accurate_mul\default=true -texture_filter_name=none -texture_filter_name\default=true -use_disk_shader_cache=true -use_disk_shader_cache\default=true -use_frame_limit_alternate=false -use_frame_limit_alternate\default=true -use_hw_renderer=true -use_hw_renderer\default=true -use_hw_shader=true -use_hw_shader\default=true -use_shader_jit=true -use_shader_jit\default=true -use_vsync_new=true -use_vsync_new\default=true - -[System] -init_clock=0 -init_clock\default=true -init_time=946681277 -init_time\default=true -is_new_3ds=true -is_new_3ds\default=true -region_value=-1 -region_value\default=true - -[UI] -GameList\hideNoIcon=false -GameList\hideNoIcon\default=true -GameList\iconSize=2 -GameList\iconSize\default=true -GameList\row1=2 -GameList\row1\default=true -GameList\row2=0 -GameList\row2\default=true -GameList\singleLineMode=false -GameList\singleLineMode\default=true -Multiplayer\game_id=0 -Multiplayer\game_id\default=true -Multiplayer\host_type=0 -Multiplayer\host_type\default=true -Multiplayer\ip= -Multiplayer\ip\default=true -Multiplayer\ip_ban_list\size=0 -Multiplayer\max_player=8 -Multiplayer\max_player\default=true -Multiplayer\nickname= -Multiplayer\nickname\default=true -Multiplayer\port=24872 -Multiplayer\port\default=true -Multiplayer\room_description= -Multiplayer\room_description\default=true -Multiplayer\room_name= -Multiplayer\room_name\default=true -Multiplayer\room_nickname= -Multiplayer\room_nickname\default=true -Multiplayer\room_port=24872 -Multiplayer\room_port\default=true -Multiplayer\username_ban_list\size=0 -Paths\gamedirs\1\deep_scan=false -Paths\gamedirs\1\deep_scan\default=true -Paths\gamedirs\1\expanded=true -Paths\gamedirs\1\expanded\default=true -Paths\gamedirs\1\path=INSTALLED -Paths\gamedirs\2\deep_scan=false -Paths\gamedirs\2\deep_scan\default=true -Paths\gamedirs\2\expanded=true -Paths\gamedirs\2\expanded\default=true -Paths\gamedirs\2\path=SYSTEM -Paths\gamedirs\size=2 -Paths\language=en -Paths\language\default=false -Paths\moviePlaybackPath= -Paths\movieRecordPath= -Paths\recentFiles=@Invalid() -Paths\romsPath= -Paths\screenshotPath= -Paths\symbolsPath= -Paths\videoDumpingPath= -Shortcuts\Main%20Window\Advance%20Frame\Context=2 -Shortcuts\Main%20Window\Advance%20Frame\Context\default=true -Shortcuts\Main%20Window\Advance%20Frame\KeySeq=\\ -Shortcuts\Main%20Window\Advance%20Frame\KeySeq\default=true -Shortcuts\Main%20Window\Capture%20Screenshot\Context=2 -Shortcuts\Main%20Window\Capture%20Screenshot\Context\default=true -Shortcuts\Main%20Window\Capture%20Screenshot\KeySeq=Ctrl+P -Shortcuts\Main%20Window\Capture%20Screenshot\KeySeq\default=true -Shortcuts\Main%20Window\Continue\Pause%20Emulation\Context=1 -Shortcuts\Main%20Window\Continue\Pause%20Emulation\Context\default=true -Shortcuts\Main%20Window\Continue\Pause%20Emulation\KeySeq=F4 -Shortcuts\Main%20Window\Continue\Pause%20Emulation\KeySeq\default=true -Shortcuts\Main%20Window\Decrease%20Speed%20Limit\Context=2 -Shortcuts\Main%20Window\Decrease%20Speed%20Limit\Context\default=true -Shortcuts\Main%20Window\Decrease%20Speed%20Limit\KeySeq=- -Shortcuts\Main%20Window\Decrease%20Speed%20Limit\KeySeq\default=true -Shortcuts\Main%20Window\Exit%20Citra\Context=1 -Shortcuts\Main%20Window\Exit%20Citra\Context\default=true -Shortcuts\Main%20Window\Exit%20Citra\KeySeq=Ctrl+Q -Shortcuts\Main%20Window\Exit%20Citra\KeySeq\default=true -Shortcuts\Main%20Window\Exit%20Fullscreen\Context=1 -Shortcuts\Main%20Window\Exit%20Fullscreen\Context\default=true -Shortcuts\Main%20Window\Exit%20Fullscreen\KeySeq=Esc -Shortcuts\Main%20Window\Exit%20Fullscreen\KeySeq\default=true -Shortcuts\Main%20Window\Fullscreen\Context=1 -Shortcuts\Main%20Window\Fullscreen\Context\default=true -Shortcuts\Main%20Window\Fullscreen\KeySeq=F11 -Shortcuts\Main%20Window\Fullscreen\KeySeq\default=true -Shortcuts\Main%20Window\Increase%20Speed%20Limit\Context=2 -Shortcuts\Main%20Window\Increase%20Speed%20Limit\Context\default=true -Shortcuts\Main%20Window\Increase%20Speed%20Limit\KeySeq=+ -Shortcuts\Main%20Window\Increase%20Speed%20Limit\KeySeq\default=true -Shortcuts\Main%20Window\Load%20Amiibo\Context=2 -Shortcuts\Main%20Window\Load%20Amiibo\Context\default=true -Shortcuts\Main%20Window\Load%20Amiibo\KeySeq=F2 -Shortcuts\Main%20Window\Load%20Amiibo\KeySeq\default=true -Shortcuts\Main%20Window\Load%20File\Context=1 -Shortcuts\Main%20Window\Load%20File\Context\default=true -Shortcuts\Main%20Window\Load%20File\KeySeq=Ctrl+O -Shortcuts\Main%20Window\Load%20File\KeySeq\default=true -Shortcuts\Main%20Window\Load%20from%20Newest%20Slot\Context=1 -Shortcuts\Main%20Window\Load%20from%20Newest%20Slot\Context\default=true -Shortcuts\Main%20Window\Load%20from%20Newest%20Slot\KeySeq=Ctrl+V -Shortcuts\Main%20Window\Load%20from%20Newest%20Slot\KeySeq\default=true -Shortcuts\Main%20Window\Remove%20Amiibo\Context=2 -Shortcuts\Main%20Window\Remove%20Amiibo\Context\default=true -Shortcuts\Main%20Window\Remove%20Amiibo\KeySeq=F3 -Shortcuts\Main%20Window\Remove%20Amiibo\KeySeq\default=true -Shortcuts\Main%20Window\Restart%20Emulation\Context=1 -Shortcuts\Main%20Window\Restart%20Emulation\Context\default=true -Shortcuts\Main%20Window\Restart%20Emulation\KeySeq=F6 -Shortcuts\Main%20Window\Restart%20Emulation\KeySeq\default=true -Shortcuts\Main%20Window\Rotate%20Screens%20Upright\Context=1 -Shortcuts\Main%20Window\Rotate%20Screens%20Upright\Context\default=true -Shortcuts\Main%20Window\Rotate%20Screens%20Upright\KeySeq=F8 -Shortcuts\Main%20Window\Rotate%20Screens%20Upright\KeySeq\default=true -Shortcuts\Main%20Window\Save%20to%20Oldest%20Slot\Context=1 -Shortcuts\Main%20Window\Save%20to%20Oldest%20Slot\Context\default=true -Shortcuts\Main%20Window\Save%20to%20Oldest%20Slot\KeySeq=Ctrl+C -Shortcuts\Main%20Window\Save%20to%20Oldest%20Slot\KeySeq\default=true -Shortcuts\Main%20Window\Stop%20Emulation\Context=1 -Shortcuts\Main%20Window\Stop%20Emulation\Context\default=true -Shortcuts\Main%20Window\Stop%20Emulation\KeySeq=F5 -Shortcuts\Main%20Window\Stop%20Emulation\KeySeq\default=true -Shortcuts\Main%20Window\Swap%20Screens\Context=1 -Shortcuts\Main%20Window\Swap%20Screens\Context\default=true -Shortcuts\Main%20Window\Swap%20Screens\KeySeq=F9 -Shortcuts\Main%20Window\Swap%20Screens\KeySeq\default=true -Shortcuts\Main%20Window\Toggle%20Alternate%20Speed\Context=2 -Shortcuts\Main%20Window\Toggle%20Alternate%20Speed\Context\default=true -Shortcuts\Main%20Window\Toggle%20Alternate%20Speed\KeySeq=Ctrl+Z -Shortcuts\Main%20Window\Toggle%20Alternate%20Speed\KeySeq\default=true -Shortcuts\Main%20Window\Toggle%20Filter%20Bar\Context=1 -Shortcuts\Main%20Window\Toggle%20Filter%20Bar\Context\default=true -Shortcuts\Main%20Window\Toggle%20Filter%20Bar\KeySeq=Ctrl+F -Shortcuts\Main%20Window\Toggle%20Filter%20Bar\KeySeq\default=true -Shortcuts\Main%20Window\Toggle%20Frame%20Advancing\Context=2 -Shortcuts\Main%20Window\Toggle%20Frame%20Advancing\Context\default=true -Shortcuts\Main%20Window\Toggle%20Frame%20Advancing\KeySeq=Ctrl+A -Shortcuts\Main%20Window\Toggle%20Frame%20Advancing\KeySeq\default=true -Shortcuts\Main%20Window\Toggle%20Screen%20Layout\Context=1 -Shortcuts\Main%20Window\Toggle%20Screen%20Layout\Context\default=true -Shortcuts\Main%20Window\Toggle%20Screen%20Layout\KeySeq=F10 -Shortcuts\Main%20Window\Toggle%20Screen%20Layout\KeySeq\default=true -Shortcuts\Main%20Window\Toggle%20Status%20Bar\Context=1 -Shortcuts\Main%20Window\Toggle%20Status%20Bar\Context\default=true -Shortcuts\Main%20Window\Toggle%20Status%20Bar\KeySeq=Ctrl+S -Shortcuts\Main%20Window\Toggle%20Status%20Bar\KeySeq\default=true -Shortcuts\Main%20Window\Toggle%20Texture%20Dumping\Context=2 -Shortcuts\Main%20Window\Toggle%20Texture%20Dumping\Context\default=true -Shortcuts\Main%20Window\Toggle%20Texture%20Dumping\KeySeq=Ctrl+D -Shortcuts\Main%20Window\Toggle%20Texture%20Dumping\KeySeq\default=true -UILayout\gameListHeaderState=@ByteArray(\0\0\0\xff\0\0\0\0\0\0\0\x1\0\0\0\x1\0\0\0\x5\x1\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x4\x98\0\0\0\x5\x1\x1\0\x1\0\0\0\0\0\0\0\0\0\0\0\0h\xff\xff\xff\xff\0\0\0\x81\0\0\0\0\0\0\0\x5\0\0\x2\x80\0\0\0\x1\0\0\0\0\0\0\0h\0\0\0\x1\0\0\0\0\0\0\0h\0\0\0\x1\0\0\0\0\0\0\0h\0\0\0\x1\0\0\0\0\0\0\0\xe0\0\0\0\x1\0\0\0\0\0\0\x3\xe8\0\0\0\0h) -UILayout\geometry="@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\0\xe3\0\0\0\x99\0\0\x4r\0\0\x2\x31\0\0\0\xe4\0\0\0\xad\0\0\x4q\0\0\x2,\0\0\0\0\0\0\0\0\x5V\0\0\0\xe4\0\0\0\xad\0\0\x4q\0\0\x2,)" -UILayout\geometryRenderWindow=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\0\0\0\0\0\x14\0\0\0\x63\0\0\0\x31\0\0\0\0\0\0\0\x14\0\0\0\x63\0\0\0\x31\0\0\0\0\0\0\0\0\x5V\0\0\0\0\0\0\0\x14\0\0\0\x63\0\0\0\x31) -UILayout\microProfileDialogGeometry=@ByteArray(\x1\xd9\xd0\xcb\0\x3\0\0\0\0\0\0\0\0\0\x14\0\0\x3\xe7\0\0\x2k\0\0\0\0\0\0\0\x14\0\0\x3\xe7\0\0\x2k\0\0\0\0\0\0\0\0\x5V\0\0\0\0\0\0\0\x14\0\0\x3\xe7\0\0\x2k) -UILayout\microProfileDialogVisible=false -UILayout\microProfileDialogVisible\default=true -UILayout\state=@ByteArray(\0\0\0\xff\0\0\0\0\xfd\0\0\0\x2\0\0\0\0\0\0\0\0\0\0\0\0\xfc\x2\0\0\0\x1\xfb\0\0\0\x1c\0W\0\x61\0i\0t\0T\0r\0\x65\0\x65\0W\0i\0\x64\0g\0\x65\0t\0\0\0\0\0\xff\xff\xff\xff\0\0\0V\0\xff\xff\xff\0\0\0\x1\0\0\0\0\0\0\0\0\xfc\x2\0\0\0\b\xfb\0\0\0\x18\0\x41\0R\0M\0R\0\x65\0g\0i\0s\0t\0\x65\0r\0s\0\0\0\0\0\xff\xff\xff\xff\0\0\0h\0\xff\xff\xff\xfb\0\0\0 \0G\0r\0\x61\0p\0h\0i\0\x63\0s\0\x44\0\x65\0\x62\0u\0g\0g\0\x65\0r\0\0\0\0\0\xff\xff\xff\xff\0\0\0V\0\xff\xff\xff\xfb\0\0\0\"\0P\0i\0\x63\0\x61\0 \0\x43\0o\0m\0m\0\x61\0n\0\x64\0 \0L\0i\0s\0t\0\0\0\0\0\xff\xff\xff\xff\0\0\0\x8d\0\xff\xff\xff\xfb\0\0\0*\0P\0i\0\x63\0\x61\0\x42\0r\0\x65\0\x61\0k\0P\0o\0i\0n\0t\0s\0W\0i\0\x64\0g\0\x65\0t\0\0\0\0\0\xff\xff\xff\xff\0\0\0\x8d\0\xff\xff\xff\xfb\0\0\0 \0P\0i\0\x63\0\x61\0V\0\x65\0r\0t\0\x65\0x\0S\0h\0\x61\0\x64\0\x65\0r\0\0\0\0\0\xff\xff\xff\xff\0\0\x1\xb8\0\xff\xff\xff\xfb\0\0\0\x12\0\x43\0i\0T\0r\0\x61\0\x63\0i\0n\0g\0\0\0\0\0\xff\xff\xff\xff\0\0\0G\0\xff\xff\xff\xfb\0\0\0.\0L\0L\0\x45\0S\0\x65\0r\0v\0i\0\x63\0\x65\0M\0o\0\x64\0u\0l\0\x65\0s\0W\0i\0\x64\0g\0\x65\0t\0\0\0\0\0\xff\xff\xff\xff\0\0\0V\0\xff\xff\xff\xfb\0\0\0\x16\0I\0P\0\x43\0R\0\x65\0\x63\0o\0r\0\x64\0\x65\0r\0\0\0\0\0\xff\xff\xff\xff\0\0\0\xcb\0\xff\xff\xff\0\0\x3\x8e\0\0\x1J\0\0\0\x4\0\0\0\x4\0\0\0\b\0\0\0\b\xfc\0\0\0\0) -Updater\check_for_update_on_start=true -Updater\check_for_update_on_start\default=true -Updater\update_on_close=false -Updater\update_on_close\default=true -calloutFlags=1 -calloutFlags\default=false -confirmClose=true -confirmClose\default=true -displayTitleBars=true -displayTitleBars\default=true -enable_discord_presence=false -enable_discord_presence\default=false -firstStart=false -firstStart\default=false -fullscreen=false -fullscreen\default=true -hideInactiveMouse=false -hideInactiveMouse\default=true -pauseWhenInBackground=false -pauseWhenInBackground\default=true -screenshot_resolution_factor=0 -screenshot_resolution_factor\default=true -showConsole=false -showConsole\default=true -showFilterBar=true -showFilterBar\default=true -showStatusBar=true -showStatusBar\default=true -singleWindowMode=true -singleWindowMode\default=true -theme=colorful_dark -theme\default=false - -[Utility] -custom_textures=false -custom_textures\default=true -dump_textures=false -dump_textures\default=true -preload_textures=false -preload_textures\default=true - -[VideoDumping] -audio_bitrate=64000 -audio_bitrate\default=true -audio_encoder=libvorbis -audio_encoder\default=true -audio_encoder_options= -audio_encoder_options\default=true -format_options= -output_format=webm -output_format\default=true -video_bitrate=2500000 -video_bitrate\default=true -video_encoder=libvpx-vp9 -video_encoder\default=true -video_encoder_options="quality:realtime,speed:6,tile-columns:4,frame-parallel:1,threads:8,row-mt:1" -video_encoder_options\default=true - -[WebService] -citra_token= -citra_username= -enable_telemetry=false -enable_telemetry\default=false -web_api_url=https://api.citra-emu.org -web_api_url\default=true diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 8600c14..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: '2' -services: - citra: - build: - context: . - dockerfile: docker/citra.dockerfile - shm_size: 2G - environment: - OPENBOX_ARGS: '--startup "citra-qt ${TEST_FILE:?}"' - RESOLUTION: 1366x768 - ports: - - "6080:80" - - "5900:5900" - volumes: - - "./citra:/tmp/citra" - - "${TEST_FILE:?}:${TEST_FILE:?}:ro" - - driver: - build: - context: . - dockerfile: docker/driver.dockerfile - volumes: - - "./driver:/tmp/driver" - working_dir: /tmp/driver/out - command: ["/usr/libexec/vncdo.sh", "/tmp/driver/main.vdo"] - diff --git a/docker/citra.dockerfile b/docker/citra.dockerfile deleted file mode 100644 index 6597747..0000000 --- a/docker/citra.dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM dorowu/ubuntu-desktop-lxde-vnc - -RUN apt-get update -y && \ - apt-get install -y \ - libqt5gui5 \ - libqt5multimedia5 - - -ARG CITRA_RELEASE=nightly-1763 -ARG CITRA_RELEASE_FILE=citra-linux-20220503-856b3d6.tar.xz - -WORKDIR /tmp -RUN wget https://github.com/citra-emu/citra-nightly/releases/download/${CITRA_RELEASE}/${CITRA_RELEASE_FILE} -RUN mkdir -p citra && \ - tar --strip-components 1 -C citra -xvf ${CITRA_RELEASE_FILE} && \ - cp citra/citra-qt citra/citra /usr/local/bin - -COPY citra/qt-config.ini /root/.config/citra-emu/ - -ENV OPENBOX_ARGS='--startup "citra-qt /root/hello-world.3dsx"' diff --git a/docker/download_citra.sh b/docker/download_citra.sh new file mode 100755 index 0000000..ec6699c --- /dev/null +++ b/docker/download_citra.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -euxo pipefail + +CITRA_CHANNEL=$1 +CITRA_RELEASE=$2 + +RELEASE_API="https://api.github.com/repos/citra-emu/citra-${CITRA_CHANNEL}/releases/tags/${CITRA_CHANNEL}-${CITRA_RELEASE}" + +curl "${RELEASE_API}" | + jq --raw-output '.assets[].browser_download_url' | + 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/docker/driver.dockerfile b/docker/driver.dockerfile deleted file mode 100644 index a993ace..0000000 --- a/docker/driver.dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM ghcr.io/ian-h-chamberlain/rust-devkitarm - -RUN apt-get update -y && \ - apt-get install -y python3 python3-pip python3-pil - -RUN pip3 install vncdotool - -COPY driver/vncdo.sh /usr/libexec/ - -CMD [ "vncdo", "--version" ] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100755 index 0000000..a64e91d --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,50 @@ +#!/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/docker/sdl2-config.ini b/docker/sdl2-config.ini new file mode 100644 index 0000000..3aeb006 --- /dev/null +++ b/docker/sdl2-config.ini @@ -0,0 +1,12 @@ +[Miscellaneous] +log_filter = *:Info Debug.Emulated:Debug + +[Debugging] +use_gdbstub=true +gdbstub_port=4000 + +[WebService] +enable_telemetry = 0 + +[Video Dumping] +output_format = webm diff --git a/docker/test-runner.gdb b/docker/test-runner.gdb new file mode 100644 index 0000000..1b7e00c --- /dev/null +++ b/docker/test-runner.gdb @@ -0,0 +1,13 @@ +# https://github.com/devkitPro/libctru/blob/master/libctru/source/system/stack_adjust.s#LL28C23-L28C23 +# or should this be `_exit` ? +break __ctru_exit +commands + # 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 + +target extended-remote :4000 +continue +quit diff --git a/driver/citra-controls.png b/driver/citra-controls.png deleted file mode 100644 index 13cf6d9..0000000 Binary files a/driver/citra-controls.png and /dev/null differ diff --git a/driver/citra-hotkeys.png b/driver/citra-hotkeys.png deleted file mode 100644 index 3bc5574..0000000 Binary files a/driver/citra-hotkeys.png and /dev/null differ diff --git a/driver/main.vdo b/driver/main.vdo deleted file mode 100644 index 40060d7..0000000 --- a/driver/main.vdo +++ /dev/null @@ -1,16 +0,0 @@ -# Wait for citra to finish startup -sleep 3 - -# Main test logic -key ctrl-p type "/tmp/citra/out/screenshot0.png" key enter -keydown a keyup a -keydown s keyup s -keydown z keyup z -keydown q - keydown a keyup a - keydown s keyup s - keydown z keyup z -keyup q -key ctrl-p type "/tmp/citra/out/screenshot1.png" key enter - -capture final.png diff --git a/driver/vncdo.sh b/driver/vncdo.sh deleted file mode 100755 index b02c161..0000000 --- a/driver/vncdo.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -while ! vncdo -s citra pause 0 &>/dev/null; do - echo "waiting for VNC server..." - sleep 1 -done - -exec vncdo -s citra --nocursor --delay 400 "$@" diff --git a/run.sh b/run.sh deleted file mode 100755 index 712eabc..0000000 --- a/run.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -set -eux -o pipefail - -if [[ $# -lt 1 ]]; then - echo "Usage: run.sh 3DSX_FILE" - exit 1 -fi - -trap 'docker-compose down' EXIT - -rm -rf citra/out driver/out -mkdir -p citra/out driver/out - -TEST_FILE=$(realpath "$1") -export TEST_FILE - -docker-compose build -docker-compose up -d citra -docker-compose run driver diff --git a/src/console.rs b/src/console.rs new file mode 100644 index 0000000..5e61951 --- /dev/null +++ b/src/console.rs @@ -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) { + // 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; + } + } + } +} diff --git a/src/gdb.rs b/src/gdb.rs new file mode 100644 index 0000000..d8a7c46 --- /dev/null +++ b/src/gdb.rs @@ -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) { + // 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); + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3f9597c --- /dev/null +++ b/src/lib.rs @@ -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::(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::(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::(tests) +} + +fn run(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); +} + +/// 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); + } +} diff --git a/src/socket.rs b/src/socket.rs new file mode 100644 index 0000000..dab54d1 --- /dev/null +++ b/src/socket.rs @@ -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) {} +} diff --git a/tests/integration.rs b/tests/integration.rs new file mode 100644 index 0000000..1cd14ac --- /dev/null +++ b/tests/integration.rs @@ -0,0 +1,13 @@ +#![feature(custom_test_frameworks)] +#![test_runner(test_runner::run_gdb)] + +#[test] +fn it_works() { + assert_eq!(2 + 2, 4); +} + +#[test] +#[should_panic] +fn it_panics() { + assert_eq!(2 + 2, 5); +}