@@ -14,6 +14,7 @@ import 'package:path_provider_platform_interface/path_provider_platform_interfac
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/database/download_operations.dart';
|
||||
import 'package:plezy/exceptions/media_server_exceptions.dart';
|
||||
import 'package:plezy/i18n/strings.g.dart';
|
||||
import 'package:plezy/media/download_resolution.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
@@ -1288,6 +1289,60 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('storage exhaustion', () {
|
||||
test('filesystem-full failure stops every active download and clears the queue', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
addTearDown(db.close);
|
||||
const currentKey = 'srv:item-1';
|
||||
const queuedKey = 'srv:item-2';
|
||||
|
||||
await db.insertDownload(
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'item-1',
|
||||
globalKey: currentKey,
|
||||
type: 'movie',
|
||||
status: DownloadStatus.downloading.index,
|
||||
);
|
||||
await db.updateBgTaskId(currentKey, 'current-task');
|
||||
await db.addToQueue(mediaGlobalKey: currentKey);
|
||||
await db.insertDownload(
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'item-2',
|
||||
globalKey: queuedKey,
|
||||
type: 'movie',
|
||||
status: DownloadStatus.queued.index,
|
||||
);
|
||||
await db.updateBgTaskId(queuedKey, 'queued-task');
|
||||
await db.addToQueue(mediaGlobalKey: queuedKey);
|
||||
|
||||
final manager = DownloadManagerService(
|
||||
database: db,
|
||||
storageService: DownloadStorageService.instance,
|
||||
clientResolver: (serverId, {clientScopeId}) => null,
|
||||
downloadsSupportedOverride: false,
|
||||
);
|
||||
addTearDown(manager.dispose);
|
||||
|
||||
await manager.debugHandleTaskStatus(
|
||||
TaskStatusUpdate(
|
||||
_downloadTask('current-task', currentKey),
|
||||
TaskStatus.failed,
|
||||
TaskFileSystemException('write failed: ENOSPC (No space left on device)'),
|
||||
),
|
||||
);
|
||||
|
||||
final current = await db.getDownloadedMedia(currentKey);
|
||||
final queued = await db.getDownloadedMedia(queuedKey);
|
||||
expect(current?.status, DownloadStatus.failed.index);
|
||||
expect(queued?.status, DownloadStatus.failed.index);
|
||||
expect(current?.bgTaskId, isNull);
|
||||
expect(queued?.bgTaskId, isNull);
|
||||
expect(current?.errorMessage, t.downloads.storageFull);
|
||||
expect(queued?.errorMessage, t.downloads.storageFull);
|
||||
expect(await db.select(db.downloadQueue).get(), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
group('task session validation', () {
|
||||
test('ignores progress from stale native task ids', () async {
|
||||
final db = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/database/download_operations.dart';
|
||||
import 'package:plezy/database/tvos_database_recovery_store.dart';
|
||||
import 'package:plezy/main.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/models/download_models.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('renders a Flutter frame before starting the initialization gate', (tester) async {
|
||||
@@ -104,4 +111,57 @@ void main() {
|
||||
expect(discarded, [9]);
|
||||
expect(find.text('ready 9'), findsNothing);
|
||||
});
|
||||
|
||||
test('storage-full database open discards native work before retrying', () async {
|
||||
final database = AppDatabase.forTesting(NativeDatabase.memory());
|
||||
addTearDown(database.close);
|
||||
await database.insertDownload(
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'active',
|
||||
globalKey: 'srv:active',
|
||||
type: 'movie',
|
||||
status: DownloadStatus.downloading.index,
|
||||
);
|
||||
await database.updateBgTaskId('srv:active', 'native-task');
|
||||
await database.addToQueue(mediaGlobalKey: 'srv:active');
|
||||
await database.insertDownload(
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'complete',
|
||||
globalKey: 'srv:complete',
|
||||
type: 'movie',
|
||||
status: DownloadStatus.completed.index,
|
||||
);
|
||||
|
||||
var openAttempts = 0;
|
||||
var recoveries = 0;
|
||||
final bootstrap = AppDatabaseBootstrap(
|
||||
database: database,
|
||||
recoveryOutcome: TvosDatabaseRecoveryOutcome.notApplicable,
|
||||
);
|
||||
|
||||
final result = await openAppDatabaseWithDownloadRecovery(
|
||||
openDatabase: () async {
|
||||
openAttempts++;
|
||||
if (openAttempts == 1) {
|
||||
throw const FileSystemException('write failed: No space left on device');
|
||||
}
|
||||
return bootstrap;
|
||||
},
|
||||
recoverNativeDownloads: () async {
|
||||
recoveries++;
|
||||
},
|
||||
storageFullMessage: 'Storage full',
|
||||
);
|
||||
|
||||
final active = await database.getDownloadedMedia('srv:active');
|
||||
final complete = await database.getDownloadedMedia('srv:complete');
|
||||
expect(result, same(bootstrap));
|
||||
expect(openAttempts, 2);
|
||||
expect(recoveries, 1);
|
||||
expect(active?.status, DownloadStatus.failed.index);
|
||||
expect(active?.bgTaskId, isNull);
|
||||
expect(active?.errorMessage, 'Storage full');
|
||||
expect(complete?.status, DownloadStatus.completed.index);
|
||||
expect(await database.select(database.downloadQueue).get(), isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user