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

#add_kotlin_declarations(file, package_path, class_to_file_path_hash) ⇒ Object

Scans a Kotlin file for additional class/interface declarations and adds them to the hash



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/jacoco/plugin.rb', line 131

def add_kotlin_declarations(file, package_path, class_to_file_path_hash)
  return unless File.exist?(file)

  file_content = File.read(file)

  # Look for class and interface declarations in the file
  # Regex catches class/interface/object declarations with modifiers and generics
  regex = /\b(?:(?:data|sealed|abstract|open|internal|private|protected|public|inline)\s+)*
           (?:class|interface|object)\s+([A-Za-z0-9_]+)(?:<.*?>)?/x
  declarations = file_content.scan(regex).flatten

  # For each additional class/interface found (excluding the one matching the filename)
  declarations.each do |class_name|
    # Skip if it matches the primary class name (already added)
    next if package_path.end_with?("/#{class_name}")

    # Create full class path by replacing the last part with the class name
    parts = package_path.split('/')
    parts[-1] = class_name
    additional_class_path = parts.join('/')

    # Add to hash
    class_to_file_path_hash[additional_class_path] = file
  end
end

#classes(delimiter) ⇒ Object

rubocop:enable Style/AbcSize



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/jacoco/plugin.rb', line 106

def classes(delimiter)
  class_to_file_path_hash = {}
  # Initialize files_extension if it's nil
  @files_extension = ['.kt', '.java'] if @files_extension.nil?

  filtered_files_to_check.each do |file| # "src/java/com/example/CachedRepository.java"
    # Get the package path
    package_path = file.split('.').first.split(delimiter)[1] # "com/example/CachedRepository"
    next unless package_path

    # Add the primary class (filename-based class)
    class_to_file_path_hash[package_path] = file

    # For Kotlin files, we need to look for multiple classes/interfaces in the same file
    add_kotlin_declarations(file, package_path, class_to_file_path_hash) if file.end_with?('.kt')
  end
  class_to_file_path_hash
end

#coverage_status(coverage, minimum_percentage) ⇒ Object

it returns an emoji for coverage status



211
212
213
214
215
216
217
# File 'lib/jacoco/plugin.rb', line 211

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

#filtered_files_to_checkObject

Returns files that match the configured file extensions



126
127
128
# File 'lib/jacoco/plugin.rb', line 126

def filtered_files_to_check
  files_to_check.select { |file| @files_extension.reduce(false) { |state, el| state || file.end_with?(el) } }
end

#package_coverage(class_name) ⇒ Object

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



197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/jacoco/plugin.rb', line 197

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



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/jacoco/plugin.rb', line 158

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



184
185
186
187
188
189
190
191
192
# File 'lib/jacoco/plugin.rb', line 184

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



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/jacoco/plugin.rb', line 220

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