require 'nokogiri' module Danger # Parse a Kover report to enforce code coverage on CI. # Results are passed out as a table in markdown. # # It depends on having a Kover coverage report generated for your project. # # # @example Running with default values for Kover # # # Report coverage of modified files, fail if either total project coverage # # or any modified file's coverage is under 90% # kover.report 'Project Name', 'path/to/kover/report.xml' # # @example Running with custom coverage thresholds for Kover # # # Report coverage of modified files, fail if total project coverage is under 80%, # # or if any modified file's coverage is under 95% # kover.report 'Project Name', 'path/to/kover/report.xml', 80, 95 # # @example Warn on builds instead of failing for Kover # # # Report coverage of modified files the same as the above example, except the # # builds will only warn instead of fail if below thresholds # kover.report 'Project Name', 'path/to/kover/report.xml', 80, 95, false # # @tags android, kover, code coverage, coverage report # class DangerKover < Plugin # Total project code coverage % threshold [0-100]. # @return [Integer] attr_accessor :total_threshold # A getter for `total_threshold`, returning 70% by default. # @return [Integer] def total_threshold @total_threshold ||= 70 end # Modified file code coverage % threshold [0-100]. # @return [Integer] attr_accessor :file_threshold # A getter for `file_threshold`, returning 70% by default. # @return [Integer] def file_threshold @file_threshold ||= 70 end # Fail if under threshould, just warn otherwise. # @return [Boolean] attr_accessor :fail_if_under_threshold # A getter for `fail_if_under_threshold`, returning `true` by default. # @return [Boolean] def fail_if_under_threshold @fail_if_under_threshold ||= true end # Show plugin repository link. # @return [Boolean] attr_accessor :link_repository # A getter for `link_repository`, returning `true` by default. # @return [Boolean] def link_repository @link_repository ||= true end # Show not found files in report count. # @return [Boolean] attr_accessor :count_not_found # A getter for `count_not_found`, returning `true` by default. # @return [Boolean] def count_not_found @count_not_found ||= true end # Report coverage on diffed files, as well as overall coverage. # # @param [String] moduleName # the display name of the project or module # # @param [String] file # file path to a Kover xml coverage report. # # @return [void] def report(moduleName, file) raise "Please specify file name." if file.empty? raise "No Kover xml report found at #{file}" unless File.exist? file rawXml = File.read(file) parsedXml = Nokogiri::XML.parse(rawXml) totalInstructionCoverage = parsedXml.xpath("/report/counter[@type='INSTRUCTION']") missed = totalInstructionCoverage.attr("missed").value.to_i covered = totalInstructionCoverage.attr("covered").value.to_i total = missed + covered coveragePercent = (covered / total.to_f) * 100 # get array of files names touched by this PR (modified + added) touchedFileNames = @dangerfile.git.modified_files.map { |file| File.basename(file) } touchedFileNames += @dangerfile.git.added_files.map { |file| File.basename(file) } # used to later report files that were modified but not included in the report fileNamesNotInReport = [] # hash for keeping track of coverage per filename: {filename => coverage percent} touchedFilesHash = {} touchedFileNames.each do |touchedFileName| xmlForFileName = parsedXml.xpath("//class[@sourcefilename='#{touchedFileName}']/counter[@type='INSTRUCTION']") if (xmlForFileName.length > 0) missed = 0 covered = 0 xmlForFileName.each do |classCountXml| missed += classCountXml.attr("missed").to_i covered += classCountXml.attr("covered").to_i end touchedFilesHash[touchedFileName] = (covered.to_f / (missed + covered)) * 100 else fileNamesNotInReport << touchedFileName end end puts "Here are unreported files" puts fileNamesNotInReport.to_s puts "Here is the touched files coverage hash" puts touchedFilesHash output = "### 🎯 #{moduleName} Code Coverage: **`#{'%.2f' % coveragePercent}%`**\n\n" if touchedFilesHash.empty? output << "The new and updated files are not part of this module coverage report 👀.\n" else output << "**Modified files:**\n\n" output << "File | Coverage\n" output << ":-----|:-----:\n" end # Go through each file: touchedFilesHash.sort.each do |fileName, coveragePercent| output << "`#{fileName}` | **`#{'%.2f' % coveragePercent}%`**\n" # Check file coverage if (coveragePercent == 0) advise("Oops! #{fileName} does not have any test coverage.") elsif (coveragePercent < file_threshold) advise("Oops! #{fileName} is under #{file_threshold}% coverage.") end end puts "Value of count_not_found: #{count_not_found}" puts "Value of link_repository: #{link_repository}" if (count_not_found) output << "\n\n" output << "Number of files not found in coverage report: #{fileNamesNotInReport.size}." end if (link_repository) output << "\n\n" output << 'Code coverage by [danger-kover](https://github.com/JCarlosR/danger-kover).' end markdown output # Check total coverage if (coveragePercent < total_threshold) advise("Oops! The module #{moduleName} codebase is under #{total_threshold}% coverage.") end end # Warn or fail, depending on the `fail_if_under_threshold` flag. private def advise(message) if (fail_if_under_threshold) fail message else warn message end end end end