| // Copyright (c) 2026, 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:convert'; | |
| import 'dart:io'; | |
| import 'package:dartdev/src/telemetry/project_cache.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:test/test.dart'; | |
| void main() { | |
| group('ProjectTelemetryCache', () { | |
| late Directory tempDir; | |
| late File cacheFile; | |
| late ProjectTelemetryCache cache; | |
| setUp(() { | |
| tempDir = Directory.systemTemp.createTempSync('dartdev_cache_test_'); | |
| cacheFile = File(path.join(tempDir.path, 'project_telemetry_cache.json')); | |
| cache = ProjectTelemetryCache(cacheFile); | |
| }); | |
| tearDown(() { | |
| try { | |
| tempDir.deleteSync(recursive: true); | |
| } catch (_) {} | |
| }); | |
| test('returns null on cache miss when file does not exist', () { | |
| expect(cache.getEntry('/path/to/project'), isNull); | |
| }); | |
| test('stores and retrieves telemetry', () { | |
| final now = 1752000000; | |
| final telemetry = ( | |
| publicDependencies: {'path', 'test'}, | |
| hasFlutterSdk: false, | |
| environmentSdk: '^3.0.0', | |
| ); | |
| cache.set( | |
| '/path/to/project', | |
| telemetry, | |
| contentHash: 'hashx', | |
| nowSeconds: now, | |
| ); | |
| expect(cacheFile.existsSync(), isTrue); | |
| // Verify anonymity: check that plaintext path is NOT in cache file | |
| final content = cacheFile.readAsStringSync(); | |
| expect(content.contains('/path/to/project'), isFalse); | |
| final retrieved = cache.getEntry( | |
| '/path/to/project', | |
| ); | |
| expect(retrieved, isNotNull); | |
| expect(retrieved!.publicDependencies, {'path', 'test'}); | |
| expect(retrieved.hasFlutterSdk, isFalse); | |
| expect(retrieved.environmentSdk, '^3.0.0'); | |
| expect(retrieved.contentHash, 'hashx'); | |
| expect(retrieved.lastSentSeconds, now); | |
| }); | |
| test('enforces 100-project LRU eviction cap', () { | |
| final now = 1752000000; | |
| // Add 105 distinct projects with sequential timestamps | |
| for (var i = 0; i < 105; i++) { | |
| cache.set( | |
| '/path/to/project_$i', | |
| ( | |
| publicDependencies: {'pkg_$i'}, | |
| hasFlutterSdk: false, | |
| environmentSdk: null, | |
| ), | |
| contentHash: 'hashx_$i', | |
| nowSeconds: now + i, | |
| ); | |
| } | |
| final content = cacheFile.readAsStringSync(); | |
| final json = jsonDecode(content) as Map<String, dynamic>; | |
| final projects = json['projects'] as Map<String, dynamic>; | |
| // Ensure exactly 100 projects remain | |
| expect(projects.length, 100); | |
| // The oldest 5 projects (project_0 through project_4) should have been evicted | |
| for (var i = 0; i < 5; i++) { | |
| expect(cache.getEntry('/path/to/project_$i'), isNull); | |
| } | |
| // The remaining 100 projects (project_5 through project_104) should be valid | |
| for (var i = 5; i < 105; i++) { | |
| final res = cache.getEntry('/path/to/project_$i'); | |
| expect(res, isNotNull); | |
| expect(res!.publicDependencies, {'pkg_$i'}); | |
| expect(res.contentHash, 'hashx_$i'); | |
| } | |
| }); | |
| test('fails silently and returns null on corrupted JSON cache file', () { | |
| cacheFile.writeAsStringSync('{ corrupted json: ['); | |
| expect(cache.getEntry('/path/to/project'), isNull); | |
| // Setting new data over a corrupted file should recover cleanly without crashing | |
| final telemetry = ( | |
| publicDependencies: {'path'}, | |
| hasFlutterSdk: false, | |
| environmentSdk: null, | |
| ); | |
| expect( | |
| () => cache.set('/path/to/project', telemetry, contentHash: 'hash'), | |
| returnsNormally, | |
| ); | |
| expect(cache.getEntry('/path/to/project'), isNotNull); | |
| }); | |
| }); | |
| } |