| 1 | import 'dart:async'; |
| 2 | import 'dart:io'; |
| 3 | import 'dart:isolate'; |
| 4 | |
| 5 | import 'package:flutter_test/flutter_test.dart'; |
| 6 | import 'package:gba_core/gba_core.dart'; |
| 7 | import 'package:splashy_gba/src/emulator/emulator_worker.dart'; |
| 8 | |
| 9 | /// Freely licensed test ROMs (jsmolka/gba-tests, MIT), fetched by |
| 10 | /// `dart run tool/fetch_test_roms.dart` in packages/gba_core. |
| 11 | const String roms = 'packages/gba_core/test/roms/jsmolka' |
| 12 | |
| 13 | /// Runs the emulation isolate the way the app does, without the UI. |
| 14 | void main() { |
| 15 | late Directory root; |
| 16 | |
| 17 | setUp(() => root = Directory |
| 18 | tearDown(() => root.deleteSync |
| 19 | |
| 20 | Future<void> run |
| 21 | String rom, |
| 22 | String savePath, |
| 23 | Future<void> Function |
| 24 | ) async { |
| 25 | final port = ReceivePort() |
| 26 | final messages = port.asBroadcastStream |
| 27 | await Isolate.spawn( |
| 28 | emulatorWorkerMain, |
| 29 | WorkerConfig(toUi: port.sendPort, |
| 30 | ); |
| 31 | final ready = await messages. |
| 32 | (m) => m is ReadyMessage || |
| 33 | ); |
| 34 | expect(ready, isA< |
| 35 | await body((ready as ReadyMessage |
| 36 | port.close(); |
| 37 | } |
| 38 | |
| 39 | test('frames arrive in real time and show the game', () async { |
| 40 | await run('$roms |
| 41 | commands, |
| 42 | messages, |
| 43 | ) async { |
| 44 | final frames = <FrameMessage |
| 45 | final subscription = messages.listen |
| 46 | if (m is FrameMessage) frames. |
| 47 | }); |
| 48 | final clock = Stopwatch() |
| 49 | await Future<void> |
| 50 | commands.send((keys: GbaKey |
| 51 | await subscription.cancel(); |
| 52 | clock.stop(); |
| 53 | |
| 54 | // About 60 frames a second, not as fast as possible. |
| 55 | final perSecond = frames.length / |
| 56 | expect(perSecond, inInclusiveRange( |
| 57 | |
| 58 | // The last frame is the ROM's result screen: mostly white, with |
| 59 | // black text. |
| 60 | final pixels = frames.last. |
| 61 | expect(pixels.length, Ppu |
| 62 | final white = pixels.where |
| 63 | final black = pixels.where |
| 64 | expect(white, greaterThan(pixels |
| 65 | expect(black, greaterThan( |
| 66 | |
| 67 | commands.send(WorkerCommand.stop) |
| 68 | await messages.firstWhere((m) => m |
| 69 | }); |
| 70 | }); |
| 71 | |
| 72 | test('the save is written when the game stops', () async { |
| 73 | final savePath = '${root |
| 74 | await run('$roms |
| 75 | await Future<void> |
| 76 | commands.send(WorkerCommand.stop) |
| 77 | await messages.firstWhere((m) => m |
| 78 | }); |
| 79 | final save = File(savePath) |
| 80 | expect(save.existsSync(), |
| 81 | expect(save.lengthSync(), |
| 82 | // The test ROM wrote 0xDD, 0xCC... at the start of its test blocks. |
| 83 | expect(save.readAsBytesSync(). |
| 84 | }); |
| 85 | |
| 86 | test('pausing stops frames; resuming restarts them', () async { |
| 87 | await run('$roms |
| 88 | commands, |
| 89 | messages, |
| 90 | ) async { |
| 91 | var count = 0; |
| 92 | final subscription = messages.listen |
| 93 | if (m is FrameMessage) count++ |
| 94 | }); |
| 95 | await Future<void> |
| 96 | commands.send(WorkerCommand.pause) |
| 97 | await Future<void> |
| 98 | final paused = count; |
| 99 | await Future<void> |
| 100 | expect(count, paused); |
| 101 | commands.send(WorkerCommand.resume) |
| 102 | await Future<void> |
| 103 | expect(count, greaterThan(paused |
| 104 | await subscription.cancel(); |
| 105 | commands.send(WorkerCommand.stop) |
| 106 | await messages.firstWhere((m) => m |
| 107 | }); |
| 108 | }); |
| 109 | |
| 110 | test('a file that is not a ROM is reported, not run', () async { |
| 111 | final notRom = File( |
| 112 | final port = ReceivePort() |
| 113 | await Isolate.spawn( |
| 114 | emulatorWorkerMain, |
| 115 | WorkerConfig( |
| 116 | toUi: port.sendPort, |
| 117 | romPath: notRom.path, |
| 118 | savePath: '${root.path |
| 119 | ), |
| 120 | ); |
| 121 | final first = await port. |
| 122 | expect(first, isA< |
| 123 | expect((first as ErrorMessage) |
| 124 | }); |
| 125 | } |