Module: SimpleCov::CLI::Merge

Extended by:
CommandHelpers
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).

Constant Summary

Constants included from CommandHelpers

CommandHelpers::STATS_ROW_FORMAT

Class Method Summary collapse

Methods included from CommandHelpers

command_name, common_options, error, parse_common, stats_row

Class Method Details

.commit(opts, result, stdout) ⇒ Object



34
35
36
37
38
# File 'lib/simplecov/cli/merge.rb', line 34

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



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

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

.parse(args) ⇒ Object



47
48
49
50
51
52
53
54
55
56
57
# File 'lib/simplecov/cli/merge.rb', line 47

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

Read first, classify by the exception: an exist?-then-read pair is racy, and rescuing only ENOENT crashed with a backtrace on a directory (EISDIR) or an unreadable file (EACCES).



75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/simplecov/cli/merge.rb', line 75

def parse_input(path, stderr)
  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 Errno::ENOENT
  parse_input_error(stderr, path, "not found")
rescue JSON::ParserError => e
  parse_input_error(stderr, path, "isn't valid JSON (#{e.message})")
rescue SystemCallError => e
  parse_input_error(stderr, path, "cannot be read (#{e.message.lines.first.to_s.strip})")
end

.parse_input_error(stderr, path, reason) ⇒ Object



88
89
90
91
# File 'lib/simplecov/cli/merge.rb', line 88

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.



63
64
65
66
67
68
69
70
# File 'lib/simplecov/cli/merge.rb', line 63

def parse_inputs(files, stderr)
  parsed = {} #: Hash[String, Hash[String, untyped]]
  files.each_with_object(parsed) do |path, memo|
    data = parse_input(path, stderr) or return nil

    memo[path] = data
  end
end

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



21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/simplecov/cli/merge.rb', line 21

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)


40
41
42
43
44
45
# File 'lib/simplecov/cli/merge.rb', line 40

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.



97
98
99
100
101
102
103
104
105
106
107
# File 'lib/simplecov/cli/merge.rb', line 97

def warn_about_duplicate_command_names(parsed, stderr)
  files_per_command = {} #: Hash[String, Array[String]]
  parsed.each do |path, data|
    data.each_key { |command_name| (files_per_command[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



115
116
117
# File 'lib/simplecov/cli/merge.rb', line 115

def write(path, result)
  AtomicFile.write(path, JSON.pretty_generate(result.to_hash))
end