Module: Kettle::Soup::Cover

Extended by:
Loaders
Includes:
Constants
Defined in:
lib/kettle/soup/cover.rb,
lib/kettle/soup/cover/loaders.rb,
lib/kettle/soup/cover/version.rb,
lib/kettle/soup/cover/constants.rb,
lib/kettle/soup/cover/formatters.rb,
lib/kettle/soup/cover/coverage_thresholds.rb,
lib/kettle/soup/cover/filters/gt_line_filter.rb,
lib/kettle/soup/cover/filters/lt_line_filter.rb

Defined Under Namespace

Modules: Constants, CoverageThresholds, Filters, Loaders, Version Classes: Error

Constant Summary collapse

VAR_HOME_PREFIX =
%r{\A/var/home(?=/|\z)}
VERSION =

Current gem version exposed at the traditional constant location.

Version::VERSION

Constants included from Constants

Constants::CI, Constants::CLEAN_RESULTSET, Constants::CLEAN_RESULTSET_DEFAULT, Constants::COMMAND_NAME, Constants::COVERAGE_DIR, Constants::COVERAGE_ROOT_DIR, Constants::DEBUG, Constants::DO_COV, Constants::ENV_GET, Constants::FALSE, Constants::FILTER_DIRS, Constants::FORMATTERS, Constants::FORMATTER_PLUGINS, Constants::IS_CI, Constants::MERGE_TIMEOUT, Constants::MIN_COVERAGE_BRANCH, Constants::MIN_COVERAGE_HARD, Constants::MIN_COVERAGE_HARD_REQUESTED, Constants::MIN_COVERAGE_LINE, Constants::MIN_COVERAGE_STRICT, Constants::MULTI_FORMATTERS, Constants::MULTI_FORMATTERS_DEFAULT, Constants::OPEN_BIN, Constants::PREFIX, Constants::QUIET_COLLATION, Constants::SIMPLECOV_COMMAND_NAME, Constants::TEST_ENV_NUMBER, Constants::TRUE, Constants::TURBO_TESTS, Constants::TURBO_TESTS_DIR, Constants::TURBO_TESTS_WORKER, Constants::USE_MERGING, Constants::VERBOSE, Constants::WASHED_CONSTANTS

Class Method Summary collapse

Methods included from Loaders

install_tasks, load_filters, load_formatters

Class Method Details

.clean_resultset! ⇒ Object

Deletes coverage/.resultset.json (if it exists) so that stale entries
from prior runs cannot pollute the current run’s coverage report.
Called automatically from config.rb when CLEAN_RESULTSET is true.



57
58
59
60
# File 'lib/kettle/soup/cover.rb', line 57

def clean_resultset!
  resultset_path = SimpleCov::ResultMerger.resultset_path
  File.delete(resultset_path) if File.exist?(resultset_path)
end

.clear_coverage_dir!(coverage_dir = Constants::COVERAGE_DIR, project_root: Dir.pwd) ⇒ Object



62
63
64
# File 'lib/kettle/soup/cover.rb', line 62

def clear_coverage_dir!(coverage_dir = Constants::COVERAGE_DIR, project_root: Dir.pwd)
  FileUtils.rm_rf(File.expand_path(coverage_dir, project_root))
end

.clear_turbo_tests_coverage_dir!(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd) ⇒ Object



74
75
76
# File 'lib/kettle/soup/cover.rb', line 74

def clear_turbo_tests_coverage_dir!(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd)
  FileUtils.rm_rf(turbo_tests_coverage_dir(coverage_dir, project_root: project_root))
end

.collate_turbo_tests_coverage!(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/kettle/soup/cover.rb', line 90

def collate_turbo_tests_coverage!(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd)
  return :disabled unless turbo_tests_coverage?

  resultsets = turbo_tests_resultset_paths(coverage_dir, project_root: project_root)
  return :empty if resultsets.empty?
  return :already_collated if turbo_tests_collated?(resultsets, coverage_dir, project_root: project_root)

  require "simplecov"

  with_quiet_collation_output do
    SimpleCov.collate(resultsets) do
      command_name("#{Constants::COMMAND_NAME} (turbo_tests2)")
      enable_coverage(:branch)
      primary_coverage(:branch)
      skip(Constants::FILTER_DIRS)
      coverage_dir(File.expand_path(coverage_dir, project_root))

      Kettle::Soup::Cover.configure_formatters!

      Kettle::Soup::Cover::CoverageThresholds.configure!(self)
    end
  end

  SimpleCov.external_at_exit = true if SimpleCov.respond_to?(:external_at_exit=)
  publish_turbo_tests_json_coverage!(coverage_dir, project_root: project_root)
  mark_turbo_tests_collated!(coverage_dir, project_root: project_root)

  :collated
end

.configure_formatters! ⇒ Object



8
9
10
11
12
13
14
15
# File 'lib/kettle/soup/cover/formatters.rb', line 8

def configure_formatters!
  if Constants::MULTI_FORMATTERS
    Kettle::Soup::Cover::Loaders.load_formatters
  else
    require "simplecov-html"
    SimpleCov.formatter(SimpleCov::Formatter::HTMLFormatter)
  end
end

.coverage_task_env(coverage_dir = Constants::COVERAGE_DIR, project_root: Dir.pwd) ⇒ Object



227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/kettle/soup/cover.rb', line 227

def coverage_task_env(coverage_dir = Constants::COVERAGE_DIR, project_root: Dir.pwd)
  resolved_coverage_dir = File.expand_path(coverage_dir, project_root)

  {
    "K_SOUP_COV_PREFIX" => Constants::PREFIX,
    "#{Constants::PREFIX}DIR" => resolved_coverage_dir,
    "#{Constants::PREFIX}DO" => Constants::TRUE,
    "#{Constants::PREFIX}FORMATTERS" => "json",
    "#{Constants::PREFIX}MIN_HARD" => Constants::FALSE,
    "#{Constants::PREFIX}MULTI_FORMATTERS" => Constants::TRUE,
    "#{Constants::PREFIX}OPEN_BIN" => ""
  }
end

.display_path(path) ⇒ Object



48
49
50
51
52
# File 'lib/kettle/soup/cover.rb', line 48

def display_path(path)
  return path if path.nil?

  path.to_s.sub(VAR_HOME_PREFIX, "/home")
end

.mark_turbo_tests_collated!(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd) ⇒ Object



128
129
130
131
132
# File 'lib/kettle/soup/cover.rb', line 128

def mark_turbo_tests_collated!(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd)
  marker_path = turbo_tests_collation_marker_path(coverage_dir, project_root: project_root)
  FileUtils.mkdir_p(File.dirname(marker_path))
  FileUtils.touch(marker_path)
end

.merge_branch_coverage(left, right) ⇒ Object



168
169
170
171
# File 'lib/kettle/soup/cover.rb', line 168

def merge_branch_coverage(left, right)
  length = [left.length, right.length].max
  Array.new(length) { |index| merge_branch_entry(left[index], right[index]) }.compact
end

.merge_branch_entry(left, right) ⇒ Object



173
174
175
176
177
178
179
180
181
# File 'lib/kettle/soup/cover.rb', line 173

def merge_branch_entry(left, right)
  return right unless left
  return left unless right
  return left unless left.is_a?(Hash) && right.is_a?(Hash)

  left.merge(right) do |key, left_value, right_value|
    (key == "coverage") ? merge_coverage_value(left_value, right_value) : left_value
  end
end

.merge_coverage_value(left, right) ⇒ Object



183
184
185
186
187
188
189
190
# File 'lib/kettle/soup/cover.rb', line 183

def merge_coverage_value(left, right)
  return left + right if left.is_a?(Integer) && right.is_a?(Integer)
  return left if left.is_a?(Integer)
  return right if right.is_a?(Integer)
  return left if left

  right
end

.merge_json_coverage!(target, source) ⇒ Object



154
155
156
157
158
159
160
161
# File 'lib/kettle/soup/cover.rb', line 154

def merge_json_coverage!(target, source)
  source.each do |path, coverage|
    target[path] ||= {"lines" => [], "branches" => []}
    target[path]["lines"] = merge_line_coverage(target[path]["lines"], coverage["lines"] || [])
    target[path]["branches"] = merge_branch_coverage(target[path]["branches"] || [], coverage["branches"] || [])
  end
  target
end

.merge_line_coverage(left, right) ⇒ Object



163
164
165
166
# File 'lib/kettle/soup/cover.rb', line 163

def merge_line_coverage(left, right)
  length = [left.length, right.length].max
  Array.new(length) { |index| merge_coverage_value(left[index], right[index]) }
end

.publish_turbo_tests_json_coverage!(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/kettle/soup/cover.rb', line 134

def publish_turbo_tests_json_coverage!(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd)
  paths = turbo_tests_json_paths(coverage_dir, project_root: project_root)
  return :empty if paths.empty?

  merged = {
    "meta" => {},
    "coverage" => {}
  }
  paths.sort_by { |path| File.basename(File.dirname(path)).to_i }.each do |path|
    data = JSON.parse(File.read(path))
    merged["meta"].merge!(data["meta"]) if data["meta"].is_a?(Hash)
    merge_json_coverage!(merged["coverage"], data["coverage"] || {})
  end

  output_path = File.expand_path(File.join(coverage_dir, "coverage.json"), project_root)
  FileUtils.mkdir_p(File.dirname(output_path))
  File.write(output_path, JSON.pretty_generate(merged))
  :published
end

.quiet_collation_formatter_line?(line) ⇒ Boolean

Returns:

  • (Boolean)


218
219
220
221
222
223
224
225
# File 'lib/kettle/soup/cover.rb', line 218

def quiet_collation_formatter_line?(line)
  stripped = line.strip
  return true if stripped.empty?
  return true if stripped.match?(/coverage report generated/i)
  return true if stripped.match?(/\A(?:Line|Branch) coverage:/i)

  false
end

.refresh_coverage_data!(coverage_dir = Constants::COVERAGE_DIR, project_root: Dir.pwd, out: $stdout, err: $stderr) ⇒ Object

Raises:



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/kettle/soup/cover.rb', line 241

def refresh_coverage_data!(coverage_dir = Constants::COVERAGE_DIR, project_root: Dir.pwd, out: $stdout, err: $stderr)
  clear_coverage_dir!(coverage_dir, project_root: project_root)

  coverage_task = File.expand_path("bin/rake", project_root)
  success = system(
    coverage_task_env(coverage_dir, project_root: project_root),
    coverage_task,
    "coverage",
    chdir: project_root,
    out: out,
    err: err
  )
  return if success

  raise Error, "Coverage refresh failed: #{coverage_task} coverage"
end

.replay_quiet_collation_output(output, io) ⇒ Object



212
213
214
215
216
# File 'lib/kettle/soup/cover.rb', line 212

def replay_quiet_collation_output(output, io)
  output.each_line do |line|
    io.print(line) unless quiet_collation_formatter_line?(line)
  end
end

.turbo_tests_collated?(resultsets, coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd) ⇒ Boolean

Returns:

  • (Boolean)


120
121
122
123
124
125
126
# File 'lib/kettle/soup/cover.rb', line 120

def turbo_tests_collated?(resultsets, coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd)
  marker_path = turbo_tests_collation_marker_path(coverage_dir, project_root: project_root)
  return false unless File.file?(marker_path)

  marker_mtime = File.mtime(marker_path)
  resultsets.all? { |path| File.file?(path) && File.mtime(path) <= marker_mtime }
end

.turbo_tests_collation_marker_path(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd) ⇒ Object



86
87
88
# File 'lib/kettle/soup/cover.rb', line 86

def turbo_tests_collation_marker_path(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd)
  File.expand_path(File.join(coverage_dir, ".turbo_tests_collated"), project_root)
end

.turbo_tests_coverage? ⇒ Boolean

Returns:

  • (Boolean)


70
71
72
# File 'lib/kettle/soup/cover.rb', line 70

def turbo_tests_coverage?
  Constants::DO_COV && Constants::TURBO_TESTS
end

.turbo_tests_coverage_dir(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd) ⇒ Object



66
67
68
# File 'lib/kettle/soup/cover.rb', line 66

def turbo_tests_coverage_dir(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd)
  File.expand_path(File.join(coverage_dir, Constants::TURBO_TESTS_DIR), project_root)
end

.turbo_tests_json_paths(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd) ⇒ Object



82
83
84
# File 'lib/kettle/soup/cover.rb', line 82

def turbo_tests_json_paths(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd)
  Dir[File.join(turbo_tests_coverage_dir(coverage_dir, project_root: project_root), "*", "coverage.json")]
end

.turbo_tests_resultset_paths(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd) ⇒ Object



78
79
80
# File 'lib/kettle/soup/cover.rb', line 78

def turbo_tests_resultset_paths(coverage_dir = Constants::COVERAGE_ROOT_DIR, project_root: Dir.pwd)
  Dir[File.join(turbo_tests_coverage_dir(coverage_dir, project_root: project_root), "*", ".resultset.json")]
end

.with_quiet_collation_output ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/kettle/soup/cover.rb', line 192

def with_quiet_collation_output
  return yield unless Constants::QUIET_COLLATION && !Constants::VERBOSE

  stdout = StringIO.new
  stderr = StringIO.new
  original_stdout = $stdout
  original_stderr = $stderr

  begin
    $stdout = stdout
    $stderr = stderr
    yield
  ensure
    $stdout = original_stdout
    $stderr = original_stderr
    replay_quiet_collation_output(stdout.string, original_stdout)
    replay_quiet_collation_output(stderr.string, original_stderr)
  end
end