Add new command `pub outdated` (#2315)
This command intends to make it easier to get an overview of your dependencies.
Support for marking null-safe dependencies is not included in this commit
diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart
new file mode 100644
index 0000000..9b65a85
--- /dev/null
+++ b/lib/src/command/outdated.dart
@@ -0,0 +1,444 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+import 'dart:math';
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:meta/meta.dart';
+
+import '../command.dart';
+import '../entrypoint.dart';
+import '../log.dart' as log;
+import '../package.dart';
+import '../package_name.dart';
+import '../pubspec.dart';
+import '../solver.dart';
+import '../source.dart';
+import '../source/hosted.dart';
+
+class OutdatedCommand extends PubCommand {
+ @override
+ String get name => 'outdated';
+ @override
+ String get description =>
+ 'Analyze your dependencies to find which ones can be upgraded.';
+ @override
+ String get invocation => 'pub outdated [options]';
+ @override
+ String get docUrl => 'https://dart.dev/tools/pub/cmd/pub-outdated';
+
+ OutdatedCommand() {
+ argParser.addOption('format',
+ help: 'Defines how the output should be formatted. Defaults to color '
+ 'when connected to a terminal, and no-color otherwise.',
+ valueHelp: 'FORMAT',
+ allowed: ['color', 'no-color', 'json']);
+
+ argParser.addFlag('up-to-date',
+ defaultsTo: false,
+ help: 'Include dependencies that are already at the latest version');
+
+ argParser.addFlag('pre-releases',
+ defaultsTo: false,
+ help: 'Include pre-releases when reporting latest version');
+
+ argParser.addFlag(
+ 'dev-dependencies',
+ defaultsTo: true,
+ help: 'When true take dev-dependencies into account when resolving.',
+ );
+
+ argParser.addOption('mark',
+ help: 'Highlight packages with some property in the report.',
+ valueHelp: 'OPTION',
+ allowed: ['outdated', 'none'],
+ defaultsTo: 'outdated');
+ }
+
+ @override
+ Future run() async {
+ entrypoint.assertUpToDate();
+
+ final includeDevDependencies = argResults['dev-dependencies'];
+
+ final upgradePubspec = includeDevDependencies
+ ? entrypoint.root.pubspec
+ : _stripDevDependencies(entrypoint.root.pubspec);
+
+ var resolvablePubspec = _stripVersionConstraints(upgradePubspec);
+
+ SolveResult upgradableSolveResult;
+ SolveResult resolvableSolveResult;
+
+ await log.warningsOnlyUnlessTerminal(
+ () => log.spinner(
+ 'Resolving',
+ () async {
+ upgradableSolveResult = await resolveVersions(
+ SolveType.UPGRADE,
+ cache,
+ Package.inMemory(upgradePubspec),
+ );
+
+ resolvableSolveResult = await resolveVersions(
+ SolveType.UPGRADE,
+ cache,
+ Package.inMemory(resolvablePubspec),
+ );
+ },
+ ),
+ );
+
+ Future<_PackageDetails> analyzeDependency(PackageRef packageRef) async {
+ final name = packageRef.name;
+ final current = (entrypoint.lockFile?.packages ?? {})[name]?.version;
+ final source = packageRef.source;
+ final available = (await cache.source(source).doGetVersions(packageRef))
+ .map((id) => id.version)
+ .toList()
+ ..sort(argResults['pre-releases'] ? null : Version.prioritize);
+ final upgradable = upgradableSolveResult.packages
+ .firstWhere((id) => id.name == name, orElse: () => null)
+ ?.version;
+ final resolvable = resolvableSolveResult.packages
+ .firstWhere((id) => id.name == name, orElse: () => null)
+ ?.version;
+ final latest = available.last;
+ final description = packageRef.description;
+ return _PackageDetails(
+ name,
+ await _describeVersion(name, source, description, current),
+ await _describeVersion(name, source, description, upgradable),
+ await _describeVersion(name, source, description, resolvable),
+ await _describeVersion(name, source, description, latest),
+ _kind(name, entrypoint));
+ }
+
+ final rows = <_PackageDetails>[];
+
+ final immediateDependencies = entrypoint.root.immediateDependencies.values;
+
+ for (final packageRange in immediateDependencies) {
+ rows.add(await analyzeDependency(packageRange.toRef()));
+ }
+
+ // Now add transitive dependencies:
+ final visited = <String>{
+ entrypoint.root.name,
+ ...immediateDependencies.map((d) => d.name)
+ };
+ for (final id in [
+ if (includeDevDependencies) ...entrypoint.lockFile.packages.values,
+ ...upgradableSolveResult.packages,
+ ...resolvableSolveResult.packages
+ ]) {
+ final name = id.name;
+ if (!visited.add(name)) continue;
+ rows.add(await analyzeDependency(id.toRef()));
+ }
+
+ if (!argResults['up-to-date']) {
+ rows.retainWhere(
+ (r) => (r.current ?? r.upgradable)?.version != r.latest?.version);
+ }
+ if (!includeDevDependencies) {
+ rows.removeWhere((r) => r.kind == _DependencyKind.dev);
+ }
+
+ rows.sort();
+
+ if (argResults['format'] == 'json') {
+ await _outputJson(rows);
+ } else {
+ final useColors = argResults['format'] == 'color' ||
+ (!argResults.wasParsed('format') && stdin.hasTerminal);
+ final marker = {
+ 'outdated': oudatedMarker,
+ 'none': noneMarker,
+ }[argResults['mark']];
+ await _outputHuman(
+ rows,
+ marker,
+ useColors: useColors,
+ includeDevDependencies: includeDevDependencies,
+ );
+ }
+ }
+
+ /// Retrieves the pubspec of package [name] in [version] from [source].
+ Future<Pubspec> _describeVersion(
+ String name, Source source, dynamic description, Version version) async {
+ return version == null
+ ? null
+ : await cache
+ .source(source)
+ .describe(PackageId(name, source, version, description));
+ }
+}
+
+Pubspec _stripDevDependencies(Pubspec original) {
+ return Pubspec(
+ original.name,
+ sdkConstraints: original.sdkConstraints,
+ dependencies: original.dependencies.values,
+ );
+}
+
+/// Returns new pubspec with the same dependencies as [original] but with no
+/// version constraints on hosted packages.
+Pubspec _stripVersionConstraints(Pubspec original) {
+ List<PackageRange> _unconstrained(Map<String, PackageRange> constrained) {
+ final result = <PackageRange>[];
+ for (final name in constrained.keys) {
+ final packageRange = constrained[name];
+ var unconstrainedRange = packageRange;
+ if (packageRange.source is HostedSource) {
+ unconstrainedRange = PackageRange(
+ packageRange.name,
+ packageRange.source,
+ VersionConstraint.any,
+ packageRange.description,
+ features: packageRange.features);
+ }
+ result.add(unconstrainedRange);
+ }
+ return result;
+ }
+
+ return Pubspec(
+ original.name,
+ sdkConstraints: original.sdkConstraints,
+ dependencies: _unconstrained(original.dependencies),
+ devDependencies: _unconstrained(original.devDependencies),
+ // TODO(sigurdm): consider dependency overrides.
+ );
+}
+
+Future<void> _outputJson(List<_PackageDetails> rows) async {
+ log.message(JsonEncoder.withIndent(' ')
+ .convert({'packages': rows.map((row) => row.toJson()).toList()}));
+}
+
+Future<void> _outputHuman(List<_PackageDetails> rows,
+ Future<List<_FormattedString>> Function(_PackageDetails) marker,
+ {@required bool useColors, @required bool includeDevDependencies}) async {
+ if (rows.isEmpty) {
+ log.message('Found no outdated packages');
+ return;
+ }
+ final directRows = rows.where((row) => row.kind == _DependencyKind.direct);
+ final devRows = rows.where((row) => row.kind == _DependencyKind.dev);
+ final transitiveRows =
+ rows.where((row) => row.kind == _DependencyKind.transitive);
+
+ final formattedRows = <List<_FormattedString>>[
+ ['Package', 'Current', 'Upgradable', 'Resolvable', 'Latest']
+ .map((s) => _format(s, log.bold))
+ .toList(),
+ [
+ directRows.isEmpty
+ ? _raw('dependencies: all up-to-date')
+ : _format('dependencies', log.bold),
+ ],
+ ...await Future.wait(directRows.map(marker)),
+ if (includeDevDependencies)
+ [
+ devRows.isEmpty
+ ? _raw('\ndev_dependencies: all up-to-date')
+ : _format('\ndev_dependencies', log.bold),
+ ],
+ ...await Future.wait(devRows.map(marker)),
+ [
+ transitiveRows.isEmpty
+ ? _raw('\ntransitive dependencies: all up-to-date')
+ : _format('\ntransitive dependencies', log.bold)
+ ],
+ ...await Future.wait(transitiveRows.map(marker)),
+ ];
+
+ final columnWidths = <int, int>{};
+ for (var i = 0; i < formattedRows.length; i++) {
+ if (formattedRows[i].length > 1) {
+ for (var j = 0; j < formattedRows[i].length; j++) {
+ final currentMaxWidth = columnWidths[j] ?? 0;
+ columnWidths[j] = max(
+ formattedRows[i][j].computeLength(useColors: useColors),
+ currentMaxWidth);
+ }
+ }
+ }
+
+ for (final row in formattedRows) {
+ final b = StringBuffer();
+ for (var j = 0; j < row.length; j++) {
+ b.write(row[j].formatted(useColors: useColors));
+ b.write(' ' *
+ ((columnWidths[j] + 2) - row[j].computeLength(useColors: useColors)));
+ }
+ log.message(b.toString());
+ }
+
+ var upgradable = rows
+ .where((row) =>
+ row.current != null &&
+ row.upgradable != null &&
+ row.current != row.upgradable)
+ .length;
+
+ var notAtResolvable = rows
+ .where(
+ (row) => row.resolvable != null && row.upgradable != row.resolvable)
+ .length;
+
+ if (upgradable != 0) {
+ if (upgradable == 1) {
+ log.message('1 upgradable dependency is locked (in pubspec.lock) to '
+ 'an older version.\n'
+ 'To update it, use `pub upgrade`.');
+ } else {
+ log.message(
+ '\n$upgradable upgradable dependencies are locked (in pubspec.lock) '
+ 'to older versions.\n'
+ 'To update these dependencies, use `pub upgrade`.');
+ }
+ }
+
+ if (notAtResolvable != 0) {
+ if (notAtResolvable == 1) {
+ log.message('\n1 dependency is constrained to a '
+ 'version that is older than a resolvable version.\n'
+ 'To update it, edit pubspec.yaml.');
+ } else {
+ log.message('\n$notAtResolvable dependencies are constrained to '
+ 'versions that are older than a resolvable version.\n'
+ 'To update these dependencies, edit pubspec.yaml.');
+ }
+ }
+}
+
+Future<List<_FormattedString>> oudatedMarker(
+ _PackageDetails packageDetails) async {
+ final cols = [_FormattedString(packageDetails.name)];
+ Version previous;
+ for (final pubspec in [
+ packageDetails.current,
+ packageDetails.upgradable,
+ packageDetails.resolvable,
+ packageDetails.latest
+ ]) {
+ final version = pubspec?.version;
+ if (version == null) {
+ cols.add(_raw('-'));
+ } else {
+ final isLatest = version == packageDetails.latest.version;
+ String Function(String) color;
+ if (isLatest) {
+ color = version == previous ? color = log.gray : null;
+ } else {
+ color = log.red;
+ }
+ final prefix = isLatest ? '' : '*';
+ cols.add(_format(version?.toString() ?? '-', color, prefix: prefix));
+ }
+ previous = version;
+ }
+ return cols;
+}
+
+Future<List<_FormattedString>> noneMarker(
+ _PackageDetails packageDetails) async {
+ return [
+ _FormattedString(packageDetails.name),
+ ...[
+ packageDetails.current,
+ packageDetails.upgradable,
+ packageDetails.resolvable,
+ packageDetails.latest,
+ ].map((p) => _raw(p?.version?.toString() ?? '-'))
+ ];
+}
+
+class _PackageDetails implements Comparable<_PackageDetails> {
+ final String name;
+ final Pubspec current;
+ final Pubspec upgradable;
+ final Pubspec resolvable;
+ final Pubspec latest;
+ final _DependencyKind kind;
+
+ _PackageDetails(this.name, this.current, this.upgradable, this.resolvable,
+ this.latest, this.kind);
+
+ @override
+ int compareTo(_PackageDetails other) {
+ if (kind != other.kind) {
+ return kind.index.compareTo(other.kind.index);
+ }
+ return name.compareTo(other.name);
+ }
+
+ Map<String, Object> toJson() {
+ return {
+ 'package': name,
+ 'current': current?.version?.toString(),
+ 'upgradable': upgradable?.version?.toString(),
+ 'resolvable': resolvable?.version?.toString(),
+ 'latest': latest?.version?.toString(),
+ };
+ }
+}
+
+_DependencyKind _kind(String name, Entrypoint entrypoint) {
+ if (entrypoint.root.dependencies.containsKey(name)) {
+ return _DependencyKind.direct;
+ } else if (entrypoint.root.devDependencies.containsKey(name)) {
+ return _DependencyKind.dev;
+ } else {
+ return _DependencyKind.transitive;
+ }
+}
+
+enum _DependencyKind {
+ /// Direct non-dev dependencies.
+ direct,
+
+ /// Direct dev dependencies.
+ dev,
+
+ /// Transitive dependencies.
+ transitive
+}
+
+_FormattedString _format(String value, Function(String) format, {prefix = ''}) {
+ return _FormattedString(value, format: format, prefix: prefix);
+}
+
+_FormattedString _raw(String value) => _FormattedString(value);
+
+class _FormattedString {
+ final String value;
+
+ /// Should apply the ansi codes to present this string.
+ final String Function(String) _format;
+
+ /// A prefix for marking this string if colors are not used.
+ final String _prefix;
+
+ _FormattedString(this.value, {String Function(String) format, prefix = ''})
+ : _format = format ?? _noFormat,
+ _prefix = prefix;
+
+ String formatted({@required bool useColors}) {
+ return useColors ? _format(value) : _prefix + value;
+ }
+
+ int computeLength({@required bool useColors}) {
+ return useColors ? value.length : _prefix.length + value.length;
+ }
+
+ static String _noFormat(String x) => x;
+}
diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart
index 6335cee..ebe270a 100644
--- a/lib/src/command_runner.dart
+++ b/lib/src/command_runner.dart
@@ -19,6 +19,7 @@
import 'command/lish.dart';
import 'command/list_package_dirs.dart';
import 'command/logout.dart';
+import 'command/outdated.dart';
import 'command/run.dart';
import 'command/serve.dart';
import 'command/upgrade.dart';
@@ -102,6 +103,7 @@
addCommand(GetCommand());
addCommand(ListPackageDirsCommand());
addCommand(LishCommand());
+ addCommand(OutdatedCommand());
addCommand(RunCommand());
addCommand(ServeCommand());
addCommand(UpgradeCommand());
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index ac2f7f9..b4d66f5 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -213,8 +213,16 @@
{List<String> useLatest,
bool dryRun = false,
bool precompile = false}) async {
- var result = await resolveVersions(type, cache, root,
- lockFile: lockFile, useLatest: useLatest);
+ var result = await log.progress(
+ 'Resolving dependencies',
+ () => resolveVersions(
+ type,
+ cache,
+ root,
+ lockFile: lockFile,
+ useLatest: useLatest,
+ ),
+ );
// Log once about all overridden packages.
if (warnAboutPreReleaseSdkOverrides && result.pubspecs != null) {
diff --git a/lib/src/executable.dart b/lib/src/executable.dart
index 2923822..1d8448e 100644
--- a/lib/src/executable.dart
+++ b/lib/src/executable.dart
@@ -61,48 +61,46 @@
// normal pub output that may be shown when recompiling snapshots if we are
// not attached to a terminal. This is to not pollute stdout when the output
// of `pub run` is piped somewhere.
- if (log.verbosity == log.Verbosity.NORMAL && !stdout.hasTerminal) {
- log.verbosity = log.Verbosity.WARNING;
- }
+ return await log.warningsOnlyUnlessTerminal(() async {
+ // Uncached packages are run from source.
+ if (snapshotPath != null) {
+ // Since we don't access the package graph, this doesn't happen
+ // automatically.
+ entrypoint.assertUpToDate();
- // Uncached packages are run from source.
- if (snapshotPath != null) {
- // Since we don't access the package graph, this doesn't happen
- // automatically.
- entrypoint.assertUpToDate();
-
- var result = await _runOrCompileSnapshot(snapshotPath, args,
- packagesFile: packagesFile, checked: checked, recompile: recompile);
- if (result != null) return result;
- }
-
- // If the command has a path separator, then it's a path relative to the
- // root of the package. Otherwise, it's implicitly understood to be in
- // "bin".
- if (p.split(executable).length == 1) executable = p.join('bin', executable);
-
- var executablePath = await _executablePath(entrypoint, package, executable);
-
- if (executablePath == null) {
- var message = 'Could not find ${log.bold(executable)}';
- if (entrypoint.isGlobal || package != entrypoint.root.name) {
- message += ' in package ${log.bold(package)}';
+ var result = await _runOrCompileSnapshot(snapshotPath, args,
+ packagesFile: packagesFile, checked: checked, recompile: recompile);
+ if (result != null) return result;
}
- log.error('$message.');
- return exit_codes.NO_INPUT;
- }
- // We use an absolute path here not because the VM insists but because it's
- // helpful for the subprocess to be able to spawn Dart with
- // Platform.executableArguments and have that work regardless of the working
- // directory.
- var packageConfig = p.toUri(p.absolute(packagesFile));
+ // If the command has a path separator, then it's a path relative to the
+ // root of the package. Otherwise, it's implicitly understood to be in
+ // "bin".
+ if (p.split(executable).length == 1) executable = p.join('bin', executable);
- await isolate.runUri(p.toUri(executablePath), args.toList(), null,
- checked: checked,
- automaticPackageResolution: packageConfig == null,
- packageConfig: packageConfig);
- return exitCode;
+ var executablePath = await _executablePath(entrypoint, package, executable);
+
+ if (executablePath == null) {
+ var message = 'Could not find ${log.bold(executable)}';
+ if (entrypoint.isGlobal || package != entrypoint.root.name) {
+ message += ' in package ${log.bold(package)}';
+ }
+ log.error('$message.');
+ return exit_codes.NO_INPUT;
+ }
+
+ // We use an absolute path here not because the VM insists but because it's
+ // helpful for the subprocess to be able to spawn Dart with
+ // Platform.executableArguments and have that work regardless of the working
+ // directory.
+ var packageConfig = p.toUri(p.absolute(packagesFile));
+
+ await isolate.runUri(p.toUri(executablePath), args.toList(), null,
+ checked: checked,
+ automaticPackageResolution: packageConfig == null,
+ packageConfig: packageConfig);
+ return exitCode;
+ });
}
/// Returns the full path the VM should use to load the executable at [path].
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index d832d89..8666ee4 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -180,7 +180,8 @@
// being available, report that as a [dataError].
SolveResult result;
try {
- result = await resolveVersions(SolveType.GET, cache, root);
+ result = await log.progress('Resolving dependencies',
+ () => resolveVersions(SolveType.GET, cache, root));
} on SolveFailure catch (error) {
for (var incompatibility
in error.incompatibility.externalIncompatibilities) {
diff --git a/lib/src/log.dart b/lib/src/log.dart
index 446a9a9..893759c 100644
--- a/lib/src/log.dart
+++ b/lib/src/log.dart
@@ -364,6 +364,21 @@
stderr.writeln('---- End log transcript ----');
}
+/// Filter out normal pub output when not attached to a terminal
+///
+/// Unless the user has overriden the verbosity,
+///
+/// This is useful to not pollute stdout when the output is piped somewhere.
+Future<T> warningsOnlyUnlessTerminal<T>(FutureOr<T> Function() callback) async {
+ final oldVerbosity = verbosity;
+ if (verbosity == Verbosity.NORMAL && !stdout.hasTerminal) {
+ verbosity = Verbosity.WARNING;
+ }
+ final result = await callback();
+ verbosity = oldVerbosity;
+ return result;
+}
+
/// Prints [message] then displays an updated elapsed time until the future
/// returned by [callback] completes.
///
@@ -379,6 +394,17 @@
return callback().whenComplete(progress.stop);
}
+/// Like [progress] but erases the message once done.
+Future<T> spinner<T>(String message, Future<T> Function() callback) {
+ _stopProgress();
+
+ var progress = Progress(message);
+ _animatedProgress = progress;
+ return callback().whenComplete(() {
+ progress.stopAndClear();
+ });
+}
+
/// Stops animating the running progress indicator, if currently running.
void _stopProgress() {
if (_animatedProgress != null) _animatedProgress.stopAnimating();
diff --git a/lib/src/progress.dart b/lib/src/progress.dart
index 50f774f..d05c361 100644
--- a/lib/src/progress.dart
+++ b/lib/src/progress.dart
@@ -75,6 +75,26 @@
stdout.writeln();
}
+ /// Erases the progress message and stops the progress indicator.
+ Future<void> stopAndClear() async {
+ _stopwatch.stop();
+
+ if (_timer != null) {
+ stdout.write('\b' * (_message.length + '... '.length + _timeLength));
+ }
+
+ // Always log the final time as [log.fine] because for the most part normal
+ // users don't care about the precise time information beyond what's shown
+ // in the animation.
+ log.fine('$_message finished $_time.');
+
+ // If we were animating, print one final update to show the user the final
+ // time.
+ if (_timer == null) return;
+ _timer.cancel();
+ _timer = null;
+ }
+
/// Stop animating the progress indicator.
///
/// This will continue running the stopwatch so that the full time can be
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index 4786470..080358e 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -474,7 +474,8 @@
Iterable<PackageRange> devDependencies,
Iterable<PackageRange> dependencyOverrides,
Map fields,
- SourceRegistry sources})
+ SourceRegistry sources,
+ Map<String, VersionConstraint> sdkConstraints})
: _version = version,
_dependencies = dependencies == null
? null
@@ -485,7 +486,8 @@
_dependencyOverrides = dependencyOverrides == null
? null
: Map.fromIterable(dependencyOverrides, key: (range) => range.name),
- _sdkConstraints = UnmodifiableMapView({'dart': VersionConstraint.any}),
+ _sdkConstraints = sdkConstraints ??
+ UnmodifiableMapView({'dart': VersionConstraint.any}),
_includeDefaultSdkConstraint = false,
fields = fields == null ? YamlMap() : YamlMap.wrap(fields),
_sources = sources;
diff --git a/lib/src/solver.dart b/lib/src/solver.dart
index c3bfb84..d7230c1 100644
--- a/lib/src/solver.dart
+++ b/lib/src/solver.dart
@@ -5,7 +5,6 @@
import 'dart:async';
import 'lock_file.dart';
-import 'log.dart' as log;
import 'package.dart';
import 'solver/result.dart';
import 'solver/type.dart';
@@ -29,9 +28,11 @@
Future<SolveResult> resolveVersions(
SolveType type, SystemCache cache, Package root,
{LockFile lockFile, Iterable<String> useLatest}) {
- return log.progress('Resolving dependencies', () {
- return VersionSolver(type, cache, root, lockFile ?? LockFile.empty(),
- useLatest ?? const [])
- .solve();
- });
+ return VersionSolver(
+ type,
+ cache,
+ root,
+ lockFile ?? LockFile.empty(),
+ useLatest ?? const [],
+ ).solve();
}
diff --git a/test/golden_file.dart b/test/golden_file.dart
new file mode 100644
index 0000000..255706a
--- /dev/null
+++ b/test/golden_file.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+import 'package:test/test.dart';
+
+/// Will test [actual] against the contests of the file at [goldenFilePath].
+///
+/// If the file doesn't exist, the file is instead created containing [actual].
+void expectMatchesGoldenFile(String actual, String goldenFilePath) {
+ var goldenFile = File(goldenFilePath);
+ if (goldenFile.existsSync()) {
+ expect(
+ actual, equals(goldenFile.readAsStringSync().replaceAll('\r\n', '\n')),
+ reason: 'goldenFilePath: "$goldenFilePath"');
+ } else {
+ // This enables writing the updated file when run in otherwise hermetic
+ // settings.
+ //
+ // This is to make updating the golden files easier in a bazel environment
+ // See https://docs.bazel.build/versions/2.0.0/user-manual.html#run .
+ final workspaceDirectory =
+ Platform.environment['BUILD_WORKSPACE_DIRECTORY'];
+ if (workspaceDirectory != null) {
+ goldenFile = File(path.join(workspaceDirectory, goldenFilePath));
+ }
+ goldenFile
+ ..createSync(recursive: true)
+ ..writeAsStringSync(actual);
+ }
+}
diff --git a/test/outdated/goldens/newer_versions.txt b/test/outdated/goldens/newer_versions.txt
new file mode 100644
index 0000000..7322203
--- /dev/null
+++ b/test/outdated/goldens/newer_versions.txt
@@ -0,0 +1,139 @@
+$ pub outdated --format=json
+Resolving...
+{
+ "packages": [
+ {
+ "package": "foo",
+ "current": "1.2.3",
+ "upgradable": "1.3.0",
+ "resolvable": "2.0.0",
+ "latest": "3.0.0"
+ },
+ {
+ "package": "builder",
+ "current": "1.2.3",
+ "upgradable": "1.3.0",
+ "resolvable": "2.0.0",
+ "latest": "2.0.0"
+ },
+ {
+ "package": "transitive",
+ "current": "1.2.3",
+ "upgradable": "1.3.0",
+ "resolvable": "1.3.0",
+ "latest": "2.0.0"
+ },
+ {
+ "package": "transitive2",
+ "current": null,
+ "upgradable": null,
+ "resolvable": "1.0.0",
+ "latest": "1.0.0"
+ },
+ {
+ "package": "transitive3",
+ "current": null,
+ "upgradable": null,
+ "resolvable": "1.0.0",
+ "latest": "1.0.0"
+ }
+ ]
+}
+
+$ pub outdated --format=no-color
+Resolving...
+Package Current Upgradable Resolvable Latest
+dependencies
+foo *1.2.3 *1.3.0 *2.0.0 3.0.0
+
+dev_dependencies
+builder *1.2.3 *1.3.0 2.0.0 2.0.0
+
+transitive dependencies
+transitive *1.2.3 *1.3.0 *1.3.0 2.0.0
+transitive2 - - 1.0.0 1.0.0
+transitive3 - - 1.0.0 1.0.0
+
+3 upgradable dependencies are locked (in pubspec.lock) to older versions.
+To update these dependencies, use `pub upgrade`.
+
+4 dependencies are constrained to versions that are older than a resolvable version.
+To update these dependencies, edit pubspec.yaml.
+
+$ pub outdated --format=no-color --mark=none
+Resolving...
+Package Current Upgradable Resolvable Latest
+dependencies
+foo 1.2.3 1.3.0 2.0.0 3.0.0
+
+dev_dependencies
+builder 1.2.3 1.3.0 2.0.0 2.0.0
+
+transitive dependencies
+transitive 1.2.3 1.3.0 1.3.0 2.0.0
+transitive2 - - 1.0.0 1.0.0
+transitive3 - - 1.0.0 1.0.0
+
+3 upgradable dependencies are locked (in pubspec.lock) to older versions.
+To update these dependencies, use `pub upgrade`.
+
+4 dependencies are constrained to versions that are older than a resolvable version.
+To update these dependencies, edit pubspec.yaml.
+
+$ pub outdated --format=no-color --up-to-date
+Resolving...
+Package Current Upgradable Resolvable Latest
+dependencies
+bar 1.0.0 1.0.0 1.0.0 1.0.0
+foo *1.2.3 *1.3.0 *2.0.0 3.0.0
+local_package 0.0.1 0.0.1 0.0.1 0.0.1
+
+dev_dependencies
+builder *1.2.3 *1.3.0 2.0.0 2.0.0
+
+transitive dependencies
+transitive *1.2.3 *1.3.0 *1.3.0 2.0.0
+transitive2 - - 1.0.0 1.0.0
+transitive3 - - 1.0.0 1.0.0
+
+3 upgradable dependencies are locked (in pubspec.lock) to older versions.
+To update these dependencies, use `pub upgrade`.
+
+4 dependencies are constrained to versions that are older than a resolvable version.
+To update these dependencies, edit pubspec.yaml.
+
+$ pub outdated --format=no-color --pre-releases
+Resolving...
+Package Current Upgradable Resolvable Latest
+dependencies
+foo *1.2.3 *1.3.0 *2.0.0 3.0.0
+
+dev_dependencies
+builder *1.2.3 *1.3.0 *2.0.0 3.0.0-alpha
+
+transitive dependencies
+transitive *1.2.3 *1.3.0 *1.3.0 2.0.0
+transitive2 - - 1.0.0 1.0.0
+transitive3 - - 1.0.0 1.0.0
+
+3 upgradable dependencies are locked (in pubspec.lock) to older versions.
+To update these dependencies, use `pub upgrade`.
+
+4 dependencies are constrained to versions that are older than a resolvable version.
+To update these dependencies, edit pubspec.yaml.
+
+$ pub outdated --format=no-color --no-dev-dependencies
+Resolving...
+Package Current Upgradable Resolvable Latest
+dependencies
+foo *1.2.3 *1.3.0 3.0.0 3.0.0
+
+transitive dependencies
+transitive *1.2.3 2.0.0 2.0.0 2.0.0
+
+2 upgradable dependencies are locked (in pubspec.lock) to older versions.
+To update these dependencies, use `pub upgrade`.
+
+1 dependency is constrained to a version that is older than a resolvable version.
+To update it, edit pubspec.yaml.
+
diff --git a/test/outdated/goldens/no_dependencies.txt b/test/outdated/goldens/no_dependencies.txt
new file mode 100644
index 0000000..9de1ac4
--- /dev/null
+++ b/test/outdated/goldens/no_dependencies.txt
@@ -0,0 +1,26 @@
+$ pub outdated --format=json
+Resolving...
+{
+ "packages": []
+}
+
+$ pub outdated --format=no-color
+Resolving...
+Found no outdated packages
+
+$ pub outdated --format=no-color --mark=none
+Resolving...
+Found no outdated packages
+
+$ pub outdated --format=no-color --up-to-date
+Resolving...
+Found no outdated packages
+
+$ pub outdated --format=no-color --pre-releases
+Resolving...
+Found no outdated packages
+
+$ pub outdated --format=no-color --no-dev-dependencies
+Resolving...
+Found no outdated packages
+
diff --git a/test/outdated/outdated_test.dart b/test/outdated/outdated_test.dart
new file mode 100644
index 0000000..7937261
--- /dev/null
+++ b/test/outdated/outdated_test.dart
@@ -0,0 +1,83 @@
+// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import '../descriptor.dart' as d;
+import '../golden_file.dart';
+import '../test_pub.dart';
+
+/// Try running 'pub outdated' with a number of different sets of arguments.
+///
+/// Compare the output to the file in goldens/$[name].
+Future<void> variations(String name) async {
+ final buffer = StringBuffer();
+ for (final args in [
+ ['--format=json'],
+ ['--format=no-color'],
+ ['--format=no-color', '--mark=none'],
+ ['--format=no-color', '--up-to-date'],
+ ['--format=no-color', '--pre-releases'],
+ ['--format=no-color', '--no-dev-dependencies'],
+ ]) {
+ final process = await startPub(args: ['outdated', ...args]);
+ await process.shouldExit(0);
+ expect(await process.stderr.rest.toList(), isEmpty);
+ buffer.writeln([
+ '\$ pub outdated ${args.join(' ')}',
+ ...await process.stdout.rest.toList()
+ ].join('\n'));
+ buffer.write('\n');
+ }
+ // The easiest way to update the golden files is to delete them and rerun the
+ // test.
+ expectMatchesGoldenFile(buffer.toString(), 'test/outdated/goldens/$name.txt');
+}
+
+Future<void> main() async {
+ test('no dependencies', () async {
+ await d.appDir().create();
+ await pubGet();
+ await variations('no_dependencies');
+ });
+
+ test('newer versions available', () async {
+ await servePackages((builder) => builder
+ ..serve('foo', '1.2.3', deps: {'transitive': '^1.0.0'})
+ ..serve('bar', '1.0.0')
+ ..serve('builder', '1.2.3', deps: {'transitive': '^1.0.0'})
+ ..serve('transitive', '1.2.3'));
+
+ await d.dir('local_package', [
+ d.libDir('local_package'),
+ d.libPubspec('local_package', '0.0.1')
+ ]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'app',
+ 'dependencies': {
+ 'foo': '^1.0.0',
+ 'bar': '^1.0.0',
+ 'local_package': {'path': '../local_package'}
+ },
+ 'dev_dependencies': {'builder': '^1.0.0'},
+ })
+ ]).create();
+ await pubGet();
+ globalPackageServer.add((builder) => builder
+ ..serve('foo', '1.3.0', deps: {'transitive': '>=1.0.0<3.0.0'})
+ ..serve('foo', '2.0.0',
+ deps: {'transitive': '>=1.0.0<3.0.0', 'transitive2': '^1.0.0'})
+ ..serve('foo', '3.0.0', deps: {'transitive': '^2.0.0'})
+ ..serve('builder', '1.3.0', deps: {'transitive': '^1.0.0'})
+ ..serve('builder', '2.0.0',
+ deps: {'transitive': '^1.0.0', 'transitive3': '^1.0.0'})
+ ..serve('builder', '3.0.0-alpha', deps: {'transitive': '^1.0.0'})
+ ..serve('transitive', '1.3.0')
+ ..serve('transitive', '2.0.0')
+ ..serve('transitive2', '1.0.0')
+ ..serve('transitive3', '1.0.0'));
+ await variations('newer_versions');
+ });
+}
diff --git a/test/pub_test.dart b/test/pub_test.dart
index ab59f71..7ee7a08 100644
--- a/test/pub_test.dart
+++ b/test/pub_test.dart
@@ -36,6 +36,7 @@
global Work with global packages.
help Display help information for pub.
logout Log out of pub.dartlang.org.
+ outdated Analyze your dependencies to find which ones can be upgraded.
publish Publish the current package to pub.dartlang.org.
run Run an executable from a package.
upgrade Upgrade the current package's dependencies to latest versions.