diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml deleted file mode 100644 index a86d6f6..0000000 --- a/.github/actions/setup/action.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Setup -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 - uses: actions-rs/cargo@v1 - with: - command: install - # TODO: this should probably just be a released version from crates.io - # once cargo-3ds gets published somewhere... - args: >- - --git https://github.com/rust3ds/cargo-3ds - --rev 78a652fdfb01e2614a792d1a56b10c980ee1dae9 - - - 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 index ee97ccc..69a3978 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,19 +9,13 @@ on: - master workflow_dispatch: -env: - # https://blog.rust-lang.org/2022/06/22/sparse-registry-testing.html - CARGO_UNSTABLE_SPARSE_REGISTRY: "true" - # actions-rust-lang/setup-rust-toolchain sets some default RUSTFLAGS - RUSTFLAGS: "" - jobs: lint: strategy: matrix: toolchain: - # Run against a "known good" nightly - - nightly-2023-05-31 + # Run against a "known good" nightly. Rustc version is 1 day behind the toolchain date + - nightly-2023-06-01 # Check for breakage on latest nightly - nightly @@ -33,11 +27,14 @@ jobs: - name: Checkout branch uses: actions/checkout@v2 - - uses: ./.github/actions/setup + - uses: rust3ds/test-runner/setup@v1 with: toolchain: ${{ matrix.toolchain }} - - name: Hide duplicate warnings from lint job + # https://github.com/actions/runner/issues/504 + # Removing the matchers won't keep the job from failing if there are errors, + # but will at least declutter pull request annotations (especially for warnings). + - name: Hide duplicate annotations from nightly if: ${{ matrix.toolchain == 'nightly' }} run: | echo "::remove-matcher owner=clippy::" @@ -46,18 +43,17 @@ jobs: - name: Check formatting run: cargo fmt --all --verbose -- --check - - name: Cargo check - run: cargo 3ds clippy --color=always --verbose --all-targets - # --deny=warnings would be nice, but can easily break CI for new clippy - # lints getting added. I'd also like to use Github's "inline warnings" - # feature, but https://github.com/actions/runner/issues/2341 means we - # can't have both that *and* colored output. + - name: Cargo check ctru-sys (without tests) + run: cargo 3ds clippy --package ctru-sys --color=always --verbose + + - name: Cargo check ctru-rs (including tests) + run: cargo 3ds clippy --package ctru-rs --color=always --verbose --all-targets - doctests: + test: strategy: matrix: toolchain: - - nightly-2023-05-31 + - nightly-2023-06-01 - nightly continue-on-error: ${{ matrix.toolchain == 'nightly' }} runs-on: ubuntu-latest @@ -66,15 +62,36 @@ jobs: - name: Checkout branch uses: actions/checkout@v2 - - uses: ./.github/actions/setup + - uses: rust3ds/test-runner/setup@v1 with: toolchain: ${{ matrix.toolchain }} - name: Hide duplicated warnings from lint job run: echo "::remove-matcher owner=clippy::" - - name: Build doc tests - run: cargo 3ds test --doc --verbose + # This needs to be done separately from running the tests to ensure the + # lib tests' .3dsx is built before the test is run (for romfs). We don't + # really have a good way to build the 3dsx in between the build + test, + # unless cargo-3ds actually runs them as separate commands. See + # https://github.com/rust3ds/cargo-3ds/issues/44 for more details + - name: Build lib and integration tests + run: cargo 3ds test --no-run --tests --package ctru-rs + + - name: Run lib and integration tests + uses: rust3ds/test-runner/run-tests@v1 + with: + args: --tests --package ctru-rs - # TODO: it would be nice to actually build 3dsx for examples/tests, etc. - # and run it somehow, but exactly how remains to be seen. + - name: Build and run doc tests + uses: rust3ds/test-runner/run-tests@v1 + with: + args: --doc --package ctru-rs + + - name: Upload citra logs and capture videos + uses: actions/upload-artifact@v3 + if: success() || failure() # always run unless the workflow was cancelled + with: + name: citra-logs-${{ matrix.toolchain }} + path: | + target/armv6k-nintendo-3ds/debug/deps/*.txt + target/armv6k-nintendo-3ds/debug/deps/*.webm diff --git a/Cargo.toml b/Cargo.toml index 889c913..d88e137 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,7 @@ default-members = ["ctru-rs", "ctru-sys"] resolver = "2" [patch.'https://github.com/rust3ds/ctru-rs'] -# Make sure all dependencies use the local ctru-sys package +# Make sure all dependencies use the local packages. This is needed for things +# like pthread-3ds that rely on ctru-sys, and test-runner which relies on ctru-rs +ctru-rs = { path = "ctru-rs" } ctru-sys = { path = "ctru-sys" } diff --git a/ctru-rs/Cargo.toml b/ctru-rs/Cargo.toml index 0523b27..ac10586 100644 --- a/ctru-rs/Cargo.toml +++ b/ctru-rs/Cargo.toml @@ -29,13 +29,14 @@ widestring = "0.2.2" toml = "0.5" [dev-dependencies] +bytemuck = "1.12.3" +cfg-if = "1.0.0" ferris-says = "0.2.1" futures = "0.3" +lewton = "0.10.2" +test-runner = { git = "https://github.com/rust3ds/test-runner.git" } time = "0.3.7" tokio = { version = "1.16", features = ["rt", "time", "sync", "macros"] } -cfg-if = "1.0.0" -bytemuck = "1.12.3" -lewton = "0.10.2" [features] default = ["romfs", "big-stack"] diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 8861ace..69d4329 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -7,7 +7,8 @@ use bitflags::bitflags; use ctru_sys::{ - self, swkbdInit, swkbdInputText, swkbdSetButton, swkbdSetFeatures, swkbdSetHintText, SwkbdState, + self, swkbdInit, swkbdInputText, swkbdSetButton, swkbdSetFeatures, swkbdSetHintText, + swkbdSetInitialText, SwkbdState, }; use libc; use std::fmt::Display; @@ -160,7 +161,8 @@ impl SoftwareKeyboard { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # /// use ctru::applets::swkbd::{SoftwareKeyboard, Kind}; @@ -191,7 +193,8 @@ impl SoftwareKeyboard { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -235,7 +238,8 @@ impl SoftwareKeyboard { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -266,7 +270,8 @@ impl SoftwareKeyboard { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # /// use ctru::applets::swkbd::{SoftwareKeyboard, Features}; @@ -286,7 +291,8 @@ impl SoftwareKeyboard { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # /// use ctru::applets::swkbd::{SoftwareKeyboard, ValidInput, Filters}; @@ -309,7 +315,8 @@ impl SoftwareKeyboard { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # /// use ctru::applets::swkbd::{SoftwareKeyboard, ValidInput, Filters}; @@ -330,13 +337,38 @@ impl SoftwareKeyboard { self.state.max_digits = digits; } + /// Set the initial text for this software keyboard. + /// + /// The initial text is the text already written when you open the software keyboard. + /// + /// # Example + /// + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// # fn main() { + /// # + /// use ctru::applets::swkbd::SoftwareKeyboard; + /// let mut keyboard = SoftwareKeyboard::default(); + /// + /// keyboard.set_initial_text("Write here what you like!"); + /// # + /// # } + #[doc(alias = "swkbdSetInitialText")] + pub fn set_initial_text(&mut self, text: &str) { + unsafe { + let nul_terminated: String = text.chars().chain(once('\0')).collect(); + swkbdSetInitialText(self.state.as_mut(), nul_terminated.as_ptr()); + } + } + /// Set the hint text for this software keyboard. /// /// The hint text is the text shown in gray before any text gets written in the input box. /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # /// use ctru::applets::swkbd::SoftwareKeyboard; @@ -363,7 +395,8 @@ impl SoftwareKeyboard { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # /// use ctru::applets::swkbd::{SoftwareKeyboard, Button, Kind}; @@ -402,7 +435,8 @@ impl SoftwareKeyboard { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # fn main() { /// # /// use ctru::applets::swkbd::{SoftwareKeyboard, Button, Kind}; diff --git a/ctru-rs/src/console.rs b/ctru-rs/src/console.rs index 697b088..f88e11b 100644 --- a/ctru-rs/src/console.rs +++ b/ctru-rs/src/console.rs @@ -82,12 +82,13 @@ impl<'screen> Console<'screen> { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # - /// use ctru::services::gfx::Gfx; /// use ctru::console::Console; + /// use ctru::services::gfx::Gfx; /// /// // Initialize graphics (using framebuffers allocated on the HEAP). /// let gfx = Gfx::new()?; @@ -121,7 +122,8 @@ impl<'screen> Console<'screen> { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -158,7 +160,8 @@ impl<'screen> Console<'screen> { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # diff --git a/ctru-rs/src/error.rs b/ctru-rs/src/error.rs index 41b09f8..0dd7cc5 100644 --- a/ctru-rs/src/error.rs +++ b/ctru-rs/src/error.rs @@ -21,13 +21,14 @@ pub type Result = ::std::result::Result; /// /// # Example /// -/// ```no_run +/// ``` /// use ctru::error::{Result, ResultCode}; /// -/// pub fn hid_init() -> Result<()> { +/// pub fn main() -> Result<()> { +/// # let _runner = test_runner::GdbRunner::default(); /// // We run an unsafe function which returns a `ctru_sys::Result`. /// let result: ctru_sys::Result = unsafe { ctru_sys::hidInit() }; -/// +/// /// // The result code is parsed and any possible error gets returned by the function. /// ResultCode(result)?; /// Ok(()) @@ -152,6 +153,8 @@ impl fmt::Debug for Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { + // TODO: should we consider using ctru_sys::osStrError here as well? + // It might do some of the work for us or provide additional details &Self::Os(err) => write!( f, "libctru result code 0x{err:08X}: [{} {}] {}: {}", diff --git a/ctru-rs/src/lib.rs b/ctru-rs/src/lib.rs index 329d063..a57cc00 100644 --- a/ctru-rs/src/lib.rs +++ b/ctru-rs/src/lib.rs @@ -18,11 +18,10 @@ #![crate_type = "rlib"] #![crate_name = "ctru"] #![warn(missing_docs)] -#![feature(test)] #![feature(custom_test_frameworks)] #![feature(try_trait_v2)] #![feature(allocator_api)] -#![test_runner(test_runner::run)] +#![test_runner(test_runner::run_gdb)] // TODO: does this make sense to have configurable? #![doc( html_favicon_url = "https://user-images.githubusercontent.com/11131775/225929072-2fa1741c-93ae-4b47-9bdf-af70f3d59910.png" )] @@ -40,7 +39,11 @@ extern crate shim_3ds; /// /// This value was chosen to support crate dependencies which expected more stack than provided. It's suggested to use less stack if possible. #[no_mangle] -#[cfg(feature = "big-stack")] +// When building lib tests, we don't want to redefine the same symbol twice, +// since ctru-rs is both the crate under test and a dev-dependency (non-test). +// We might also be able to use #[linkage] for similar effect, but this way +// works without depending on another unstable feature. +#[cfg(all(feature = "big-stack", not(test)))] static __stacksize__: usize = 2 * 1024 * 1024; // 2MB macro_rules! from_impl { @@ -116,10 +119,8 @@ pub mod console; pub mod error; pub mod linear; pub mod mii; +pub mod os; pub mod prelude; pub mod services; -#[cfg(test)] -mod test_runner; - pub use crate::error::{Error, Result}; diff --git a/ctru-rs/src/os.rs b/ctru-rs/src/os.rs new file mode 100644 index 0000000..baf654c --- /dev/null +++ b/ctru-rs/src/os.rs @@ -0,0 +1,154 @@ +//! Utilities to get information about the operating system and hardware state. + +/// System version information. This struct is used for both kernel and firmware versions. +/// +/// # Example +/// ``` +/// # let _runner = test_runner::GdbRunner::default(); +/// let firm_version = ctru::os::firm_version(); +/// assert_ne!(firm_version.major(), 0); +/// +/// let kernel_version = ctru::os::kernel_version(); +/// assert_ne!(kernel_version.major(), 0); +/// ``` +#[derive(Clone, Copy)] +pub struct Version(u32); + +impl Version { + /// Pack a system version from its components + pub fn new(major: u8, minor: u8, revision: u8) -> Self { + let major = u32::from(major); + let minor = u32::from(minor); + let revision = u32::from(revision); + + Self(major << 24 | minor << 16 | revision << 8) + } + + /// Get the major version from a packed system version. + pub fn major(&self) -> u8 { + (self.0 >> 24).try_into().unwrap() + } + + /// Get the minor version from a packed system version. + pub fn minor(&self) -> u8 { + (self.0 >> 16 & 0xFF).try_into().unwrap() + } + + /// Get the revision from a packed system version. + pub fn revision(&self) -> u8 { + (self.0 >> 8 & 0xFF).try_into().unwrap() + } +} + +/// Get the system's FIRM version. +pub fn firm_version() -> Version { + Version(unsafe { ctru_sys::osGetFirmVersion() }) +} + +/// Get the system's kernel version. +pub fn kernel_version() -> Version { + Version(unsafe { ctru_sys::osGetKernelVersion() }) +} + +// TODO: I can't seem to find good documentation on it, but we could probably +// define enums for firmware type (NATIVE_FIRM, SAFE_FIRM etc.) as well as +// application memory layout. Leaving those as future enhancements for now + +/// A region of memory. Most applications will only use [`Application`](MemRegion::Application) +/// memory, but the other types can be used to query memory usage information. +/// See +/// for more details on the different types of memory. +/// +/// # Example +/// ``` +/// # let _runner = test_runner::GdbRunner::default(); +/// let all_memory = ctru::os::MemRegion::All; +/// +/// assert!(all_memory.size() > 0); +/// assert!(all_memory.used() > 0); +/// assert!(all_memory.free() > 0); +/// ``` +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +#[repr(u32)] +pub enum MemRegion { + /// All memory regions. + All = ctru_sys::MEMREGION_ALL, + /// APPLICATION memory. + Application = ctru_sys::MEMREGION_APPLICATION, + /// SYSTEM memory. + System = ctru_sys::MEMREGION_SYSTEM, + /// BASE memory. + Base = ctru_sys::MEMREGION_BASE, +} + +impl MemRegion { + /// Get the total size of this memory region, in bytes. + pub fn size(&self) -> usize { + unsafe { ctru_sys::osGetMemRegionSize(*self as u32) } + .try_into() + .unwrap() + } + + /// Get the number of bytes used within this memory region. + pub fn used(&self) -> usize { + unsafe { ctru_sys::osGetMemRegionUsed(*self as u32) } + .try_into() + .unwrap() + } + + /// Get the number of bytes free within this memory region. + pub fn free(&self) -> usize { + unsafe { ctru_sys::osGetMemRegionFree(*self as u32) } + .try_into() + .unwrap() + } +} + +/// WiFi signal strength. This enum's `u8` representation corresponds with +/// the number of bars displayed in the Home menu. +/// +/// # Example +/// +/// ``` +/// let _runner = test_runner::GdbRunner::default(); +/// let strength = ctru::os::WifiStrength::current(); +/// assert!((strength as u8) < 4); +/// ``` +#[derive(Clone, Copy, Debug)] +#[non_exhaustive] +#[repr(u8)] +pub enum WifiStrength { + /// This may indicate a very poor signal quality even worse than `Bad`, + /// or that no network is connected at all. + Disconnected = 0, + /// Poor signal strength. + Bad = 1, + /// Medium signal strength. + Decent = 2, + /// Good signal strength. + Good = 3, +} + +impl WifiStrength { + /// Get the current WiFi signal strength. + pub fn current() -> Self { + match unsafe { ctru_sys::osGetWifiStrength() } { + 0 => Self::Disconnected, + 1 => Self::Bad, + 2 => Self::Decent, + 3 => Self::Good, + other => panic!("Got unexpected WiFi strength value {other}"), + } + } +} + +/// Get the current value of the stereoscopic 3D slider on a scale from 0.0­–­1.0. +pub fn current_3d_slider_state() -> f32 { + unsafe { ctru_sys::osGet3DSliderState() } +} + +/// Whether or not a headset is currently plugged into the device. +pub fn is_headset_connected() -> bool { + unsafe { ctru_sys::osIsHeadsetConnected() } +} diff --git a/ctru-rs/src/services/am.rs b/ctru-rs/src/services/am.rs index 68b3af3..cab13b3 100644 --- a/ctru-rs/src/services/am.rs +++ b/ctru-rs/src/services/am.rs @@ -61,7 +61,8 @@ impl Am { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -84,11 +85,13 @@ impl Am { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # - /// use ctru::services::{fs::FsMediaType, am::Am}; + /// use ctru::services::am::Am; + /// use ctru::services::fs::FsMediaType; /// let app_manager = Am::new()?; /// /// // Number of titles installed on the Nand storage. @@ -113,11 +116,13 @@ impl Am { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # - /// use ctru::services::{fs::FsMediaType, am::Am}; + /// use ctru::services::am::Am; + /// use ctru::services::fs::FsMediaType; /// let app_manager = Am::new()?; /// /// // Number of apps installed on the SD card storage diff --git a/ctru-rs/src/services/apt.rs b/ctru-rs/src/services/apt.rs index b30be85..89ad255 100644 --- a/ctru-rs/src/services/apt.rs +++ b/ctru-rs/src/services/apt.rs @@ -16,7 +16,8 @@ impl Apt { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -44,7 +45,8 @@ impl Apt { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// use std::error::Error; /// use ctru::services::apt::Apt; /// diff --git a/ctru-rs/src/services/cam.rs b/ctru-rs/src/services/cam.rs index 991db74..3dd52ef 100644 --- a/ctru-rs/src/services/cam.rs +++ b/ctru-rs/src/services/cam.rs @@ -561,7 +561,8 @@ pub trait Camera: private::ConfigurableCamera { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -595,7 +596,8 @@ pub trait Camera: private::ConfigurableCamera { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -981,7 +983,8 @@ pub trait Camera: private::ConfigurableCamera { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # use std::time::Duration; /// # fn main() -> Result<(), Box> { @@ -1103,7 +1106,8 @@ impl Cam { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -1158,7 +1162,8 @@ impl Cam { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # diff --git a/ctru-rs/src/services/cfgu.rs b/ctru-rs/src/services/cfgu.rs index b1cbe76..981f8e6 100644 --- a/ctru-rs/src/services/cfgu.rs +++ b/ctru-rs/src/services/cfgu.rs @@ -84,7 +84,8 @@ impl Cfgu { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -105,7 +106,8 @@ impl Cfgu { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -129,7 +131,8 @@ impl Cfgu { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -153,7 +156,8 @@ impl Cfgu { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -177,7 +181,8 @@ impl Cfgu { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -205,7 +210,8 @@ impl Cfgu { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # diff --git a/ctru-rs/src/services/fs.rs b/ctru-rs/src/services/fs.rs index 821b8b9..0be5425 100644 --- a/ctru-rs/src/services/fs.rs +++ b/ctru-rs/src/services/fs.rs @@ -136,7 +136,8 @@ pub struct Fs(()); /// /// # Examples /// -/// ```no_run +/// ``` +/// # let _runner = test_runner::GdbRunner::default(); /// use ctru::services::fs::Fs; /// /// let mut fs = Fs::new().unwrap(); @@ -158,12 +159,14 @@ pub struct Archive { /// /// Create a new file and write bytes to it: /// -/// ```no_run +/// ``` +/// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # /// use std::io::prelude::*; -/// use ctru::services::fs::{Fs, File}; +/// +/// use ctru::services::fs::{File, Fs}; /// /// let mut fs = Fs::new()?; /// let mut sdmc = fs.sdmc()?; @@ -174,12 +177,14 @@ pub struct Archive { /// /// Read the contents of a file into a `String`:: /// -/// ```no_run +/// ``` +/// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # /// use std::io::prelude::*; -/// use ctru::services::fs::{Fs, File}; +/// +/// use ctru::services::fs::{File, Fs}; /// /// let mut fs = Fs::new()?; /// let mut sdmc = fs.sdmc()?; @@ -196,13 +201,15 @@ pub struct Archive { /// It can be more efficient to read the contents of a file with a buffered /// `Read`er. This can be accomplished with `BufReader`: /// -/// ```no_run +/// ``` +/// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # -/// use std::io::BufReader; /// use std::io::prelude::*; -/// use ctru::services::fs::{Fs, File}; +/// use std::io::BufReader; +/// +/// use ctru::services::fs::{File, Fs}; /// /// let mut fs = Fs::new()?; /// let mut sdmc = fs.sdmc()?; @@ -247,22 +254,25 @@ pub struct Metadata { /// /// Opening a file to read: /// -/// ```no_run +/// ``` +/// # let _runner = test_runner::GdbRunner::default(); /// use ctru::services::fs::{Fs, OpenOptions}; /// /// let mut fs = Fs::new().unwrap(); /// let mut sdmc_archive = fs.sdmc().unwrap(); -/// let file = OpenOptions::new() +/// let result = OpenOptions::new() /// .read(true) /// .archive(&sdmc_archive) -/// .open("foo.txt") -/// .unwrap(); +/// .open("foo.txt"); +/// +/// assert!(result.is_err()); /// ``` /// /// Opening a file for both reading and writing, as well as creating it if it /// doesn't exist: /// -/// ```no_run +/// ``` +/// # let _runner = test_runner::GdbRunner::default(); /// use ctru::services::fs::{Fs, OpenOptions}; /// /// let mut fs = Fs::new().unwrap(); @@ -272,7 +282,7 @@ pub struct Metadata { /// .write(true) /// .create(true) /// .archive(&sdmc_archive) -/// .open("foo.txt") +/// .open("/foo.txt") /// .unwrap(); /// ``` #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -380,12 +390,14 @@ impl File { /// /// # Examples /// - /// ```no_run - /// use ctru::services::fs::{Fs, File}; + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// use ctru::services::fs::{File, Fs}; /// /// let mut fs = Fs::new().unwrap(); /// let mut sdmc_archive = fs.sdmc().unwrap(); - /// let mut f = File::open(&sdmc_archive, "/foo.txt").unwrap(); + /// // Non-existent file: + /// assert!(File::open(&sdmc_archive, "/foo.txt").is_err()); /// ``` pub fn open>(arch: &Archive, path: P) -> IoResult { OpenOptions::new() @@ -407,8 +419,9 @@ impl File { /// /// # Examples /// - /// ```no_run - /// use ctru::services::fs::{Fs, File}; + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); + /// use ctru::services::fs::{File, Fs}; /// /// let mut fs = Fs::new().unwrap(); /// let mut sdmc_archive = fs.sdmc().unwrap(); diff --git a/ctru-rs/src/services/gfx.rs b/ctru-rs/src/services/gfx.rs index dc1dca6..25830a6 100644 --- a/ctru-rs/src/services/gfx.rs +++ b/ctru-rs/src/services/gfx.rs @@ -260,7 +260,8 @@ impl Gfx { /// The new `Gfx` instance will allocate the needed framebuffers in the CPU-GPU shared memory region (to ensure compatibiltiy with all possible uses of the `Gfx` service). /// As such, it's the same as calling: /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -277,7 +278,8 @@ impl Gfx { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -299,11 +301,13 @@ impl Gfx { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # - /// use ctru::services::{gfx::Gfx, gspgpu::FramebufferFormat}; + /// use ctru::services::gfx::Gfx; + /// use ctru::services::gspgpu::FramebufferFormat; /// /// // Top screen uses RGBA8, bottom screen uses RGB565. /// // The screen buffers are allocated in the standard HEAP memory, and not in VRAM. @@ -391,18 +395,20 @@ impl Gfx { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # - /// use ctru::services::{apt::Apt, gfx::Gfx}; + /// use ctru::services::apt::Apt; + /// use ctru::services::gfx::Gfx; /// let apt = Apt::new()?; /// let gfx = Gfx::new()?; /// /// // Simple main loop. /// while apt.main_loop() { /// // Main program logic - /// + /// /// // Wait for the screens to refresh. /// // This blocks the current thread to make it run at 60Hz. /// gfx.wait_for_vblank(); @@ -434,7 +440,8 @@ impl TopScreen3D<'_> { /// /// # Example /// -/// ```no_run +/// ``` +/// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -557,7 +564,10 @@ mod tests { #[test] fn gfx_duplicate() { - // We don't need to build a `Gfx` because the test runner has one already + // NOTE: this is expected to fail if using the console test runner, since + // that necessarily creates a Gfx as part of its test setup: + let _gfx = Gfx::new().unwrap(); + assert!(matches!(Gfx::new(), Err(Error::ServiceAlreadyActive))); } } diff --git a/ctru-rs/src/services/hid.rs b/ctru-rs/src/services/hid.rs index 7f6abc7..4997158 100644 --- a/ctru-rs/src/services/hid.rs +++ b/ctru-rs/src/services/hid.rs @@ -106,7 +106,8 @@ impl Hid { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -148,7 +149,8 @@ impl Hid { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -170,7 +172,8 @@ impl Hid { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -199,7 +202,8 @@ impl Hid { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -228,7 +232,8 @@ impl Hid { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -260,7 +265,8 @@ impl Hid { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -293,7 +299,8 @@ impl Hid { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # diff --git a/ctru-rs/src/services/ndsp/mod.rs b/ctru-rs/src/services/ndsp/mod.rs index 78c744b..4176f70 100644 --- a/ctru-rs/src/services/ndsp/mod.rs +++ b/ctru-rs/src/services/ndsp/mod.rs @@ -3,8 +3,17 @@ //! The NDSP service is used to handle communications to the DSP processor present on the console's motherboard. //! Thanks to the DSP processor the program can play sound effects and music on the console's built-in speakers or to any audio device //! connected via the audio jack. +//! +//! To use NDSP audio, you will need to dump DSP firmware from a real 3DS using +//! something like [DSP1](https://www.gamebrew.org/wiki/DSP1_3DS). +//! +//! `libctru` expects to find it at `sdmc:/3ds/dspfirm.cdc` when initializing the NDSP service. #![doc(alias = "audio")] +// As a result of requiring DSP firmware to initialize, all of the doctests in +// this module are `no_run`, since Citra doesn't provide a stub for the DSP firmware: +// https://github.com/citra-emu/citra/issues/6111 + pub mod wave; use wave::{Status, Wave}; @@ -124,7 +133,8 @@ impl Ndsp { /// # Errors /// /// This function will return an error if an instance of the [`Ndsp`] struct already exists - /// or if there are any issues during initialization. + /// or if there are any issues during initialization (for example, DSP firmware + /// cannot be found. See [module documentation](super::ndsp) for more details.). /// /// # Example /// @@ -508,14 +518,15 @@ impl Channel<'_> { /// # fn main() -> Result<(), Box> { /// # /// # use ctru::linear::LinearAllocator; - /// use ctru::services::ndsp::{AudioFormat, Ndsp, wave::Wave}; + /// use ctru::services::ndsp::wave::Wave; + /// use ctru::services::ndsp::{AudioFormat, Ndsp}; /// let ndsp = Ndsp::new()?; /// let mut channel_0 = ndsp.channel(0)?; /// - /// # let _audio_data = Box::new_in([0u8; 96], LinearAllocator); + /// # let audio_data = Box::new_in([0u8; 96], LinearAllocator); /// /// // Provide your own audio data. - /// let mut wave = Wave::new(_audio_data, AudioFormat::PCM16Stereo, false); + /// let mut wave = Wave::new(audio_data, AudioFormat::PCM16Stereo, false); /// /// // Clear the audio queue and stop playback. /// channel_0.queue_wave(&mut wave); diff --git a/ctru-rs/src/services/ndsp/wave.rs b/ctru-rs/src/services/ndsp/wave.rs index 7f8c9f9..5389ebd 100644 --- a/ctru-rs/src/services/ndsp/wave.rs +++ b/ctru-rs/src/services/ndsp/wave.rs @@ -36,9 +36,10 @@ impl Wave { /// /// # Example /// - /// ```no_run + /// ``` /// # #![feature(allocator_api)] /// # fn main() { + /// # let _runner = test_runner::GdbRunner::default(); /// # /// use ctru::linear::LinearAllocator; /// use ctru::services::ndsp::{AudioFormat, wave::Wave}; @@ -110,9 +111,10 @@ impl Wave { /// /// # Example /// - /// ```no_run + /// ``` /// # #![feature(allocator_api)] /// # fn main() { + /// # let _runner = test_runner::GdbRunner::default(); /// # /// # use ctru::linear::LinearAllocator; /// # let _audio_data = Box::new_in([0u8; 96], LinearAllocator); diff --git a/ctru-rs/src/services/ps.rs b/ctru-rs/src/services/ps.rs index fc20a78..27f21a2 100644 --- a/ctru-rs/src/services/ps.rs +++ b/ctru-rs/src/services/ps.rs @@ -63,7 +63,8 @@ impl Ps { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -86,7 +87,8 @@ impl Ps { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -110,7 +112,8 @@ impl Ps { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -134,7 +137,8 @@ impl Ps { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # diff --git a/ctru-rs/src/services/romfs.rs b/ctru-rs/src/services/romfs.rs index 029525c..a311e9c 100644 --- a/ctru-rs/src/services/romfs.rs +++ b/ctru-rs/src/services/romfs.rs @@ -45,7 +45,8 @@ impl RomFS { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -82,11 +83,13 @@ impl RomFS { mod tests { use super::*; + // NOTE: this test only passes when run with a .3dsx, which for now requires separate build + // and run steps so the 3dsx is built before the runner looks for the executable #[test] #[should_panic] fn romfs_lock() { let romfs = RomFS::new().unwrap(); - + ROMFS_ACTIVE.try_lock().unwrap(); drop(romfs); diff --git a/ctru-rs/src/services/soc.rs b/ctru-rs/src/services/soc.rs index 5e61484..dd7b601 100644 --- a/ctru-rs/src/services/soc.rs +++ b/ctru-rs/src/services/soc.rs @@ -30,7 +30,8 @@ impl Soc { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -56,7 +57,8 @@ impl Soc { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -96,7 +98,8 @@ impl Soc { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # @@ -129,7 +132,8 @@ impl Soc { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # diff --git a/ctru-rs/src/services/sslc.rs b/ctru-rs/src/services/sslc.rs index af8b65d..81674ad 100644 --- a/ctru-rs/src/services/sslc.rs +++ b/ctru-rs/src/services/sslc.rs @@ -12,7 +12,8 @@ impl SslC { /// /// # Example /// - /// ```no_run + /// ``` + /// # let _runner = test_runner::GdbRunner::default(); /// # use std::error::Error; /// # fn main() -> Result<(), Box> { /// # diff --git a/ctru-rs/src/test_runner.rs b/ctru-rs/src/test_runner.rs deleted file mode 100644 index 97aa5f4..0000000 --- a/ctru-rs/src/test_runner.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! Custom test runner for building/running unit tests on the 3DS. - -extern crate test; - -use std::io; - -use test::{ColorConfig, OutputFormat, TestDescAndFn, TestFn, TestOpts}; - -use crate::prelude::*; - -/// A custom runner to be used with `#[test_runner]`. This simple implementation -/// runs all tests in series, "failing" on the first one to panic (really, the -/// panic is just treated the same as any normal application panic). -pub(crate) fn run(tests: &[&TestDescAndFn]) { - let gfx = Gfx::new().unwrap(); - let mut hid = Hid::new().unwrap(); - let apt = Apt::new().unwrap(); - - let mut top_screen = gfx.top_screen.borrow_mut(); - top_screen.set_wide_mode(true); - let _console = Console::new(top_screen); - - 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::AutoColor, - format: OutputFormat::Pretty, - // 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() - }; - // Use the default test implementation with our hardcoded options - let _success = run_static_tests(&opts, tests).unwrap(); - - // Make sure the user can actually see the results before we exit - println!("Press START to exit."); - - while apt.main_loop() { - gfx.wait_for_vblank(); - - hid.scan_input(); - if hid.keys_down().contains(KeyPad::START) { - break; - } - } -} - -/// Adapted from [`test::test_main_static`] and [`test::make_owned_test`]. -fn run_static_tests(opts: &TestOpts, tests: &[&TestDescAndFn]) -> io::Result { - let tests = tests.iter().map(make_owned_test).collect(); - test::run_tests_console(opts, tests) -} - -/// 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 { - match test.testfn { - TestFn::StaticTestFn(f) => TestDescAndFn { - testfn: TestFn::StaticTestFn(f), - desc: test.desc.clone(), - }, - TestFn::StaticBenchFn(f) => TestDescAndFn { - testfn: TestFn::StaticBenchFn(f), - desc: test.desc.clone(), - }, - _ => panic!("non-static tests passed to test::test_main_static"), - } -} diff --git a/ctru-sys/build.rs b/ctru-sys/build.rs index fa34560..3ad5851 100644 --- a/ctru-sys/build.rs +++ b/ctru-sys/build.rs @@ -171,7 +171,8 @@ fn check_libctru_version() -> Result<(String, String, String), Box> { if lib_version != crate_built_version { Err(format!( "libctru version is {lib_version} but this crate was built for {crate_built_version}" - ))?; + ) + .into()); } let Output { stdout, .. } = Command::new(pacman) diff --git a/ctru-sys/src/lib.rs b/ctru-sys/src/lib.rs index 8801af3..32252e6 100644 --- a/ctru-sys/src/lib.rs +++ b/ctru-sys/src/lib.rs @@ -14,3 +14,11 @@ include!(concat!(env!("OUT_DIR"), "/bindings.rs")); pub unsafe fn errno() -> s32 { *__errno() } + +// TODO: not sure if there's a better way to do this, but I have gotten myself +// with this a couple times so having the hint seems nice to have. +#[cfg(test)] +compile_error!(concat!( + "ctru-sys doesn't have tests and its lib test will fail to build at link time. ", + "Try specifying `--package ctru-rs` to build those tests.", +));