Adding the `pub add` command (#2618)
* Naive implementation of pub add
* Minor touching up
* Resolves packages in memory before getting them
* Moved functions to addCommand
* Warns if dependency is present in dev-dependency or otherwise
* Allow package:any
* Naive implementation of PubspecDescriptor
* Added tests
* Extended add functionality
* Added remove functionality
* Updated add functionality
* Updated tests for add command
* Added tests for remove
* Add more tests
* Added hosted dependencies
* Updated hosted package option
* Added tests
* Throws DataError if result of version resolution was unexpected by user
* Fixes tests
* Updated code according to review comments
* Fixed bug in add to handle the dependencies map not existing.
* Sdk packages support
* Update dependencies
* Updated comments
* Fixed bug in pubspec validation
* Fixed bug in pubspec validation
* Fixed as per review comments
* Removed remove
* Removed more remove stuff
* More minor edits
* Fixes to tests as per commands
* Allowed version constraints in path/git dependencies
* Fixed more tests, improved behavior re: transitive deps
* Updated test for windows
* More windows fixes
* Fixed as per review comments
* Fixed tests
* Fixed nits
* Waiting for yaml_edit:1.0.1
* Updated yaml_edit version
diff --git a/lib/src/command/add.dart b/lib/src/command/add.dart
new file mode 100644
index 0000000..4839545
--- /dev/null
+++ b/lib/src/command/add.dart
@@ -0,0 +1,404 @@
+// Copyright (c) 2020, 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 'package:pub_semver/pub_semver.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import '../command.dart';
+import '../entrypoint.dart';
+import '../exceptions.dart';
+import '../git.dart';
+import '../io.dart';
+import '../log.dart' as log;
+import '../package.dart';
+import '../package_name.dart';
+import '../pubspec.dart';
+import '../solver.dart';
+import '../utils.dart';
+
+/// Handles the `add` pub command. Adds a dependency to `pubspec.yaml` and gets
+/// the package. The user may pass in a git constraint, host url, or path as
+/// requirements. If no such options are passed in, this command will do a
+/// resolution to find the latest version of the package that is compatible with
+/// the other dependencies in `pubpsec.yaml`, and then enter that as the lower
+/// bound in a ^x.y.z constraint.
+///
+/// Currently supports only adding one dependency at a time.
+class AddCommand extends PubCommand {
+ @override
+ String get name => 'add';
+ @override
+ String get description => 'Add a dependency to pubspec.yaml.';
+ @override
+ String get invocation => 'pub add <package>[:<constraint>] [options]';
+ @override
+ String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-add';
+
+ bool get isDev => argResults['dev'];
+ bool get isDryRun => argResults['dry-run'];
+ String get gitUrl => argResults['git-url'];
+ String get gitPath => argResults['git-path'];
+ String get gitRef => argResults['git-ref'];
+ String get hostUrl => argResults['hosted-url'];
+ String get path => argResults['path'];
+ String get sdk => argResults['sdk'];
+
+ bool get hasGitOptions => gitUrl != null || gitRef != null || gitPath != null;
+ bool get hasHostOptions => hostUrl != null;
+
+ AddCommand() {
+ argParser.addFlag('dev',
+ abbr: 'd',
+ negatable: false,
+ help: 'Adds package to the development dependencies instead.');
+
+ argParser.addOption('git-url', help: 'Git URL of the package');
+ argParser.addOption('git-ref',
+ help: 'Git branch or commit to be retrieved');
+ argParser.addOption('git-path', help: 'Path of git package in repository');
+ argParser.addOption('hosted-url', help: 'URL of package host server');
+ argParser.addOption('path', help: 'Local path');
+ argParser.addOption('sdk', help: 'SDK source for package');
+
+ argParser.addFlag('offline',
+ help: 'Use cached packages instead of accessing the network.');
+
+ argParser.addFlag('dry-run',
+ abbr: 'n',
+ negatable: false,
+ help: "Report what dependencies would change but don't change any.");
+
+ argParser.addFlag('precompile',
+ help: 'Precompile executables in immediate dependencies.');
+ }
+
+ @override
+ Future run() async {
+ if (argResults.rest.isEmpty) {
+ usageException('Must specify a package to be added.');
+ }
+
+ final packageInformation = _parsePackage(argResults.rest.first);
+ final package = packageInformation.first;
+
+ /// Perform version resolution in-memory.
+ final updatedPubSpec =
+ await _addPackageToPubspec(entrypoint.root.pubspec, package);
+
+ SolveResult solveResult;
+
+ try {
+ /// Use [SolveType.UPGRADE] to solve for the highest version of [package]
+ /// in case [package] was already a transitive dependency. In the case
+ /// where the user specifies a version constraint, this serves to ensure
+ /// that a resolution exists before we update pubspec.yaml.
+ solveResult = await resolveVersions(
+ SolveType.UPGRADE, cache, Package.inMemory(updatedPubSpec));
+ } on GitException {
+ dataError('Unable to resolve package "${package.name}" with the given '
+ 'git parameters.');
+ } on SolveFailure catch (e) {
+ dataError(e.message);
+ } on WrappedException catch (e) {
+ /// [WrappedException]s may appear if an invalid [hostUrl] is passed in.
+ dataError(e.message);
+ }
+
+ final resultPackage = solveResult.packages
+ .firstWhere((packageId) => packageId.name == package.name);
+
+ /// Assert that [resultPackage] is within the original user's expectations.
+ if (package.constraint != null &&
+ !package.constraint.allows(resultPackage.version)) {
+ if (updatedPubSpec.dependencyOverrides != null &&
+ updatedPubSpec.dependencyOverrides.isNotEmpty) {
+ dataError(
+ '"${package.name}" resolved to "${resultPackage.version}" which '
+ 'does not satisfy constraint "${package.constraint}". This could be '
+ 'caused by "dependency_overrides".');
+ }
+ dataError(
+ '"${package.name}" resolved to "${resultPackage.version}" which '
+ 'does not satisfy constraint "${package.constraint}".');
+ }
+
+ if (isDryRun) {
+ /// Even if it is a dry run, run `acquireDependencies` so that the user
+ /// gets a report on the other packages that might change version due
+ /// to this new dependency.
+ final newRoot = Package.inMemory(updatedPubSpec);
+
+ await Entrypoint.global(newRoot, entrypoint.lockFile, cache,
+ solveResult: solveResult)
+ .acquireDependencies(SolveType.GET,
+ dryRun: true, precompile: argResults['precompile']);
+ } else {
+ /// Update the `pubspec.yaml` before calling [acquireDependencies] to
+ /// ensure that the modification timestamp on `pubspec.lock` and
+ /// `.dart_tool/package_config.json` is newer than `pubspec.yaml`,
+ /// ensuring that [entrypoint.assertUptoDate] will pass.
+ _updatePubspec(resultPackage, packageInformation, isDev);
+
+ /// Create a new [Entrypoint] since we have to reprocess the updated
+ /// pubspec file.
+ await Entrypoint.current(cache).acquireDependencies(SolveType.GET,
+ precompile: argResults['precompile']);
+ }
+
+ if (isOffline) {
+ log.warning('Warning: Packages added when offline may not resolve to '
+ 'the latest compatible version available.');
+ }
+ }
+
+ /// Creates a new in-memory [Pubspec] by adding [package] to the
+ /// dependencies of [original].
+ Future<Pubspec> _addPackageToPubspec(
+ Pubspec original, PackageRange package) async {
+ ArgumentError.checkNotNull(original, 'original');
+ ArgumentError.checkNotNull(package, 'package');
+
+ final dependencies = [...original.dependencies.values];
+ var devDependencies = [...original.devDependencies.values];
+ final dependencyNames = dependencies.map((dependency) => dependency.name);
+ final devDependencyNames =
+ devDependencies.map((devDependency) => devDependency.name);
+
+ if (isDev) {
+ /// TODO(walnut): Change the error message once pub upgrade --bump is
+ /// released
+ if (devDependencyNames.contains(package.name)) {
+ usageException('"${package.name}" is already in "dev_dependencies". '
+ 'Use "pub upgrade ${package.name}" to upgrade to a later version!');
+ }
+
+ /// If package is originally in dependencies and we wish to add it to
+ /// dev_dependencies, this is a redundant change, and we should not
+ /// remove the package from dependencies, since it might cause the user's
+ /// code to break.
+ if (dependencyNames.contains(package.name)) {
+ usageException('"${package.name}" is already in "dependencies". '
+ 'Use "pub remove ${package.name}" to remove it before adding it '
+ 'to "dev_dependencies"');
+ }
+
+ devDependencies.add(package);
+ } else {
+ /// TODO(walnut): Change the error message once pub upgrade --bump is
+ /// released
+ if (dependencyNames.contains(package.name)) {
+ usageException('"${package.name}" is already in "dependencies". '
+ 'Use "pub upgrade ${package.name}" to upgrade to a later version!');
+ }
+
+ /// If package is originally in dev_dependencies and we wish to add it to
+ /// dependencies, we remove the package from dev_dependencies, since it is
+ /// now redundant.
+ if (devDependencyNames.contains(package.name)) {
+ log.message('"${package.name}" was found in dev_dependencies. '
+ 'Removing "${package.name}" and adding it to dependencies instead.');
+ devDependencies =
+ devDependencies.where((d) => d.name != package.name).toList();
+ }
+
+ dependencies.add(package);
+ }
+
+ return Pubspec(
+ original.name,
+ version: original.version,
+ sdkConstraints: original.sdkConstraints,
+ dependencies: dependencies,
+ devDependencies: devDependencies,
+ dependencyOverrides: original.dependencyOverrides.values,
+ );
+ }
+
+ /// Parse [package] to return the corresponding [PackageRange], as well as its
+ /// representation in `pubspec.yaml`.
+ ///
+ /// [package] must be written in the format
+ /// `<package-name>[:<version-constraint>]`, where quotations should be used
+ /// if necessary.
+ ///
+ /// Examples:
+ /// ```
+ /// retry
+ /// retry:2.0.0
+ /// retry:^2.0.0
+ /// retry:'>=2.0.0'
+ /// retry:'>2.0.0 <3.0.1'
+ /// 'retry:>2.0.0 <3.0.1'
+ /// retry:any
+ /// ```
+ ///
+ /// If a version constraint is provided when the `--path` or any of the
+ /// `--git-<option>` options are used, a [PackageParseError] will be thrown.
+ ///
+ /// Packages must either be a git, hosted, sdk, or path package. Mixing of
+ /// options is not allowed and will cause a [PackageParseError] to be thrown.
+ ///
+ /// If any of the other git options are defined when `--git-url` is not
+ /// defined, an error will be thrown.
+ Pair<PackageRange, dynamic> _parsePackage(String package) {
+ ArgumentError.checkNotNull(package, 'package');
+
+ final _conflictingFlagSets = [
+ ['git-url', 'git-ref', 'git-path'],
+ ['hosted-url'],
+ ['path'],
+ ['sdk'],
+ ];
+
+ for (final flag
+ in _conflictingFlagSets.expand((s) => s).where(argResults.wasParsed)) {
+ final conflictingFlag = _conflictingFlagSets
+ .where((s) => !s.contains(flag))
+ .expand((s) => s)
+ .firstWhere(argResults.wasParsed, orElse: () => null);
+ if (conflictingFlag != null) {
+ usageException(
+ 'Packages can only have one source, "pub add" flags "--$flag" and '
+ '"--$conflictingFlag" are conflicting.');
+ }
+ }
+
+ /// The package to be added, along with the user-defined package constraints
+ /// if present.
+ PackageRange packageRange;
+
+ /// The entry to be added to the pubspec. Assigned dynamic because it can
+ /// take on either a string for simple version constraints or a map for
+ /// more complicated hosted/git options.
+ dynamic pubspecInformation;
+
+ final splitPackage = package.split(':');
+ final packageName = splitPackage[0];
+
+ /// There shouldn't be more than one `:` in the package information
+ if (splitPackage.length > 2) {
+ usageException('Invalid package and version constraint: $package');
+ }
+
+ /// We want to allow for [constraint] to take on a `null` value here to
+ /// preserve the fact that the user did not specify a constraint.
+ VersionConstraint constraint;
+
+ try {
+ constraint = splitPackage.length == 2
+ ? VersionConstraint.parse(splitPackage[1])
+ : null;
+ } on FormatException catch (e) {
+ usageException('Invalid version constraint: ${e.message}');
+ }
+
+ /// Determine the relevant [packageRange] and [pubspecInformation] depending
+ /// on the type of package.
+ if (hasGitOptions) {
+ dynamic git;
+
+ if (gitUrl == null) {
+ usageException('The `--git-url` is required for git dependencies.');
+ }
+
+ /// Process the git options to return the simplest representation to be
+ /// added to the pubspec.
+ if (gitRef == null && gitPath == null) {
+ git = gitUrl;
+ } else {
+ git = {'url': gitUrl, 'ref': gitRef, 'path': gitPath};
+ git.removeWhere((key, value) => value == null);
+ }
+
+ packageRange = cache.sources['git']
+ .parseRef(packageName, git)
+ .withConstraint(constraint ?? VersionConstraint.any);
+ pubspecInformation = {'git': git};
+ } else if (path != null) {
+ packageRange = cache.sources['path']
+ .parseRef(packageName, path, containingPath: entrypoint.pubspecPath)
+ .withConstraint(constraint ?? VersionConstraint.any);
+ pubspecInformation = {'path': path};
+ } else if (sdk != null) {
+ packageRange = cache.sources['sdk']
+ .parseRef(packageName, sdk)
+ .withConstraint(constraint ?? VersionConstraint.any);
+ pubspecInformation = {'sdk': sdk};
+ } else {
+ final hostInfo =
+ hasHostOptions ? {'url': hostUrl, 'name': packageName} : null;
+
+ if (hostInfo == null) {
+ pubspecInformation = constraint?.toString();
+ } else {
+ pubspecInformation = {'hosted': hostInfo};
+ }
+
+ packageRange = PackageRange(packageName, cache.sources['hosted'],
+ constraint ?? VersionConstraint.any, hostInfo ?? packageName);
+ }
+
+ if (pubspecInformation is Map && constraint != null) {
+ /// We cannot simply assign the value of version since it is likely that
+ /// [pubspecInformation] takes on the type
+ /// [Map<String, Map<String, String>>]
+ pubspecInformation = {
+ ...pubspecInformation,
+ 'version': constraint.toString()
+ };
+ }
+
+ return Pair(packageRange, pubspecInformation);
+ }
+
+ /// Writes the changes to the pubspec file.
+ void _updatePubspec(PackageId resultPackage,
+ Pair<PackageRange, dynamic> packageInformation, bool isDevelopment) {
+ ArgumentError.checkNotNull(resultPackage, 'resultPackage');
+ ArgumentError.checkNotNull(packageInformation, 'pubspecInformation');
+
+ final package = packageInformation.first;
+ var pubspecInformation = packageInformation.last;
+
+ if ((sdk != null || hasHostOptions) &&
+ pubspecInformation is Map &&
+ pubspecInformation['version'] == null) {
+ /// We cannot simply assign the value of version since it is likely that
+ /// [pubspecInformation] takes on the type
+ /// [Map<String, Map<String, String>>]
+ pubspecInformation = {
+ ...pubspecInformation,
+ 'version': '^${resultPackage.version}'
+ };
+ }
+
+ final dependencyKey = isDevelopment ? 'dev_dependencies' : 'dependencies';
+ final packagePath = [dependencyKey, package.name];
+
+ final yamlEditor = YamlEditor(readTextFile(entrypoint.pubspecPath));
+
+ /// Handle situations where the user might not have the dependencies or
+ /// dev_dependencies map.
+ if (yamlEditor.parseAt([dependencyKey], orElse: () => null)?.value ==
+ null) {
+ yamlEditor.update([dependencyKey],
+ {package.name: pubspecInformation ?? '^${resultPackage.version}'});
+ } else {
+ yamlEditor.update(
+ packagePath, pubspecInformation ?? '^${resultPackage.version}');
+ }
+
+ /// Remove the package from dev_dependencies if we are adding it to
+ /// dependencies. Refer to [_addPackageToPubspec] for additional discussion.
+ if (!isDevelopment &&
+ yamlEditor.parseAt(['dev_dependencies', package.name],
+ orElse: () => null) !=
+ null) {
+ yamlEditor.remove(['dev_dependencies', package.name]);
+ }
+
+ /// Windows line endings are already handled by [yamlEditor]
+ writeTextFile(entrypoint.pubspecPath, yamlEditor.toString());
+ }
+}
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index 8065b78..4a3f547 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -11,6 +11,7 @@
import 'package:path/path.dart' as p;
import 'command.dart' show pubCommandAliases, lineLength;
+import 'command/add.dart';
import 'command/build.dart';
import 'command/cache.dart';
import 'command/deps.dart';
@@ -104,6 +105,7 @@
argParser.addFlag('verbose',
abbr: 'v', negatable: false, help: 'Shortcut for "--verbosity=all".');
+ addCommand(AddCommand());
addCommand(BuildCommand());
addCommand(CacheCommand());
addCommand(DepsCommand());
diff --git a/pubspec.yaml b/pubspec.yaml
index 30b464d..076e303 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -28,6 +28,7 @@
source_span: ^1.4.0
stack_trace: ^1.0.0
yaml: ^2.2.0
+ yaml_edit: ^1.0.1
dev_dependencies:
shelf_test_handler: ^1.0.0
diff --git a/test/add/common/add_test.dart b/test/add/common/add_test.dart
new file mode 100644
index 0000000..737dfa6
--- /dev/null
+++ b/test/add/common/add_test.dart
@@ -0,0 +1,809 @@
+// Copyright (c) 2020, 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' show File;
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+import 'package:pub/src/exit_codes.dart' as exit_codes;
+
+import '../../descriptor.dart' as d;
+import '../../descriptor/yaml.dart';
+import '../../test_pub.dart';
+
+void main() {
+ test('URL encodes the package name', () async {
+ await serveNoPackages();
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['bad name!:1.2.3'],
+ error: allOf([
+ contains(
+ "Because myapp depends on bad name! any which doesn't exist (could "
+ 'not find package bad name! at http://localhost:'),
+ contains('), version solving failed.')
+ ]),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({}).validate();
+
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ group('normally', () {
+ test('adds a package from a pub server', () async {
+ await servePackages((builder) => builder.serve('foo', '1.2.3'));
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: ['foo:1.2.3']);
+
+ await d.cacheDir({'foo': '1.2.3'}).validate();
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+ await d.appDir({'foo': '1.2.3'}).validate();
+ });
+
+ test('dry run does not actually add the package or modify the pubspec',
+ () async {
+ await servePackages((builder) => builder.serve('foo', '1.2.3'));
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['foo:1.2.3', '--dry-run'],
+ output: allOf([
+ contains('Would change 1 dependency'),
+ contains('+ foo 1.2.3')
+ ]));
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test(
+ 'adds a package from a pub server even when dependencies key does not exist',
+ () async {
+ await servePackages((builder) => builder.serve('foo', '1.2.3'));
+
+ await d.dir(appPath, [
+ d.pubspec({'name': 'myapp'})
+ ]).create();
+
+ await pubAdd(args: ['foo:1.2.3']);
+
+ await d.cacheDir({'foo': '1.2.3'}).validate();
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+ await d.appDir({'foo': '1.2.3'}).validate();
+ });
+
+ group('warns user to use pub upgrade if package exists', () {
+ test('if package is added without a version constraint', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.appDir({'foo': '1.2.2'}).create();
+
+ await pubAdd(
+ args: ['foo'],
+ exitCode: exit_codes.USAGE,
+ error:
+ contains('"foo" is already in "dependencies". Use "pub upgrade '
+ 'foo" to upgrade to a later\nversion!'));
+
+ await d.appDir({'foo': '1.2.2'}).validate();
+ });
+
+ test('if package is added with a specific version constraint', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.appDir({'foo': '1.2.2'}).create();
+
+ await pubAdd(
+ args: ['foo:1.2.3'],
+ exitCode: exit_codes.USAGE,
+ error:
+ contains('"foo" is already in "dependencies". Use "pub upgrade '
+ 'foo" to upgrade to a later\nversion!'));
+
+ await d.appDir({'foo': '1.2.2'}).validate();
+ });
+
+ test('if package is added with a version constraint range', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.appDir({'foo': '1.2.2'}).create();
+
+ await pubAdd(
+ args: ['foo:>=1.2.2'],
+ exitCode: exit_codes.USAGE,
+ error:
+ contains('"foo" is already in "dependencies". Use "pub upgrade '
+ 'foo" to upgrade to a later\nversion!'));
+
+ await d.appDir({'foo': '1.2.2'}).validate();
+ });
+ });
+
+ test('removes dev_dependency and add to normal dependency', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dev_dependencies': {'foo': '1.2.2'}
+ })
+ ]).create();
+
+ await pubAdd(
+ args: ['foo:1.2.3'],
+ output: contains(
+ '"foo" was found in dev_dependencies. Removing "foo" and '
+ 'adding it to dependencies instead.'));
+
+ await d.cacheDir({'foo': '1.2.3'}).validate();
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'foo': '1.2.3'},
+ 'dev_dependencies': {}
+ })
+ ]).validate();
+ });
+
+ group('dependency override', () {
+ test('passes if package does not specify a range', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).create();
+
+ await pubAdd(args: ['foo']);
+
+ await d.cacheDir({'foo': '1.2.2'}).validate();
+ await d.appPackagesFile({'foo': '1.2.2'}).validate();
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'foo': '^1.2.2'},
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).validate();
+ });
+
+ test('passes if constraint matches git dependency override', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ });
+
+ await d.git('foo.git',
+ [d.libDir('foo'), d.libPubspec('foo', '1.2.3')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'git': '../foo.git'}
+ }
+ })
+ ]).create();
+
+ await pubAdd(args: ['foo:1.2.3']);
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'foo': '1.2.3'},
+ 'dependency_overrides': {
+ 'foo': {'git': '../foo.git'}
+ }
+ })
+ ]).validate();
+ });
+
+ test('passes if constraint matches path dependency override', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.2');
+ });
+ await d.dir(
+ 'foo', [d.libDir('foo'), d.libPubspec('foo', '1.2.2')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'path': '../foo'}
+ }
+ })
+ ]).create();
+
+ await pubAdd(args: ['foo:1.2.2']);
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'foo': '1.2.2'},
+ 'dependency_overrides': {
+ 'foo': {'path': '../foo'}
+ }
+ })
+ ]).validate();
+ });
+
+ test('fails with bad version constraint', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({'name': 'myapp', 'dependencies': {}})
+ ]).create();
+
+ await pubAdd(
+ args: ['foo:one-two-three'],
+ exitCode: exit_codes.USAGE,
+ error: contains('Invalid version constraint: Could '
+ 'not parse version "one-two-three".'));
+
+ await d.dir(appPath, [
+ d.pubspec({'name': 'myapp', 'dependencies': {}}),
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('fails if constraint does not match override', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).create();
+
+ await pubAdd(
+ args: ['foo:1.2.3'],
+ exitCode: exit_codes.DATA,
+ error: contains(
+ '"foo" resolved to "1.2.2" which does not satisfy constraint '
+ '"1.2.3". This could be caused by "dependency_overrides".'));
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {'foo': '1.2.2'}
+ }),
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('fails if constraint matches git dependency override', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ });
+
+ await d.git('foo.git',
+ [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'git': '../foo.git'}
+ }
+ })
+ ]).create();
+
+ await pubAdd(
+ args: ['foo:1.2.3'],
+ exitCode: exit_codes.DATA,
+ error: contains(
+ '"foo" resolved to "1.0.0" which does not satisfy constraint '
+ '"1.2.3". This could be caused by "dependency_overrides".'));
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'git': '../foo.git'}
+ }
+ }),
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('fails if constraint does not match path dependency override',
+ () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.2');
+ });
+ await d.dir(
+ 'foo', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'path': '../foo'}
+ }
+ })
+ ]).create();
+
+ await pubAdd(
+ args: ['foo:1.2.2'],
+ exitCode: exit_codes.DATA,
+ error: contains(
+ '"foo" resolved to "1.0.0" which does not satisfy constraint '
+ '"1.2.2". This could be caused by "dependency_overrides".'));
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'path': '../foo'}
+ }
+ }),
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+ });
+ });
+
+ group('--dev', () {
+ test('--dev adds packages to dev_dependencies instead', () async {
+ await servePackages((builder) => builder.serve('foo', '1.2.3'));
+
+ await d.dir(appPath, [
+ d.pubspec({'name': 'myapp', 'dev_dependencies': {}})
+ ]).create();
+
+ await pubAdd(args: ['--dev', 'foo:1.2.3']);
+
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '1.2.3'}
+ })
+ ]).validate();
+ });
+
+ group('warns user to use pub upgrade if package exists', () {
+ test('if package is added without a version constraint', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '1.2.2'}
+ })
+ ]).create();
+
+ await pubAdd(
+ args: ['foo', '--dev'],
+ exitCode: exit_codes.USAGE,
+ error: contains(
+ '"foo" is already in "dev_dependencies". Use "pub upgrade '
+ 'foo" to upgrade to a\nlater version!'));
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '1.2.2'}
+ })
+ ]).validate();
+ });
+
+ test('if package is added with a specific version constraint', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '1.2.2'}
+ })
+ ]).create();
+
+ await pubAdd(
+ args: ['foo:1.2.3', '--dev'],
+ exitCode: exit_codes.USAGE,
+ error: contains(
+ '"foo" is already in "dev_dependencies". Use "pub upgrade '
+ 'foo" to upgrade to a\nlater version!'));
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '1.2.2'}
+ })
+ ]).validate();
+ });
+
+ test('if package is added with a version constraint range', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '1.2.2'}
+ })
+ ]).create();
+
+ await pubAdd(
+ args: ['foo:>=1.2.2', '--dev'],
+ exitCode: exit_codes.USAGE,
+ error: contains(
+ '"foo" is already in "dev_dependencies". Use "pub upgrade '
+ 'foo" to upgrade to a\nlater version!'));
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '1.2.2'}
+ })
+ ]).validate();
+ });
+ });
+
+ group('dependency override', () {
+ test('passes if package does not specify a range', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {},
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).create();
+
+ await pubAdd(args: ['foo', '--dev']);
+
+ await d.cacheDir({'foo': '1.2.2'}).validate();
+ await d.appPackagesFile({'foo': '1.2.2'}).validate();
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '^1.2.2'},
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).validate();
+ });
+
+ test('passes if constraint is git dependency', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ });
+
+ await d.git('foo.git',
+ [d.libDir('foo'), d.libPubspec('foo', '1.2.3')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'git': '../foo.git'}
+ }
+ })
+ ]).create();
+
+ await pubAdd(args: ['foo:1.2.3', '--dev']);
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '1.2.3'},
+ 'dependency_overrides': {
+ 'foo': {'git': '../foo.git'}
+ }
+ })
+ ]).validate();
+ });
+
+ test('passes if constraint matches path dependency override', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.2');
+ });
+ await d.dir(
+ 'foo', [d.libDir('foo'), d.libPubspec('foo', '1.2.2')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'path': '../foo'}
+ }
+ })
+ ]).create();
+
+ await pubAdd(args: ['foo:1.2.2', '--dev']);
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {'foo': '1.2.2'},
+ 'dependency_overrides': {
+ 'foo': {'path': '../foo'}
+ }
+ })
+ ]).validate();
+ });
+
+ test('fails if constraint does not match override', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {},
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).create();
+
+ await pubAdd(
+ args: ['foo:1.2.3', '--dev'],
+ exitCode: exit_codes.DATA,
+ error: contains(
+ '"foo" resolved to "1.2.2" which does not satisfy constraint '
+ '"1.2.3". This could be caused by "dependency_overrides".'));
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {},
+ 'dependency_overrides': {'foo': '1.2.2'}
+ }),
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('fails if constraint matches git dependency override', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ });
+
+ await d.git('foo.git',
+ [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'git': '../foo.git'}
+ }
+ })
+ ]).create();
+
+ await pubAdd(
+ args: ['foo:1.2.3'],
+ exitCode: exit_codes.DATA,
+ error: contains(
+ '"foo" resolved to "1.0.0" which does not satisfy constraint '
+ '"1.2.3". This could be caused by "dependency_overrides".'));
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'git': '../foo.git'}
+ }
+ }),
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('fails if constraint does not match path dependency override',
+ () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.2');
+ });
+ await d.dir(
+ 'foo', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'path': '../foo'}
+ }
+ })
+ ]).create();
+
+ await pubAdd(
+ args: ['foo:1.2.2', '--dev'],
+ exitCode: exit_codes.DATA,
+ error: contains(
+ '"foo" resolved to "1.0.0" which does not satisfy constraint '
+ '"1.2.2". This could be caused by "dependency_overrides".'));
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dev_dependencies': {},
+ 'dependency_overrides': {
+ 'foo': {'path': '../foo'}
+ }
+ }),
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+ });
+
+ test(
+ 'prints information saying that package is already a dependency if it '
+ 'already exists and exits with a usage exception', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'foo': '1.2.2'},
+ 'dev_dependencies': {}
+ })
+ ]).create();
+
+ await pubAdd(
+ args: ['foo:1.2.3', '--dev'],
+ error: contains('"foo" is already in "dependencies". Use '
+ '"pub remove foo" to remove it before\nadding it to '
+ '"dev_dependencies"'),
+ exitCode: exit_codes.USAGE);
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {'foo': '1.2.2'},
+ 'dev_dependencies': {}
+ }),
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+ });
+
+ /// Differs from the previous test because this tests YAML in flow format.
+ test('adds to empty ', () async {
+ await servePackages((builder) {
+ builder.serve('bar', '1.0.0');
+ });
+
+ final initialPubspec = YamlDescriptor('pubspec.yaml', '''
+ name: myapp
+ dependencies:
+''');
+ await d.dir(appPath, [initialPubspec]).create();
+
+ await pubGet();
+
+ await pubAdd(args: ['bar']);
+
+ final finalPubspec = YamlDescriptor('pubspec.yaml', '''
+ name: myapp
+ dependencies:
+ bar: ^1.0.0''');
+ await d.dir(appPath, [finalPubspec]).validate();
+ final fullPath = p.join(d.sandbox, appPath, 'pubspec.yaml');
+
+ expect(File(fullPath).existsSync(), true);
+
+ final contents = File(fullPath).readAsStringSync();
+ expect(contents, await finalPubspec.read());
+ });
+
+ test('preserves comments', () async {
+ await servePackages((builder) {
+ builder.serve('bar', '1.0.0');
+ builder.serve('foo', '1.0.0');
+ });
+
+ final initialPubspec = YamlDescriptor('pubspec.yaml', '''
+ name: myapp
+ dependencies: # comment A
+ # comment B
+ foo: 1.0.0 # comment C
+ # comment D
+ ''');
+ await d.dir(appPath, [initialPubspec]).create();
+
+ await pubGet();
+
+ await pubAdd(args: ['bar']);
+
+ final finalPubspec = YamlDescriptor('pubspec.yaml', '''
+ name: myapp
+ dependencies: # comment A
+ # comment B
+ bar: ^1.0.0
+ foo: 1.0.0 # comment C
+ # comment D
+ ''');
+ await d.dir(appPath, [finalPubspec]).validate();
+ final fullPath = p.join(d.sandbox, appPath, 'pubspec.yaml');
+
+ expect(File(fullPath).existsSync(), true);
+
+ final contents = File(fullPath).readAsStringSync();
+ expect(contents, await finalPubspec.read());
+ });
+}
diff --git a/test/add/common/invalid_options.dart b/test/add/common/invalid_options.dart
new file mode 100644
index 0000000..9b3a8f0
--- /dev/null
+++ b/test/add/common/invalid_options.dart
@@ -0,0 +1,112 @@
+// Copyright (c) 2020, 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 'package:pub/src/exit_codes.dart' as exit_codes;
+import 'package:test/test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+void main() {
+ test('cannot use both --path and --git-<option> flags', () async {
+ ensureGit();
+
+ await d.git(
+ 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+ await d
+ .dir('bar', [d.libDir('bar'), d.libPubspec('foo', '0.0.1')]).create();
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['foo', '--git-url', '../foo.git', '--path', '../bar'],
+ error: allOf([
+ contains('Packages can only have one source, pub add flags '
+ '"--git-url" and "--path" are'),
+ contains('conflicting.')
+ ]),
+ exitCode: exit_codes.USAGE);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('cannot use both --path and --host-<option> flags', () async {
+ // Make the default server serve errors. Only the custom server should
+ // be accessed.
+ await serveErrors();
+
+ final server = await PackageServer.start((builder) {
+ builder.serve('foo', '1.2.3');
+ });
+
+ await d
+ .dir('bar', [d.libDir('bar'), d.libPubspec('foo', '0.0.1')]).create();
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: [
+ 'foo',
+ '--hosted-url',
+ 'http://localhost:${server.port}',
+ '--path',
+ '../bar'
+ ],
+ error: allOf([
+ contains('Packages can only have one source, pub add flags '
+ '"--hosted-url" and "--path" are'),
+ contains('conflicting.')
+ ]),
+ exitCode: exit_codes.USAGE);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('cannot use both --hosted-url and --git-<option> flags', () async {
+ // Make the default server serve errors. Only the custom server should
+ // be accessed.
+ await serveErrors();
+
+ final server = await PackageServer.start((builder) {
+ builder.serve('foo', '1.2.3');
+ });
+
+ ensureGit();
+
+ await d.git(
+ 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: [
+ 'foo',
+ '--hosted-url',
+ 'http://localhost:${server.port}',
+ '--git-url',
+ '../foo.git'
+ ],
+ error: allOf([
+ contains('Packages can only have one source, pub add flags '
+ '"--git-url" and "--hosted-url"'),
+ contains('are conflicting.')
+ ]),
+ exitCode: exit_codes.USAGE);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+}
diff --git a/test/add/common/version_constraint_test.dart b/test/add/common/version_constraint_test.dart
new file mode 100644
index 0000000..d865727
--- /dev/null
+++ b/test/add/common/version_constraint_test.dart
@@ -0,0 +1,151 @@
+// Copyright (c) 2020, 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 'package:pub/src/exit_codes.dart' as exit_codes;
+import 'package:test/test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+void main() {
+ test('allows empty version constraint', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '0.2.3');
+ builder.serve('foo', '1.0.1');
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '2.0.0-dev');
+ builder.serve('foo', '1.3.4-dev');
+ });
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: ['foo']);
+
+ await d.cacheDir({'foo': '1.2.3'}).validate();
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+ await d.appDir({'foo': '^1.2.3'}).validate();
+ });
+
+ test('allows specific version constraint', () async {
+ await servePackages((builder) => builder.serve('foo', '1.2.3'));
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: ['foo:1.2.3']);
+
+ await d.cacheDir({'foo': '1.2.3'}).validate();
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+ await d.appDir({'foo': '1.2.3'}).validate();
+ });
+
+ test('allows specific pre-release version constraint', () async {
+ await servePackages((builder) => builder.serve('foo', '1.2.3-dev'));
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: ['foo:1.2.3-dev']);
+
+ await d.cacheDir({'foo': '1.2.3-dev'}).validate();
+ await d.appPackagesFile({'foo': '1.2.3-dev'}).validate();
+ await d.appDir({'foo': '1.2.3-dev'}).validate();
+ });
+
+ test('allows the "any" version constraint', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '0.2.3');
+ builder.serve('foo', '1.0.1');
+ builder.serve('foo', '1.2.3');
+ builder.serve('foo', '2.0.0-dev');
+ builder.serve('foo', '1.3.4-dev');
+ });
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: ['foo:any']);
+
+ await d.cacheDir({'foo': '1.2.3'}).validate();
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+ await d.appDir({'foo': 'any'}).validate();
+ });
+
+ test('allows version constraint range', () async {
+ await servePackages((builder) => builder.serve('foo', '1.2.3'));
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: ['foo:>1.2.0 <2.0.0']);
+
+ await d.cacheDir({'foo': '1.2.3'}).validate();
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+ await d.appDir({'foo': '>1.2.0 <2.0.0'}).validate();
+ });
+
+ test(
+ 'empty constraint allows it to choose the latest version not in conflict',
+ () async {
+ await servePackages((builder) {
+ builder.serve('foo', '0.1.0');
+ builder.serve('foo', '1.2.3', deps: {'bar': '2.0.4'});
+ builder.serve('bar', '2.0.3');
+ builder.serve('bar', '2.0.4');
+ });
+
+ await d.appDir({'bar': '2.0.3'}).create();
+
+ await pubAdd(args: ['foo']);
+
+ await d.appDir({'foo': '^0.1.0', 'bar': '2.0.3'}).validate();
+
+ await d.cacheDir({'foo': '0.1.0', 'bar': '2.0.3'}).validate();
+ await d.appPackagesFile({'foo': '0.1.0', 'bar': '2.0.3'}).validate();
+ });
+
+ group('does not update pubspec if no available version found', () {
+ test('simple', () async {
+ await servePackages((builder) => builder.serve('foo', '1.0.3'));
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['foo:>1.2.0 <2.0.0'],
+ error: contains(
+ "Because myapp depends on foo >1.2.0 <2.0.0 which doesn't "
+ 'match any versions, version solving failed.'),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ // The lockfile should not be created.
+ d.nothing('pubspec.lock'),
+ // The ".packages" file should not have been created.
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('transitive', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.3', deps: {'bar': '2.0.4'});
+ builder.serve('bar', '2.0.3');
+ builder.serve('bar', '2.0.4');
+ });
+
+ await d.appDir({'bar': '2.0.3'}).create();
+
+ await pubAdd(
+ args: ['foo:1.2.3'],
+ error: contains(
+ 'Because every version of foo depends on bar 2.0.4 and myapp '
+ 'depends on bar 2.0.3, foo is forbidden.'),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({'bar': '2.0.3'}).validate();
+ await d.dir(appPath, [
+ // The lockfile should not be created.
+ d.nothing('pubspec.lock'),
+ // The ".packages" file should not have been created.
+ d.nothing('.packages'),
+ ]).validate();
+ });
+ });
+}
diff --git a/test/add/common/version_resolution_test.dart b/test/add/common/version_resolution_test.dart
new file mode 100644
index 0000000..9d7d2a6
--- /dev/null
+++ b/test/add/common/version_resolution_test.dart
@@ -0,0 +1,89 @@
+// Copyright (c) 2020, 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 'package:test/test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+/// This test suite attempts to cover the edge cases of version resolution
+/// with regards to transitive dependencies.
+void main() {
+ test('unlocks transitive dependencies', () async {
+ /// The server used to only have the foo v3.2.1 as the latest,
+ /// so pub get will create a pubspec.lock to foo 3.2.1
+ await servePackages((builder) {
+ builder.serve('foo', '3.2.1');
+ builder.serve('bar', '1.0.0', deps: {'foo': '^3.2.1'});
+ });
+
+ await d.appDir({'bar': '1.0.0'}).create();
+ await pubGet();
+
+ /// foo's package creator releases a newer version of foo, and we
+ /// want to test that this is what the user gets when they run
+ /// pub add foo.
+ globalPackageServer.add((builder) {
+ builder.serve('foo', '3.5.0');
+ builder.serve('foo', '3.1.0');
+ builder.serve('foo', '2.5.0');
+ });
+
+ await pubAdd(args: ['foo']);
+
+ await d.appDir({'foo': '^3.5.0', 'bar': '1.0.0'}).validate();
+ await d.cacheDir({'foo': '3.5.0', 'bar': '1.0.0'}).validate();
+ await d.appPackagesFile({'foo': '3.5.0', 'bar': '1.0.0'}).validate();
+ });
+
+ test('chooses the appropriate version to not break other dependencies',
+ () async {
+ /// The server used to only have the foo v3.2.1 as the latest,
+ /// so pub get will create a pubspec.lock to foo 3.2.1
+ await servePackages((builder) {
+ builder.serve('foo', '3.2.1');
+ builder.serve('bar', '1.0.0', deps: {'foo': '^3.2.1'});
+ });
+
+ await d.appDir({'bar': '1.0.0'}).create();
+ await pubGet();
+
+ globalPackageServer.add((builder) {
+ builder.serve('foo', '4.0.0');
+ builder.serve('foo', '2.0.0');
+ });
+
+ await pubAdd(args: ['foo']);
+
+ await d.appDir({'foo': '^3.2.1', 'bar': '1.0.0'}).validate();
+ await d.cacheDir({'foo': '3.2.1', 'bar': '1.0.0'}).validate();
+ await d.appPackagesFile({'foo': '3.2.1', 'bar': '1.0.0'}).validate();
+ });
+
+ test('may upgrade other packages if they allow a later version to be chosen',
+ () async {
+ /// The server used to only have the foo v3.2.1 as the latest,
+ /// so pub get will create a pubspec.lock to foo 3.2.1
+ await servePackages((builder) {
+ builder.serve('foo', '3.2.1');
+ builder.serve('bar', '1.0.0', deps: {'foo': '^3.2.1'});
+ });
+
+ await d.appDir({'bar': '^1.0.0'}).create();
+ await pubGet();
+
+ globalPackageServer.add((builder) {
+ builder.serve('foo', '5.0.0');
+ builder.serve('foo', '4.0.0');
+ builder.serve('foo', '2.0.0');
+ builder.serve('bar', '1.5.0', deps: {'foo': '^4.0.0'});
+ });
+
+ await pubAdd(args: ['foo']);
+
+ await d.appDir({'foo': '^4.0.0', 'bar': '^1.0.0'}).validate();
+ await d.cacheDir({'foo': '4.0.0', 'bar': '1.5.0'}).validate();
+ await d.appPackagesFile({'foo': '4.0.0', 'bar': '1.5.0'}).validate();
+ });
+}
diff --git a/test/add/git/git_test.dart b/test/add/git/git_test.dart
new file mode 100644
index 0000000..305b6a6
--- /dev/null
+++ b/test/add/git/git_test.dart
@@ -0,0 +1,146 @@
+// Copyright (c) 2020, 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 'package:pub/src/exit_codes.dart' as exit_codes;
+import 'package:test/test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+void main() {
+ test('adds a package from git', () async {
+ ensureGit();
+
+ await d.git(
+ 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: ['foo', '--git-url', '../foo.git']);
+
+ await d.dir(cachePath, [
+ d.dir('git', [
+ d.dir('cache', [d.gitPackageRepoCacheDir('foo')]),
+ d.gitPackageRevisionCacheDir('foo')
+ ])
+ ]).validate();
+
+ await d.appDir({
+ 'foo': {'git': '../foo.git'}
+ }).validate();
+ });
+
+ test('adds a package from git with version constraint', () async {
+ ensureGit();
+
+ await d.git(
+ 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: ['foo:1.0.0', '--git-url', '../foo.git']);
+
+ await d.dir(cachePath, [
+ d.dir('git', [
+ d.dir('cache', [d.gitPackageRepoCacheDir('foo')]),
+ d.gitPackageRevisionCacheDir('foo')
+ ])
+ ]).validate();
+
+ await d.appDir({
+ 'foo': {'git': '../foo.git', 'version': '1.0.0'}
+ }).validate();
+ });
+
+ test('fails when adding with an invalid version constraint', () async {
+ ensureGit();
+
+ await d.git(
+ 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['foo:2.0.0', '--git-url', '../foo.git'],
+ error: equalsIgnoringWhitespace(
+ 'Because myapp depends on foo 2.0.0 from git which doesn\'t match '
+ 'any versions, version solving failed.'),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('fails when adding from an invalid url', () async {
+ ensureGit();
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['foo', '--git-url', '../foo.git'],
+ error: contains('Unable to resolve package "foo" with the given '
+ 'git parameters'),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('fails if git-url is not declared', () async {
+ ensureGit();
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['foo', '--git-ref', 'master'],
+ error: contains('The `--git-url` is required for git dependencies.'),
+ exitCode: exit_codes.USAGE);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('can be overriden by dependency override', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.2');
+ });
+
+ await d.git(
+ 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).create();
+
+ await pubAdd(args: ['foo', '--git-url', '../foo.git']);
+
+ await d.cacheDir({'foo': '1.2.2'}).validate();
+ await d.appPackagesFile({'foo': '1.2.2'}).validate();
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {
+ 'foo': {'git': '../foo.git'}
+ },
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).validate();
+ });
+}
diff --git a/test/add/git/ref_test.dart b/test/add/git/ref_test.dart
new file mode 100644
index 0000000..11dfc0b
--- /dev/null
+++ b/test/add/git/ref_test.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2020, 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 'package:pub/src/exit_codes.dart' as exit_codes;
+import 'package:test/test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+void main() {
+ test('adds a package from git with ref', () async {
+ ensureGit();
+
+ final repo = d.git(
+ 'foo.git', [d.libDir('foo', 'foo 1'), d.libPubspec('foo', '1.0.0')]);
+ await repo.create();
+ await repo.runGit(['branch', 'old']);
+
+ await d.git('foo.git',
+ [d.libDir('foo', 'foo 2'), d.libPubspec('foo', '1.0.0')]).commit();
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: ['foo', '--git-url', '../foo.git', '--git-ref', 'old']);
+
+ await d.dir(cachePath, [
+ d.dir('git', [
+ d.dir('cache', [
+ d.gitPackageRepoCacheDir('foo'),
+ ]),
+ d.gitPackageRevisionCacheDir('foo', modifier: 1),
+ ])
+ ]).validate();
+
+ await d.appDir({
+ 'foo': {
+ 'git': {'url': '../foo.git', 'ref': 'old'}
+ }
+ }).validate();
+ });
+
+ test('fails when adding from an invalid ref', () async {
+ ensureGit();
+
+ final repo = d.git(
+ 'foo.git', [d.libDir('foo', 'foo 1'), d.libPubspec('foo', '1.0.0')]);
+ await repo.create();
+ await repo.runGit(['branch', 'new']);
+
+ await d.git('foo.git',
+ [d.libDir('foo', 'foo 2'), d.libPubspec('foo', '1.0.0')]).commit();
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['foo', '--git-url', '../foo.git', '--git-ref', 'old'],
+ error: contains('Unable to resolve package "foo" with the given '
+ 'git parameters'),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+}
diff --git a/test/add/git/subdir_test.dart b/test/add/git/subdir_test.dart
new file mode 100644
index 0000000..0f78069
--- /dev/null
+++ b/test/add/git/subdir_test.dart
@@ -0,0 +1,81 @@
+// Copyright (c) 2020, 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 'package:test/test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+void main() {
+ test('adds a package from git subdirectory', () async {
+ ensureGit();
+
+ final repo = d.git('foo.git', [
+ d.dir('subdir', [d.libPubspec('sub', '1.0.0'), d.libDir('sub', '1.0.0')])
+ ]);
+
+ await repo.create();
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['sub', '--git-url', '../foo.git', '--git-path', 'subdir']);
+
+ await d.dir(cachePath, [
+ d.dir('git', [
+ d.dir('cache', [d.gitPackageRepoCacheDir('foo')]),
+ d.hashDir('foo', [
+ d.dir('subdir', [d.libDir('sub', '1.0.0')])
+ ])
+ ])
+ ]).validate();
+
+ await d.appPackagesFile({
+ 'sub': pathInCache('git/foo-${await repo.revParse('HEAD')}/subdir')
+ }).validate();
+
+ await d.appDir({
+ 'sub': {
+ 'git': {'url': '../foo.git', 'path': 'subdir'}
+ }
+ }).validate();
+ });
+
+ test('adds a package in a deep subdirectory', () async {
+ ensureGit();
+
+ final repo = d.git('foo.git', [
+ d.dir('sub', [
+ d.dir('dir', [d.libPubspec('sub', '1.0.0'), d.libDir('sub', '1.0.0')])
+ ])
+ ]);
+ await repo.create();
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['sub', '--git-url', '../foo.git', '--git-path', 'sub/dir']);
+
+ await d.dir(cachePath, [
+ d.dir('git', [
+ d.dir('cache', [d.gitPackageRepoCacheDir('foo')]),
+ d.hashDir('foo', [
+ d.dir('sub', [
+ d.dir('dir', [d.libDir('sub', '1.0.0')])
+ ])
+ ])
+ ])
+ ]).validate();
+
+ await d.appPackagesFile({
+ 'sub': pathInCache('git/foo-${await repo.revParse('HEAD')}/sub/dir')
+ }).validate();
+
+ await d.appDir({
+ 'sub': {
+ 'git': {'url': '../foo.git', 'path': 'sub/dir'}
+ }
+ }).validate();
+ });
+}
diff --git a/test/add/hosted/non_default_pub_server_test.dart b/test/add/hosted/non_default_pub_server_test.dart
new file mode 100644
index 0000000..75a4efd
--- /dev/null
+++ b/test/add/hosted/non_default_pub_server_test.dart
@@ -0,0 +1,142 @@
+// Copyright (c) 2020, 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 'package:pub/src/exit_codes.dart' as exit_codes;
+import 'package:test/test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+void main() {
+ test('adds a package from a non-default pub server', () async {
+ // Make the default server serve errors. Only the custom server should
+ // be accessed.
+ await serveErrors();
+
+ var server = await PackageServer.start((builder) {
+ builder.serve('foo', '0.2.5');
+ builder.serve('foo', '1.1.0');
+ builder.serve('foo', '1.2.3');
+ });
+
+ await d.appDir({}).create();
+
+ final url = server.url;
+
+ await pubAdd(args: ['foo:1.2.3', '--hosted-url', url]);
+
+ await d.cacheDir({'foo': '1.2.3'}, port: server.port).validate();
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+ await d.appDir({
+ 'foo': {
+ 'version': '1.2.3',
+ 'hosted': {'name': 'foo', 'url': url}
+ }
+ }).validate();
+ });
+
+ test('fails when adding from an invalid url', () async {
+ ensureGit();
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['foo', '--hosted-url', 'https://invalid-url.foo'],
+ error: contains('Could not resolve URL "https://invalid-url.foo".'),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test(
+ 'adds a package from a non-default pub server with no version constraint',
+ () async {
+ // Make the default server serve errors. Only the custom server should
+ // be accessed.
+ await serveErrors();
+
+ var server = await PackageServer.start((builder) {
+ builder.serve('foo', '0.2.5');
+ builder.serve('foo', '1.1.0');
+ builder.serve('foo', '1.2.3');
+ });
+
+ await d.appDir({}).create();
+
+ final url = server.url;
+
+ await pubAdd(args: ['foo', '--hosted-url', url]);
+
+ await d.cacheDir({'foo': '1.2.3'}, port: server.port).validate();
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+ await d.appDir({
+ 'foo': {
+ 'version': '^1.2.3',
+ 'hosted': {'name': 'foo', 'url': url}
+ }
+ }).validate();
+ });
+
+ test('adds a package from a non-default pub server with a version constraint',
+ () async {
+ // Make the default server serve errors. Only the custom server should
+ // be accessed.
+ await serveErrors();
+
+ var server = await PackageServer.start((builder) {
+ builder.serve('foo', '0.2.5');
+ builder.serve('foo', '1.1.0');
+ builder.serve('foo', '1.2.3');
+ });
+
+ await d.appDir({}).create();
+
+ final url = server.url;
+
+ await pubAdd(args: ['foo', '--hosted-url', url]);
+
+ await d.cacheDir({'foo': '1.2.3'}, port: server.port).validate();
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+ await d.appDir({
+ 'foo': {
+ 'version': '^1.2.3',
+ 'hosted': {'name': 'foo', 'url': url}
+ }
+ }).validate();
+ });
+
+ test(
+ 'adds a package from a non-default pub server with the "any" version '
+ 'constraint', () async {
+ // Make the default server serve errors. Only the custom server should
+ // be accessed.
+ await serveErrors();
+
+ var server = await PackageServer.start((builder) {
+ builder.serve('foo', '0.2.5');
+ builder.serve('foo', '1.1.0');
+ builder.serve('foo', '1.2.3');
+ });
+
+ await d.appDir({}).create();
+
+ final url = server.url;
+
+ await pubAdd(args: ['foo:any', '--hosted-url', url]);
+
+ await d.cacheDir({'foo': '1.2.3'}, port: server.port).validate();
+ await d.appPackagesFile({'foo': '1.2.3'}).validate();
+ await d.appDir({
+ 'foo': {
+ 'version': 'any',
+ 'hosted': {'name': 'foo', 'url': url}
+ }
+ }).validate();
+ });
+}
diff --git a/test/add/path/absolute_path_test.dart b/test/add/path/absolute_path_test.dart
new file mode 100644
index 0000000..0811bea
--- /dev/null
+++ b/test/add/path/absolute_path_test.dart
@@ -0,0 +1,120 @@
+// Copyright (c) 2020, 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 'package:path/path.dart' as path;
+import 'package:pub/src/exit_codes.dart' as exit_codes;
+import 'package:test/test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+void main() {
+ test('path dependency with absolute path', () async {
+ await d
+ .dir('foo', [d.libDir('foo'), d.libPubspec('foo', '0.0.1')]).create();
+
+ await d.appDir({}).create();
+
+ final absolutePath = path.join(d.sandbox, 'foo');
+
+ await pubAdd(args: ['foo', '--path', absolutePath]);
+
+ await d.appPackagesFile({'foo': absolutePath}).validate();
+
+ await d.appDir({
+ 'foo': {'path': absolutePath}
+ }).validate();
+ });
+
+ test('adds a package from absolute path with version constraint', () async {
+ await d
+ .dir('foo', [d.libDir('foo'), d.libPubspec('foo', '0.0.1')]).create();
+
+ await d.appDir({}).create();
+ final absolutePath = path.join(d.sandbox, 'foo');
+
+ await pubAdd(args: ['foo:0.0.1', '--path', absolutePath]);
+
+ await d.appDir({
+ 'foo': {'path': absolutePath, 'version': '0.0.1'}
+ }).validate();
+ });
+
+ test('fails when adding with an invalid version constraint', () async {
+ ensureGit();
+
+ await d.git(
+ 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+
+ await d.appDir({}).create();
+ final absolutePath = path.join(d.sandbox, 'foo');
+
+ await pubAdd(
+ args: ['foo:2.0.0', '--path', absolutePath],
+ error: equalsIgnoringWhitespace(
+ 'Because myapp depends on foo from path which doesn\'t exist '
+ '(could not find package foo at "$absolutePath"), version solving '
+ 'failed.'),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('fails if path does not exist', () async {
+ await d.appDir({}).create();
+
+ final absolutePath = path.join(d.sandbox, 'foo');
+
+ await pubAdd(
+ args: ['foo', '--path', absolutePath],
+ error: equalsIgnoringWhitespace(
+ 'Because myapp depends on foo from path which doesn\'t exist '
+ '(could not find package foo at "$absolutePath"), version solving '
+ 'failed.'),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('can be overriden by dependency override', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.2');
+ });
+ await d
+ .dir('foo', [d.libDir('foo'), d.libPubspec('foo', '0.0.1')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).create();
+
+ final absolutePath = path.join(d.sandbox, 'foo');
+ await pubAdd(args: ['foo', '--path', absolutePath]);
+
+ await d.cacheDir({'foo': '1.2.2'}).validate();
+ await d.appPackagesFile({'foo': '1.2.2'}).validate();
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {
+ 'foo': {'path': absolutePath}
+ },
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).validate();
+ });
+}
diff --git a/test/add/path/relative_path_test.dart b/test/add/path/relative_path_test.dart
new file mode 100644
index 0000000..6a191a6
--- /dev/null
+++ b/test/add/path/relative_path_test.dart
@@ -0,0 +1,114 @@
+// Copyright (c) 2020, 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' show Platform;
+
+import 'package:pub/src/exit_codes.dart' as exit_codes;
+import 'package:test/test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+void main() {
+ test('can use relative path', () async {
+ await d
+ .dir('foo', [d.libDir('foo'), d.libPubspec('foo', '0.0.1')]).create();
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: ['foo', '--path', '../foo']);
+
+ await d.appPackagesFile({'foo': '../foo'}).validate();
+
+ await d.appDir({
+ 'foo': {'path': '../foo'}
+ }).validate();
+ });
+
+ test('fails if path does not exist', () async {
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['foo', '--path', '../foo'],
+ error: equalsIgnoringWhitespace(
+ 'Because myapp depends on foo from path which doesn\'t exist '
+ '(could not find package foo at "..${Platform.pathSeparator}foo"), '
+ 'version solving failed.'),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('adds a package from absolute path with version constraint', () async {
+ await d
+ .dir('foo', [d.libDir('foo'), d.libPubspec('foo', '0.0.1')]).create();
+
+ await d.appDir({}).create();
+
+ await pubAdd(args: ['foo:0.0.1', '--path', '../foo']);
+
+ await d.appDir({
+ 'foo': {'path': '../foo', 'version': '0.0.1'}
+ }).validate();
+ });
+
+ test('fails when adding with an invalid version constraint', () async {
+ ensureGit();
+
+ await d.git(
+ 'foo.git', [d.libDir('foo'), d.libPubspec('foo', '1.0.0')]).create();
+
+ await d.appDir({}).create();
+
+ await pubAdd(
+ args: ['foo:2.0.0', '--path', '../foo'],
+ error: equalsIgnoringWhitespace(
+ 'Because myapp depends on foo from path which doesn\'t exist '
+ '(could not find package foo at "..${Platform.pathSeparator}foo"), '
+ 'version solving failed.'),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+
+ test('can be overriden by dependency override', () async {
+ await servePackages((builder) {
+ builder.serve('foo', '1.2.2');
+ });
+ await d
+ .dir('foo', [d.libDir('foo'), d.libPubspec('foo', '0.0.1')]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {},
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).create();
+
+ await pubAdd(args: ['foo', '--path', '../foo']);
+
+ await d.cacheDir({'foo': '1.2.2'}).validate();
+ await d.appPackagesFile({'foo': '1.2.2'}).validate();
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {
+ 'foo': {'path': '../foo'}
+ },
+ 'dependency_overrides': {'foo': '1.2.2'}
+ })
+ ]).validate();
+ });
+}
diff --git a/test/add/sdk/sdk_test.dart b/test/add/sdk/sdk_test.dart
new file mode 100644
index 0000000..25995e3
--- /dev/null
+++ b/test/add/sdk/sdk_test.dart
@@ -0,0 +1,109 @@
+// Copyright (c) 2020, 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 'package:path/path.dart' as p;
+import 'package:pub/src/exit_codes.dart' as exit_codes;
+import 'package:test/test.dart';
+
+import '../../descriptor.dart' as d;
+import '../../test_pub.dart';
+
+void main() {
+ setUp(() async {
+ await servePackages((builder) {
+ builder.serve('bar', '1.0.0');
+ });
+
+ await d.dir('flutter', [
+ d.dir('packages', [
+ d.dir('foo', [
+ d.libDir('foo', 'foo 0.0.1'),
+ d.libPubspec('foo', '0.0.1', deps: {'bar': 'any'})
+ ])
+ ]),
+ d.dir('bin/cache/pkg', [
+ d.dir(
+ 'baz', [d.libDir('baz', 'foo 0.0.1'), d.libPubspec('baz', '0.0.1')])
+ ]),
+ d.file('version', '1.2.3')
+ ]).create();
+ });
+
+ test("adds an SDK dependency's dependencies", () async {
+ await d.appDir({}).create();
+ await pubAdd(
+ args: ['foo', '--sdk', 'flutter'],
+ environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')});
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {
+ 'foo': {'sdk': 'flutter', 'version': '^0.0.1'}
+ }
+ }),
+ d.packagesFile({
+ 'myapp': '.',
+ 'foo': p.join(d.sandbox, 'flutter', 'packages', 'foo'),
+ 'bar': '1.0.0'
+ })
+ ]).validate();
+ });
+
+ test(
+ "adds an SDK dependency's dependencies with version constraint specified",
+ () async {
+ await d.appDir({}).create();
+ await pubAdd(
+ args: ['foo:0.0.1', '--sdk', 'flutter'],
+ environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')});
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {
+ 'foo': {'sdk': 'flutter', 'version': '0.0.1'}
+ }
+ }),
+ d.packagesFile({
+ 'myapp': '.',
+ 'foo': p.join(d.sandbox, 'flutter', 'packages', 'foo'),
+ 'bar': '1.0.0'
+ })
+ ]).validate();
+ });
+
+ test('adds an SDK dependency from bin/cache/pkg', () async {
+ await d.appDir({}).create();
+ await pubAdd(
+ args: ['baz', '--sdk', 'flutter'],
+ environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')});
+
+ await d.dir(appPath, [
+ d.packagesFile({
+ 'myapp': '.',
+ 'baz': p.join(d.sandbox, 'flutter', 'bin', 'cache', 'pkg', 'baz')
+ })
+ ]).validate();
+ });
+
+ test("fails if the version constraint doesn't match", () async {
+ await d.appDir({}).create();
+ await pubAdd(
+ args: ['foo:^1.0.0', '--sdk', 'flutter'],
+ environment: {'FLUTTER_ROOT': p.join(d.sandbox, 'flutter')},
+ error: equalsIgnoringWhitespace("""
+ Because myapp depends on foo ^1.0.0 from sdk which doesn't match
+ any versions, version solving failed.
+ """),
+ exitCode: exit_codes.DATA);
+
+ await d.appDir({}).validate();
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ d.nothing('pubspec.lock'),
+ d.nothing('.packages'),
+ ]).validate();
+ });
+}
diff --git a/test/descriptor.dart b/test/descriptor.dart
index 7dcfc6f..73518ee 100644
--- a/test/descriptor.dart
+++ b/test/descriptor.dart
@@ -13,6 +13,7 @@
import 'descriptor/git.dart';
import 'descriptor/packages.dart';
import 'descriptor/tar.dart';
+import 'descriptor/yaml.dart';
import 'test_pub.dart';
export 'package:test_descriptor/test_descriptor.dart';
@@ -49,7 +50,7 @@
/// [contents] may contain [Future]s that resolve to serializable objects,
/// which may in turn contain [Future]s recursively.
Descriptor pubspec(Map<String, Object> contents) =>
- file('pubspec.yaml', yaml(contents));
+ YamlDescriptor('pubspec.yaml', yaml(contents));
/// Describes a file named `pubspec.yaml` for an application package with the
/// given [dependencies].
diff --git a/test/descriptor/yaml.dart b/test/descriptor/yaml.dart
new file mode 100644
index 0000000..fc56ee3
--- /dev/null
+++ b/test/descriptor/yaml.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2020, 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:async' show Future;
+import 'dart:convert' show utf8;
+import 'dart:io';
+
+import 'package:collection/collection.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart';
+import 'package:yaml/yaml.dart';
+
+import '../descriptor.dart';
+
+/// Describes a YAML file and its contents.
+class YamlDescriptor extends FileDescriptor {
+ /// Contents of the YAML file. Must be valid YAML.
+ final String _contents;
+
+ YamlDescriptor(String name, this._contents) : super.protected(name);
+
+ @override
+ Future<String> read() async => _contents;
+
+ @override
+ Stream<List<int>> readAsBytes() =>
+ Stream.fromIterable([utf8.encode(_contents)]);
+
+ @override
+ Future validate([String parent]) async {
+ var fullPath = p.join(parent ?? sandbox, name);
+ if (!await File(fullPath).exists()) {
+ fail("File not found: '$fullPath'.");
+ }
+
+ var bytes = await File(fullPath).readAsBytes();
+
+ final actualContentsText = utf8.decode(bytes);
+ final actual = loadYaml(actualContentsText);
+ final expected = loadYaml(_contents);
+
+ if (!DeepCollectionEquality().equals(expected, actual)) {
+ fail('Expected $expected, found: $actual');
+ }
+ }
+}
diff --git a/test/pub_test.dart b/test/pub_test.dart
index 21227d2..0d0c91b 100644
--- a/test/pub_test.dart
+++ b/test/pub_test.dart
@@ -29,6 +29,7 @@
-v, --verbose Shortcut for "--verbosity=all".
Available commands:
+ add Add a dependency to pubspec.yaml.
cache Work with the system cache.
deps Print package dependencies.
downgrade Downgrade the current package's dependencies to oldest versions.
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 3dd5407..8c5cc73 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -83,6 +83,8 @@
/// Enum identifying a pub command that can be run with a well-defined success
/// output.
class RunCommand {
+ static final add = RunCommand(
+ 'add', RegExp(r'Got dependencies!|Changed \d+ dependenc(y|ies)!'));
static final get = RunCommand(
'get', RegExp(r'Got dependencies!|Changed \d+ dependenc(y|ies)!'));
static final upgrade = RunCommand('upgrade', RegExp(r'''
@@ -152,6 +154,21 @@
environment: environment);
}
+Future pubAdd(
+ {Iterable<String> args,
+ output,
+ error,
+ warning,
+ int exitCode,
+ Map<String, String> environment}) =>
+ pubCommand(RunCommand.add,
+ args: args,
+ output: output,
+ error: error,
+ warning: warning,
+ exitCode: exitCode,
+ environment: environment);
+
Future pubGet(
{Iterable<String> args,
output,