Module: SimpleCov::CLI::Affected

Extended by:
Affected, CommandHelpers
Included in:
Affected
Defined in:
lib/simplecov/cli/affected.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

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

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



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

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



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

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".



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

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



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

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



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

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



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

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

#parse(args) ⇒ Object



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

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.



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

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



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

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



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

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 selection's paths are relative to the repository root, so the runner starts there. The command is named explicitly in the failure because not every engine's exception message carries it.



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

def run_command(command, root, stderr)
  _, status = Process.wait2(spawn(*command, chdir: root))
  status.exitstatus || 1
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.



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

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

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