Collapses indirection layers and one-caller abstractions across the video player, shortcut dispatch, shader loading and context-menu code, including the VideoPIPManager pass-through over PipService.
209 lines
9.2 KiB
Dart
209 lines
9.2 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
|
|
import 'package:plezy/models/shader_preset.dart';
|
|
import 'package:plezy/services/shader_asset_loader.dart';
|
|
|
|
import '../test_helpers/io_fakes.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
late PathProviderPlatform originalPathProvider;
|
|
late Directory root;
|
|
late Directory supportDirectory;
|
|
late File sentinel;
|
|
|
|
setUp(() async {
|
|
originalPathProvider = PathProviderPlatform.instance;
|
|
root = await Directory.systemTemp.createTemp('plezy_shader_asset_loader_test_');
|
|
PathProviderPlatform.instance = FakePathProvider(root);
|
|
supportDirectory = Directory(path.join(root.path, 'support'))..createSync(recursive: true);
|
|
sentinel = File(path.join(supportDirectory.path, 'sentinel.glsl'))..writeAsStringSync('sentinel');
|
|
ShaderAssetLoader.clearCache();
|
|
});
|
|
|
|
tearDown(() async {
|
|
PathProviderPlatform.instance = originalPathProvider;
|
|
ShaderAssetLoader.clearCache();
|
|
if (await root.exists()) await root.delete(recursive: true);
|
|
});
|
|
|
|
ShaderPreset customPreset(String fileName) {
|
|
return ShaderPreset(id: 'custom_$fileName', name: fileName, type: ShaderPresetType.custom, fileName: fileName);
|
|
}
|
|
|
|
Future<List<int>> bundledBytes(String assetPath) async {
|
|
final data = await rootBundle.load('assets/shaders/$assetPath');
|
|
return data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
|
|
}
|
|
|
|
Future<void> expectBundledFile(String filePath, String assetPath) async {
|
|
expect(await File(filePath).readAsBytes(), await bundledBytes(assetPath));
|
|
}
|
|
|
|
test('traversal and absolute names cannot load or delete outside managed directory', () async {
|
|
for (final fileName in ['../sentinel.glsl', sentinel.path]) {
|
|
expect(await ShaderAssetLoader.getShadersForPreset(customPreset(fileName)), isEmpty);
|
|
await ShaderAssetLoader.deleteCustomShader(fileName);
|
|
expect(await sentinel.readAsString(), 'sentinel');
|
|
}
|
|
});
|
|
|
|
test('repairs stale built-in bytes in the application cache without touching temporary storage', () async {
|
|
final cacheFile = File(path.join(root.path, 'cache', 'shaders', 'nvscaler', 'NVScaler.glsl'))
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('stale');
|
|
final oldTemporaryFile = File(path.join(root.path, 'temp', 'shaders', 'nvscaler', 'NVScaler.glsl'))
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('old temporary sentinel');
|
|
|
|
final shaders = await ShaderAssetLoader.getShadersForPreset(ShaderPreset.nvscalerDefault);
|
|
|
|
expect(shaders, [cacheFile.path]);
|
|
await expectBundledFile(shaders.single, 'nvscaler/NVScaler.glsl');
|
|
expect(await oldTemporaryFile.readAsString(), 'old temporary sentinel');
|
|
});
|
|
|
|
test('recovers a truncated built-in final through a complete staged replacement', () async {
|
|
final targetDirectory = Directory(path.join(root.path, 'cache', 'shaders', 'nvscaler'))
|
|
..createSync(recursive: true);
|
|
final target = File(path.join(targetDirectory.path, 'NVScaler.glsl'))..writeAsBytesSync([1, 2, 3]);
|
|
final unrelatedPending = File('${target.path}.pending.interrupted')..writeAsBytesSync([1]);
|
|
|
|
final shaders = await ShaderAssetLoader.getNVScalerShaders();
|
|
|
|
expect(shaders, [target.path]);
|
|
await expectBundledFile(target.path, 'nvscaler/NVScaler.glsl');
|
|
expect(unrelatedPending.existsSync(), isTrue);
|
|
final ownedPending = await targetDirectory
|
|
.list()
|
|
.where((entry) => entry.path.contains('.pending.') && entry.path != unrelatedPending.path)
|
|
.toList();
|
|
expect(ownedPending, isEmpty);
|
|
});
|
|
|
|
test('does not return a partial path when final-file promotion fails', () async {
|
|
final target = Directory(path.join(root.path, 'cache', 'shaders', 'nvscaler', 'NVScaler.glsl'))
|
|
..createSync(recursive: true);
|
|
|
|
expect(await ShaderAssetLoader.getNVScalerShaders(), isEmpty);
|
|
|
|
final siblings = await target.parent.list().toList();
|
|
expect(siblings.where((entry) => entry.path.contains('.pending.')), isEmpty);
|
|
expect(target.existsSync(), isTrue);
|
|
});
|
|
|
|
test('coalesces concurrent built-in loads onto one complete final path', () async {
|
|
final results = await Future.wait([ShaderAssetLoader.getNVScalerShaders(), ShaderAssetLoader.getNVScalerShaders()]);
|
|
|
|
expect(results[0], results[1]);
|
|
expect(results[0], hasLength(1));
|
|
await expectBundledFile(results[0].single, 'nvscaler/NVScaler.glsl');
|
|
final directory = File(results[0].single).parent;
|
|
expect((await directory.list().toList()).where((entry) => entry.path.contains('.pending.')), isEmpty);
|
|
});
|
|
|
|
test('materializes representative built-in chains in MPV order with bundled bytes', () async {
|
|
final nvscaler = await ShaderAssetLoader.getNVScalerShaders();
|
|
final artcnn = await ShaderAssetLoader.getArtCNNShaders(
|
|
const ArtCNNConfig(model: ArtCNNModel.c4f16, variant: ArtCNNVariant.denoise),
|
|
);
|
|
final anime4k = await ShaderAssetLoader.getAnime4KShaders(
|
|
const Anime4KConfig(quality: Anime4KQuality.fast, mode: Anime4KMode.modeB),
|
|
);
|
|
|
|
final expected = {
|
|
nvscaler.single: 'nvscaler/NVScaler.glsl',
|
|
artcnn.single: 'artcnn/ArtCNN_C4F16_DN.glsl',
|
|
anime4k[0]: 'anime4k/Anime4K_Clamp_Highlights.glsl',
|
|
anime4k[1]: 'anime4k/Anime4K_Restore_CNN_M.glsl',
|
|
anime4k[2]: 'anime4k/Anime4K_Upscale_CNN_x2_M.glsl',
|
|
anime4k[3]: 'anime4k/Anime4K_AutoDownscalePre_x2.glsl',
|
|
};
|
|
expect(anime4k.map(path.basename).toList(), [
|
|
'Anime4K_Clamp_Highlights.glsl',
|
|
'Anime4K_Restore_CNN_M.glsl',
|
|
'Anime4K_Upscale_CNN_x2_M.glsl',
|
|
'Anime4K_AutoDownscalePre_x2.glsl',
|
|
]);
|
|
for (final entry in expected.entries) {
|
|
expect(path.isWithin(path.join(root.path, 'cache'), entry.key), isTrue);
|
|
await expectBundledFile(entry.key, entry.value);
|
|
}
|
|
});
|
|
|
|
test('repeats the restore pass in place for doubled Anime4K modes', () async {
|
|
final shaders = await ShaderAssetLoader.getAnime4KShaders(
|
|
const Anime4KConfig(quality: Anime4KQuality.fast, mode: Anime4KMode.modeBB),
|
|
);
|
|
|
|
expect(shaders.map(path.basename).toList(), [
|
|
'Anime4K_Clamp_Highlights.glsl',
|
|
'Anime4K_Restore_CNN_M.glsl',
|
|
'Anime4K_Restore_CNN_M.glsl',
|
|
'Anime4K_Upscale_CNN_x2_M.glsl',
|
|
'Anime4K_AutoDownscalePre_x2.glsl',
|
|
]);
|
|
expect(shaders[1], shaders[2]);
|
|
});
|
|
|
|
test('nested and non-GLSL names are rejected without touching matching files', () async {
|
|
final customDirectory = Directory(path.join(supportDirectory.path, 'custom_shaders'))..createSync(recursive: true);
|
|
final nested = File(path.join(customDirectory.path, 'subdir', 'name.glsl'))
|
|
..createSync(recursive: true)
|
|
..writeAsStringSync('nested');
|
|
final extraExtension = File(path.join(customDirectory.path, 'name.glsl.txt'))..writeAsStringSync('extra');
|
|
|
|
for (final fileName in ['subdir/name.glsl', r'subdir\name.glsl', '.', '..', 'name.glsl.txt', 'name.txt']) {
|
|
expect(ShaderAssetLoader.isValidCustomShaderFileName(fileName), isFalse);
|
|
expect(await ShaderAssetLoader.getShadersForPreset(customPreset(fileName)), isEmpty);
|
|
await ShaderAssetLoader.deleteCustomShader(fileName);
|
|
}
|
|
|
|
expect(await nested.readAsString(), 'nested');
|
|
expect(await extraExtension.readAsString(), 'extra');
|
|
});
|
|
|
|
test('non-GLSL import fails before creating a managed file', () async {
|
|
final source = File(path.join(root.path, 'shader.txt'))..writeAsStringSync('not glsl');
|
|
|
|
await expectLater(ShaderAssetLoader.importCustomShader(source.path), throwsArgumentError);
|
|
|
|
final customDirectory = Directory(path.join(supportDirectory.path, 'custom_shaders'));
|
|
expect(customDirectory.existsSync(), isFalse);
|
|
});
|
|
|
|
test('imports a direct UUID GLSL child and deletes only that file', () async {
|
|
final source = File(path.join(root.path, 'shader.GLSL'))..writeAsStringSync('shader');
|
|
|
|
final storedName = await ShaderAssetLoader.importCustomShader(source.path);
|
|
expect(storedName, matches(RegExp(r'^[0-9a-f-]+\.glsl$')));
|
|
expect(ShaderAssetLoader.isValidCustomShaderFileName(storedName), isTrue);
|
|
|
|
final shaders = await ShaderAssetLoader.getShadersForPreset(customPreset(storedName));
|
|
expect(shaders, hasLength(1));
|
|
expect(path.dirname(shaders.single), path.join(supportDirectory.path, 'custom_shaders'));
|
|
expect(await File(shaders.single).readAsString(), 'shader');
|
|
|
|
await ShaderAssetLoader.deleteCustomShader(storedName);
|
|
expect(File(shaders.single).existsSync(), isFalse);
|
|
expect(await source.readAsString(), 'shader');
|
|
});
|
|
|
|
test('legacy base-36 managed names remain loadable and deletable', () async {
|
|
const storedName = 'ks9p7.glsl';
|
|
final customDirectory = Directory(path.join(supportDirectory.path, 'custom_shaders'))..createSync(recursive: true);
|
|
final managedFile = File(path.join(customDirectory.path, storedName))..writeAsStringSync('legacy');
|
|
|
|
expect(ShaderAssetLoader.isValidCustomShaderFileName(storedName), isTrue);
|
|
expect(await ShaderAssetLoader.getShadersForPreset(customPreset(storedName)), [managedFile.path]);
|
|
|
|
await ShaderAssetLoader.deleteCustomShader(storedName);
|
|
expect(managedFile.existsSync(), isFalse);
|
|
});
|
|
}
|