Skip to content

Add --fail-under flag for minimum coverage threshold #2075

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 29, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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
5 changes: 5 additions & 0 deletions pkgs/coverage/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.13.0

- Introduced support for minimum coverage thresholds using --fail-under flag in
format_coverage..

## 1.12.0

- Introduced support for specifying coverage flags through a YAML file.
Expand Down
79 changes: 62 additions & 17 deletions pkgs/coverage/bin/format_coverage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'dart:io';
import 'package:args/args.dart';
import 'package:coverage/coverage.dart';
import 'package:coverage/src/coverage_options.dart';
import 'package:coverage/src/coverage_percentage.dart';
import 'package:glob/glob.dart';
import 'package:path/path.dart' as p;

Expand All @@ -30,6 +31,7 @@ class Environment {
required this.sdkRoot,
required this.verbose,
required this.workers,
required this.failUnder,
});

String? baseDirectory;
Expand All @@ -49,6 +51,7 @@ class Environment {
String? sdkRoot;
bool verbose;
int workers;
double? failUnder;
}

Future<void> main(List<String> arguments) async {
Expand Down Expand Up @@ -130,6 +133,28 @@ Future<void> main(List<String> arguments) async {
}
}
await outputSink.close();

// Check coverage against the fail-under threshold if specified
if (env.failUnder != null) {
// Calculate the overall coverage percentage using the utility function
final result = calculateCoveragePercentage(
hitmap,
);

if (env.verbose) {
print('Coverage: ${result.percentage.toStringAsFixed(2)}% '
'(${result.coveredLines} of ${result.totalLines} lines)');
}

if (result.percentage < env.failUnder!) {
print('Error: Coverage ${result.percentage.toStringAsFixed(2)}% '
'is less than required ${env.failUnder}%');
exit(1);
} else if (env.verbose) {
print('Coverage ${result.percentage.toStringAsFixed(2)}% '
'meets or exceeds the required ${env.failUnder}%');
}
}
}

/// Checks the validity of the provided arguments. Does not initialize actual
Expand All @@ -143,6 +168,10 @@ Environment parseArgs(List<String> arguments, CoverageOptions defaultOptions) {
abbr: 's',
help: 'path to the SDK root',
)
..addOption(
'fail-under',
help: 'Fail if coverage is less than the given percentage (0-100)',
)
..addOption(
'packages',
help: '[DEPRECATED] path to the package spec file',
Expand Down Expand Up @@ -308,24 +337,40 @@ Environment parseArgs(List<String> arguments, CoverageOptions defaultOptions) {
final checkIgnore = args['check-ignore'] as bool;
final ignoredGlobs = args['ignore-files'] as List<String>;
final verbose = args['verbose'] as bool;

double? failUnder;
final failUnderStr = args['fail-under'] as String?;
if (failUnderStr != null) {
try {
failUnder = double.parse(failUnderStr);
if (failUnder < 0 || failUnder > 100) {
fail('--fail-under must be a percentage between 0 and 100');
}
} catch (e) {
fail('Invalid --fail-under value: $e');
}
}

return Environment(
baseDirectory: baseDirectory,
bazel: bazel,
bazelWorkspace: bazelWorkspace,
checkIgnore: checkIgnore,
input: input,
lcov: lcov,
output: output,
packagesPath: packagesPath,
packagePath: packagePath,
prettyPrint: prettyPrint,
prettyPrintFunc: prettyPrintFunc,
prettyPrintBranch: prettyPrintBranch,
reportOn: reportOn,
ignoreFiles: ignoredGlobs,
sdkRoot: sdkRoot,
verbose: verbose,
workers: workers);
baseDirectory: baseDirectory,
bazel: bazel,
bazelWorkspace: bazelWorkspace,
checkIgnore: checkIgnore,
input: input,
lcov: lcov,
output: output,
packagesPath: packagesPath,
packagePath: packagePath,
prettyPrint: prettyPrint,
prettyPrintFunc: prettyPrintFunc,
prettyPrintBranch: prettyPrintBranch,
reportOn: reportOn,
ignoreFiles: ignoredGlobs,
sdkRoot: sdkRoot,
verbose: verbose,
workers: workers,
failUnder: failUnder,
);
}

/// Given an absolute path absPath, this function returns a [List] of files
Expand Down
10 changes: 9 additions & 1 deletion pkgs/coverage/bin/test_with_coverage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ ArgParser _createArgParser(CoverageOptions defaultOptions) => ArgParser()
defaultsTo: defaultOptions.branchCoverage,
help: 'Collect branch coverage info.',
)
..addOption(
'fail-under',
help: 'Fail if coverage is less than the given percentage (0-100)',
)
..addMultiOption('scope-output',
defaultsTo: defaultOptions.scopeOutput,
help: 'restrict coverage results so that only scripts that start with '
Expand All @@ -101,7 +105,8 @@ class Flags {
this.testScript,
this.functionCoverage,
this.branchCoverage,
this.scopeOutput, {
this.scopeOutput,
this.failUnder, {
required this.rest,
});

Expand All @@ -113,6 +118,7 @@ class Flags {
final bool functionCoverage;
final bool branchCoverage;
final List<String> scopeOutput;
final String? failUnder;
final List<String> rest;
}

Expand Down Expand Up @@ -169,6 +175,7 @@ ${parser.usage}
args['function-coverage'] as bool,
args['branch-coverage'] as bool,
args['scope-output'] as List<String>,
args['fail-under'] as String?,
rest: args.rest,
);
}
Expand Down Expand Up @@ -233,6 +240,7 @@ Future<void> main(List<String> arguments) async {
outJson,
'-o',
outLcov,
if (flags.failUnder != null) '--fail-under=${flags.failUnder}',
]);
exit(0);
}
55 changes: 55 additions & 0 deletions pkgs/coverage/lib/src/coverage_percentage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2024, 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 'hitmap.dart';

/// Calculates the coverage percentage from a hitmap.
///
/// [hitmap] is the map of file paths to hit maps.
///
/// Returns a [CoverageResult] containing the coverage percentage and line
/// counts.
CoverageResult calculateCoveragePercentage(Map<String, HitMap> hitmap) {
var totalLines = 0;
var coveredLines = 0;
for (final entry in hitmap.entries) {
var coveredBranches = 0;
final lineHits = entry.value.lineHits;
final branchHits = entry.value.branchHits;
totalLines += lineHits.length;
if (branchHits != null) {
coveredBranches += branchHits.values.where((v) => v > 0).length;
totalLines += branchHits.length;
}
coveredLines +=
lineHits.values.where((v) => v > 0).length + coveredBranches;
}
final coveragePercentage =
totalLines > 0 ? coveredLines * 100 / totalLines : 0.0;

return CoverageResult(
percentage: coveragePercentage,
coveredLines: coveredLines,
totalLines: totalLines,
);
}

/// The result of a coverage calculation.
class CoverageResult {
/// Creates a new [CoverageResult].
const CoverageResult({
required this.percentage,
required this.coveredLines,
required this.totalLines,
});

/// The coverage percentage.
final double percentage;

/// The number of covered lines.
final int coveredLines;

/// The total number of lines.
final int totalLines;
}
2 changes: 1 addition & 1 deletion pkgs/coverage/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: coverage
version: 1.12.0
version: 1.13.0-wip
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
Expand Down
75 changes: 75 additions & 0 deletions pkgs/coverage/test/coverage_percentage_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) 2024, 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_percentage.dart';
import 'package:coverage/src/hitmap.dart';
import 'package:test/test.dart';

void main() {
group('calculateCoveragePercentage', () {
test('calculates correct percentage', () {
final hitmap = {
'file1.dart': HitMap({
1: 1, // covered
2: 1, // covered
3: 0, // not covered
4: 1, // covered
}),
'file2.dart': HitMap({
1: 1, // covered
2: 0, // not covered
3: 0, // not covered
}),
};

final result = calculateCoveragePercentage(hitmap);

// 4 covered lines out of 7 total lines = 57.14%
expect(result.percentage.toStringAsFixed(2), equals('57.14'));
expect(result.coveredLines, equals(4));
expect(result.totalLines, equals(7));
});
test('handles empty hitmap', () {
final hitmap = <String, HitMap>{};

final result = calculateCoveragePercentage(hitmap);

expect(result.percentage, equals(0));
expect(result.coveredLines, equals(0));
expect(result.totalLines, equals(0));
});

test('handles hitmap with no covered lines', () {
final hitmap = {
'file1.dart': HitMap({
1: 0, // not covered
2: 0, // not covered
3: 0, // not covered
}),
};

final result = calculateCoveragePercentage(hitmap);

expect(result.percentage, equals(0));
expect(result.coveredLines, equals(0));
expect(result.totalLines, equals(3));
});

test('handles hitmap with all lines covered', () {
final hitmap = {
'file1.dart': HitMap({
1: 1, // covered
2: 1, // covered
3: 1, // covered
}),
};

final result = calculateCoveragePercentage(hitmap);

expect(result.percentage, equals(100));
expect(result.coveredLines, equals(3));
expect(result.totalLines, equals(3));
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,17 @@ int product(Iterable<int> values) {
}
return val;
}

String evaluateScore(int score) {
if (score < 0) {
return 'Invalid';
} else if (score < 50) {
return 'Fail';
} else if (score < 70) {
return 'Pass';
} else if (score <= 100) {
return 'Excellent';
} else {
return 'Overflow';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2022, 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 '../lib/validate_lib.dart';
import 'package:test/test.dart';

void main() {
group('evaluateScore', () {
test('returns Invalid for negative score', () {
expect(evaluateScore(-10), equals('Invalid'));
});

test('returns Fail for score < 50', () {
expect(evaluateScore(30), equals('Fail'));
});

test('returns Excellent for score 85', () {
expect(evaluateScore(85), equals('Excellent'));
});
});
}
Loading
Loading