Last active
December 11, 2024 07:45
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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