Compare commits

...
1 Commits
Author SHA1 Message Date
Thomas SharedInboxandClaude Sonnet 4.6 5fc26057d7 test: cover _resolveDatabasePath retry logic to catch budget regressions (#167)
Issue #166 exposed that the retry budget in _resolveDatabasePath() was too
small for slow Android devices, but no unit test exercised that code path.

Expose two testing helpers (resolveDatabasePathForTesting /
resetDatabasePathForTesting) and add two new tests using fake_async:

- verifies the retry loop eventually succeeds after transient failures
  (a _SucceedAfterNPathProvider that fails N times then returns a path)
- verifies the function throws PlatformException with the right message
  after exhausting all retries

Both tests advance fake time instead of waiting real-world milliseconds,
so they run in < 1 ms each.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 18:29:30 +02:00
2 changed files with 97 additions and 0 deletions
+5
View File
@@ -616,6 +616,11 @@ Future<String> _resolveDatabasePath() async {
); );
} }
// These two functions are only called from unit tests (database_path_test.dart).
// They expose internals that cannot be reached via the public API.
Future<String> resolveDatabasePathForTesting() => _resolveDatabasePath();
void resetDatabasePathForTesting() => _dbPath = null;
LazyDatabase _openConnection() { LazyDatabase _openConnection() {
return LazyDatabase(() async { return LazyDatabase(() async {
final file = File(await _resolveDatabasePath()); final file = File(await _resolveDatabasePath());
+92
View File
@@ -1,3 +1,6 @@
import 'dart:async';
import 'package:fake_async/fake_async.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
@@ -19,6 +22,30 @@ class _UnavailablePathProvider extends Fake
} }
} }
// Fake PathProviderPlatform that fails the first [failCount] calls, then
// returns a fixed path. Used to exercise the retry loop in
// _resolveDatabasePath() without waiting for real timers.
class _SucceedAfterNPathProvider extends Fake
with MockPlatformInterfaceMixin
implements PathProviderPlatform {
_SucceedAfterNPathProvider({required this.failCount});
final int failCount;
int _callCount = 0;
@override
Future<String?> getApplicationSupportPath() async {
_callCount++;
if (_callCount <= failCount) {
throw PlatformException(
code: 'channel-error',
message: 'Simulated: path_provider channel not ready',
);
}
return '/tmp/test_app_support';
}
}
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@@ -38,4 +65,69 @@ void main() {
await expectLater(initDatabasePath(), completes); await expectLater(initDatabasePath(), completes);
}, },
); );
// Tests for _resolveDatabasePath() — the lazy retry path called on first DB
// access when initDatabasePath() already failed. fake_async lets us advance
// the back-off timers without waiting real-world milliseconds.
test(
'_resolveDatabasePath retries and eventually succeeds after transient failures',
() {
resetDatabasePathForTesting();
final prev = PathProviderPlatform.instance;
// Fail 3 times, succeed on the 4th call. The delays in
// _resolveDatabasePath are [200, 500, 1000, 2000, 4000] ms, so three
// failures cost 200+500+1000 = 1700 ms before the fourth attempt.
PathProviderPlatform.instance = _SucceedAfterNPathProvider(failCount: 3);
addTearDown(() {
PathProviderPlatform.instance = prev;
resetDatabasePathForTesting();
});
fakeAsync((fake) {
String? result;
unawaited(resolveDatabasePathForTesting().then((r) => result = r));
// Advance fake time through the three back-off delays.
fake.elapse(const Duration(milliseconds: 200 + 500 + 1000 + 1));
expect(result, isNotNull);
expect(result, endsWith('sharedinbox.db'));
});
},
);
test(
'_resolveDatabasePath throws PlatformException after exhausting all retries',
() {
resetDatabasePathForTesting();
final prev = PathProviderPlatform.instance;
PathProviderPlatform.instance = _UnavailablePathProvider();
addTearDown(() {
PathProviderPlatform.instance = prev;
resetDatabasePathForTesting();
});
fakeAsync((fake) {
Object? caughtError;
unawaited(
resolveDatabasePathForTesting().catchError((Object e) {
caughtError = e;
return ''; // ignored; satisfies the Future<String> return type
}),
);
// Advance past all five back-off delays: 200+500+1000+2000+4000 ms.
fake.elapse(
const Duration(milliseconds: 200 + 500 + 1000 + 2000 + 4000 + 1),
);
expect(caughtError, isA<PlatformException>());
expect(
(caughtError! as PlatformException).message,
contains('cannot open database'),
);
});
},
);
} }