blob: d2d33106596e490862c2b4b5ce033ea73a558c3f [file] [log] [blame]
// Copyright 2019 The Flutter Authors. 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 'dart:typed_data';
import 'package:cocoon_service/src/service/cache_service.dart';
import 'package:mockito/mockito.dart';
import 'package:neat_cache/neat_cache.dart';
import 'package:test/test.dart';
import '../src/utilities/mocks.dart';
void main() {
group('CacheService', () {
late CacheService cache;
const String testSubcacheName = 'test';
setUp(() {
cache = CacheService(inMemory: true, inMemoryMaxNumberEntries: 1);
});
test('returns null when no value exists', () async {
final Uint8List? value = await cache.getOrCreate(
testSubcacheName,
'abc',
createFn: null,
);
expect(value, isNull);
});
test('returns value when it exists', () async {
const String testKey = 'abc';
final Uint8List expectedValue = Uint8List.fromList('123'.codeUnits);
await cache.set(testSubcacheName, testKey, expectedValue);
final Uint8List? value = await cache.getOrCreate(
testSubcacheName,
testKey,
createFn: null,
);
expect(value, expectedValue);
});
test('last used value is rotated out of cache if cache is full', () async {
const String testKey1 = 'abc';
const String testKey2 = 'def';
final Uint8List expectedValue1 = Uint8List.fromList('123'.codeUnits);
final Uint8List expectedValue2 = Uint8List.fromList('456'.codeUnits);
await cache.set(testSubcacheName, testKey1, expectedValue1);
await cache.set(testSubcacheName, testKey2, expectedValue2);
final Uint8List? value1 = await cache.getOrCreate(
testSubcacheName,
testKey1,
createFn: null,
);
expect(value1, null);
final Uint8List? value2 = await cache.getOrCreate(
testSubcacheName,
testKey2,
createFn: null,
);
expect(value2, expectedValue2);
});
test('retries when get throws exception', () async {
final Cache<Uint8List> mockMainCache = MockCache();
final Cache<Uint8List> mockTestSubcache = MockCache();
when<Cache<Uint8List>>(mockMainCache.withPrefix(testSubcacheName)).thenReturn(mockTestSubcache);
int getCallCount = 0;
final Entry<Uint8List> entry = FakeEntry();
// Only on the first call do we want it to throw the exception.
when(mockTestSubcache['does not matter']).thenAnswer(
(Invocation invocation) => getCallCount++ < 1 ? throw Exception('simulate stream sink error') : entry,
);
cache.cacheValue = mockMainCache;
final Uint8List? value = await cache.getOrCreate(
testSubcacheName,
'does not matter',
createFn: null,
);
verify(mockTestSubcache['does not matter']).called(2);
expect(value, Uint8List.fromList('abc123'.codeUnits));
});
test('returns null if reaches max attempts of retries', () async {
final Cache<Uint8List> mockMainCache = MockCache();
final Cache<Uint8List> mockTestSubcache = MockCache();
when<Cache<Uint8List>>(mockMainCache.withPrefix(testSubcacheName)).thenReturn(mockTestSubcache);
int getCallCount = 0;
final Entry<Uint8List> entry = FakeEntry();
// Always throw exception until max retries
when(mockTestSubcache['does not matter']).thenAnswer(
(Invocation invocation) =>
getCallCount++ < CacheService.maxCacheGetAttempts ? throw Exception('simulate stream sink error') : entry,
);
cache.cacheValue = mockMainCache;
final Uint8List? value = await cache.getOrCreate(
testSubcacheName,
'does not matter',
createFn: null,
);
verify(mockTestSubcache['does not matter']).called(CacheService.maxCacheGetAttempts);
expect(value, isNull);
});
test('creates value if given createFn', () async {
final Uint8List cat = Uint8List.fromList('cat'.codeUnits);
Future<Uint8List> createCat() async => cat;
final Uint8List? value = await cache.getOrCreate(testSubcacheName, 'dog', createFn: createCat);
expect(value, cat);
});
test('purge removes value', () async {
const String testKey = 'abc';
final Uint8List expectedValue = Uint8List.fromList('123'.codeUnits);
await cache.set(testSubcacheName, testKey, expectedValue);
final Uint8List? value = await cache.getOrCreate(
testSubcacheName,
testKey,
createFn: null,
);
expect(value, expectedValue);
await cache.purge(testSubcacheName, testKey);
final Uint8List? valueAfterPurge = await cache.getOrCreate(
testSubcacheName,
testKey,
createFn: null,
);
expect(valueAfterPurge, isNull);
});
test('sets ttl from set', () async {
final Cache<Uint8List> mockMainCache = MockCache();
final Cache<Uint8List> mockTestSubcache = MockCache();
when<Cache<Uint8List>>(mockMainCache.withPrefix(testSubcacheName)).thenReturn(mockTestSubcache);
final Entry<Uint8List> entry = MockFakeEntry();
when(mockTestSubcache['fish']).thenAnswer((Invocation invocation) => entry);
cache.cacheValue = mockMainCache;
const Duration testDuration = Duration(days: 40);
when(entry.set(any, testDuration)).thenAnswer((_) async => null);
verifyNever(entry.set(any, testDuration));
await cache.set(testSubcacheName, 'fish', Uint8List.fromList('bigger fish'.codeUnits), ttl: testDuration);
verify(entry.set(any, testDuration)).called(1);
});
test('sets ttl is passed through correctly from createFn', () async {
const String value = 'bigger fish';
final Uint8List valueBytes = Uint8List.fromList(value.codeUnits);
const Duration testDuration = Duration(days: 40);
final Entry<Uint8List> entry = MockFakeEntry();
when(entry.set(valueBytes, testDuration)).thenAnswer((_) async => valueBytes);
final Cache<Uint8List> mockTestSubcache = MockCache();
final Cache<Uint8List> mockMainCache = MockCache();
when<Cache<Uint8List>>(mockMainCache.withPrefix(testSubcacheName)).thenReturn(mockTestSubcache);
when(mockTestSubcache['fish']).thenAnswer((Invocation invocation) => entry);
cache.cacheValue = mockMainCache;
verifyNever(entry.set(any, testDuration));
await cache.getOrCreate(
testSubcacheName,
'fish',
createFn: () async => valueBytes,
ttl: testDuration,
);
verify(entry.set(any, testDuration)).called(1);
});
test('set does not block read attempt', () async {
const String testKey = 'abc';
final Uint8List expectedValue = Uint8List.fromList('123'.codeUnits);
final cacheWrite = cache.setWithLocking(testSubcacheName, testKey, expectedValue);
Uint8List? valueAfterSet = await cache.getOrCreateWithLocking(
testSubcacheName,
testKey,
createFn: null,
);
expect(valueAfterSet, null);
await cacheWrite;
valueAfterSet = await cache.getOrCreateWithLocking(
testSubcacheName,
testKey,
createFn: null,
);
expect(valueAfterSet, expectedValue);
});
test('read locks are not blocking', () async {
const String testKey = 'abc';
final Uint8List expectedValue = Uint8List.fromList('123'.codeUnits);
await cache.setWithLocking(testSubcacheName, testKey, expectedValue);
final Future<Uint8List?> valueAfterSet = cache.getOrCreateWithLocking(
testSubcacheName,
testKey,
createFn: null,
);
final Uint8List? valueAfterSet2 = await cache.getOrCreateWithLocking(
testSubcacheName,
testKey,
createFn: null,
);
expect(valueAfterSet2, expectedValue);
await valueAfterSet.then((value) => expect(value, expectedValue));
});
test('write locks are blocking', () async {
const String testKey = 'abc';
final Uint8List expectedValue = Uint8List.fromList('123'.codeUnits);
final Uint8List newValue = Uint8List.fromList('345'.codeUnits);
final cacheWrite = cache.setWithLocking(testSubcacheName, testKey, expectedValue);
final cacheWrite2 = cache.setWithLocking(testSubcacheName, testKey, newValue);
await cacheWrite;
final Uint8List? readValue = await cache.getOrCreateWithLocking(
testSubcacheName,
testKey,
createFn: null,
);
expect(readValue, expectedValue);
await cacheWrite2;
});
});
}
class FakeEntry extends Entry<Uint8List> {
Uint8List value = Uint8List.fromList('abc123'.codeUnits);
@override
Future<Uint8List> get([Future<Uint8List?> Function()? create, Duration? ttl]) async => value;
@override
Future<void> purge({int retries = 0}) => throw UnimplementedError();
@override
Future<Uint8List?> set(Uint8List? value, [Duration? ttl]) async {
value = value;
return value;
}
}