обновленная синхронизация клиента
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import 'dart:async';
|
||||
// import 'dart:collection';
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
// import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart';
|
||||
import 'package:moonwell_launcher/features/downloader/domain/entities/download_progress.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/application/client_sync_use_case.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/data/game_installation_service.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/data/launcher_api_client.dart';
|
||||
@@ -10,140 +11,265 @@ import 'package:moonwell_launcher/features/launcher/data/launcher_log_service.da
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/client_installation_snapshot.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/client_manifest.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/client_manifest_file.dart';
|
||||
// import 'package:moonwell_launcher/features/launcher/domain/entities/client_sync_status.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/client_sync_status.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/launcher_exception.dart';
|
||||
|
||||
// import 'package:moonwell_launcher/features/launcher/domain/entities/local_client_file.dart';
|
||||
import 'package:moonwell_launcher/features/launcher/domain/entities/local_client_file.dart';
|
||||
|
||||
void main() {
|
||||
group('ClientSyncUseCase', () {
|
||||
// test(
|
||||
// 'completes without downloads when local snapshot matches manifest',
|
||||
// () async {
|
||||
// final manifest = _manifestFromFiles([
|
||||
// const ClientManifestFile(
|
||||
// path: 'Wow.exe',
|
||||
// size: 5,
|
||||
// sha256: 'wow-hash',
|
||||
// ),
|
||||
// const ClientManifestFile(
|
||||
// path: 'Data/common.MPQ',
|
||||
// size: 10,
|
||||
// sha256: 'data-hash',
|
||||
// ),
|
||||
// ]);
|
||||
// final matchingSnapshot = ClientInstallationSnapshot.fromFiles([
|
||||
// const LocalClientFile(path: 'Wow.exe', size: 5, sha256: 'wow-hash'),
|
||||
// const LocalClientFile(
|
||||
// path: 'Data/common.MPQ',
|
||||
// size: 10,
|
||||
// sha256: 'data-hash',
|
||||
// ),
|
||||
// ]);
|
||||
test(
|
||||
'completes without downloads when local snapshot matches manifest',
|
||||
() async {
|
||||
final manifest = _manifestFromFiles([
|
||||
const ClientManifestFile(
|
||||
path: 'MoonWell.exe',
|
||||
size: 5,
|
||||
sha256: 'wow-hash',
|
||||
),
|
||||
const ClientManifestFile(
|
||||
path: 'Data/common.MPQ',
|
||||
size: 10,
|
||||
sha256: 'data-hash',
|
||||
),
|
||||
]);
|
||||
final matchingSnapshot = ClientInstallationSnapshot.fromFiles([
|
||||
const LocalClientFile(
|
||||
path: 'MoonWell.exe',
|
||||
size: 5,
|
||||
sha256: 'wow-hash',
|
||||
),
|
||||
const LocalClientFile(
|
||||
path: 'Data/common.MPQ',
|
||||
size: 10,
|
||||
sha256: 'data-hash',
|
||||
),
|
||||
]);
|
||||
|
||||
// final api = _FakeLauncherApiClient(manifest);
|
||||
// final installationService = _FakeGameInstallationService([
|
||||
// matchingSnapshot,
|
||||
// ]);
|
||||
// final logger = _FakeLauncherLogService();
|
||||
// final useCase = ClientSyncUseCase(
|
||||
// launcherApiClient: api,
|
||||
// installationService: installationService,
|
||||
// launcherLogService: logger,
|
||||
// );
|
||||
final api = _FakeLauncherApiClient(manifest);
|
||||
final installationService = _FakeGameInstallationService([
|
||||
matchingSnapshot,
|
||||
]);
|
||||
final logger = _FakeLauncherLogService();
|
||||
final useCase = ClientSyncUseCase(
|
||||
launcherApiClient: api,
|
||||
installationService: installationService,
|
||||
launcherLogService: logger,
|
||||
);
|
||||
|
||||
// final statuses = await useCase
|
||||
// .call(
|
||||
// ClientSyncUseCaseInput(
|
||||
// request: ClientSyncRequest(
|
||||
// installationDir: 'C:/MoonWell',
|
||||
// accessToken: 'token',
|
||||
// manifest: manifest,
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// .toList();
|
||||
final statuses = await useCase
|
||||
.call(
|
||||
ClientSyncUseCaseInput(
|
||||
request: ClientSyncRequest(
|
||||
installationDir: 'C:/MoonWell',
|
||||
accessToken: 'token',
|
||||
manifest: manifest,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
// expect(statuses.last.stage, ClientSyncStage.completed);
|
||||
// expect(statuses.last.message, 'Клиент обновлён.');
|
||||
// expect(api.downloadedPaths, isEmpty);
|
||||
// expect(installationService.deletedRelativeFiles, isEmpty);
|
||||
// expect(logger.errorMessages, isEmpty);
|
||||
// },
|
||||
// );
|
||||
expect(statuses.last.stage, ClientSyncStage.completed);
|
||||
expect(statuses.last.message, 'Клиент обновлён.');
|
||||
expect(api.downloadedPaths, isEmpty);
|
||||
expect(installationService.deletedRelativeFiles, isEmpty);
|
||||
expect(logger.errorMessages, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
// test(
|
||||
// 'removes stale files and downloads changed files before final verify',
|
||||
// () async {
|
||||
// final manifest = _manifestFromFiles([
|
||||
// const ClientManifestFile(
|
||||
// path: 'Wow.exe',
|
||||
// size: 5,
|
||||
// sha256: 'wow-hash',
|
||||
// ),
|
||||
// const ClientManifestFile(
|
||||
// path: 'Data/common.MPQ',
|
||||
// size: 10,
|
||||
// sha256: 'data-hash',
|
||||
// ),
|
||||
// ]);
|
||||
// final initialSnapshot = ClientInstallationSnapshot.fromFiles([
|
||||
// const LocalClientFile(path: 'Wow.exe', size: 5, sha256: 'old-wow'),
|
||||
// const LocalClientFile(path: 'legacy.txt', size: 2, sha256: 'legacy'),
|
||||
// ]);
|
||||
// final verifiedSnapshot = ClientInstallationSnapshot.fromFiles([
|
||||
// const LocalClientFile(path: 'Wow.exe', size: 5, sha256: 'wow-hash'),
|
||||
// const LocalClientFile(
|
||||
// path: 'Data/common.MPQ',
|
||||
// size: 10,
|
||||
// sha256: 'data-hash',
|
||||
// ),
|
||||
// ]);
|
||||
test(
|
||||
'removes stale files and downloads changed files before final verify',
|
||||
() async {
|
||||
final manifest = _manifestFromFiles([
|
||||
const ClientManifestFile(
|
||||
path: 'MoonWell.exe',
|
||||
size: 5,
|
||||
sha256: 'wow-hash',
|
||||
),
|
||||
const ClientManifestFile(
|
||||
path: 'Data/common.MPQ',
|
||||
size: 10,
|
||||
sha256: 'data-hash',
|
||||
),
|
||||
]);
|
||||
final initialSnapshot = ClientInstallationSnapshot.fromFiles(
|
||||
[
|
||||
const LocalClientFile(
|
||||
path: 'MoonWell.exe',
|
||||
size: 5,
|
||||
sha256: 'old-wow',
|
||||
),
|
||||
],
|
||||
stalePaths: ['Data/legacy.MPQ', 'WoW.exe', 'old.dll'],
|
||||
);
|
||||
final verifiedSnapshot = ClientInstallationSnapshot.fromFiles([
|
||||
const LocalClientFile(
|
||||
path: 'MoonWell.exe',
|
||||
size: 5,
|
||||
sha256: 'wow-hash',
|
||||
),
|
||||
const LocalClientFile(
|
||||
path: 'Data/common.MPQ',
|
||||
size: 10,
|
||||
sha256: 'data-hash',
|
||||
),
|
||||
]);
|
||||
|
||||
// final api = _FakeLauncherApiClient(manifest);
|
||||
// final installationService =
|
||||
// _FakeGameInstallationService([initialSnapshot, verifiedSnapshot])
|
||||
// ..fileSizesByPath['C:/MoonWell/Wow.exe.moonwell.part'] = 5
|
||||
// ..computedHashesByPath['C:/MoonWell/Wow.exe.moonwell.part'] =
|
||||
// 'wow-hash'
|
||||
// ..fileSizesByPath['C:/MoonWell/Data/common.MPQ.moonwell.part'] =
|
||||
// 10
|
||||
// ..computedHashesByPath['C:/MoonWell/Data/common.MPQ.moonwell.part'] =
|
||||
// 'data-hash';
|
||||
// final logger = _FakeLauncherLogService();
|
||||
// final useCase = ClientSyncUseCase(
|
||||
// launcherApiClient: api,
|
||||
// installationService: installationService,
|
||||
// launcherLogService: logger,
|
||||
// );
|
||||
final api = _FakeLauncherApiClient(manifest);
|
||||
final installationService =
|
||||
_FakeGameInstallationService([initialSnapshot, verifiedSnapshot])
|
||||
..fileSizesByPath['C:/MoonWell/MoonWell.exe.moonwell.part'] = 5
|
||||
..computedHashesByPath['C:/MoonWell/MoonWell.exe.moonwell.part'] =
|
||||
'wow-hash'
|
||||
..fileSizesByPath['C:/MoonWell/Data/common.MPQ.moonwell.part'] =
|
||||
10
|
||||
..computedHashesByPath['C:/MoonWell/Data/common.MPQ.moonwell.part'] =
|
||||
'data-hash';
|
||||
final logger = _FakeLauncherLogService();
|
||||
final useCase = ClientSyncUseCase(
|
||||
launcherApiClient: api,
|
||||
installationService: installationService,
|
||||
launcherLogService: logger,
|
||||
);
|
||||
|
||||
// final statuses = await useCase
|
||||
// .call(
|
||||
// ClientSyncUseCaseInput(
|
||||
// request: ClientSyncRequest(
|
||||
// installationDir: 'C:/MoonWell',
|
||||
// accessToken: 'token',
|
||||
// manifest: manifest,
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// .toList();
|
||||
final statuses = await useCase
|
||||
.call(
|
||||
ClientSyncUseCaseInput(
|
||||
request: ClientSyncRequest(
|
||||
installationDir: 'C:/MoonWell',
|
||||
accessToken: 'token',
|
||||
manifest: manifest,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
// expect(
|
||||
// statuses.map((status) => status.stage),
|
||||
// containsAll([
|
||||
// ClientSyncStage.removingStaleFiles,
|
||||
// ClientSyncStage.downloadingFiles,
|
||||
// ClientSyncStage.verifyingInstallation,
|
||||
// ClientSyncStage.completed,
|
||||
// ]),
|
||||
// );
|
||||
// expect(installationService.deletedRelativeFiles, ['legacy.txt']);
|
||||
// expect(api.downloadedPaths, ['Wow.exe', 'Data/common.MPQ']);
|
||||
// expect(statuses.last.message, 'Клиент готов к игре.');
|
||||
// expect(logger.errorMessages, isEmpty);
|
||||
// },
|
||||
// );
|
||||
expect(
|
||||
statuses.map((status) => status.stage),
|
||||
containsAll([
|
||||
ClientSyncStage.removingStaleFiles,
|
||||
ClientSyncStage.downloadingFiles,
|
||||
ClientSyncStage.verifyingInstallation,
|
||||
ClientSyncStage.completed,
|
||||
]),
|
||||
);
|
||||
expect(installationService.deletedRelativeFiles, [
|
||||
'Data/legacy.MPQ',
|
||||
'WoW.exe',
|
||||
'old.dll',
|
||||
]);
|
||||
expect(api.downloadedPaths, ['MoonWell.exe', 'Data/common.MPQ']);
|
||||
expect(statuses.last.message, 'Клиент готов к игре.');
|
||||
expect(logger.errorMessages, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('cleans an up-to-date installation without downloading', () async {
|
||||
final root = await Directory.systemTemp.createTemp('moonwell_sync_');
|
||||
addTearDown(() => root.delete(recursive: true));
|
||||
final service = GameInstallationService();
|
||||
final retained = ['MoonWell.exe', 'Data/common.MPQ', 'required.dll'];
|
||||
final obsolete = ['Data/old/patch.MPQ', 'WoW.exe', 'unused.DLL'];
|
||||
for (final path in [
|
||||
...retained,
|
||||
...obsolete,
|
||||
'WTF/Config.wtf',
|
||||
'realmlist.wtf',
|
||||
]) {
|
||||
final file = File(service.resolveClientPath(root.path, path));
|
||||
await file.parent.create(recursive: true);
|
||||
await file.writeAsString(path);
|
||||
}
|
||||
final entries = <ClientManifestFile>[];
|
||||
for (final path in retained) {
|
||||
final filePath = service.resolveClientPath(root.path, path);
|
||||
entries.add(
|
||||
ClientManifestFile(
|
||||
path: path,
|
||||
size: (await File(filePath).stat()).size,
|
||||
sha256: await service.computeSha256(filePath),
|
||||
),
|
||||
);
|
||||
}
|
||||
final manifest = _manifestFromFiles(entries);
|
||||
final api = _FakeLauncherApiClient(manifest);
|
||||
final useCase = ClientSyncUseCase(
|
||||
launcherApiClient: api,
|
||||
installationService: service,
|
||||
launcherLogService: _FakeLauncherLogService(),
|
||||
);
|
||||
final statuses = await useCase
|
||||
.call(
|
||||
ClientSyncUseCaseInput(
|
||||
request: ClientSyncRequest(
|
||||
installationDir: root.path,
|
||||
accessToken: 'token',
|
||||
manifest: manifest,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
expect(statuses.last.stage, ClientSyncStage.completed);
|
||||
expect(statuses.last.localBuildHash, manifest.buildHash);
|
||||
expect(
|
||||
statuses.map((status) => status.stage),
|
||||
contains(ClientSyncStage.removingStaleFiles),
|
||||
);
|
||||
expect(api.downloadedPaths, isEmpty);
|
||||
for (final path in [...obsolete, 'Data/old']) {
|
||||
expect(
|
||||
await FileSystemEntity.type(
|
||||
service.resolveClientPath(root.path, path),
|
||||
followLinks: false,
|
||||
),
|
||||
FileSystemEntityType.notFound,
|
||||
reason: path,
|
||||
);
|
||||
}
|
||||
for (final path in [...retained, 'WTF/Config.wtf', 'realmlist.wtf']) {
|
||||
expect(
|
||||
await File(service.resolveClientPath(root.path, path)).readAsString(),
|
||||
path,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('fails final verification when stale files remain', () async {
|
||||
final manifest = _manifestFromFiles([
|
||||
const ClientManifestFile(path: 'MoonWell.exe', size: 5, sha256: 'hash'),
|
||||
]);
|
||||
final snapshot = ClientInstallationSnapshot.fromFiles(
|
||||
[const LocalClientFile(path: 'MoonWell.exe', size: 5, sha256: 'hash')],
|
||||
stalePaths: ['Data/extra.MPQ'],
|
||||
);
|
||||
final service = _FakeGameInstallationService([snapshot, snapshot]);
|
||||
final useCase = ClientSyncUseCase(
|
||||
launcherApiClient: _FakeLauncherApiClient(manifest),
|
||||
installationService: service,
|
||||
launcherLogService: _FakeLauncherLogService(),
|
||||
);
|
||||
await expectLater(
|
||||
useCase
|
||||
.call(
|
||||
ClientSyncUseCaseInput(
|
||||
request: ClientSyncRequest(
|
||||
installationDir: 'C:/MoonWell',
|
||||
accessToken: 'token',
|
||||
manifest: manifest,
|
||||
),
|
||||
),
|
||||
)
|
||||
.drain<void>(),
|
||||
throwsA(
|
||||
isA<LauncherSyncException>().having(
|
||||
(error) => error.message,
|
||||
'message',
|
||||
contains('Data/extra.MPQ'),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'preserves failed temporary file and returns a log hint on verification mismatch',
|
||||
@@ -244,7 +370,10 @@ class _FakeLauncherApiClient extends LauncherApiClient {
|
||||
}
|
||||
|
||||
class _FakeGameInstallationService extends GameInstallationService {
|
||||
_FakeGameInstallationService(List<ClientInstallationSnapshot> snapshots);
|
||||
_FakeGameInstallationService(List<ClientInstallationSnapshot> snapshots)
|
||||
: _snapshots = Queue.of(snapshots);
|
||||
|
||||
final Queue<ClientInstallationSnapshot> _snapshots;
|
||||
|
||||
final List<String> ensuredParentDirectories = [];
|
||||
final List<String> deletedRelativeFiles = [];
|
||||
@@ -254,34 +383,35 @@ class _FakeGameInstallationService extends GameInstallationService {
|
||||
final Map<String, String> computedHashesByPath = {};
|
||||
final Map<String, int?> fileSizesByPath = {};
|
||||
|
||||
// @override
|
||||
// Future<ClientInstallationSnapshot> scanInstallation(
|
||||
// String installationDir, {
|
||||
// InstallationScanProgressCallback? onProgress,
|
||||
// FutureOr<bool> Function()? isCancelled,
|
||||
// }) async {
|
||||
// final snapshot = _snapshots.removeFirst();
|
||||
// var processedFiles = 0;
|
||||
// var processedBytes = 0;
|
||||
@override
|
||||
Future<ClientInstallationSnapshot> scanInstallation(
|
||||
String installationDir, {
|
||||
required ClientManifest manifest,
|
||||
InstallationScanProgressCallback? onProgress,
|
||||
FutureOr<bool> Function()? isCancelled,
|
||||
}) async {
|
||||
final snapshot = _snapshots.removeFirst();
|
||||
var processedFiles = 0;
|
||||
var processedBytes = 0;
|
||||
|
||||
// for (final file in snapshot.files) {
|
||||
// processedFiles += 1;
|
||||
// processedBytes += file.size;
|
||||
// onProgress?.call(
|
||||
// DownloadProgress(
|
||||
// speed: 0,
|
||||
// downloaded: processedBytes,
|
||||
// total: snapshot.files.fold<int>(0, (sum, item) => sum + item.size),
|
||||
// eta: Duration.zero,
|
||||
// ),
|
||||
// file.path,
|
||||
// processedFiles,
|
||||
// snapshot.files.length,
|
||||
// );
|
||||
// }
|
||||
for (final file in snapshot.files) {
|
||||
processedFiles += 1;
|
||||
processedBytes += file.size;
|
||||
onProgress?.call(
|
||||
DownloadProgress(
|
||||
speed: 0,
|
||||
downloaded: processedBytes,
|
||||
total: snapshot.files.fold<int>(0, (sum, item) => sum + item.size),
|
||||
eta: Duration.zero,
|
||||
),
|
||||
file.path,
|
||||
processedFiles,
|
||||
snapshot.files.length,
|
||||
);
|
||||
}
|
||||
|
||||
// return snapshot;
|
||||
// }
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteRelativeFile(
|
||||
|
||||
Reference in New Issue
Block a user