feat: add package source compatibility controls
This commit is contained in:
220
test/runtime/packages/game_package_repository_test.dart
Normal file
220
test/runtime/packages/game_package_repository_test.dart
Normal file
@@ -0,0 +1,220 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flame_lua_runtime/runtime/game/runtime_options.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/packages/game_package_repository.dart';
|
||||
import 'package:flame_lua_runtime/runtime/packages/stable_package_store.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
|
||||
void main() {
|
||||
group('FileGamePackageRepository', () {
|
||||
test('loads a package from a local development directory', () async {
|
||||
final root = await Directory.systemTemp.createTemp('local_packages_');
|
||||
addTearDown(() => root.deleteSync(recursive: true));
|
||||
await _writePackage(root.path, 'gomoku', version: '0.2.0');
|
||||
|
||||
final package = await FileGamePackageRepository(
|
||||
baseDirectory: root.path,
|
||||
runtimeOptions: const RuntimeOptions(runtimeLuaRoot: 'runtime/lua'),
|
||||
).load('gomoku');
|
||||
|
||||
expect(package.source, GamePackageSource.file);
|
||||
expect(package.manifest.gameId, 'gomoku');
|
||||
expect(package.manifest.version, '0.2.0');
|
||||
expect(package.runtimeLuaRoot, 'runtime/lua');
|
||||
expect(await package.readText('scripts/main.lua'), contains('init'));
|
||||
});
|
||||
});
|
||||
|
||||
group('RemoteGamePackageRepository compatibility', () {
|
||||
test(
|
||||
'sends host compatibility query and falls back when incompatible',
|
||||
() async {
|
||||
late Uri requestedUri;
|
||||
var downloadedPackage = false;
|
||||
final fallback = await _createPackage('fallback');
|
||||
final client = MockClient((request) async {
|
||||
requestedUri = request.url;
|
||||
if (request.url.path.endsWith('/remote_manifest.json')) {
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'gameId': 'gomoku',
|
||||
'version': '2.0.0',
|
||||
'packageUrl': 'http://example.test/packages/gomoku.zip',
|
||||
'sha256': 'unused',
|
||||
'compat': {
|
||||
'runtimeApiVersion': 1,
|
||||
'minRuntimeVersion': '1.2.0',
|
||||
'minHostBuild': 200,
|
||||
'platforms': ['windows'],
|
||||
'channels': ['prod'],
|
||||
},
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}
|
||||
downloadedPackage = true;
|
||||
return http.Response('not used', 500);
|
||||
});
|
||||
|
||||
final package = await RemoteGamePackageRepository(
|
||||
baseUri: Uri.parse('http://example.test/'),
|
||||
client: client,
|
||||
fallback: _SinglePackageRepository(fallback),
|
||||
store: _EmptyStablePackageStore(),
|
||||
runtimeOptions: const RuntimeOptions(
|
||||
runtimeVersion: '1.1.0',
|
||||
hostBuild: 100,
|
||||
channel: 'prod',
|
||||
platform: 'windows',
|
||||
),
|
||||
).load('gomoku');
|
||||
|
||||
expect(package.rootPath, fallback.rootPath);
|
||||
expect(requestedUri.queryParameters['runtimeApiVersion'], '1');
|
||||
expect(requestedUri.queryParameters['runtimeVersion'], '1.1.0');
|
||||
expect(requestedUri.queryParameters['hostBuild'], '100');
|
||||
expect(requestedUri.queryParameters['platform'], 'windows');
|
||||
expect(requestedUri.queryParameters['channel'], 'prod');
|
||||
expect(downloadedPackage, isFalse);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('StablePackageStore', () {
|
||||
const channel = MethodChannel('plugins.flutter.io/path_provider');
|
||||
|
||||
setUp(() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null);
|
||||
});
|
||||
|
||||
test('ignores empty or malformed stable marker files', () async {
|
||||
final support = await Directory.systemTemp.createTemp('support_');
|
||||
addTearDown(() => support.deleteSync(recursive: true));
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
if (call.method == 'getApplicationSupportDirectory') {
|
||||
return support.path;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
final store = const StablePackageStore();
|
||||
final gameDir = Directory('${support.path}/flame_lua_packages/gomoku')
|
||||
..createSync(recursive: true);
|
||||
final marker = File('${gameDir.path}/stable.json');
|
||||
|
||||
marker.writeAsStringSync('');
|
||||
expect(await store.stablePackage('gomoku'), isNull);
|
||||
expect(await store.previousStablePackage('gomoku'), isNull);
|
||||
|
||||
marker.writeAsStringSync('{bad json');
|
||||
expect(await store.stablePackage('gomoku'), isNull);
|
||||
expect(await store.previousStablePackage('gomoku'), isNull);
|
||||
});
|
||||
|
||||
test('writes stable marker atomically and reads current package', () async {
|
||||
final support = await Directory.systemTemp.createTemp('support_');
|
||||
addTearDown(() => support.deleteSync(recursive: true));
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
if (call.method == 'getApplicationSupportDirectory') {
|
||||
return support.path;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
final package = await _createPackage('stable');
|
||||
final store = const StablePackageStore();
|
||||
|
||||
await store.markStable(package);
|
||||
|
||||
final marker = File(
|
||||
'${support.path}/flame_lua_packages/gomoku/stable.json',
|
||||
);
|
||||
expect(marker.existsSync(), isTrue);
|
||||
expect(File('${marker.path}.tmp').existsSync(), isFalse);
|
||||
final stable = await store.stablePackage('gomoku');
|
||||
expect(stable?.rootPath, package.rootPath);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<GamePackage> _createPackage(String name) async {
|
||||
final root = await Directory.systemTemp.createTemp('package_${name}_');
|
||||
addTearDown(() => root.deleteSync(recursive: true));
|
||||
await _writePackageRoot(root.path, gameId: 'gomoku', version: '1.0.0');
|
||||
return GamePackage.file(
|
||||
rootPath: root.path,
|
||||
manifest: GamePackageManifest.fromJsonString(
|
||||
await File('${root.path}/manifest.json').readAsString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _writePackage(
|
||||
String baseDirectory,
|
||||
String gameId, {
|
||||
required String version,
|
||||
}) async {
|
||||
final root = Directory('$baseDirectory/$gameId')..createSync(recursive: true);
|
||||
await _writePackageRoot(root.path, gameId: gameId, version: version);
|
||||
}
|
||||
|
||||
Future<void> _writePackageRoot(
|
||||
String root, {
|
||||
required String gameId,
|
||||
required String version,
|
||||
}) async {
|
||||
Directory('$root/scripts').createSync(recursive: true);
|
||||
await File('$root/scripts/main.lua').writeAsString('''
|
||||
function smoke_test(ctx) return true end
|
||||
function init(ctx) return {} end
|
||||
function on_event(event) return {} end
|
||||
''');
|
||||
await File('$root/manifest.json').writeAsString(
|
||||
jsonEncode({
|
||||
'gameId': gameId,
|
||||
'name': gameId,
|
||||
'version': version,
|
||||
'runtimeApiVersion': 1,
|
||||
'entry': 'scripts/main.lua',
|
||||
'assetsBase': 'assets',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
class _SinglePackageRepository implements GamePackageRepository {
|
||||
const _SinglePackageRepository(this.package);
|
||||
|
||||
final GamePackage package;
|
||||
|
||||
@override
|
||||
Future<GamePackage> load(String gameId) async => package;
|
||||
}
|
||||
|
||||
class _EmptyStablePackageStore implements StablePackageStore {
|
||||
@override
|
||||
Future<Directory> cacheRoot() => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> markStable(GamePackage package) async {}
|
||||
|
||||
@override
|
||||
Future<GamePackage?> previousStablePackage(String gameId) async => null;
|
||||
|
||||
@override
|
||||
Future<GamePackage?> stablePackage(String gameId) async => null;
|
||||
|
||||
@override
|
||||
Future<Directory> versionDirectory(String gameId, String version) =>
|
||||
throw UnimplementedError();
|
||||
}
|
||||
@@ -4,7 +4,14 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
void main() {
|
||||
test('public runtime API exposes minimal integration surface', () {
|
||||
const repository = AssetGamePackageRepository();
|
||||
const options = RuntimeOptions(runtimeLuaRoot: 'custom/runtime/lua');
|
||||
const fileRepository = FileGamePackageRepository(baseDirectory: 'packages');
|
||||
const options = RuntimeOptions(
|
||||
runtimeLuaRoot: 'custom/runtime/lua',
|
||||
runtimeVersion: '1.2.0',
|
||||
hostBuild: 12,
|
||||
platform: 'windows',
|
||||
channel: 'dev',
|
||||
);
|
||||
const widget = LuaGameWidget(
|
||||
gameId: 'template',
|
||||
packageRepository: repository,
|
||||
@@ -14,6 +21,11 @@ void main() {
|
||||
expect(widget.gameId, 'template');
|
||||
expect(widget.packageRepository, same(repository));
|
||||
expect(widget.runtimeOptions.runtimeLuaRoot, 'custom/runtime/lua');
|
||||
expect(widget.runtimeOptions.runtimeVersion, '1.2.0');
|
||||
expect(widget.runtimeOptions.hostBuild, 12);
|
||||
expect(widget.runtimeOptions.platform, 'windows');
|
||||
expect(widget.runtimeOptions.channel, 'dev');
|
||||
expect(fileRepository.baseDirectory, 'packages');
|
||||
expect(LuaDardoScriptEngine.new, isA<ScriptEngine Function()>());
|
||||
expect(RuntimeLocaleResolver.localeFromTag('zh-Hans').scriptCode, 'Hans');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user