Smallest update in `dependency-services report` (#3761)
diff --git a/lib/src/command/dependency_services.dart b/lib/src/command/dependency_services.dart
index 44ae700..f1d8143 100644
--- a/lib/src/command/dependency_services.dart
+++ b/lib/src/command/dependency_services.dart
@@ -14,6 +14,7 @@
import 'package:yaml_edit/yaml_edit.dart';
import '../command.dart';
+import '../entrypoint.dart';
import '../exceptions.dart';
import '../io.dart';
import '../lock_file.dart';
@@ -23,6 +24,7 @@
import '../pubspec.dart';
import '../pubspec_utils.dart';
import '../solver.dart';
+import '../solver/version_solver.dart';
import '../source/git.dart';
import '../source/hosted.dart';
import '../system_cache.dart';
@@ -51,133 +53,42 @@
@override
Future<void> runProtected() async {
+ final stdinString = await utf8.decodeStream(stdin);
+ final input = json.decode(stdinString.isEmpty ? '{}' : stdinString)
+ as Map<String, Object?>;
+ final additionalConstraints = _parseDisallowed(input, cache);
+ final targetPackageName = input['target'];
+ if (targetPackageName is! String?) {
+ throw FormatException('"target" should be a String.');
+ }
+
final compatiblePubspec = stripDependencyOverrides(entrypoint.root.pubspec);
final breakingPubspec = stripVersionBounds(compatiblePubspec);
- final compatiblePackagesResult =
- await _tryResolve(compatiblePubspec, cache);
+ final compatiblePackagesResult = await _tryResolve(
+ compatiblePubspec,
+ cache,
+ additionalConstraints: additionalConstraints,
+ );
- final breakingPackagesResult = await _tryResolve(breakingPubspec, cache);
+ final breakingPackagesResult = await _tryResolve(
+ breakingPubspec,
+ cache,
+ additionalConstraints: additionalConstraints,
+ );
- // 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 as PackageId).name,
- );
- }
- currentPackages.remove(entrypoint.root.name);
+ final currentPackages = await _computeCurrentPackages(entrypoint, cache);
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
- ? stripVersionBounds(rootPubspec)
- : Pubspec(
- rootPubspec.name,
- dependencies: rootPubspec.dependencies.values,
- devDependencies: rootPubspec.devDependencies.values,
- sdkConstraints: rootPubspec.sdkConstraints,
- );
+ final targetPackage =
+ targetPackageName == null ? null : currentPackages[targetPackageName];
- final dependencySet = _dependencySetOfPackage(pubspec, package);
- if (dependencySet != null) {
- // Force the version to be the new version.
- dependencySet[package.name] =
- package.toRef().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 != originalVersion;
- }).map((p) {
- final depset = _dependencySetOfPackage(rootPubspec, p);
- final originalConstraint = depset?[p.name]?.constraint;
- final currentPackage = currentPackages[p.name];
- return {
- 'name': p.name,
- 'version': p.versionOrHash(),
- 'kind': _kindString(pubspec, p.name),
- 'source': _source(p, containingDir: directory),
- 'constraintBumped': originalConstraint == null
- ? null
- : upgradeType == _UpgradeType.compatible
- ? originalConstraint.toString()
- : _bumpConstraint(originalConstraint, p.version).toString(),
- 'constraintWidened': originalConstraint == null
- ? null
- : upgradeType == _UpgradeType.compatible
- ? originalConstraint.toString()
- : _widenConstraint(originalConstraint, p.version)
- .toString(),
- 'constraintBumpedIfNeeded': originalConstraint == null
- ? null
- : upgradeType == _UpgradeType.compatible
- ? originalConstraint.toString()
- : originalConstraint.allows(p.version)
- ? originalConstraint.toString()
- : _bumpConstraint(originalConstraint, p.version)
- .toString(),
- 'previousVersion': currentPackage?.versionOrHash(),
- 'previousConstraint': originalConstraint?.toString(),
- 'previousSource': currentPackage == null
- ? null
- : _source(currentPackage, containingDir: directory),
- };
- }),
- // Find packages that were removed by the resolution
- 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.
- 'constraintBumped': null,
- 'constraintWidened': null,
- 'constraintBumpedIfNeeded': null,
- 'previousVersion':
- currentPackages[oldPackageName]?.versionOrHash(),
- 'previousConstraint': null,
- 'previous': _source(
- currentPackages[oldPackageName]!,
- containingDir: directory,
- )
- },
- ];
- }
-
- for (final package in currentPackages.values) {
+ for (final package in targetPackage == null
+ ? currentPackages.values
+ : <PackageId>[targetPackage]) {
final compatibleVersion = compatiblePackagesResult
?.firstWhereOrNull((element) => element.name == package.name);
final multiBreakingVersion = breakingPackagesResult
@@ -202,6 +113,42 @@
singleBreakingVersion = singleBreakingPackagesResult
?.firstWhereOrNull((element) => element.name == package.name);
}
+ PackageId? smallestUpgrade;
+ if (additionalConstraints.any(
+ (c) => c.range.toRef() == package.toRef() && !c.range.allows(package),
+ )) {
+ // Current version disallowed by restrictions.
+ final atLeastCurrentPubspec = atLeastCurrent(
+ compatiblePubspec,
+ entrypoint.lockFile.packages.values.toList(),
+ );
+
+ final smallestUpgradeResult = await _tryResolve(
+ atLeastCurrentPubspec,
+ cache,
+ solveType: SolveType.downgrade,
+ additionalConstraints: additionalConstraints,
+ );
+
+ smallestUpgrade = smallestUpgradeResult
+ ?.firstWhereOrNull((element) => element.name == package.name);
+ }
+
+ Future<List<Object>> computeUpgradeSet(
+ PackageId? package,
+ _UpgradeType upgradeType,
+ ) async {
+ return await _computeUpgradeSet(
+ compatiblePubspec,
+ package,
+ entrypoint,
+ cache,
+ currentPackages: currentPackages,
+ upgradeType: upgradeType,
+ additionalConstraints: additionalConstraints,
+ );
+ }
+
dependencies.add({
'name': package.name,
'version': package.versionOrHash(),
@@ -213,64 +160,32 @@
'constraint':
_constraintOf(compatiblePubspec, package.name)?.toString(),
'compatible': await computeUpgradeSet(
- compatiblePubspec,
compatibleVersion,
- upgradeType: _UpgradeType.compatible,
+ _UpgradeType.compatible,
),
'singleBreaking': kind != 'transitive' && singleBreakingVersion == null
? []
: await computeUpgradeSet(
- compatiblePubspec,
singleBreakingVersion,
- upgradeType: _UpgradeType.singleBreaking,
+ _UpgradeType.singleBreaking,
),
'multiBreaking': kind != 'transitive' && multiBreakingVersion != null
? await computeUpgradeSet(
- compatiblePubspec,
multiBreakingVersion,
- upgradeType: _UpgradeType.multiBreaking,
+ _UpgradeType.multiBreaking,
)
: [],
+ if (smallestUpgrade != null)
+ 'smallestUpdate': await computeUpgradeSet(
+ smallestUpgrade,
+ _UpgradeType.smallestUpdate,
+ ),
});
}
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';
-}
-
-Map<String, Object?> _source(PackageId id, {required String containingDir}) {
- return {
- 'type': id.source.name,
- 'description':
- id.description.serializeForLockfile(containingDir: containingDir),
- };
-}
-
-/// 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';
@@ -336,6 +251,9 @@
/// Unlock any dependencies in pubspec.yaml needed for getting the
/// latest resolvable version.
multiBreaking,
+
+ /// Try to upgrade as little as possible.
+ smallestUpdate,
}
class DependencyServicesApplyCommand extends PubCommand {
@@ -715,6 +633,231 @@
return false;
}
+/// 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, {
+ SolveType solveType = SolveType.upgrade,
+ Iterable<ConstraintAndCause>? additionalConstraints,
+}) async {
+ final solveResult = await tryResolveVersions(
+ solveType,
+ cache,
+ Package.inMemory(pubspec),
+ additionalConstraints: additionalConstraints,
+ );
+
+ return solveResult?.packages;
+}
+
+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';
+}
+
+Map<String, Object?> _source(PackageId id, {required String containingDir}) {
+ return {
+ 'type': id.source.name,
+ 'description':
+ id.description.serializeForLockfile(containingDir: containingDir),
+ };
+}
+
+/// The packages in the current lockfile or resolved from current pubspec.yaml.
+/// Does not include the root package.
+Future<Map<String, PackageId>> _computeCurrentPackages(
+ Entrypoint entrypoint,
+ SystemCache cache,
+) async {
+ 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 as PackageId).name,
+ );
+ }
+ currentPackages.remove(entrypoint.root.name);
+ return currentPackages;
+}
+
+Future<List<Object>> _computeUpgradeSet(
+ Pubspec rootPubspec,
+ PackageId? package,
+ Entrypoint entrypoint,
+ SystemCache cache, {
+ required Map<String, PackageId> currentPackages,
+ required _UpgradeType upgradeType,
+ required List<ConstraintAndCause> additionalConstraints,
+}) async {
+ if (package == null) return [];
+ final lockFile = entrypoint.lockFile;
+ final pubspec = (upgradeType == _UpgradeType.multiBreaking ||
+ upgradeType == _UpgradeType.smallestUpdate)
+ ? stripVersionBounds(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.toRef().withConstraint(package.toRange().constraint);
+ }
+
+ final resolution = await tryResolveVersions(
+ upgradeType == _UpgradeType.smallestUpdate
+ ? SolveType.downgrade
+ : SolveType.get,
+ cache,
+ Package.inMemory(pubspec),
+ lockFile: lockFile,
+ additionalConstraints: additionalConstraints,
+ );
+
+ // TODO(sigurdm): improve error messages.
+ if (resolution == null) {
+ return [];
+ }
+
+ return [
+ ...resolution.packages.where((r) {
+ if (r.name == rootPubspec.name) return false;
+ final originalVersion = currentPackages[r.name];
+ return originalVersion == null || r != originalVersion;
+ }).map((p) {
+ final depset = _dependencySetOfPackage(rootPubspec, p);
+ final originalConstraint = depset?[p.name]?.constraint;
+ final currentPackage = currentPackages[p.name];
+ return {
+ 'name': p.name,
+ 'version': p.versionOrHash(),
+ 'kind': _kindString(pubspec, p.name),
+ 'source': _source(p, containingDir: entrypoint.root.dir),
+ 'constraintBumped': originalConstraint == null
+ ? null
+ : upgradeType == _UpgradeType.compatible
+ ? originalConstraint.toString()
+ : _bumpConstraint(originalConstraint, p.version).toString(),
+ 'constraintWidened': originalConstraint == null
+ ? null
+ : upgradeType == _UpgradeType.compatible
+ ? originalConstraint.toString()
+ : _widenConstraint(originalConstraint, p.version).toString(),
+ 'constraintBumpedIfNeeded': originalConstraint == null
+ ? null
+ : upgradeType == _UpgradeType.compatible
+ ? originalConstraint.toString()
+ : originalConstraint.allows(p.version)
+ ? originalConstraint.toString()
+ : _bumpConstraint(originalConstraint, p.version).toString(),
+ 'previousVersion': currentPackage?.versionOrHash(),
+ 'previousConstraint': originalConstraint?.toString(),
+ 'previousSource': currentPackage == null
+ ? null
+ : _source(currentPackage, containingDir: entrypoint.root.dir),
+ };
+ }),
+ // Find packages that were removed by the resolution
+ 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.
+ 'constraintBumped': null,
+ 'constraintWidened': null,
+ 'constraintBumpedIfNeeded': null,
+ 'previousVersion': currentPackages[oldPackageName]?.versionOrHash(),
+ 'previousConstraint': null,
+ 'previous': _source(
+ currentPackages[oldPackageName]!,
+ containingDir: entrypoint.root.dir,
+ )
+ },
+ ];
+}
+
+List<ConstraintAndCause> _parseDisallowed(
+ Map<String, Object?> input,
+ SystemCache cache,
+) {
+ final disallowedList = input['disallowed'];
+ if (disallowedList == null) {
+ return [];
+ }
+ if (disallowedList is! List<Object?>) {
+ throw FormatException('Disallowed should be a list of maps');
+ }
+ final result = <ConstraintAndCause>[];
+ for (final disallowed in disallowedList) {
+ if (disallowed is! Map) {
+ throw FormatException('Disallowed should be a list of maps');
+ }
+ final name = disallowed['name'];
+ if (name is! String) {
+ throw FormatException('"name" should be a string.');
+ }
+ final url = disallowed['url'] ?? cache.hosted.defaultUrl;
+ if (url is! String) {
+ throw FormatException('"url" should be a string.');
+ }
+ final ref = PackageRef(
+ name,
+ HostedDescription(
+ name,
+ url,
+ ),
+ );
+ final constraints = disallowed['versions'];
+ if (constraints is! List) {
+ throw FormatException('"versions" should be a list.');
+ }
+ final reason = disallowed['reason'];
+ if (reason is! String?) {
+ throw FormatException('"reason", if present, should be a string.');
+ }
+ for (final entry in constraints) {
+ if (entry is! Map) {
+ throw FormatException(
+ 'Each element of "versions" should be an object.',
+ );
+ }
+ final rangeString = entry['range'];
+ if (rangeString is! String) {
+ throw FormatException('"range" should be a string');
+ }
+ final range = VersionConstraint.parse(rangeString);
+ result.add(
+ ConstraintAndCause(
+ PackageRange(ref, VersionConstraint.any.difference(range)),
+ reason,
+ ),
+ );
+ }
+ }
+ return result;
+}
+
/// `true` iff any of the packages described by the [lockfile] uses
/// `https://pub.dev` as url.
///
diff --git a/lib/src/pubspec_utils.dart b/lib/src/pubspec_utils.dart
index 3f57976..5edf74a 100644
--- a/lib/src/pubspec_utils.dart
+++ b/lib/src/pubspec_utils.dart
@@ -2,6 +2,7 @@
// 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:collection/collection.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package_name.dart';
@@ -84,6 +85,42 @@
);
}
+/// Returns a pubspec with the same dependencies as [original] but with all
+/// version constraints replaced by `>=c` where `c`, is the member of `current`
+/// that has same name as the dependency.
+Pubspec atLeastCurrent(Pubspec original, List<PackageId> current) {
+ List<PackageRange> fixBounds(
+ Map<String, PackageRange> constrained,
+ ) {
+ final result = <PackageRange>[];
+
+ for (final name in constrained.keys) {
+ final packageRange = constrained[name]!;
+ final currentVersion = current.firstWhereOrNull((id) => id.name == name);
+ if (currentVersion == null) {
+ result.add(packageRange);
+ } else {
+ result.add(
+ packageRange.toRef().withConstraint(
+ VersionRange(min: currentVersion.version, includeMin: true),
+ ),
+ );
+ }
+ }
+
+ return result;
+ }
+
+ return Pubspec(
+ original.name,
+ version: original.version,
+ sdkConstraints: original.sdkConstraints,
+ dependencies: fixBounds(original.dependencies),
+ devDependencies: fixBounds(original.devDependencies),
+ dependencyOverrides: original.dependencyOverrides.values,
+ );
+}
+
/// Removes the upper bound of [constraint]. If [constraint] is the
/// empty version constraint, [VersionConstraint.empty] will be returned.
VersionConstraint stripUpperBound(VersionConstraint constraint) {
diff --git a/lib/src/solver.dart b/lib/src/solver.dart
index b8f68e7..71e2fcd 100644
--- a/lib/src/solver.dart
+++ b/lib/src/solver.dart
@@ -29,6 +29,9 @@
/// If [unlock] is empty [SolveType.get] interprets this as lock everything,
/// while [SolveType.upgrade] and [SolveType.downgrade] interprets an empty
/// [unlock] as unlock everything.
+///
+/// [additionalConstraints] can contain a list of extra constraints for this
+/// resolution.
Future<SolveResult> resolveVersions(
SolveType type,
SystemCache cache,
@@ -36,16 +39,21 @@
LockFile? lockFile,
Iterable<String> unlock = const [],
Map<String, Version> sdkOverrides = const {},
+ Iterable<ConstraintAndCause>? additionalConstraints,
}) {
lockFile ??= LockFile.empty();
- return VersionSolver(
+ final solver = VersionSolver(
type,
cache,
root,
lockFile,
unlock,
sdkOverrides: sdkOverrides,
- ).solve();
+ );
+ if (additionalConstraints != null) {
+ solver.addConstraints(additionalConstraints);
+ }
+ return solver.solve();
}
/// Attempts to select the best concrete versions for all of the transitive
@@ -68,6 +76,7 @@
Package root, {
LockFile? lockFile,
Iterable<String>? unlock,
+ Iterable<ConstraintAndCause>? additionalConstraints,
}) async {
try {
return await resolveVersions(
@@ -76,6 +85,7 @@
root,
lockFile: lockFile,
unlock: unlock ?? [],
+ additionalConstraints: additionalConstraints,
);
} on SolveFailure {
return null;
diff --git a/lib/src/solver/incompatibility_cause.dart b/lib/src/solver/incompatibility_cause.dart
index 13047df..01b9dcf 100644
--- a/lib/src/solver/incompatibility_cause.dart
+++ b/lib/src/solver/incompatibility_cause.dart
@@ -147,3 +147,15 @@
@override
String? get hint => exception.hint;
}
+
+/// The incompatibility represents a package-version that is not allowed to be
+/// used in the solve for some external reason.
+class PackageVersionForbiddenCause extends IncompatibilityCause {
+ /// The reason this package version was forbidden.
+ final String? reason;
+
+ PackageVersionForbiddenCause({this.reason});
+
+ @override
+ String? get hint => reason;
+}
diff --git a/lib/src/solver/version_solver.dart b/lib/src/solver/version_solver.dart
index afef413..5194e59 100644
--- a/lib/src/solver/version_solver.dart
+++ b/lib/src/solver/version_solver.dart
@@ -92,6 +92,18 @@
_dependencyOverrides = _root.dependencyOverrides,
_unlock = {...unlock};
+ /// Prime the solver with [constraints].
+ void addConstraints(Iterable<ConstraintAndCause> constraints) {
+ for (final constraint in constraints) {
+ _addIncompatibility(
+ Incompatibility(
+ [Term(constraint.range, false)],
+ PackageVersionForbiddenCause(reason: constraint.cause),
+ ),
+ );
+ }
+ }
+
/// Finds a set of dependencies that match the root package's constraints, or
/// throws an error if no such set is available.
Future<SolveResult> solve() async {
@@ -581,3 +593,20 @@
log.solver(prefixLines(message, prefix: ' ' * _solution.decisionLevel));
}
}
+
+// An additional constraint to a version resolution.
+class ConstraintAndCause {
+ /// Stated like constraints in the pubspec. (The constraint specifies those
+ /// versions that are allowed).
+ ///
+ /// Meaning that to forbid a version you must do
+ /// `VersionConstraint.any.difference(version)`.
+ ///
+ /// Example:
+ /// `ConstraintAndCause(packageRef, VersionConstraint.parse('> 1.0.0'))`
+ /// To require `packageRef` be greater than `1.0.0`.
+ final PackageRange range;
+ final String? cause;
+
+ ConstraintAndCause(this.range, this.cause);
+}
diff --git a/test/dependency_services/dependency_services_test.dart b/test/dependency_services/dependency_services_test.dart
index 4dbef36..f372701 100644
--- a/test/dependency_services/dependency_services_test.dart
+++ b/test/dependency_services/dependency_services_test.dart
@@ -46,7 +46,7 @@
/// Returns the stdout.
Future<String> runDependencyServices(
List<String> args, {
- String? stdin,
+ String stdin = '',
}) async {
final buffer = StringBuffer();
buffer.writeln('## Section ${args.join(' ')}');
@@ -64,16 +64,15 @@
},
workingDirectory: p.join(d.sandbox, appPath),
);
- if (stdin != null) {
- process.stdin.write(stdin);
- await process.stdin.flush();
- await process.stdin.close();
- }
+ 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)} |';
+ final pipe = ' echo ${filterUnstableText(escapeShellArgument(stdin))} |';
buffer.writeln(
[
'\$$pipe dependency_services ${args.map(escapeShellArgument).join(' ')}',
@@ -113,6 +112,33 @@
manifestAndLockfile(context);
}
+Future<void> _reportWithForbidden(
+ GoldenTestContext context,
+ Map<String, List<String>> disallowedVersions, {
+ void Function(Map)? resultAssertions,
+ String? targetPackage,
+}) async {
+ manifestAndLockfile(context);
+ final input = json.encode({
+ 'target': targetPackage,
+ 'disallowed': [
+ for (final e in disallowedVersions.entries)
+ {
+ 'name': e.key,
+ 'url': globalServer.url,
+ 'versions': e.value.map((d) => {'range': d}).toList()
+ }
+ ]
+ });
+ final report = await context.runDependencyServices(['report'], stdin: input);
+ if (resultAssertions != null) {
+ resultAssertions(json.decode(report) as Map);
+ }
+
+ // await context.runDependencyServices(['apply'], stdin: input);
+ manifestAndLockfile(context);
+}
+
Future<void> main() async {
setUpAll(() async {
final tempDir = Directory.systemTemp.createTempSync();
@@ -161,6 +187,39 @@
);
});
+ testWithGolden('Ignoring version', (context) async {
+ final server = await servePackages();
+ server.serve('foo', '1.0.0');
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'app',
+ 'dependencies': {
+ 'foo': '^1.0.0',
+ },
+ })
+ ]).create();
+ await pubGet();
+
+ server.serve('foo', '1.0.1'); // should get this.
+ server.serve('foo', '1.0.2'); // ignored
+ server.serve('foo', '1.0.3', deps: {'transitive': '1.0.0'});
+ server.serve('transitive', '1.0.0'); // ignored
+ await _reportWithForbidden(
+ context,
+ {
+ 'foo': ['1.0.2'],
+ 'transitive': ['1.0.0'],
+ },
+ resultAssertions: (report) {
+ expect(
+ findChangeVersion(report, 'compatible', 'foo'),
+ '1.0.1',
+ );
+ },
+ );
+ });
+
testWithGolden('No pubspec.lock', (context) async {
final server = (await servePackages())
..serve('foo', '1.2.3', deps: {'transitive': '^1.0.0'})
@@ -465,6 +524,110 @@
},
);
});
+
+ testWithGolden('Finds smallest possible upgrade', (context) async {
+ final server = await servePackages();
+ server.serve('foo', '1.1.1'); // This version will be disallowed.
+
+ await d.appDir(dependencies: {'foo': '^1.0.0'}).create();
+ await pubGet();
+ server.serve(
+ 'foo',
+ '1.0.0',
+ ); // We don't want the downgrade to go below the current.
+
+ server.serve(
+ 'foo',
+ '1.1.2',
+ ); // This will also be disallowed, a minimal update should not find this.
+ server.serve('foo', '1.1.3'); // We would like this to be the new version.
+ server.serve('foo', '1.1.4'); // This version would not be a minimal update.
+
+ await _reportWithForbidden(
+ context,
+ {
+ 'foo': ['1.1.1', '1.1.2']
+ },
+ targetPackage: 'foo',
+ resultAssertions: (r) {
+ expect(findChangeVersion(r, 'smallestUpdate', 'foo'), '1.1.3');
+ },
+ );
+ });
+
+ testWithGolden('Smallest possible upgrade can upgrade beyond breaking',
+ (context) async {
+ final server = await servePackages();
+ server.serve('foo', '1.1.1'); // This version will be disallowed.
+
+ await d.appDir(dependencies: {'foo': '^1.0.0'}).create();
+ await pubGet();
+
+ server.serve(
+ 'foo',
+ '2.0.0',
+ ); // This will also be disallowed, a minimal update should not find this.
+ server.serve('foo', '2.0.1'); // We would like this to be the new version.
+ server.serve('foo', '2.0.2'); // This version would not be a minimal update.
+
+ await _reportWithForbidden(
+ context,
+ {
+ 'foo': ['1.1.1', '2.0.0']
+ },
+ targetPackage: 'foo',
+ resultAssertions: (r) {
+ expect(findChangeVersion(r, 'smallestUpdate', 'foo'), '2.0.1');
+ },
+ );
+ });
+
+ testWithGolden(
+ 'Smallest possible upgrade can upgrade other packages if needed',
+ (context) async {
+ final server = await servePackages();
+ server.serve('bar', '1.0.0');
+ server.serve('bar', '2.0.0');
+ server.serve('bar', '2.2.0');
+
+ server.serve(
+ 'foo',
+ '1.1.1',
+ deps: {'bar': '^1.0.0'},
+ ); // This version will be disallowed.
+
+ await d.appDir(dependencies: {'foo': '^1.0.0', 'bar': '^1.0.0'}).create();
+ await pubGet();
+
+ server.serve(
+ 'foo',
+ '2.0.0',
+ deps: {'bar': '^2.0.0'},
+ ); // This will also be disallowed, a minimal update should not find this.
+ server.serve(
+ 'foo',
+ '2.0.1',
+ deps: {'bar': '^2.0.0'},
+ ); // We would like this to be the new version.
+ server.serve(
+ 'foo',
+ '2.0.2',
+ deps: {'bar': '^2.0.0'},
+ ); // This version would not be a minimal update.
+
+ await _reportWithForbidden(
+ context,
+ {
+ 'foo': ['1.1.1', '2.0.0'],
+ 'bar': ['2.0.0']
+ },
+ targetPackage: 'foo',
+ resultAssertions: (r) {
+ expect(findChangeVersion(r, 'smallestUpdate', 'foo'), '2.0.1');
+ expect(findChangeVersion(r, 'smallestUpdate', 'bar'), '2.2.0');
+ },
+ );
+ });
}
dynamic findChangeVersion(dynamic json, String updateType, String name) {
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
index 9c04c2f..e0533ae 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Adding transitive.txt
@@ -19,7 +19,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section list
-$ dependency_services list
+$ echo '' | dependency_services list
{
"dependencies": [
{
@@ -42,7 +42,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section report
-$ dependency_services report
+$ echo '' | dependency_services report
{
"dependencies": [
{
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Can update a git package.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Can update a git package.txt
index c34c46f..da10be4 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Can update a git package.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Can update a git package.txt
@@ -29,7 +29,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section list
-$ dependency_services list
+$ echo '' | dependency_services list
{
"dependencies": [
{
@@ -68,7 +68,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section report
-$ dependency_services report
+$ echo '' | dependency_services report
{
"dependencies": [
{
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt
index 86e82b9..c4e816c 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Compatible.txt
@@ -35,7 +35,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section list
-$ dependency_services list
+$ echo '' | dependency_services list
{
"dependencies": [
{
@@ -86,7 +86,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section report
-$ dependency_services report
+$ echo '' | dependency_services report
{
"dependencies": [
{
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Finds smallest possible upgrade.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Finds smallest possible upgrade.txt
new file mode 100644
index 0000000..7fcfaf4
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Finds smallest possible upgrade.txt
@@ -0,0 +1,172 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"myapp","dependencies":{"foo":"^1.0.0"},"environment":{"sdk":"^3.0.2"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ sha256: ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.1.1"
+sdks:
+ dart: ">=3.0.2 <4.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ echo '{"target":"foo","disallowed":[{"name":"foo","url":"http://localhost:$PORT","versions":[{"range":"1.1.1"},{"range":"1.1.2"}]}]}' | dependency_services report
+{
+ "dependencies": [
+ {
+ "name": "foo",
+ "version": "1.1.1",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457"
+ }
+ },
+ "latest": "1.1.4",
+ "constraint": "^1.0.0",
+ "compatible": [
+ {
+ "name": "foo",
+ "version": "1.1.4",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "4e29148fe4f00dc3e48082c074a538bc10030ed519622ee350815d5b2fd96a6d"
+ }
+ },
+ "constraintBumped": "^1.0.0",
+ "constraintWidened": "^1.0.0",
+ "constraintBumpedIfNeeded": "^1.0.0",
+ "previousVersion": "1.1.1",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457"
+ }
+ }
+ }
+ ],
+ "singleBreaking": [
+ {
+ "name": "foo",
+ "version": "1.1.4",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "4e29148fe4f00dc3e48082c074a538bc10030ed519622ee350815d5b2fd96a6d"
+ }
+ },
+ "constraintBumped": "^1.1.4",
+ "constraintWidened": "^1.0.0",
+ "constraintBumpedIfNeeded": "^1.0.0",
+ "previousVersion": "1.1.1",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457"
+ }
+ }
+ }
+ ],
+ "multiBreaking": [
+ {
+ "name": "foo",
+ "version": "1.1.4",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "4e29148fe4f00dc3e48082c074a538bc10030ed519622ee350815d5b2fd96a6d"
+ }
+ },
+ "constraintBumped": "^1.1.4",
+ "constraintWidened": "^1.0.0",
+ "constraintBumpedIfNeeded": "^1.0.0",
+ "previousVersion": "1.1.1",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457"
+ }
+ }
+ }
+ ],
+ "smallestUpdate": [
+ {
+ "name": "foo",
+ "version": "1.1.3",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "5efda06be6bc23558d40eeab6f92729c53e5018621e7bda9868d97dd613693b9"
+ }
+ },
+ "constraintBumped": "^1.1.3",
+ "constraintWidened": "^1.0.0",
+ "constraintBumpedIfNeeded": "^1.0.0",
+ "previousVersion": "1.1.1",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457"
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"myapp","dependencies":{"foo":"^1.0.0"},"environment":{"sdk":"^3.0.2"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ sha256: ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.1.1"
+sdks:
+ dart: ">=3.0.2 <4.0.0"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Ignoring version.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Ignoring version.txt
new file mode 100644
index 0000000..1636fda
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Ignoring version.txt
@@ -0,0 +1,117 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":"^1.0.0"},"environment":{"sdk":"^3.0.2"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ sha256: "4a8df8c695623e81d90f663801ead4a5269b406599b43b90ad558561a6f09c59"
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.0.0"
+sdks:
+ dart: ">=3.0.2 <4.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ echo '{"target":null,"disallowed":[{"name":"foo","url":"http://localhost:$PORT","versions":[{"range":"1.0.2"}]},{"name":"transitive","url":"http://localhost:$PORT","versions":[{"range":"1.0.0"}]}]}' | dependency_services report
+{
+ "dependencies": [
+ {
+ "name": "foo",
+ "version": "1.0.0",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "4a8df8c695623e81d90f663801ead4a5269b406599b43b90ad558561a6f09c59"
+ }
+ },
+ "latest": "1.0.3",
+ "constraint": "^1.0.0",
+ "compatible": [
+ {
+ "name": "foo",
+ "version": "1.0.1",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "7e5c9a856d9aaa086b9724210ae9017edd53d704d630fe0cae743abc5074a997"
+ }
+ },
+ "constraintBumped": "^1.0.0",
+ "constraintWidened": "^1.0.0",
+ "constraintBumpedIfNeeded": "^1.0.0",
+ "previousVersion": "1.0.0",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "4a8df8c695623e81d90f663801ead4a5269b406599b43b90ad558561a6f09c59"
+ }
+ }
+ }
+ ],
+ "singleBreaking": [],
+ "multiBreaking": [
+ {
+ "name": "foo",
+ "version": "1.0.1",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "7e5c9a856d9aaa086b9724210ae9017edd53d704d630fe0cae743abc5074a997"
+ }
+ },
+ "constraintBumped": "^1.0.1",
+ "constraintWidened": "^1.0.0",
+ "constraintBumpedIfNeeded": "^1.0.0",
+ "previousVersion": "1.0.0",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "4a8df8c695623e81d90f663801ead4a5269b406599b43b90ad558561a6f09c59"
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"app","dependencies":{"foo":"^1.0.0"},"environment":{"sdk":"^3.0.2"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ sha256: "4a8df8c695623e81d90f663801ead4a5269b406599b43b90ad558561a6f09c59"
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.0.0"
+sdks:
+ dart: ">=3.0.2 <4.0.0"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt b/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt
index 46d1a77..0d46baa 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/No pubspec.lock.txt
@@ -7,7 +7,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section list
-$ dependency_services list
+$ echo '' | dependency_services list
{
"dependencies": [
{
@@ -59,7 +59,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section report
-$ dependency_services report
+$ echo '' | dependency_services report
{
"dependencies": [
{
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Preserves no content-hashes.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Preserves no content-hashes.txt
index a0ffae6..61c69a2 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Preserves no content-hashes.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Preserves no content-hashes.txt
@@ -32,7 +32,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section list
-$ dependency_services list
+$ echo '' | dependency_services list
{
"dependencies": [
{
@@ -80,7 +80,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section report
-$ dependency_services report
+$ echo '' | dependency_services report
{
"dependencies": [
{
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Preserves pub.dartlang.org as hosted url.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Preserves pub.dartlang.org as hosted url.txt
index 5a95570..2483450 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Preserves pub.dartlang.org as hosted url.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Preserves pub.dartlang.org as hosted url.txt
@@ -27,7 +27,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section list
-$ dependency_services list
+$ echo '' | dependency_services list
{
"dependencies": [
{
@@ -64,7 +64,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section report
-$ dependency_services report
+$ echo '' | dependency_services report
{
"dependencies": [
{
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt
index 6ac254b..7560bdd 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Relative paths are allowed.txt
@@ -26,7 +26,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section list
-$ dependency_services list
+$ echo '' | dependency_services list
{
"dependencies": [
{
@@ -62,7 +62,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section report
-$ dependency_services report
+$ echo '' | dependency_services report
{
"dependencies": [
{
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
index 097456a..3f3a887 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Removing transitive.txt
@@ -27,7 +27,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section list
-$ dependency_services list
+$ echo '' | dependency_services list
{
"dependencies": [
{
@@ -64,7 +64,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section report
-$ dependency_services report
+$ echo '' | dependency_services report
{
"dependencies": [
{
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Smallest possible upgrade can upgrade beyond breaking.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Smallest possible upgrade can upgrade beyond breaking.txt
new file mode 100644
index 0000000..fdf4b1a
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Smallest possible upgrade can upgrade beyond breaking.txt
@@ -0,0 +1,145 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"myapp","dependencies":{"foo":"^1.0.0"},"environment":{"sdk":"^3.0.2"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ sha256: ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.1.1"
+sdks:
+ dart: ">=3.0.2 <4.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ echo '{"target":"foo","disallowed":[{"name":"foo","url":"http://localhost:$PORT","versions":[{"range":"1.1.1"},{"range":"2.0.0"}]}]}' | dependency_services report
+{
+ "dependencies": [
+ {
+ "name": "foo",
+ "version": "1.1.1",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457"
+ }
+ },
+ "latest": "2.0.2",
+ "constraint": "^1.0.0",
+ "compatible": [],
+ "singleBreaking": [
+ {
+ "name": "foo",
+ "version": "2.0.2",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "3db0dc36e17a64a3f2ba5c2aa5871c056621886c9cc8464a0123642d3d68d272"
+ }
+ },
+ "constraintBumped": "^2.0.2",
+ "constraintWidened": ">=1.0.0 <3.0.0",
+ "constraintBumpedIfNeeded": "^2.0.2",
+ "previousVersion": "1.1.1",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457"
+ }
+ }
+ }
+ ],
+ "multiBreaking": [
+ {
+ "name": "foo",
+ "version": "2.0.2",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "3db0dc36e17a64a3f2ba5c2aa5871c056621886c9cc8464a0123642d3d68d272"
+ }
+ },
+ "constraintBumped": "^2.0.2",
+ "constraintWidened": ">=1.0.0 <3.0.0",
+ "constraintBumpedIfNeeded": "^2.0.2",
+ "previousVersion": "1.1.1",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457"
+ }
+ }
+ }
+ ],
+ "smallestUpdate": [
+ {
+ "name": "foo",
+ "version": "2.0.1",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "bcaaad4d1c92a89ff8dfb1ce146e11635fbce6219dc8992d5a5d8eaca0658fdd"
+ }
+ },
+ "constraintBumped": "^2.0.1",
+ "constraintWidened": ">=1.0.0 <3.0.0",
+ "constraintBumpedIfNeeded": "^2.0.1",
+ "previousVersion": "1.1.1",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457"
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"myapp","dependencies":{"foo":"^1.0.0"},"environment":{"sdk":"^3.0.2"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ sha256: ee9afb23699244da9e40f5001a3600e529c3696f2b8696906fd43ea8e54e0457
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.1.1"
+sdks:
+ dart: ">=3.0.2 <4.0.0"
diff --git a/test/testdata/goldens/dependency_services/dependency_services_test/Smallest possible upgrade can upgrade other packages if needed.txt b/test/testdata/goldens/dependency_services/dependency_services_test/Smallest possible upgrade can upgrade other packages if needed.txt
new file mode 100644
index 0000000..745d857
--- /dev/null
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/Smallest possible upgrade can upgrade other packages if needed.txt
@@ -0,0 +1,186 @@
+# GENERATED BY: test/dependency_services/dependency_services_test.dart
+
+$ cat pubspec.yaml
+{"name":"myapp","dependencies":{"foo":"^1.0.0","bar":"^1.0.0"},"environment":{"sdk":"^3.0.2"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ bar:
+ dependency: "direct main"
+ description:
+ name: bar
+ sha256: "8cd4b5a00de63aa592f4240249affd87abf49de4281233870f22b30919f87d42"
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.0.0"
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ sha256: "6f20056a28b780546e364af618f5c357863660259f8df4431c7fd12ccb8b4372"
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.1.1"
+sdks:
+ dart: ">=3.0.2 <4.0.0"
+-------------------------------- END OF OUTPUT ---------------------------------
+
+## Section report
+$ echo '{"target":"foo","disallowed":[{"name":"foo","url":"http://localhost:$PORT","versions":[{"range":"1.1.1"},{"range":"2.0.0"}]},{"name":"bar","url":"http://localhost:$PORT","versions":[{"range":"2.0.0"}]}]}' | dependency_services report
+{
+ "dependencies": [
+ {
+ "name": "foo",
+ "version": "1.1.1",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "6f20056a28b780546e364af618f5c357863660259f8df4431c7fd12ccb8b4372"
+ }
+ },
+ "latest": "2.0.2",
+ "constraint": "^1.0.0",
+ "compatible": [],
+ "singleBreaking": [],
+ "multiBreaking": [
+ {
+ "name": "foo",
+ "version": "2.0.2",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "2fe8aa833d5a1c720c47cde975d63ff8cd1436da5a13b9ff413eb2b489bdf850"
+ }
+ },
+ "constraintBumped": "^2.0.2",
+ "constraintWidened": ">=1.0.0 <3.0.0",
+ "constraintBumpedIfNeeded": "^2.0.2",
+ "previousVersion": "1.1.1",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "6f20056a28b780546e364af618f5c357863660259f8df4431c7fd12ccb8b4372"
+ }
+ }
+ },
+ {
+ "name": "bar",
+ "version": "2.2.0",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "bar",
+ "url": "http://localhost:$PORT",
+ "sha256": "3c51e3f45f9b993f18c64ab5c55e5f8149c98bac06b0c3e9100fef98385afe4f"
+ }
+ },
+ "constraintBumped": "^2.2.0",
+ "constraintWidened": ">=1.0.0 <3.0.0",
+ "constraintBumpedIfNeeded": "^2.2.0",
+ "previousVersion": "1.0.0",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "bar",
+ "url": "http://localhost:$PORT",
+ "sha256": "8cd4b5a00de63aa592f4240249affd87abf49de4281233870f22b30919f87d42"
+ }
+ }
+ }
+ ],
+ "smallestUpdate": [
+ {
+ "name": "foo",
+ "version": "2.0.1",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "3fc65bb8d960eb60a8ff969ba138df09de120b4e05dbde7a45b7f77aba179237"
+ }
+ },
+ "constraintBumped": "^2.0.1",
+ "constraintWidened": ">=1.0.0 <3.0.0",
+ "constraintBumpedIfNeeded": "^2.0.1",
+ "previousVersion": "1.1.1",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "foo",
+ "url": "http://localhost:$PORT",
+ "sha256": "6f20056a28b780546e364af618f5c357863660259f8df4431c7fd12ccb8b4372"
+ }
+ }
+ },
+ {
+ "name": "bar",
+ "version": "2.2.0",
+ "kind": "direct",
+ "source": {
+ "type": "hosted",
+ "description": {
+ "name": "bar",
+ "url": "http://localhost:$PORT",
+ "sha256": "3c51e3f45f9b993f18c64ab5c55e5f8149c98bac06b0c3e9100fef98385afe4f"
+ }
+ },
+ "constraintBumped": "^2.2.0",
+ "constraintWidened": ">=1.0.0 <3.0.0",
+ "constraintBumpedIfNeeded": "^2.2.0",
+ "previousVersion": "1.0.0",
+ "previousConstraint": "^1.0.0",
+ "previousSource": {
+ "type": "hosted",
+ "description": {
+ "name": "bar",
+ "url": "http://localhost:$PORT",
+ "sha256": "8cd4b5a00de63aa592f4240249affd87abf49de4281233870f22b30919f87d42"
+ }
+ }
+ }
+ ]
+ }
+ ]
+}
+
+-------------------------------- END OF OUTPUT ---------------------------------
+
+$ cat pubspec.yaml
+{"name":"myapp","dependencies":{"foo":"^1.0.0","bar":"^1.0.0"},"environment":{"sdk":"^3.0.2"}}
+$ cat pubspec.lock
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ bar:
+ dependency: "direct main"
+ description:
+ name: bar
+ sha256: "8cd4b5a00de63aa592f4240249affd87abf49de4281233870f22b30919f87d42"
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.0.0"
+ foo:
+ dependency: "direct main"
+ description:
+ name: foo
+ sha256: "6f20056a28b780546e364af618f5c357863660259f8df4431c7fd12ccb8b4372"
+ url: "http://localhost:$PORT"
+ source: hosted
+ version: "1.1.1"
+sdks:
+ dart: ">=3.0.2 <4.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
index a65da00..1e63cb2 100644
--- a/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt
+++ b/test/testdata/goldens/dependency_services/dependency_services_test/multibreaking.txt
@@ -35,7 +35,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section list
-$ dependency_services list
+$ echo '' | dependency_services list
{
"dependencies": [
{
@@ -86,7 +86,7 @@
-------------------------------- END OF OUTPUT ---------------------------------
## Section report
-$ dependency_services report
+$ echo '' | dependency_services report
{
"dependencies": [
{