dependency_services (#3304)
diff --git a/bin/dependency_services.dart b/bin/dependency_services.dart
new file mode 100644
index 0000000..3dabf5d
--- /dev/null
+++ b/bin/dependency_services.dart
@@ -0,0 +1,84 @@
+// Copyright (c) 2021, 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.
+
+/// Support for automated upgrades.
+///
+/// For now this is not a finalized interface. Don't rely on this.
+library dependency_services;
+
+import 'dart:async';
+
+import 'package:args/args.dart';
+import 'package:args/command_runner.dart';
+import 'package:pub/src/command.dart';
+import 'package:pub/src/command/dependency_services.dart';
+import 'package:pub/src/exit_codes.dart' as exit_codes;
+import 'package:pub/src/io.dart';
+import 'package:pub/src/log.dart' as log;
+
+class _DependencyServicesCommandRunner extends CommandRunner<int>
+ implements PubTopLevel {
+ @override
+ String? get directory => argResults['directory'];
+
+ @override
+ bool get captureStackChains => argResults['verbose'];
+
+ @override
+ bool get trace => argResults['verbose'];
+
+ ArgResults? _argResults;
+
+ /// The top-level options parsed by the command runner.
+ @override
+ ArgResults get argResults {
+ final a = _argResults;
+ if (a == null) {
+ throw StateError(
+ 'argResults cannot be used before Command.run is called.');
+ }
+ return a;
+ }
+
+ _DependencyServicesCommandRunner()
+ : super('dependency_services', 'Support for automatic upgrades',
+ usageLineLength: lineLength) {
+ argParser.addFlag('verbose',
+ abbr: 'v', negatable: false, help: 'Shortcut for "--verbosity=all".');
+ argParser.addOption(
+ 'directory',
+ abbr: 'C',
+ help: 'Run the subcommand in the directory<dir>.',
+ defaultsTo: '.',
+ valueHelp: 'dir',
+ );
+
+ addCommand(DependencyServicesListCommand());
+ addCommand(DependencyServicesReportCommand());
+ addCommand(DependencyServicesApplyCommand());
+ }
+
+ @override
+ Future<int> run(Iterable<String> args) async {
+ try {
+ _argResults = parse(args);
+ return await runCommand(argResults) ?? exit_codes.SUCCESS;
+ } on UsageException catch (error) {
+ log.exception(error);
+ return exit_codes.USAGE;
+ }
+ }
+
+ @override
+ void printUsage() {
+ log.message(usage);
+ }
+
+ @override
+ log.Verbosity get verbosity => log.Verbosity.normal;
+}
+
+Future<void> main(List<String> arguments) async {
+ await flushThenExit(await _DependencyServicesCommandRunner().run(arguments));
+}
diff --git a/lib/src/command.dart b/lib/src/command.dart
index 556c04f..ee62392 100644
--- a/lib/src/command.dart
+++ b/lib/src/command.dart
@@ -13,7 +13,6 @@
import 'package:path/path.dart' as p;
import 'authentication/token_store.dart';
-import 'command_runner.dart';
import 'entrypoint.dart';
import 'exceptions.dart';
import 'exit_codes.dart' as exit_codes;
@@ -127,7 +126,7 @@
}
PubTopLevel get _pubTopLevel =>
- _pubEmbeddableCommand ?? runner as PubCommandRunner;
+ _pubEmbeddableCommand ?? runner as PubTopLevel;
PubAnalytics? get analytics => _pubEmbeddableCommand?.analytics;
diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart
new file mode 100644
index 0000000..47805bc
--- /dev/null
+++ b/lib/src/command/dependency_services.dart
@@ -0,0 +1,381 @@
+// Copyright (c) 2021, 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.
+
+/// This implements support for dependency-bot style automated upgrades.
+/// It is still work in progress - do not rely on the current output.
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:collection/collection.dart';
+import 'package:pub_semver/pub_semver.dart';
+import 'package:yaml/yaml.dart';
+import 'package:yaml_edit/yaml_edit.dart';
+
+import '../command.dart';
+import '../entrypoint.dart';
+import '../exceptions.dart';
+import '../io.dart';
+import '../log.dart' as log;
+import '../package.dart';
+import '../package_name.dart';
+import '../pubspec.dart';
+import '../pubspec_utils.dart';
+import '../solver.dart';
+import '../system_cache.dart';
+import '../utils.dart';
+
+class DependencyServicesReportCommand extends PubCommand {
+ @override
+ String get name => 'report';
+ @override
+ String get description =>
+ 'Output a machine-digestible report of the upgrade options for each dependency.';
+ @override
+ String get argumentsDescription => '[options]';
+
+ @override
+ bool get takesArguments => false;
+
+ DependencyServicesReportCommand() {
+ argParser.addOption('directory',
+ abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
+ }
+
+ @override
+ Future<void> runProtected() async {
+ final compatiblePubspec = stripDependencyOverrides(entrypoint.root.pubspec);
+
+ final breakingPubspec = stripVersionUpperBounds(compatiblePubspec);
+
+ final compatiblePackagesResult =
+ await _tryResolve(compatiblePubspec, cache);
+
+ final breakingPackagesResult = await _tryResolve(breakingPubspec, cache);
+
+ // The packages in the current lockfile or resolved from current pubspec.yaml.
+ late Map<String, PackageId> currentPackages;
+
+ if (fileExists(entrypoint.lockFilePath)) {
+ currentPackages =
+ Map<String, PackageId>.from(entrypoint.lockFile.packages);
+ } else {
+ final resolution = await _tryResolve(entrypoint.root.pubspec, cache) ??
+ (throw DataException('Failed to resolve pubspec'));
+ currentPackages =
+ Map<String, PackageId>.fromIterable(resolution, key: (e) => e.name);
+ }
+ currentPackages.remove(entrypoint.root.name);
+
+ final dependencies = <Object>[];
+ final result = <String, Object>{'dependencies': dependencies};
+
+ Future<List<Object>> _computeUpgradeSet(
+ Pubspec rootPubspec,
+ PackageId? package, {
+ required UpgradeType upgradeType,
+ }) async {
+ if (package == null) return [];
+ final lockFile = entrypoint.lockFile;
+ final pubspec = upgradeType == UpgradeType.multiBreaking
+ ? stripVersionUpperBounds(rootPubspec)
+ : Pubspec(
+ rootPubspec.name,
+ dependencies: rootPubspec.dependencies.values,
+ devDependencies: rootPubspec.devDependencies.values,
+ sdkConstraints: rootPubspec.sdkConstraints,
+ );
+
+ final dependencySet = dependencySetOfPackage(pubspec, package);
+ if (dependencySet != null) {
+ // Force the version to be the new version.
+ dependencySet[package.name] =
+ package.toRange().withConstraint(package.toRange().constraint);
+ }
+
+ final resolution = await tryResolveVersions(
+ SolveType.get,
+ cache,
+ Package.inMemory(pubspec),
+ lockFile: lockFile,
+ );
+
+ // TODO(sigurdm): improve error messages.
+ if (resolution == null) {
+ throw DataException('Failed resolving');
+ }
+
+ return [
+ ...resolution.packages.where((r) {
+ if (r.name == rootPubspec.name) return false;
+ final originalVersion = currentPackages[r.name];
+ return originalVersion == null ||
+ r.version != originalVersion.version;
+ }).map((p) {
+ final depset = dependencySetOfPackage(rootPubspec, p);
+ final originalConstraint = depset?[p.name]?.constraint;
+ return {
+ 'name': p.name,
+ 'version': p.version.toString(),
+ 'kind': _kindString(pubspec, p.name),
+ 'constraint': originalConstraint == null
+ ? null
+ : upgradeType == UpgradeType.compatible
+ ? originalConstraint.toString()
+ : VersionConstraint.compatibleWith(p.version).toString(),
+ 'previousVersion': currentPackages[p.name]?.version.toString(),
+ 'previousConstraint': originalConstraint?.toString(),
+ };
+ }),
+ for (final oldPackageName in lockFile.packages.keys)
+ if (!resolution.packages
+ .any((newPackage) => newPackage.name == oldPackageName))
+ {
+ 'name': oldPackageName,
+ 'version': null,
+ 'kind':
+ 'transitive', // Only transitive constraints can be removed.
+ 'constraint': null,
+ 'previousVersion':
+ currentPackages[oldPackageName]?.version.toString(),
+ 'previousConstraint': null,
+ },
+ ];
+ }
+
+ for (final package in currentPackages.values) {
+ final compatibleVersion = compatiblePackagesResult
+ ?.firstWhereOrNull((element) => element.name == package.name);
+ final multiBreakingVersion = breakingPackagesResult
+ ?.firstWhereOrNull((element) => element.name == package.name);
+ final singleBreakingPubspec = Pubspec(
+ compatiblePubspec.name,
+ version: compatiblePubspec.version,
+ sdkConstraints: compatiblePubspec.sdkConstraints,
+ dependencies: compatiblePubspec.dependencies.values,
+ devDependencies: compatiblePubspec.devDependencies.values,
+ );
+ final dependencySet =
+ dependencySetOfPackage(singleBreakingPubspec, package);
+ final kind = _kindString(compatiblePubspec, package.name);
+ PackageId? singleBreakingVersion;
+ if (dependencySet != null) {
+ dependencySet[package.name] = package
+ .toRange()
+ .withConstraint(stripUpperBound(package.toRange().constraint));
+ final singleBreakingPackagesResult =
+ await _tryResolve(singleBreakingPubspec, cache);
+ singleBreakingVersion = singleBreakingPackagesResult
+ ?.firstWhereOrNull((element) => element.name == package.name);
+ }
+ dependencies.add({
+ 'name': package.name,
+ 'version': package.version.toString(),
+ 'kind': kind,
+ 'latest': (await cache.getLatest(package))?.version.toString(),
+ 'constraint':
+ _constraintOf(compatiblePubspec, package.name)?.toString(),
+ if (compatibleVersion != null)
+ 'compatible': await _computeUpgradeSet(
+ compatiblePubspec, compatibleVersion,
+ upgradeType: UpgradeType.compatible),
+ 'singleBreaking': kind != 'transitive' && singleBreakingVersion == null
+ ? []
+ : await _computeUpgradeSet(compatiblePubspec, singleBreakingVersion,
+ upgradeType: UpgradeType.singleBreaking),
+ 'multiBreaking': kind != 'transitive' && multiBreakingVersion != null
+ ? await _computeUpgradeSet(compatiblePubspec, multiBreakingVersion,
+ upgradeType: UpgradeType.multiBreaking)
+ : [],
+ });
+ }
+ log.message(JsonEncoder.withIndent(' ').convert(result));
+ }
+}
+
+VersionConstraint? _constraintOf(Pubspec pubspec, String packageName) {
+ return (pubspec.dependencies[packageName] ??
+ pubspec.devDependencies[packageName])
+ ?.constraint;
+}
+
+String _kindString(Pubspec pubspec, String packageName) {
+ return pubspec.dependencies.containsKey(packageName)
+ ? 'direct'
+ : pubspec.devDependencies.containsKey(packageName)
+ ? 'dev'
+ : 'transitive';
+}
+
+/// Try to solve [pubspec] return [PackageId]s in the resolution or `null` if no
+/// resolution was found.
+Future<List<PackageId>?> _tryResolve(Pubspec pubspec, SystemCache cache) async {
+ final solveResult = await tryResolveVersions(
+ SolveType.upgrade,
+ cache,
+ Package.inMemory(pubspec),
+ );
+
+ return solveResult?.packages;
+}
+
+class DependencyServicesListCommand extends PubCommand {
+ @override
+ String get name => 'list';
+
+ @override
+ String get description =>
+ 'Output a machine digestible listing of all dependencies';
+
+ @override
+ bool get takesArguments => false;
+
+ DependencyServicesListCommand() {
+ argParser.addOption('directory',
+ abbr: 'C', help: 'Run this in the directory<dir>.', valueHelp: 'dir');
+ }
+
+ @override
+ Future<void> runProtected() async {
+ final pubspec = entrypoint.root.pubspec;
+
+ final currentPackages = fileExists(entrypoint.lockFilePath)
+ ? entrypoint.lockFile.packages.values.toList()
+ : (await _tryResolve(pubspec, cache) ?? <PackageId>[]);
+
+ final dependencies = <Object>[];
+ final result = <String, Object>{'dependencies': dependencies};
+
+ for (final package in currentPackages) {
+ dependencies.add({
+ 'name': package.name,
+ 'version': package.version.toString(),
+ 'kind': _kindString(pubspec, package.name),
+ 'constraint': _constraintOf(pubspec, package.name).toString(),
+ });
+ }
+ log.message(JsonEncoder.withIndent(' ').convert(result));
+ }
+}
+
+enum UpgradeType {
+ /// Only upgrade pubspec.lock
+ compatible,
+
+ /// Unlock at most one dependency in pubspec.yaml
+ singleBreaking,
+
+ /// Unlock any dependencies in pubspec.yaml needed for getting the
+ /// latest resolvable version.
+ multiBreaking,
+}
+
+class DependencyServicesApplyCommand extends PubCommand {
+ @override
+ String get name => 'apply';
+
+ @override
+ String get description =>
+ 'Output a machine digestible listing of all dependencies';
+
+ @override
+ bool get takesArguments => true;
+
+ DependencyServicesApplyCommand() {
+ argParser.addOption('directory',
+ abbr: 'C', help: 'Run this in the directory <dir>.', valueHelp: 'dir');
+ }
+
+ @override
+ Future<void> runProtected() async {
+ YamlEditor(readTextFile(entrypoint.pubspecPath));
+ final toApply = <_PackageVersion>[];
+ final input = json.decode(await utf8.decodeStream(stdin));
+ for (final change in input['dependencyChanges']) {
+ toApply.add(
+ _PackageVersion(
+ change['name'],
+ change['version'] != null ? Version.parse(change['version']) : null,
+ change['constraint'] != null
+ ? VersionConstraint.parse(change['constraint'])
+ : null,
+ ),
+ );
+ }
+
+ final pubspec = entrypoint.root.pubspec;
+ final pubspecEditor = YamlEditor(readTextFile(entrypoint.pubspecPath));
+ final lockFile = fileExists(entrypoint.lockFilePath)
+ ? readTextFile(entrypoint.lockFilePath)
+ : null;
+ final lockFileYaml = lockFile == null ? null : loadYaml(lockFile);
+ final lockFileEditor = lockFile == null ? null : YamlEditor(lockFile);
+ for (final p in toApply) {
+ final targetPackage = p.name;
+ final targetVersion = p.version;
+ final targetConstraint = p.constraint;
+
+ if (targetConstraint != null) {
+ final section = pubspec.dependencies[targetPackage] != null
+ ? 'dependencies'
+ : 'dev_dependencies';
+ pubspecEditor
+ .update([section, targetPackage], targetConstraint.toString());
+ } else if (targetVersion != null) {
+ final constraint = _constraintOf(pubspec, targetPackage);
+ if (constraint != null && !constraint.allows(targetVersion)) {
+ final section = pubspec.dependencies[targetPackage] != null
+ ? 'dependencies'
+ : 'dev_dependencies';
+ pubspecEditor.update([section, targetPackage],
+ VersionConstraint.compatibleWith(targetVersion).toString());
+ }
+ }
+ if (targetVersion != null &&
+ lockFileEditor != null &&
+ lockFileYaml['packages'].containsKey(targetPackage)) {
+ lockFileEditor.update(
+ ['packages', targetPackage, 'version'], targetVersion.toString());
+ }
+ if (targetVersion == null &&
+ lockFileEditor != null &&
+ !lockFileYaml['packages'].containsKey(targetPackage)) {
+ dataError(
+ 'Trying to remove non-existing transitive dependency $targetPackage.',
+ );
+ }
+ }
+
+ if (pubspecEditor.edits.isNotEmpty) {
+ writeTextFile(entrypoint.pubspecPath, pubspecEditor.toString());
+ }
+ if (lockFileEditor != null && lockFileEditor.edits.isNotEmpty) {
+ writeTextFile(entrypoint.lockFilePath, lockFileEditor.toString());
+ }
+ await log.warningsOnlyUnlessTerminal(
+ () async {
+ // This will fail if the new configuration does not resolve.
+ await Entrypoint(directory, cache).acquireDependencies(SolveType.get,
+ analytics: null, generateDotPackages: false);
+ },
+ );
+ // Dummy message.
+ log.message(json.encode({'dependencies': []}));
+ }
+}
+
+class _PackageVersion {
+ String name;
+ Version? version;
+ VersionConstraint? constraint;
+ _PackageVersion(this.name, this.version, this.constraint);
+}
+
+Map<String, PackageRange>? dependencySetOfPackage(
+ Pubspec pubspec, PackageName package) {
+ return pubspec.dependencies.containsKey(package.name)
+ ? pubspec.dependencies
+ : pubspec.devDependencies.containsKey(package.name)
+ ? pubspec.devDependencies
+ : null;
+}
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
index 609b7ae..2941952 100644
--- a/lib/src/command/outdated.dart
+++ b/lib/src/command/outdated.dart
@@ -10,7 +10,6 @@
import 'package:collection/collection.dart'
show IterableExtension, IterableNullableExtension;
import 'package:path/path.dart' as path;
-import 'package:pub_semver/pub_semver.dart';
import '../command.dart';
import '../command_runner.dart';
@@ -181,18 +180,23 @@
PackageId? latest;
// If not overridden in current resolution we can use this
if (!entrypoint.root.pubspec.dependencyOverrides.containsKey(name)) {
- latest ??= await _getLatest(current);
+ latest ??=
+ await cache.getLatest(current, allowPrereleases: prereleases);
}
// If present as a dependency or dev_dependency we use this
- latest ??= await _getLatest(rootPubspec.dependencies[name]);
- latest ??= await _getLatest(rootPubspec.devDependencies[name]);
+ latest ??= await cache.getLatest(rootPubspec.dependencies[name],
+ allowPrereleases: prereleases);
+ latest ??= await cache.getLatest(rootPubspec.devDependencies[name],
+ allowPrereleases: prereleases);
// If not overridden and present in either upgradable or resolvable we
// use this reference to find the latest
if (!upgradablePubspec.dependencyOverrides.containsKey(name)) {
- latest ??= await _getLatest(upgradable);
+ latest ??=
+ await cache.getLatest(upgradable, allowPrereleases: prereleases);
}
if (!resolvablePubspec.dependencyOverrides.containsKey(name)) {
- latest ??= await _getLatest(resolvable);
+ latest ??=
+ await cache.getLatest(resolvable, allowPrereleases: prereleases);
}
// Otherwise, we might simply not have a latest, when a transitive
// dependency is overridden the source can depend on which versions we
@@ -200,7 +204,8 @@
// allow 3rd party pub servers, but other servers might. Hence, we choose
// to fallback to using the overridden source for latest.
if (latest == null) {
- latest ??= await _getLatest(current ?? upgradable ?? resolvable);
+ latest ??= await cache.getLatest(current ?? upgradable ?? resolvable,
+ allowPrereleases: prereleases);
latestIsOverridden = true;
}
@@ -304,37 +309,6 @@
return argResults['mode'] == 'null-safety';
}();
- /// Get the latest version of [package].
- ///
- /// Will include prereleases in the comparison if '--prereleases' was enabled
- /// by the arguments.
- ///
- /// If [package] is a [PackageId] with a prerelease version and there are no
- /// later stable version we return a prerelease version if it exists.
- ///
- /// Returns `null`, if unable to find the package.
- Future<PackageId?> _getLatest(PackageName? package) async {
- if (package == null) {
- return null;
- }
- final ref = package.toRef();
- final available = await cache.source(ref.source).getVersions(ref);
- if (available.isEmpty) {
- return null;
- }
-
- // TODO(sigurdm): Refactor this to share logic with report.dart.
- available.sort(prereleases
- ? (x, y) => x.version.compareTo(y.version)
- : (x, y) => Version.prioritize(x.version, y.version));
- if (package is PackageId &&
- package.version.isPreRelease &&
- package.version > available.last.version) {
- available.sort((x, y) => x.version.compareTo(y.version));
- }
- return available.last;
- }
-
/// Retrieves the pubspec of package [name] in [version] from [source].
///
/// Returns `null`, if given `null` as a convinience.
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 280dcbd..2faf85f 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -1080,3 +1080,13 @@
return null;
}
}();
+
+/// Escape [x] for users to copy-paste in bash.
+///
+/// If x is alphanumeric we leave it as is.
+///
+/// Otherwise, wrap with single quotation, and use '\'' to insert single quote.
+String escapeShellArgument(String x) =>
+ RegExp(r'^[a-zA-Z0-9-_=@.]+$').stringMatch(x) == null
+ ? "'${x.replaceAll(r'\', '\\').replaceAll("'", r"'\''")}'"
+ : x;
diff --git a/lib/src/pubspec_utils.dart b/lib/src/pubspec_utils.dart
index 251c8c9..ee904a7 100644
--- a/lib/src/pubspec_utils.dart
+++ b/lib/src/pubspec_utils.dart
@@ -4,7 +4,6 @@
import 'dart:async';
-import 'package:meta/meta.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package_name.dart';
@@ -148,7 +147,6 @@
/// Removes the upper bound of [constraint]. If [constraint] is the
/// empty version constraint, [VersionConstraint.empty] will be returned.
-@visibleForTesting
VersionConstraint stripUpperBound(VersionConstraint constraint) {
ArgumentError.checkNotNull(constraint, 'constraint');
diff --git a/lib/src/system_cache.dart b/lib/src/system_cache.dart
index dc6eb4a..1653f1e 100644
--- a/lib/src/system_cache.dart
+++ b/lib/src/system_cache.dart
@@ -5,6 +5,7 @@
import 'dart:io';
import 'package:path/path.dart' as p;
+import 'package:pub_semver/pub_semver.dart';
import 'authentication/token_store.dart';
import 'io.dart';
@@ -20,6 +21,7 @@
import 'source/sdk.dart';
import 'source/unknown.dart';
import 'source_registry.dart';
+import 'utils.dart';
/// The system-wide cache of downloaded packages.
///
@@ -146,4 +148,43 @@
log.fine('Clean up system cache temp directory $tempDir.');
if (dirExists(tempDir)) deleteEntry(tempDir);
}
+
+ /// Get the latest version of [package].
+ ///
+ /// Will consider _prereleases_ if:
+ /// * [allowPrereleases] is true, or,
+ /// * [package] is a [PackageId] with a prerelease version, and no later prerelease exists.
+ ///
+ /// Returns `null`, if unable to find the package.
+ Future<PackageId?> getLatest(
+ PackageName? package, {
+ bool allowPrereleases = false,
+ }) async {
+ if (package == null) {
+ return null;
+ }
+ final ref = package.toRef();
+ // TODO: Pass some maxAge to getVersions
+ final available = await source(ref.source).getVersions(ref);
+ if (available.isEmpty) {
+ return null;
+ }
+
+ final latest = maxAll(
+ available.map((id) => id.version),
+ allowPrereleases ? Comparable.compare : Version.prioritize,
+ );
+
+ if (package is PackageId &&
+ package.version.isPreRelease &&
+ package.version > latest &&
+ !allowPrereleases) {
+ return getLatest(package, allowPrereleases: true);
+ }
+
+ // There should be exactly one entry in [available] matching [latest]
+ assert(available.where((id) => id.version == latest).length == 1);
+
+ return available.firstWhere((id) => id.version == latest);
+ }
}
diff --git a/test/dependency_services/dependency_services_test.dart b/test/dependency_services/dependency_services_test.dart
new file mode 100644
index 0000000..4503e67
--- /dev/null
+++ b/test/dependency_services/dependency_services_test.dart
@@ -0,0 +1,254 @@
+// 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:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:pub/src/io.dart';
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import '../descriptor.dart' as d;
+import '../golden_file.dart';
+import '../test_pub.dart';
+
+void manifestAndLockfile(GoldenTestContext context) {
+ String catFile(String filename) {
+ final contents = filterUnstableLines(
+ File(p.join(d.sandbox, appPath, filename)).readAsLinesSync());
+
+ return '''
+\$ cat $filename
+${contents.join('\n')}''';
+ }
+
+ context.expectNextSection('''
+${catFile('pubspec.yaml')}
+${catFile('pubspec.lock')}
+''');
+}
+
+late final String snapshot;
+
+extension on GoldenTestContext {
+ /// Returns the stdout.
+ Future<String> runDependencyServices(List<String> args,
+ {String? stdin}) async {
+ final buffer = StringBuffer();
+ buffer.writeln('## Section ${args.join(' ')}');
+ final process = await Process.start(
+ Platform.resolvedExecutable,
+ [
+ snapshot,
+ ...args,
+ ],
+ environment: getPubTestEnvironment(),
+ workingDirectory: p.join(d.sandbox, appPath),
+ );
+ if (stdin != null) {
+ process.stdin.write(stdin);
+ await process.stdin.flush();
+ await process.stdin.close();
+ }
+ final outLines = outputLines(process.stdout);
+ final errLines = outputLines(process.stderr);
+ final exitCode = await process.exitCode;
+
+ final pipe = stdin == null ? '' : ' echo ${escapeShellArgument(stdin)} |';
+ buffer.writeln([
+ '\$$pipe dependency_services ${args.map(escapeShellArgument).join(' ')}',
+ ...await outLines,
+ ...(await errLines).map((e) => '[STDERR] $e'),
+ if (exitCode != 0) '[EXIT CODE] $exitCode',
+ ].join('\n'));
+
+ expectNextSection(buffer.toString());
+ return (await outLines).join('\n');
+ }
+}
+
+Future<Iterable<String>> outputLines(Stream<List<int>> stream) async {
+ final s = await utf8.decodeStream(stream);
+ if (s.isEmpty) return [];
+ return filterUnstableLines(s.split('\n'));
+}
+
+Future<void> listReportApply(
+ GoldenTestContext context,
+ List<_PackageVersion> upgrades, {
+ void Function(Map)? reportAssertions,
+}) async {
+ manifestAndLockfile(context);
+ await context.runDependencyServices(['list']);
+ final report = await context.runDependencyServices(['report']);
+ if (reportAssertions != null) {
+ reportAssertions(json.decode(report));
+ }
+ final input = json.encode({
+ 'dependencyChanges': upgrades,
+ });
+
+ await context.runDependencyServices(['apply'], stdin: input);
+ manifestAndLockfile(context);
+}
+
+Future<void> main() async {
+ setUpAll(() async {
+ final tempDir = Directory.systemTemp.createTempSync();
+ snapshot = p.join(tempDir.path, 'dependency_services.dart.snapshot');
+ final r = Process.runSync(Platform.resolvedExecutable, [
+ '--snapshot=$snapshot',
+ p.join('bin', 'dependency_services.dart'),
+ ]);
+ expect(r.exitCode, 0, reason: r.stderr);
+ });
+
+ tearDownAll(() {
+ File(snapshot).parent.deleteSync(recursive: true);
+ });
+
+ testWithGolden('Removing transitive', (context) async {
+ (await servePackages())
+ ..serve('foo', '1.2.3', deps: {'transitive': '^1.0.0'})
+ ..serve('foo', '2.2.3')
+ ..serve('transitive', '1.0.0');
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'app',
+ 'dependencies': {
+ 'foo': '^1.0.0',
+ },
+ })
+ ]).create();
+ await pubGet();
+ await listReportApply(context, [
+ _PackageVersion('foo', Version.parse('2.2.3')),
+ _PackageVersion('transitive', null)
+ ], reportAssertions: (report) {
+ expect(
+ findChangeVersion(report, 'singleBreaking', 'foo'),
+ '2.2.3',
+ );
+ expect(
+ findChangeVersion(report, 'singleBreaking', 'transitive'),
+ null,
+ );
+ });
+ });
+
+ testWithGolden('Compatible', (context) async {
+ final server = (await servePackages())
+ ..serve('foo', '1.2.3')
+ ..serve('foo', '2.2.3')
+ ..serve('bar', '1.2.3')
+ ..serve('bar', '2.2.3');
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'app',
+ 'dependencies': {
+ 'foo': '^1.0.0',
+ 'bar': '^1.0.0',
+ },
+ })
+ ]).create();
+ await pubGet();
+ server.serve('foo', '1.2.4');
+ await listReportApply(context, [
+ _PackageVersion('foo', Version.parse('1.2.4')),
+ ], reportAssertions: (report) {
+ expect(
+ findChangeVersion(report, 'compatible', 'foo'),
+ '1.2.4',
+ );
+ });
+ });
+
+ testWithGolden('Adding transitive', (context) async {
+ (await servePackages())
+ ..serve('foo', '1.2.3')
+ ..serve('foo', '2.2.3', deps: {'transitive': '^1.0.0'})
+ ..serve('transitive', '1.0.0');
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'app',
+ 'dependencies': {
+ 'foo': '^1.0.0',
+ },
+ })
+ ]).create();
+ await pubGet();
+ await listReportApply(context, [
+ _PackageVersion('foo', Version.parse('2.2.3')),
+ _PackageVersion('transitive', Version.parse('1.0.0'))
+ ], reportAssertions: (report) {
+ expect(
+ findChangeVersion(report, 'singleBreaking', 'foo'),
+ '2.2.3',
+ );
+ expect(
+ findChangeVersion(report, 'singleBreaking', 'transitive'),
+ '1.0.0',
+ );
+ });
+ });
+
+ testWithGolden('multibreaking', (context) async {
+ final server = (await servePackages())
+ ..serve('foo', '1.0.0')
+ ..serve('bar', '1.0.0');
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'app',
+ 'dependencies': {
+ 'foo': '^1.0.0',
+ 'bar': '^1.0.0',
+ },
+ })
+ ]).create();
+ await pubGet();
+ server
+ ..serve('foo', '1.5.0') // compatible
+ ..serve('foo', '2.0.0') // single breaking
+ ..serve('foo', '3.0.0', deps: {'bar': '^2.0.0'}) // multi breaking
+ ..serve('foo', '3.0.1', deps: {'bar': '^2.0.0'})
+ ..serve('bar', '2.0.0', deps: {'foo': '^3.0.0'})
+ ..serve('transitive', '1.0.0');
+ await listReportApply(context, [
+ _PackageVersion('foo', Version.parse('3.0.1'),
+ constraint: VersionConstraint.parse('^3.0.0')),
+ _PackageVersion('bar', Version.parse('2.0.0'))
+ ], reportAssertions: (report) {
+ expect(
+ findChangeVersion(report, 'multiBreaking', 'foo'),
+ '3.0.1',
+ );
+ expect(
+ findChangeVersion(report, 'multiBreaking', 'bar'),
+ '2.0.0',
+ );
+ });
+ });
+}
+
+dynamic findChangeVersion(dynamic json, String updateType, String name) {
+ final dep = json['dependencies'].firstWhere((p) => p['name'] == 'foo');
+ return dep[updateType].firstWhere((p) => p['name'] == name)['version'];
+}
+
+class _PackageVersion {
+ String name;
+ Version? version;
+ VersionConstraint? constraint;
+ _PackageVersion(this.name, this.version, {this.constraint});
+
+ Map<String, Object?> toJson() => {
+ 'name': name,
+ 'version': version?.toString(),
+ if (constraint != null) 'constraint': constraint.toString()
+ };
+}
diff --git a/test/golden_file.dart b/test/golden_file.dart
index 7927776..6a88dd9 100644
--- a/test/golden_file.dart
+++ b/test/golden_file.dart
@@ -151,6 +151,7 @@
List<String> args, {
Map<String, String>? environment,
String? workingDirectory,
+ String? stdin,
}) async {
// Create new section index number (before doing anything async)
final sectionIndex = _nextSectionIndex++;
@@ -161,6 +162,7 @@
s,
environment: environment,
workingDirectory: workingDirectory,
+ stdin: stdin,
);
_expectSection(sectionIndex, s.toString());
diff --git a/test/test_pub.dart b/test/test_pub.dart
index fbf63de..94e64fc 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -891,7 +891,8 @@
/// A [StreamMatcher] that matches multiple lines of output.
StreamMatcher emitsLines(String output) => emitsInOrder(output.split('\n'));
-Iterable<String> _filter(List<String> input) {
+/// Removes output from pub known to be unstable.
+Iterable<String> filterUnstableLines(List<String> input) {
return input
// Downloading order is not deterministic, so to avoid flakiness we filter
// out these lines.
@@ -916,12 +917,18 @@
StringBuffer buffer, {
Map<String, String>? environment,
String? workingDirectory,
+ String? stdin,
}) async {
final process = await startPub(
args: args,
environment: environment,
workingDirectory: workingDirectory,
);
+ if (stdin != null) {
+ process.stdin.write(stdin);
+ await process.stdin.flush();
+ await process.stdin.close();
+ }
final exitCode = await process.exitCode;
// TODO(jonasfj): Clean out temporary directory names from env vars...
@@ -933,11 +940,12 @@
// .map((e) => '\$ export ${e.key}=${e.value}')
// .join('\n'));
// }
- buffer.writeln(_filter([
- '\$ pub ${args.join(' ')}',
+ final pipe = stdin == null ? '' : ' echo ${escapeShellArgument(stdin)} |';
+ buffer.writeln(filterUnstableLines([
+ '\$$pipe pub ${args.map(escapeShellArgument).join(' ')}',
...await process.stdout.rest.toList(),
]).join('\n'));
- for (final line in _filter(await process.stderr.rest.toList())) {
+ for (final line in filterUnstableLines(await process.stderr.rest.toList())) {
buffer.writeln('[STDERR] $line');
}
if (exitCode != 0) {
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt
new file mode 100644
index 0000000..368a120
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt
@@ -0,0 +1,115 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":"^1.0.0"},"environment":{"sdk":">=0.1.2 <1.0.0"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.2.3"
+sdks:
+ dart: ">=0.1.2 <1.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section list
+$ dependency_services list
+{
+ "dependencies": [
+ {
+ "name": "foo",
+ "version": "1.2.3",
+ "kind": "direct",
+ "constraint": "^1.0.0"
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ dependency_services report
+{
+ "dependencies": [
+ {
+ "name": "foo",
+ "version": "1.2.3",
+ "kind": "direct",
+ "latest": "2.2.3",
+ "constraint": "^1.0.0",
+ "compatible": [],
+ "singleBreaking": [
+ {
+ "name": "foo",
+ "version": "2.2.3",
+ "kind": "direct",
+ "constraint": "^2.2.3",
+ "previousVersion": "1.2.3",
+ "previousConstraint": "^1.0.0"
+ },
+ {
+ "name": "transitive",
+ "version": "1.0.0",
+ "kind": "transitive",
+ "constraint": null,
+ "previousVersion": null,
+ "previousConstraint": null
+ }
+ ],
+ "multiBreaking": [
+ {
+ "name": "foo",
+ "version": "2.2.3",
+ "kind": "direct",
+ "constraint": "^2.2.3",
+ "previousVersion": "1.2.3",
+ "previousConstraint": "^1.0.0"
+ },
+ {
+ "name": "transitive",
+ "version": "1.0.0",
+ "kind": "transitive",
+ "constraint": null,
+ "previousVersion": null,
+ "previousConstraint": null
+ }
+ ]
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section apply
+$ echo '{"dependencyChanges":[{"name":"foo","version":"2.2.3"},{"name":"transitive","version":"1.0.0"}]}' | dependency_services apply
+{"dependencies":[]}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":^2.2.3},"environment":{"sdk":">=0.1.2 <1.0.0"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "2.2.3"
+ transitive:
+ dependency: transitive
+ description:
+ name: transitive
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.0.0"
+sdks:
+ dart: ">=0.1.2 <1.0.0"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt
new file mode 100644
index 0000000..4c3fd0b
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt
@@ -0,0 +1,149 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":"^1.0.0","bar":"^1.0.0"},"environment":{"sdk":">=0.1.2 <1.0.0"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ bar:
+ dependency: "direct main"
+ description:
+ name: bar
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.2.3"
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.2.3"
+sdks:
+ dart: ">=0.1.2 <1.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section list
+$ dependency_services list
+{
+ "dependencies": [
+ {
+ "name": "bar",
+ "version": "1.2.3",
+ "kind": "direct",
+ "constraint": "^1.0.0"
+ },
+ {
+ "name": "foo",
+ "version": "1.2.3",
+ "kind": "direct",
+ "constraint": "^1.0.0"
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ dependency_services report
+{
+ "dependencies": [
+ {
+ "name": "bar",
+ "version": "1.2.3",
+ "kind": "direct",
+ "latest": "2.2.3",
+ "constraint": "^1.0.0",
+ "compatible": [],
+ "singleBreaking": [
+ {
+ "name": "bar",
+ "version": "2.2.3",
+ "kind": "direct",
+ "constraint": "^2.2.3",
+ "previousVersion": "1.2.3",
+ "previousConstraint": "^1.0.0"
+ }
+ ],
+ "multiBreaking": [
+ {
+ "name": "bar",
+ "version": "2.2.3",
+ "kind": "direct",
+ "constraint": "^2.2.3",
+ "previousVersion": "1.2.3",
+ "previousConstraint": "^1.0.0"
+ }
+ ]
+ },
+ {
+ "name": "foo",
+ "version": "1.2.3",
+ "kind": "direct",
+ "latest": "2.2.3",
+ "constraint": "^1.0.0",
+ "compatible": [
+ {
+ "name": "foo",
+ "version": "1.2.4",
+ "kind": "direct",
+ "constraint": "^1.0.0",
+ "previousVersion": "1.2.3",
+ "previousConstraint": "^1.0.0"
+ }
+ ],
+ "singleBreaking": [
+ {
+ "name": "foo",
+ "version": "2.2.3",
+ "kind": "direct",
+ "constraint": "^2.2.3",
+ "previousVersion": "1.2.3",
+ "previousConstraint": "^1.0.0"
+ }
+ ],
+ "multiBreaking": [
+ {
+ "name": "foo",
+ "version": "2.2.3",
+ "kind": "direct",
+ "constraint": "^2.2.3",
+ "previousVersion": "1.2.3",
+ "previousConstraint": "^1.0.0"
+ }
+ ]
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section apply
+$ echo '{"dependencyChanges":[{"name":"foo","version":"1.2.4"}]}' | dependency_services apply
+{"dependencies":[]}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":"^1.0.0","bar":"^1.0.0"},"environment":{"sdk":">=0.1.2 <1.0.0"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ bar:
+ dependency: "direct main"
+ description:
+ name: bar
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.2.3"
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.2.4"
+sdks:
+ dart: ">=0.1.2 <1.0.0"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt
new file mode 100644
index 0000000..3a0133a
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt
@@ -0,0 +1,131 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":"^1.0.0"},"environment":{"sdk":">=0.1.2 <1.0.0"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.2.3"
+ transitive:
+ dependency: transitive
+ description:
+ name: transitive
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.0.0"
+sdks:
+ dart: ">=0.1.2 <1.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section list
+$ dependency_services list
+{
+ "dependencies": [
+ {
+ "name": "foo",
+ "version": "1.2.3",
+ "kind": "direct",
+ "constraint": "^1.0.0"
+ },
+ {
+ "name": "transitive",
+ "version": "1.0.0",
+ "kind": "transitive",
+ "constraint": "null"
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ dependency_services report
+{
+ "dependencies": [
+ {
+ "name": "foo",
+ "version": "1.2.3",
+ "kind": "direct",
+ "latest": "2.2.3",
+ "constraint": "^1.0.0",
+ "compatible": [],
+ "singleBreaking": [
+ {
+ "name": "foo",
+ "version": "2.2.3",
+ "kind": "direct",
+ "constraint": "^2.2.3",
+ "previousVersion": "1.2.3",
+ "previousConstraint": "^1.0.0"
+ },
+ {
+ "name": "transitive",
+ "version": null,
+ "kind": "transitive",
+ "constraint": null,
+ "previousVersion": "1.0.0",
+ "previousConstraint": null
+ }
+ ],
+ "multiBreaking": [
+ {
+ "name": "foo",
+ "version": "2.2.3",
+ "kind": "direct",
+ "constraint": "^2.2.3",
+ "previousVersion": "1.2.3",
+ "previousConstraint": "^1.0.0"
+ },
+ {
+ "name": "transitive",
+ "version": null,
+ "kind": "transitive",
+ "constraint": null,
+ "previousVersion": "1.0.0",
+ "previousConstraint": null
+ }
+ ]
+ },
+ {
+ "name": "transitive",
+ "version": "1.0.0",
+ "kind": "transitive",
+ "latest": "1.0.0",
+ "constraint": null,
+ "compatible": [],
+ "singleBreaking": [],
+ "multiBreaking": []
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section apply
+$ echo '{"dependencyChanges":[{"name":"foo","version":"2.2.3"},{"name":"transitive","version":null}]}' | dependency_services apply
+{"dependencies":[]}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":^2.2.3},"environment":{"sdk":">=0.1.2 <1.0.0"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "2.2.3"
+sdks:
+ dart: ">=0.1.2 <1.0.0"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt b/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt
new file mode 100644
index 0000000..1b3af24
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt
@@ -0,0 +1,156 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":"^1.0.0","bar":"^1.0.0"},"environment":{"sdk":">=0.1.2 <1.0.0"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ bar:
+ dependency: "direct main"
+ description:
+ name: bar
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.0.0"
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.0.0"
+sdks:
+ dart: ">=0.1.2 <1.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section list
+$ dependency_services list
+{
+ "dependencies": [
+ {
+ "name": "bar",
+ "version": "1.0.0",
+ "kind": "direct",
+ "constraint": "^1.0.0"
+ },
+ {
+ "name": "foo",
+ "version": "1.0.0",
+ "kind": "direct",
+ "constraint": "^1.0.0"
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ dependency_services report
+{
+ "dependencies": [
+ {
+ "name": "bar",
+ "version": "1.0.0",
+ "kind": "direct",
+ "latest": "2.0.0",
+ "constraint": "^1.0.0",
+ "compatible": [],
+ "singleBreaking": [],
+ "multiBreaking": [
+ {
+ "name": "bar",
+ "version": "2.0.0",
+ "kind": "direct",
+ "constraint": "^2.0.0",
+ "previousVersion": "1.0.0",
+ "previousConstraint": "^1.0.0"
+ },
+ {
+ "name": "foo",
+ "version": "3.0.1",
+ "kind": "direct",
+ "constraint": "^3.0.1",
+ "previousVersion": "1.0.0",
+ "previousConstraint": "^1.0.0"
+ }
+ ]
+ },
+ {
+ "name": "foo",
+ "version": "1.0.0",
+ "kind": "direct",
+ "latest": "3.0.1",
+ "constraint": "^1.0.0",
+ "compatible": [
+ {
+ "name": "foo",
+ "version": "1.5.0",
+ "kind": "direct",
+ "constraint": "^1.0.0",
+ "previousVersion": "1.0.0",
+ "previousConstraint": "^1.0.0"
+ }
+ ],
+ "singleBreaking": [
+ {
+ "name": "foo",
+ "version": "2.0.0",
+ "kind": "direct",
+ "constraint": "^2.0.0",
+ "previousVersion": "1.0.0",
+ "previousConstraint": "^1.0.0"
+ }
+ ],
+ "multiBreaking": [
+ {
+ "name": "foo",
+ "version": "3.0.1",
+ "kind": "direct",
+ "constraint": "^3.0.1",
+ "previousVersion": "1.0.0",
+ "previousConstraint": "^1.0.0"
+ },
+ {
+ "name": "bar",
+ "version": "2.0.0",
+ "kind": "direct",
+ "constraint": "^2.0.0",
+ "previousVersion": "1.0.0",
+ "previousConstraint": "^1.0.0"
+ }
+ ]
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section apply
+$ echo '{"dependencyChanges":[{"name":"foo","version":"3.0.1","constraint":"^3.0.0"},{"name":"bar","version":"2.0.0"}]}' | dependency_services apply
+{"dependencies":[]}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":^3.0.0,"bar":^2.0.0},"environment":{"sdk":">=0.1.2 <1.0.0"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ bar:
+ dependency: "direct main"
+ description:
+ name: bar
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "2.0.0"
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "3.0.1"
+sdks:
+ dart: ">=0.1.2 <1.0.0"
diff --git a/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt b/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt
index 237b3b5..d80d984 100644
--- a/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt
+++ b/test/testdata/goldens/directory_option_test/commands taking a --directory~-C parameter work.txt
@@ -17,7 +17,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section 2
-$ pub -C myapp/example get --directory=myapp bar
+$ pub -C 'myapp/example' get --directory=myapp bar
Resolving dependencies in myapp...
Got dependencies in myapp!
@@ -40,7 +40,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section 5
-$ pub get bar -C myapp/example
+$ pub get bar -C 'myapp/example'
Resolving dependencies in myapp/example...
+ foo 1.0.0
+ test_pkg 1.0.0 from path myapp
@@ -49,7 +49,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section 6
-$ pub get bar -C myapp/example2
+$ pub get bar -C 'myapp/example2'
Resolving dependencies in myapp/example2...
[STDERR] Error on line 1, column 9 of myapp/pubspec.yaml: "name" field doesn't match expected name "myapp".
[STDERR] ╷
@@ -61,7 +61,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section 7
-$ pub get bar -C myapp/broken_dir
+$ pub get bar -C 'myapp/broken_dir'
[STDERR] Could not find a file named "pubspec.yaml" in "$SANDBOX/myapp/broken_dir".
[EXIT CODE] 66
@@ -84,7 +84,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section 10
-$ pub run -C myapp bin/app.dart
+$ pub run -C myapp 'bin/app.dart'
Building package executable...
Built test_pkg:app.
Hi