Warn first time a package version opts in to null-safety (#2465)

diff --git a/lib/src/command/lish.dart b/lib/src/command/lish.dart
index 0f361af..d2a710b 100644
--- a/lib/src/command/lish.dart
+++ b/lib/src/command/lish.dart
@@ -158,9 +158,12 @@
   /// Validates the package. Completes to false if the upload should not
   /// proceed.
   Future<bool> _validate(Future<int> packageSize) async {
-    var pair = await Validator.runAll(entrypoint, packageSize);
-    var errors = pair.first;
-    var warnings = pair.last;
+    final hints = <String>[];
+    final warnings = <String>[];
+    final errors = <String>[];
+
+    await Validator.runAll(entrypoint, packageSize, server.toString(),
+        hints: hints, warnings: warnings, errors: errors);
 
     if (errors.isNotEmpty) {
       log.error('Sorry, your package is missing '
@@ -172,9 +175,15 @@
 
     if (force) return true;
 
+    String formatWarningCount() {
+      final hs = hints.length == 1 ? '' : 's';
+      final hintText = hints.isEmpty ? '' : ' and ${hints.length} hint$hs.';
+      final ws = warnings.length == 1 ? '' : 's';
+      return '\nPackage has ${warnings.length} warning$ws$hintText.';
+    }
+
     if (dryRun) {
-      var s = warnings.length == 1 ? '' : 's';
-      log.warning('\nPackage has ${warnings.length} warning$s.');
+      log.warning(formatWarningCount());
       return warnings.isEmpty;
     }
 
@@ -183,12 +192,9 @@
     final package = entrypoint.root;
     var message = 'Do you want to publish ${package.name} ${package.version}';
 
-    if (warnings.isNotEmpty) {
-      final s = warnings.length == 1 ? '' : 's';
-      final warning = log.bold(log.red(
-        'Package has ${warnings.length} warning$s',
-      ));
-      message = '$warning. $message';
+    if (warnings.isNotEmpty || hints.isNotEmpty) {
+      final warning = formatWarningCount();
+      message = '${log.bold(log.red(warning))}. $message';
     }
 
     var confirmed = await confirm('\n$message');
diff --git a/lib/src/validator.dart b/lib/src/validator.dart
index 51fa569..2a3ef89 100644
--- a/lib/src/validator.dart
+++ b/lib/src/validator.dart
@@ -10,7 +10,6 @@
 import 'entrypoint.dart';
 import 'log.dart' as log;
 import 'sdk.dart';
-import 'utils.dart';
 import 'validator/changelog.dart';
 import 'validator/compiled_dartdoc.dart';
 import 'validator/dependency.dart';
@@ -25,6 +24,7 @@
 import 'validator/pubspec.dart';
 import 'validator/pubspec_field.dart';
 import 'validator/readme.dart';
+import 'validator/relative_version_numbering.dart';
 import 'validator/sdk_constraint.dart';
 import 'validator/size.dart';
 import 'validator/strict_dependencies.dart';
@@ -50,6 +50,11 @@
   /// Filled by calling [validate].
   final warnings = <String>[];
 
+  /// The accumulated hints for this validator.
+  ///
+  /// Filled by calling [validate].
+  final hints = <String>[];
+
   Validator(this.entrypoint);
 
   /// Validates the entrypoint, adding any errors and warnings to [errors] and
@@ -104,13 +109,15 @@
 
   /// Run all validators on the [entrypoint] package and print their results.
   ///
-  /// The future completes with the error and warning messages, respectively.
+  /// When the future completes [hints] [warnings] amd [errors] will have been
+  /// appended with the reported hints warnings and errors respectively.
   ///
   /// [packageSize], if passed, should complete to the size of the tarred
   /// package, in bytes. This is used to validate that it's not too big to
   /// upload to the server.
-  static Future<Pair<List<String>, List<String>>> runAll(Entrypoint entrypoint,
-      [Future<int> packageSize]) {
+  static Future<void> runAll(
+      Entrypoint entrypoint, Future<int> packageSize, String serverUrl,
+      {List<String> hints, List<String> warnings, List<String> errors}) {
     var validators = [
       PubspecValidator(entrypoint),
       LicenseValidator(entrypoint),
@@ -128,6 +135,7 @@
       StrictDependenciesValidator(entrypoint),
       FlutterPluginFormatValidator(entrypoint),
       LanguageVersionValidator(entrypoint),
+      RelativeVersionNumberingValidator(entrypoint, serverUrl),
     ];
     if (packageSize != null) {
       validators.add(SizeValidator(entrypoint, packageSize));
@@ -135,9 +143,10 @@
 
     return Future.wait(validators.map((validator) => validator.validate()))
         .then((_) {
-      var errors = validators.expand((validator) => validator.errors).toList();
-      var warnings =
-          validators.expand((validator) => validator.warnings).toList();
+      hints.addAll([for (final validator in validators) ...validator.hints]);
+      warnings
+          .addAll([for (final validator in validators) ...validator.warnings]);
+      errors.addAll([for (final validator in validators) ...validator.errors]);
 
       if (errors.isNotEmpty) {
         final s = errors.length > 1 ? 's' : '';
@@ -158,8 +167,6 @@
         }
         log.warning('');
       }
-
-      return Pair<List<String>, List<String>>(errors, warnings);
     });
   }
 }
diff --git a/lib/src/validator/relative_version_numbering.dart b/lib/src/validator/relative_version_numbering.dart
new file mode 100644
index 0000000..5e227fc
--- /dev/null
+++ b/lib/src/validator/relative_version_numbering.dart
@@ -0,0 +1,86 @@
+// 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 'package:pub_semver/pub_semver.dart';
+
+import '../entrypoint.dart';
+import '../exceptions.dart';
+import '../package_name.dart';
+import '../pubspec.dart';
+import '../validator.dart';
+
+/// Gives a warning when publishing a new version, if the latest published
+/// version lower to this was not opted into null-safety.
+class RelativeVersionNumberingValidator extends Validator {
+  static const String guideUrl = 'https://dart.dev/null-safety/migration-guide';
+  static const String semverUrl =
+      'https://dart.dev/tools/pub/versioning#semantic-versions';
+
+  final String _server;
+
+  RelativeVersionNumberingValidator(Entrypoint entrypoint, this._server)
+      : super(entrypoint);
+
+  @override
+  Future<void> validate() async {
+    final hostedSource = entrypoint.cache.sources.hosted;
+    List<PackageId> existingVersions;
+    try {
+      existingVersions = await hostedSource
+          .bind(entrypoint.cache)
+          .getVersions(hostedSource.refFor(entrypoint.root.name, url: _server));
+    } on PackageNotFoundException {
+      existingVersions = [];
+    }
+    existingVersions..sort((a, b) => a.version.compareTo(b.version));
+    final previousVersion = existingVersions.lastWhere(
+        (id) =>
+            !id.version.isPreRelease && id.version < entrypoint.root.version,
+        orElse: () => null);
+    if (previousVersion == null) return;
+
+    final previousPubspec =
+        await hostedSource.bind(entrypoint.cache).describe(previousVersion);
+
+    final currentOptedIn = _optedIntoNullSafety(entrypoint.root.pubspec);
+    final previousOptedIn = _optedIntoNullSafety(previousPubspec);
+
+    if (currentOptedIn && !previousOptedIn) {
+      hints.add(
+          'You\'re about to publish a package that opts into null safety.\n'
+          'The previous version (${previousVersion.version}) isn\'t opted in.\n'
+          'See $guideUrl for best practices.');
+    } else if (!currentOptedIn && previousOptedIn) {
+      hints.add(
+          'You\'re about to publish a package that doesn\'t opt into null safety,\n'
+          'but the previous version (${previousVersion.version}) was opted in.\n'
+          'This change is likely to be backwards incompatible.\n'
+          'See $semverUrl for information about versioning.');
+    }
+  }
+
+  static bool _optedIntoNullSafety(Pubspec pubspec) {
+    final sdkConstraint = pubspec.originalDartSdkConstraint;
+
+    /// If the sdk constraint is not a `VersionRange` something is wrong, and
+    /// we cannot deduce the language version.
+    ///
+    /// This will hopefully be detected elsewhere.
+    ///
+    /// A single `Version` is also a `VersionRange`.
+    if (sdkConstraint is! VersionRange) return false;
+    final constraintMin = (sdkConstraint as VersionRange).min;
+
+    if (constraintMin == null) return false;
+
+    final languageVersion =
+        Version(constraintMin.major, constraintMin.minor, 0);
+
+    return languageVersion >= _firstVersionSupportingNullSafety;
+  }
+
+  static final _firstVersionSupportingNullSafety = Version.parse('2.10.0');
+}
diff --git a/test/descriptor.dart b/test/descriptor.dart
index 69427eb..ce65803 100644
--- a/test/descriptor.dart
+++ b/test/descriptor.dart
@@ -6,7 +6,6 @@
 import 'package:oauth2/oauth2.dart' as oauth2;
 import 'package:pub/src/io.dart';
 import 'package:pub/src/package_config.dart';
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test_descriptor/test_descriptor.dart';
 import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
@@ -141,14 +140,14 @@
 /// Describes the file in the system cache that contains the client's OAuth2
 /// credentials. The URL "/token" on [server] will be used as the token
 /// endpoint for refreshing the access token.
-Descriptor credentialsFile(ShelfTestServer server, String accessToken,
+Descriptor credentialsFile(PackageServer server, String accessToken,
     {String refreshToken, DateTime expiration}) {
   return dir(cachePath, [
     file(
         'credentials.json',
         oauth2.Credentials(accessToken,
                 refreshToken: refreshToken,
-                tokenEndpoint: server.url.resolve('/token'),
+                tokenEndpoint: Uri.parse(server.url).resolve('/token'),
                 scopes: [
                   'openid',
                   'https://www.googleapis.com/auth/userinfo.email',
diff --git a/test/descriptor_server.dart b/test/descriptor_server.dart
index d14dbed..fdfa67a 100644
--- a/test/descriptor_server.dart
+++ b/test/descriptor_server.dart
@@ -37,7 +37,7 @@
 /// This server will exist only for the duration of the pub run. It's accessible
 /// via [server]. Subsequent calls to [serve] replace the previous server.
 Future serve([List<d.Descriptor> contents]) async {
-  globalServer = await DescriptorServer.start(contents);
+  globalServer = (await DescriptorServer.start())..contents.addAll(contents);
 }
 
 /// Like [serve], but reports an error if a request ever comes in to the server.
@@ -63,21 +63,33 @@
   /// This can safely be modified between requests.
   List<d.Descriptor> get contents => _baseDir.contents;
 
+  /// Handlers for requests not easily described as files.
+  final Map<Pattern, shelf.Handler> extraHandlers = {};
+
   /// Creates an HTTP server to serve [contents] as static files.
   ///
   /// This server exists only for the duration of the pub run. Subsequent calls
   /// to [serve] replace the previous server.
-  static Future<DescriptorServer> start([List<d.Descriptor> contents]) async =>
-      DescriptorServer._(
-          await shelf_io.IOServer.bind('localhost', 0), contents);
+  static Future<DescriptorServer> start() async =>
+      DescriptorServer._(await shelf_io.IOServer.bind('localhost', 0));
 
   /// Creates a server that reports an error if a request is ever received.
   static Future<DescriptorServer> errors() async =>
-      DescriptorServer._errors(await shelf_io.IOServer.bind('localhost', 0));
+      DescriptorServer._(await shelf_io.IOServer.bind('localhost', 0))
+        ..extraHandlers[RegExp('.*')] = (request) {
+          fail('The HTTP server received an unexpected request:\n'
+              '${request.method} ${request.requestedUri}');
+        };
 
-  DescriptorServer._(this._server, Iterable<d.Descriptor> contents)
-      : _baseDir = d.dir('serve-dir', contents) {
+  DescriptorServer._(this._server) : _baseDir = d.dir('serve-dir', []) {
     _server.mount((request) async {
+      final pathWithInitialSlash = '/${request.url.path}';
+      final key = extraHandlers.keys.firstWhere((pattern) {
+        final match = pattern.matchAsPrefix(pathWithInitialSlash);
+        return match != null && match.end == pathWithInitialSlash.length;
+      }, orElse: () => null);
+      if (key != null) return extraHandlers[key](request);
+
       var path = p.posix.fromUri(request.url.path);
       requestedPaths.add(path);
 
@@ -91,14 +103,6 @@
     addTearDown(_server.close);
   }
 
-  DescriptorServer._errors(this._server) : _baseDir = d.dir('serve-dir', []) {
-    _server.mount((request) {
-      fail('The HTTP server received an unexpected request:\n'
-          '${request.method} ${request.requestedUri}');
-    });
-    addTearDown(_server.close);
-  }
-
   /// Closes this server.
   Future close() => _server.close();
 }
diff --git a/test/hosted/version_negotiation_test.dart b/test/hosted/version_negotiation_test.dart
index 6d06729..bed8e2d 100644
--- a/test/hosted/version_negotiation_test.dart
+++ b/test/hosted/version_negotiation_test.dart
@@ -2,8 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'package:pub/src/exit_codes.dart' as exit_codes;
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -12,38 +12,37 @@
 void main() {
   forBothPubGetAndUpgrade((command) {
     test('sends the correct Accept header', () async {
-      var server = await ShelfTestServer.create();
+      await servePackages();
 
       await d.appDir({
         'foo': {
-          'hosted': {'name': 'foo', 'url': server.url.toString()}
+          'hosted': {'name': 'foo', 'url': globalPackageServer.url}
         }
       }).create();
 
-      var pub = await startPub(args: [command.name]);
-
-      server.handler.expect('GET', '/api/packages/foo', (request) {
+      globalPackageServer.expect('GET', '/api/packages/foo', (request) {
         expect(
             request.headers['accept'], equals('application/vnd.pub.v2+json'));
-        return shelf.Response(200);
+        return shelf.Response(404);
       });
 
-      await pub.kill();
+      await pubCommand(command,
+          output: anything, exitCode: exit_codes.UNAVAILABLE);
     });
 
     test('prints a friendly error if the version is out-of-date', () async {
-      var server = await ShelfTestServer.create();
+      await servePackages();
 
       await d.appDir({
         'foo': {
-          'hosted': {'name': 'foo', 'url': server.url.toString()}
+          'hosted': {'name': 'foo', 'url': globalPackageServer.url}
         }
       }).create();
 
       var pub = await startPub(args: [command.name]);
 
-      server.handler
-          .expect('GET', '/api/packages/foo', (request) => shelf.Response(406));
+      globalPackageServer.expect(
+          'GET', '/api/packages/foo', (request) => shelf.Response(406));
 
       await pub.shouldExit(1);
 
diff --git a/test/lish/archives_and_uploads_a_package_test.dart b/test/lish/archives_and_uploads_a_package_test.dart
index 8a57716..4c9e114 100644
--- a/test/lish/archives_and_uploads_a_package_test.dart
+++ b/test/lish/archives_and_uploads_a_package_test.dart
@@ -6,7 +6,6 @@
 
 import 'package:path/path.dart' as p;
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import 'package:pub/src/exit_codes.dart' as exit_codes;
@@ -20,15 +19,15 @@
   setUp(d.validPackage.create);
 
   test('archives and uploads a package', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
-    handleUploadForm(server);
-    handleUpload(server);
+    handleUploadForm(globalPackageServer);
+    handleUpload(globalPackageServer);
 
-    server.handler.expect('GET', '/create', (request) {
+    globalPackageServer.expect('GET', '/create', (request) {
       return shelf.Response.ok(jsonEncode({
         'success': {'message': 'Package test_pkg 1.0.0 uploaded!'}
       }));
@@ -54,15 +53,15 @@
     deleteEntry(p.join(d.sandbox, appPath, 'empty'));
     await d.dir(p.join(appPath, 'empty')).create();
 
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
-    handleUploadForm(server);
-    handleUpload(server);
+    handleUploadForm(globalPackageServer);
+    handleUpload(globalPackageServer);
 
-    server.handler.expect('GET', '/create', (request) {
+    globalPackageServer.expect('GET', '/create', (request) {
       return shelf.Response.ok(jsonEncode({
         'success': {'message': 'Package test_pkg 1.0.0 uploaded!'}
       }));
diff --git a/test/lish/cloud_storage_upload_doesnt_redirect_test.dart b/test/lish/cloud_storage_upload_doesnt_redirect_test.dart
index 14fc05f..242b2a9 100644
--- a/test/lish/cloud_storage_upload_doesnt_redirect_test.dart
+++ b/test/lish/cloud_storage_upload_doesnt_redirect_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -14,14 +13,14 @@
   setUp(d.validPackage.create);
 
   test("cloud storage upload doesn't redirect", () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
-    handleUploadForm(server);
+    handleUploadForm(globalPackageServer);
 
-    server.handler.expect('POST', '/upload', (request) async {
+    globalPackageServer.expect('POST', '/upload', (request) async {
       await request.read().drain();
       return shelf.Response(200);
     });
diff --git a/test/lish/cloud_storage_upload_provides_an_error_test.dart b/test/lish/cloud_storage_upload_provides_an_error_test.dart
index aa1956f..048f10a 100644
--- a/test/lish/cloud_storage_upload_provides_an_error_test.dart
+++ b/test/lish/cloud_storage_upload_provides_an_error_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -14,14 +13,14 @@
   setUp(d.validPackage.create);
 
   test('cloud storage upload provides an error', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
-    handleUploadForm(server);
+    handleUploadForm(globalPackageServer);
 
-    server.handler.expect('POST', '/upload', (request) {
+    globalPackageServer.expect('POST', '/upload', (request) {
       return request.read().drain().then((_) {
         return shelf.Response.notFound(
             '<Error><Message>Your request sucked.</Message></Error>',
diff --git a/test/lish/force_does_not_publish_if_there_are_errors_test.dart b/test/lish/force_does_not_publish_if_there_are_errors_test.dart
index 098c40a..0e95efe 100644
--- a/test/lish/force_does_not_publish_if_there_are_errors_test.dart
+++ b/test/lish/force_does_not_publish_if_there_are_errors_test.dart
@@ -2,7 +2,6 @@
 // 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:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import 'package:pub/src/exit_codes.dart' as exit_codes;
@@ -18,8 +17,8 @@
     pkg.remove('description');
     await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-    var server = await ShelfTestServer.create();
-    var pub = await startPublish(server, args: ['--force']);
+    await servePackages();
+    var pub = await startPublish(globalPackageServer, args: ['--force']);
 
     await pub.shouldExit(exit_codes.DATA);
     expect(
diff --git a/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart b/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart
index b39e8ec..256cf58 100644
--- a/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart
+++ b/test/lish/force_publishes_if_tests_are_no_warnings_or_errors_test.dart
@@ -5,7 +5,6 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import 'package:pub/src/exit_codes.dart' as exit_codes;
@@ -18,14 +17,14 @@
   setUp(d.validPackage.create);
 
   test('--force publishes if there are no warnings or errors', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server, args: ['--force']);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer, args: ['--force']);
 
-    handleUploadForm(server);
-    handleUpload(server);
+    handleUploadForm(globalPackageServer);
+    handleUpload(globalPackageServer);
 
-    server.handler.expect('GET', '/create', (request) {
+    globalPackageServer.expect('GET', '/create', (request) {
       return shelf.Response.ok(jsonEncode({
         'success': {'message': 'Package test_pkg 1.0.0 uploaded!'}
       }));
diff --git a/test/lish/force_publishes_if_there_are_warnings_test.dart b/test/lish/force_publishes_if_there_are_warnings_test.dart
index 23b7996..9bd0c01 100644
--- a/test/lish/force_publishes_if_there_are_warnings_test.dart
+++ b/test/lish/force_publishes_if_there_are_warnings_test.dart
@@ -5,7 +5,6 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import 'package:pub/src/exit_codes.dart' as exit_codes;
@@ -23,14 +22,14 @@
     pkg['dependencies'] = {'foo': 'any'};
     await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server, args: ['--force']);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer, args: ['--force']);
 
-    handleUploadForm(server);
-    handleUpload(server);
+    handleUploadForm(globalPackageServer);
+    handleUpload(globalPackageServer);
 
-    server.handler.expect('GET', '/create', (request) {
+    globalPackageServer.expect('GET', '/create', (request) {
       return shelf.Response.ok(jsonEncode({
         'success': {'message': 'Package test_pkg 1.0.0 uploaded!'}
       }));
diff --git a/test/lish/many_files_test.dart b/test/lish/many_files_test.dart
index 7ef9d2c..d20e433 100644
--- a/test/lish/many_files_test.dart
+++ b/test/lish/many_files_test.dart
@@ -8,7 +8,6 @@
 
 import 'package:path/path.dart' as p;
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import 'package:pub/src/exit_codes.dart' as exit_codes;
@@ -75,15 +74,15 @@
       File(p.join(appRoot, fileName)).writeAsStringSync('');
     }
 
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
-    handleUploadForm(server);
-    handleUpload(server);
+    handleUploadForm(globalPackageServer);
+    handleUpload(globalPackageServer);
 
-    server.handler.expect('GET', '/create', (request) {
+    globalPackageServer.expect('GET', '/create', (request) {
       return shelf.Response.ok(jsonEncode({
         'success': {'message': 'Package test_pkg 1.0.0 uploaded!'}
       }));
diff --git a/test/lish/package_creation_provides_a_malformed_error_test.dart b/test/lish/package_creation_provides_a_malformed_error_test.dart
index a4b601d..c1fc105 100644
--- a/test/lish/package_creation_provides_a_malformed_error_test.dart
+++ b/test/lish/package_creation_provides_a_malformed_error_test.dart
@@ -5,7 +5,6 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -16,16 +15,16 @@
   setUp(d.validPackage.create);
 
   test('package creation provides a malformed error', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
-    handleUploadForm(server);
-    handleUpload(server);
+    handleUploadForm(globalPackageServer);
+    handleUpload(globalPackageServer);
 
     var body = {'error': 'Your package was too boring.'};
-    server.handler.expect('GET', '/create', (request) {
+    globalPackageServer.expect('GET', '/create', (request) {
       return shelf.Response.notFound(jsonEncode(body));
     });
 
diff --git a/test/lish/package_creation_provides_a_malformed_success_test.dart b/test/lish/package_creation_provides_a_malformed_success_test.dart
index 27415b8..997a61f 100644
--- a/test/lish/package_creation_provides_a_malformed_success_test.dart
+++ b/test/lish/package_creation_provides_a_malformed_success_test.dart
@@ -5,7 +5,6 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -16,16 +15,16 @@
   setUp(d.validPackage.create);
 
   test('package creation provides a malformed success', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
-    handleUploadForm(server);
-    handleUpload(server);
+    handleUploadForm(globalPackageServer);
+    handleUpload(globalPackageServer);
 
     var body = {'success': 'Your package was awesome.'};
-    server.handler.expect('GET', '/create', (request) {
+    globalPackageServer.expect('GET', '/create', (request) {
       return shelf.Response.ok(jsonEncode(body));
     });
 
diff --git a/test/lish/package_creation_provides_an_error_test.dart b/test/lish/package_creation_provides_an_error_test.dart
index 39f41af..a4c5fd4 100644
--- a/test/lish/package_creation_provides_an_error_test.dart
+++ b/test/lish/package_creation_provides_an_error_test.dart
@@ -5,7 +5,6 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -16,15 +15,15 @@
   setUp(d.validPackage.create);
 
   test('package creation provides an error', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
-    handleUploadForm(server);
-    handleUpload(server);
+    handleUploadForm(globalPackageServer);
+    handleUpload(globalPackageServer);
 
-    server.handler.expect('GET', '/create', (request) {
+    globalPackageServer.expect('GET', '/create', (request) {
       return shelf.Response.notFound(jsonEncode({
         'error': {'message': 'Your package was too boring.'}
       }));
diff --git a/test/lish/package_creation_provides_invalid_json_test.dart b/test/lish/package_creation_provides_invalid_json_test.dart
index 2666b95..9d845c4 100644
--- a/test/lish/package_creation_provides_invalid_json_test.dart
+++ b/test/lish/package_creation_provides_invalid_json_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -14,15 +13,15 @@
   setUp(d.validPackage.create);
 
   test('package creation provides invalid JSON', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
-    handleUploadForm(server);
-    handleUpload(server);
+    handleUploadForm(globalPackageServer);
+    handleUpload(globalPackageServer);
 
-    server.handler.expect('GET', '/create', (request) {
+    globalPackageServer.expect('GET', '/create', (request) {
       return shelf.Response.ok('{not json');
     });
 
diff --git a/test/lish/package_validation_has_a_warning_and_continues_test.dart b/test/lish/package_validation_has_a_warning_and_continues_test.dart
index b672126..aec98b7 100644
--- a/test/lish/package_validation_has_a_warning_and_continues_test.dart
+++ b/test/lish/package_validation_has_a_warning_and_continues_test.dart
@@ -5,7 +5,6 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import 'package:pub/src/exit_codes.dart' as exit_codes;
@@ -23,14 +22,14 @@
     pkg['author'] = 'Natalie Weizenbaum';
     await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
     pub.stdin.writeln('y');
-    handleUploadForm(server);
-    handleUpload(server);
+    handleUploadForm(globalPackageServer);
+    handleUpload(globalPackageServer);
 
-    server.handler.expect('GET', '/create', (request) {
+    globalPackageServer.expect('GET', '/create', (request) {
       return shelf.Response.ok(jsonEncode({
         'success': {'message': 'Package test_pkg 1.0.0 uploaded!'}
       }));
diff --git a/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart b/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart
index fa67de6..19c5d2a 100644
--- a/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart
+++ b/test/lish/package_validation_has_a_warning_and_is_canceled_test.dart
@@ -2,7 +2,6 @@
 // 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:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import 'package:pub/src/exit_codes.dart' as exit_codes;
@@ -19,8 +18,8 @@
     pkg['author'] = 'Natalie Weizenbaum';
     await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-    var server = await ShelfTestServer.create();
-    var pub = await startPublish(server);
+    await servePackages();
+    var pub = await startPublish(globalPackageServer);
 
     pub.stdin.writeln('n');
     await pub.shouldExit(exit_codes.DATA);
diff --git a/test/lish/package_validation_has_an_error_test.dart b/test/lish/package_validation_has_an_error_test.dart
index a23bf3c..8c22290 100644
--- a/test/lish/package_validation_has_an_error_test.dart
+++ b/test/lish/package_validation_has_an_error_test.dart
@@ -2,7 +2,6 @@
 // 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:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import 'package:pub/src/exit_codes.dart' as exit_codes;
@@ -18,8 +17,8 @@
     pkg.remove('description');
     await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-    var server = await ShelfTestServer.create();
-    var pub = await startPublish(server);
+    await servePackages();
+    var pub = await startPublish(globalPackageServer);
 
     await pub.shouldExit(exit_codes.DATA);
     expect(
diff --git a/test/lish/preview_package_validation_has_a_warning_test.dart b/test/lish/preview_package_validation_has_a_warning_test.dart
index 43e3272..7581aa0 100644
--- a/test/lish/preview_package_validation_has_a_warning_test.dart
+++ b/test/lish/preview_package_validation_has_a_warning_test.dart
@@ -2,7 +2,6 @@
 // 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:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import 'package:pub/src/exit_codes.dart' as exit_codes;
@@ -19,8 +18,8 @@
     pkg['dependencies'] = {'foo': 'any'};
     await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-    var server = await ShelfTestServer.create();
-    var pub = await startPublish(server, args: ['--dry-run']);
+    await servePackages();
+    var pub = await startPublish(globalPackageServer, args: ['--dry-run']);
 
     await pub.shouldExit(exit_codes.DATA);
     expect(
diff --git a/test/lish/preview_package_validation_has_no_warnings_test.dart b/test/lish/preview_package_validation_has_no_warnings_test.dart
index 5a39ddb..83ef097 100644
--- a/test/lish/preview_package_validation_has_no_warnings_test.dart
+++ b/test/lish/preview_package_validation_has_no_warnings_test.dart
@@ -2,7 +2,6 @@
 // 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:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import 'package:pub/src/exit_codes.dart' as exit_codes;
@@ -18,8 +17,8 @@
         packageMap('test_pkg', '1.0.0', null, null, {'sdk': '>=1.8.0 <2.0.0'});
     await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-    var server = await ShelfTestServer.create();
-    var pub = await startPublish(server, args: ['--dry-run']);
+    await servePackages((_) {});
+    var pub = await startPublish(globalPackageServer, args: ['--dry-run']);
 
     await pub.shouldExit(exit_codes.SUCCESS);
     expect(pub.stderr, emitsThrough('Package has 0 warnings.'));
diff --git a/test/lish/upload_form_fields_has_a_non_string_value_test.dart b/test/lish/upload_form_fields_has_a_non_string_value_test.dart
index 86c37d2..d26ac3e 100644
--- a/test/lish/upload_form_fields_has_a_non_string_value_test.dart
+++ b/test/lish/upload_form_fields_has_a_non_string_value_test.dart
@@ -4,7 +4,6 @@
 
 import 'dart:convert';
 
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -15,9 +14,9 @@
   setUp(d.validPackage.create);
 
   test('upload form fields has a non-string value', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages((_) {});
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
 
@@ -25,7 +24,7 @@
       'url': 'http://example.com/upload',
       'fields': {'field': 12}
     };
-    handleUploadForm(server, body);
+    handleUploadForm(globalPackageServer, body);
     expect(pub.stderr, emits('Invalid server response:'));
     expect(pub.stderr, emits(jsonEncode(body)));
     await pub.shouldExit(1);
diff --git a/test/lish/upload_form_fields_is_not_a_map_test.dart b/test/lish/upload_form_fields_is_not_a_map_test.dart
index 081cbdc..c21183a 100644
--- a/test/lish/upload_form_fields_is_not_a_map_test.dart
+++ b/test/lish/upload_form_fields_is_not_a_map_test.dart
@@ -4,7 +4,6 @@
 
 import 'dart:convert';
 
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -15,14 +14,14 @@
   setUp(d.validPackage.create);
 
   test('upload form fields is not a map', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
 
     var body = {'url': 'http://example.com/upload', 'fields': 12};
-    handleUploadForm(server, body);
+    handleUploadForm(globalPackageServer, body);
     expect(pub.stderr, emits('Invalid server response:'));
     expect(pub.stderr, emits(jsonEncode(body)));
     await pub.shouldExit(1);
diff --git a/test/lish/upload_form_is_missing_fields_test.dart b/test/lish/upload_form_is_missing_fields_test.dart
index 524b10d..24453fb 100644
--- a/test/lish/upload_form_is_missing_fields_test.dart
+++ b/test/lish/upload_form_is_missing_fields_test.dart
@@ -4,7 +4,6 @@
 
 import 'dart:convert';
 
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -15,14 +14,14 @@
   setUp(d.validPackage.create);
 
   test('upload form is missing fields', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
 
     var body = {'url': 'http://example.com/upload'};
-    handleUploadForm(server, body);
+    handleUploadForm(globalPackageServer, body);
     expect(pub.stderr, emits('Invalid server response:'));
     expect(pub.stderr, emits(jsonEncode(body)));
     await pub.shouldExit(1);
diff --git a/test/lish/upload_form_is_missing_url_test.dart b/test/lish/upload_form_is_missing_url_test.dart
index 838c54b..7861cf1 100644
--- a/test/lish/upload_form_is_missing_url_test.dart
+++ b/test/lish/upload_form_is_missing_url_test.dart
@@ -4,7 +4,6 @@
 
 import 'dart:convert';
 
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -15,9 +14,9 @@
   setUp(d.validPackage.create);
 
   test('upload form is missing url', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
 
@@ -25,7 +24,7 @@
       'fields': {'field1': 'value1', 'field2': 'value2'}
     };
 
-    handleUploadForm(server, body);
+    handleUploadForm(globalPackageServer, body);
     expect(pub.stderr, emits('Invalid server response:'));
     expect(pub.stderr, emits(jsonEncode(body)));
     await pub.shouldExit(1);
diff --git a/test/lish/upload_form_provides_an_error_test.dart b/test/lish/upload_form_provides_an_error_test.dart
index e0a24cc..767ba10 100644
--- a/test/lish/upload_form_provides_an_error_test.dart
+++ b/test/lish/upload_form_provides_an_error_test.dart
@@ -5,7 +5,6 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -15,13 +14,15 @@
   setUp(d.validPackage.create);
 
   test('upload form provides an error', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages((_) {});
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
 
-    server.handler.expect('GET', '/api/packages/versions/new', (request) {
+    globalPackageServer.extraHandlers['/api/packages/versions/new'] =
+        expectAsync1((request) {
+      expect(request.method, 'GET');
       return shelf.Response.notFound(jsonEncode({
         'error': {'message': 'your request sucked'}
       }));
diff --git a/test/lish/upload_form_provides_invalid_json_test.dart b/test/lish/upload_form_provides_invalid_json_test.dart
index f3633d0..6e02185 100644
--- a/test/lish/upload_form_provides_invalid_json_test.dart
+++ b/test/lish/upload_form_provides_invalid_json_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -13,13 +12,13 @@
   setUp(d.validPackage.create);
 
   test('upload form provides invalid JSON', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
 
-    server.handler.expect('GET', '/api/packages/versions/new',
+    globalPackageServer.expect('GET', '/api/packages/versions/new',
         (request) => shelf.Response.ok('{not json'));
 
     expect(
diff --git a/test/lish/upload_form_url_is_not_a_string_test.dart b/test/lish/upload_form_url_is_not_a_string_test.dart
index f832ccb..cc9b14f 100644
--- a/test/lish/upload_form_url_is_not_a_string_test.dart
+++ b/test/lish/upload_form_url_is_not_a_string_test.dart
@@ -4,7 +4,6 @@
 
 import 'dart:convert';
 
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -15,9 +14,9 @@
   setUp(d.validPackage.create);
 
   test('upload form url is not a string', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
 
@@ -26,7 +25,7 @@
       'fields': {'field1': 'value1', 'field2': 'value2'}
     };
 
-    handleUploadForm(server, body);
+    handleUploadForm(globalPackageServer, body);
     expect(pub.stderr, emits('Invalid server response:'));
     expect(pub.stderr, emits(jsonEncode(body)));
     await pub.shouldExit(1);
diff --git a/test/lish/utils.dart b/test/lish/utils.dart
index 2389205..0a67350 100644
--- a/test/lish/utils.dart
+++ b/test/lish/utils.dart
@@ -5,16 +5,17 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
-void handleUploadForm(ShelfTestServer server, [Map body]) {
-  server.handler.expect('GET', '/api/packages/versions/new', (request) {
+import '../test_pub.dart';
+
+void handleUploadForm(PackageServer server, [Map body]) {
+  server.expect('GET', '/api/packages/versions/new', (request) {
     expect(
         request.headers, containsPair('authorization', 'Bearer access token'));
 
     body ??= {
-      'url': server.url.resolve('/upload').toString(),
+      'url': Uri.parse(server.url).resolve('/upload').toString(),
       'fields': {'field1': 'value1', 'field2': 'value2'}
     };
 
@@ -23,14 +24,14 @@
   });
 }
 
-void handleUpload(ShelfTestServer server) {
-  server.handler.expect('POST', '/upload', (request) {
+void handleUpload(PackageServer server) {
+  server.expect('POST', '/upload', (request) {
     // TODO(nweiz): Once a multipart/form-data parser in Dart exists, validate
     // that the request body is correctly formatted. See issue 6952.
     return request
         .read()
         .drain()
         .then((_) => server.url)
-        .then((url) => shelf.Response.found(url.resolve('/create')));
+        .then((url) => shelf.Response.found(Uri.parse(url).resolve('/create')));
   });
 }
diff --git a/test/oauth2/logout_test.dart b/test/oauth2/logout_test.dart
index 4489f79..d1dee58 100644
--- a/test/oauth2/logout_test.dart
+++ b/test/oauth2/logout_test.dart
@@ -2,7 +2,6 @@
 // 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:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -10,9 +9,9 @@
 
 void main() {
   test('with an existing credentials.json, deletes it.', () async {
-    var server = await ShelfTestServer.create();
+    await servePackages();
     await d
-        .credentialsFile(server, 'access token',
+        .credentialsFile(globalPackageServer, 'access token',
             refreshToken: 'refresh token',
             expiration: DateTime.now().add(Duration(hours: 1)))
         .create();
diff --git a/test/oauth2/utils.dart b/test/oauth2/utils.dart
index 464e2fa..4eb2720 100644
--- a/test/oauth2/utils.dart
+++ b/test/oauth2/utils.dart
@@ -7,13 +7,14 @@
 
 import 'package:http/http.dart' as http;
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 import 'package:test_process/test_process.dart';
 
 import 'package:pub/src/utils.dart';
 
-Future authorizePub(TestProcess pub, ShelfTestServer server,
+import '../test_pub.dart';
+
+Future authorizePub(TestProcess pub, PackageServer server,
     [String accessToken = 'access token']) async {
   await expectLater(
       pub.stdout,
@@ -39,8 +40,8 @@
       equals('https://pub.dartlang.org/authorized'));
 }
 
-void handleAccessTokenRequest(ShelfTestServer server, String accessToken) {
-  server.handler.expect('POST', '/token', (request) async {
+void handleAccessTokenRequest(PackageServer server, String accessToken) {
+  server.expect('POST', '/token', (request) async {
     var body = await request.readAsString();
     expect(body, matches(RegExp(r'(^|&)code=access\+code(&|$)')));
 
diff --git a/test/oauth2/with_a_malformed_credentials_authenticates_again_test.dart b/test/oauth2/with_a_malformed_credentials_authenticates_again_test.dart
index 838f89f..c7e5375 100644
--- a/test/oauth2/with_a_malformed_credentials_authenticates_again_test.dart
+++ b/test/oauth2/with_a_malformed_credentials_authenticates_again_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -16,14 +15,14 @@
       'saves credentials.json', () async {
     await d.validPackage.create();
 
-    var server = await ShelfTestServer.create();
+    await servePackages();
     await d.dir(cachePath, [d.file('credentials.json', '{bad json')]).create();
 
-    var pub = await startPublish(server);
+    var pub = await startPublish(globalPackageServer);
     await confirmPublish(pub);
-    await authorizePub(pub, server, 'new access token');
+    await authorizePub(pub, globalPackageServer, 'new access token');
 
-    server.handler.expect('GET', '/api/packages/versions/new', (request) {
+    globalPackageServer.expect('GET', '/api/packages/versions/new', (request) {
       expect(request.headers,
           containsPair('authorization', 'Bearer new access token'));
 
@@ -34,6 +33,6 @@
     // do so rather than killing it so it'll write out the credentials file.
     await pub.shouldExit(1);
 
-    await d.credentialsFile(server, 'new access token').validate();
+    await d.credentialsFile(globalPackageServer, 'new access token').validate();
   });
 }
diff --git a/test/oauth2/with_a_pre_existing_credentials_does_not_authenticate_test.dart b/test/oauth2/with_a_pre_existing_credentials_does_not_authenticate_test.dart
index 40ce91e..4994503 100644
--- a/test/oauth2/with_a_pre_existing_credentials_does_not_authenticate_test.dart
+++ b/test/oauth2/with_a_pre_existing_credentials_does_not_authenticate_test.dart
@@ -2,8 +2,6 @@
 // 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:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -13,18 +11,12 @@
   test('with a pre-existing credentials.json does not authenticate', () async {
     await d.validPackage.create();
 
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
+
     await confirmPublish(pub);
 
-    server.handler.expect('GET', '/api/packages/versions/new', (request) {
-      expect(request.headers,
-          containsPair('authorization', 'Bearer access token'));
-
-      return shelf.Response(200);
-    });
-
     await pub.kill();
   });
 }
diff --git a/test/oauth2/with_a_server_rejected_refresh_token_authenticates_again_test.dart b/test/oauth2/with_a_server_rejected_refresh_token_authenticates_again_test.dart
index a57bcf2..c8b56c3 100644
--- a/test/oauth2/with_a_server_rejected_refresh_token_authenticates_again_test.dart
+++ b/test/oauth2/with_a_server_rejected_refresh_token_authenticates_again_test.dart
@@ -6,7 +6,6 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -20,16 +19,16 @@
       'saves credentials.json', () async {
     await d.validPackage.create();
 
-    var server = await ShelfTestServer.create();
+    await servePackages();
     await d
-        .credentialsFile(server, 'access token',
+        .credentialsFile(globalPackageServer, 'access token',
             refreshToken: 'bad refresh token',
             expiration: DateTime.now().subtract(Duration(hours: 1)))
         .create();
 
-    var pub = await startPublish(server);
+    var pub = await startPublish(globalPackageServer);
 
-    server.handler.expect('POST', '/token', (request) {
+    globalPackageServer.expect('POST', '/token', (request) {
       return request.read().drain().then((_) {
         return shelf.Response(400,
             body: jsonEncode({'error': 'invalid_request'}),
@@ -40,10 +39,11 @@
     await confirmPublish(pub);
 
     await expectLater(pub.stdout, emits(startsWith('Uploading...')));
-    await authorizePub(pub, server, 'new access token');
+    await authorizePub(pub, globalPackageServer, 'new access token');
 
     var done = Completer();
-    server.handler.expect('GET', '/api/packages/versions/new', (request) async {
+    globalPackageServer.expect('GET', '/api/packages/versions/new',
+        (request) async {
       expect(request.headers,
           containsPair('authorization', 'Bearer new access token'));
 
@@ -55,6 +55,5 @@
     });
 
     await done.future;
-    await server.close();
   });
 }
diff --git a/test/oauth2/with_an_expired_credentials_refreshes_and_saves_test.dart b/test/oauth2/with_an_expired_credentials_refreshes_and_saves_test.dart
index 3290846..de2e53c 100644
--- a/test/oauth2/with_an_expired_credentials_refreshes_and_saves_test.dart
+++ b/test/oauth2/with_an_expired_credentials_refreshes_and_saves_test.dart
@@ -5,7 +5,6 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -17,17 +16,17 @@
       'refreshed access token to credentials.json', () async {
     await d.validPackage.create();
 
-    var server = await ShelfTestServer.create();
+    await servePackages();
     await d
-        .credentialsFile(server, 'access token',
+        .credentialsFile(globalPackageServer, 'access token',
             refreshToken: 'refresh token',
             expiration: DateTime.now().subtract(Duration(hours: 1)))
         .create();
 
-    var pub = await startPublish(server);
+    var pub = await startPublish(globalPackageServer);
     await confirmPublish(pub);
 
-    server.handler.expect('POST', '/token', (request) {
+    globalPackageServer.expect('POST', '/token', (request) {
       return request.readAsString().then((body) {
         expect(
             body, matches(RegExp(r'(^|&)refresh_token=refresh\+token(&|$)')));
@@ -39,7 +38,7 @@
       });
     });
 
-    server.handler.expect('GET', '/api/packages/versions/new', (request) {
+    globalPackageServer.expect('GET', '/api/packages/versions/new', (request) {
       expect(request.headers,
           containsPair('authorization', 'Bearer new access token'));
 
@@ -49,7 +48,7 @@
     await pub.shouldExit();
 
     await d
-        .credentialsFile(server, 'new access token',
+        .credentialsFile(globalPackageServer, 'new access token',
             refreshToken: 'refresh token')
         .validate();
   });
diff --git a/test/oauth2/with_an_expired_credentials_without_a_refresh_token_authenticates_again_test.dart b/test/oauth2/with_an_expired_credentials_without_a_refresh_token_authenticates_again_test.dart
index 75a37d4..14009a7 100644
--- a/test/oauth2/with_an_expired_credentials_without_a_refresh_token_authenticates_again_test.dart
+++ b/test/oauth2/with_an_expired_credentials_without_a_refresh_token_authenticates_again_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -16,22 +15,22 @@
       'authenticates again and saves credentials.json', () async {
     await d.validPackage.create();
 
-    var server = await ShelfTestServer.create();
+    await servePackages();
     await d
-        .credentialsFile(server, 'access token',
+        .credentialsFile(globalPackageServer, 'access token',
             expiration: DateTime.now().subtract(Duration(hours: 1)))
         .create();
 
-    var pub = await startPublish(server);
+    var pub = await startPublish(globalPackageServer);
     await confirmPublish(pub);
 
     await expectLater(
         pub.stderr,
         emits("Pub's authorization to upload packages has expired and "
             "can't be automatically refreshed."));
-    await authorizePub(pub, server, 'new access token');
+    await authorizePub(pub, globalPackageServer, 'new access token');
 
-    server.handler.expect('GET', '/api/packages/versions/new', (request) {
+    globalPackageServer.expect('GET', '/api/packages/versions/new', (request) {
       expect(request.headers,
           containsPair('authorization', 'Bearer new access token'));
 
@@ -42,6 +41,6 @@
     // do so rather than killing it so it'll write out the credentials file.
     await pub.shouldExit(1);
 
-    await d.credentialsFile(server, 'new access token').validate();
+    await d.credentialsFile(globalPackageServer, 'new access token').validate();
   });
 }
diff --git a/test/oauth2/with_no_credentials_authenticates_and_saves_credentials_test.dart b/test/oauth2/with_no_credentials_authenticates_and_saves_credentials_test.dart
index b781f02..e1dae14 100644
--- a/test/oauth2/with_no_credentials_authenticates_and_saves_credentials_test.dart
+++ b/test/oauth2/with_no_credentials_authenticates_and_saves_credentials_test.dart
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -16,12 +15,12 @@
       'credentials.json', () async {
     await d.validPackage.create();
 
-    var server = await ShelfTestServer.create();
-    var pub = await startPublish(server);
+    await servePackages();
+    var pub = await startPublish(globalPackageServer);
     await confirmPublish(pub);
-    await authorizePub(pub, server);
+    await authorizePub(pub, globalPackageServer);
 
-    server.handler.expect('GET', '/api/packages/versions/new', (request) {
+    globalPackageServer.expect('GET', '/api/packages/versions/new', (request) {
       expect(request.headers,
           containsPair('authorization', 'Bearer access token'));
 
@@ -32,6 +31,6 @@
     // do so rather than killing it so it'll write out the credentials file.
     await pub.shouldExit(1);
 
-    await d.credentialsFile(server, 'access token').validate();
+    await d.credentialsFile(globalPackageServer, 'access token').validate();
   });
 }
diff --git a/test/oauth2/with_server_rejected_credentials_authenticates_again_test.dart b/test/oauth2/with_server_rejected_credentials_authenticates_again_test.dart
index 20ca653..136342f 100644
--- a/test/oauth2/with_server_rejected_credentials_authenticates_again_test.dart
+++ b/test/oauth2/with_server_rejected_credentials_authenticates_again_test.dart
@@ -5,7 +5,6 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 
 import '../descriptor.dart' as d;
@@ -16,13 +15,13 @@
       'with server-rejected credentials, authenticates again and saves '
       'credentials.json', () async {
     await d.validPackage.create();
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPublish(server);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPublish(globalPackageServer);
 
     await confirmPublish(pub);
 
-    server.handler.expect('GET', '/api/packages/versions/new', (request) {
+    globalPackageServer.expect('GET', '/api/packages/versions/new', (request) {
       return shelf.Response(401,
           body: jsonEncode({
             'error': {'message': 'your token sucks'}
diff --git a/test/package_server.dart b/test/package_server.dart
index 493bcba..58b2550 100644
--- a/test/package_server.dart
+++ b/test/package_server.dart
@@ -7,7 +7,9 @@
 
 import 'package:path/path.dart' as p;
 import 'package:pub_semver/pub_semver.dart';
+import 'package:shelf/shelf.dart' as shelf;
 import 'package:test/test.dart';
+import 'package:test/test.dart' as test show expect;
 
 import 'descriptor.dart' as d;
 import 'test_pub.dart';
@@ -21,8 +23,8 @@
 ///
 /// Calls [callback] with a [PackageServerBuilder] that's used to specify
 /// which packages to serve.
-Future servePackages(void Function(PackageServerBuilder) callback) async {
-  _globalPackageServer = await PackageServer.start(callback);
+Future servePackages([void Function(PackageServerBuilder) callback]) async {
+  _globalPackageServer = await PackageServer.start(callback ?? (_) {});
   globalServer = _globalPackageServer._inner;
 
   addTearDown(() {
@@ -66,6 +68,9 @@
   /// The URL for the server.
   String get url => 'http://localhost:$port';
 
+  /// Handlers for requests not easily described as packages.
+  Map<Pattern, shelf.Handler> get extraHandlers => _inner.extraHandlers;
+
   /// Creates an HTTP server that replicates the structure of pub.dartlang.org.
   ///
   /// Calls [callback] with a [PackageServerBuilder] that's used to specify
@@ -122,6 +127,15 @@
     });
   }
 
+  // Installs a handler at [pattern] that expects to be called exactly once with
+  // the given [method].
+  void expect(String method, Pattern pattern, shelf.Handler handler) {
+    extraHandlers[pattern] = expectAsync1((request) {
+      test.expect(request.method, method);
+      return handler(request);
+    });
+  }
+
   /// Returns the path of [package] at [version], installed from this server, in
   /// the pub cache.
   String pathInCache(String package, String version) => p.join(
diff --git a/test/pub_uploader_test.dart b/test/pub_uploader_test.dart
index 34390ef..1de8708 100644
--- a/test/pub_uploader_test.dart
+++ b/test/pub_uploader_test.dart
@@ -6,7 +6,6 @@
 import 'dart:convert';
 
 import 'package:shelf/shelf.dart' as shelf;
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart';
 import 'package:test_process/test_process.dart';
 
@@ -29,9 +28,8 @@
 See https://dart.dev/tools/pub/cmd/pub-uploader for detailed documentation.
 ''';
 
-Future<TestProcess> startPubUploader(
-    ShelfTestServer server, List<String> args) {
-  var tokenEndpoint = server.url.resolve('/token').toString();
+Future<TestProcess> startPubUploader(PackageServer server, List<String> args) {
+  var tokenEndpoint = Uri.parse(server.url).resolve('/token').toString();
   var allArgs = ['uploader', '--server', tokenEndpoint, ...args];
   return startPub(args: allArgs, tokenEndpoint: tokenEndpoint);
 }
@@ -59,12 +57,13 @@
   });
 
   test('adds an uploader', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub =
-        await startPubUploader(server, ['--package', 'pkg', 'add', 'email']);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPubUploader(
+        globalPackageServer, ['--package', 'pkg', 'add', 'email']);
 
-    server.handler.expect('POST', '/api/packages/pkg/uploaders', (request) {
+    globalPackageServer.expect('POST', '/api/packages/pkg/uploaders',
+        (request) {
       return request.readAsString().then((body) {
         expect(body, equals('email=email'));
 
@@ -81,12 +80,12 @@
   });
 
   test('removes an uploader', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub =
-        await startPubUploader(server, ['--package', 'pkg', 'remove', 'email']);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPubUploader(
+        globalPackageServer, ['--package', 'pkg', 'remove', 'email']);
 
-    server.handler.expect('DELETE', '/api/packages/pkg/uploaders/email',
+    globalPackageServer.expect('DELETE', '/api/packages/pkg/uploaders/email',
         (request) {
       return shelf.Response.ok(
           jsonEncode({
@@ -102,11 +101,11 @@
   test('defaults to the current package', () async {
     await d.validPackage.create();
 
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub = await startPubUploader(server, ['add', 'email']);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPubUploader(globalPackageServer, ['add', 'email']);
 
-    server.handler.expect('POST', '/api/packages/test_pkg/uploaders',
+    globalPackageServer.expect('POST', '/api/packages/test_pkg/uploaders',
         (request) {
       return shelf.Response.ok(
           jsonEncode({
@@ -120,12 +119,13 @@
   });
 
   test('add provides an error', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub =
-        await startPubUploader(server, ['--package', 'pkg', 'add', 'email']);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPubUploader(
+        globalPackageServer, ['--package', 'pkg', 'add', 'email']);
 
-    server.handler.expect('POST', '/api/packages/pkg/uploaders', (request) {
+    globalPackageServer.expect('POST', '/api/packages/pkg/uploaders',
+        (request) {
       return shelf.Response(400,
           body: jsonEncode({
             'error': {'message': 'Bad job!'}
@@ -138,12 +138,12 @@
   });
 
   test('remove provides an error', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
     var pub = await startPubUploader(
-        server, ['--package', 'pkg', 'remove', 'e/mail']);
+        globalPackageServer, ['--package', 'pkg', 'remove', 'e/mail']);
 
-    server.handler.expect('DELETE', '/api/packages/pkg/uploaders/e%2Fmail',
+    globalPackageServer.expect('DELETE', '/api/packages/pkg/uploaders/e%2Fmail',
         (request) {
       return shelf.Response(400,
           body: jsonEncode({
@@ -157,12 +157,12 @@
   });
 
   test('add provides invalid JSON', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub =
-        await startPubUploader(server, ['--package', 'pkg', 'add', 'email']);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPubUploader(
+        globalPackageServer, ['--package', 'pkg', 'add', 'email']);
 
-    server.handler.expect('POST', '/api/packages/pkg/uploaders',
+    globalPackageServer.expect('POST', '/api/packages/pkg/uploaders',
         (request) => shelf.Response.ok('{not json'));
 
     expect(
@@ -173,12 +173,12 @@
   });
 
   test('remove provides invalid JSON', () async {
-    var server = await ShelfTestServer.create();
-    await d.credentialsFile(server, 'access token').create();
-    var pub =
-        await startPubUploader(server, ['--package', 'pkg', 'remove', 'email']);
+    await servePackages();
+    await d.credentialsFile(globalPackageServer, 'access token').create();
+    var pub = await startPubUploader(
+        globalPackageServer, ['--package', 'pkg', 'remove', 'email']);
 
-    server.handler.expect('DELETE', '/api/packages/pkg/uploaders/email',
+    globalPackageServer.expect('DELETE', '/api/packages/pkg/uploaders/email',
         (request) => shelf.Response.ok('{not json'));
 
     expect(
diff --git a/test/test_pub.dart b/test/test_pub.dart
index 38ed12a..deb68b9 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -17,7 +17,6 @@
 import 'package:http/testing.dart';
 import 'package:path/path.dart' as p;
 import 'package:pub_semver/pub_semver.dart';
-import 'package:shelf_test_handler/shelf_test_handler.dart';
 import 'package:test/test.dart' hide fail;
 import 'package:test/test.dart' as test show fail;
 import 'package:test_process/test_process.dart';
@@ -39,6 +38,7 @@
 
 import 'descriptor.dart' as d;
 import 'descriptor_server.dart';
+import 'package_server.dart';
 
 export 'descriptor_server.dart';
 export 'package_server.dart';
@@ -288,10 +288,10 @@
 /// package server.
 ///
 /// Any futures in [args] will be resolved before the process is started.
-Future<PubProcess> startPublish(ShelfTestServer server,
+Future<PubProcess> startPublish(PackageServer server,
     {List<String> args}) async {
-  var tokenEndpoint = server.url.resolve('/token').toString();
-  args = ['lish', '--server', tokenEndpoint, ...?args];
+  var tokenEndpoint = Uri.parse(server.url).resolve('/token').toString();
+  args = ['lish', '--server', server.url, ...?args];
   return await startPub(args: args, tokenEndpoint: tokenEndpoint);
 }
 
@@ -751,14 +751,12 @@
 
 /// Schedules a single [Validator] to run on the [appPath].
 ///
-/// Returns a scheduled Future that contains the errors and warnings produced
-/// by that validator.
-Future<Pair<List<String>, List<String>>> validatePackage(
-    ValidatorCreator fn) async {
+/// Returns a scheduled Future that contains the validator after validation.
+Future<Validator> validatePackage(ValidatorCreator fn) async {
   var cache = SystemCache(rootDir: _pathInSandbox(cachePath));
   var validator = fn(Entrypoint(_pathInSandbox(appPath), cache));
   await validator.validate();
-  return Pair(validator.errors, validator.warnings);
+  return validator;
 }
 
 /// A matcher that matches a Pair.
diff --git a/test/validator/changelog_test.dart b/test/validator/changelog_test.dart
index 4870cc9..67547e6 100644
--- a/test/validator/changelog_test.dart
+++ b/test/validator/changelog_test.dart
@@ -28,7 +28,7 @@
 * Passes Turing test.
 '''),
       ]).create();
-      expectNoValidationError(changelog);
+      await expectValidation(changelog);
     });
   });
 
@@ -37,7 +37,7 @@
       await d.dir(appPath, [
         d.libPubspec('test_pkg', '1.0.0'),
       ]).create();
-      expectValidationWarning(changelog);
+      await expectValidation(changelog, warnings: isNotEmpty);
     });
 
     test('has has a CHANGELOG not named CHANGELOG.md', () async {
@@ -50,7 +50,7 @@
 * Passes Turing test.
 '''),
       ]).create();
-      expectValidationWarning(changelog);
+      await expectValidation(changelog, warnings: isNotEmpty);
     });
 
     test('has a CHANGELOG that doesn\'t include the current package version',
@@ -64,7 +64,7 @@
 * Passes Turing test.
 '''),
       ]).create();
-      expectValidationWarning(changelog);
+      await expectValidation(changelog, warnings: isNotEmpty);
     });
 
     test('has a CHANGELOG with invalid utf-8', () async {
@@ -72,7 +72,7 @@
         d.libPubspec('test_pkg', '1.0.0'),
         d.file('CHANGELOG.md', [192]),
       ]).create();
-      expectValidationWarning(changelog);
+      await expectValidation(changelog, warnings: isNotEmpty);
     });
   });
 }
diff --git a/test/validator/compiled_dartdoc_test.dart b/test/validator/compiled_dartdoc_test.dart
index 78c34fb..495f72f 100644
--- a/test/validator/compiled_dartdoc_test.dart
+++ b/test/validator/compiled_dartdoc_test.dart
@@ -19,7 +19,7 @@
   setUp(d.validPackage.create);
 
   group('should consider a package valid if it', () {
-    test('looks normal', () => expectNoValidationError(compiledDartdoc));
+    test('looks normal', () => expectValidation(compiledDartdoc));
 
     test('has most but not all files from compiling dartdoc', () async {
       await d.dir(appPath, [
@@ -30,7 +30,7 @@
           d.file('dart-logo-small.png', '')
         ])
       ]).create();
-      expectNoValidationError(compiledDartdoc);
+      await expectValidation(compiledDartdoc);
     });
 
     test('contains compiled dartdoc in a hidden directory', () async {
@@ -45,7 +45,7 @@
           d.file('client-live-nav.js', '')
         ])
       ]).create();
-      expectNoValidationError(compiledDartdoc);
+      await expectValidation(compiledDartdoc);
     });
 
     test('contains compiled dartdoc in a gitignored directory', () async {
@@ -61,7 +61,7 @@
         ]),
         d.file('.gitignore', '/doc-out')
       ]).create();
-      expectNoValidationError(compiledDartdoc);
+      await expectValidation(compiledDartdoc);
     });
   });
 
@@ -77,7 +77,7 @@
         ])
       ]).create();
 
-      expectValidationWarning(compiledDartdoc);
+      await expectValidation(compiledDartdoc, warnings: isNotEmpty);
     });
 
     test(
@@ -95,7 +95,7 @@
         ])
       ]).create();
 
-      expectValidationWarning(compiledDartdoc);
+      await expectValidation(compiledDartdoc, warnings: isNotEmpty);
     });
   });
 }
diff --git a/test/validator/dependency_override_test.dart b/test/validator/dependency_override_test.dart
index f9ae01e..bff7597 100644
--- a/test/validator/dependency_override_test.dart
+++ b/test/validator/dependency_override_test.dart
@@ -27,7 +27,7 @@
       })
     ]).create();
 
-    expectNoValidationError(dependencyOverride);
+    await expectValidation(dependencyOverride);
   });
 
   group('should consider a package invalid if', () {
@@ -39,7 +39,7 @@
         })
       ]).create();
 
-      expectValidationError(dependencyOverride);
+      await expectValidation(dependencyOverride, errors: isNotEmpty);
     });
 
     test('it has any non-dev dependency overrides', () async {
@@ -54,7 +54,7 @@
         })
       ]).create();
 
-      expectValidationError(dependencyOverride);
+      await expectValidation(dependencyOverride, errors: isNotEmpty);
     });
   });
 }
diff --git a/test/validator/dependency_test.dart b/test/validator/dependency_test.dart
index 3d93b68..ddb6edd 100644
--- a/test/validator/dependency_test.dart
+++ b/test/validator/dependency_test.dart
@@ -20,15 +20,11 @@
 
 Validator dependency(Entrypoint entrypoint) => DependencyValidator(entrypoint);
 
-void expectDependencyValidationError(String error) {
-  expect(validatePackage(dependency),
-      completion(pairOf(anyElement(contains(error)), isEmpty)));
-}
+Future<void> expectDependencyValidationError(String substring) =>
+    expectValidation(dependency, errors: anyElement(contains(substring)));
 
-void expectDependencyValidationWarning(String warning) {
-  expect(validatePackage(dependency),
-      completion(pairOf(isEmpty, anyElement(contains(warning)))));
-}
+Future<void> expectDependencyValidationWarning(String substring) =>
+    expectValidation(dependency, warnings: anyElement(contains(substring)));
 
 /// Sets up a test package with dependency [dep] and mocks a server with
 /// [hostedVersions] of the package available.
@@ -62,7 +58,7 @@
   group('should consider a package valid if it', () {
     test('looks normal', () async {
       await d.validPackage.create();
-      expectNoValidationError(dependency);
+      await expectValidation(dependency);
     });
 
     test('has a ^ constraint with an appropriate SDK constraint', () async {
@@ -70,7 +66,7 @@
         d.libPubspec('test_pkg', '1.0.0',
             deps: {'foo': '^1.2.3'}, sdk: '>=1.8.0 <2.0.0')
       ]).create();
-      expectNoValidationError(dependency);
+      await expectValidation(dependency);
     });
 
     test('with a dependency on a pre-release while being one', () async {
@@ -83,7 +79,7 @@
         )
       ]).create();
 
-      expectNoValidationError(dependency);
+      await expectValidation(dependency);
     });
 
     test('has a git path dependency with an appropriate SDK constraint',
@@ -102,7 +98,7 @@
       ]).create();
 
       // We should get a warning for using a git dependency, but not an error.
-      expectDependencyValidationWarning('  foo: any');
+      await expectDependencyValidationWarning('  foo: any');
     });
 
     test('depends on Flutter from an SDK source', () async {
@@ -117,7 +113,7 @@
         })
       ]).create();
 
-      expectNoValidationError(dependency);
+      await expectValidation(dependency);
     });
 
     test(
@@ -134,7 +130,7 @@
         })
       ]).create();
 
-      expectNoValidationError(dependency);
+      await expectValidation(dependency);
     });
 
     test(
@@ -151,7 +147,7 @@
         })
       ]).create();
 
-      expectNoValidationError(dependency);
+      await expectValidation(dependency);
     });
   });
 
@@ -163,7 +159,7 @@
         test('and should suggest the hosted primary version', () async {
           await setUpDependency({'git': 'git://github.com/dart-lang/foo'},
               hostedVersions: ['3.0.0-pre', '2.0.0', '1.0.0']);
-          expectDependencyValidationWarning('  foo: ^2.0.0');
+          await expectDependencyValidationWarning('  foo: ^2.0.0');
         });
 
         test(
@@ -171,7 +167,7 @@
             "it's the only version available", () async {
           await setUpDependency({'git': 'git://github.com/dart-lang/foo'},
               hostedVersions: ['3.0.0-pre', '2.0.0-pre']);
-          expectDependencyValidationWarning('  foo: ^3.0.0-pre');
+          await expectDependencyValidationWarning('  foo: ^3.0.0-pre');
         });
 
         test(
@@ -179,7 +175,7 @@
             'pre-1.0.0', () async {
           await setUpDependency({'git': 'git://github.com/dart-lang/foo'},
               hostedVersions: ['0.0.1', '0.0.2']);
-          expectDependencyValidationWarning('  foo: ^0.0.2');
+          await expectDependencyValidationWarning('  foo: ^0.0.2');
         });
       });
 
@@ -189,7 +185,7 @@
             'git': 'git://github.com/dart-lang/foo',
             'version': '>=1.0.0 <2.0.0'
           });
-          expectDependencyValidationWarning('  foo: ">=1.0.0 <2.0.0"');
+          await expectDependencyValidationWarning('  foo: ">=1.0.0 <2.0.0"');
         });
 
         test(
@@ -197,7 +193,7 @@
             'concrete', () async {
           await setUpDependency(
               {'git': 'git://github.com/dart-lang/foo', 'version': '0.2.3'});
-          expectDependencyValidationWarning('  foo: 0.2.3');
+          await expectDependencyValidationWarning('  foo: 0.2.3');
         });
       });
     });
@@ -207,7 +203,7 @@
         test('and should suggest the hosted primary version', () async {
           await setUpDependency({'path': path.join(d.sandbox, 'foo')},
               hostedVersions: ['3.0.0-pre', '2.0.0', '1.0.0']);
-          expectDependencyValidationError('  foo: ^2.0.0');
+          await expectDependencyValidationError('  foo: ^2.0.0');
         });
 
         test(
@@ -215,7 +211,7 @@
             "it's the only version available", () async {
           await setUpDependency({'path': path.join(d.sandbox, 'foo')},
               hostedVersions: ['3.0.0-pre', '2.0.0-pre']);
-          expectDependencyValidationError('  foo: ^3.0.0-pre');
+          await expectDependencyValidationError('  foo: ^3.0.0-pre');
         });
 
         test(
@@ -223,7 +219,7 @@
             'pre-1.0.0', () async {
           await setUpDependency({'path': path.join(d.sandbox, 'foo')},
               hostedVersions: ['0.0.1', '0.0.2']);
-          expectDependencyValidationError('  foo: ^0.0.2');
+          await expectDependencyValidationError('  foo: ^0.0.2');
         });
       });
 
@@ -233,7 +229,7 @@
             'path': path.join(d.sandbox, 'foo'),
             'version': '>=1.0.0 <2.0.0'
           });
-          expectDependencyValidationError('  foo: ">=1.0.0 <2.0.0"');
+          await expectDependencyValidationError('  foo: ">=1.0.0 <2.0.0"');
         });
 
         test(
@@ -241,7 +237,7 @@
             'concrete', () async {
           await setUpDependency(
               {'path': path.join(d.sandbox, 'foo'), 'version': '0.2.3'});
-          expectDependencyValidationError('  foo: 0.2.3');
+          await expectDependencyValidationError('  foo: 0.2.3');
         });
       });
     });
@@ -253,10 +249,8 @@
             d.libPubspec('test_pkg', '1.0.0', deps: {'foo': 'any'})
           ]).create();
 
-          expect(
-              validatePackage(dependency),
-              completion(
-                  pairOf(isEmpty, everyElement(isNot(contains('\n  foo:'))))));
+          await expectValidation(dependency,
+              warnings: everyElement(isNot(contains('\n  foo:'))));
         });
 
         test(
@@ -280,10 +274,8 @@
                 }))
           ]).create();
 
-          expect(
-              validatePackage(dependency),
-              completion(
-                  pairOf(isEmpty, everyElement(isNot(contains('\n  foo:'))))));
+          await expectValidation(dependency,
+              warnings: everyElement(isNot(contains('\n  foo:'))));
         });
       });
 
@@ -309,7 +301,7 @@
                 }))
           ]).create();
 
-          expectDependencyValidationWarning('  foo: ^1.2.3');
+          await expectDependencyValidationWarning('  foo: ^1.2.3');
         });
 
         test(
@@ -333,7 +325,7 @@
                 }))
           ]).create();
 
-          expectDependencyValidationWarning('  foo: ^0.1.2');
+          await expectDependencyValidationWarning('  foo: ^0.1.2');
         });
       });
     });
@@ -348,7 +340,8 @@
         )
       ]).create();
 
-      expectDependencyValidationWarning('Packages dependent on a pre-release');
+      await expectDependencyValidationWarning(
+          'Packages dependent on a pre-release');
     });
     test(
         'with a single-version dependency and it should suggest a '
@@ -357,7 +350,7 @@
         d.libPubspec('test_pkg', '1.0.0', deps: {'foo': '1.2.3'})
       ]).create();
 
-      expectDependencyValidationWarning('  foo: ^1.2.3');
+      await expectDependencyValidationWarning('  foo: ^1.2.3');
     });
 
     group('has a dependency without a lower bound', () {
@@ -367,10 +360,8 @@
             d.libPubspec('test_pkg', '1.0.0', deps: {'foo': '<3.0.0'})
           ]).create();
 
-          expect(
-              validatePackage(dependency),
-              completion(
-                  pairOf(isEmpty, everyElement(isNot(contains('\n  foo:'))))));
+          await expectValidation(dependency,
+              warnings: everyElement(isNot(contains('\n  foo:'))));
         });
 
         test(
@@ -394,10 +385,8 @@
                 }))
           ]).create();
 
-          expect(
-              validatePackage(dependency),
-              completion(
-                  pairOf(isEmpty, everyElement(isNot(contains('\n  foo:'))))));
+          await expectValidation(dependency,
+              warnings: everyElement(isNot(contains('\n  foo:'))));
         });
       });
 
@@ -423,7 +412,7 @@
                 }))
           ]).create();
 
-          expectDependencyValidationWarning('  foo: ">=1.2.3 <3.0.0"');
+          await expectDependencyValidationWarning('  foo: ">=1.2.3 <3.0.0"');
         });
 
         test('and it should preserve the upper-bound operator', () async {
@@ -445,7 +434,7 @@
                 }))
           ]).create();
 
-          expectDependencyValidationWarning('  foo: ">=1.2.3 <=3.0.0"');
+          await expectDependencyValidationWarning('  foo: ">=1.2.3 <=3.0.0"');
         });
 
         test(
@@ -469,7 +458,7 @@
                 }))
           ]).create();
 
-          expectDependencyValidationWarning('  foo: ^1.2.3');
+          await expectDependencyValidationWarning('  foo: ^1.2.3');
         });
       });
     });
@@ -481,7 +470,7 @@
           d.libPubspec('test_pkg', '1.0.0', deps: {'foo': '>=1.2.3'})
         ]).create();
 
-        expectDependencyValidationWarning('  foo: ^1.2.3');
+        await expectDependencyValidationWarning('  foo: ^1.2.3');
       });
 
       test('and it should preserve the lower-bound operator', () async {
@@ -489,7 +478,7 @@
           d.libPubspec('test_pkg', '1.0.0', deps: {'foo': '>1.2.3'})
         ]).create();
 
-        expectDependencyValidationWarning('  foo: ">1.2.3 <2.0.0"');
+        await expectDependencyValidationWarning('  foo: ">1.2.3 <2.0.0"');
       });
     });
 
@@ -499,7 +488,7 @@
           d.libPubspec('integration_pkg', '1.0.0', deps: {'foo': '^1.2.3'})
         ]).create();
 
-        expectDependencyValidationError('  sdk: ">=1.8.0 <2.0.0"');
+        await expectDependencyValidationError('  sdk: ">=1.8.0 <2.0.0"');
       });
 
       test('with a too-broad SDK constraint', () async {
@@ -508,7 +497,7 @@
               deps: {'foo': '^1.2.3'}, sdk: '>=1.5.0 <2.0.0')
         ]).create();
 
-        expectDependencyValidationError('  sdk: ">=1.8.0 <2.0.0"');
+        await expectDependencyValidationError('  sdk: ">=1.8.0 <2.0.0"');
       });
     });
 
@@ -522,10 +511,9 @@
           })
         ]).create();
 
-        expect(
-            validatePackage(dependency),
-            completion(pairOf(anyElement(contains('  sdk: ">=2.0.0 <3.0.0"')),
-                anyElement(contains('  foo: any')))));
+        await expectValidation(dependency,
+            errors: anyElement(contains('  sdk: ">=2.0.0 <3.0.0"')),
+            warnings: anyElement(contains('  foo: any')));
       });
 
       test('with a too-broad SDK constraint', () async {
@@ -542,10 +530,9 @@
               sdk: '>=1.24.0 <3.0.0')
         ]).create();
 
-        expect(
-            validatePackage(dependency),
-            completion(pairOf(anyElement(contains('  sdk: ">=2.0.0 <3.0.0"')),
-                anyElement(contains('  foo: any')))));
+        await expectValidation(dependency,
+            errors: anyElement(contains('  sdk: ">=2.0.0 <3.0.0"')),
+            warnings: anyElement(contains('  foo: any')));
       });
     });
 
@@ -559,7 +546,7 @@
         })
       ]).create();
 
-      expectDependencyValidationError(
+      await expectDependencyValidationError(
           'Packages with package features may not be published yet.');
     });
 
@@ -576,7 +563,7 @@
         })
       ]).create();
 
-      expectDependencyValidationError(
+      await expectDependencyValidationError(
           'Packages with package features may not be published yet.');
     });
 
@@ -585,7 +572,7 @@
         d.libPubspec('test_pkg', '1.0.0', deps: {'flutter': '>=1.2.3 <2.0.0'})
       ]).create();
 
-      expectDependencyValidationError('sdk: >=1.2.3 <2.0.0');
+      await expectDependencyValidationError('sdk: >=1.2.3 <2.0.0');
     });
 
     test('depends on a Flutter package from an unknown SDK', () async {
@@ -599,7 +586,7 @@
         })
       ]).create();
 
-      expectDependencyValidationError(
+      await expectDependencyValidationError(
           'Unknown SDK "fblthp" for dependency "foo".');
     });
 
@@ -616,7 +603,7 @@
         })
       ]).create();
 
-      expectDependencyValidationError('sdk: ">=1.19.0 <2.0.0"');
+      await expectDependencyValidationError('sdk: ">=1.19.0 <2.0.0"');
     });
 
     test('depends on a Flutter package with no SDK constraint', () async {
@@ -630,7 +617,7 @@
         })
       ]).create();
 
-      expectDependencyValidationError('sdk: ">=1.19.0 <2.0.0"');
+      await expectDependencyValidationError('sdk: ">=1.19.0 <2.0.0"');
     });
 
     test('depends on a Fuchsia package with a too-broad SDK constraint',
@@ -646,7 +633,7 @@
         })
       ]).create();
 
-      expectDependencyValidationError('sdk: ">=2.0.0 <3.0.0"');
+      await expectDependencyValidationError('sdk: ">=2.0.0 <3.0.0"');
     });
 
     test('depends on a Fuchsia package with no SDK constraint', () async {
@@ -660,7 +647,7 @@
         })
       ]).create();
 
-      expectDependencyValidationError('sdk: ">=2.0.0 <3.0.0"');
+      await expectDependencyValidationError('sdk: ">=2.0.0 <3.0.0"');
     });
   });
 }
diff --git a/test/validator/deprecated_fields_test.dart b/test/validator/deprecated_fields_test.dart
index 213ec56..4d29c03 100644
--- a/test/validator/deprecated_fields_test.dart
+++ b/test/validator/deprecated_fields_test.dart
@@ -18,9 +18,8 @@
 void main() {
   setUp(d.validPackage.create);
 
-  test('should not warn if neither transformers or web is included', () {
-    expectNoValidationError(deprecatedFields);
-  });
+  test('should not warn if neither transformers or web is included',
+      () => expectValidation(deprecatedFields));
 
   test('should warn if pubspec has a transformers section', () async {
     await d.dir(appPath, [
@@ -29,7 +28,7 @@
       })
     ]).create();
 
-    expectValidationWarning(deprecatedFields);
+    await expectValidation(deprecatedFields, warnings: isNotEmpty);
   });
 
   test('should warn if pubspec has a web section', () async {
@@ -39,7 +38,7 @@
       })
     ]).create();
 
-    expectValidationWarning(deprecatedFields);
+    await expectValidation(deprecatedFields, warnings: isNotEmpty);
   });
 
   test('should warn if pubspec has an author', () async {
@@ -47,7 +46,7 @@
       d.pubspec({'author': 'Ronald <ronald@example.com>'})
     ]).create();
 
-    expectValidationWarning(deprecatedFields);
+    await expectValidation(deprecatedFields, warnings: isNotEmpty);
   });
 
   test('should warn if pubspec has a list of authors', () async {
@@ -57,6 +56,6 @@
       })
     ]).create();
 
-    expectValidationWarning(deprecatedFields);
+    await expectValidation(deprecatedFields, warnings: isNotEmpty);
   });
 }
diff --git a/test/validator/directory_test.dart b/test/validator/directory_test.dart
index 13a110a..ea05e24 100644
--- a/test/validator/directory_test.dart
+++ b/test/validator/directory_test.dart
@@ -18,13 +18,13 @@
   group('should consider a package valid if it', () {
     setUp(d.validPackage.create);
 
-    test('looks normal', () => expectNoValidationError(directory));
+    test('looks normal', () => expectValidation(directory));
 
     test('has a nested directory named "tools"', () async {
       await d.dir(appPath, [
         d.dir('foo', [d.dir('tools')])
       ]).create();
-      expectNoValidationError(directory);
+      await expectValidation(directory);
     });
   });
 
@@ -46,7 +46,7 @@
     for (var name in names) {
       test('"$name"', () async {
         await d.dir(appPath, [d.dir(name)]).create();
-        expectValidationWarning(directory);
+        await expectValidation(directory, warnings: isNotEmpty);
       });
     }
   });
diff --git a/test/validator/executable_test.dart b/test/validator/executable_test.dart
index fa32d58..df4222d 100644
--- a/test/validator/executable_test.dart
+++ b/test/validator/executable_test.dart
@@ -30,7 +30,7 @@
           d.file('two.dart', "main() => print('ok');")
         ])
       ]).create();
-      expectNoValidationError(executable);
+      await expectValidation(executable);
     });
   });
 
@@ -43,7 +43,7 @@
           'executables': {'nope': 'not_there', 'nada': null}
         })
       ]).create();
-      expectValidationWarning(executable);
+      await expectValidation(executable, warnings: isNotEmpty);
     });
 
     test('has .gitignored one or more listed executables', () async {
@@ -59,7 +59,7 @@
         ]),
         d.file('.gitignore', 'bin')
       ]).create();
-      expectValidationWarning(executable);
+      await expectValidation(executable, warnings: isNotEmpty);
     });
   });
 }
diff --git a/test/validator/flutter_plugin_format_test.dart b/test/validator/flutter_plugin_format_test.dart
index 61d8309..8906ec8 100644
--- a/test/validator/flutter_plugin_format_test.dart
+++ b/test/validator/flutter_plugin_format_test.dart
@@ -19,7 +19,7 @@
   group('should consider a package valid if it', () {
     setUp(d.validPackage.create);
 
-    test('looks normal', () => expectNoValidationError(flutterPluginFormat));
+    test('looks normal', () => expectValidation(flutterPluginFormat));
 
     test('is a Flutter 1.9.0 package', () async {
       var pkg = packageMap('test_pkg', '1.0.0', {
@@ -29,7 +29,7 @@
         'flutter': '>=1.9.0 <2.0.0',
       });
       await d.dir(appPath, [d.pubspec(pkg)]).create();
-      expectNoValidationError(flutterPluginFormat);
+      await expectValidation(flutterPluginFormat);
     });
 
     test('is a Flutter 1.10.0 package', () async {
@@ -40,7 +40,7 @@
         'flutter': '>=1.10.0 <2.0.0',
       });
       await d.dir(appPath, [d.pubspec(pkg)]).create();
-      expectNoValidationError(flutterPluginFormat);
+      await expectValidation(flutterPluginFormat);
     });
 
     test('is a Flutter 1.10.0-0 package', () async {
@@ -51,7 +51,7 @@
         'flutter': '>=1.10.0-0 <2.0.0',
       });
       await d.dir(appPath, [d.pubspec(pkg)]).create();
-      expectNoValidationError(flutterPluginFormat);
+      await expectValidation(flutterPluginFormat);
     });
 
     test('is a flutter 1.10.0 plugin with the new format', () async {
@@ -72,7 +72,7 @@
         },
       };
       await d.dir(appPath, [d.pubspec(pkg)]).create();
-      expectNoValidationError(flutterPluginFormat);
+      await expectValidation(flutterPluginFormat);
     });
   });
 
@@ -100,7 +100,7 @@
         },
       };
       await d.dir(appPath, [d.pubspec(pkg)]).create();
-      expectValidationError(flutterPluginFormat);
+      await expectValidation(flutterPluginFormat, errors: isNotEmpty);
     });
 
     test('is a flutter 1.9.0 plugin with old format', () async {
@@ -118,7 +118,7 @@
         },
       };
       await d.dir(appPath, [d.pubspec(pkg)]).create();
-      expectValidationWarning(flutterPluginFormat);
+      await expectValidation(flutterPluginFormat, warnings: isNotEmpty);
     });
 
     test('is a flutter 1.9.0 plugin with new format', () async {
@@ -139,7 +139,7 @@
         },
       };
       await d.dir(appPath, [d.pubspec(pkg)]).create();
-      expectValidationError(flutterPluginFormat);
+      await expectValidation(flutterPluginFormat, errors: isNotEmpty);
     });
 
     test(
@@ -161,7 +161,7 @@
         },
       };
       await d.dir(appPath, [d.pubspec(pkg)]).create();
-      expectValidationError(flutterPluginFormat);
+      await expectValidation(flutterPluginFormat, errors: isNotEmpty);
     });
 
     test('is a non-flutter package with using the new format', () async {
@@ -179,7 +179,7 @@
         },
       };
       await d.dir(appPath, [d.pubspec(pkg)]).create();
-      expectValidationError(flutterPluginFormat);
+      await expectValidation(flutterPluginFormat, errors: isNotEmpty);
     });
 
     test('is a flutter 1.8.0 plugin with new format', () async {
@@ -200,7 +200,7 @@
         },
       };
       await d.dir(appPath, [d.pubspec(pkg)]).create();
-      expectValidationError(flutterPluginFormat);
+      await expectValidation(flutterPluginFormat, errors: isNotEmpty);
     });
 
     test('is a flutter 1.9.999 plugin with new format', () async {
@@ -221,7 +221,7 @@
         },
       };
       await d.dir(appPath, [d.pubspec(pkg)]).create();
-      expectValidationError(flutterPluginFormat);
+      await expectValidation(flutterPluginFormat, errors: isNotEmpty);
     });
   });
 }
diff --git a/test/validator/language_version_test.dart b/test/validator/language_version_test.dart
index 304b68a..94237c6 100644
--- a/test/validator/language_version_test.dart
+++ b/test/validator/language_version_test.dart
@@ -30,34 +30,33 @@
     ])
   ]).create();
   await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '2.7.0'});
-  print(await d.file('.dart_tool/package_config.json').read());
 }
 
 void main() {
   group('should consider a package valid if it', () {
     test('has no library-level language version annotations', () async {
       await setup(sdkConstraint: '>=2.4.0 <3.0.0');
-      expectNoValidationError(validator);
+      await expectValidation(validator);
     });
 
     test('opts in to older language versions', () async {
       await setup(
           sdkConstraint: '>=2.4.0 <3.0.0', libraryLanguageVersion: '2.0');
       await d.dir(appPath, []).create();
-      expectNoValidationError(validator);
+      await expectValidation(validator);
     });
     test('opts in to same language versions', () async {
       await setup(
           sdkConstraint: '>=2.4.0 <3.0.0', libraryLanguageVersion: '2.4');
       await d.dir(appPath, []).create();
-      expectNoValidationError(validator);
+      await expectValidation(validator);
     });
 
     test('opts in to older language version, with non-range constraint',
         () async {
       await setup(sdkConstraint: '2.7.0', libraryLanguageVersion: '2.3');
       await d.dir(appPath, []).create();
-      expectNoValidationError(validator);
+      await expectValidation(validator);
     });
   });
 
@@ -65,11 +64,11 @@
     test('opts in to a newer version.', () async {
       await setup(
           sdkConstraint: '>=2.4.1 <3.0.0', libraryLanguageVersion: '2.5');
-      expectValidationError(validator);
+      await expectValidation(validator, errors: isNotEmpty);
     });
     test('opts in to a newer version, with non-range constraint.', () async {
       await setup(sdkConstraint: '2.7.0', libraryLanguageVersion: '2.8');
-      expectValidationError(validator);
+      await expectValidation(validator, errors: isNotEmpty);
     });
   });
 }
diff --git a/test/validator/license_test.dart b/test/validator/license_test.dart
index eb77ef4..b1b7b23 100644
--- a/test/validator/license_test.dart
+++ b/test/validator/license_test.dart
@@ -20,13 +20,13 @@
   group('should consider a package valid if it', () {
     test('looks normal', () async {
       await d.validPackage.create();
-      expectNoValidationError(license);
+      await expectValidation(license);
     });
 
     test('has both LICENSE and UNLICENSE file', () async {
       await d.validPackage.create();
       await d.file(path.join(appPath, 'UNLICENSE'), '').create();
-      expectNoValidationError(license);
+      await expectValidation(license);
     });
   });
 
@@ -35,28 +35,28 @@
       await d.validPackage.create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
       await d.file(path.join(appPath, 'COPYING'), '').create();
-      expectValidationWarning(license);
+      await expectValidation(license, warnings: isNotEmpty);
     });
 
     test('has only an UNLICENSE file', () async {
       await d.validPackage.create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
       await d.file(path.join(appPath, 'UNLICENSE'), '').create();
-      expectValidationWarning(license);
+      await expectValidation(license, warnings: isNotEmpty);
     });
 
     test('has only a prefixed LICENSE file', () async {
       await d.validPackage.create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
       await d.file(path.join(appPath, 'MIT_LICENSE'), '').create();
-      expectValidationWarning(license);
+      await expectValidation(license, warnings: isNotEmpty);
     });
 
     test('has only a suffixed LICENSE file', () async {
       await d.validPackage.create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
       await d.file(path.join(appPath, 'LICENSE.md'), '').create();
-      expectValidationWarning(license);
+      await expectValidation(license, warnings: isNotEmpty);
     });
   });
 
@@ -64,21 +64,21 @@
     test('has no LICENSE file', () async {
       await d.validPackage.create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
-      expectValidationError(license);
+      await expectValidation(license, errors: isNotEmpty);
     });
 
     test('has a prefixed UNLICENSE file', () async {
       await d.validPackage.create();
       deleteEntry(path.join(d.sandbox, appPath, 'LICENSE'));
       await d.file(path.join(appPath, 'MIT_UNLICENSE'), '').create();
-      expectValidationError(license);
+      await expectValidation(license, errors: isNotEmpty);
     });
 
     test('has a .gitignored LICENSE file', () async {
       var repo = d.git(appPath, [d.file('.gitignore', 'LICENSE')]);
       await d.validPackage.create();
       await repo.create();
-      expectValidationError(license);
+      await expectValidation(license, errors: isNotEmpty);
     });
   });
 }
diff --git a/test/validator/name_test.dart b/test/validator/name_test.dart
index 5acd257..3ea3d04 100644
--- a/test/validator/name_test.dart
+++ b/test/validator/name_test.dart
@@ -20,7 +20,7 @@
   group('should consider a package valid if it', () {
     setUp(d.validPackage.create);
 
-    test('looks normal', () => expectNoValidationError(name));
+    test('looks normal', () => expectValidation(name));
 
     test('has dots in potential library names', () async {
       await d.dir(appPath, [
@@ -30,7 +30,7 @@
           d.file('test_pkg.g.dart', 'int j = 2;')
         ])
       ]).create();
-      expectNoValidationError(name);
+      await expectValidation(name);
     });
 
     test('has a name that starts with an underscore', () async {
@@ -38,7 +38,7 @@
         d.libPubspec('_test_pkg', '1.0.0'),
         d.dir('lib', [d.file('_test_pkg.dart', 'int i = 1;')])
       ]).create();
-      expectNoValidationError(name);
+      await expectValidation(name);
     });
   });
 
@@ -47,7 +47,7 @@
 
     test('has a package name that contains upper-case letters', () async {
       await d.dir(appPath, [d.libPubspec('TestPkg', '1.0.0')]).create();
-      expectValidationWarning(name);
+      await expectValidation(name, warnings: isNotEmpty);
     });
 
     test('has a single library named differently than the package', () async {
@@ -55,7 +55,7 @@
       await d.dir(appPath, [
         d.dir('lib', [d.file('best_pkg.dart', 'int i = 0;')])
       ]).create();
-      expectValidationWarning(name);
+      await expectValidation(name, warnings: isNotEmpty);
     });
   });
 }
diff --git a/test/validator/pubspec_field_test.dart b/test/validator/pubspec_field_test.dart
index fa6f85f..cdfb955 100644
--- a/test/validator/pubspec_field_test.dart
+++ b/test/validator/pubspec_field_test.dart
@@ -19,14 +19,14 @@
   group('should consider a package valid if it', () {
     setUp(d.validPackage.create);
 
-    test('looks normal', () => expectNoValidationError(pubspecField));
+    test('looks normal', () => expectValidation(pubspecField));
 
     test('has an HTTPS homepage URL', () async {
       var pkg = packageMap('test_pkg', '1.0.0');
       pkg['homepage'] = 'https://pub.dartlang.org';
       await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-      expectNoValidationError(pubspecField);
+      await expectValidation(pubspecField);
     });
 
     test('has an HTTPS repository URL instead of homepage', () async {
@@ -35,7 +35,7 @@
       pkg['repository'] = 'https://pub.dartlang.org';
       await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-      expectNoValidationError(pubspecField);
+      await expectValidation(pubspecField);
     });
 
     test('has an HTTPS documentation URL', () async {
@@ -43,7 +43,7 @@
       pkg['documentation'] = 'https://pub.dartlang.org';
       await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-      expectNoValidationError(pubspecField);
+      await expectValidation(pubspecField);
     });
   });
 
@@ -54,7 +54,7 @@
       pkg.remove('homepage');
       await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-      expectValidationWarning(pubspecField);
+      await expectValidation(pubspecField, warnings: isNotEmpty);
     });
   });
 
@@ -66,7 +66,7 @@
       pkg.remove('description');
       await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-      expectValidationError(pubspecField);
+      await expectValidation(pubspecField, errors: isNotEmpty);
     });
 
     test('has a non-string "homepage" field', () async {
@@ -74,7 +74,7 @@
       pkg['homepage'] = 12;
       await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-      expectValidationError(pubspecField);
+      await expectValidation(pubspecField, errors: isNotEmpty);
     });
 
     test('has a non-string "repository" field', () async {
@@ -82,7 +82,7 @@
       pkg['repository'] = 12;
       await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-      expectValidationError(pubspecField);
+      await expectValidation(pubspecField, errors: isNotEmpty);
     });
 
     test('has a non-string "description" field', () async {
@@ -90,7 +90,7 @@
       pkg['description'] = 12;
       await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-      expectValidationError(pubspecField);
+      await expectValidation(pubspecField, errors: isNotEmpty);
     });
 
     test('has a non-HTTP homepage URL', () async {
@@ -98,7 +98,7 @@
       pkg['homepage'] = 'file:///foo/bar';
       await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-      expectValidationError(pubspecField);
+      await expectValidation(pubspecField, errors: isNotEmpty);
     });
 
     test('has a non-HTTP documentation URL', () async {
@@ -106,7 +106,7 @@
       pkg['documentation'] = 'file:///foo/bar';
       await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-      expectValidationError(pubspecField);
+      await expectValidation(pubspecField, errors: isNotEmpty);
     });
 
     test('has a non-HTTP repository URL', () async {
@@ -114,7 +114,7 @@
       pkg['repository'] = 'file:///foo/bar';
       await d.dir(appPath, [d.pubspec(pkg)]).create();
 
-      expectValidationError(pubspecField);
+      await expectValidation(pubspecField, errors: isNotEmpty);
     });
   });
 }
diff --git a/test/validator/pubspec_test.dart b/test/validator/pubspec_test.dart
index 3e61b3a..3aa0ef2 100644
--- a/test/validator/pubspec_test.dart
+++ b/test/validator/pubspec_test.dart
@@ -14,16 +14,16 @@
   test('should consider a package valid if it has a pubspec', () async {
     await d.validPackage.create();
 
-    expectNoValidationError((entrypoint) => PubspecValidator(entrypoint));
+    await expectValidation((entrypoint) => PubspecValidator(entrypoint));
   });
 
-  test(
-      'should consider a package invalid if it has a .gitignored '
-      'pubspec', () async {
+  test('should consider a package invalid if it has a .gitignored pubspec',
+      () async {
     var repo = d.git(appPath, [d.file('.gitignore', 'pubspec.yaml')]);
     await d.validPackage.create();
     await repo.create();
 
-    expectValidationError((entrypoint) => PubspecValidator(entrypoint));
+    await expectValidation((entrypoint) => PubspecValidator(entrypoint),
+        errors: isNotEmpty);
   });
 }
diff --git a/test/validator/readme_test.dart b/test/validator/readme_test.dart
index 7a45278..8020e1e 100644
--- a/test/validator/readme_test.dart
+++ b/test/validator/readme_test.dart
@@ -22,7 +22,7 @@
   group('should consider a package valid if it', () {
     test('looks normal', () async {
       await d.validPackage.create();
-      expectNoValidationError(readme);
+      await expectValidation(readme);
     });
 
     test('has a non-primary readme with invalid utf-8', () async {
@@ -30,7 +30,7 @@
       await d.dir(appPath, [
         d.file('README.x.y.z', [192])
       ]).create();
-      expectNoValidationError(readme);
+      await expectValidation(readme);
     });
 
     test('has a gitignored README with invalid utf-8', () async {
@@ -40,7 +40,7 @@
         d.file('.gitignore', 'README')
       ]);
       await repo.create();
-      expectNoValidationError(readme);
+      await expectValidation(readme);
     });
   });
 
@@ -49,13 +49,13 @@
       await d.validPackage.create();
 
       deleteEntry(p.join(d.sandbox, 'myapp/README.md'));
-      expectValidationWarning(readme);
+      await expectValidation(readme, warnings: isNotEmpty);
     });
 
     test('has only a .gitignored README', () async {
       await d.validPackage.create();
       await d.git(appPath, [d.file('.gitignore', 'README.md')]).create();
-      expectValidationWarning(readme);
+      await expectValidation(readme, warnings: isNotEmpty);
     });
 
     test('has a primary README with invalid utf-8', () async {
@@ -63,28 +63,28 @@
       await d.dir(appPath, [
         d.file('README', [192])
       ]).create();
-      expectValidationWarning(readme);
+      await expectValidation(readme, warnings: isNotEmpty);
     });
 
     test('has only a non-primary readme', () async {
       await d.validPackage.create();
       deleteEntry(p.join(d.sandbox, 'myapp/README.md'));
       await d.dir(appPath, [d.file('README.whatever')]).create();
-      expectValidationWarning(readme);
+      await expectValidation(readme, warnings: isNotEmpty);
     });
 
     test('Uses only deprecated readme name .markdown', () async {
       await d.validPackage.create();
       deleteEntry(p.join(d.sandbox, 'myapp/README.md'));
       await d.dir(appPath, [d.file('README.markdown')]).create();
-      expectValidationWarning(readme);
+      await expectValidation(readme, warnings: isNotEmpty);
     });
 
     test('Uses only deprecated readme name .mdown', () async {
       await d.validPackage.create();
       deleteEntry(p.join(d.sandbox, 'myapp/README.md'));
       await d.dir(appPath, [d.file('README.mdown')]).create();
-      expectValidationWarning(readme);
+      await expectValidation(readme, warnings: isNotEmpty);
     });
   });
 }
diff --git a/test/validator/relative_version_numbering_test.dart b/test/validator/relative_version_numbering_test.dart
new file mode 100644
index 0000000..a723706
--- /dev/null
+++ b/test/validator/relative_version_numbering_test.dart
@@ -0,0 +1,363 @@
+// 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 'package:pub/src/entrypoint.dart';
+import 'package:pub/src/validator.dart';
+import 'package:pub/src/validator/relative_version_numbering.dart';
+
+import '../descriptor.dart' as d;
+import '../test_pub.dart';
+import 'utils.dart';
+
+Validator validator(Entrypoint entrypoint) =>
+    RelativeVersionNumberingValidator(entrypoint, globalPackageServer.url);
+
+Future<void> setup({String sdkConstraint}) async {
+  await d.validPackage.create();
+  await d.dir(appPath, [
+    d.pubspec({
+      'name': 'test_pkg',
+      'version': '1.0.0',
+      'environment': {'sdk': sdkConstraint},
+    }),
+  ]).create();
+
+  await pubGet(environment: {'_PUB_TEST_SDK_VERSION': '2.10.0'});
+  print(await d.file('.dart_tool/package_config.json').read());
+}
+
+void main() {
+  group('should consider a package valid if it', () {
+    test('is not opting in to null-safety with previous non-null-safe version',
+        () async {
+      await servePackages(
+        (server) => server.serve(
+          'test_pkg',
+          '0.0.1',
+          pubspec: {
+            'environment': {'sdk': '>=2.9.0<3.0.0'}
+          },
+        ),
+      );
+
+      await setup(sdkConstraint: '>=2.9.0 <3.0.0');
+      await expectValidation(validator);
+    });
+
+    test(
+        'is not opting in to null-safety with previous non-null-safe version. '
+        'Even with a later null-safe version', () async {
+      await servePackages(
+        (server) => server
+          ..serve(
+            'test_pkg',
+            '0.0.1',
+            pubspec: {
+              'environment': {'sdk': '>=2.9.0<3.0.0'}
+            },
+          )
+          ..serve(
+            'test_pkg',
+            '2.0.0',
+            pubspec: {
+              'environment': {'sdk': '>=2.10.0<3.0.0'}
+            },
+          ),
+      );
+
+      await setup(sdkConstraint: '>=2.9.0 <3.0.0');
+      await expectValidation(validator);
+    });
+
+    test(
+        'is not opting in to null-safety with previous non-null-safe version. '
+        'Even with an in-between null-safe prerelease', () async {
+      await servePackages(
+        (server) => server
+          ..serve(
+            'test_pkg',
+            '0.0.1',
+            pubspec: {
+              'environment': {'sdk': '>=2.9.0<3.0.0'}
+            },
+          )
+          ..serve(
+            'test_pkg',
+            '0.0.2-dev',
+            pubspec: {
+              'environment': {'sdk': '>=2.10.0<3.0.0'}
+            },
+          ),
+      );
+
+      await setup(sdkConstraint: '>=2.9.0 <3.0.0');
+      await expectValidation(validator);
+    });
+
+    test('is opting in to null-safety with previous null-safe version',
+        () async {
+      await servePackages(
+        (server) => server.serve(
+          'test_pkg',
+          '0.0.1',
+          pubspec: {
+            'environment': {'sdk': '>=2.10.0<3.0.0'}
+          },
+        ),
+      );
+
+      await setup(sdkConstraint: '>=2.10.0 <3.0.0');
+      await expectValidation(validator);
+    });
+
+    test(
+        'is opting in to null-safety using a pre-release of 2.10.0 '
+        'with previous null-safe version', () async {
+      await servePackages(
+        (server) => server.serve(
+          'test_pkg',
+          '0.0.1',
+          pubspec: {
+            'environment': {'sdk': '>=2.10.0<3.0.0'}
+          },
+        ),
+      );
+
+      await setup(sdkConstraint: '>=2.10.0-dev <3.0.0');
+      await expectValidation(validator);
+    });
+
+    test(
+        'is opting in to null-safety with previous null-safe version. '
+        'Even with a later non-null-safe version', () async {
+      await servePackages(
+        (server) => server
+          ..serve(
+            'test_pkg',
+            '0.0.1',
+            pubspec: {
+              'environment': {'sdk': '>=2.10.0<3.0.0'}
+            },
+          )
+          ..serve(
+            'test_pkg',
+            '2.0.1',
+            pubspec: {
+              'environment': {'sdk': '>=2.9.0<3.0.0'}
+            },
+          ),
+      );
+
+      await setup(sdkConstraint: '>=2.10.0 <3.0.0');
+      await expectValidation(validator);
+    });
+
+    test(
+        'is opting in to null-safety with previous null-safe version. '
+        'Even with an in-between non-null-safe prerelease', () async {
+      await servePackages(
+        (server) => server
+          ..serve(
+            'test_pkg',
+            '0.0.1',
+            pubspec: {
+              'environment': {'sdk': '>=2.10.0<3.0.0'}
+            },
+          )
+          ..serve(
+            'test_pkg',
+            '0.0.2-dev',
+            pubspec: {
+              'environment': {'sdk': '>=2.9.0<3.0.0'}
+            },
+          ),
+      );
+
+      await setup(sdkConstraint: '>=2.10.0 <3.0.0');
+      await expectValidation(validator);
+    });
+
+    test('is opting in to null-safety with no existing versions', () async {
+      await setup(sdkConstraint: '>=2.10.0 <3.0.0');
+      await servePackages((x) => x);
+      await expectValidation(validator);
+    });
+
+    test(
+        'is opting in to null-safety with no existing versions. '
+        'Even with an in-between non-null-safe prerelease', () async {
+      await setup(sdkConstraint: '>=2.10.0 <3.0.0');
+      await servePackages(
+        (server) => server.serve(
+          'test_pkg',
+          '0.0.2-dev',
+          pubspec: {
+            'environment': {'sdk': '>=2.9.0<3.0.0'}
+          },
+        ),
+      );
+      await expectValidation(validator);
+    });
+
+    test('is not opting in to null-safety with no existing versions', () async {
+      await setup(sdkConstraint: '>=2.9.0 <3.0.0');
+      await servePackages((x) => x);
+
+      await expectValidation(validator);
+    });
+
+    test(
+        'is not opting in to null-safety with no existing versions. '
+        'Even with an in-between null-safe prerelease', () async {
+      await setup(sdkConstraint: '>=2.9.0 <3.0.0');
+      await servePackages(
+        (server) => server.serve(
+          'test_pkg',
+          '0.0.2-dev',
+          pubspec: {
+            'environment': {'sdk': '>=2.10.0<3.0.0'}
+          },
+        ),
+      );
+
+      await expectValidation(validator);
+    });
+  });
+
+  group('should warn if ', () {
+    test('opts in to null-safety, with previous version not-null-safe',
+        () async {
+      await servePackages(
+        (server) => server.serve(
+          'test_pkg',
+          '0.0.1',
+          pubspec: {
+            'environment': {'sdk': '>=2.9.0<3.0.0'}
+          },
+        ),
+      );
+
+      await setup(sdkConstraint: '>=2.10.0 <3.0.0');
+      await expectValidation(validator, hints: isNotEmpty);
+    });
+
+    test(
+        'opts in to null-safety, with previous version not-null-safe. '
+        'Even with a later null-safe version', () async {
+      await servePackages(
+        (server) => server
+          ..serve(
+            'test_pkg',
+            '0.0.1',
+            pubspec: {
+              'environment': {'sdk': '>=2.9.0<3.0.0'}
+            },
+          )
+          ..serve(
+            'test_pkg',
+            '2.0.0',
+            pubspec: {
+              'environment': {'sdk': '>=2.10.0<3.0.0'}
+            },
+          ),
+      );
+
+      await setup(sdkConstraint: '>=2.10.0 <3.0.0');
+      await expectValidation(validator, hints: isNotEmpty);
+    });
+
+    test(
+        'opts in to null-safety, with previous version not-null-safe. '
+        'Even with an in-between non-null-safe prerelease', () async {
+      await servePackages(
+        (server) => server
+          ..serve(
+            'test_pkg',
+            '0.0.1',
+            pubspec: {
+              'environment': {'sdk': '>=2.9.0<3.0.0'}
+            },
+          )
+          ..serve(
+            'test_pkg',
+            '0.0.2-dev',
+            pubspec: {
+              'environment': {'sdk': '>=2.10.0<3.0.0'}
+            },
+          ),
+      );
+
+      await setup(sdkConstraint: '>=2.10.0 <3.0.0');
+      await expectValidation(validator, hints: isNotEmpty);
+    });
+
+    test('is not opting in to null-safety with previous null-safe version',
+        () async {
+      await servePackages(
+        (server) => server.serve(
+          'test_pkg',
+          '0.0.1',
+          pubspec: {
+            'environment': {'sdk': '>=2.10.0<3.0.0'}
+          },
+        ),
+      );
+
+      await setup(sdkConstraint: '>=2.9.0 <3.0.0');
+      await expectValidation(validator, hints: isNotEmpty);
+    });
+
+    test(
+        'is not opting in to null-safety with previous null-safe version. '
+        'Even with a later non-null-safe version', () async {
+      await servePackages(
+        (server) => server
+          ..serve(
+            'test_pkg',
+            '0.0.1',
+            pubspec: {
+              'environment': {'sdk': '>=2.10.0<3.0.0'}
+            },
+          )
+          ..serve(
+            'test_pkg',
+            '2.0.0',
+            pubspec: {
+              'environment': {'sdk': '>=2.9.0<3.0.0'}
+            },
+          ),
+      );
+
+      await setup(sdkConstraint: '>=2.9.0 <3.0.0');
+      await expectValidation(validator, hints: isNotEmpty);
+    });
+
+    test(
+        'is not opting in to null-safety with previous null-safe version. '
+        'Even with an in-between not null-safe prerelease', () async {
+      await servePackages(
+        (server) => server
+          ..serve(
+            'test_pkg',
+            '0.0.1',
+            pubspec: {
+              'environment': {'sdk': '>=2.10.0<3.0.0'}
+            },
+          )
+          ..serve(
+            'test_pkg',
+            '0.0.2-dev',
+            pubspec: {
+              'environment': {'sdk': '>=2.9.0<3.0.0'}
+            },
+          ),
+      );
+
+      await setup(sdkConstraint: '>=2.9.0 <3.0.0');
+      await expectValidation(validator, hints: isNotEmpty);
+    });
+  });
+}
diff --git a/test/validator/sdk_constraint_test.dart b/test/validator/sdk_constraint_test.dart
index d610bb8..b3296b5 100644
--- a/test/validator/sdk_constraint_test.dart
+++ b/test/validator/sdk_constraint_test.dart
@@ -19,20 +19,20 @@
   group('should consider a package valid if it', () {
     test('has no SDK constraint', () async {
       await d.validPackage.create();
-      expectNoValidationError(sdkConstraint);
+      await expectValidation(sdkConstraint);
     });
 
     test('has an SDK constraint without ^', () async {
       await d.dir(appPath,
           [d.libPubspec('test_pkg', '1.0.0', sdk: '>=1.8.0 <2.0.0')]).create();
-      expectNoValidationError(sdkConstraint);
+      await expectValidation(sdkConstraint);
     });
 
     test('depends on a pre-release Dart SDK from a pre-release', () async {
       await d.dir(appPath, [
         d.libPubspec('test_pkg', '1.0.0-dev.1', sdk: '>=1.8.0-dev.1 <2.0.0')
       ]).create();
-      expectNoValidationError(sdkConstraint);
+      await expectValidation(sdkConstraint);
     });
 
     test(
@@ -45,7 +45,7 @@
           'environment': {'sdk': '>=1.19.0 <2.0.0', 'flutter': '^1.2.3'}
         })
       ]).create();
-      expectNoValidationError(sdkConstraint);
+      await expectValidation(sdkConstraint);
     });
 
     test(
@@ -58,7 +58,7 @@
           'environment': {'sdk': '>=2.0.0-dev.51.0 <2.0.0', 'fuchsia': '^1.2.3'}
         })
       ]).create();
-      expectNoValidationError(sdkConstraint);
+      await expectValidation(sdkConstraint);
     });
   });
 
@@ -66,29 +66,23 @@
     test('has an SDK constraint with ^', () async {
       await d.dir(
           appPath, [d.libPubspec('test_pkg', '1.0.0', sdk: '^1.8.0')]).create();
-      expect(
-          validatePackage(sdkConstraint),
-          completion(
-              pairOf(anyElement(contains('">=1.8.0 <2.0.0"')), isEmpty)));
+      await expectValidation(sdkConstraint,
+          errors: anyElement(contains('">=1.8.0 <2.0.0"')));
     });
 
     test('has no upper bound SDK constraint', () async {
       await d.dir(appPath,
           [d.libPubspec('test_pkg', '1.0.0', sdk: '>=1.8.0')]).create();
-      expect(
-          validatePackage(sdkConstraint),
-          completion(pairOf(
-              anyElement(contains('should have an upper bound constraint')),
-              isEmpty)));
+      await expectValidation(sdkConstraint,
+          errors:
+              anyElement(contains('should have an upper bound constraint')));
     });
 
     test('has no SDK constraint', () async {
       await d.dir(appPath, [d.libPubspec('test_pkg', '1.0.0')]).create();
-      expect(
-          validatePackage(sdkConstraint),
-          completion(pairOf(
-              anyElement(contains('should have an upper bound constraint')),
-              isEmpty)));
+      await expectValidation(sdkConstraint,
+          errors:
+              anyElement(contains('should have an upper bound constraint')));
     });
 
     test(
@@ -101,10 +95,8 @@
           'environment': {'sdk': '>=1.18.0 <1.50.0', 'flutter': '^1.2.3'}
         })
       ]).create();
-      expect(
-          validatePackage(sdkConstraint),
-          completion(
-              pairOf(anyElement(contains('">=1.19.0 <1.50.0"')), isEmpty)));
+      await expectValidation(sdkConstraint,
+          errors: anyElement(contains('">=1.19.0 <1.50.0"')));
     });
 
     test('has a Flutter SDK constraint with no SDK constraint', () async {
@@ -115,10 +107,8 @@
           'environment': {'flutter': '^1.2.3'}
         })
       ]).create();
-      expect(
-          validatePackage(sdkConstraint),
-          completion(
-              pairOf(anyElement(contains('">=1.19.0 <2.0.0"')), isEmpty)));
+      await expectValidation(sdkConstraint,
+          errors: anyElement(contains('">=1.19.0 <2.0.0"')));
     });
 
     test(
@@ -131,10 +121,8 @@
           'environment': {'sdk': '>=2.0.0-dev.50.0 <2.0.0', 'fuchsia': '^1.2.3'}
         })
       ]).create();
-      expect(
-          validatePackage(sdkConstraint),
-          completion(
-              pairOf(anyElement(contains('">=2.0.0 <3.0.0"')), isEmpty)));
+      await expectValidation(sdkConstraint,
+          errors: anyElement(contains('">=2.0.0 <3.0.0"')));
     });
 
     test('has a Fuchsia SDK constraint with no SDK constraint', () async {
@@ -145,26 +133,17 @@
           'environment': {'fuchsia': '^1.2.3'}
         })
       ]).create();
-      expect(
-          validatePackage(sdkConstraint),
-          completion(
-              pairOf(anyElement(contains('">=2.0.0 <3.0.0"')), isEmpty)));
+      await expectValidation(sdkConstraint,
+          errors: anyElement(contains('">=2.0.0 <3.0.0"')));
     });
 
     test('depends on a pre-release sdk from a non-pre-release', () async {
       await d.dir(appPath, [
         d.libPubspec('test_pkg', '1.0.0', sdk: '>=1.8.0-dev.1 <2.0.0')
       ]).create();
-      expect(
-        validatePackage(sdkConstraint),
-        completion(
-          pairOf(
-            isEmpty,
-            anyElement(contains(
-                'consider publishing the package as a pre-release instead')),
-          ),
-        ),
-      );
+      await expectValidation(sdkConstraint,
+          warnings: anyElement(contains(
+              'consider publishing the package as a pre-release instead')));
     });
   });
 }
diff --git a/test/validator/size_test.dart b/test/validator/size_test.dart
index 838b082..59a967f 100644
--- a/test/validator/size_test.dart
+++ b/test/validator/size_test.dart
@@ -17,17 +17,19 @@
   return (entrypoint) => SizeValidator(entrypoint, Future.value(size));
 }
 
-void expectSizeValidationError(Matcher matcher) {
-  expect(validatePackage(size(100 * math.pow(2, 20) + 1)),
-      completion(pairOf(contains(matcher), anything)));
+Future<void> expectSizeValidationError(Matcher matcher) async {
+  await expectValidation(
+    size(100 * math.pow(2, 20) + 1),
+    errors: contains(matcher),
+  );
 }
 
 void main() {
   test('considers a package valid if it is <= 100 MB', () async {
     await d.validPackage.create();
 
-    expectNoValidationError(size(100));
-    expectNoValidationError(size(100 * math.pow(2, 20)));
+    await expectValidation(size(100));
+    await expectValidation(size(100 * math.pow(2, 20)));
   });
 
   group('considers a package invalid if it is more than 100 MB', () {
@@ -35,7 +37,7 @@
         () async {
       await d.validPackage.create();
 
-      expectSizeValidationError(
+      await expectSizeValidationError(
           equals('Your package is 100.0 MB. Hosted packages must '
               'be smaller than 100 MB.'));
     });
@@ -44,7 +46,7 @@
       await d.validPackage.create();
       await d.dir(appPath, [d.file('.gitignore', 'ignored')]).create();
 
-      expectSizeValidationError(allOf(
+      await expectSizeValidationError(allOf(
           contains('Hosted packages must be smaller than 100 MB.'),
           contains('Your .gitignore has no effect since your project '
               'does not appear to be in version control.')));
@@ -54,7 +56,7 @@
       await d.validPackage.create();
       await d.git(appPath).create();
 
-      expectSizeValidationError(allOf(
+      await expectSizeValidationError(allOf(
           contains('Hosted packages must be smaller than 100 MB.'),
           contains('Consider adding a .gitignore to avoid including '
               'temporary files.')));
@@ -64,7 +66,7 @@
       await d.validPackage.create();
       await d.git(appPath, [d.file('.gitignore', 'ignored')]).create();
 
-      expectSizeValidationError(
+      await expectSizeValidationError(
           equals('Your package is 100.0 MB. Hosted packages must '
               'be smaller than 100 MB.'));
     });
diff --git a/test/validator/strict_dependencies_test.dart b/test/validator/strict_dependencies_test.dart
index 3c7d649..9a68b46 100644
--- a/test/validator/strict_dependencies_test.dart
+++ b/test/validator/strict_dependencies_test.dart
@@ -19,7 +19,7 @@
   group('should consider a package valid if it', () {
     setUp(d.validPackage.create);
 
-    test('looks normal', () => expectNoValidationError(strictDeps));
+    test('looks normal', () => expectValidation(strictDeps));
 
     test('declares an "import" as a dependency in lib/', () async {
       await d.dir(appPath, [
@@ -32,7 +32,7 @@
         ]),
       ]).create();
 
-      expectNoValidationError(strictDeps);
+      await expectValidation(strictDeps);
     });
 
     test('declares an "export" as a dependency in lib/', () async {
@@ -46,7 +46,7 @@
         ]),
       ]).create();
 
-      expectNoValidationError(strictDeps);
+      await expectValidation(strictDeps);
     });
 
     test('declares an "import" as a dependency in bin/', () async {
@@ -60,7 +60,7 @@
         ]),
       ]).create();
 
-      expectNoValidationError(strictDeps);
+      await expectValidation(strictDeps);
     });
 
     for (var port in ['import', 'export']) {
@@ -87,7 +87,7 @@
               ]),
             ]).create();
 
-            expectNoValidationError(strictDeps);
+            await expectValidation(strictDeps);
           });
         }
       }
@@ -100,7 +100,7 @@
         import 'dart:typed_data';
       ''').create();
 
-      expectNoValidationError(strictDeps);
+      await expectValidation(strictDeps);
     });
 
     test('imports itself', () async {
@@ -108,7 +108,7 @@
         import 'package:test_pkg/test_pkg.dart';
       ''').create();
 
-      expectNoValidationError(strictDeps);
+      await expectValidation(strictDeps);
     });
 
     test('has a relative import', () async {
@@ -116,7 +116,7 @@
         import 'some/relative/path.dart';
       ''').create();
 
-      expectNoValidationError(strictDeps);
+      await expectValidation(strictDeps);
     });
 
     test('has an absolute import', () async {
@@ -124,7 +124,7 @@
         import 'file://shared/some/library.dart';
       ''').create();
 
-      expectNoValidationError(strictDeps);
+      await expectValidation(strictDeps);
     });
 
     test('has a parse error preventing reading directives', () async {
@@ -132,7 +132,7 @@
         import not_supported_keyword 'dart:async';
       ''').create();
 
-      expectNoValidationError(strictDeps);
+      await expectValidation(strictDeps);
     });
 
     test('has a top-level Dart file with an invalid dependency', () async {
@@ -140,7 +140,7 @@
         import 'package:';
       ''').create();
 
-      expectNoValidationError(strictDeps);
+      await expectValidation(strictDeps);
     });
 
     test('has a Dart-like file with an invalid dependency', () async {
@@ -148,7 +148,7 @@
         import 'package:';
       ''').create();
 
-      expectNoValidationError(strictDeps);
+      await expectValidation(strictDeps);
     });
 
     test('has analysis_options.yaml that excludes files', () async {
@@ -178,7 +178,7 @@
 '''),
       ]).create();
 
-      expectNoValidationError(strictDeps);
+      await expectValidation(strictDeps);
     });
   });
 
@@ -190,7 +190,7 @@
         import 'package:silly_monkey/silly_monkey.dart';
       ''').create();
 
-      expectValidationError(strictDeps);
+      await expectValidation(strictDeps, errors: isNotEmpty);
     });
 
     test('does not declare an "export" as a dependency', () async {
@@ -198,7 +198,7 @@
         export 'package:silly_monkey/silly_monkey.dart';
       ''').create();
 
-      expectValidationError(strictDeps);
+      await expectValidation(strictDeps, errors: isNotEmpty);
     });
 
     test('has an invalid URI', () async {
@@ -206,7 +206,7 @@
         import 'package:/';
       ''').create();
 
-      expectValidationError(strictDeps);
+      await expectValidation(strictDeps, errors: isNotEmpty);
     });
 
     for (var port in ['import', 'export']) {
@@ -222,7 +222,7 @@
             ]),
           ]).create();
 
-          expectValidationError(strictDeps);
+          await expectValidation(strictDeps, errors: isNotEmpty);
         });
       }
     }
@@ -240,7 +240,7 @@
             ]),
           ]).create();
 
-          expectValidationWarning(strictDeps);
+          await expectValidation(strictDeps, warnings: isNotEmpty);
         });
       }
     }
@@ -255,7 +255,7 @@
           ]),
         ]).create();
 
-        expectValidationError(strictDeps);
+        await expectValidation(strictDeps, errors: isNotEmpty);
       });
 
       test('"package:silly_monkey"', () async {
@@ -269,7 +269,7 @@
           ]),
         ]).create();
 
-        expectValidationError(strictDeps);
+        await expectValidation(strictDeps, errors: isNotEmpty);
       });
 
       test('"package:/"', () async {
@@ -281,7 +281,7 @@
           ]),
         ]).create();
 
-        expectValidationError(strictDeps);
+        await expectValidation(strictDeps, errors: isNotEmpty);
       });
 
       test('"package:/]"', () async {
@@ -293,7 +293,7 @@
           ]),
         ]).create();
 
-        expectValidationError(strictDeps);
+        await expectValidation(strictDeps, errors: isNotEmpty);
       });
     });
   });
diff --git a/test/validator/utils.dart b/test/validator/utils.dart
index aae403a..6e3a0e3 100644
--- a/test/validator/utils.dart
+++ b/test/validator/utils.dart
@@ -6,14 +6,10 @@
 
 import '../test_pub.dart';
 
-void expectNoValidationError(ValidatorCreator fn) {
-  expect(validatePackage(fn), completion(pairOf(isEmpty, isEmpty)));
-}
-
-void expectValidationError(ValidatorCreator fn) {
-  expect(validatePackage(fn), completion(pairOf(isNot(isEmpty), anything)));
-}
-
-void expectValidationWarning(ValidatorCreator fn) {
-  expect(validatePackage(fn), completion(pairOf(isEmpty, isNot(isEmpty))));
+Future<void> expectValidation(ValidatorCreator fn,
+    {hints, warnings, errors}) async {
+  final validator = await validatePackage(fn);
+  expect(validator.errors, errors ?? isEmpty);
+  expect(validator.warnings, warnings ?? isEmpty);
+  expect(validator.hints, hints ?? isEmpty);
 }