blob: 73f1a200c04274ee5f0e7d76eec6a8e1ad83015d [file] [log] [blame] [edit]
// Copyright (c) 2025, 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:meta/meta.dart';
import 'package:pub_formats/pub_formats.dart';
import 'package:test/test.dart';
import '../utils.dart';
import 'helpers.dart';
/// A package in the Dart SDK that we use for testing.
///
/// This package is on pub.dev, and on a Git repo, and on disk. So, this can be
/// used for testing hosted, git, and path installs.
///
/// Moreover it has an executables section in the pubspec.
const _packageForTest = 'vm_snapshot_analysis';
/// A valid version for [_packageForTest].
///
/// Not the newest version.
const _packageVersion = '0.7.5';
/// The name of an executable in [_packageForTest].
const _cliToolForTest = 'snapshot_analysis';
final _pathEnvVarSeparator = Platform.isWindows ? ';' : ':';
/// A package not in the Dart SDK repo. The Dart SDK repo takes too long to
/// clone.
const _gitPackageForTest = 'dart_app';
final _gitHttpsUrl = Uri.parse('https://dart.googlesource.com/native');
const _gitPath = 'pkgs/hooks_runner/test_data/dart_app/';
const _gitRef = '8ce789991cddb8864b0f3fa210c83d3d10e78316';
const String _dartDirectoryEnvKey = 'DART_DATA_HOME';
final _dartDevEntryScriptUri = resolveDartDevUri('bin/dartdev.dart');
final _sdkUri = resolveDartDevUri('.').resolve('../../');
final _packageRelativePath = Uri.directory('pkg/vm_snapshot_analysis/');
final _packageDir = Directory.fromUri(
_sdkUri.resolveUri(_packageRelativePath),
);
void main([List<String> args = const []]) async {
if (!nativeAssetsExperimentAvailableOnCurrentChannel) {
return;
}
final bool fromDartdevSource = args.contains('--source');
final errorExitCode = fromDartdevSource
? /* Dartdev doesn't exit the process, it sends a message to the VM.*/ 0
: 255;
final argsFiltered = args.where((e) => e != '--source').toList();
final testName = argsFiltered.isEmpty ? null : argsFiltered.join(' ');
@isTest
void skippableTest(
String description,
dynamic Function() body, {
Timeout? timeout,
}) {
test(
description,
skip: !(testName == null || description.contains(testName)),
timeout: timeout,
body,
);
}
final commandsHelpmessages = [
(
'install',
'''
Install or upgrade a Dart CLI tool for global use.
Install all executables specified in a package's pubspec.yaml executables
section (https://dart.dev/tools/pub/pubspec#executables) on the PATH. If the
executables section doesn't exist, installs all `bin/*.dart` entry points as
executables.
If the same package has been previously installed, it will be overwritten.
You can specify three different values for the <package> argument:
1. A package name. This will install the package from pub.dev. (hosted)
The [version-constraint] argument can only be passed to 'hosted'.
2. A git url. This will install the package from a git repository. (git)
3. A path on your machine. This will install the package from that path. (path)
Usage: dart install <package> [version-constraint]
-h, --help Print this usage information.
--git-path Path of git package in repository. Only applies when using a git url for <package>.
--git-ref Git branch or commit to be retrieved. Only applies when using a git url for <package>.
--overwrite Overwrite executables from other packages with the same name.
-u, --hosted-url A custom pub server URL for the package. Only applies when using a package name for <package>.
Run "dart help" to see global options.
'''
),
(
'installed',
'''
List globally installed Dart CLI tools.
Usage: dart installed [arguments]
-h, --help Print this usage information.
-a, --[no-]all Also list packages which are currently not active.
Active package have executables on `PATH`.
App bundles of packages on disk which have no executables
on `PATH` are non-active.
Run "dart help" to see global options.
'''
),
(
'uninstall',
'''
Remove a globally installed Dart CLI tool.
Completely deletes all installed versions of <package> and all executables from
<package> placed on PATH.
Usage: dart uninstall <package>
-h, --help Print this usage information.
Run "dart help" to see global options.
'''
),
];
for (final (command, helpMessage) in commandsHelpmessages) {
skippableTest('dart $command --help', timeout: longTimeout, () async {
final result = await _runDartdev(
fromDartdevSource,
command,
['--help'],
null,
{},
);
expect(result.stdout, contains(helpMessage));
});
}
final argumentss = [
(
null,
[_packageForTest],
),
(
null,
[_packageForTest, _packageVersion],
),
(
null,
[_packageForTest, _packageVersion, '--hosted-url', 'https://pub.dev/'],
),
(
null,
[_packageDir.path],
),
(
_sdkUri,
[_packageRelativePath.path],
),
(
_packageDir.uri,
['.'],
),
];
for (final (workingDirectory, arguments) in argumentss) {
var testName = arguments.join(' ');
if (workingDirectory != null) {
testName += ' in ${workingDirectory.toFilePath()}';
}
skippableTest('dart install $testName', timeout: longTimeout, () async {
await inTempDir((tempUri) async {
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
final environment = {
_dartDirectoryEnvKey: tempUri.toFilePath(),
'PATH':
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
};
await _runDartdev(
fromDartdevSource,
'install',
arguments,
workingDirectory,
environment,
);
await _runToolForTest(environment);
final installedResult = await _runDartdev(
fromDartdevSource,
'installed',
[],
null,
environment,
);
final installedLines = installedResult.stdout.split('\n');
expect(installedLines.where((e) => e.isNotEmpty).length, equals(1));
final installedLine = installedLines.first;
expect(
installedLine,
startsWith(_packageForTest),
);
if (arguments.contains(_packageVersion)) {
expect(
installedLine,
equals('$_packageForTest $_packageVersion'),
);
}
if (arguments.contains(_packageRelativePath.toString())) {
expect(
installedLine,
stringContainsInOrder([_packageRelativePath.toString(), '" at 20']),
);
}
await _runDartdev(
fromDartdevSource,
'uninstall',
[_packageForTest],
null,
environment,
);
});
});
}
final argumentssGit = [
(
null,
[_gitHttpsUrl.toString(), '--git-path', _gitPath],
),
(
null,
[_gitHttpsUrl.toString(), '--git-path', _gitPath, '--git-ref', _gitRef],
),
];
for (final (workingDirectory, arguments) in argumentssGit) {
var testName = arguments.join(' ');
if (workingDirectory != null) {
testName += ' in ${workingDirectory.toFilePath()}';
}
skippableTest('dart install $testName', timeout: longTimeout, () async {
await inTempDir((tempUri) async {
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
final environment = {
_dartDirectoryEnvKey: tempUri.toFilePath(),
'PATH':
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
};
await _runDartdev(
fromDartdevSource,
'install',
arguments,
workingDirectory,
environment,
);
final installedResult = await _runDartdev(
fromDartdevSource,
'installed',
[],
null,
environment,
);
final installedLines = installedResult.stdout.split('\n');
expect(installedLines.where((e) => e.isNotEmpty).length, equals(1));
final installedLine = installedLines.first;
expect(
installedLine,
startsWith(_gitPackageForTest),
);
expect(
installedLine,
contains(' from Git repository "${_gitHttpsUrl.toString()}"'),
);
if (arguments.contains('--git-ref')) {
expect(
installedLine,
contains(' at "${_gitRef.substring(0, 8)}"'),
);
}
await _runDartdev(
fromDartdevSource,
'uninstall',
[_gitPackageForTest],
null,
environment,
);
});
});
}
skippableTest('dart install ~/.dart/install/bin/ not on PATH', () async {
await inTempDir((tempUri) async {
final environment = {
_dartDirectoryEnvKey: tempUri.toFilePath(),
};
await inTempDir((tempUri) async {
final installResult = await _runDartdev(
fromDartdevSource,
'install',
[_packageForTest],
null,
environment,
);
if (Platform.isWindows) {
expect(
installResult.stdout,
stringContainsInOrder([
'Warning: Dart installs executables into ',
'which is not on your path.',
"You can fix that by adding that directory to your system's ",
'"Path" environment variable.',
'A web search for "configure windows path" will show you how.',
]),
);
} else {
expect(
installResult.stdout,
stringContainsInOrder([
'Warning: Dart installs executables into',
'You can fix that by adding this to your shell\'s config file ',
'export PATH="\$PATH":',
]),
);
}
});
});
});
skippableTest('dart install dart_app (with build hooks and code assets)',
timeout: longTimeout, () async {
await inTempDir((tempUri) async {
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
final environment = {
_dartDirectoryEnvKey: tempUri.toFilePath(),
'PATH':
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
};
await nativeAssetsTest('dart_app', (dartAppUri) async {
// Add a second executable.
final entryPoint1 =
File.fromUri(dartAppUri.resolve('bin/dart_app.dart'));
final entryPoint2 =
File.fromUri(dartAppUri.resolve('bin/dart_app_copy.dart'));
final entryPoint1Contents = await entryPoint1.readAsString();
final entryPoint2Contents = entryPoint1Contents.replaceAll('5', '42');
await entryPoint2.writeAsString(entryPoint2Contents);
final pubspecFile = File.fromUri(dartAppUri.resolve('pubspec.yaml'));
final pubspecOld =
pubspecFile.readAsStringSync().replaceAll('\r\n', '\n');
final pubspecNew = pubspecOld.replaceAll(
'''executables:
dart_app:'''
.replaceAll('\r\n', '\n'),
'''executables:
dart_app:
dart_app_copy:'''
.replaceAll('\r\n', '\n'),
);
expect(pubspecNew, isNot(equals(pubspecOld)));
pubspecFile.writeAsStringSync(pubspecNew);
await _runDartdev(
fromDartdevSource,
'install',
[dartAppUri.toFilePath()],
null,
environment,
);
for (final (tool, someInt) in [
('dart_app', 5),
('dart_app_copy', 42)
]) {
final toolResult = await runProcess(
// Note this has `runInShell: true` under it to ensure PATHEXT is used on
// Windows so that invoking an executable without extension works.
executable: Uri.file(tool),
// Run in some unrelated directory ensuring PATH is picked up.
workingDirectory: Directory.systemTemp.uri,
logger: logger,
environment: environment,
);
expect(
toolResult.stdout,
stringContainsInOrder([
'add($someInt, 6) = ${someInt + 6}',
'subtract($someInt, 6) = ${someInt - 6}',
]),
);
expect(toolResult.exitCode, 0);
}
});
});
});
skippableTest('dart install --overwrite', timeout: longTimeout, () async {
await inTempDir((tempUri) async {
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
final environment = {
_dartDirectoryEnvKey: tempUri.toFilePath(),
'PATH':
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
};
await nativeAssetsTest('dart_app', (dartAppUri) async {
await _runDartdev(
fromDartdevSource,
'install',
[dartAppUri.toFilePath()],
null,
environment,
);
// Not overwriting, but the same package is fine.
await _runDartdev(
fromDartdevSource,
'install',
[dartAppUri.toFilePath()],
null,
environment,
);
final pubspecFile = File.fromUri(dartAppUri.resolve('pubspec.yaml'));
final pubspecContents = await pubspecFile.readAsString();
final pubspecContentsNew =
pubspecContents.replaceFirst('dart_app', 'a_different_name');
await pubspecFile.writeAsString(pubspecContentsNew);
// Trying to install an executable with the same name from a different
// package should fail.
await _runDartdev(
fromDartdevSource,
'install',
[dartAppUri.toFilePath()],
null,
environment,
expectedExitCode: errorExitCode,
);
// Overwriting is fine.
await _runDartdev(
fromDartdevSource,
'install',
[dartAppUri.toFilePath(), '--overwrite'],
null,
environment,
);
// Using --overwrite leads to inactive versions.
// `dart installed --all` should also report the non-active versions.
for (final all in [true, false]) {
final installedResult = await _runDartdev(
fromDartdevSource,
'installed',
[if (all) '--all'],
null,
environment,
);
final installedLines = installedResult.stdout
.split('\n')
.where((e) => e.isNotEmpty)
.toList();
if (all) {
expect(installedLines, hasLength(2));
expect(
installedLines,
contains(startsWith('dart_app')),
);
} else {
expect(installedLines, hasLength(1));
expect(
installedLines,
isNot(contains(startsWith('dart_app'))),
);
}
}
});
});
});
skippableTest('dart install check exit codes', timeout: longTimeout,
() async {
await inTempDir((tempUri) async {
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
final environment = {
_dartDirectoryEnvKey: tempUri.toFilePath(),
'PATH':
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
};
const appName = 'test_app';
final dartAppUri = tempUri.resolve('$appName/');
final pubspec = File.fromUri(dartAppUri.resolve('pubspec.yaml'));
await pubspec.create(recursive: true);
await pubspec.writeAsString(jsonEncode(PubspecYamlFileSyntax(
name: appName,
environment: EnvironmentSyntax(
sdk: '^${Platform.version.split(' ').first}',
),
executables: {
appName: appName,
},
).json));
final mainFile = File.fromUri(dartAppUri.resolve('bin/$appName.dart'));
await mainFile.create(recursive: true);
mainFile.writeAsString('''
import 'dart:io';
void main(List<String> args) {
exit(int.parse(args.first));
}
''');
await _runDartdev(
fromDartdevSource,
'install',
[dartAppUri.toFilePath()],
null,
environment,
);
const testExitCode = 55;
final toolResult = await runProcess(
// Note this has `runInShell: true` under it to ensure PATHEXT is used on
// Windows so that invoking an executable without extension works.
executable: Uri.file(appName),
// Run in some unrelated directory ensuring PATH is picked up.
workingDirectory: Directory.systemTemp.uri,
arguments: ['$testExitCode'],
logger: logger,
environment: environment,
expectedExitCode: testExitCode,
);
expect(toolResult.exitCode, testExitCode);
});
});
skippableTest('dart install hooks user-defines and failures',
timeout: longTimeout, () async {
await inTempDir((tempUri) async {
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
final environment = {
_dartDirectoryEnvKey: tempUri.toFilePath(),
'PATH':
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
};
const packageName = 'test_app';
final dartAppUri = tempUri.resolve('$packageName/');
final pubspec = File.fromUri(dartAppUri.resolve('pubspec.yaml'));
await pubspec.create(recursive: true);
final mainFile =
File.fromUri(dartAppUri.resolve('bin/$packageName.dart'));
await mainFile.create(recursive: true);
mainFile.writeAsString('''
void main(List<String> args) { }
''');
final buildHookFile = File.fromUri(dartAppUri.resolve('hook/build.dart'));
await buildHookFile.create(recursive: true);
buildHookFile.writeAsString('''
import 'package:hooks/hooks.dart';
void main(List<String> args) async {
await build(args, (input, output) async {
final myUserDefine = input.userDefines['my_user_define'];
if (myUserDefine == null) {
throw Exception('Expected a user define');
}
});
}
''');
for (final addUserDefine in [true, false]) {
await pubspec.writeAsString(jsonEncode(PubspecYamlFileSyntax(
name: packageName,
environment: EnvironmentSyntax(
sdk: '^${Platform.version.split(' ').first}',
),
executables: {
packageName: packageName,
},
dependencies: {
'hooks': PathDependencySourceSyntax(
path$: sdkRootUri
.resolve('third_party/pkg/native/pkgs/hooks/')
.toFilePath(),
),
},
hooks: HooksSyntax(
userDefines: {
packageName: {
if (addUserDefine) 'my_user_define': 'a_value,',
},
},
),
).json));
final installResult = await _runDartdev(
fromDartdevSource,
'install',
[dartAppUri.toFilePath()],
null,
environment,
expectedExitCode: addUserDefine ? 0 : errorExitCode,
);
if (addUserDefine) {
expect(installResult.exitCode, equals(0));
expect(installResult.stderr, isEmpty);
} else {
// Check that build hook failures are surfaced and that error messages
// are visible.
expect(installResult.exitCode, equals(errorExitCode));
expect(installResult.stderr, contains('Expected a user define'));
}
}
});
});
skippableTest('dart install uninstalls old versions', timeout: longTimeout,
() async {
await inTempDir((tempUri) async {
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
final environment = {
_dartDirectoryEnvKey: tempUri.toFilePath(),
'PATH':
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
};
// Install two versions.
await _runDartdev(
fromDartdevSource,
'install',
['.'],
_packageDir.uri,
environment,
);
final installResult = await _runDartdev(
fromDartdevSource,
'install',
[_packageForTest, _packageVersion],
null,
environment,
);
expect(
installResult.stdout,
stringContainsInOrder(['Uninstalling ', _packageForTest]),
);
// `--all` should also report the non-active versions.
Future<List<String>> runInstalled() async {
final installedResult = await _runDartdev(
fromDartdevSource,
'installed',
['--all'],
null,
environment,
);
final installedLines = installedResult.stdout
.split('\n')
.where((e) => e.isNotEmpty)
.toList();
return installedLines;
}
expect(await runInstalled(), hasLength(1));
// `uninstall` uninstalls all versions.
await _runDartdev(
fromDartdevSource,
'uninstall',
[_packageForTest],
null,
environment,
);
expect(await runInstalled(), hasLength(0));
});
});
skippableTest('dart uninstall', timeout: longTimeout, () async {
await inTempDir((tempUri) async {
final environment = {
_dartDirectoryEnvKey: tempUri.toFilePath(),
};
// `uninstall` should have a non-zero exit if nothing was uninstalled.
await _runDartdev(
fromDartdevSource,
'uninstall',
[_packageForTest],
null,
environment,
expectedExitCode: errorExitCode,
);
});
});
skippableTest('dart uninstall while running', timeout: longTimeout, () async {
await inTempDir((tempUri) async {
final binDir = Directory.fromUri(tempUri.resolve('install/bin'));
final environment = {
_dartDirectoryEnvKey: tempUri.toFilePath(),
'PATH':
'${binDir.path}$_pathEnvVarSeparator${Platform.environment['PATH']!}',
};
const packageName = 'test_app';
final dartAppUri = tempUri.resolve('$packageName/');
final pubspec = File.fromUri(dartAppUri.resolve('pubspec.yaml'));
await pubspec.create(recursive: true);
await pubspec.writeAsString(jsonEncode(PubspecYamlFileSyntax(
name: packageName,
environment: EnvironmentSyntax(
sdk: '^${Platform.version.split(' ').first}',
),
executables: {
packageName: packageName,
},
).json));
final mainFile =
File.fromUri(dartAppUri.resolve('bin/$packageName.dart'));
await mainFile.create(recursive: true);
mainFile.writeAsString('''
void main(List<String> args) async {
await Future.delayed(Duration(days: 1000000));
}
''');
Future<RunProcessResult> doInstall(int expectedExitCode) async {
return await _runDartdev(fromDartdevSource, 'install',
[dartAppUri.toFilePath()], null, environment,
expectedExitCode: expectedExitCode);
}
await doInstall(0);
final runningProcess = await Process.start(
packageName,
[],
environment: environment,
runInShell: true,
);
final installWhileRunningResult = await doInstall(
Platform.isWindows ? errorExitCode : 0,
);
if (Platform.isWindows) {
expect(
installWhileRunningResult.stderr,
contains('The application might be in use.'),
);
} else {
expect(installWhileRunningResult.stderr, isEmpty);
}
final uninstallWhileRunningResult = await _runDartdev(
fromDartdevSource,
'uninstall',
[packageName],
null,
environment,
expectedExitCode: Platform.isWindows ? errorExitCode : 0,
);
if (Platform.isWindows) {
expect(
uninstallWhileRunningResult.stderr,
contains('The application might be in use.'),
);
} else {
expect(uninstallWhileRunningResult.stderr, isEmpty);
}
runningProcess.kill();
});
});
}
Future<RunProcessResult> _runDartdev(
bool fromDartdevSource,
String command,
List<String> arguments,
Uri? workingDirectory,
Map<String, String> environment, {
int expectedExitCode = 0,
}) async {
final installResult = await runDart(
arguments: [
if (fromDartdevSource) _dartDevEntryScriptUri.toFilePath(),
command,
...arguments,
],
workingDirectory: workingDirectory,
logger: logger,
environment: environment,
expectExitCodeZero: false,
);
expect(installResult.exitCode, equals(expectedExitCode));
return installResult;
}
/// Runs [_cliToolForTest] and expects the help message.
Future<RunProcessResult> _runToolForTest(
Map<String, String> environment,
) async {
final toolResult = await runProcess(
// Note this has `runInShell: true` under it to ensure PATHEXT is used on
// Windows so that invoking an executable without extension works.
executable: Uri.file(_cliToolForTest),
arguments: ['--help'],
// Run in some unrelated directory ensuring PATH is picked up.
workingDirectory: Directory.systemTemp.uri,
logger: logger,
environment: environment,
);
expect(
toolResult.stdout,
stringContainsInOrder([
'Tools for binary size analysis of Dart VM AOT snapshots.',
]),
);
expect(toolResult.exitCode, 0);
return toolResult;
}