[coverage] Partial workspace support (#2095)

diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml
index 98e1bb4..997e33f 100644
--- a/.github/workflows/coverage.yaml
+++ b/.github/workflows/coverage.yaml
@@ -56,12 +56,7 @@
       fail-fast: false
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        sdk: [3.4, dev]
-        exclude:
-          # VM service times out on windows before Dart 3.5
-          # https://github.com/dart-lang/coverage/issues/490
-          - os: windows-latest
-            sdk: 3.4
+        sdk: [3.6, dev]
     steps:
       - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
       - uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c
@@ -89,7 +84,7 @@
         name: Install dependencies
         run: dart pub get
       - name: Collect and report coverage
-        run: dart run bin/test_with_coverage.dart --port=9292
+        run: dart run bin/test_with_coverage.dart
       - name: Upload coverage
         uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b
         with:
diff --git a/pkgs/coverage/CHANGELOG.md b/pkgs/coverage/CHANGELOG.md
index 6dc80a5..2a680ad 100644
--- a/pkgs/coverage/CHANGELOG.md
+++ b/pkgs/coverage/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 1.14.0
+
+- Require Dart ^3.6.0
+- Partial support for workspace packages in `test_wth_coverage`.
+- Deprecate `test_wth_coverage`'s `--package-name` flag, because it doesn't make
+  sense for workspaces.
+- Change the default `--port` to 0, allowing the VM to choose a free port.
+
 ## 1.13.1
 
 - Fix a bug where the VM service can be shut down while some coverage
diff --git a/pkgs/coverage/README.md b/pkgs/coverage/README.md
index 2963c6a..7a90c8e 100644
--- a/pkgs/coverage/README.md
+++ b/pkgs/coverage/README.md
@@ -43,6 +43,30 @@
 For more complicated use cases, where you want to control each of these stages,
 see the sections below.
 
+#### Workspaces
+
+package:coverage has partial support for
+[workspaces](https://dart.dev/tools/pub/workspaces). You can run
+`test_with_coverage` from the root of the workspace to collect coverage for all
+the tests in all the subpackages, but you must specify the test directories to
+run.
+
+For example, in a workspace with subpackages `pkgs/foo` and `pkgs/bar`, you
+could run the following command from the root directory of the workspace:
+
+```
+dart run coverage:test_with_coverage -- pkgs/foo/test pkgs/bar/test
+```
+
+This would output coverage to ./coverage/ as normal. An important caveat is that
+the working directory of the tests will be the workspace's root directory. So
+this approach won't work if your tests assume that they are being run from the
+subpackage directory.
+
+[Full support](https://github.com/dart-lang/tools/issues/2083) for workspaces
+will likely be added in a future version. This will mean you won't need to
+explicitly specify the test directories: `dart run coverage:test_with_coverage`
+
 #### Collecting coverage from the VM
 
 ```
diff --git a/pkgs/coverage/bin/test_with_coverage.dart b/pkgs/coverage/bin/test_with_coverage.dart
index c22e9b8..9f4884d 100644
--- a/pkgs/coverage/bin/test_with_coverage.dart
+++ b/pkgs/coverage/bin/test_with_coverage.dart
@@ -7,10 +7,8 @@
 
 import 'package:args/args.dart';
 import 'package:coverage/src/coverage_options.dart';
-import 'package:coverage/src/util.dart'
-    show StandardOutExtension, extractVMServiceUri;
+import 'package:coverage/src/util.dart';
 import 'package:meta/meta.dart';
-import 'package:package_config/package_config.dart';
 import 'package:path/path.dart' as path;
 
 import 'collect_coverage.dart' as collect_coverage;
@@ -19,37 +17,36 @@
 final _allProcesses = <Process>[];
 
 Future<void> _dartRun(List<String> args,
-    {void Function(String)? onStdout, String? workingDir}) async {
-  final process = await Process.start(
-    Platform.executable,
-    args,
-    workingDirectory: workingDir,
-  );
+    {required void Function(String) onStdout,
+    required void Function(String) onStderr}) async {
+  final process = await Process.start(Platform.executable, args);
   _allProcesses.add(process);
-  final broadStdout = process.stdout.asBroadcastStream();
-  broadStdout.listen(stdout.add);
-  if (onStdout != null) {
-    broadStdout.lines().listen(onStdout);
+
+  void listen(
+      Stream<List<int>> stream, IOSink sink, void Function(String) onLine) {
+    final broadStream = stream.asBroadcastStream();
+    broadStream.listen(sink.add);
+    broadStream.lines().listen(onLine);
   }
-  process.stderr.listen(stderr.add);
+
+  listen(process.stdout, stdout, onStdout);
+  listen(process.stderr, stderr, onStderr);
+
   final result = await process.exitCode;
   if (result != 0) {
     throw ProcessException(Platform.executable, args, '', result);
   }
 }
 
-Future<String?> _packageNameFromConfig(String packageDir) async {
-  final config = await findPackageConfig(Directory(packageDir));
-  return config?.packageOf(Uri.directory(packageDir))?.name;
+void _killSubprocessesAndExit(ProcessSignal signal) {
+  for (final process in _allProcesses) {
+    process.kill(signal);
+  }
+  exit(1);
 }
 
 void _watchExitSignal(ProcessSignal signal) {
-  signal.watch().listen((sig) {
-    for (final process in _allProcesses) {
-      process.kill(sig);
-    }
-    exit(1);
-  });
+  signal.watch().listen(_killSubprocessesAndExit);
 }
 
 ArgParser _createArgParser(CoverageOptions defaultOptions) => ArgParser()
@@ -61,10 +58,10 @@
   ..addOption(
     'package-name',
     help: 'Name of the package to test. '
-        'Deduced from --package if not provided.',
-    defaultsTo: defaultOptions.packageName,
+        'Deduced from --package if not provided. '
+        'DEPRECATED: use --scope-output',
   )
-  ..addOption('port', help: 'VM service port.', defaultsTo: '8181')
+  ..addOption('port', help: 'VM service port. Defaults to using any free port.')
   ..addOption(
     'out',
     defaultsTo: defaultOptions.outputDirectory,
@@ -93,13 +90,13 @@
       defaultsTo: defaultOptions.scopeOutput,
       help: 'restrict coverage results so that only scripts that start with '
           'the provided package path are considered. Defaults to the name of '
-          'the package under test.')
+          'the current package (including all subpackages, if this is a '
+          'workspace).')
   ..addFlag('help', abbr: 'h', negatable: false, help: 'Show this help.');
 
 class Flags {
   Flags(
     this.packageDir,
-    this.packageName,
     this.outDir,
     this.port,
     this.testScript,
@@ -111,7 +108,6 @@
   });
 
   final String packageDir;
-  final String packageName;
   final String outDir;
   final String port;
   final String testScript;
@@ -157,25 +153,23 @@
     fail('--package is not a valid directory.');
   }
 
-  final packageName = (args['package-name'] as String?) ??
-      await _packageNameFromConfig(packageDir);
-  if (packageName == null) {
+  final pubspecPath = getPubspecPath(packageDir);
+  if (!File(pubspecPath).existsSync()) {
     fail(
-      "Couldn't figure out package name from --package. Make sure this is a "
-      'package directory, or try passing --package-name explicitly.',
+      "Couldn't find $pubspecPath. Make sure this command is run in a "
+      'package directory, or pass --package to explicitly set the directory.',
     );
   }
 
   return Flags(
     packageDir,
-    packageName,
-    (args['out'] as String?) ?? path.join(packageDir, 'coverage'),
-    args['port'] as String,
-    args['test'] as String,
-    args['function-coverage'] as bool,
-    args['branch-coverage'] as bool,
-    args['scope-output'] as List<String>,
-    args['fail-under'] as String?,
+    args.option('out') ?? path.join(packageDir, 'coverage'),
+    args.option('port') ?? '0',
+    args.option('test')!,
+    args.flag('function-coverage'),
+    args.flag('branch-coverage'),
+    args.multiOption('scope-output'),
+    args.option('fail-under'),
     rest: args.rest,
   );
 }
@@ -215,11 +209,19 @@
         }
       }
     },
+    onStderr: (line) {
+      if (!serviceUriCompleter.isCompleted) {
+        if (line.contains('Could not start the VM service')) {
+          _killSubprocessesAndExit(ProcessSignal.sigkill);
+        }
+      }
+    },
   );
   final serviceUri = await serviceUriCompleter.future;
 
-  final scopes =
-      flags.scopeOutput.isEmpty ? [flags.packageName] : flags.scopeOutput;
+  final scopes = flags.scopeOutput.isEmpty
+      ? getAllWorkspaceNames(flags.packageDir)
+      : flags.scopeOutput;
   await collect_coverage.main([
     '--wait-paused',
     '--resume-isolates',
diff --git a/pkgs/coverage/dart_test.yaml b/pkgs/coverage/dart_test.yaml
new file mode 100644
index 0000000..f982167
--- /dev/null
+++ b/pkgs/coverage/dart_test.yaml
@@ -0,0 +1,5 @@
+tags:
+  # Tests that start subprocesses, so are slower and can be a bit flaky.
+  integration:
+    timeout: 2x
+    retry: 3
diff --git a/pkgs/coverage/lib/src/collect.dart b/pkgs/coverage/lib/src/collect.dart
index 76227ba..1bed28d 100644
--- a/pkgs/coverage/lib/src/collect.dart
+++ b/pkgs/coverage/lib/src/collect.dart
@@ -170,6 +170,7 @@
         isolateReport,
         includeDart,
         functionCoverage,
+        branchCoverage,
         coverableLineCache,
         scopedOutput);
     allCoverage.addAll(coverage);
@@ -244,6 +245,7 @@
     SourceReport report,
     bool includeDart,
     bool functionCoverage,
+    bool branchCoverage,
     Map<String, Set<int>>? coverableLineCache,
     Set<String> scopedOutput) async {
   final hitMaps = <Uri, HitMap>{};
@@ -262,7 +264,10 @@
     return scripts[scriptRef];
   }
 
-  HitMap getHitMap(Uri scriptUri) => hitMaps.putIfAbsent(scriptUri, HitMap.new);
+  HitMap getHitMap(Uri scriptUri) => hitMaps.putIfAbsent(
+      scriptUri,
+      () => HitMap.empty(
+          functionCoverage: functionCoverage, branchCoverage: branchCoverage));
 
   Future<void> processFunction(FuncRef funcRef) async {
     final func = await service.getObject(isolateRef.id!, funcRef.id!) as Func;
@@ -290,8 +295,7 @@
       return;
     }
     final hits = getHitMap(Uri.parse(script.uri!));
-    hits.funcHits ??= <int, int>{};
-    (hits.funcNames ??= <int, String>{})[line] = funcName;
+    hits.funcNames![line] = funcName;
   }
 
   for (var range in report.ranges!) {
@@ -385,13 +389,12 @@
       hits.funcHits?.putIfAbsent(line, () => 0);
     });
 
-    final branchCoverage = range.branchCoverage;
-    if (branchCoverage != null) {
-      hits.branchHits ??= <int, int>{};
-      forEachLine(branchCoverage.hits, (line) {
+    final branches = range.branchCoverage;
+    if (branchCoverage && branches != null) {
+      forEachLine(branches.hits, (line) {
         hits.branchHits!.increment(line);
       });
-      forEachLine(branchCoverage.misses, (line) {
+      forEachLine(branches.misses, (line) {
         hits.branchHits!.putIfAbsent(line, () => 0);
       });
     }
diff --git a/pkgs/coverage/lib/src/coverage_options.dart b/pkgs/coverage/lib/src/coverage_options.dart
index a15c31d..87790df 100644
--- a/pkgs/coverage/lib/src/coverage_options.dart
+++ b/pkgs/coverage/lib/src/coverage_options.dart
@@ -9,7 +9,6 @@
     required this.functionCoverage,
     required this.branchCoverage,
     required this.packageDirectory,
-    this.packageName,
     required this.testScript,
   });
 
@@ -40,8 +39,6 @@
       branchCoverage: options.optionalBool('branch_coverage') ??
           defaultOptions.branchCoverage,
       packageDirectory: packageDirectory,
-      packageName:
-          options.optionalString('package_name') ?? defaultOptions.packageName,
       testScript:
           options.optionalString('test_script') ?? defaultOptions.testScript,
     );
@@ -52,7 +49,6 @@
   final bool functionCoverage;
   final bool branchCoverage;
   final String packageDirectory;
-  final String? packageName;
   final String testScript;
 }
 
@@ -119,7 +115,6 @@
     functionCoverage: false,
     branchCoverage: false,
     packageDirectory: '.',
-    packageName: null,
     testScript: 'test',
   );
 }
diff --git a/pkgs/coverage/lib/src/formatter.dart b/pkgs/coverage/lib/src/formatter.dart
index a37df73..2680607 100644
--- a/pkgs/coverage/lib/src/formatter.dart
+++ b/pkgs/coverage/lib/src/formatter.dart
@@ -151,6 +151,20 @@
     );
     final buf = StringBuffer();
     for (final entry in entries) {
+      final source = resolver.resolve(entry.key);
+      if (source == null) {
+        continue;
+      }
+
+      if (!pathFilter(source)) {
+        continue;
+      }
+
+      final lines = await loader.load(source);
+      if (lines == null) {
+        continue;
+      }
+
       final v = entry.value;
       if (reportFuncs && v.funcHits == null) {
         throw StateError(
@@ -165,24 +179,12 @@
             'missing branch coverage information. Did you run '
             'collect_coverage with the --branch-coverage flag?');
       }
+
       final hits = reportFuncs
           ? v.funcHits!
           : reportBranches
               ? v.branchHits!
               : v.lineHits;
-      final source = resolver.resolve(entry.key);
-      if (source == null) {
-        continue;
-      }
-
-      if (!pathFilter(source)) {
-        continue;
-      }
-
-      final lines = await loader.load(source);
-      if (lines == null) {
-        continue;
-      }
       buf.writeln(source);
       for (var line = 1; line <= lines.length; line++) {
         var prefix = _prefix;
diff --git a/pkgs/coverage/lib/src/hitmap.dart b/pkgs/coverage/lib/src/hitmap.dart
index 4c3b468..133c24b 100644
--- a/pkgs/coverage/lib/src/hitmap.dart
+++ b/pkgs/coverage/lib/src/hitmap.dart
@@ -18,6 +18,15 @@
     this.branchHits,
   ]) : lineHits = lineHits ?? {};
 
+  /// Constructs an empty hitmap, optionally with function and branch coverage
+  /// tables.
+  HitMap.empty({bool functionCoverage = false, bool branchCoverage = false})
+      : this(
+            null,
+            functionCoverage ? <int, int>{} : null,
+            functionCoverage ? <int, String>{} : null,
+            branchCoverage ? <int, int>{} : null);
+
   /// Map from line to hit count for that line.
   final Map<int, int> lineHits;
 
diff --git a/pkgs/coverage/lib/src/util.dart b/pkgs/coverage/lib/src/util.dart
index cc7f584..74a1697 100644
--- a/pkgs/coverage/lib/src/util.dart
+++ b/pkgs/coverage/lib/src/util.dart
@@ -6,6 +6,8 @@
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:path/path.dart' as path;
+import 'package:pubspec_parse/pubspec_parse.dart';
 import 'package:vm_service/vm_service.dart';
 
 // TODO(cbracken) make generic
@@ -57,25 +59,6 @@
   return null;
 }
 
-/// Returns an open port by creating a temporary Socket
-Future<int> getOpenPort() async {
-  ServerSocket socket;
-
-  try {
-    socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
-  } catch (_) {
-    // try again v/ V6 only. Slight possibility that V4 is disabled
-    socket =
-        await ServerSocket.bind(InternetAddress.loopbackIPv6, 0, v6Only: true);
-  }
-
-  try {
-    return socket.port;
-  } finally {
-    await socket.close();
-  }
-}
-
 final muliLineIgnoreStart = RegExp(r'//\s*coverage:ignore-start[\w\d\s]*$');
 final muliLineIgnoreEnd = RegExp(r'//\s*coverage:ignore-end[\w\d\s]*$');
 final singleLineIgnore = RegExp(r'//\s*coverage:ignore-line[\w\d\s]*$');
@@ -184,3 +167,19 @@
 
 Future<List<IsolateRef>> getAllIsolates(VmService service) async =>
     (await service.getVM()).isolates ?? [];
+
+String getPubspecPath(String root) => path.join(root, 'pubspec.yaml');
+
+List<String> getAllWorkspaceNames(String packageRoot) =>
+    _getAllWorkspaceNames(packageRoot, <String>[]);
+
+List<String> _getAllWorkspaceNames(String packageRoot, List<String> results) {
+  final pubspecPath = getPubspecPath(packageRoot);
+  final yaml = File(pubspecPath).readAsStringSync();
+  final pubspec = Pubspec.parse(yaml, sourceUrl: Uri.file(pubspecPath));
+  results.add(pubspec.name);
+  for (final workspace in pubspec.workspace ?? <String>[]) {
+    _getAllWorkspaceNames(path.join(packageRoot, workspace), results);
+  }
+  return results;
+}
diff --git a/pkgs/coverage/pubspec.yaml b/pkgs/coverage/pubspec.yaml
index 46307b8..e4a229e 100644
--- a/pkgs/coverage/pubspec.yaml
+++ b/pkgs/coverage/pubspec.yaml
@@ -1,5 +1,5 @@
 name: coverage
-version: 1.13.1
+version: 1.14.0
 description: Coverage data manipulation and formatting
 repository: https://github.com/dart-lang/tools/tree/main/pkgs/coverage
 issue_tracker: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Acoverage
@@ -15,6 +15,7 @@
   meta: ^1.0.2
   package_config: ^2.0.0
   path: ^1.8.0
+  pubspec_parse: ^1.5.0
   source_maps: ^0.10.10
   stack_trace: ^1.10.0
   vm_service: '>=12.0.0 <16.0.0'
diff --git a/pkgs/coverage/test/collect_coverage_api_test.dart b/pkgs/coverage/test/collect_coverage_api_test.dart
index fd3de43..5ccedd8 100644
--- a/pkgs/coverage/test/collect_coverage_api_test.dart
+++ b/pkgs/coverage/test/collect_coverage_api_test.dart
@@ -2,6 +2,9 @@
 // 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.
 
+@Tags(['integration'])
+library;
+
 import 'dart:async';
 
 import 'package:coverage/coverage.dart';
@@ -140,10 +143,8 @@
     bool functionCoverage = false,
     bool branchCoverage = false,
     Map<String, Set<int>>? coverableLineCache}) async {
-  final openPort = await getOpenPort();
-
   // run the sample app, with the right flags
-  final sampleProcess = await runTestApp(openPort);
+  final sampleProcess = await runTestApp();
 
   final serviceUri = await serviceUriFromProcess(sampleProcess.stdoutStream());
   final isolateIdSet = isolateIds ? <String>{} : null;
diff --git a/pkgs/coverage/test/collect_coverage_config_test.dart b/pkgs/coverage/test/collect_coverage_config_test.dart
index 5f6c460..d2b6b58 100644
--- a/pkgs/coverage/test/collect_coverage_config_test.dart
+++ b/pkgs/coverage/test/collect_coverage_config_test.dart
@@ -1,3 +1,7 @@
+// Copyright (c) 2025, 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:coverage/src/coverage_options.dart';
 import 'package:path/path.dart' as path;
 import 'package:test/test.dart';
@@ -35,7 +39,6 @@
 
       expect(path.canonicalize(testCoverage.packageDir),
           path.canonicalize(defaults.packageDirectory));
-      expect(testCoverage.packageName, 'coverage');
       expect(path.canonicalize(testCoverage.outDir),
           path.canonicalize('coverage'));
       expect(testCoverage.testScript, defaults.testScript);
@@ -73,7 +76,6 @@
     // Verify test with coverage yaml values
     expect(path.canonicalize(testCoverage.packageDir),
         path.canonicalize('test/test_files'));
-    expect(testCoverage.packageName, 'My Dart Package');
     expect(path.canonicalize(testCoverage.outDir),
         path.canonicalize('var/coverage_data'));
     expect(testCoverage.testScript, 'test1');
@@ -102,7 +104,6 @@
       expect(collectedCoverage.functionCoverage, isFalse);
       expect(path.canonicalize(formattedCoverage.output!),
           path.canonicalize('var/coverage_data/custom_coverage/lcov.info'));
-      expect(testCoverage.packageName, 'Custom Dart Package');
       expect(testCoverage.scopeOutput, ['lib', 'test']);
     });
 
@@ -133,7 +134,6 @@
           path.canonicalize('test/test_coverage_options'));
 
       // Verify test with coverage yaml values
-      expect(testCoverage.packageName, 'coverage');
       expect(path.canonicalize(testCoverage.outDir),
           path.canonicalize('var/coverage_data/custom_lcov'));
       expect(testCoverage.testScript, 'custom_test');
@@ -178,7 +178,6 @@
       expect(formattedCoverage.packagePath, '../code_builder');
 
       // Verify test with coverage command line args
-      expect(testCoverage.packageName, 'test');
       expect(testCoverage.outDir, 'test_coverage.json');
       expect(testCoverage.testScript, 'test_test.dart');
       expect(testCoverage.functionCoverage, isTrue);
@@ -218,7 +217,6 @@
       expect(formattedCoverage.packagePath, '../cli_config');
 
       // Verify test with coverage command line args
-      expect(testCoverage.packageName, 'cli_config');
       expect(testCoverage.outDir, 'cli_config_coverage.json');
       expect(testCoverage.testScript, 'cli_config_test.dart');
       expect(testCoverage.functionCoverage, isTrue);
diff --git a/pkgs/coverage/test/collect_coverage_test.dart b/pkgs/coverage/test/collect_coverage_test.dart
index 476fee1..5758b5e 100644
--- a/pkgs/coverage/test/collect_coverage_test.dart
+++ b/pkgs/coverage/test/collect_coverage_test.dart
@@ -2,7 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-@Retry(3)
+@Tags(['integration'])
 library;
 
 import 'dart:async';
@@ -294,10 +294,8 @@
     bool functionCoverage, bool branchCoverage) async {
   expect(FileSystemEntity.isFileSync(testAppPath), isTrue);
 
-  final openPort = await getOpenPort();
-
   // Run the sample app with the right flags.
-  final sampleProcess = await runTestApp(openPort);
+  final sampleProcess = await runTestApp();
 
   // Capture the VM service URI.
   final serviceUri = await serviceUriFromProcess(sampleProcess.stdoutStream());
diff --git a/pkgs/coverage/test/config_file_locator_test.dart b/pkgs/coverage/test/config_file_locator_test.dart
index d46fc86..db131e8 100644
--- a/pkgs/coverage/test/config_file_locator_test.dart
+++ b/pkgs/coverage/test/config_file_locator_test.dart
@@ -1,3 +1,7 @@
+// Copyright (c) 2025, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
 import 'dart:io';
 import 'package:coverage/src/coverage_options.dart';
 import 'package:path/path.dart' as path;
diff --git a/pkgs/coverage/test/function_coverage_test.dart b/pkgs/coverage/test/function_coverage_test.dart
index 965d0d0..d8aaa74 100644
--- a/pkgs/coverage/test/function_coverage_test.dart
+++ b/pkgs/coverage/test/function_coverage_test.dart
@@ -2,6 +2,9 @@
 // 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.
 
+@Tags(['integration'])
+library;
+
 import 'dart:async';
 import 'dart:convert' show json;
 import 'dart:io';
@@ -74,14 +77,9 @@
 Future<String> _collectCoverage() async {
   expect(FileSystemEntity.isFileSync(_funcCovApp), isTrue);
 
-  final openPort = await getOpenPort();
-
   // Run the sample app with the right flags.
-  final sampleProcess = await TestProcess.start(Platform.resolvedExecutable, [
-    '--enable-vm-service=$openPort',
-    '--pause_isolates_on_exit',
-    _funcCovApp
-  ]);
+  final sampleProcess = await TestProcess.start(Platform.resolvedExecutable,
+      ['--enable-vm-service=0', '--pause_isolates_on_exit', _funcCovApp]);
 
   final serviceUri = await serviceUriFromProcess(sampleProcess.stdoutStream());
 
diff --git a/pkgs/coverage/test/lcov_test.dart b/pkgs/coverage/test/lcov_test.dart
index ca62117..a03612c 100644
--- a/pkgs/coverage/test/lcov_test.dart
+++ b/pkgs/coverage/test/lcov_test.dart
@@ -2,6 +2,9 @@
 // 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.
 
+@Tags(['integration'])
+library;
+
 import 'dart:async';
 import 'dart:io';
 
@@ -337,13 +340,10 @@
 Future<Map<String, HitMap>> _getHitMap() async {
   expect(FileSystemEntity.isFileSync(_sampleAppPath), isTrue);
 
-  // select service port.
-  final port = await getOpenPort();
-
   // start sample app.
   final sampleAppArgs = [
     '--pause-isolates-on-exit',
-    '--enable-vm-service=$port',
+    '--enable-vm-service=0',
     '--branch-coverage',
     _sampleAppPath
   ];
diff --git a/pkgs/coverage/test/run_and_collect_test.dart b/pkgs/coverage/test/run_and_collect_test.dart
index a538a88..0a45a44 100644
--- a/pkgs/coverage/test/run_and_collect_test.dart
+++ b/pkgs/coverage/test/run_and_collect_test.dart
@@ -2,6 +2,9 @@
 // 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.
 
+@Tags(['integration'])
+library;
+
 import 'package:coverage/coverage.dart';
 import 'package:path/path.dart' as p;
 import 'package:test/test.dart';
diff --git a/pkgs/coverage/test/test_coverage_options/pubspec.yaml b/pkgs/coverage/test/test_coverage_options/pubspec.yaml
new file mode 100644
index 0000000..9d55a4f
--- /dev/null
+++ b/pkgs/coverage/test/test_coverage_options/pubspec.yaml
@@ -0,0 +1 @@
+name: test_coverage_options
diff --git a/pkgs/coverage/test/test_files/pubspec.yaml b/pkgs/coverage/test/test_files/pubspec.yaml
new file mode 100644
index 0000000..ab2a25b
--- /dev/null
+++ b/pkgs/coverage/test/test_files/pubspec.yaml
@@ -0,0 +1,5 @@
+name: coverage_test_files
+
+dev_dependencies:
+  coverage:
+    path: ../../
diff --git a/pkgs/coverage/test/test_util.dart b/pkgs/coverage/test/test_util.dart
index e0a3edb..6fe89d3 100644
--- a/pkgs/coverage/test/test_util.dart
+++ b/pkgs/coverage/test/test_util.dart
@@ -11,12 +11,12 @@
 
 final String testAppPath = p.join('test', 'test_files', 'test_app.dart');
 
-const Duration timeout = Duration(seconds: 20);
+const Duration timeout = Duration(seconds: 30);
 
-Future<TestProcess> runTestApp(int openPort) => TestProcess.start(
+Future<TestProcess> runTestApp() => TestProcess.start(
       Platform.resolvedExecutable,
       [
-        '--enable-vm-service=$openPort',
+        '--enable-vm-service=0',
         '--pause_isolates_on_exit',
         '--branch-coverage',
         testAppPath
diff --git a/pkgs/coverage/test/test_with_coverage_test.dart b/pkgs/coverage/test/test_with_coverage_test.dart
index 53e2253..364510b 100644
--- a/pkgs/coverage/test/test_with_coverage_test.dart
+++ b/pkgs/coverage/test/test_with_coverage_test.dart
@@ -2,7 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-@Timeout(Duration(seconds: 60))
+@Tags(['integration'])
 library;
 
 import 'dart:convert';
diff --git a/pkgs/coverage/test/util_test.dart b/pkgs/coverage/test/util_test.dart
index 6a4e556..75405ba 100644
--- a/pkgs/coverage/test/util_test.dart
+++ b/pkgs/coverage/test/util_test.dart
@@ -355,4 +355,22 @@
       ]);
     });
   });
+
+  test('getAllWorkspaceNames', () {
+    // Uses the workspace_names directory:
+    // workspace_names
+    // └── pkgs
+    //     ├── foo
+    //     │   └── foo_example  // Not part of foo's workspace.
+    //     └── bar
+    //         └── bar_example  // Part of bar's workspace.
+    expect(
+        getAllWorkspaceNames('test/workspace_names'),
+        unorderedEquals([
+          'workspace_names',
+          'foo',
+          'bar',
+          'bar_example',
+        ]));
+  });
 }
diff --git a/pkgs/coverage/test/workspace_names/pkgs/bar/bar_example/pubspec.yaml b/pkgs/coverage/test/workspace_names/pkgs/bar/bar_example/pubspec.yaml
new file mode 100644
index 0000000..e59be7d
--- /dev/null
+++ b/pkgs/coverage/test/workspace_names/pkgs/bar/bar_example/pubspec.yaml
@@ -0,0 +1,2 @@
+name: bar_example
+resolution: workspace
diff --git a/pkgs/coverage/test/workspace_names/pkgs/bar/pubspec.yaml b/pkgs/coverage/test/workspace_names/pkgs/bar/pubspec.yaml
new file mode 100644
index 0000000..a003681
--- /dev/null
+++ b/pkgs/coverage/test/workspace_names/pkgs/bar/pubspec.yaml
@@ -0,0 +1,4 @@
+name: bar
+resolution: workspace
+workspace:
+- bar_example
diff --git a/pkgs/coverage/test/workspace_names/pkgs/foo/foo_example/pubspec.yaml b/pkgs/coverage/test/workspace_names/pkgs/foo/foo_example/pubspec.yaml
new file mode 100644
index 0000000..92033a9
--- /dev/null
+++ b/pkgs/coverage/test/workspace_names/pkgs/foo/foo_example/pubspec.yaml
@@ -0,0 +1 @@
+name: foo_example
diff --git a/pkgs/coverage/test/workspace_names/pkgs/foo/pubspec.yaml b/pkgs/coverage/test/workspace_names/pkgs/foo/pubspec.yaml
new file mode 100644
index 0000000..e6ffc83
--- /dev/null
+++ b/pkgs/coverage/test/workspace_names/pkgs/foo/pubspec.yaml
@@ -0,0 +1,2 @@
+name: foo
+resolution: workspace
diff --git a/pkgs/coverage/test/workspace_names/pubspec.yaml b/pkgs/coverage/test/workspace_names/pubspec.yaml
new file mode 100644
index 0000000..90e1fbc
--- /dev/null
+++ b/pkgs/coverage/test/workspace_names/pubspec.yaml
@@ -0,0 +1,4 @@
+name: workspace_names
+workspace:
+- pkgs/bar
+- pkgs/foo