Fix flaky image_store_io_test "without picker cop…
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
repo-contract / repo contract (push) Waiting to run

Todo: 2961b5ff
This commit is contained in:
Krzysztof Rudnicki 2026-11-09 08:37:02 +01:00
parent 75788c7bdb
commit 9a8e7aefd2
3 changed files with 91 additions and 8 deletions

View File

@ -132,10 +132,16 @@ class ImageCacheStore {
for (final ref in pending()) {
final file = fileFor(ref);
if (!file.existsSync()) continue;
final length = await file.length();
if (await client.upload(ref, file.openRead(), length)) {
await _marker(ref).delete();
uploaded++;
try {
final length = await file.length();
if (await client.upload(ref, file.openRead(), length)) {
await _marker(ref).delete();
uploaded++;
}
} on FileSystemException {
// Deleted mid-pass (note removed, cache cleared): nothing left to
// upload. A surviving marker is skipped by the check above next time.
continue;
}
}
for (final ref in pass.referenced) {

View File

@ -36,14 +36,19 @@ void main() {
installFakeSecureStorage(initial: {'dufsImagesPassword': 'pw'});
store = make();
});
tearDown(() => root.deleteSync(recursive: true));
tearDown(() async {
// attach/attachFile start an upload pass they do not await; let it end
// before its directory goes, or it races the delete.
await store.reconcile(const []);
root.deleteSync(recursive: true);
});
test('attach stores, notifies and uploads straight away', () async {
final ref = (await store.attach('n1', Uint8List.fromList([0xFF, 1])))!;
expect(ref.noteId, 'n1');
expect(store.sourceFor(ref), isA<LocalImageSource>());
expect(notified, greaterThanOrEqualTo(1));
await pumpEventQueue();
await store.reconcile(const []); // drains the pass attach queued
expect(dufs.files[ref.relativePath], [0xFF, 1]);
expect(store.isPending(ref), isFalse);
});
@ -67,7 +72,7 @@ void main() {
test('without a password attach queues offline', () async {
installFakeSecureStorage();
final ref = (await store.attach('n1', Uint8List.fromList([0xFF])))!;
await pumpEventQueue();
await store.reconcile(const []); // drains the pass attach queued
expect(dufs.requests, isEmpty);
expect(store.isPending(ref), isTrue);
expect((await store.status()).configured, isFalse);
@ -114,7 +119,7 @@ void main() {
expect(a.isImage, isFalse);
expect(notified, greaterThanOrEqualTo(1));
expect(await store.sizeOf(a.ref), 2);
await pumpEventQueue();
await store.reconcile(const []); // drains the pass attach queued
expect(dufs.files[a.ref.relativePath], [1, 2]);
expect(File(p.join(elsewhere.path, 'r.pdf')).existsSync(), isTrue);
});
@ -149,6 +154,9 @@ void main() {
final picked = file(picks, 'kept.txt', [1]);
await plain.attachFile('n1', picked);
expect(File(picked.path).existsSync(), isTrue);
// Drain the upload pass attachFile started, so teardown cannot delete
// the root underneath it.
await plain.reconcile(const []);
});
});

View File

@ -0,0 +1,69 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:todo/images/attachment_ref.dart';
import 'package:todo/images/image_cache_store.dart';
import 'fake_dufs.dart';
/// A file that `existsSync()` still sees but that is gone by the time its
/// length is read: the window a teardown or a note deletion can hit while an
/// upload pass is between the two calls.
class _VanishingFile implements File {
_VanishingFile(this.path);
@override
final String path;
@override
bool existsSync() => true;
@override
Future<int> length() async =>
throw PathNotFoundException(path, const OSError('gone', 2));
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
/// Hands out a [_VanishingFile] for exactly one path, real files otherwise.
final class _VanishOverrides extends IOOverrides {
_VanishOverrides(this.vanishing);
final String vanishing;
@override
File createFile(String path) =>
path == vanishing ? _VanishingFile(path) : super.createFile(path);
}
void main() {
late Directory root;
late ImageCacheStore store;
late FakeDufs dufs;
final a = AttachmentRef.tryParse('n1/a.jpg')!;
final b = AttachmentRef.tryParse('n2/b.jpg')!;
final bytes = Uint8List.fromList([1, 2, 3]);
setUp(() {
root = Directory.systemTemp.createTempSync('image_cache_vanish');
store = ImageCacheStore(root);
dufs = FakeDufs();
});
tearDown(() => root.deleteSync(recursive: true));
test('a file vanishing mid-pass is skipped; the rest still upload', () async {
await store.put(a, bytes);
await store.put(b, bytes);
final report = await IOOverrides.runWithIOOverrides(
() => store.reconcile({}, dufs.client()),
_VanishOverrides(store.fileFor(a).path),
);
expect(report.uploaded, 1);
expect(dufs.files.keys, ['n2/b.jpg']);
expect(store.isPending(a), isTrue);
expect(store.isPending(b), isFalse);
});
}