blob: d6af84fd776f5e8c13b4cc535f46543086116cd5 [file]
// 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:io';
import 'package:dartdev/src/sdk.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:unified_analytics/unified_analytics.dart';
import 'utils.dart';
/// The location of the unified_analytics config file under [home].
String _configPath(String home) =>
path.join(home, '.dart-tool', 'dart-flutter-telemetry.config');
/// Creates an isolated home directory for running `dart`, so tests don't
/// depend on (or modify) the analytics state of the machine running them.
///
/// Returns the home directory path and an environment that points at it.
///
/// If [consented] is true, the home directory's analytics config records that
/// the Dart CLI has already shown its consent message. Otherwise the home
/// directory is empty, which `dart` treats as a first run.
///
/// The parent environment is not inherited (use `includeParentEnvironment:
/// false`), so a `DASH__SUPPRESS_ANALYTICS` or bot variable set for the test
/// process doesn't leak in. `BOT` is forced to `false` so that a CI
/// environment doesn't implicitly suppress analytics; pass `'BOT': null` in
/// [extra] to leave it unset.
({Map<String, String> env, String home}) _setUpIsolatedHome(
TestProject p, {
required bool consented,
Map<String, String?> extra = const {},
}) {
final home = path.join(p.root.path, 'home');
Directory(home).createSync(recursive: true);
if (consented) {
File(_configPath(home))
..createSync(recursive: true)
..writeAsStringSync('reporting=1\ndart-tool=2026-01-01,1\n');
}
final env = <String, String?>{
'PUB_CACHE': p.pubCachePath,
'BOT': 'false',
'PATH': Platform.environment['PATH'] ?? '',
if (Platform.isWindows) ...{
'SystemRoot': Platform.environment['SystemRoot'],
'TEMP': Platform.environment['TEMP'],
'LOCALAPPDATA': Platform.environment['LOCALAPPDATA'],
'USERPROFILE': Platform.environment['USERPROFILE'],
'SystemDrive': Platform.environment['SystemDrive'],
'APPDATA': home,
},
if (Platform.isMacOS || Platform.isLinux) 'HOME': home,
...extra,
};
return (
env: {for (final MapEntry(:key, :value) in env.entries) key: ?value},
home: home,
);
}
/// Source for a script that prints the analytics variables it inherited.
const _printAnalyticsEnvSrc = '''
import 'dart:io';
void main() {
print('DASH__TOOL: \${Platform.environment['DASH__TOOL']}');
print('DASH__SUPPRESS_ANALYTICS: '
'\${Platform.environment['DASH__SUPPRESS_ANALYTICS']}');
print('DART_ROOT: \${Platform.environment['DART_ROOT']}');
}
''';
/// Creates a project with an executable that prints the analytics variables
/// it inherited, for use with `dart run <name>@{path: ...}`.
TestProject _executableProject() {
final p = project();
p.file(
'pubspec.yaml',
'name: ${p.name}\nenvironment:\n sdk: ^3.0.0\nexecutables:\n ${p.name}:\n',
);
p.file('bin/${p.name}.dart', _printAnalyticsEnvSrc);
return p;
}
void main() {
ensureRunFromSdkBinDart();
group('Environment modification', () {
test('run command sets DART_ROOT', () async {
final p = project(
mainSrc: '''
import 'dart:io';
void main() {
print('DART_ROOT: \${Platform.environment['DART_ROOT']}');
}
''',
);
final result = await p.run(['run', p.relativeFilePath]);
expect(result.exitCode, 0);
// The environment variable was set in the parent isolate (dartdev)
// via VmInteropHandler.setEnvironmentVariable before the run command
// spawned the new isolate.
expect(result.stdout, contains('DART_ROOT: ${sdk.sdkPath}'));
});
test('run command does not overwrite existing DART_ROOT', () async {
final p = project(
mainSrc: '''
import 'dart:io';
void main() {
print('DART_ROOT: \${Platform.environment['DART_ROOT']}');
}
''',
);
final result = await Process.run(
Platform.resolvedExecutable,
['run', p.relativeFilePath],
workingDirectory: p.dir.path,
environment: {
'PUB_CACHE': p.pubCachePath,
'DART_ROOT': 'original_value',
},
);
expect(result.exitCode, 0);
// The environment variable was already set to 'original_value',
// so dartdev should not overwrite it.
expect(result.stdout, contains('DART_ROOT: original_value'));
});
test('run command sets DASH__TOOL and DASH__SUPPRESS_ANALYTICS', () async {
final p = project(mainSrc: _printAnalyticsEnvSrc);
final result = await Process.run(
Platform.resolvedExecutable,
['run', p.relativeFilePath],
workingDirectory: p.dir.path,
includeParentEnvironment: false,
// Use a home directory where the consent message has already been
// shown, so the result doesn't depend on the machine's analytics
// state.
environment: _setUpIsolatedHome(p, consented: true).env,
);
expect(result.exitCode, 0);
expect(result.stdout, contains('DASH__TOOL: dart-tool'));
expect(result.stdout, contains('DASH__SUPPRESS_ANALYTICS: false'));
});
test('run command preserves existing DASH__TOOL', () async {
final p = project(
mainSrc: '''
import 'dart:io';
void main() {
print('DASH__TOOL: \${Platform.environment['DASH__TOOL']}');
}
''',
);
final result = await Process.run(
Platform.resolvedExecutable,
['run', p.relativeFilePath],
workingDirectory: p.dir.path,
environment: {
'PUB_CACHE': p.pubCachePath,
'DASH__TOOL': 'flutter-tool',
},
);
expect(result.exitCode, 0);
expect(result.stdout, contains('DASH__TOOL: flutter-tool'));
});
test(
'--suppress-analytics propagates DASH__SUPPRESS_ANALYTICS=true',
() async {
final p = project(mainSrc: _printAnalyticsEnvSrc);
final result = await Process.run(
Platform.resolvedExecutable,
['--suppress-analytics', 'run', p.relativeFilePath],
workingDirectory: p.dir.path,
environment: {
'PUB_CACHE': p.pubCachePath,
},
);
expect(result.exitCode, 0);
expect(result.stdout, contains('DASH__SUPPRESS_ANALYTICS: true'));
},
);
test(
'parent env DASH__SUPPRESS_ANALYTICS=true propagates as true',
() async {
final p = project(mainSrc: _printAnalyticsEnvSrc);
final result = await Process.run(
Platform.resolvedExecutable,
['run', p.relativeFilePath],
workingDirectory: p.dir.path,
environment: {
'PUB_CACHE': p.pubCachePath,
'DASH__SUPPRESS_ANALYTICS': 'true',
},
);
expect(result.exitCode, 0);
expect(result.stdout, contains('DASH__SUPPRESS_ANALYTICS: true'));
},
);
test(
'run command with @ descriptor sets DASH__TOOL, DASH__SUPPRESS_ANALYTICS, and DART_ROOT',
() async {
final p = _executableProject();
final result = await Process.run(
Platform.resolvedExecutable,
['run', '${p.name}@{path: ${p.dir.path}}'],
workingDirectory: p.dir.path,
includeParentEnvironment: false,
environment: _setUpIsolatedHome(p, consented: true).env,
);
expect(result.exitCode, 0);
expect(result.stdout, contains('DASH__TOOL: dart-tool'));
expect(result.stdout, contains('DASH__SUPPRESS_ANALYTICS: false'));
expect(result.stdout, contains('DART_ROOT: ${sdk.sdkPath}'));
},
);
test(
'run command with @ descriptor preserves existing DASH__TOOL',
() async {
final p = _executableProject();
final result = await Process.run(
Platform.resolvedExecutable,
['run', '${p.name}@{path: ${p.dir.path}}'],
workingDirectory: p.dir.path,
includeParentEnvironment: false,
environment: _setUpIsolatedHome(
p,
consented: true,
extra: {'BOT': null, 'DASH__TOOL': 'flutter-tool'},
).env,
);
expect(result.exitCode, 0);
expect(result.stdout, contains('DASH__TOOL: flutter-tool'));
},
);
test(
'run command with @ descriptor and --suppress-analytics propagates DASH__SUPPRESS_ANALYTICS=true',
() async {
final p = _executableProject();
final result = await Process.run(
Platform.resolvedExecutable,
['--suppress-analytics', 'run', '${p.name}@{path: ${p.dir.path}}'],
workingDirectory: p.dir.path,
includeParentEnvironment: false,
environment: _setUpIsolatedHome(
p,
consented: true,
extra: {'BOT': null},
).env,
);
expect(result.exitCode, 0);
expect(result.stdout, contains('DASH__SUPPRESS_ANALYTICS: true'));
},
);
});
// `Process.run` gives `dart` no terminal, so the consent message is never
// printed. Consent stays pending, nothing is recorded, and sub-tools are
// not suppressed. (The terminal case, where sub-tools are suppressed, is
// covered by unit tests in analytics_test.dart.)
group(
'First run without a terminal',
() {
Future<ProcessResult> runWith(
TestProject p,
Map<String, String> env, {
List<String> dartArgs = const [],
}) => Process.run(
Platform.resolvedExecutable,
[...dartArgs, 'run', p.relativeFilePath],
workingDirectory: p.dir.path,
includeParentEnvironment: false,
environment: env,
);
bool dartToolConsentRecorded(String home) {
final config = File(_configPath(home));
return config.existsSync() &&
RegExp(
r'^dart-tool=\S+,1$',
multiLine: true,
).hasMatch(config.readAsStringSync());
}
test('does not suppress sub-tools or record consent', () async {
final p = project(mainSrc: _printAnalyticsEnvSrc);
final (:env, :home) = _setUpIsolatedHome(p, consented: false);
final result = await runWith(p, env);
expect(result.exitCode, 0);
expect(result.stdout, contains('DASH__SUPPRESS_ANALYTICS: false'));
// The config file was created, but the Dart CLI did not record that
// it showed the consent message.
expect(File(_configPath(home)).existsSync(), isTrue);
expect(dartToolConsentRecorded(home), isFalse);
// A second run is still a first run as far as consent goes.
final second = await runWith(p, env);
expect(second.exitCode, 0);
expect(second.stdout, contains('DASH__SUPPRESS_ANALYTICS: false'));
expect(dartToolConsentRecorded(home), isFalse);
});
test('outdated consent version does not suppress sub-tools', () async {
final p = project(mainSrc: _printAnalyticsEnvSrc);
final (:env, :home) = _setUpIsolatedHome(p, consented: false);
File(_configPath(home))
..createSync(recursive: true)
..writeAsStringSync('reporting=1\ndart-tool=2026-01-01,0\n');
final result = await runWith(p, env);
expect(result.exitCode, 0);
expect(result.stdout, contains('DASH__SUPPRESS_ANALYTICS: false'));
expect(dartToolConsentRecorded(home), isFalse);
});
test('with --suppress-analytics suppresses sub-tools', () async {
final p = project(mainSrc: _printAnalyticsEnvSrc);
final env = _setUpIsolatedHome(
p,
consented: false,
extra: {'BOT': null},
).env;
final result = await runWith(
p,
env,
dartArgs: const ['--suppress-analytics'],
);
expect(result.exitCode, 0);
expect(result.stdout, contains('DASH__SUPPRESS_ANALYTICS: true'));
});
},
// Internal builds have no consent flow and never create the config file.
//
// `NoOpAnalytics().isExternal` reflects the package's build-time
// `isExternal` constant (`Analytics.fake` would not: it defaults to
// `true`). This assumes the test process and the SDK under test were built
// with the same value, which holds when both come from the same checkout.
skip: NoOpAnalytics().isExternal
? false
: 'Consent only applies to external builds.',
);
}