Class: Danger::DangerJacoco

Inherits:
Plugin
  • Object
show all
Defined in:
lib/jacoco/plugin.rb

Overview

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

Examples:

Verify coverage

jacoco.minimum_project_coverage_percentage = 50

Verify coverage per package

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

See Also:

  • Malinskiy/danger-jacoco

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#class_column_titleObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def class_column_title
  @class_column_title
end

#fail_no_coverage_data_foundObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def fail_no_coverage_data_found
  @fail_no_coverage_data_found
end

#file_to_create_on_failureObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def file_to_create_on_failure
  @file_to_create_on_failure
end

#files_extensionObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def files_extension
  @files_extension
end

#files_to_checkObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def files_to_check
  @files_to_check
end

#minimum_class_coverage_mapObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def minimum_class_coverage_map
  @minimum_class_coverage_map
end

#minimum_class_coverage_percentageObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def minimum_class_coverage_percentage
  @minimum_class_coverage_percentage
end

#minimum_composable_class_coverage_percentageObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def minimum_composable_class_coverage_percentage
  @minimum_composable_class_coverage_percentage
end

#minimum_package_coverage_mapObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def minimum_package_coverage_map
  @minimum_package_coverage_map
end

#minimum_project_coverage_percentageObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def minimum_project_coverage_percentage
  @minimum_project_coverage_percentage
end

#subtitle_failureObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def subtitle_failure
  @subtitle_failure
end

#subtitle_successObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def subtitle_success
  @subtitle_success
end

#titleObject

rubocop:disable Metrics/ClassLength



23
24
25
# File 'lib/jacoco/plugin.rb', line 23

def title
  @title
end

Instance Method Details

#classes(delimiter) ⇒ Object

rubocop:enable Style/AbcSize



106
107
108
109
110
111
112
113
114
# File 'lib/jacoco/plugin.rb', line 106

def classes(delimiter)
  class_to_file_path_hash = {}
  files_to_check.select { |file| files_extension.reduce(false) { |state, el| state || file.end_with?(el) } }
                .each do |file| # "src/java/com/example/CachedRepository.java"
                  classname = file.split('.').first.split(delimiter)[1] # "com/example/CachedRepository"
                  class_to_file_path_hash[classname] = file
                end
  class_to_file_path_hash
end

#coverage_status(coverage, minimum_percentage) ⇒ Object

it returns an emoji for coverage status



170
171
172
173
174
175
176
# File 'lib/jacoco/plugin.rb', line 170

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) ⇒ Object

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



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/jacoco/plugin.rb', line 156

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) ⇒ Object

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



59
60
61
# File 'lib/jacoco/plugin.rb', line 59

def parse(path)
  Jacoco::DOMParser.read_path(path)
end

#report(path, report_url = '', delimiter = %r{/java/|/kotlin/}, fail_no_coverage_data_found: true) ⇒ Object

This is a fast report based on SAX parser

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

rubocop:disable Style/AbcSize



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/jacoco/plugin.rb', line 81

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
  class_to_file_path_hash = classes(delimiter)
  classnames = class_to_file_path_hash.keys

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

  total_covered = total_coverage(path)

  header = "### #{title} Code Coverage #{total_covered[:covered]}% #{total_covered[:status]}\n"
  report_markdown = header
  report_markdown += "| #{class_column_title} | Covered | Required | Status |\n"
  report_markdown += "|:---|:---:|:---:|:---:|\n"
  class_coverage_above_minimum = markdown_class(parser, report_markdown, report_url, class_to_file_path_hash)
  subtitle = class_coverage_above_minimum ? subtitle_success : subtitle_failure
  report_markdown.insert(header.length, "#### #{subtitle}\n")
  markdown(report_markdown)

  report_fails(parser, report_url, class_coverage_above_minimum, total_covered)
end

#report_class(jacoco_class, file_path) ⇒ Object

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



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/jacoco/plugin.rb', line 117

def report_class(jacoco_class, file_path)
  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, file_path)
    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, file_path) ⇒ Object

Determines the required coverage for the class rubocop:disable Metrics/AbcSize rubocop:disable Metrics/CyclomaticComplexity



143
144
145
146
147
148
149
150
151
# File 'lib/jacoco/plugin.rb', line 143

def required_class_coverage(jacoco_class, file_path)
  key = minimum_class_coverage_map.keys.detect { |k| jacoco_class.name.match(k) } || jacoco_class.name
  required_coverage = minimum_class_coverage_map[key]
  includes_composables = File.read(file_path).include? '@Composable' if File.exist?(file_path)
  required_coverage = minimum_composable_class_coverage_percentage if required_coverage.nil? && includes_composables
  required_coverage = package_coverage(jacoco_class.name) if required_coverage.nil?
  required_coverage = minimum_class_coverage_percentage if required_coverage.nil?
  required_coverage
end

#setupObject

Initialize the plugin with configured parameters or defaults



29
30
31
32
33
34
35
# File 'lib/jacoco/plugin.rb', line 29

def setup
  setup_minimum_coverages
  setup_texts
  @files_to_check = [] unless files_to_check
  @files_extension = ['.kt', '.java'] unless files_extension
  @file_to_create_on_failure = 'danger_jacoco_failure_status_file.json' unless file_to_create_on_failure
end

#setup_minimum_coveragesObject

Initialize the plugin with configured coverage minimum parameters or defaults



46
47
48
49
50
51
52
# File 'lib/jacoco/plugin.rb', line 46

def setup_minimum_coverages
  @minimum_project_coverage_percentage = 0 unless minimum_project_coverage_percentage
  @minimum_class_coverage_percentage = 0 unless minimum_class_coverage_percentage
  @minimum_composable_class_coverage_percentage = 0 unless minimum_composable_class_coverage_percentage
  @minimum_package_coverage_map = {} unless minimum_package_coverage_map
  @minimum_class_coverage_map = {} unless minimum_class_coverage_map
end

#setup_textsObject

Initialize the plugin with configured optional texts



38
39
40
41
42
43
# File 'lib/jacoco/plugin.rb', line 38

def setup_texts
  @title = 'JaCoCo' unless title
  @class_column_title = 'Class' unless class_column_title
  @subtitle_success = 'All classes meet coverage requirement. Well done! :white_check_mark:' unless subtitle_success
  @subtitle_failure = 'There are classes that do not meet coverage requirement :warning:' unless subtitle_failure
end

#total_coverage(report_path) ⇒ Object

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



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'lib/jacoco/plugin.rb', line 179

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