| // 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/pubspec_scanner.dart'; |
| import 'package:path/path.dart' as path; |
| import 'package:test/test.dart'; |
| |
| void main() { |
| group('scanPubspecTelemetry', () { |
| late Directory tempDir; |
| late Directory originalCurrentDir; |
| |
| setUp(() { |
| tempDir = Directory.systemTemp.createTempSync('dartdev_scanner_test_'); |
| originalCurrentDir = Directory.current; |
| }); |
| |
| tearDown(() { |
| Directory.current = originalCurrentDir; |
| try { |
| tempDir.deleteSync(recursive: true); |
| } catch (_) {} |
| }); |
| |
| 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', () { |
| expect(scanPubspecTelemetry(tempDir), isNull); |
| }); |
| |
| test( |
| 'returns null when pubspec exists but package_config.json is missing (conservative fallback)', |
| () { |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' |
| name: test_project |
| dependencies: |
| path: ^1.8.0 |
| '''); |
| expect(scanPubspecTelemetry(tempDir), isNull); |
| }, |
| ); |
| |
| test('finds pubspec.yaml and package_config.json up to 5 levels deep', () { |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' |
| name: test_project |
| dependencies: |
| path: ^1.8.0 |
| '''); |
| createPackageConfig(tempDir, [ |
| { |
| 'name': 'path', |
| 'rootUri': 'file:///home/user/.pub-cache/hosted/pub.dev/path-1.8.0', |
| 'packageUri': 'lib/', |
| }, |
| ]); |
| |
| var deepDir = tempDir; |
| for (var i = 0; i < 5; i++) { |
| deepDir = Directory(path.join(deepDir.path, 'level_$i'))..createSync(); |
| } |
| |
| final telemetry = scanPubspecTelemetry(deepDir); |
| expect(telemetry, isNotNull); |
| expect(telemetry!.publicDependencies, {'path'}); |
| expect(telemetry.hasFlutterSdk, isFalse); |
| expect(telemetry.environmentSdk, isNull); |
| }); |
| |
| test('finds pubspec.yaml even > 5 levels deep', () { |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' |
| name: test_project |
| dependencies: |
| path: ^1.8.0 |
| '''); |
| createPackageConfig(tempDir, [ |
| { |
| 'name': 'path', |
| 'rootUri': 'file:///home/user/.pub-cache/hosted/pub.dev/path-1.8.0', |
| 'packageUri': 'lib/', |
| }, |
| ]); |
| |
| var deepDir = tempDir; |
| for (var i = 0; i < 6; i++) { |
| deepDir = Directory(path.join(deepDir.path, 'level_$i'))..createSync(); |
| } |
| |
| final telemetry = scanPubspecTelemetry(deepDir); |
| expect(telemetry, isNotNull); |
| expect(telemetry!.publicDependencies, {'path'}); |
| }); |
| |
| test( |
| 'strictly strips private workspace packages in a Dart 3.5+ Pub Workspace', |
| () { |
| // Simulate a workspace root with .dart_tool/package_config.json |
| createPackageConfig(tempDir, [ |
| { |
| 'name': 'path', |
| 'rootUri': 'file:///home/user/.pub-cache/hosted/pub.dev/path-1.8.0', |
| 'packageUri': 'lib/', |
| }, |
| { |
| 'name': 'secret_trading_algo', |
| 'rootUri': '../secret_trading_algo', |
| 'packageUri': 'lib/', |
| }, |
| ]); |
| |
| // Simulate member package inside workspace |
| final memberDir = Directory(path.join(tempDir.path, 'pkgs', 'app')) |
| ..createSync(recursive: true); |
| File(path.join(memberDir.path, 'pubspec.yaml')).writeAsStringSync(''' |
| name: app |
| resolution: workspace |
| dependencies: |
| path: ^1.8.0 |
| secret_trading_algo: |
| '''); |
| |
| final telemetry = scanPubspecTelemetry(memberDir); |
| expect(telemetry, isNotNull); |
| // 'path' is kept because rootUri is hosted on pub.dev |
| // 'secret_trading_algo' is stripped because rootUri is a relative workspace path! |
| expect(telemetry!.publicDependencies, {'path'}); |
| expect(telemetry.hasFlutterSdk, isFalse); |
| }, |
| ); |
| |
| test('extracts both dependencies and dev_dependencies', () { |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' |
| name: test_project |
| dependencies: |
| path: ^1.8.0 |
| dev_dependencies: |
| test: ^1.24.0 |
| lints: ^3.0.0 |
| '''); |
| createPackageConfig(tempDir, [ |
| { |
| 'name': 'path', |
| 'rootUri': 'file:///home/user/.pub-cache/hosted/pub.dev/path-1.8.0', |
| 'packageUri': 'lib/', |
| }, |
| { |
| 'name': 'test', |
| 'rootUri': 'file:///home/user/.pub-cache/hosted/pub.dev/test-1.24.0', |
| 'packageUri': 'lib/', |
| }, |
| { |
| 'name': 'lints', |
| 'rootUri': 'file:///home/user/.pub-cache/hosted/pub.dev/lints-3.0.0', |
| 'packageUri': 'lib/', |
| }, |
| ]); |
| |
| final telemetry = scanPubspecTelemetry(tempDir); |
| expect(telemetry, isNotNull); |
| expect(telemetry!.publicDependencies, {'path', 'test', 'lints'}); |
| expect(telemetry.hasFlutterSdk, isFalse); |
| }); |
| |
| test('detects Flutter SDK in environment', () { |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' |
| name: test_project |
| environment: |
| sdk: ^3.5.0 |
| flutter: '>=3.0.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 telemetry = scanPubspecTelemetry(tempDir); |
| expect(telemetry, isNotNull); |
| expect(telemetry!.publicDependencies, {'path'}); |
| expect(telemetry.hasFlutterSdk, isTrue); |
| expect(telemetry.environmentSdk, '^3.5.0'); |
| }); |
| |
| test('detects Flutter SDK in dependencies', () { |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' |
| name: test_project |
| environment: |
| sdk: ^3.5.0 |
| dependencies: |
| path: ^1.8.0 |
| flutter: |
| sdk: flutter |
| '''); |
| createPackageConfig(tempDir, [ |
| { |
| 'name': 'path', |
| 'rootUri': 'file:///home/user/.pub-cache/hosted/pub.dev/path-1.8.0', |
| 'packageUri': 'lib/', |
| }, |
| ]); |
| |
| final telemetry = scanPubspecTelemetry(tempDir); |
| expect(telemetry, isNotNull); |
| expect(telemetry!.publicDependencies, {'path'}); |
| expect(telemetry.hasFlutterSdk, isTrue); |
| expect(telemetry.environmentSdk, '^3.5.0'); |
| }); |
| |
| test('extracts and truncates long sdk constraint', () { |
| final longSdk = '^3.0.0 ${'a' * 110}'; |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' |
| name: test_project |
| environment: |
| sdk: "$longSdk" |
| 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 telemetry = scanPubspecTelemetry(tempDir); |
| expect(telemetry, isNotNull); |
| expect(telemetry!.environmentSdk!.length, 100); |
| expect(telemetry.environmentSdk, longSdk.substring(0, 100)); |
| }); |
| |
| test('supports custom PUB_CACHE environment variable', () { |
| final customCacheDir = Directory( |
| path.join(tempDir.path, 'my_custom_cache'), |
| )..createSync(recursive: true); |
| File(path.join(tempDir.path, 'pubspec.yaml')).writeAsStringSync(''' |
| name: test_project |
| environment: |
| sdk: ^3.5.0 |
| dependencies: |
| shelf: ^1.4.0 |
| fake_hosted: ^1.0.0 |
| '''); |
| createPackageConfig(tempDir, [ |
| { |
| 'name': 'shelf', |
| 'rootUri': path |
| .toUri( |
| path.join( |
| customCacheDir.path, |
| 'hosted', |
| 'pub.dev', |
| 'shelf-1.4.0', |
| ), |
| ) |
| .toString(), |
| 'packageUri': 'lib/', |
| }, |
| { |
| 'name': 'fake_hosted', |
| 'rootUri': path |
| .toUri( |
| path.join( |
| tempDir.path, |
| 'not_in_cache', |
| 'hosted', |
| 'pub.dev', |
| 'fake_hosted-1.0.0', |
| ), |
| ) |
| .toString(), |
| 'packageUri': 'lib/', |
| }, |
| ]); |
| |
| final telemetry = scanPubspecTelemetry( |
| tempDir, |
| environment: {'PUB_CACHE': customCacheDir.path}, |
| ); |
| expect(telemetry, isNotNull); |
| expect(telemetry!.publicDependencies, {'shelf'}); |
| }); |
| }); |
| |
| group('findPubspecFile', () { |
| late Directory tempDir; |
| |
| setUp(() { |
| tempDir = Directory.systemTemp.createTempSync( |
| 'dartdev_find_pubspec_test_', |
| ); |
| }); |
| |
| tearDown(() { |
| try { |
| tempDir.deleteSync(recursive: true); |
| } catch (_) {} |
| }); |
| |
| test('returns null when no pubspec exists', () { |
| expect(findPubspecFile(tempDir), isNull); |
| }); |
| |
| test('finds pubspec.yaml in current directory', () { |
| final pubspec = File(path.join(tempDir.path, 'pubspec.yaml')) |
| ..writeAsStringSync('name: test_pkg\n'); |
| final found = findPubspecFile(tempDir); |
| expect(found, isNotNull); |
| expect(found!.path, pubspec.path); |
| }); |
| |
| test('finds pubspec.yaml in deeply nested directory', () { |
| final pubspec = File(path.join(tempDir.path, 'pubspec.yaml')) |
| ..writeAsStringSync('name: test_pkg\n'); |
| final nestedDir = Directory( |
| path.join( |
| tempDir.path, |
| 'level1', |
| 'level2', |
| 'level3', |
| 'level4', |
| 'l5', |
| 'l6', |
| 'l7', |
| ), |
| )..createSync(recursive: true); |
| |
| final found = findPubspecFile(nestedDir); |
| expect(found, isNotNull); |
| expect(found!.path, pubspec.path); |
| }); |
| }); |
| } |