gameboy-rust/crates/desktop/src/cartridge/save_file/tests.rs

201 lines
7 KiB
Rust

//! File tests use generated bytes in private temporary directories, never user saves.
use super::*;
use gba_core::cartridge::{FLASH_PROGRAM_CYCLES, SAVE_START};
struct Fixture {
dir: PathBuf,
rom: PathBuf,
save: PathBuf,
}
impl Fixture {
fn new() -> Self {
static NEXT: AtomicU64 = AtomicU64::new(0);
let dir = std::env::temp_dir().join(format!(
"gba-save-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
));
fs::create_dir(&dir).unwrap();
let rom = dir.join("original.gba");
fs::write(&rom, [0xfe, 0xff, 0xff, 0xea]).unwrap();
let save = dir.join("original.sav");
Self { dir, rom, save }
}
fn open(&self) -> io::Result<SaveFile> {
SaveFile::open(&self.save, &self.rom, SaveDevice::Flash128)
}
fn image(&self, value: u8) -> Vec<u8> {
let bytes = vec![value; SaveDevice::Flash128.capacity()];
fs::write(&self.save, &bytes).unwrap();
bytes
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.dir);
}
}
fn memory() -> Memory {
let mut m = Memory::new(vec![]).unwrap();
m.set_save_device(SaveDevice::Flash128);
m
}
fn start_program(m: &mut Memory, value: u8) {
for (offset, byte) in [(0x5555, 0xaa), (0x2aaa, 0x55), (0x5555, 0xa0), (0, value)] {
m.write8(SAVE_START + offset, byte).unwrap();
}
}
fn program(m: &mut Memory, value: u8) {
start_program(m, value);
m.advance_cycles(FLASH_PROGRAM_CYCLES);
}
#[test]
fn fresh_image_is_volatile_until_modified_then_reloads_with_unique_backups() {
let f = Fixture::new();
let mut m = memory();
let mut save = f.open().unwrap();
assert!(!save.loaded());
save.initialize(&mut m).unwrap();
assert!(save.persist(&m).unwrap().is_none());
assert!(!f.save.exists());
program(&mut m, 0xa5);
assert!(save.persist(&m).unwrap().unwrap().backup.is_none());
assert_eq!(fs::read(&f.save).unwrap(), m.save_image().unwrap());
assert!(save.persist(&m).unwrap().is_none());
drop(save);
assert!(!sibling(&f.save, ".lock").exists());
let mut save = f.open().unwrap();
assert!(save.loaded());
let mut m = memory();
save.initialize(&mut m).unwrap();
assert!(!m.save_modified());
assert_eq!(m.read8(SAVE_START).unwrap(), 0xa5);
program(&mut m, 0xa0);
let first = save.persist(&m).unwrap().unwrap().backup.unwrap();
let original = fs::read(&first).unwrap();
assert_eq!(original[0], 0xa5);
program(&mut m, 0x80);
let second = save.persist(&m).unwrap().unwrap().backup.unwrap();
assert_ne!(first, second);
assert_eq!(fs::read(&first).unwrap(), original);
assert_eq!(fs::read(second).unwrap()[0], 0xa0);
assert_eq!(fs::read(&f.save).unwrap()[0], 0x80);
assert!(!fs::read_dir(&f.dir).unwrap().any(|p| p
.unwrap()
.file_name()
.to_string_lossy()
.contains(".tmp.")));
assert_eq!(fs::read(&f.rom).unwrap(), [0xfe, 0xff, 0xff, 0xea]);
}
#[test]
fn incomplete_commands_and_busy_operations_preserve_the_existing_image() {
let f = Fixture::new();
let original = f.image(255);
let mut save = f.open().unwrap();
let mut m = memory();
save.initialize(&mut m).unwrap();
m.write8(SAVE_START + 0x5555, 0xaa).unwrap();
assert!(save.persist(&m).is_err());
assert_eq!(fs::read(&f.save).unwrap(), original);
m.load_save_image(&original).unwrap();
start_program(&mut m, 0x12);
assert!(save.persist(&m).is_err());
assert_eq!(fs::read(&f.save).unwrap(), original);
m.advance_cycles(FLASH_PROGRAM_CYCLES);
assert!(save.persist(&m).unwrap().is_some());
}
#[test]
fn lock_excludes_other_sessions_and_does_not_delete_replaced_locks() {
let f = Fixture::new();
let first = f.open().unwrap();
assert!(f.open().is_err());
let lock = sibling(&f.save, ".lock");
fs::rename(&lock, f.dir.join("owned-lock")).unwrap();
fs::write(&lock, b"another owner").unwrap();
drop(first);
assert_eq!(fs::read(&lock).unwrap(), b"another owner");
assert!(f.open().is_err());
fs::remove_file(&lock).unwrap();
drop(f.open().unwrap());
assert!(!lock.exists());
}
#[test]
fn size_and_device_errors_leave_no_lock_and_no_changed_bytes() {
let f = Fixture::new();
for length in [0, 1, 65536, 131071, 131073] {
let bytes = vec![0x5a; length];
fs::write(&f.save, &bytes).unwrap();
assert!(f.open().is_err());
assert_eq!(fs::read(&f.save).unwrap(), bytes);
assert!(!sibling(&f.save, ".lock").exists());
}
assert!(SaveFile::open(&f.save, &f.rom, SaveDevice::None).is_err());
let bytes = vec![255; 65536];
fs::write(&f.save, &bytes).unwrap();
let save = SaveFile::open(&f.save, &f.rom, SaveDevice::Flash64).unwrap();
let mut m = Memory::new(vec![]).unwrap();
m.set_save_device(SaveDevice::Flash64);
save.initialize(&mut m).unwrap();
assert_eq!(m.save_image().unwrap(), bytes);
}
#[test]
fn external_changes_and_new_files_are_never_overwritten() {
for existing in [false, true] {
let f = Fixture::new();
if existing {
f.image(255);
}
let mut save = f.open().unwrap();
let mut m = memory();
save.initialize(&mut m).unwrap();
program(&mut m, 0x12);
let external = f.image(0x5a);
assert!(save
.persist(&m)
.unwrap_err()
.to_string()
.contains("changed externally"));
assert_eq!(fs::read(&f.save).unwrap(), external);
assert_eq!(fs::read_dir(&f.dir).unwrap().count(), 3); // ROM, image, owned lock only.
}
}
#[cfg(unix)]
#[test]
fn reject_rom_aliases_symlinks_directories_and_readonly_images() {
use std::os::unix::fs::{symlink, PermissionsExt};
let f = Fixture::new();
assert!(SaveFile::open(&f.rom, &f.rom, SaveDevice::Flash128).is_err());
fs::hard_link(&f.rom, &f.save).unwrap();
assert!(f.open().is_err());
fs::remove_file(&f.save).unwrap();
let target = f.dir.join("target");
fs::write(&target, vec![255; 131072]).unwrap();
symlink(&target, &f.save).unwrap();
assert!(f.open().is_err());
fs::remove_file(&f.save).unwrap();
symlink(f.dir.join("absent"), &f.save).unwrap();
assert!(f.open().is_err());
fs::remove_file(&f.save).unwrap();
fs::create_dir(&f.save).unwrap();
assert!(f.open().is_err());
fs::remove_dir(&f.save).unwrap();
let image = f.image(255);
fs::set_permissions(&f.save, fs::Permissions::from_mode(0o400)).unwrap();
assert!(f.open().is_err());
assert_eq!(fs::read(&f.save).unwrap(), image);
fs::set_permissions(&f.save, fs::Permissions::from_mode(0o600)).unwrap();
let mut save = f.open().unwrap();
let mut m = memory();
save.initialize(&mut m).unwrap();
program(&mut m, 0x12);
fs::set_permissions(&f.save, fs::Permissions::from_mode(0o400)).unwrap();
assert!(save.persist(&m).is_err());
assert_eq!(fs::read(&f.save).unwrap(), image);
fs::set_permissions(&f.save, fs::Permissions::from_mode(0o600)).unwrap();
}