Initial flame_lua_runtime package
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flame_lua_runtime/runtime/audio/runtime_audio_manager.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/game_diff.dart';
|
||||
import 'package:flame_lua_runtime/runtime/models/runtime_event.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package_activation_controller.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package_repository.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/stable_package_store.dart';
|
||||
import 'package:flame_lua_runtime/runtime/resources/game_resource_manager.dart';
|
||||
import 'package:flame_lua_runtime/runtime/scripting/script_engine.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('PackageActivationController', () {
|
||||
test(
|
||||
'activates repository candidate and marks stable after init',
|
||||
() async {
|
||||
final candidate = await _createPackage('candidate');
|
||||
final store = _FakeStablePackageStore();
|
||||
final scriptEngine = _FakeScriptEngine();
|
||||
|
||||
final result = await PackageActivationController(
|
||||
repository: _FakeRepository(candidate),
|
||||
resources: GameResourceManager(),
|
||||
scriptEngine: scriptEngine,
|
||||
store: store,
|
||||
assetFallback: _FakeRepository(await _createPackage('asset')),
|
||||
).activate(gameId: 'ludo', contextBuilder: _context);
|
||||
|
||||
expect(result.package.rootPath, candidate.rootPath);
|
||||
expect(scriptEngine.loadedPackages, [candidate.rootPath]);
|
||||
expect(scriptEngine.initPackages, [candidate.rootPath]);
|
||||
expect(store.markedPackages, [candidate.rootPath]);
|
||||
},
|
||||
);
|
||||
|
||||
test('falls back to stable when repository candidate is invalid', () async {
|
||||
final invalidCandidate = await _createPackage('invalid', script: 'bad');
|
||||
final stable = await _createPackage('stable');
|
||||
final store = _FakeStablePackageStore(stable: stable);
|
||||
final scriptEngine = _FakeScriptEngine();
|
||||
|
||||
final result = await PackageActivationController(
|
||||
repository: _FakeRepository(invalidCandidate),
|
||||
resources: GameResourceManager(),
|
||||
scriptEngine: scriptEngine,
|
||||
store: store,
|
||||
assetFallback: _FakeRepository(await _createPackage('asset')),
|
||||
).activate(gameId: 'ludo', contextBuilder: _context);
|
||||
|
||||
expect(result.package.rootPath, stable.rootPath);
|
||||
expect(scriptEngine.loadedPackages, [stable.rootPath]);
|
||||
expect(store.markedPackages, [stable.rootPath]);
|
||||
});
|
||||
|
||||
test(
|
||||
'falls back to previous stable when current stable is invalid',
|
||||
() async {
|
||||
final invalidCandidate = await _createPackage(
|
||||
'invalid_candidate',
|
||||
script: 'bad',
|
||||
);
|
||||
final invalidStable = await _createPackage(
|
||||
'invalid_stable',
|
||||
script: 'bad',
|
||||
);
|
||||
final previous = await _createPackage('previous');
|
||||
final store = _FakeStablePackageStore(
|
||||
stable: invalidStable,
|
||||
previous: previous,
|
||||
);
|
||||
final scriptEngine = _FakeScriptEngine();
|
||||
|
||||
final result = await PackageActivationController(
|
||||
repository: _FakeRepository(invalidCandidate),
|
||||
resources: GameResourceManager(),
|
||||
scriptEngine: scriptEngine,
|
||||
store: store,
|
||||
assetFallback: _FakeRepository(await _createPackage('asset')),
|
||||
).activate(gameId: 'ludo', contextBuilder: _context);
|
||||
|
||||
expect(result.package.rootPath, previous.rootPath);
|
||||
expect(scriptEngine.loadedPackages, [previous.rootPath]);
|
||||
expect(store.markedPackages, [previous.rootPath]);
|
||||
},
|
||||
);
|
||||
|
||||
test('falls back to asset when repository load fails', () async {
|
||||
final failedCandidate = await _createPackage('failed_candidate');
|
||||
final fallback = await _createPackage('asset');
|
||||
final store = _FakeStablePackageStore();
|
||||
final scriptEngine = _FakeScriptEngine();
|
||||
|
||||
final result = await PackageActivationController(
|
||||
repository: _FakeRepository(
|
||||
failedCandidate,
|
||||
error: StateError('network failed'),
|
||||
),
|
||||
resources: GameResourceManager(),
|
||||
scriptEngine: scriptEngine,
|
||||
store: store,
|
||||
assetFallback: _FakeRepository(fallback),
|
||||
).activate(gameId: 'ludo', contextBuilder: _context);
|
||||
|
||||
expect(result.package.rootPath, fallback.rootPath);
|
||||
expect(scriptEngine.loadedPackages, [fallback.rootPath]);
|
||||
expect(store.markedPackages, [fallback.rootPath]);
|
||||
});
|
||||
|
||||
test('stops activation when cancellation guard turns false', () async {
|
||||
final candidate = await _createPackage('candidate');
|
||||
final fallback = await _createPackage('asset');
|
||||
final store = _FakeStablePackageStore();
|
||||
final scriptEngine = _FakeScriptEngine();
|
||||
var active = false;
|
||||
|
||||
await expectLater(
|
||||
PackageActivationController(
|
||||
repository: _FakeRepository(candidate),
|
||||
resources: GameResourceManager(),
|
||||
scriptEngine: scriptEngine,
|
||||
store: store,
|
||||
assetFallback: _FakeRepository(fallback),
|
||||
).activate(
|
||||
gameId: 'ludo',
|
||||
contextBuilder: _context,
|
||||
shouldContinue: () => active,
|
||||
),
|
||||
throwsStateError,
|
||||
);
|
||||
|
||||
expect(scriptEngine.loadedPackages, isEmpty);
|
||||
expect(store.markedPackages, isEmpty);
|
||||
});
|
||||
|
||||
test('uses staging resources and script engine before commit', () async {
|
||||
final candidate = await _createPackage('candidate');
|
||||
final activeResources = _RecordingResourceManager();
|
||||
final activeScriptEngine = _FakeScriptEngine();
|
||||
final stagingResources = <_RecordingResourceManager>[];
|
||||
final stagingEngines = <_FakeScriptEngine>[];
|
||||
|
||||
final result = await PackageActivationController(
|
||||
repository: _FakeRepository(candidate),
|
||||
resources: activeResources,
|
||||
scriptEngine: activeScriptEngine,
|
||||
store: _FakeStablePackageStore(),
|
||||
assetFallback: _FakeRepository(await _createPackage('asset')),
|
||||
resourceManagerFactory: () {
|
||||
final resources = _RecordingResourceManager();
|
||||
stagingResources.add(resources);
|
||||
return resources;
|
||||
},
|
||||
scriptEngineFactory: () {
|
||||
final engine = _FakeScriptEngine();
|
||||
stagingEngines.add(engine);
|
||||
return engine;
|
||||
},
|
||||
).activate(gameId: 'ludo', contextBuilder: _context);
|
||||
|
||||
expect(activeResources.mountedPackages, isEmpty);
|
||||
expect(activeScriptEngine.loadedPackages, isEmpty);
|
||||
expect(result.resources, same(stagingResources.single));
|
||||
expect(result.scriptEngine, same(stagingEngines.single));
|
||||
expect(stagingResources.single.mountedPackages, [candidate.rootPath]);
|
||||
expect(stagingEngines.single.loadedPackages, [candidate.rootPath]);
|
||||
});
|
||||
|
||||
test('falls back when required audio preload fails', () async {
|
||||
final candidate = await _createAudioPackage(
|
||||
'candidate_bad_audio',
|
||||
writeAudio: false,
|
||||
);
|
||||
final fallback = await _createAudioPackage('asset_audio');
|
||||
final store = _FakeStablePackageStore();
|
||||
final scriptEngine = _FakeScriptEngine();
|
||||
|
||||
final result = await PackageActivationController(
|
||||
repository: _FakeRepository(candidate),
|
||||
resources: GameResourceManager(),
|
||||
scriptEngine: scriptEngine,
|
||||
audio: RuntimeAudioManager(),
|
||||
store: store,
|
||||
assetFallback: _FakeRepository(fallback),
|
||||
audioManagerFactory: RuntimeAudioManager.new,
|
||||
).activate(gameId: 'ludo', contextBuilder: _context);
|
||||
|
||||
expect(result.package.rootPath, fallback.rootPath);
|
||||
expect(result.audio, isNotNull);
|
||||
expect(store.markedPackages, [fallback.rootPath]);
|
||||
});
|
||||
|
||||
test('does not mark a package when smoke_test fails', () async {
|
||||
final candidate = await _createPackage('candidate');
|
||||
final fallback = await _createPackage('asset');
|
||||
final store = _FakeStablePackageStore();
|
||||
final scriptEngine = _FakeScriptEngine(
|
||||
smokeFailures: {candidate.rootPath},
|
||||
);
|
||||
|
||||
final result = await PackageActivationController(
|
||||
repository: _FakeRepository(candidate),
|
||||
resources: GameResourceManager(),
|
||||
scriptEngine: scriptEngine,
|
||||
store: store,
|
||||
assetFallback: _FakeRepository(fallback),
|
||||
).activate(gameId: 'ludo', contextBuilder: _context);
|
||||
|
||||
expect(result.package.rootPath, fallback.rootPath);
|
||||
expect(scriptEngine.loadedPackages, [
|
||||
candidate.rootPath,
|
||||
fallback.rootPath,
|
||||
]);
|
||||
expect(store.markedPackages, [fallback.rootPath]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Map<String, Object?> _context(GamePackage package) {
|
||||
return {
|
||||
'runtimeApiVersion': 1,
|
||||
'gameId': package.manifest.gameId,
|
||||
'gameVersion': package.manifest.version,
|
||||
};
|
||||
}
|
||||
|
||||
Future<GamePackage> _createPackage(
|
||||
String name, {
|
||||
String script = _validScript,
|
||||
}) async {
|
||||
final root = await Directory.systemTemp.createTemp('activation_${name}_');
|
||||
Directory('${root.path}/scripts').createSync(recursive: true);
|
||||
File('${root.path}/scripts/main.lua').writeAsStringSync(script);
|
||||
addTearDown(() {
|
||||
if (root.existsSync()) {
|
||||
root.deleteSync(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
return GamePackage.file(
|
||||
rootPath: root.path,
|
||||
manifest: const GamePackageManifest(
|
||||
gameId: 'ludo',
|
||||
name: 'Ludo',
|
||||
version: '0.1.0',
|
||||
runtimeApiVersion: 1,
|
||||
entry: 'scripts/main.lua',
|
||||
assetsBase: 'assets',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<GamePackage> _createAudioPackage(
|
||||
String name, {
|
||||
bool writeAudio = true,
|
||||
}) async {
|
||||
final root = await Directory.systemTemp.createTemp('activation_${name}_');
|
||||
Directory('${root.path}/scripts').createSync(recursive: true);
|
||||
Directory('${root.path}/assets').createSync(recursive: true);
|
||||
File('${root.path}/scripts/main.lua').writeAsStringSync(_validScript);
|
||||
if (writeAudio) {
|
||||
File('${root.path}/assets/dice.wav').writeAsBytesSync(const [1, 2, 3]);
|
||||
}
|
||||
addTearDown(() {
|
||||
if (root.existsSync()) {
|
||||
root.deleteSync(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
return GamePackage.file(
|
||||
rootPath: root.path,
|
||||
manifest: const GamePackageManifest(
|
||||
gameId: 'ludo',
|
||||
name: 'Ludo',
|
||||
version: '0.1.0',
|
||||
runtimeApiVersion: 1,
|
||||
entry: 'scripts/main.lua',
|
||||
assetsBase: 'assets',
|
||||
resources: {
|
||||
'dice': GameResource(
|
||||
type: GameResourceType.audio,
|
||||
path: 'assets/dice.wav',
|
||||
preload: GameResourcePreload.required,
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeRepository implements GamePackageRepository {
|
||||
const _FakeRepository(this.package, {this.error});
|
||||
|
||||
final GamePackage package;
|
||||
final Object? error;
|
||||
|
||||
@override
|
||||
Future<GamePackage> load(String gameId) async {
|
||||
final value = error;
|
||||
if (value != null) {
|
||||
throw value;
|
||||
}
|
||||
return package;
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeStablePackageStore implements StablePackageStore {
|
||||
_FakeStablePackageStore({this.stable, this.previous});
|
||||
|
||||
final GamePackage? stable;
|
||||
final GamePackage? previous;
|
||||
final markedPackages = <String>[];
|
||||
|
||||
@override
|
||||
Future<Directory> cacheRoot() => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> markStable(GamePackage package) async {
|
||||
markedPackages.add(package.rootPath);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<GamePackage?> previousStablePackage(String gameId) async => previous;
|
||||
|
||||
@override
|
||||
Future<GamePackage?> stablePackage(String gameId) async => stable;
|
||||
|
||||
@override
|
||||
Future<Directory> versionDirectory(String gameId, String version) =>
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
class _RecordingResourceManager extends GameResourceManager {
|
||||
final mountedPackages = <String>[];
|
||||
var disposed = false;
|
||||
|
||||
@override
|
||||
Future<void> mount(GamePackage package) async {
|
||||
mountedPackages.add(package.rootPath);
|
||||
await super.mount(package);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
disposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeScriptEngine implements ScriptEngine {
|
||||
_FakeScriptEngine({this.smokeFailures = const {}});
|
||||
|
||||
final Set<String> smokeFailures;
|
||||
final loadedPackages = <String>[];
|
||||
final initPackages = <String>[];
|
||||
GamePackage? _package;
|
||||
|
||||
@override
|
||||
Future<void> loadPackage(GamePackage package) async {
|
||||
_package = package;
|
||||
loadedPackages.add(package.rootPath);
|
||||
}
|
||||
|
||||
@override
|
||||
bool smokeTest(Map<String, Object?> context) {
|
||||
return !smokeFailures.contains(_package?.rootPath);
|
||||
}
|
||||
|
||||
@override
|
||||
GameDiff init(Map<String, Object?> context) {
|
||||
final package = _package;
|
||||
if (package != null) {
|
||||
initPackages.add(package.rootPath);
|
||||
}
|
||||
return GameDiff.empty;
|
||||
}
|
||||
|
||||
@override
|
||||
GameDiff dispatchEvent(RuntimeEvent event) => GameDiff.empty;
|
||||
}
|
||||
|
||||
const _validScript = '''
|
||||
function smoke_test(ctx)
|
||||
return true
|
||||
end
|
||||
|
||||
function init(ctx)
|
||||
return {}
|
||||
end
|
||||
|
||||
function on_event(event)
|
||||
return {}
|
||||
end
|
||||
''';
|
||||
236
test/runtime/packages/game_package_manifest_test.dart
Normal file
236
test/runtime/packages/game_package_manifest_test.dart
Normal file
@@ -0,0 +1,236 @@
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('GamePackageManifest', () {
|
||||
test('parses manifest with resources', () {
|
||||
final manifest = GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
'assetsBase': 'assets',
|
||||
'defaultLocale': 'zh-Hans',
|
||||
'supportedLocales': ['zh-Hans', 'en'],
|
||||
'display': {
|
||||
'designWidth': 720,
|
||||
'designHeight': 1280,
|
||||
'scaleMode': 'fit',
|
||||
},
|
||||
'modules': {
|
||||
'runtime_ui': 'runtime:runtime_ui.lua',
|
||||
'theme': 'scripts/theme.lua',
|
||||
},
|
||||
'resources': {
|
||||
'board': {
|
||||
'type': 'image',
|
||||
'path': 'assets/board.png',
|
||||
'preload': 'lazy',
|
||||
'group': 'board',
|
||||
},
|
||||
'roll': {'type': 'audio', 'path': 'assets/roll.mp3'},
|
||||
'hero': {
|
||||
'type': 'spine',
|
||||
'atlas': 'assets/hero.atlas',
|
||||
'skeleton': 'assets/hero.skel',
|
||||
'preload': 'lazy',
|
||||
'group': 'actors',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(manifest.gameId, 'ludo');
|
||||
expect(manifest.name, 'Ludo');
|
||||
expect(manifest.version, '0.1.0');
|
||||
expect(manifest.runtimeApiVersion, 1);
|
||||
expect(manifest.entry, 'scripts/main.lua');
|
||||
expect(manifest.assetsBase, 'assets');
|
||||
expect(manifest.defaultLocale, 'zh-Hans');
|
||||
expect(manifest.supportedLocales, ['zh-Hans', 'en']);
|
||||
expect(manifest.display.designWidth, 720);
|
||||
expect(manifest.display.designHeight, 1280);
|
||||
expect(manifest.display.scaleMode, 'fit');
|
||||
expect(manifest.resources['board']?.type, 'image');
|
||||
expect(manifest.resources['board']?.path, 'assets/board.png');
|
||||
expect(manifest.resources['board']?.preload, GameResourcePreload.lazy);
|
||||
expect(manifest.resources['board']?.group, 'board');
|
||||
expect(manifest.resources['roll']?.type, GameResourceType.audio);
|
||||
expect(manifest.resources['roll']?.preload, GameResourcePreload.required);
|
||||
expect(manifest.resources['hero']?.type, GameResourceType.spine);
|
||||
expect(manifest.resources['hero']?.atlas, 'assets/hero.atlas');
|
||||
expect(manifest.resources['hero']?.skeleton, 'assets/hero.skel');
|
||||
expect(manifest.resources['hero']?.path, isEmpty);
|
||||
expect(manifest.modules, {
|
||||
'runtime_ui': 'runtime:runtime_ui.lua',
|
||||
'theme': 'scripts/theme.lua',
|
||||
});
|
||||
});
|
||||
|
||||
test('defaults assetsBase to assets', () {
|
||||
final manifest = GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
});
|
||||
|
||||
expect(manifest.assetsBase, 'assets');
|
||||
expect(manifest.defaultLocale, 'en');
|
||||
expect(manifest.supportedLocales, ['en']);
|
||||
expect(manifest.display.designWidth, 720);
|
||||
expect(manifest.display.designHeight, 720);
|
||||
expect(manifest.display.scaleMode, 'fit');
|
||||
expect(manifest.resources, isEmpty);
|
||||
expect(manifest.modules, isEmpty);
|
||||
});
|
||||
|
||||
test('rejects invalid required fields and resources', () {
|
||||
expect(
|
||||
() => GamePackageManifest.fromMap({
|
||||
'gameId': '',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': '1',
|
||||
'entry': 'scripts/main.lua',
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
'resources': {
|
||||
'board': {'type': '', 'path': 'assets/board.png'},
|
||||
},
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
'resources': {
|
||||
'roll': {'type': 'sound', 'path': 'assets/roll.mp3'},
|
||||
},
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
'modules': {'theme': 1},
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
'resources': {
|
||||
'board': {
|
||||
'type': 'image',
|
||||
'path': 'assets/board.png',
|
||||
'preload': 'eager',
|
||||
},
|
||||
},
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
'resources': {
|
||||
'board': {'type': 'image', 'path': 'assets/board.png', 'group': ''},
|
||||
},
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
'resources': {
|
||||
'hero': {'type': 'spine', 'atlas': 'assets/hero.atlas'},
|
||||
},
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
'defaultLocale': 'zh-Hans',
|
||||
'supportedLocales': ['en'],
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
'display': {'designWidth': 0},
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => GamePackageManifest.fromMap({
|
||||
'gameId': 'ludo',
|
||||
'name': 'Ludo',
|
||||
'version': '0.1.0',
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
'display': {'scaleMode': 'zoom'},
|
||||
}),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
84
test/runtime/packages/game_package_test.dart
Normal file
84
test/runtime/packages/game_package_test.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/game_package_manifest.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('GamePackage', () {
|
||||
test('resolves manifest resource keys', () {
|
||||
final package = _package();
|
||||
|
||||
expect(
|
||||
package.resolveResourcePath('board'),
|
||||
'example/assets/games/ludo/assets/board.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('resolves package-relative paths and assetsBase fallback', () {
|
||||
final package = _package();
|
||||
|
||||
expect(
|
||||
package.resolveResourcePath('scripts/main.lua'),
|
||||
'example/assets/games/ludo/scripts/main.lua',
|
||||
);
|
||||
expect(
|
||||
package.resolveResourcePath('unknown.png'),
|
||||
'example/assets/games/ludo/assets/unknown.png',
|
||||
);
|
||||
expect(
|
||||
package.resolveResourcePath(
|
||||
'example/assets/games/ludo/assets/board.png',
|
||||
),
|
||||
'example/assets/games/ludo/assets/board.png',
|
||||
);
|
||||
});
|
||||
|
||||
test('keeps runtime Lua root configurable per package', () {
|
||||
final package = GamePackage.asset(
|
||||
rootPath: 'example/assets/games/ludo',
|
||||
manifest: _manifest(),
|
||||
runtimeLuaRoot: 'packages/flame_lua_runtime/assets/runtime/lua',
|
||||
);
|
||||
|
||||
expect(
|
||||
package.runtimeLuaRoot,
|
||||
'packages/flame_lua_runtime/assets/runtime/lua',
|
||||
);
|
||||
});
|
||||
|
||||
test('reads file package text and bytes', () async {
|
||||
final root = await Directory.systemTemp.createTemp('game_package_test_');
|
||||
addTearDown(() => root.deleteSync(recursive: true));
|
||||
Directory('${root.path}/scripts').createSync(recursive: true);
|
||||
File('${root.path}/scripts/main.lua').writeAsStringSync('return true');
|
||||
|
||||
final package = GamePackage.file(
|
||||
rootPath: root.path,
|
||||
manifest: _manifest(),
|
||||
);
|
||||
|
||||
expect(await package.readText('scripts/main.lua'), 'return true');
|
||||
expect((await package.readBytes('scripts/main.lua')).lengthInBytes, 11);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
GamePackage _package() {
|
||||
return GamePackage.asset(
|
||||
rootPath: 'example/assets/games/ludo',
|
||||
manifest: _manifest(),
|
||||
);
|
||||
}
|
||||
|
||||
GamePackageManifest _manifest() {
|
||||
return const GamePackageManifest(
|
||||
gameId: 'ludo',
|
||||
name: 'Ludo',
|
||||
version: '0.1.0',
|
||||
runtimeApiVersion: 1,
|
||||
entry: 'scripts/main.lua',
|
||||
assetsBase: 'assets',
|
||||
resources: {'board': GameResource(type: 'image', path: 'assets/board.png')},
|
||||
);
|
||||
}
|
||||
144
test/runtime/packages/package_verifier_test.dart
Normal file
144
test/runtime/packages/package_verifier_test.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'dart:io';
|
||||
|
||||
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/packages/package_verifier.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('PackageVerifier', () {
|
||||
test('accepts a valid file package', () async {
|
||||
final package = await _createPackage();
|
||||
|
||||
await expectLater(
|
||||
const PackageVerifier(runtimeApiVersion: 1).verify(package),
|
||||
completes,
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts runtime framework module paths', () async {
|
||||
final package = await _createPackage(
|
||||
modules: {'runtime_ui': 'runtime:runtime_ui.lua'},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
const PackageVerifier(runtimeApiVersion: 1).verify(package),
|
||||
completes,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects unsupported runtimeApiVersion', () async {
|
||||
final package = await _createPackage(runtimeApiVersion: 2);
|
||||
|
||||
await expectLater(
|
||||
const PackageVerifier(runtimeApiVersion: 1).verify(package),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects missing Lua entry functions', () async {
|
||||
final package = await _createPackage(script: 'function init(ctx) end');
|
||||
|
||||
await expectLater(
|
||||
const PackageVerifier(runtimeApiVersion: 1).verify(package),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects unsafe declared modules', () async {
|
||||
final package = await _createPackage(
|
||||
modules: {'../theme': 'scripts/theme.lua'},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
const PackageVerifier(runtimeApiVersion: 1).verify(package),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects module paths outside scripts directory', () async {
|
||||
final package = await _createPackage(
|
||||
modules: {'theme': 'assets/theme.lua'},
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
const PackageVerifier(runtimeApiVersion: 1).verify(package),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects missing declared resources', () async {
|
||||
final package = await _createPackage(writeResource: false);
|
||||
|
||||
await expectLater(
|
||||
const PackageVerifier(runtimeApiVersion: 1).verify(package),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects resource paths that escape package root', () async {
|
||||
final package = await _createPackage(resourcePath: '../outside.png');
|
||||
|
||||
await expectLater(
|
||||
const PackageVerifier(runtimeApiVersion: 1).verify(package),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<GamePackage> _createPackage({
|
||||
int runtimeApiVersion = 1,
|
||||
String script = _validScript,
|
||||
String resourcePath = 'assets/board.png',
|
||||
bool writeResource = true,
|
||||
Map<String, String> modules = const {'theme': 'scripts/theme.lua'},
|
||||
}) async {
|
||||
final root = await Directory.systemTemp.createTemp('package_verifier_test_');
|
||||
Directory('${root.path}/scripts').createSync(recursive: true);
|
||||
Directory('${root.path}/assets').createSync(recursive: true);
|
||||
File('${root.path}/scripts/main.lua').writeAsStringSync(script);
|
||||
File('${root.path}/scripts/theme.lua').writeAsStringSync('return {}');
|
||||
if (writeResource && !resourcePath.contains('..')) {
|
||||
File('${root.path}/$resourcePath')
|
||||
..createSync(recursive: true)
|
||||
..writeAsBytesSync([1, 2, 3]);
|
||||
}
|
||||
|
||||
final package = GamePackage.file(
|
||||
rootPath: root.path,
|
||||
manifest: GamePackageManifest(
|
||||
gameId: 'ludo',
|
||||
name: 'Ludo',
|
||||
version: '0.1.0',
|
||||
runtimeApiVersion: runtimeApiVersion,
|
||||
entry: 'scripts/main.lua',
|
||||
assetsBase: 'assets',
|
||||
resources: {'board': GameResource(type: 'image', path: resourcePath)},
|
||||
modules: modules,
|
||||
),
|
||||
);
|
||||
|
||||
addTearDown(() {
|
||||
if (root.existsSync()) {
|
||||
root.deleteSync(recursive: true);
|
||||
}
|
||||
});
|
||||
return package;
|
||||
}
|
||||
|
||||
const _validScript = '''
|
||||
function smoke_test(ctx)
|
||||
return true
|
||||
end
|
||||
|
||||
function init(ctx)
|
||||
return {}
|
||||
end
|
||||
|
||||
function on_event(event)
|
||||
return {}
|
||||
end
|
||||
''';
|
||||
Reference in New Issue
Block a user