Module: SimpleCov::CLI::Merge
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
- #commit(opts, result, stdout) ⇒ Object
- #duplicate_warning(command_name, paths) ⇒ Object
- #parse(args) ⇒ Object
-
#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.
- #parse_input_error(stderr, path, reason) ⇒ Object
-
#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.
-
#result_merger ⇒ Object
Loaded here rather than at the top of the file, so the read-only subcommands never pay for ResultMerger and its Coverage runtime guard.
- #run(args, stdout:, stderr:) ⇒ Object
-
#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.
- #write(path, result) ⇒ Object
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_merger ⇒ Object
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 |