diff --git a/.github/workflows/ci-builds.yml b/.github/workflows/ci-builds.yml index 271ef03..7cd33d2 100644 --- a/.github/workflows/ci-builds.yml +++ b/.github/workflows/ci-builds.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v2 - name: Install Apt Dependencies - run: sudo apt-get update && sudo apt-get install binutils-arm-none-eabi + run: sudo apt-get update && sudo apt-get install binutils-arm-none-eabi libelf-dev - uses: actions-rs/toolchain@v1 with: @@ -39,3 +39,19 @@ jobs: toolchain: ${{ matrix.rust.toolchain }} command: check args: --no-default-features + + - name: Install mgba-test-runner + uses: actions-rs/cargo@v1 + with: + toolchain: ${{ matrix.rust.toolchain }} + command: install + # newer revisions don't build on aarch64, at least, because of a c_char mishap + args: --git https://github.com/agbrs/agb --rev a7f9fdf01118a7a77d4dcf72f2b74a1961458b36 mgba-test-runner + + - name: Run unit tests + uses: actions-rs/cargo@v1 + env: + CARGO_TARGET_THUMBV4T_NONE_EABI_RUNNER: mgba-test-runner + with: + toolchain: ${{ matrix.rust.toolchain }} + command: test diff --git a/src/fixed.rs b/src/fixed.rs index 3c8aa2a..c4b3ccb 100644 --- a/src/fixed.rs +++ b/src/fixed.rs @@ -241,6 +241,24 @@ macro_rules! impl_common_fixed_ops { Self(self.0 >> rhs) } } + + impl core::fmt::Debug for Fixed<$t, B> { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + let raw: $t = self.to_bits(); + write!( + f, + concat!( + "Fixed::<", + stringify!($t), + ",{}>::from_bits({:#x}_u32 as ", + stringify!($t), + ")" + ), + B, raw + ) + } + } + impl_trait_op_unit!($t, Not, not); impl_trait_op_self_rhs!($t, Add, add); impl_trait_op_self_rhs!($t, Sub, sub); @@ -335,18 +353,13 @@ macro_rules! impl_signed_fixed_ops { } } impl_trait_op_unit!($t, Neg, neg); - impl core::fmt::Debug for Fixed<$t, B> { - #[inline] + + impl core::fmt::Display for Fixed<$t, B> { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - let whole: $t = self.trunc().to_bits() >> B; - let fract: $t = self.fract().to_bits(); - let divisor: $t = 1 << B; - if self.is_negative() { - let whole = whole.unsigned_abs(); - write!(f, "-({whole}+{fract}/{divisor})") - } else { - write!(f, "{whole}+{fract}/{divisor}") + if self.to_bits() < 0 { + f.write_str("-")?; } + fixed_fmt_abs::(f, self.to_bits().abs() as u32) } } }; @@ -393,13 +406,10 @@ macro_rules! impl_unsigned_fixed_ops { Self(self.0 & (<$t>::MAX << B)) } } - impl core::fmt::Debug for Fixed<$t, B> { - #[inline] + + impl core::fmt::Display for Fixed<$t, B> { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - let whole: $t = self.trunc().to_bits() >> B; - let fract: $t = self.fract().to_bits(); - let divisor: $t = 1 << B; - write!(f, "{whole}+{fract}/{divisor}") + fixed_fmt_abs::(f, self.to_bits() as u32) } } }; @@ -407,3 +417,78 @@ macro_rules! impl_unsigned_fixed_ops { impl_unsigned_fixed_ops!(u8); impl_unsigned_fixed_ops!(u16); impl_unsigned_fixed_ops!(u32); + +fn fixed_fmt_abs( + f: &mut core::fmt::Formatter, abs: u32, +) -> core::fmt::Result { + let width = f.width().unwrap_or(0); + let precision = f.precision().unwrap_or(const { ((B as usize) + 1) / 3 }); + let fract = abs & ((1 << B) - 1); + let fract_dec = 10u32 + .checked_pow(precision as u32) + .and_then(|digits| fract.checked_mul(digits)) + .map(|x| (x >> B) as u64) + .unwrap_or_else(|| (fract as u64 * 10u64.pow(precision as u32) >> B)); + write!(f, "{:width$}.{:0precision$}", abs >> B, fract_dec) +} + +#[cfg(test)] +mod test { + use crate::fixed::{i16fx14, i32fx8}; + use core::{fmt::Write, str}; + + struct WriteBuf([u8; N], usize); + impl<'a, const N: usize> Default for WriteBuf { + fn default() -> Self { + Self([0u8; N], 0) + } + } + impl Write for WriteBuf { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + let src = s.as_bytes(); + let len = (self.0.len() - self.1).min(src.len()); + self.0[self.1..self.1 + len].copy_from_slice(&src[..len]); + self.1 += len; + if len < src.len() { + Err(core::fmt::Error) + } else { + Ok(()) + } + } + } + impl WriteBuf { + fn take(&mut self) -> &str { + let len = self.1; + self.1 = 0; + str::from_utf8(&self.0[..len]).unwrap() + } + } + + #[test_case] + fn decimal_display() { + let mut wbuf = WriteBuf::<16>::default(); + + let x = i32fx8::from_bits(0x12345678); + + write!(&mut wbuf, "{x}").unwrap(); + assert_eq!(wbuf.take(), "1193046.468"); + + write!(&mut wbuf, "{x:9.1}").unwrap(); + assert_eq!(wbuf.take(), " 1193046.4"); + + write!(&mut wbuf, "{x:1.6}").unwrap(); + assert_eq!(wbuf.take(), "1193046.468750"); + + let x = x.neg(); + write!(&mut wbuf, "{x}").unwrap(); + assert_eq!(wbuf.take(), "-1193046.468"); + + let x = i16fx14::from_bits(0x6544 as i16); + write!(&mut wbuf, "{x}").unwrap(); + assert_eq!(wbuf.take(), "1.58227"); + + let x = x.neg(); + write!(&mut wbuf, "{x:.10}").unwrap(); + assert_eq!(wbuf.take(), "-1.5822753906"); + } +} diff --git a/src/lib.rs b/src/lib.rs index e57641f..4263d00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,10 @@ #![allow(clippy::let_and_return)] #![allow(clippy::result_unit_err)] #![warn(clippy::missing_inline_in_public_items)] +#![cfg_attr(test, feature(custom_test_frameworks))] +#![cfg_attr(test, test_runner(test_harness::test_runner))] +#![cfg_attr(test, no_main)] +#![cfg_attr(test, reexport_test_harness_main = "test_main")] //! A crate for GBA development. //! @@ -174,3 +178,130 @@ macro_rules! include_aligned_bytes { Align4(*include_bytes!($file)) }}; } + +#[cfg(test)] +mod test_harness { + use crate::prelude::*; + use crate::{bios, mem, mgba}; + use core::fmt::Write; + + #[panic_handler] + fn panic(info: &core::panic::PanicInfo) -> ! { + DISPSTAT.write(DisplayStatus::new().with_irq_vblank(true)); + BG_PALETTE.index(0).write(Color::from_rgb(25, 10, 5)); + IE.write(IrqBits::VBLANK); + IME.write(true); + VBlankIntrWait(); + VBlankIntrWait(); + VBlankIntrWait(); + + // the Fatal one kills emulation after one line / 256 bytes + // so emit all the information as Error first + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Error) + { + writeln!(log, "[failed]").ok(); + write!(log, "{}", info).ok(); + } + + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Fatal) + { + if let Some(loc) = info.location() { + write!(log, "panic at {loc}! see mgba error log for details.").ok(); + } else { + write!(log, "panic! see mgba error log for details.").ok(); + } + } + + IE.write(IrqBits::new()); + bios::IntrWait(true, IrqBits::new()); + loop {} + } + + pub(crate) trait UnitTest { + fn run(&self); + } + + impl UnitTest for T { + fn run(&self) { + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) + { + write!(log, "{}...", core::any::type_name::()).ok(); + } + + self(); + + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) + { + writeln!(log, "[ok]").ok(); + } + } + } + + pub(crate) fn test_runner(tests: &[&dyn UnitTest]) { + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) + { + write!(log, "Running {} tests", tests.len()).ok(); + } + + for test in tests { + test.run(); + } + if let Ok(mut log) = + mgba::MgbaBufferedLogger::try_new(mgba::MgbaMessageLevel::Info) + { + write!(log, "Tests finished successfully").ok(); + } + } + + #[no_mangle] + extern "C" fn main() { + DISPCNT.write(DisplayControl::new().with_video_mode(VideoMode::_0)); + BG_PALETTE.index(0).write(Color::new()); + + crate::test_main(); + + BG_PALETTE.index(0).write(Color::from_rgb(5, 15, 25)); + BG_PALETTE.index(1).write(Color::new()); + BG0CNT + .write(BackgroundControl::new().with_charblock(0).with_screenblock(31)); + DISPCNT.write( + DisplayControl::new().with_video_mode(VideoMode::_0).with_show_bg0(true), + ); + + // some niceties for people without mgba-test-runner + let tsb = TEXT_SCREENBLOCKS.get_frame(31).unwrap(); + unsafe { + mem::set_u32x80_unchecked( + tsb.into_block::<1024>().as_mut_ptr().cast(), + 0, + 12, + ); + } + Cga8x8Thick.bitunpack_4bpp(CHARBLOCK0_4BPP.as_region(), 0); + + let row = tsb.get_row(9).unwrap().iter().skip(6); + for (addr, ch) in row.zip(b"all tests passed!") { + addr.write(TextEntry::new().with_tile(*ch as u16)); + } + + DISPSTAT.write(DisplayStatus::new()); + bios::IntrWait(true, IrqBits::new()); + } +} + +#[cfg(test)] +mod test { + use super::Align4; + + #[test_case] + fn align4_as_u16_u32_slice() { + let a = Align4([0u8, 1u8, 2u8, 3u8]); + assert_eq!(a.as_u16_slice(), &[0x100_u16.to_le(), 0x302_u16.to_le()]); + assert_eq!(a.as_u32_slice(), &[0x3020100_u32.to_le()]); + } +} diff --git a/src/mem.rs b/src/mem.rs index 0587234..7bfca54 100644 --- a/src/mem.rs +++ b/src/mem.rs @@ -1,3 +1,5 @@ +#![cfg_attr(not(feature = "on_gba"), allow(unused_variables))] + use crate::macros::on_gba_or_unimplemented; /// Copies `u8` at a time between exclusive regions.