class Danger::DangerJacoco

Verify code coverage inside your projects This is done using the jacoco output Results are passed out as a table in markdown

@example Verify coverage

jacoco.minimum_project_coverage_percentage = 50

@example Verify coverage per package

jacoco.minimum_package_coverage_map = { # optional (default is empty)
 'com/package/' => 55,
 'com/package/more/specific/' => 15
}

@see Anton Malinskiy/danger-jacoco @tags jacoco, coverage, java, android, kotlin

Attributes

fail_no_coverage_data_found[RW]
files_extension[RW]
minimum_class_coverage_map[RW]
minimum_class_coverage_percentage[RW]
minimum_package_coverage_map[RW]
minimum_project_coverage_percentage[RW]
title[RW]

Public Instance Methods

classes(delimiter) click to toggle source

Select modified and added files in this PR

# File lib/jacoco/plugin.rb, line 83
def classes(delimiter)
  git = @dangerfile.git
  affected_files = git.modified_files + git.added_files
  affected_files.select { |file| files_extension.reduce(false) { |state, el| state || file.end_with?(el) } }
                .map { |file| file.split('.').first.split(delimiter)[1] }
end
coverage_status(coverage, minimum_percentage) click to toggle source

it returns an emoji for coverage status

# File lib/jacoco/plugin.rb, line 138
def coverage_status(coverage, minimum_percentage)
  if coverage < (minimum_percentage / 2) then ':skull:'
  elsif coverage < minimum_percentage then ':warning:'
  else ':white_check_mark:'
  end
end
package_coverage(class_name) click to toggle source

it returns the most suitable coverage by package name to class or nil

# File lib/jacoco/plugin.rb, line 124
def package_coverage(class_name)
  path = class_name
  package_parts = class_name.split('/')
  package_parts.reverse_each do |item|
    size = item.size
    path = path[0...-size]
    coverage = minimum_package_coverage_map[path]
    path = path[0...-1] unless path.empty?
    return coverage unless coverage.nil?
  end
  nil
end
parse(path) click to toggle source

Parses the xml output of jacoco to Ruby model classes This is slow since it's basically DOM parsing

@path path to the xml output of jacoco

# File lib/jacoco/plugin.rb, line 41
def parse(path)
  Jacoco::DOMParser.read_path(path)
end
report(path, report_url = '', delimiter = %r{/java/|/kotlin/}, fail_no_coverage_data_found: true) click to toggle source

This is a fast report based on SAX parser

@path path to the xml output of jacoco @report_url URL where html report hosted @delimiter git.modified_files returns full paths to the changed files. We need to get the class from this path to check the Jacoco report,

e.g. src/java/com/example/SomeJavaClass.java -> com/example/SomeJavaClass e.g. src/kotlin/com/example/SomeKotlinClass.kt -> com/example/SomeKotlinClass

The default value supposes that you're using gradle structure, that is your path to source files is something like

Java => blah/blah/java/slashed_package/Source.java Kotlin => blah/blah/kotlin/slashed_package/Source.kt

# File lib/jacoco/plugin.rb, line 62
def report(path, report_url = '', delimiter = %r{/java/|/kotlin/}, fail_no_coverage_data_found: true)
  @fail_no_coverage_data_found = fail_no_coverage_data_found

  setup
  classes = classes(delimiter)

  parser = Jacoco::SAXParser.new(classes)
  Nokogiri::XML::SAX::Parser.new(parser).parse(File.open(path))

  total_covered = total_coverage(path)

  report_markdown = "### #{title} Code Coverage #{total_covered[:covered]}% #{total_covered[:status]}\n"
  report_markdown += "| Class | Covered | Meta | Status |\n"
  report_markdown += "|:---|:---:|:---:|:---:|\n"
  class_coverage_above_minimum = markdown_class(parser, report_markdown, report_url)
  markdown(report_markdown)

  report_fails(class_coverage_above_minimum, total_covered)
end
report_class(jacoco_class) click to toggle source

It returns a specific class code coverage and an emoji status as well

# File lib/jacoco/plugin.rb, line 91
def report_class(jacoco_class)
  report_result = {
    covered: 'No coverage data found : -',
    status: ':black_joker:',
    required_coverage_percentage: 'No coverage data found : -'
  }

  counter = coverage_counter(jacoco_class)
  unless counter.nil?
    coverage = (counter.covered.fdiv(counter.covered + counter.missed) * 100).floor
    required_coverage = required_class_coverage(jacoco_class)
    status = coverage_status(coverage, required_coverage)

    report_result = {
      covered: coverage,
      status: status,
      required_coverage_percentage: required_coverage
    }
  end

  report_result
end
required_class_coverage(jacoco_class) click to toggle source

Determines the required coverage for the class

# File lib/jacoco/plugin.rb, line 115
def required_class_coverage(jacoco_class)
  key = minimum_class_coverage_map.keys.detect { |k| jacoco_class.name.match(k) } || jacoco_class.name
  required_coverage = minimum_class_coverage_map[key]
  required_coverage = package_coverage(jacoco_class.name) if required_coverage.nil?
  required_coverage = minimum_class_coverage_percentage if required_coverage.nil?
  required_coverage
end
setup() click to toggle source

Initialize the plugin with configured parameters or defaults

# File lib/jacoco/plugin.rb, line 27
def setup
  @minimum_project_coverage_percentage = 0 unless minimum_project_coverage_percentage
  @minimum_class_coverage_percentage = 0 unless minimum_class_coverage_percentage
  @minimum_package_coverage_map = {} unless minimum_package_coverage_map
  @minimum_class_coverage_map = {} unless minimum_class_coverage_map
  @files_extension = ['.kt', '.java'] unless files_extension
  @title = 'JaCoCo' unless title
end
total_coverage(report_path) click to toggle source

It returns total of project code coverage and an emoji status as well

# File lib/jacoco/plugin.rb, line 146
def total_coverage(report_path)
  jacoco_report = Nokogiri::XML(File.open(report_path))

  report = jacoco_report.xpath('report/counter').select { |item| item['type'] == 'INSTRUCTION' }
  missed_instructions = report.first['missed'].to_f
  covered_instructions = report.first['covered'].to_f
  total_instructions = missed_instructions + covered_instructions
  covered_percentage = (covered_instructions * 100 / total_instructions).round(2)
  coverage_status = coverage_status(covered_percentage, minimum_project_coverage_percentage)

  {
    covered: covered_percentage,
    status: coverage_status
  }
end

Private Instance Methods

coverage_counter(jacoco_class) click to toggle source
# File lib/jacoco/plugin.rb, line 164
def coverage_counter(jacoco_class)
  counters = jacoco_class.counters
  branch_counter = counters.detect { |e| e.type.eql? 'BRANCH' }
  line_counter = counters.detect { |e| e.type.eql? 'LINE' }
  counter = branch_counter.nil? ? line_counter : branch_counter

  if counter.nil?
    no_coverage_data_found_message = "No coverage data found for #{jacoco_class.name}"

    raise no_coverage_data_found_message if @fail_no_coverage_data_found.instance_of?(TrueClass)

    warn no_coverage_data_found_message
  end

  counter
end
markdown_class(parser, report_markdown, report_url) click to toggle source

rubocop:enable Style/SignalException

# File lib/jacoco/plugin.rb, line 195
def markdown_class(parser, report_markdown, report_url)
  class_coverage_above_minimum = true
  parser.classes.each do |jacoco_class| # Check metrics for each classes
    rp = report_class(jacoco_class)
    rl = report_link(jacoco_class.name, report_url)
    ln = "| #{rl} | #{rp[:covered]}% | #{rp[:required_coverage_percentage]}% | #{rp[:status]} |\n"
    report_markdown << ln

    class_coverage_above_minimum &&= rp[:covered] >= rp[:required_coverage_percentage]
  end

  class_coverage_above_minimum
end
report_fails(class_coverage_above_minimum, total_covered) click to toggle source

rubocop:disable Style/SignalException

# File lib/jacoco/plugin.rb, line 182
def report_fails(class_coverage_above_minimum, total_covered)
  if total_covered[:covered] < minimum_project_coverage_percentage
    # fail danger if total coverage is smaller than minimum_project_coverage_percentage
    covered = total_covered[:covered]
    fail("Total coverage of #{covered}%. Improve this to at least #{minimum_project_coverage_percentage}%")
  end

  return if class_coverage_above_minimum

  fail("Class coverage is below minimum. Improve to at least #{minimum_class_coverage_percentage}%")
end