Files
plezy/test/services/shader_asset_loader_test.dart
T
edde746 83f4e2a263 refactor: fold single-use helpers into their call sites
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.
2026-07-26 06:09:49 +02:00

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);
});
}