Skip to content

Instantly share code, notes, and snippets.

@amkisko
Last active December 11, 2024 07:45
Show Gist options
  • Save amkisko/b0685ae05b581072527ff0a4d7ed4a45 to your computer and use it in GitHub Desktop.
Save amkisko/b0685ae05b581072527ff0a4d7ed4a45 to your computer and use it in GitHub Desktop.
rspec helper for reporting missing test coverage for changed lines according to git diff with selected branch and simplecov json resultset
class DiffCoverageReporter
attr_reader :coverage_file, :default_branch
def initialize(file_path: "coverage/.resultset.json", default_branch: "master")
@coverage_file = Rails.root.join(file_path)
@default_branch = default_branch
end
def coverage_report
JSON.parse(coverage_file.read).dig("RSpec", "coverage")
end
def changed_files
`git diff --name-only origin/#{default_branch}`.split("\n").select do |file|
file.match?(%r{app/})
end
end
def changed_line_numbers
@changed_line_numbers ||= changed_files.each_with_object({}) do |file, file_numbers|
diff = `git diff origin/#{default_branch} -U0 -- #{file}`
changes = diff.split("\n").each_with_object(Set.new) do |line, numbers|
if /^@@ -\d+,\d+ \+(\d+),\d+ @@$/.match?(line)
result = line.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/).captures.map(&:to_i)
removed = (result[0]..(result[0] + result[1]))
added = (result[2]..(result[2] + result[3]))
numbers.merge(removed.to_a)
numbers.merge(added.to_a)
end
end
next if changes.empty?
file_numbers[file] = changes.map { [_1, true] }.to_h
end
end
# NOTE: require_relative "spec/support/diff_coverage_reporter"; DiffCoverageReporter.diff_missing_coverage
def diff_missing_coverage
@diff_coverage_report ||= coverage_report.map do |file, obj|
path = file.gsub(/^#{Rails.root}\//, "")
diff = `git diff origin/#{default_branch} -U0 -- #{file}`
changes = diff.split("\n").each_with_object(Set.new) do |line, numbers|
if /^@@ -\d+,\d+ \+(\d+),\d+ @@$/.match?(line)
result = line.match(/@@ -(\d+),(\d+) \+(\d+),(\d+) @@/).captures.map(&:to_i)
removed = (result[0]..(result[0] + result[1]))
added = (result[2]..(result[2] + result[3]))
numbers.merge(removed.to_a)
numbers.merge(added.to_a)
end
end.map { [_1, true] }.to_h
[
path,
obj["lines"].each_with_index.each_with_object([]) do |(number, index), groups|
line_coverage = number.nil? || number.positive?
line_changed = changed_line_numbers.dig(path, index)
changed_and_not_covered = !line_coverage && line_changed
next unless changed_and_not_covered
prev_group = groups.last
view_index = index + 1
if prev_group && prev_group.last == view_index - 1
prev_group[1] = view_index
else
groups << [view_index, view_index]
end
end
]
end.reject { |_, groups| groups.empty? }
end
def all_missing_coverage
coverage_report.map do |file, obj|
path = file.gsub(/^#{Rails.root}\//, "")
[
path,
obj["lines"].each_with_index.each_with_object([]) do |(number, index), groups|
line_coverage = number.nil? || number.positive?
next if line_coverage
prev_group = groups.last
view_index = index + 1
if prev_group && prev_group.last == view_index - 1
prev_group[1] = view_index
else
groups << [view_index, view_index]
end
end
]
end.reject { |_, groups| groups.empty? }
end
def self.instance
@@instance ||= new
end
# NOTE: require_relative "spec/support/diff_coverage_reporter"; DiffCoverageReporter.print_all_missing_coverage
def self.print_all_missing_coverage
instance.all_missing_coverage.each do |path, groups|
groups.each do |start_line, end_line|
lines = start_line == end_line ? start_line : "#{start_line}-#{end_line}"
puts "#{path}:#{lines}"
end
end
true
end
# NOTE: require_relative "spec/support/diff_coverage_reporter"; DiffCoverageReporter.print_diff_missing_coverage
def self.print_diff_missing_coverage
instance.diff_missing_coverage.each do |path, groups|
groups.each do |start_line, end_line|
lines = start_line == end_line ? start_line : "#{start_line}-#{end_line}"
puts "#{path}:#{lines}"
end
end
true
end
# NOTE: require_relative "spec/support/diff_coverage_reporter"; DiffCoverageReporter.export_diff_missing_coverage
def self.export_diff_missing_coverage
result = instance.diff_missing_coverage
return if result.empty?
File.write(Rails.root.join("coverage/.diff_missing_coverage.json"), result.to_json)
true
end
end
RSpec.configure do |config|
config.after(:suite) do
unless defined?(SimpleCov)
puts "SimpleCov is not enabled, skipping coverage reporting"
next
end
next if config.instance_variable_get(:@files_or_directories_to_run).reject { _1.match?(/^spec$/) }.present?
puts "Generating diff coverage report"
DiffCoverageReporter.print_diff_missing_coverage
DiffCoverageReporter.export_diff_missing_coverage
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment