| // 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:dartdev/src/unified_analytics.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:test/test.dart'; | |
| void main() { | |
| group('collectPubspecTelemetry', () { | |
| late Directory tempDir; | |
| late File cacheFile; | |
| late ProjectTelemetryCache cache; | |
| setUp(() { | |
| tempDir = Directory.systemTemp.createTempSync('dartdev_analytics_test_'); | |
| cacheFile = File(path.join(tempDir.path, 'cache.json')); | |
| cache = ProjectTelemetryCache(cacheFile); | |
| }); | |
| tearDown(() { | |
| try { | |
| tempDir.deleteSync(recursive: true); | |
| } catch (_) { | |
| // Ignore cleanup errors | |
| } | |
| }); | |
| void createPackageConfig( | |
| Directory dir, | |
| List<Map<String, String>> packages, | |
| ) { | |
| final dotDartTool = Directory(path.join(dir.path, '.dart_tool')) | |
| ..createSync(recursive: true); | |
| final file = File(path.join(dotDartTool.path, 'package_config.json')); | |
| file.writeAsStringSync( | |
| jsonEncode({'configVersion': 2, 'packages': packages}), | |
| ); | |
| } | |
| test('returns null when no pubspec.yaml exists in parent hierarchy', () { | |
| expect(collectPubspecTelemetry(dir: tempDir, cache: cache), isNull); | |
| }); | |
| test('returns null when package_config.json is missing', () { | |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' | |
| name: test_project | |
| dependencies: | |
| path: ^1.8.0 | |
| '''); | |
| expect(collectPubspecTelemetry(dir: tempDir, cache: cache), isNull); | |
| }); | |
| test('finds pubspec.yaml, extracts telemetry, and populates cache', () { | |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' | |
| name: test_project | |
| environment: | |
| sdk: ^3.5.0 | |
| dependencies: | |
| path: ^1.8.0 | |
| '''); | |
| createPackageConfig(tempDir, [ | |
| { | |
| 'name': 'path', | |
| 'rootUri': 'file:///home/user/.pub-cache/hosted/pub.dev/path-1.8.0', | |
| 'packageUri': 'lib/', | |
| }, | |
| ]); | |
| final now = 1752000000; | |
| final telemetry = collectPubspecTelemetry( | |
| dir: tempDir, | |
| cache: cache, | |
| nowSeconds: now, | |
| ); | |
| expect(telemetry, isNotNull); | |
| expect(telemetry!.publicDependencies, {'path'}); | |
| expect(telemetry.hasFlutterSdk, isFalse); | |
| expect(telemetry.environmentSdk, '^3.5.0'); | |
| // Verify cache hit on second run throttles (returns null) within 24 hours | |
| final cachedTelemetry = collectPubspecTelemetry( | |
| dir: tempDir, | |
| cache: cache, | |
| nowSeconds: now, | |
| ); | |
| expect(cachedTelemetry, isNull); | |
| // Verify telemetry is re-sent after 24 hours have elapsed | |
| final expiredTelemetry = collectPubspecTelemetry( | |
| dir: tempDir, | |
| cache: cache, | |
| nowSeconds: now + 60 * 60 * 24 + 1, | |
| ); | |
| expect(expiredTelemetry, isNotNull); | |
| // Verify telemetry is re-sent when pubspec content changes within 24 hours | |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' | |
| name: test_project | |
| environment: | |
| sdk: ^3.6.0 | |
| dependencies: | |
| path: ^1.8.0 | |
| '''); | |
| final mutatedTelemetry = collectPubspecTelemetry( | |
| dir: tempDir, | |
| cache: cache, | |
| nowSeconds: now + 60 * 60 * 24 + 2, | |
| ); | |
| expect(mutatedTelemetry, isNotNull); | |
| expect(mutatedTelemetry!.environmentSdk, '^3.6.0'); | |
| }); | |
| test( | |
| 'finds pubspec from nested subdirectory and shares cache with root', | |
| () { | |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' | |
| name: test_project | |
| environment: | |
| sdk: ^3.5.0 | |
| dependencies: | |
| path: ^1.8.0 | |
| '''); | |
| createPackageConfig(tempDir, [ | |
| { | |
| 'name': 'path', | |
| 'rootUri': 'file:///home/user/.pub-cache/hosted/pub.dev/path-1.8.0', | |
| 'packageUri': 'lib/', | |
| }, | |
| ]); | |
| final nestedDir = Directory(path.join(tempDir.path, 'test', 'unit')) | |
| ..createSync(recursive: true); | |
| // Invocation from nested subdirectory resolves root and populates cache | |
| final nestedTelemetry = collectPubspecTelemetry( | |
| dir: nestedDir, | |
| cache: cache, | |
| ); | |
| expect(nestedTelemetry, isNotNull); | |
| expect(nestedTelemetry!.publicDependencies, {'path'}); | |
| expect(nestedTelemetry.hasFlutterSdk, isFalse); | |
| expect(nestedTelemetry.environmentSdk, '^3.5.0'); | |
| // Running from project root hits the exact same cache entry | |
| final rootTelemetry = collectPubspecTelemetry( | |
| dir: tempDir, | |
| cache: cache, | |
| ); | |
| expect(rootTelemetry, isNull); | |
| }, | |
| ); | |
| test('fails silently and returns null when pubspec.yaml is malformed', () { | |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' | |
| name: test_project | |
| dependencies: | |
| invalid: [yaml: structure | |
| '''); | |
| createPackageConfig(tempDir, [ | |
| { | |
| 'name': 'path', | |
| 'rootUri': 'file:///home/user/.pub-cache/hosted/pub.dev/path-1.8.0', | |
| 'packageUri': 'lib/', | |
| }, | |
| ]); | |
| expect(collectPubspecTelemetry(dir: tempDir, cache: cache), isNull); | |
| }); | |
| }); | |
| } |