Initial flame_lua_runtime package
This commit is contained in:
401
test/runtime/resources/game_resource_manager_test.dart
Normal file
401
test/runtime/resources/game_resource_manager_test.dart
Normal file
@@ -0,0 +1,401 @@
|
||||
import 'dart:async' as async;
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flame_lua_runtime/runtime/diagnostics/runtime_diagnostics.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
||||
import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('GameResourceManager', () {
|
||||
test(
|
||||
'advances generation and clears cache when mounting package',
|
||||
() async {
|
||||
final resources = GameResourceManager();
|
||||
final first = await _createPackage('first');
|
||||
final second = await _createPackage('second');
|
||||
|
||||
expect(resources.generation, 0);
|
||||
|
||||
await resources.mount(first);
|
||||
expect(resources.generation, 1);
|
||||
|
||||
await resources.mount(second);
|
||||
expect(resources.generation, 2);
|
||||
},
|
||||
);
|
||||
|
||||
test('resolves resource keys from active package', () async {
|
||||
final resources = GameResourceManager();
|
||||
final package = await _createPackage('resources');
|
||||
|
||||
await resources.mount(package);
|
||||
|
||||
expect(resources.resolve('tile'), endsWith('/assets/tile.png'));
|
||||
});
|
||||
|
||||
test(
|
||||
'lazy image load records failed state, error and diagnostics',
|
||||
() async {
|
||||
final diagnostics = RuntimeDiagnostics();
|
||||
final resources = GameResourceManager(diagnostics: diagnostics);
|
||||
final package = await _createPackage('lazy_failed');
|
||||
|
||||
await resources.mount(package);
|
||||
expect(resources.imageState('tile'), GameResourceState.idle);
|
||||
|
||||
final image = await resources.loadImage('tile');
|
||||
|
||||
expect(image, isNull);
|
||||
expect(resources.imageState('tile'), GameResourceState.failed);
|
||||
expect(resources.imageError('tile'), isNotNull);
|
||||
expect(diagnostics.entries, hasLength(1));
|
||||
expect(
|
||||
diagnostics.entries.single.type,
|
||||
RuntimeDiagnosticType.resourceLoadError,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('exports image debug json and evicts failed records', () async {
|
||||
final resources = GameResourceManager();
|
||||
final package = await _createPackage('debug_json');
|
||||
|
||||
await resources.mount(package);
|
||||
|
||||
expect(resources.imagesDebugJson(), {
|
||||
'generation': 1,
|
||||
'hasPackage': true,
|
||||
'count': 1,
|
||||
'activeLoads': 0,
|
||||
'pendingLoads': 0,
|
||||
'resources': [
|
||||
{
|
||||
'key': 'tile',
|
||||
'path': endsWith('/assets/tile.png'),
|
||||
'type': GameResourceType.image,
|
||||
'declared': true,
|
||||
'preload': GameResourcePreload.lazy,
|
||||
'state': 'idle',
|
||||
'loading': false,
|
||||
'ready': false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await resources.loadImage('tile');
|
||||
final failedJson = resources.imagesDebugJson();
|
||||
final failedResources = failedJson['resources'] as List<Object?>;
|
||||
final failedTile = failedResources.single as Map<String, Object?>;
|
||||
|
||||
expect(failedTile['state'], 'failed');
|
||||
expect(failedTile['error'], isA<String>());
|
||||
|
||||
expect(resources.evictImage('tile'), isTrue);
|
||||
expect(resources.imageState('tile'), GameResourceState.idle);
|
||||
expect(resources.evictImage('tile'), isFalse);
|
||||
});
|
||||
|
||||
test('preloads and evicts image resource groups', () async {
|
||||
final resources = GameResourceManager();
|
||||
final package = await _createMultiImagePackage('image_group');
|
||||
|
||||
await resources.mount(package);
|
||||
await resources.preloadGroup('board');
|
||||
|
||||
expect(resources.imageState('board'), GameResourceState.ready);
|
||||
expect(resources.imageState('piece'), GameResourceState.ready);
|
||||
expect(resources.imageState('avatar'), GameResourceState.idle);
|
||||
expect(resources.evictGroup('board'), 2);
|
||||
expect(resources.imageState('board'), GameResourceState.idle);
|
||||
expect(resources.imageState('piece'), GameResourceState.idle);
|
||||
});
|
||||
|
||||
test('image LRU evicts least recently used unretained images', () async {
|
||||
final resources = GameResourceManager(maxCacheEntries: 1);
|
||||
final package = await _createMultiImagePackage('image_lru');
|
||||
|
||||
await resources.mount(package);
|
||||
await resources.loadImage('board');
|
||||
expect(resources.imageState('board'), GameResourceState.ready);
|
||||
|
||||
await resources.loadImage('avatar');
|
||||
|
||||
expect(resources.imageState('board'), GameResourceState.idle);
|
||||
expect(resources.imageState('avatar'), GameResourceState.ready);
|
||||
});
|
||||
|
||||
test('image LRU keeps retained images until released', () async {
|
||||
final resources = GameResourceManager(maxCacheEntries: 1);
|
||||
final package = await _createMultiImagePackage('image_lru_retained');
|
||||
|
||||
await resources.mount(package);
|
||||
await resources.loadImage('board', retain: true);
|
||||
await resources.loadImage('avatar');
|
||||
|
||||
expect(resources.imageState('board'), GameResourceState.ready);
|
||||
expect(resources.imageState('avatar'), GameResourceState.idle);
|
||||
|
||||
resources.releaseImage('board');
|
||||
await resources.loadImage('avatar');
|
||||
|
||||
expect(resources.imageState('board'), GameResourceState.idle);
|
||||
expect(resources.imageState('avatar'), GameResourceState.ready);
|
||||
});
|
||||
|
||||
test('deduplicates concurrent image load requests', () async {
|
||||
final resources = GameResourceManager();
|
||||
final package = await _createPackage('dedupe');
|
||||
final countingPackage = _CountingPackage(package);
|
||||
|
||||
await resources.mount(countingPackage);
|
||||
|
||||
final first = resources.loadImage('tile');
|
||||
final second = resources.loadImage('tile');
|
||||
|
||||
expect(countingPackage.readCount, 1);
|
||||
countingPackage.releaseReads();
|
||||
|
||||
await Future.wait([first, second]);
|
||||
|
||||
expect(countingPackage.readCount, 1);
|
||||
expect(resources.imageState('tile'), GameResourceState.failed);
|
||||
});
|
||||
|
||||
test(
|
||||
'drops stale image load result after dispose without diagnostics',
|
||||
() async {
|
||||
final diagnostics = RuntimeDiagnostics();
|
||||
final resources = GameResourceManager(diagnostics: diagnostics);
|
||||
final package = await _createPackage('stale_image');
|
||||
final countingPackage = _CountingPackage(package);
|
||||
|
||||
await resources.mount(countingPackage);
|
||||
final image = resources.loadImage('tile');
|
||||
|
||||
resources.dispose();
|
||||
countingPackage.releaseReads();
|
||||
|
||||
expect(await image, isNull);
|
||||
expect(diagnostics.entries, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('optional preload failure does not fail mount', () async {
|
||||
final resources = GameResourceManager();
|
||||
final package = await _createPackage(
|
||||
'optional_failed',
|
||||
preload: GameResourcePreload.optional,
|
||||
);
|
||||
|
||||
await resources.mount(package);
|
||||
|
||||
expect(resources.imageState('tile'), GameResourceState.failed);
|
||||
expect(resources.imageError('tile'), isNotNull);
|
||||
});
|
||||
|
||||
test('required preload failure fails mount', () async {
|
||||
final resources = GameResourceManager();
|
||||
final package = await _createPackage(
|
||||
'required_failed',
|
||||
preload: GameResourcePreload.required,
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
resources.mount(package),
|
||||
throwsA(isA<ResourceLoadException>()),
|
||||
);
|
||||
expect(resources.imageState('tile'), GameResourceState.failed);
|
||||
expect(resources.imageError('tile'), isNotNull);
|
||||
});
|
||||
|
||||
test('dispose clears active package and advances generation', () async {
|
||||
final resources = GameResourceManager();
|
||||
final package = await _createPackage('dispose');
|
||||
|
||||
await resources.mount(package);
|
||||
expect(resources.generation, 1);
|
||||
|
||||
resources.dispose();
|
||||
|
||||
expect(resources.generation, 2);
|
||||
expect(() => resources.package, throwsStateError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const _pngBytes = [
|
||||
0x89,
|
||||
0x50,
|
||||
0x4e,
|
||||
0x47,
|
||||
0x0d,
|
||||
0x0a,
|
||||
0x1a,
|
||||
0x0a,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0d,
|
||||
0x49,
|
||||
0x48,
|
||||
0x44,
|
||||
0x52,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x08,
|
||||
0x06,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x1f,
|
||||
0x15,
|
||||
0xc4,
|
||||
0x89,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x0a,
|
||||
0x49,
|
||||
0x44,
|
||||
0x41,
|
||||
0x54,
|
||||
0x78,
|
||||
0x9c,
|
||||
0x63,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x01,
|
||||
0x0d,
|
||||
0x0a,
|
||||
0x2d,
|
||||
0xb4,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x49,
|
||||
0x45,
|
||||
0x4e,
|
||||
0x44,
|
||||
0xae,
|
||||
0x42,
|
||||
0x60,
|
||||
0x82,
|
||||
];
|
||||
|
||||
Future<GamePackage> _createPackage(
|
||||
String name, {
|
||||
String preload = GameResourcePreload.lazy,
|
||||
}) async {
|
||||
final root = await Directory.systemTemp.createTemp('resource_${name}_');
|
||||
Directory('${root.path}/assets').createSync(recursive: true);
|
||||
File('${root.path}/assets/tile.png').writeAsBytesSync(const []);
|
||||
|
||||
addTearDown(() {
|
||||
if (root.existsSync()) {
|
||||
root.deleteSync(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
return GamePackage.file(
|
||||
rootPath: root.path,
|
||||
manifest: GamePackageManifest(
|
||||
gameId: 'test',
|
||||
name: 'Test',
|
||||
version: '0.1.0',
|
||||
runtimeApiVersion: 1,
|
||||
entry: 'scripts/main.lua',
|
||||
assetsBase: 'assets',
|
||||
resources: {
|
||||
'tile': GameResource(
|
||||
type: 'image',
|
||||
path: 'assets/tile.png',
|
||||
preload: preload,
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<GamePackage> _createMultiImagePackage(String name) async {
|
||||
final root = await Directory.systemTemp.createTemp('resource_${name}_');
|
||||
Directory('${root.path}/assets').createSync(recursive: true);
|
||||
for (final file in ['board.png', 'piece.png', 'avatar.png']) {
|
||||
File('${root.path}/assets/$file').writeAsBytesSync(_pngBytes);
|
||||
}
|
||||
|
||||
addTearDown(() {
|
||||
if (root.existsSync()) {
|
||||
root.deleteSync(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
return GamePackage.file(
|
||||
rootPath: root.path,
|
||||
manifest: GamePackageManifest(
|
||||
gameId: 'test',
|
||||
name: 'Test',
|
||||
version: '0.1.0',
|
||||
runtimeApiVersion: 1,
|
||||
entry: 'scripts/main.lua',
|
||||
assetsBase: 'assets',
|
||||
resources: const {
|
||||
'board': GameResource(
|
||||
type: GameResourceType.image,
|
||||
path: 'assets/board.png',
|
||||
preload: GameResourcePreload.lazy,
|
||||
group: 'board',
|
||||
),
|
||||
'piece': GameResource(
|
||||
type: GameResourceType.image,
|
||||
path: 'assets/piece.png',
|
||||
preload: GameResourcePreload.lazy,
|
||||
group: 'board',
|
||||
),
|
||||
'avatar': GameResource(
|
||||
type: GameResourceType.image,
|
||||
path: 'assets/avatar.png',
|
||||
preload: GameResourcePreload.lazy,
|
||||
group: 'hud',
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _CountingPackage extends GamePackage {
|
||||
_CountingPackage(GamePackage package)
|
||||
: _releaseReads = async.Completer<void>(),
|
||||
super.file(rootPath: package.rootPath, manifest: package.manifest);
|
||||
|
||||
final async.Completer<void> _releaseReads;
|
||||
int readCount = 0;
|
||||
|
||||
void releaseReads() {
|
||||
if (!_releaseReads.isCompleted) {
|
||||
_releaseReads.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ByteData> readBytes(String relativeOrAbsolutePath) async {
|
||||
readCount++;
|
||||
await _releaseReads.future;
|
||||
return ByteData.sublistView(Uint8List(0));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user