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
37 changes: 37 additions & 0 deletions .evergreen/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,29 @@ functions:
TEST_CMD="bundle exec rspec spec/spec_tests/open_telemetry_spec.rb --format Rfc::Riff --format RspecJunitFormatter --out tmp/rspec.xml" \
.evergreen/run-tests.sh

"run coverage tests":
- command: shell.exec
type: test
params:
shell: bash
working_dir: "src"
script: |
${PREPARE_SHELL}
export COVERAGE=1
unset TOPOLOGY
export TOPOLOGY=${MLAUNCH_TOPOLOGY}
.evergreen/run-tests.sh
- command: shell.exec
type: test
params:
shell: bash
working_dir: "src"
script: |
${PREPARE_SHELL}
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init - bash)"
bundle exec rake coverage:check

"export FLE credentials":
- command: shell.exec
type: test
Expand Down Expand Up @@ -658,6 +681,9 @@ tasks:
- name: "test-otel"
commands:
- func: "run OTel tests"
- name: "test-coverage"
commands:
- func: "run coverage tests"
- name: "test-fle"
commands:
- func: "export FLE credentials"
Expand Down Expand Up @@ -1293,6 +1319,17 @@ buildvariants:
tasks:
- name: test-otel

- matrix_name: Coverage
matrix_spec:
ruby: "ruby-4.0"
mongodb-version: "8.0"
topology: replica-set
os: ubuntu2404
display_name: "Coverage - ${mongodb-version}"
tags: ["pr"]
tasks:
- name: test-coverage

- matrix_name: "no-retry-reads"
matrix_spec:
retry-reads: no-retry-reads
Expand Down
26 changes: 26 additions & 0 deletions .evergreen/config/common.yml.erb
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,29 @@ functions:
TEST_CMD="bundle exec rspec spec/spec_tests/open_telemetry_spec.rb --format Rfc::Riff --format RspecJunitFormatter --out tmp/rspec.xml" \
.evergreen/run-tests.sh

"run coverage tests":
- command: shell.exec
type: test
params:
shell: bash
working_dir: "src"
script: |
${PREPARE_SHELL}
export COVERAGE=1
unset TOPOLOGY
export TOPOLOGY=${MLAUNCH_TOPOLOGY}
.evergreen/run-tests.sh
- command: shell.exec
type: test
params:
shell: bash
working_dir: "src"
script: |
${PREPARE_SHELL}
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init - bash)"
bundle exec rake coverage:check

"export FLE credentials":
- command: shell.exec
type: test
Expand Down Expand Up @@ -655,6 +678,9 @@ tasks:
- name: "test-otel"
commands:
- func: "run OTel tests"
- name: "test-coverage"
commands:
- func: "run coverage tests"
- name: "test-fle"
commands:
- func: "export FLE credentials"
Expand Down
11 changes: 11 additions & 0 deletions .evergreen/config/standard.yml.erb
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,17 @@ buildvariants:
tasks:
- name: test-otel

- matrix_name: Coverage
matrix_spec:
ruby: <%= latest_ruby %>
mongodb-version: <%= latest_stable_mdb %>
topology: replica-set
os: ubuntu2404
display_name: "Coverage - ${mongodb-version}"
tags: ["pr"]
tasks:
- name: test-coverage

- matrix_name: "no-retry-reads"
matrix_spec:
retry-reads: no-retry-reads
Expand Down
174 changes: 174 additions & 0 deletions .evergreen/lib/coverage_gate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# frozen_string_literal: true

require 'json'
require 'time'

# Compares a SimpleCov resultset against a checked-in per-file baseline and
# fails if any tracked file's line-coverage ratio decreased.
class CoverageGate
Entry = Struct.new(
:path,
:baseline_covered, :baseline_total,
:current_covered, :current_total,
:status,
keyword_init: true
)

TRACKED_PREFIX = 'lib/mongo/'

def initialize(resultset_path:, baseline_path:, project_root: Dir.pwd, output: $stdout)
@resultset_path = resultset_path
@baseline_path = baseline_path
@project_root = project_root
@output = output
end

# Returns 0 if no regressions, 1 otherwise.
def check
entries = compare(load_current, load_baseline)
@output.puts(format_report(entries))
(entries.any? { |e| e.status == :regression }) ? 1 : 0
end

# Writes the current resultset back to the baseline path. Used by developers
# to lock in an intentional coverage change.
def update_baseline
File.write(@baseline_path, "#{format_baseline(load_current)}\n")
0
end

# Like #check, but always returns 0. Used for local inspection.
def report
entries = compare(load_current, load_baseline)
@output.puts(format_report(entries))
0
end

private

def format_baseline(current)
files = current.sort.to_h.transform_values do |v|
{ 'covered' => v[:covered], 'total' => v[:total] }
end

JSON.pretty_generate(
'generated_at' => Time.now.utc.iso8601,
'ruby_version' => RUBY_VERSION,
'files' => files
)
end

def load_current
unless File.exist?(@resultset_path)
raise 'SimpleCov did not produce a result; was COVERAGE=1 set? ' \
"(looked for #{@resultset_path})"
end

parse_resultset(JSON.parse(File.read(@resultset_path)))
end

def parse_resultset(data)
merged = data.each_value.with_object({}) do |run, acc|
run.fetch('coverage').each do |abs_path, file_data|
lines = file_data.is_a?(Hash) ? file_data['lines'] : file_data
existing = acc[abs_path]
acc[abs_path] = existing ? merge_lines(existing, lines) : lines.dup
end
end

merged.each_with_object({}) do |(abs_path, lines), out|
rel = relative_path(abs_path)
next unless rel

out[rel] = count_lines(lines)
end
end

# SimpleCov's track_files placeholder uses a heuristic to mark executable
# lines in files no process actually loaded. Ruby's Coverage module is
# authoritative when the file was loaded, so a nil from any session wins
# over a heuristic 0 from another.
def merge_lines(a, b)
a.zip(b).map do |x, y|
next nil if x.nil? || y.nil?

x + y
end
end

def load_baseline
return { 'files' => {} } unless File.exist?(@baseline_path)

JSON.parse(File.read(@baseline_path))
end

def relative_path(abs_path)
prefix = "#{@project_root}/"
return nil unless abs_path.start_with?(prefix)

rel = abs_path.sub(prefix, '')
rel.start_with?(TRACKED_PREFIX) ? rel : nil
end

def count_lines(line_hits)
relevant = line_hits.compact
{
covered: relevant.count { |c| c.is_a?(Integer) && c.positive? },
total: relevant.size,
}
end

def compare(current, baseline)
files = baseline.fetch('files', {})
keys = (current.keys + files.keys).uniq.sort
keys.map { |path| build_entry(path, current[path], files[path]) }
end

def build_entry(path, cur, base)
if cur && base
Entry.new(
path: path,
baseline_covered: base['covered'], baseline_total: base['total'],
current_covered: cur[:covered], current_total: cur[:total],
status: regression?(cur, base) ? :regression : :ok
)
elsif cur
Entry.new(
path: path, baseline_covered: nil, baseline_total: nil,
current_covered: cur[:covered], current_total: cur[:total],
status: :new
)
else
Entry.new(
path: path,
baseline_covered: base['covered'], baseline_total: base['total'],
current_covered: nil, current_total: nil,
status: :missing
)
end
end

def regression?(cur, base)
cur[:covered] * base['total'] < base['covered'] * cur[:total]
end

def format_report(entries)
header = 'file baseline current status'
rows = entries.map do |e|
format(
'%-50s %10s %10s %s',
e.path,
format_pct(e.baseline_covered, e.baseline_total),
format_pct(e.current_covered, e.current_total),
e.status
)
end
([ header ] + rows).join("\n")
end

def format_pct(covered, total)
return '-' if covered.nil? || total.nil? || total.zero?

format('%.1f%%', covered * 100.0 / total)
end
end
Loading
Loading