| // 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'])); |
| }); |
| } |