import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:rippr/src/tiles/tile_cache.dart'; import 'package:rippr/src/tiles/tile_math.dart'; void main() { late Directory tempDir; late FileTileCache cache; setUp(() async { tempDir = await Directory.systemTemp.createTemp('rippr_tile_cache_test_'); }); tearDown(() async { if (tempDir.existsSync()) tempDir.deleteSync(recursive: true); }); Uint8List bytesOfSize(int n) => Uint8List.fromList(List.filled(n, 1)); test('a stored tile round-trips', () async { cache = FileTileCache(directory: tempDir, maxBytes: 1024 * 1024); const key = TileKey(12, 100, 200); await cache.put(key, bytesOfSize(50)); final read = await cache.get(key); expect(read, isNotNull); expect(read!.length, 50); }); test('a tile never written is a miss, not an error', () async { cache = FileTileCache(directory: tempDir, maxBytes: 1024 * 1024); expect(await cache.get(const TileKey(1, 1, 1)), isNull); }); test('sizeBytes reflects everything stored', () async { cache = FileTileCache(directory: tempDir, maxBytes: 1024 * 1024); await cache.put(const TileKey(1, 0, 0), bytesOfSize(100)); await cache.put(const TileKey(1, 0, 1), bytesOfSize(200)); expect(await cache.sizeBytes(), 300); }); test('eviction at the cap removes the least-recently-used tile first', () async { // Room for two 100-byte tiles at a time. cache = FileTileCache(directory: tempDir, maxBytes: 200); const a = TileKey(1, 0, 0); const b = TileKey(1, 0, 1); const c = TileKey(1, 0, 2); await cache.put(a, bytesOfSize(100)); await cache.put(b, bytesOfSize(100)); // Touch `a` so `b` becomes the least-recently-used. await cache.get(a); await cache.put(c, bytesOfSize(100)); expect(await cache.get(a), isNotNull, reason: 'recently touched, must survive'); expect(await cache.get(b), isNull, reason: 'least-recently-used, must be evicted'); expect(await cache.get(c), isNotNull, reason: 'just written, must survive'); expect(await cache.sizeBytes(), lessThanOrEqualTo(200)); }); test('the cache never exceeds its cap after many writes', () async { cache = FileTileCache(directory: tempDir, maxBytes: 500); for (var i = 0; i < 20; i++) { await cache.put(TileKey(1, i, 0), bytesOfSize(100)); } expect(await cache.sizeBytes(), lessThanOrEqualTo(500)); }); test('clear removes every tile and resets size to zero', () async { cache = FileTileCache(directory: tempDir, maxBytes: 1024 * 1024); await cache.put(const TileKey(1, 0, 0), bytesOfSize(100)); await cache.put(const TileKey(1, 0, 1), bytesOfSize(100)); await cache.clear(); expect(await cache.sizeBytes(), 0); expect(await cache.get(const TileKey(1, 0, 0)), isNull); }); test('a cache re-opened over the same directory sees what was stored', () async { cache = FileTileCache(directory: tempDir, maxBytes: 1024 * 1024); await cache.put(const TileKey(5, 10, 10), bytesOfSize(64)); final reopened = FileTileCache(directory: tempDir, maxBytes: 1024 * 1024); expect(await reopened.get(const TileKey(5, 10, 10)), isNotNull); expect(await reopened.sizeBytes(), 64); }); test('a tile cached under one provider directory is not served from another ' '(UI-09)', () async { // `TileKey` carries no provider identity -- (z, x, y) alone can't tell an OSM tan // tile from a CARTO dark one at the same coordinates. `tileCacheProvider` (see // app/providers.dart) relies entirely on giving each tile source its own // subdirectory to prevent a switch from silently serving stale, visually-mismatched // tiles. This proves that isolation actually holds at the `FileTileCache` level. final osm = FileTileCache( directory: Directory('${tempDir.path}/osm'), maxBytes: 1024 * 1024, ); final cartoDark = FileTileCache( directory: Directory('${tempDir.path}/carto_dark_v1'), maxBytes: 1024 * 1024, ); await osm.put(const TileKey(5, 10, 10), bytesOfSize(64)); expect(await cartoDark.get(const TileKey(5, 10, 10)), isNull, reason: 'a tile cached under the old provider must not leak into the new one'); }); }