Module: SimpleCov::CLI::Affected

Extended by:
Affected, CommandHelpers
Included in:
Affected
Defined in:
lib/simplecov/cli/affected.rb,
lib/simplecov/cli/affected/runner.rb,
lib/simplecov/cli/affected/selection.rb,
lib/simplecov/cli/affected/changed_files.rb

Overview

simplecov affected: select the test files whose recorded tests touch the code changed since a git base ref, from a coverage.json written under track_tests. The change is everything between the merge base of --base and the working tree, plus untracked files.

The set intersection is the easy half; the hard half is knowing when to distrust the map, because a test map is stale the moment something changes that no test mentions by name. Any changed file outside the tracked set fails open to the full suite, out loud: each trigger is named on stderr and stdout stays empty, which a bare runner reads as "run everything". Test files always select themselves, recorded or not.

Defined Under Namespace

Modules: ChangedFiles, Selection, SpawnedRunner, SystemRunner

Constant Summary collapse

RUNNER =

simplecov:disable branch — fixed by the running engine and OS

(RUBY_ENGINE.eql?("jruby") && Gem.win_platform?) ? SystemRunner : SpawnedRunner

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

#deliver(selection, opts, stdout, stderr) ⇒ Object



91
92
93
94
95
96
97
98
99
# File 'lib/simplecov/cli/affected.rb', line 91

def deliver(selection, opts, stdout, stderr)
  full = !selection.fetch(:triggers).empty?
  selection.fetch(:triggers).each { |trigger| stderr.puts("simplecov affected: #{trigger}") }
  stderr.puts("simplecov affected: falling back to the full suite") if full
  return emit_json(selection, full, stdout) if opts.fetch(:json)
  return execute(selection, full, opts, stderr) if opts.fetch(:run)

  emit_text(selection, full, stdout, stderr)
end

#emit_json(selection, full, stdout) ⇒ Object



101
102
103
104
105
106
# File 'lib/simplecov/cli/affected.rb', line 101

def emit_json(selection, full, stdout)
  none = [] #: Array[String]
  stdout.puts(JSON.pretty_generate("full_suite" => full, "triggers" => selection.fetch(:triggers),
    "tests" => full ? none : selection.fetch(:tests)))
  0
end

#emit_text(selection, full, stdout, stderr) ⇒ Object

Stdout carries the selected files and nothing else, so the list can feed a runner directly, and a full-suite fallback prints nothing, which rspec $(simplecov affected) reads as "run everything".



111
112
113
114
115
116
117
118
119
120
121
# File 'lib/simplecov/cli/affected.rb', line 111

def emit_text(selection, full, stdout, stderr)
  return 0 if full

  tests = selection.fetch(:tests)
  if tests.empty?
    note_untouched(stderr)
  else
    tests.each { |file| stdout.puts(file) }
    0
  end
end

#execute(selection, full, opts, stderr) ⇒ Object



128
129
130
131
132
133
134
135
# File 'lib/simplecov/cli/affected.rb', line 128

def execute(selection, full, opts, stderr)
  return run_command(opts.fetch(:run), opts.fetch(:root), stderr) if full
  return note_untouched(stderr) if selection.fetch(:tests).empty?

  count = selection.fetch(:tests).size
  stderr.puts("simplecov affected: running #{count} test file#{"s" unless count.eql?(1)}")
  run_command(opts.fetch(:run) + selection.fetch(:tests), opts.fetch(:root), stderr)
end

#no_changes(opts, stdout, stderr) ⇒ Object



84
85
86
87
88
89
# File 'lib/simplecov/cli/affected.rb', line 84

def no_changes(opts, stdout, stderr)
  stderr.puts("simplecov affected: no changes against #{opts.fetch(:base)}")
  empty = {tests: [], triggers: []} #: Hash[Symbol, Array[String]]
  emit_json(empty, false, stdout) if opts.fetch(:json)
  0
end

#note_untouched(stderr) ⇒ Object



123
124
125
126
# File 'lib/simplecov/cli/affected.rb', line 123

def note_untouched(stderr)
  stderr.puts("simplecov affected: no recorded test touches the changed code")
  0
end

#parse(args) ⇒ Object



46
47
48
49
50
51
52
53
# File 'lib/simplecov/cli/affected.rb', line 46

def parse(args)
  head, runner = split_runner(args)
  opts, rest = parse_common(head, base: nil, run: runner) do |parser, into|
    parser.on("--base REF") { |v| into[:base] = v }
  end
  opts[:rest] = rest
  opts
end

#precheck(opts) ⇒ Object

A stray positional looks exactly like a forgotten --base ref, and silently dropping it would select tests for the wrong change.



66
67
68
69
70
71
72
# File 'lib/simplecov/cli/affected.rb', line 66

def precheck(opts)
  stray = opts.fetch(:rest).first
  return "unexpected argument #{stray.inspect} (did you mean `--base #{stray}`?)" if stray
  return "missing command after --run" if opts.fetch(:run) && opts.fetch(:run).empty?

  "--run and --json cannot be combined" if opts.fetch(:run) && opts.fetch(:json)
end

#respond(diffed, document, contexts, opts, stdout:, stderr:) ⇒ Object



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

def respond(diffed, document, contexts, opts, stdout:, stderr:)
  opts[:root] = diffed.fetch(:root)
  return no_changes(opts, stdout, stderr) if diffed.fetch(:changed).empty?

  selection = Selection.build(diffed.fetch(:changed), document, contexts, opts, stderr, root: diffed.fetch(:root))
  return 1 unless selection

  deliver(selection, opts, stdout, stderr)
end

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



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/simplecov/cli/affected.rb', line 29

def run(args, stdout:, stderr:)
  opts = parse(args)
  issue = precheck(opts)
  return error(stderr, issue) if issue

  document = CoverageFile.load_document(opts.fetch(:input), command: "affected", stderr: stderr)
  return 1 unless document

  contexts = recorded_contexts(document, opts, stderr)
  return 1 unless contexts

  diffed = ChangedFiles.call(opts[:base] ||= Git.default_base, stderr)
  return 1 unless diffed

  respond(diffed, document, contexts, opts, stdout: stdout, stderr: stderr)
end

#run_command(command, root, stderr) ⇒ Object

The command is named explicitly in the failure because not every engine's exception message carries it.



139
140
141
142
143
144
# File 'lib/simplecov/cli/affected.rb', line 139

def run_command(command, root, stderr)
  RUNNER.call(command, root)
rescue SystemCallError => e
  stderr.puts("simplecov affected: cannot run #{command.first.inspect} (#{e})")
  127
end

#split_runner(args) ⇒ Object

Everything after --run is the runner command, verbatim, so the command's own flags never collide with ours.



57
58
59
60
61
62
# File 'lib/simplecov/cli/affected.rb', line 57

def split_runner(args)
  index = args.index("--run")
  return [args, nil] unless index

  [args.take(index), args.drop(index + 1)]
end