Module: SimpleCov::CLI::Merge

Defined in:
lib/simplecov/cli/merge.rb

Overview

‘simplecov merge <files…>` — wrap SimpleCov::ResultMerger so a CI matrix that produces one .resultset.json per worker can stitch them together from the shell instead of dropping a Rake task into every project. Requires the full simplecov library to be on the load path; lazy-required so the read-only subcommands above don’t pay for ResultMerger (and its Coverage runtime guard).

Class Method Summary collapse

Class Method Details

.commit(opts, result, stdout) ⇒ Object



30
31
32
33
34
# File 'lib/simplecov/cli/merge.rb', line 30

def commit(opts, result, stdout)
  verb = opts[:dry_run] ? "would write" : "wrote"
  write(opts[:output], result) unless opts[:dry_run]
  stdout.puts("simplecov merge: #{verb} #{opts[:output]}") unless opts[:quiet]
end

.duplicate_warning(command_name, paths) ⇒ Object



98
99
100
101
102
# File 'lib/simplecov/cli/merge.rb', line 98

def duplicate_warning(command_name, paths)
  "simplecov merge: warning — command_name #{command_name.inspect} " \
    "appears in #{paths.size} input files (#{paths.join(', ')}); " \
    "entries will be merged"
end

.error(stderr, message) ⇒ Object



110
111
112
113
# File 'lib/simplecov/cli/merge.rb', line 110

def error(stderr, message)
  stderr.puts("simplecov merge: #{message}")
  1
end

.parse(args) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
# File 'lib/simplecov/cli/merge.rb', line 43

def parse(args)
  opts = {output: SimpleCov::CLI.default_resultset, honor_timeout: false, dry_run: false, quiet: false}
  files =
    OptionParser.new do |o|
      o.on("--output PATH") { |v| opts[:output] = v }
      o.on("--honor-timeout") { opts[:honor_timeout] = true }
      o.on("--dry-run") { opts[:dry_run] = true }
      o.on("-q", "--quiet") { opts[:quiet] = true }
    end.parse(args)
  opts.merge(files: files)
end

.parse_input(path, stderr) ⇒ Object



67
68
69
70
71
72
73
74
75
76
# File 'lib/simplecov/cli/merge.rb', line 67

def parse_input(path, stderr)
  return parse_input_error(stderr, path, "not found") unless File.exist?(path)

  data = JSON.parse(File.read(path))
  return data if data.is_a?(Hash) && !data.empty?

  parse_input_error(stderr, path, "has no resultset entries")
rescue JSON::ParserError => e
  parse_input_error(stderr, path, "isn't valid JSON (#{e.message})")
end

.parse_input_error(stderr, path, reason) ⇒ Object



78
79
80
81
# File 'lib/simplecov/cli/merge.rb', line 78

def parse_input_error(stderr, path, reason)
  stderr.puts("simplecov merge: input file #{path.inspect} #{reason}")
  nil
end

.parse_inputs(files, stderr) ⇒ Object

Validate every input file up-front and return a => parsed hash. Surfacing per-file errors here turns ResultMerger’s generic “no mergeable results” into a message that points at the specific input causing the failure.



59
60
61
62
63
64
65
# File 'lib/simplecov/cli/merge.rb', line 59

def parse_inputs(files, stderr)
  files.each_with_object({}) do |path, memo|
    data = parse_input(path, stderr) or return nil

    memo[path] = data
  end
end

.run(args, stdout:, stderr:) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/simplecov/cli/merge.rb', line 17

def run(args, stdout:, stderr:, **)
  opts = parse(args)
  return error(stderr, "missing input files") if opts[:files].empty?
  return 1 unless valid_inputs?(opts[:files], stderr)

  require "simplecov"
  result = SimpleCov::ResultMerger.merge_results(*opts[:files], ignore_timeout: !opts[:honor_timeout])
  return error(stderr, "no mergeable results in input files") unless result

  commit(opts, result, stdout)
  0
end

.valid_inputs?(files, stderr) ⇒ Boolean

Returns:

  • (Boolean)


36
37
38
39
40
41
# File 'lib/simplecov/cli/merge.rb', line 36

def valid_inputs?(files, stderr)
  parsed = parse_inputs(files, stderr) or return false

  warn_about_duplicate_command_names(parsed, stderr)
  true
end

.warn_about_duplicate_command_names(parsed, stderr) ⇒ Object

When two input files share a command_name, ResultMerger folds them together with last-write-wins on the timestamp — easy to mistake for “no merge happened.” Surface the overlap so the operator can rename the workers or accept the merge knowingly.



87
88
89
90
91
92
93
94
95
96
# File 'lib/simplecov/cli/merge.rb', line 87

def warn_about_duplicate_command_names(parsed, stderr)
  files_per_command = parsed.each_with_object({}) do |(path, data), memo|
    data.each_key { |command_name| (memo[command_name] ||= []) << path }
  end
  files_per_command.each do |command_name, paths|
    next if paths.size < 2

    stderr.puts(duplicate_warning(command_name, paths))
  end
end

.write(path, result) ⇒ Object



104
105
106
107
108
# File 'lib/simplecov/cli/merge.rb', line 104

def write(path, result)
  require "fileutils"
  FileUtils.mkdir_p(File.dirname(path))
  File.write(path, JSON.pretty_generate(result.to_hash))
end