Skip to content
Draft
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 .ci/coverage-thresholds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"android": {"Lines": 85}
}
4 changes: 3 additions & 1 deletion .github/workflows/android-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ jobs:

- name: Run tests and generate library coverage
id: coverage_tests
run: ./gradlew test :lib:createDebugUnitTestCoverageReport --console=plain
run: |
./gradlew test :lib:createDebugUnitTestCoverageReport --console=plain
ruby ../../scripts/check_coverage android lib/build/reports/coverage/test/debug/report.xml

- name: Record coverage
if: ${{ !cancelled() }}
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
with:
filters: |
android:
- '.ci/coverage-thresholds.json'
- 'scripts/check_coverage'
- 'scripts/lib/coverage_thresholds.rb'
- '.github/actions/report-coverage/**'
- 'scripts/report_coverage'
- 'scripts/lib/coverage_report.rb'
Expand Down Expand Up @@ -114,6 +117,8 @@ jobs:
- '.ci/changed-file-filters.yml'
- '.github/workflows/ci.yml'
scripts:
- '.ci/coverage-thresholds.json'
- 'scripts/check_coverage'
- '.github/scripts/*coverage*'
- '.github/workflows/coverage-report.yml'
- '.github/actions/report-coverage/**'
Expand Down
2 changes: 2 additions & 0 deletions dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,9 @@ commands:
run: |
source platforms/android/scripts/parse_local_protocol_flag
parse_local_protocol_flag "$@"
set -e
platforms/android/gradlew -p platforms/android :lib:createDebugUnitTestCoverageReport --console=plain
ruby scripts/check_coverage android platforms/android/lib/build/reports/coverage/test/debug/report.xml
samples:
desc: Run JVM tests for the Checkout Kit Android sample app
syntax:
Expand Down
12 changes: 12 additions & 0 deletions scripts/check_coverage
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require_relative "lib/coverage_report"
require_relative "lib/coverage_thresholds"

abort "Usage: ruby scripts/check_coverage <platform> <report-path>" unless ARGV.length == 2
report = CoverageReport.new(ARGV[0], File.read(ARGV[1]))
configuration = JSON.parse(File.read(File.expand_path("../.ci/coverage-thresholds.json", __dir__)))
failures = CoverageThresholds.new(configuration).failures(report)
abort "Coverage threshold failed for #{CoverageReport::TITLES.fetch(report.platform)}:\n#{failures.join("\n")}" unless failures.empty?
puts "Coverage thresholds passed for #{CoverageReport::TITLES.fetch(report.platform)}."
24 changes: 24 additions & 0 deletions scripts/lib/coverage_thresholds.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

require "json"

class CoverageThresholds
def initialize(configuration)
@configuration = configuration
end

def failures(report)
@configuration.fetch(report.platform).filter_map do |metric, minimum|
raise "Invalid threshold for #{metric}" unless minimum.is_a?(Numeric) && minimum.finite? && minimum.between?(0, 100)

row = report.rows.find { |entry| entry[0] == metric }
next "#{metric}: coverage is missing (minimum #{minimum}%)" unless row

_, covered, total = row
next "#{metric}: no executable code was measured (minimum #{minimum}%)" if total.zero?
next if covered * 100 >= minimum * total

"#{metric}: #{format('%.2f', 100.0 * covered / total)}% (#{covered}/#{total}) is below #{minimum}%"
end
end
end
40 changes: 40 additions & 0 deletions scripts/test/coverage_thresholds_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

require "minitest/autorun"
require_relative "../lib/coverage_thresholds"

class CoverageThresholdsTest < Minitest::Test
Report = Struct.new(:platform, :rows)

def threshold_failures(rows, configuration = {"android" => {"Lines" => 85}})
CoverageThresholds.new(configuration).failures(Report.new("android", rows))
end

def test_exact_threshold_passes_but_rounding_cannot_hide_a_failure
assert_empty threshold_failures([["Lines", 85, 100]])
assert_equal 1, threshold_failures([["Lines", 849999, 1000000]]).length
assert_empty threshold_failures([["Lines", 86, 100]])
end

def test_missing_or_uninstrumented_code_does_not_pass
assert_match(/missing/, threshold_failures([]).first)
assert_match(/no executable code/, threshold_failures([["Lines", 0, 0]]).first)
end

def test_unconfigured_metrics_remain_report_only
assert_empty threshold_failures([["Lines", 90, 100], ["Branches", 1, 100]])
end

def test_every_configured_target_must_pass_independently
configuration = {"android" => {"First" => 85, "Second" => 85}}
result = threshold_failures([["First", 99, 100], ["Second", 70, 100]], configuration)
assert_equal ["Second: 70.00% (70/100) is below 85%"], result
end

def test_invalid_configuration_fails_closed
assert_raises(KeyError) { threshold_failures([], {}) }
[-1, 101, "85", Float::NAN].each do |minimum|
assert_raises(RuntimeError) { threshold_failures([["Lines", 90, 100]], {"android" => {"Lines" => minimum}}) }
end
end
end
Loading