Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
- test/commands/test/spaced_golden_file_name/spaced_golden_file_name_test.dart
- test/commands/test/very_good_config/very_good_config_test.dart

# E2E tests for the coverage command
- test/commands/coverage/merge/merge_test.dart

# E2E tests for the create command
- test/commands/create/flutter_app/core_test.dart
- test/commands/create/dart_cli/dart_cli_test.dart
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ doc/api/
# Files generated during tests
.test_coverage.dart
coverage/
!lib/src/commands/coverage/
!test/src/commands/coverage/
!e2e/test/commands/coverage/
.test_optimizer.dart
!bricks/test_optimizer/__brick__/test/.test_optimizer.dart
*.vm.json
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ very_good test -r
very_good test --platform chrome
```

### [`very_good coverage merge`](https://cli.vgv.dev/docs/commands/coverage)

Merge the lcov reports of sharded or recursive test runs, and enforce a minimum coverage on the result.

```sh
# Merge the reports of every shard and enforce 100% coverage
very_good coverage merge 'shards/*/lcov.info' --min-coverage 100

# Merge the reports of every package, after `very_good test -r --coverage`
very_good coverage merge
```

### [`very_good packages get`](https://cli.vgv.dev/docs/commands/get_pkgs)

Get packages in a Dart or Flutter project.
Expand Down
1 change: 1 addition & 0 deletions e2e/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
include: package:very_good_analysis/analysis_options.yaml
analyzer:
exclude:
- test/commands/coverage/merge/fixture/**
- test/commands/test/**
- build/**
- android/**
Expand Down
9 changes: 9 additions & 0 deletions e2e/test/commands/coverage/merge/fixture/lib/src/add.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Returns [a] + [b].
int add(int a, int b) {
return a + b;
}

/// Applies [add] to every value of [values], never called by the tests.
int addAll(List<int> values) {
return values.reduce(add);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Returns [a] * [b].
int multiply(int a, int b) {
return a * b;
}

/// Applies [multiply] to every value of [values], never called by the tests.
int multiplyAll(List<int> values) {
return values.reduce(multiply);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// Returns [a] - [b].
int subtract(int a, int b) {
return a - b;
}

/// Applies [subtract] to every value of [values], never called by the tests.
int subtractAll(List<int> values) {
return values.reduce(subtract);
}
15 changes: 15 additions & 0 deletions e2e/test/commands/coverage/merge/fixture/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: coverage_merge_fixture
description: Fixture for testing the merge of sharded coverage reports.
version: 0.1.0+1
publish_to: none

environment:
sdk: ^3.13.0

dependencies:
flutter:
sdk: flutter

dev_dependencies:
flutter_test:
sdk: flutter
8 changes: 8 additions & 0 deletions e2e/test/commands/coverage/merge/fixture/test/add_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:coverage_merge_fixture/src/add.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
test('add', () {
expect(add(4, 2), equals(4 + 2));
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:coverage_merge_fixture/src/multiply.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
test('multiply', () {
expect(multiply(4, 2), equals(4 * 2));
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:coverage_merge_fixture/src/subtract.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
test('subtract', () {
expect(subtract(4, 2), equals(4 - 2));
});
}
105 changes: 105 additions & 0 deletions e2e/test/commands/coverage/merge/merge_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import 'package:mason/mason.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as path;
import 'package:test/test.dart';
import 'package:universal_io/io.dart';
import 'package:very_good_cli/src/cli/cli.dart';

import '../../../../helpers/helpers.dart';

/// The records of the lcov report at [filePath], merged and serialized per
/// source file, so reports listing the same files in a different order compare
/// equal while lines moved between files do not.
Map<String, String> _recordsByFile(String filePath) => {
for (final record in mergeLcovRecords(
parseLcov(File(filePath).readAsStringSync()),
))
record.file: record.toLcov(),
};

void main() {
test(
'merges sharded coverage into the coverage of an unsharded run',
timeout: const Timeout(Duration(minutes: 5)),
withRunner((commandRunner, logger, updater, logs, progressLogs) async {
final tempDirectory = Directory.systemTemp.createTempSync('merge');
addTearDown(() => tempDirectory.deleteSync(recursive: true));

await copyDirectory(
Directory(
path.join(
Directory.current.path,
'test/commands/coverage/merge/fixture',
),
),
tempDirectory,
);
await expectSuccessfulProcessResult('flutter', [
'pub',
'get',
], workingDirectory: tempDirectory.path);

final cwd = Directory.current;
Directory.current = tempDirectory;
addTearDown(() => Directory.current = cwd);

final lcovPath = path.join('coverage', 'lcov.info');

await expectLater(
commandRunner.run(['test', '--coverage']),
completion(equals(ExitCode.success.code)),
);
final unshardedPath = File(lcovPath).copySync('unsharded.info').path;

for (final shard in ['1', '2']) {
await expectLater(
commandRunner.run([
'test',
'--coverage',
'--shard-index',
shard,
'--total-shards',
'2',
]),
completion(equals(ExitCode.success.code)),
);
final shardPath = path.join('shards', shard, 'lcov.info');
Directory(path.dirname(shardPath)).createSync(recursive: true);
File(lcovPath).copySync(shardPath);
}

await expectLater(
commandRunner.run([
'coverage',
'merge',
'shards/*/lcov.info',
'--output',
'merged.info',
]),
completion(equals(ExitCode.success.code)),
);
expect(
_recordsByFile('merged.info'),
equals(_recordsByFile(unshardedPath)),
);

await expectLater(
commandRunner.run([
'coverage',
'merge',
'shards/*/lcov.info',
'--output',
'merged.info',
'--min-coverage',
'100',
]),
completion(equals(ExitCode.software.code)),
);
verify(
() => logger.err(
any(that: startsWith('Expected coverage >= 100.00% but actual is ')),
),
).called(1);
}),
);
}
18 changes: 17 additions & 1 deletion lib/src/cli/cli.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';

import 'package:collection/collection.dart';
import 'package:coverage/coverage.dart' as coverage;
import 'package:glob/glob.dart';
import 'package:lcov_parser/lcov_parser.dart';
import 'package:glob/list_local_fs.dart';
import 'package:mason/mason.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
Expand All @@ -15,9 +16,11 @@ import 'package:very_good_test_runner/very_good_test_runner.dart';

export 'package:very_good_cli/src/test_optimizer/test_optimizer.dart';

part 'coverage_check.dart';
part 'dart_cli.dart';
part 'flutter_cli.dart';
part 'git_cli.dart';
part 'lcov.dart';
part 'test_cli_runner.dart';

const R Function<R>(
Expand Down Expand Up @@ -187,11 +190,24 @@ const _ignoredDirectories = {
'.fvm',
};

/// Whether the relative [path] goes through a platform, build or tool
/// directory, which recursive commands skip.
bool isInIgnoredDirectory(String path) =>
p.split(path).any(_ignoredDirectories.contains);

bool _isPubspec(FileSystemEntity entity) {
if (entity is! File) return false;
return p.basename(entity.path) == 'pubspec.yaml';
}

/// Whether [entity] is the `pubspec.yaml` of a package that recursive commands
/// run on, skipping platform, build and tool directories and the [ignore]d
/// ones.
bool _isPackagePubspec(
FileSystemEntity entity, {
Set<String> ignore = const {},
}) => _isPubspec(entity) && !ignore.excludes(entity);

extension on Set<String> {
bool excludes(FileSystemEntity entity) {
final segments = p.split(entity.path).toSet();
Expand Down
Loading
Loading