blob: 3ceda792b29a55390c31bd2bf07ed41129c284b6 [file] [log] [blame]
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:async/async.dart';
import 'package:fake_async/fake_async.dart';
import 'package:test/test.dart';
void main() {
late AsyncCache<String> cache;
setUp(() {
// Create a cache that is fresh for an hour.
cache = AsyncCache(const Duration(hours: 1));
});
test('should fetch via a callback when no cache exists', () async {
expect(await cache.fetch(() async => 'Expensive'), 'Expensive');
});
test('should not fetch via callback when a cache exists', () async {
await cache.fetch(() async => 'Expensive');
expect(await cache.fetch(expectAsync0(() async => 'fake', count: 0)),
'Expensive');
});
test('should not fetch via callback when a future is in-flight', () async {
// No actual caching is done, just avoid duplicate requests.
cache = AsyncCache.ephemeral();
var completer = Completer<String>();
expect(cache.fetch(() => completer.future), completion('Expensive'));
expect(cache.fetch(expectAsync0(() async => 'fake', count: 0)),
completion('Expensive'));
completer.complete('Expensive');
});
test('should fetch via a callback again when cache expires', () {
FakeAsync().run((fakeAsync) async {
var timesCalled = 0;
Future<String> call() async => 'Called ${++timesCalled}';
expect(await cache.fetch(call), 'Called 1');
expect(await cache.fetch(call), 'Called 1', reason: 'Cache still fresh');
fakeAsync.elapse(const Duration(hours: 1) - const Duration(seconds: 1));
expect(await cache.fetch(call), 'Called 1', reason: 'Cache still fresh');
fakeAsync.elapse(const Duration(seconds: 1));
expect(await cache.fetch(call), 'Called 2');
expect(await cache.fetch(call), 'Called 2', reason: 'Cache fresh again');
fakeAsync.elapse(const Duration(hours: 1));
expect(await cache.fetch(call), 'Called 3');
});
});
test('should fetch via a callback when manually invalidated', () async {
var timesCalled = 0;
Future<String> call() async => 'Called ${++timesCalled}';
expect(await cache.fetch(call), 'Called 1');
cache.invalidate();
expect(await cache.fetch(call), 'Called 2');
cache.invalidate();
expect(await cache.fetch(call), 'Called 3');
});
test('should fetch a stream via a callback', () async {
expect(
await cache.fetchStream(expectAsync0(() {
return Stream.fromIterable(['1', '2', '3']);
})).toList(),
['1', '2', '3']);
});
test('should not fetch stream via callback when a cache exists', () async {
await cache.fetchStream(() async* {
yield '1';
yield '2';
yield '3';
}).toList();
expect(
await cache
.fetchStream(expectAsync0(() => Stream.empty(), count: 0))
.toList(),
['1', '2', '3']);
});
test('should not fetch stream via callback when request in flight', () async {
// Unlike the above test, we want to verify that we don't make multiple
// calls if a cache is being filled currently, and instead wait for that
// cache to be completed.
var controller = StreamController<String>();
Stream<String> call() => controller.stream;
expect(cache.fetchStream(call).toList(), completion(['1', '2', '3']));
controller.add('1');
controller.add('2');
await Future.value();
expect(cache.fetchStream(call).toList(), completion(['1', '2', '3']));
controller.add('3');
await controller.close();
});
test('should fetch stream via a callback again when cache expires', () {
FakeAsync().run((fakeAsync) async {
var timesCalled = 0;
Stream<String> call() {
return Stream.fromIterable(['Called ${++timesCalled}']);
}
expect(await cache.fetchStream(call).toList(), ['Called 1']);
expect(await cache.fetchStream(call).toList(), ['Called 1'],
reason: 'Cache still fresh');
fakeAsync.elapse(const Duration(hours: 1) - const Duration(seconds: 1));
expect(await cache.fetchStream(call).toList(), ['Called 1'],
reason: 'Cache still fresh');
fakeAsync.elapse(const Duration(seconds: 1));
expect(await cache.fetchStream(call).toList(), ['Called 2']);
expect(await cache.fetchStream(call).toList(), ['Called 2'],
reason: 'Cache fresh again');
fakeAsync.elapse(const Duration(hours: 1));
expect(await cache.fetchStream(call).toList(), ['Called 3']);
});
});
test('should fetch via a callback when manually invalidated', () async {
var timesCalled = 0;
Stream<String> call() {
return Stream.fromIterable(['Called ${++timesCalled}']);
}
expect(await cache.fetchStream(call).toList(), ['Called 1']);
cache.invalidate();
expect(await cache.fetchStream(call).toList(), ['Called 2']);
cache.invalidate();
expect(await cache.fetchStream(call).toList(), ['Called 3']);
});
test('should cancel a cached stream without affecting others', () async {
Stream<String> call() => Stream.fromIterable(['1', '2', '3']);
expect(cache.fetchStream(call).toList(), completion(['1', '2', '3']));
// Listens to the stream for the initial value, then cancels subscription.
expect(await cache.fetchStream(call).first, '1');
});
test('should pause a cached stream without affecting others', () async {
Stream<String> call() => Stream.fromIterable(['1', '2', '3']);
late StreamSubscription sub;
sub = cache.fetchStream(call).listen(expectAsync1((event) {
if (event == '1') sub.pause();
}));
expect(cache.fetchStream(call).toList(), completion(['1', '2', '3']));
});
test('should invalidate even if the future throws an exception', () async {
cache = AsyncCache.ephemeral();
Future<String> throwingCall() async => throw Exception();
await expectLater(cache.fetch(throwingCall), throwsA(isException));
// To let the timer invalidate the cache
await Future.delayed(Duration(milliseconds: 5));
Future<String> call() async => 'Completed';
expect(await cache.fetch(call), 'Completed', reason: 'Cache invalidates');
});
}