mutineer 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,8 @@ module Mutineer
12
12
  # Child-process-only RSpec runner.
13
13
  #
14
14
  # Mirrors MinitestIntegration's contract: run the given spec files and
15
- # return 0 (all passed) or 1 (any failure).
15
+ # return 0 (all passed), 1 (an example failed) or 2 (only an error
16
+ # outside examples, such as a spec file that raised while loading).
16
17
  module RSpec
17
18
  # Runs the given RSpec files.
18
19
  #
@@ -21,7 +22,7 @@ module Mutineer
21
22
  # first failing example (RSpec `--fail-fast`).
22
23
  # @param record_to [IO, nil] a `--matrix` run: send each example's outcome
23
24
  # to this channel (see {KillFormatter}) and run every example.
24
- # @return [Integer] 0 on success, 1 on failure.
25
+ # @return [Integer] 0 on success, else {.failure_code}.
25
26
  # @raise [ArgumentError] when both options are given: a matrix run never stops.
26
27
  def self.run(spec_files, stop_at_first_failure: false, record_to: nil)
27
28
  if stop_at_first_failure && record_to
@@ -44,6 +45,7 @@ module Mutineer
44
45
  # silenced here: the fork boundary that calls this method has already
45
46
  # pointed stdout at File::NULL (see ChildStdout).
46
47
  sink = StringIO.new
48
+ keep_file_order(spec_files) if stop_at_first_failure || record_to
47
49
  args = ["--no-color"]
48
50
  args << "--fail-fast" if stop_at_first_failure
49
51
  status = with_fail_fast_off(record_to) do
@@ -55,7 +57,56 @@ module Mutineer
55
57
  # early, so `end` needs a reported outcome for every planned example.
56
58
  KillChannel.write_end(record_to) if record_to && KillFormatter.last&.saw_every_example?
57
59
 
58
- status.zero? ? 0 : 1
60
+ status.zero? ? 0 : failure_code
61
+ ensure
62
+ FileOrder.rank = nil
63
+ end
64
+
65
+ # The exit status of a failed run: 2 when no example failed and RSpec
66
+ # recorded an error outside examples, else 1.
67
+ #
68
+ # @api private
69
+ # @return [Integer] 1 or 2.
70
+ def self.failure_code
71
+ world = ::RSpec.world
72
+ outside_examples = world.respond_to?(:non_example_failure) && world.non_example_failure
73
+ outside_examples && ::RSpec.configuration.reporter.failed_examples.empty? ? 2 : 1
74
+ end
75
+
76
+ # Runs the top-level example groups in the order of `spec_files` (#203),
77
+ # after RSpec's own ordering, which then decides the order within a file.
78
+ #
79
+ # @api private
80
+ # @param spec_files [String, Array<String>] the files, in run order.
81
+ # @return [void]
82
+ def self.keep_file_order(spec_files)
83
+ FileOrder.rank = Array(spec_files).each_with_index.to_h { |f, i| [File.expand_path(f), i] }
84
+ ::RSpec::Core::World.prepend(FileOrder) unless ::RSpec::Core::World <= FileOrder
85
+ end
86
+
87
+ # Prepended on `RSpec::Core::World`: a stable sort of the ordered
88
+ # top-level groups by their file's position. Does nothing without a rank.
89
+ module FileOrder
90
+ class << self
91
+ # Each spec file's position, keyed by absolute path, or nil.
92
+ #
93
+ # @return [Hash{String => Integer}, nil]
94
+ attr_accessor :rank
95
+ end
96
+
97
+ # The groups RSpec ordered, sorted by file position.
98
+ #
99
+ # @return [Array<RSpec::Core::ExampleGroup>]
100
+ def ordered_example_groups
101
+ groups = super
102
+ rank = FileOrder.rank
103
+ return groups unless rank
104
+
105
+ groups.sort_by.with_index do |group, i|
106
+ file = group.metadata[:absolute_file_path] || File.expand_path(group.file_path)
107
+ [rank.fetch(file, rank.size), i]
108
+ end
109
+ end
59
110
  end
60
111
 
61
112
  # Runs the block with fail-fast forced off when `record_to` is set: a
@@ -7,8 +7,9 @@ module Mutineer
7
7
  # Picks the test-framework runner.
8
8
  #
9
9
  # Each runner responds to
10
- # `.run(files, stop_at_first_failure: false, record_to: nil) -> 0/1`
11
- # (0 = all passed, 1 = any failure) and is called only inside a forked child.
10
+ # `.run(files, stop_at_first_failure: false, record_to: nil) -> 0/1/2`
11
+ # (0 = all passed, 1 = any failure, 2 = only an error outside tests, which
12
+ # the RSpec runner reports) and is called only inside a forked child.
12
13
  # Only the mutant path passes `stop_at_first_failure: true`; a `--matrix`
13
14
  # mutant run passes `record_to:` instead (a {KillChannel} pipe) and runs every
14
15
  # test. The fork boundary silences stdout (see ChildStdout); the runners do not.
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "optparse"
5
+ require "yaml"
6
+
7
+ module Mutineer
8
+ # Prints `.mutineer.yml` `ignore:` entries for survivors in a JSON report.
9
+ # The command writes to stdout and does not edit files. The user pastes the
10
+ # lines under `ignore:`.
11
+ module Triage
12
+ # Usage line for a bad invocation.
13
+ USAGE = "Usage: mutineer triage REPORT.json --reason TEXT (--id ID ... | --all)"
14
+
15
+ # Reads argv, prints the entries, and returns the process exit code.
16
+ # 0 is success. 2 is a usage error.
17
+ #
18
+ # @param argv [Array<String>] arguments after the `triage` command.
19
+ # @param out [IO] where the entries go.
20
+ # @param err [IO] where usage errors go.
21
+ # @return [Integer] 0 or 2.
22
+ def self.run(argv, out: $stdout, err: $stderr)
23
+ reason = nil
24
+ ids = []
25
+ all = false
26
+ parser = OptionParser.new do |o|
27
+ o.banner = USAGE
28
+ o.on("--reason TEXT") { |text| reason = text }
29
+ o.on("--id ID") { |id| ids << id }
30
+ o.on("--all") { all = true }
31
+ end
32
+ begin
33
+ parser.parse!(argv)
34
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
35
+ err.puts "mutineer: #{e.message}"
36
+ return 2
37
+ end
38
+
39
+ report = argv.shift
40
+ if argv.any?
41
+ err.puts "mutineer: triage takes one report file"
42
+ return 2
43
+ end
44
+ if report.nil? || report.empty?
45
+ err.puts "mutineer: triage requires a JSON report file"
46
+ return 2
47
+ end
48
+ text = reason.to_s.strip
49
+ if reason.nil? || text.empty?
50
+ err.puts "mutineer: triage requires --reason TEXT"
51
+ return 2
52
+ end
53
+ if all && !ids.empty?
54
+ err.puts "mutineer: triage takes --id or --all, not both"
55
+ return 2
56
+ end
57
+ if !all && ids.empty?
58
+ err.puts "mutineer: triage requires --id ID or --all"
59
+ return 2
60
+ end
61
+
62
+ survivors = survivor_ids(report, err)
63
+ return 2 if survivors.nil?
64
+
65
+ chosen = all ? survivors : ids
66
+ missing = chosen.reject { |id| survivors.include?(id) }
67
+ unless missing.empty?
68
+ err.puts "mutineer: triage id is not a survivor: #{missing.join(', ')}"
69
+ return 2
70
+ end
71
+
72
+ entries = chosen.uniq.map { |id| { "id" => id, "reason" => text } }
73
+ if entries.empty?
74
+ err.puts "mutineer: triage has nothing to paste under ignore:"
75
+ return 0
76
+ end
77
+
78
+ body = YAML.dump(entries).sub(/\A---\n/, "")
79
+ out.write(body.end_with?("\n") ? body : "#{body}\n")
80
+ 0
81
+ end
82
+
83
+ # Survivor ids from a JSON report, in report order. Nil when the file
84
+ # cannot be used, after a message on `err`.
85
+ #
86
+ # @param path [String] report path.
87
+ # @param err [IO] usage errors.
88
+ # @return [Array<String>, nil]
89
+ def self.survivor_ids(path, err)
90
+ unless File.file?(path)
91
+ err.puts "mutineer: triage report not found: #{path}"
92
+ return nil
93
+ end
94
+
95
+ doc = JSON.parse(File.read(path))
96
+ rows = doc.is_a?(Hash) ? doc["survivors"] : nil
97
+ unless rows.is_a?(Array)
98
+ err.puts "mutineer: triage report has no survivors list"
99
+ return nil
100
+ end
101
+
102
+ rows.filter_map { |row| row["id"] if row.is_a?(Hash) && row["id"].is_a?(String) }
103
+ rescue JSON::ParserError => e
104
+ err.puts "mutineer: triage report is not JSON: #{e.message}"
105
+ nil
106
+ rescue SystemCallError
107
+ err.puts "mutineer: triage report cannot be read: #{path}"
108
+ nil
109
+ end
110
+ end
111
+ end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mutineer
4
4
  # Current Mutineer release version.
5
- VERSION = "1.5.0"
5
+ VERSION = "1.7.0"
6
6
  end
data/lib/mutineer.rb CHANGED
@@ -29,6 +29,7 @@ require_relative "mutineer/mutators/safe_navigation"
29
29
  require_relative "mutineer/mutators/range_literal"
30
30
  require_relative "mutineer/mutators/negation_removal"
31
31
  require_relative "mutineer/mutators/chain_link"
32
+ require_relative "mutineer/mutators/call_to_receiver"
32
33
  require_relative "mutineer/mutators/operand_removal"
33
34
  require_relative "mutineer/mutators/array_literal"
34
35
  require_relative "mutineer/mutators/condition_forcing"
@@ -36,10 +37,14 @@ require_relative "mutineer/mutators/operator_assignment"
36
37
  require_relative "mutineer/mutator_registry"
37
38
  require_relative "mutineer/worker_pool"
38
39
  require_relative "mutineer/progress"
40
+ require_relative "mutineer/job_plan"
39
41
  require_relative "mutineer/runner"
40
42
  require_relative "mutineer/kill_matrix"
41
43
  require_relative "mutineer/reporter"
42
44
  require_relative "mutineer/baseline"
45
+ require_relative "mutineer/triage"
46
+ require_relative "mutineer/migrate"
47
+ require_relative "mutineer/init"
43
48
  require_relative "mutineer/cli"
44
49
 
45
50
  # Mutineer is the top-level namespace for the mutation-testing library.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mutineer
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Teren
@@ -54,8 +54,10 @@ files:
54
54
  - lib/mutineer.rb
55
55
  - lib/mutineer/baseline.rb
56
56
  - lib/mutineer/changed_lines.rb
57
+ - lib/mutineer/child_coverage.rb
57
58
  - lib/mutineer/child_stdout.rb
58
59
  - lib/mutineer/cli.rb
60
+ - lib/mutineer/concern_blocks.rb
59
61
  - lib/mutineer/config.rb
60
62
  - lib/mutineer/coverage_map.rb
61
63
  - lib/mutineer/daemon_backend.rb
@@ -63,9 +65,12 @@ files:
63
65
  - lib/mutineer/daemon_server.rb
64
66
  - lib/mutineer/external_backend.rb
65
67
  - lib/mutineer/file_swap.rb
68
+ - lib/mutineer/init.rb
66
69
  - lib/mutineer/isolation.rb
70
+ - lib/mutineer/job_plan.rb
67
71
  - lib/mutineer/kill_channel.rb
68
72
  - lib/mutineer/kill_matrix.rb
73
+ - lib/mutineer/migrate.rb
69
74
  - lib/mutineer/minitest_integration.rb
70
75
  - lib/mutineer/minitest_integration/kill_recorder.rb
71
76
  - lib/mutineer/minitest_integration/outer_reporter.rb
@@ -78,6 +83,7 @@ files:
78
83
  - lib/mutineer/mutators/base.rb
79
84
  - lib/mutineer/mutators/boolean_connector.rb
80
85
  - lib/mutineer/mutators/boolean_literal.rb
86
+ - lib/mutineer/mutators/call_to_receiver.rb
81
87
  - lib/mutineer/mutators/chain_link.rb
82
88
  - lib/mutineer/mutators/collection_method.rb
83
89
  - lib/mutineer/mutators/comparison.rb
@@ -93,12 +99,15 @@ files:
93
99
  - lib/mutineer/mutators/safe_navigation.rb
94
100
  - lib/mutineer/mutators/statement_removal.rb
95
101
  - lib/mutineer/mutators/string_literal.rb
102
+ - lib/mutineer/orphan_guard.rb
96
103
  - lib/mutineer/pairing.rb
97
104
  - lib/mutineer/parser.rb
98
105
  - lib/mutineer/progress.rb
99
106
  - lib/mutineer/project.rb
100
107
  - lib/mutineer/project_path.rb
108
+ - lib/mutineer/rails_run_env.rb
101
109
  - lib/mutineer/rails_worker_db.rb
110
+ - lib/mutineer/redaction.rb
102
111
  - lib/mutineer/reporter.rb
103
112
  - lib/mutineer/result.rb
104
113
  - lib/mutineer/runner.rb
@@ -107,6 +116,7 @@ files:
107
116
  - lib/mutineer/test_runners.rb
108
117
  - lib/mutineer/test_runners/minitest.rb
109
118
  - lib/mutineer/test_runners/rspec.rb
119
+ - lib/mutineer/triage.rb
110
120
  - lib/mutineer/version.rb
111
121
  - lib/mutineer/worker_pool.rb
112
122
  homepage: https://github.com/davidteren/mutineer