Skip to content

Commit 89997b0

Browse files
authored
[coverage] Partial workspace support (#2095)
1 parent a433200 commit 89997b0

28 files changed

Lines changed: 207 additions & 125 deletions

.github/workflows/coverage.yaml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,7 @@ jobs:
5656
fail-fast: false
5757
matrix:
5858
os: [ubuntu-latest, macos-latest, windows-latest]
59-
sdk: [3.4, dev]
60-
exclude:
61-
# VM service times out on windows before Dart 3.5
62-
# https://github.com/dart-lang/coverage/issues/490
63-
- os: windows-latest
64-
sdk: 3.4
59+
sdk: [3.6, dev]
6560
steps:
6661
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
6762
- uses: dart-lang/setup-dart@e51d8e571e22473a2ddebf0ef8a2123f0ab2c02c
@@ -89,7 +84,7 @@ jobs:
8984
name: Install dependencies
9085
run: dart pub get
9186
- name: Collect and report coverage
92-
run: dart run bin/test_with_coverage.dart --port=9292
87+
run: dart run bin/test_with_coverage.dart
9388
- name: Upload coverage
9489
uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b
9590
with:

pkgs/coverage/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 1.14.0
2+
3+
- Require Dart ^3.6.0
4+
- Partial support for workspace packages in `test_wth_coverage`.
5+
- Deprecate `test_wth_coverage`'s `--package-name` flag, because it doesn't make
6+
sense for workspaces.
7+
- Change the default `--port` to 0, allowing the VM to choose a free port.
8+
19
## 1.13.1
210

311
- Fix a bug where the VM service can be shut down while some coverage

pkgs/coverage/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,30 @@ dart pub global run coverage:format_coverage --packages=.dart_tool/package_confi
4343
For more complicated use cases, where you want to control each of these stages,
4444
see the sections below.
4545

46+
#### Workspaces
47+
48+
package:coverage has partial support for
49+
[workspaces](https://dart.dev/tools/pub/workspaces). You can run
50+
`test_with_coverage` from the root of the workspace to collect coverage for all
51+
the tests in all the subpackages, but you must specify the test directories to
52+
run.
53+
54+
For example, in a workspace with subpackages `pkgs/foo` and `pkgs/bar`, you
55+
could run the following command from the root directory of the workspace:
56+
57+
```
58+
dart run coverage:test_with_coverage -- pkgs/foo/test pkgs/bar/test
59+
```
60+
61+
This would output coverage to ./coverage/ as normal. An important caveat is that
62+
the working directory of the tests will be the workspace's root directory. So
63+
this approach won't work if your tests assume that they are being run from the
64+
subpackage directory.
65+
66+
[Full support](https://github.com/dart-lang/tools/issues/2083) for workspaces
67+
will likely be added in a future version. This will mean you won't need to
68+
explicitly specify the test directories: `dart run coverage:test_with_coverage`
69+
4670
#### Collecting coverage from the VM
4771

4872
```

pkgs/coverage/bin/test_with_coverage.dart

Lines changed: 46 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ import 'dart:io';
77

88
import 'package:args/args.dart';
99
import 'package:coverage/src/coverage_options.dart';
10-
import 'package:coverage/src/util.dart'
11-
show StandardOutExtension, extractVMServiceUri;
10+
import 'package:coverage/src/util.dart';
1211
import 'package:meta/meta.dart';
13-
import 'package:package_config/package_config.dart';
1412
import 'package:path/path.dart' as path;
1513

1614
import 'collect_coverage.dart' as collect_coverage;
@@ -19,37 +17,36 @@ import 'format_coverage.dart' as format_coverage;
1917
final _allProcesses = <Process>[];
2018

2119
Future<void> _dartRun(List<String> args,
22-
{void Function(String)? onStdout, String? workingDir}) async {
23-
final process = await Process.start(
24-
Platform.executable,
25-
args,
26-
workingDirectory: workingDir,
27-
);
20+
{required void Function(String) onStdout,
21+
required void Function(String) onStderr}) async {
22+
final process = await Process.start(Platform.executable, args);
2823
_allProcesses.add(process);
29-
final broadStdout = process.stdout.asBroadcastStream();
30-
broadStdout.listen(stdout.add);
31-
if (onStdout != null) {
32-
broadStdout.lines().listen(onStdout);
24+
25+
void listen(
26+
Stream<List<int>> stream, IOSink sink, void Function(String) onLine) {
27+
final broadStream = stream.asBroadcastStream();
28+
broadStream.listen(sink.add);
29+
broadStream.lines().listen(onLine);
3330
}
34-
process.stderr.listen(stderr.add);
31+
32+
listen(process.stdout, stdout, onStdout);
33+
listen(process.stderr, stderr, onStderr);
34+
3535
final result = await process.exitCode;
3636
if (result != 0) {
3737
throw ProcessException(Platform.executable, args, '', result);
3838
}
3939
}
4040

41-
Future<String?> _packageNameFromConfig(String packageDir) async {
42-
final config = await findPackageConfig(Directory(packageDir));
43-
return config?.packageOf(Uri.directory(packageDir))?.name;
41+
void _killSubprocessesAndExit(ProcessSignal signal) {
42+
for (final process in _allProcesses) {
43+
process.kill(signal);
44+
}
45+
exit(1);
4446
}
4547

4648
void _watchExitSignal(ProcessSignal signal) {
47-
signal.watch().listen((sig) {
48-
for (final process in _allProcesses) {
49-
process.kill(sig);
50-
}
51-
exit(1);
52-
});
49+
signal.watch().listen(_killSubprocessesAndExit);
5350
}
5451

5552
ArgParser _createArgParser(CoverageOptions defaultOptions) => ArgParser()
@@ -61,10 +58,10 @@ ArgParser _createArgParser(CoverageOptions defaultOptions) => ArgParser()
6158
..addOption(
6259
'package-name',
6360
help: 'Name of the package to test. '
64-
'Deduced from --package if not provided.',
65-
defaultsTo: defaultOptions.packageName,
61+
'Deduced from --package if not provided. '
62+
'DEPRECATED: use --scope-output',
6663
)
67-
..addOption('port', help: 'VM service port.', defaultsTo: '8181')
64+
..addOption('port', help: 'VM service port. Defaults to using any free port.')
6865
..addOption(
6966
'out',
7067
defaultsTo: defaultOptions.outputDirectory,
@@ -93,13 +90,13 @@ ArgParser _createArgParser(CoverageOptions defaultOptions) => ArgParser()
9390
defaultsTo: defaultOptions.scopeOutput,
9491
help: 'restrict coverage results so that only scripts that start with '
9592
'the provided package path are considered. Defaults to the name of '
96-
'the package under test.')
93+
'the current package (including all subpackages, if this is a '
94+
'workspace).')
9795
..addFlag('help', abbr: 'h', negatable: false, help: 'Show this help.');
9896

9997
class Flags {
10098
Flags(
10199
this.packageDir,
102-
this.packageName,
103100
this.outDir,
104101
this.port,
105102
this.testScript,
@@ -111,7 +108,6 @@ class Flags {
111108
});
112109

113110
final String packageDir;
114-
final String packageName;
115111
final String outDir;
116112
final String port;
117113
final String testScript;
@@ -157,25 +153,23 @@ ${parser.usage}
157153
fail('--package is not a valid directory.');
158154
}
159155

160-
final packageName = (args['package-name'] as String?) ??
161-
await _packageNameFromConfig(packageDir);
162-
if (packageName == null) {
156+
final pubspecPath = getPubspecPath(packageDir);
157+
if (!File(pubspecPath).existsSync()) {
163158
fail(
164-
"Couldn't figure out package name from --package. Make sure this is a "
165-
'package directory, or try passing --package-name explicitly.',
159+
"Couldn't find $pubspecPath. Make sure this command is run in a "
160+
'package directory, or pass --package to explicitly set the directory.',
166161
);
167162
}
168163

169164
return Flags(
170165
packageDir,
171-
packageName,
172-
(args['out'] as String?) ?? path.join(packageDir, 'coverage'),
173-
args['port'] as String,
174-
args['test'] as String,
175-
args['function-coverage'] as bool,
176-
args['branch-coverage'] as bool,
177-
args['scope-output'] as List<String>,
178-
args['fail-under'] as String?,
166+
args.option('out') ?? path.join(packageDir, 'coverage'),
167+
args.option('port') ?? '0',
168+
args.option('test')!,
169+
args.flag('function-coverage'),
170+
args.flag('branch-coverage'),
171+
args.multiOption('scope-output'),
172+
args.option('fail-under'),
179173
rest: args.rest,
180174
);
181175
}
@@ -215,11 +209,19 @@ Future<void> main(List<String> arguments) async {
215209
}
216210
}
217211
},
212+
onStderr: (line) {
213+
if (!serviceUriCompleter.isCompleted) {
214+
if (line.contains('Could not start the VM service')) {
215+
_killSubprocessesAndExit(ProcessSignal.sigkill);
216+
}
217+
}
218+
},
218219
);
219220
final serviceUri = await serviceUriCompleter.future;
220221

221-
final scopes =
222-
flags.scopeOutput.isEmpty ? [flags.packageName] : flags.scopeOutput;
222+
final scopes = flags.scopeOutput.isEmpty
223+
? getAllWorkspaceNames(flags.packageDir)
224+
: flags.scopeOutput;
223225
await collect_coverage.main([
224226
'--wait-paused',
225227
'--resume-isolates',

pkgs/coverage/dart_test.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
tags:
2+
# Tests that start subprocesses, so are slower and can be a bit flaky.
3+
integration:
4+
timeout: 2x
5+
retry: 3

pkgs/coverage/lib/src/collect.dart

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ Future<Map<String, dynamic>> _getAllCoverage(
170170
isolateReport,
171171
includeDart,
172172
functionCoverage,
173+
branchCoverage,
173174
coverableLineCache,
174175
scopedOutput);
175176
allCoverage.addAll(coverage);
@@ -244,6 +245,7 @@ Future<List<Map<String, dynamic>>> _processSourceReport(
244245
SourceReport report,
245246
bool includeDart,
246247
bool functionCoverage,
248+
bool branchCoverage,
247249
Map<String, Set<int>>? coverableLineCache,
248250
Set<String> scopedOutput) async {
249251
final hitMaps = <Uri, HitMap>{};
@@ -262,7 +264,10 @@ Future<List<Map<String, dynamic>>> _processSourceReport(
262264
return scripts[scriptRef];
263265
}
264266

265-
HitMap getHitMap(Uri scriptUri) => hitMaps.putIfAbsent(scriptUri, HitMap.new);
267+
HitMap getHitMap(Uri scriptUri) => hitMaps.putIfAbsent(
268+
scriptUri,
269+
() => HitMap.empty(
270+
functionCoverage: functionCoverage, branchCoverage: branchCoverage));
266271

267272
Future<void> processFunction(FuncRef funcRef) async {
268273
final func = await service.getObject(isolateRef.id!, funcRef.id!) as Func;
@@ -290,8 +295,7 @@ Future<List<Map<String, dynamic>>> _processSourceReport(
290295
return;
291296
}
292297
final hits = getHitMap(Uri.parse(script.uri!));
293-
hits.funcHits ??= <int, int>{};
294-
(hits.funcNames ??= <int, String>{})[line] = funcName;
298+
hits.funcNames![line] = funcName;
295299
}
296300

297301
for (var range in report.ranges!) {
@@ -385,13 +389,12 @@ Future<List<Map<String, dynamic>>> _processSourceReport(
385389
hits.funcHits?.putIfAbsent(line, () => 0);
386390
});
387391

388-
final branchCoverage = range.branchCoverage;
389-
if (branchCoverage != null) {
390-
hits.branchHits ??= <int, int>{};
391-
forEachLine(branchCoverage.hits, (line) {
392+
final branches = range.branchCoverage;
393+
if (branchCoverage && branches != null) {
394+
forEachLine(branches.hits, (line) {
392395
hits.branchHits!.increment(line);
393396
});
394-
forEachLine(branchCoverage.misses, (line) {
397+
forEachLine(branches.misses, (line) {
395398
hits.branchHits!.putIfAbsent(line, () => 0);
396399
});
397400
}

pkgs/coverage/lib/src/coverage_options.dart

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ class CoverageOptions {
99
required this.functionCoverage,
1010
required this.branchCoverage,
1111
required this.packageDirectory,
12-
this.packageName,
1312
required this.testScript,
1413
});
1514

@@ -40,8 +39,6 @@ class CoverageOptions {
4039
branchCoverage: options.optionalBool('branch_coverage') ??
4140
defaultOptions.branchCoverage,
4241
packageDirectory: packageDirectory,
43-
packageName:
44-
options.optionalString('package_name') ?? defaultOptions.packageName,
4542
testScript:
4643
options.optionalString('test_script') ?? defaultOptions.testScript,
4744
);
@@ -52,7 +49,6 @@ class CoverageOptions {
5249
final bool functionCoverage;
5350
final bool branchCoverage;
5451
final String packageDirectory;
55-
final String? packageName;
5652
final String testScript;
5753
}
5854

@@ -119,7 +115,6 @@ class CoverageOptionsProvider {
119115
functionCoverage: false,
120116
branchCoverage: false,
121117
packageDirectory: '.',
122-
packageName: null,
123118
testScript: 'test',
124119
);
125120
}

pkgs/coverage/lib/src/formatter.dart

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,20 @@ extension FileHitMapsFormatter on Map<String, HitMap> {
151151
);
152152
final buf = StringBuffer();
153153
for (final entry in entries) {
154+
final source = resolver.resolve(entry.key);
155+
if (source == null) {
156+
continue;
157+
}
158+
159+
if (!pathFilter(source)) {
160+
continue;
161+
}
162+
163+
final lines = await loader.load(source);
164+
if (lines == null) {
165+
continue;
166+
}
167+
154168
final v = entry.value;
155169
if (reportFuncs && v.funcHits == null) {
156170
throw StateError(
@@ -165,24 +179,12 @@ extension FileHitMapsFormatter on Map<String, HitMap> {
165179
'missing branch coverage information. Did you run '
166180
'collect_coverage with the --branch-coverage flag?');
167181
}
182+
168183
final hits = reportFuncs
169184
? v.funcHits!
170185
: reportBranches
171186
? v.branchHits!
172187
: v.lineHits;
173-
final source = resolver.resolve(entry.key);
174-
if (source == null) {
175-
continue;
176-
}
177-
178-
if (!pathFilter(source)) {
179-
continue;
180-
}
181-
182-
final lines = await loader.load(source);
183-
if (lines == null) {
184-
continue;
185-
}
186188
buf.writeln(source);
187189
for (var line = 1; line <= lines.length; line++) {
188190
var prefix = _prefix;

pkgs/coverage/lib/src/hitmap.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ class HitMap {
1818
this.branchHits,
1919
]) : lineHits = lineHits ?? {};
2020

21+
/// Constructs an empty hitmap, optionally with function and branch coverage
22+
/// tables.
23+
HitMap.empty({bool functionCoverage = false, bool branchCoverage = false})
24+
: this(
25+
null,
26+
functionCoverage ? <int, int>{} : null,
27+
functionCoverage ? <int, String>{} : null,
28+
branchCoverage ? <int, int>{} : null);
29+
2130
/// Map from line to hit count for that line.
2231
final Map<int, int> lineHits;
2332

0 commit comments

Comments
 (0)