import 'dart:async'; import 'dart:io'; import 'dart:isolate'; import 'package:flutter_test/flutter_test.dart'; import 'package:gba_core/gba_core.dart'; import 'package:splashy_gba/src/emulator/emulator_worker.dart'; /// Freely licensed test ROMs (jsmolka/gba-tests, MIT), fetched by /// `dart run tool/fetch_test_roms.dart` in packages/gba_core. const String roms = 'packages/gba_core/test/roms/jsmolka'; /// Runs the emulation isolate the way the app does, without the UI. void main() { late Directory root; setUp(() => root = Directory.systemTemp.createTempSync('splashy_worker')); tearDown(() => root.deleteSync(recursive: true)); Future run( String rom, String savePath, Future Function(SendPort commands, Stream messages) body, ) async { final port = ReceivePort(); final messages = port.asBroadcastStream(); await Isolate.spawn( emulatorWorkerMain, WorkerConfig(toUi: port.sendPort, romPath: rom, savePath: savePath), ); final ready = await messages.firstWhere( (m) => m is ReadyMessage || m is ErrorMessage, ); expect(ready, isA(), reason: '$ready'); await body((ready as ReadyMessage).commands, messages); port.close(); } test('frames arrive in real time and show the game', () async { await run('$roms/arm.gba', '${root.path}/arm.sav', ( commands, messages, ) async { final frames = []; final subscription = messages.listen((m) { if (m is FrameMessage) frames.add(m); }); final clock = Stopwatch()..start(); await Future.delayed(const Duration(seconds: 2)); commands.send((keys: GbaKey.a)); await subscription.cancel(); clock.stop(); // About 60 frames a second, not as fast as possible. final perSecond = frames.length / (clock.elapsedMilliseconds / 1000); expect(perSecond, inInclusiveRange(40, 62)); // The last frame is the ROM's result screen: mostly white, with // black text. final pixels = frames.last.pixels.materialize().asUint32List(); expect(pixels.length, Ppu.width * Ppu.height); final white = pixels.where((p) => p == 0xFFFFFFFF).length; final black = pixels.where((p) => p == 0xFF000000).length; expect(white, greaterThan(pixels.length * 0.9)); expect(black, greaterThan(100)); commands.send(WorkerCommand.stop); await messages.firstWhere((m) => m == stoppedMessage); }); }); test('the save is written when the game stops', () async { final savePath = '${root.path}/saves/sram.sav'; await run('$roms/save/sram.gba', savePath, (commands, messages) async { await Future.delayed(const Duration(milliseconds: 500)); commands.send(WorkerCommand.stop); await messages.firstWhere((m) => m == stoppedMessage); }); final save = File(savePath); expect(save.existsSync(), isTrue); expect(save.lengthSync(), Sram.size); // The test ROM wrote 0xDD, 0xCC... at the start of its test blocks. expect(save.readAsBytesSync().any((b) => b != 0xFF), isTrue); }); test('pausing stops frames; resuming restarts them', () async { await run('$roms/arm.gba', '${root.path}/arm.sav', ( commands, messages, ) async { var count = 0; final subscription = messages.listen((m) { if (m is FrameMessage) count++; }); await Future.delayed(const Duration(milliseconds: 300)); commands.send(WorkerCommand.pause); await Future.delayed(const Duration(milliseconds: 100)); final paused = count; await Future.delayed(const Duration(milliseconds: 500)); expect(count, paused); commands.send(WorkerCommand.resume); await Future.delayed(const Duration(milliseconds: 300)); expect(count, greaterThan(paused + 5)); await subscription.cancel(); commands.send(WorkerCommand.stop); await messages.firstWhere((m) => m == stoppedMessage); }); }); test('a file that is not a ROM is reported, not run', () async { final notRom = File('${root.path}/x.gba')..writeAsBytesSync([1, 2, 3]); final port = ReceivePort(); await Isolate.spawn( emulatorWorkerMain, WorkerConfig( toUi: port.sendPort, romPath: notRom.path, savePath: '${root.path}/x.sav', ), ); final first = await port.first; expect(first, isA()); expect((first as ErrorMessage).error, contains('too small')); }); }