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



100
101
102
103
104
# File 'lib/simplecov/cli/merge.rb', line 100

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



112
113
114
115
# File 'lib/simplecov/cli/merge.rb', line 112

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



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

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



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

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
66
# File 'lib/simplecov/cli/merge.rb', line 59

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



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.



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

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



106
107
108
109
110
# File 'lib/simplecov/cli/merge.rb', line 106

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