Module: SimpleCov::CLI::Merge

Extended by:
CommandHelpers, Merge
Included in:
Merge
Defined in:
lib/simplecov/cli/merge.rb

Overview

simplecov merge <files...>: wraps 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.

Constant Summary

Constants included from CommandHelpers

CommandHelpers::STATS_ROW_FORMAT

Instance Method Summary collapse

Methods included from CommandHelpers

build_parser, command_name, common_options, error, error_nil, on_help, one?, parse_common, quiet_option, recorded_contexts, stats_row

Instance Method Details

#commit(opts, result, stdout) ⇒ Object



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

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

#duplicate_warning(command_name, paths) ⇒ Object



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

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



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

def parse(args)
  opts = {output: CLI.default_resultset, honor_timeout: false, dry_run: false, quiet: false}
  files =
    build_parser 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 }
      quiet_option(o, opts)
    end.parse(args)
  opts.merge(files: files)
end

#parse_input(path, stderr) ⇒ Object

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



72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/simplecov/cli/merge.rb', line 72

def parse_input(path, stderr)
  data = JSON.parse(File.read(path))
  return data if data.instance_of?(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})")
rescue SystemCallError => e
  parse_input_error(stderr, path, "cannot be read (#{e.message.lines.first.to_s.rstrip})")
end

#parse_input_error(stderr, path, reason) ⇒ Object



85
86
87
# File 'lib/simplecov/cli/merge.rb', line 85

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

#parse_inputs(files, stderr) ⇒ Object

Validating every input up front turns ResultMerger's generic "no mergeable results" into a message that points at the specific input causing it.



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

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

#result_mergerObject

Loaded here rather than at the top of the file, so the read-only subcommands never pay for ResultMerger and its Coverage runtime guard.



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

def result_merger
  require "simplecov"
  ResultMerger
end

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



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

def run(args, stdout:, stderr:, **)
  opts = parse(args)
  return error(stderr, "missing input files") if opts.fetch(:files).empty?

  parsed = parse_inputs(opts.fetch(:files), stderr)
  return 1 unless parsed

  warn_about_duplicate_command_names(parsed, stderr)
  result = result_merger.merge_results(*opts.fetch(:files), ignore_timeout: !opts.fetch(:honor_timeout))
  return error(stderr, "no mergeable results in input files") unless result

  commit(opts, result, stdout)
  0
end

#warn_about_duplicate_command_names(parsed, stderr) ⇒ Object

ResultMerger folds entries that share a command_name together with last-write-wins on the timestamp, which is easy to mistake for "no merge happened", so the overlap is surfaced.



92
93
94
95
96
97
98
99
100
101
102
# File 'lib/simplecov/cli/merge.rb', line 92

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



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

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