mutant 0.10.4 → 0.10.9

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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mutant +0 -2
  3. data/lib/mutant.rb +7 -5
  4. data/lib/mutant/cli/command.rb +8 -6
  5. data/lib/mutant/cli/command/run.rb +23 -10
  6. data/lib/mutant/config.rb +80 -77
  7. data/lib/mutant/env.rb +14 -4
  8. data/lib/mutant/integration.rb +7 -10
  9. data/lib/mutant/integration/null.rb +0 -1
  10. data/lib/mutant/isolation.rb +11 -48
  11. data/lib/mutant/isolation/fork.rb +107 -40
  12. data/lib/mutant/isolation/none.rb +18 -5
  13. data/lib/mutant/license/subscription/commercial.rb +2 -3
  14. data/lib/mutant/license/subscription/opensource.rb +0 -1
  15. data/lib/mutant/matcher/config.rb +13 -0
  16. data/lib/mutant/matcher/method/instance.rb +0 -2
  17. data/lib/mutant/mutator/node/send.rb +1 -1
  18. data/lib/mutant/parallel.rb +0 -1
  19. data/lib/mutant/parallel/worker.rb +0 -2
  20. data/lib/mutant/reporter/cli.rb +0 -2
  21. data/lib/mutant/reporter/cli/printer/config.rb +9 -5
  22. data/lib/mutant/reporter/cli/printer/coverage_result.rb +19 -0
  23. data/lib/mutant/reporter/cli/printer/env_progress.rb +2 -0
  24. data/lib/mutant/reporter/cli/printer/isolation_result.rb +19 -35
  25. data/lib/mutant/reporter/cli/printer/mutation_result.rb +4 -9
  26. data/lib/mutant/reporter/cli/printer/subject_result.rb +2 -2
  27. data/lib/mutant/result.rb +91 -30
  28. data/lib/mutant/runner/sink.rb +12 -5
  29. data/lib/mutant/timer.rb +60 -11
  30. data/lib/mutant/transform.rb +25 -21
  31. data/lib/mutant/version.rb +1 -1
  32. data/lib/mutant/warnings.rb +0 -1
  33. data/lib/mutant/world.rb +67 -0
  34. metadata +12 -13
  35. data/lib/mutant/reporter/cli/printer/mutation_progress_result.rb +0 -28
  36. data/lib/mutant/reporter/cli/printer/subject_progress.rb +0 -58
  37. data/lib/mutant/reporter/cli/printer/test_result.rb +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4645aa0ee3e10159d6e91c5762b1f43bd11a94e229ea2cdec29af0f15d922f92
4
- data.tar.gz: 3c8eb88ab56bd83c1b4e26eb18ccb1c51f43875c3db5e36453661837e88b16b6
3
+ metadata.gz: e2d4849ffe2057370e38c3718afe4cbbfcbfaf76b058c4c5364ff2a7475a3122
4
+ data.tar.gz: 6e9abc09f95b929c6ee5b1f09745aa7ce01cc66ab70aaa3e044f2da5b897ce25
5
5
  SHA512:
6
- metadata.gz: 7e961ab0e43eb8208f8a9c5dc626fb3787e37f81fbf96be9f4e3da0fb2532556dcf93ed491bf29fbedc7c099c0a6ad4e81b4b32448e5615fe2fb37b1bb08dd2b
7
- data.tar.gz: c9d2e9344bcbfdeeb7a3108cfa5b94bae2b8527fdf03ae5f4a346f2d207371c40319a79f7a44782a0d8d98003834bf22d79188e46d555d9c18f1444d7fa3149a
6
+ metadata.gz: '050159a447e4f328e9283cea9785dc08bd51d6132995ff9ecda4cdfbda08f665f8294607f5eeaf390e9b3a069ae69dab675fc2767b1300973ec9bfa2b918b70a'
7
+ data.tar.gz: 582384118f7e8df7091280f6bc78e6d6f95f612f28a0892637d1e1a845c93754df29f5e463e8a1a69ca5f85ed95e666f82ab4ac957bf3a896d3f34fb26a5693b
data/bin/mutant CHANGED
@@ -10,7 +10,6 @@ require 'mutant'
10
10
 
11
11
  command = Mutant::CLI.parse(
12
12
  arguments: ARGV,
13
- config: Mutant::Config::DEFAULT,
14
13
  world: Mutant::WORLD
15
14
  )
16
15
 
@@ -40,7 +39,6 @@ status =
40
39
 
41
40
  Zombie::Mutant::CLI.parse(
42
41
  arguments: ARGV,
43
- config: Zombie::Mutant::Config::DEFAULT,
44
42
  world: Zombie::Mutant::WORLD
45
43
  ).call
46
44
  else
@@ -163,6 +163,7 @@ require 'mutant/integration/null'
163
163
  require 'mutant/selector'
164
164
  require 'mutant/selector/expression'
165
165
  require 'mutant/selector/null'
166
+ require 'mutant/world'
166
167
  require 'mutant/config'
167
168
  require 'mutant/cli'
168
169
  require 'mutant/cli/command'
@@ -178,16 +179,14 @@ require 'mutant/reporter/sequence'
178
179
  require 'mutant/reporter/cli'
179
180
  require 'mutant/reporter/cli/printer'
180
181
  require 'mutant/reporter/cli/printer/config'
182
+ require 'mutant/reporter/cli/printer/coverage_result'
181
183
  require 'mutant/reporter/cli/printer/env'
182
184
  require 'mutant/reporter/cli/printer/env_progress'
183
185
  require 'mutant/reporter/cli/printer/env_result'
184
186
  require 'mutant/reporter/cli/printer/isolation_result'
185
- require 'mutant/reporter/cli/printer/mutation_progress_result'
186
187
  require 'mutant/reporter/cli/printer/mutation_result'
187
188
  require 'mutant/reporter/cli/printer/status_progressive'
188
- require 'mutant/reporter/cli/printer/subject_progress'
189
189
  require 'mutant/reporter/cli/printer/subject_result'
190
- require 'mutant/reporter/cli/printer/test_result'
191
190
  require 'mutant/reporter/cli/format'
192
191
  require 'mutant/repository'
193
192
  require 'mutant/repository/diff'
@@ -218,12 +217,14 @@ module Mutant
218
217
  stderr: $stderr,
219
218
  stdout: $stdout,
220
219
  thread: Thread,
220
+ timer: Timer.new(Process),
221
221
  warnings: Warnings.new(Warning)
222
222
  )
223
223
 
224
224
  # Reopen class to initialize constant to avoid dep circle
225
225
  class Config
226
226
  DEFAULT = new(
227
+ coverage_criteria: Config::CoverageCriteria::DEFAULT,
227
228
  expression_parser: Expression::Parser.new([
228
229
  Expression::Method,
229
230
  Expression::Methods,
@@ -232,10 +233,11 @@ module Mutant
232
233
  ]),
233
234
  fail_fast: false,
234
235
  includes: EMPTY_ARRAY,
235
- integration: 'null',
236
+ integration: nil,
236
237
  isolation: Mutant::Isolation::Fork.new(WORLD),
237
- jobs: Etc.nprocessors,
238
+ jobs: nil,
238
239
  matcher: Matcher::Config::DEFAULT,
240
+ mutation_timeout: nil,
239
241
  reporter: Reporter::CLI.build(WORLD.stdout),
240
242
  requires: EMPTY_ARRAY,
241
243
  zombie: false
@@ -4,7 +4,7 @@ module Mutant
4
4
  module CLI
5
5
  # rubocop:disable Metrics/ClassLength
6
6
  class Command
7
- include AbstractType, Anima.new(:world, :config, :main, :parent, :arguments)
7
+ include AbstractType, Anima.new(:world, :main, :parent, :arguments)
8
8
 
9
9
  include Equalizer.new(:parent, :arguments)
10
10
 
@@ -105,7 +105,7 @@ module Mutant
105
105
  def parse
106
106
  Either
107
107
  .wrap_error(OptionParser::InvalidOption) { parser.order(arguments) }
108
- .lmap { |error| "#{full_name}: #{error}" }
108
+ .lmap(&method(:with_help))
109
109
  .bind(&method(:parse_remaining))
110
110
  end
111
111
 
@@ -159,9 +159,7 @@ module Mutant
159
159
  command_name, *arguments = arguments
160
160
 
161
161
  if command_name.nil?
162
- Either::Left.new(
163
- "Missing required subcommand!\n\n#{parser}"
164
- )
162
+ Either::Left.new(with_help('Missing required subcommand!'))
165
163
  else
166
164
  find_command(command_name).bind do |command|
167
165
  command.parse(**to_h, parent: self, arguments: arguments)
@@ -187,9 +185,13 @@ module Mutant
187
185
  if subcommand
188
186
  Either::Right.new(subcommand)
189
187
  else
190
- Either::Left.new("#{full_name}: Cannot find subcommand #{name.inspect}")
188
+ Either::Left.new(with_help("Cannot find subcommand #{name.inspect}"))
191
189
  end
192
190
  end
191
+
192
+ def with_help(message)
193
+ "#{full_name}: #{message}\n\n#{parser}"
194
+ end
193
195
  end # Command
194
196
  # rubocop:enable Metrics/ClassLength
195
197
  end # CLI
@@ -28,20 +28,30 @@ module Mutant
28
28
  #
29
29
  # @return [Bool]
30
30
  def zombie?
31
- config.zombie
31
+ @config.zombie
32
32
  end
33
33
 
34
34
  private
35
35
 
36
+ def initialize(attributes)
37
+ super(attributes)
38
+ @config = Config.env
39
+ end
40
+
36
41
  def execute
37
42
  soft_fail(License.apply(world))
38
- .bind { Config.load_config_file(world, config) }
39
- .bind { |cli_config| Bootstrap.apply(world, cli_config) }
43
+ .bind { Config.load_config_file(world) }
44
+ .fmap(&method(:expand))
45
+ .bind { Bootstrap.apply(world, @config) }
40
46
  .bind(&Runner.public_method(:apply))
41
47
  .from_right { |error| world.stderr.puts(error); return false }
42
48
  .success?
43
49
  end
44
50
 
51
+ def expand(file_config)
52
+ @config = @config.merge(file_config)
53
+ end
54
+
45
55
  def soft_fail(result)
46
56
  result.either(
47
57
  lambda do |message|
@@ -60,7 +70,7 @@ module Mutant
60
70
  end
61
71
 
62
72
  def parse_remaining_arguments(arguments)
63
- traverse(config.expression_parser.public_method(:apply), arguments)
73
+ traverse(@config.expression_parser.public_method(:apply), arguments)
64
74
  .fmap do |match_expressions|
65
75
  matcher(match_expressions: match_expressions)
66
76
  self
@@ -78,19 +88,19 @@ module Mutant
78
88
  end
79
89
 
80
90
  def set(**attributes)
81
- @config = config.with(attributes)
91
+ @config = @config.with(attributes)
82
92
  end
83
93
 
84
94
  def matcher(**attributes)
85
- set(matcher: config.matcher.with(attributes))
95
+ set(matcher: @config.matcher.with(attributes))
86
96
  end
87
97
 
88
98
  def add(attribute, value)
89
- set(attribute => config.public_send(attribute) + [value])
99
+ set(attribute => @config.public_send(attribute) + [value])
90
100
  end
91
101
 
92
102
  def add_matcher(attribute, value)
93
- set(matcher: config.matcher.add(attribute, value))
103
+ set(matcher: @config.matcher.add(attribute, value))
94
104
  end
95
105
 
96
106
  def add_environment_options(parser)
@@ -119,10 +129,10 @@ module Mutant
119
129
  parser.separator('Matcher:')
120
130
 
121
131
  parser.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
122
- add_matcher(:ignore_expressions, config.expression_parser.apply(pattern).from_right)
132
+ add_matcher(:ignore_expressions, @config.expression_parser.apply(pattern).from_right)
123
133
  end
124
134
  parser.on('--start-subject EXPRESSION', 'Start mutation testing at a specific subject') do |pattern|
125
- add_matcher(:start_expressions, config.expression_parser.apply(pattern).from_right)
135
+ add_matcher(:start_expressions, @config.expression_parser.apply(pattern).from_right)
126
136
  end
127
137
  parser.on('--since REVISION', 'Only select subjects touched since REVISION') do |revision|
128
138
  add_matcher(
@@ -143,6 +153,9 @@ module Mutant
143
153
  parser.on('-j', '--jobs NUMBER', 'Number of kill jobs. Defaults to number of processors.') do |number|
144
154
  set(jobs: Integer(number))
145
155
  end
156
+ parser.on('-t', '--mutation-timeout NUMBER', 'Per mutation analysis timeout') do |number|
157
+ set(mutation_timeout: Float(number))
158
+ end
146
159
  end
147
160
  end # Run
148
161
  # rubocop:enable Metrics/ClassLength
@@ -1,61 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mutant
4
- # The outer world IO objects mutant does interact with
5
- class World
6
- include Adamantium::Flat, Anima.new(
7
- :condition_variable,
8
- :gem,
9
- :gem_method,
10
- :io,
11
- :json,
12
- :kernel,
13
- :load_path,
14
- :marshal,
15
- :mutex,
16
- :object_space,
17
- :open3,
18
- :pathname,
19
- :process,
20
- :stderr,
21
- :stdout,
22
- :thread,
23
- :warnings
24
- )
25
-
26
- INSPECT = '#<Mutant::World>'
27
-
28
- private_constant(*constants(false))
29
-
30
- # Object inspection
31
- #
32
- # @return [String]
33
- def inspect
34
- INSPECT
35
- end
36
-
37
- # Capture stdout of a command
38
- #
39
- # @param [Array<String>] command
40
- #
41
- # @return [Either<String,String>]
42
- def capture_stdout(command)
43
- stdout, status = open3.capture2(*command, binmode: true)
44
-
45
- if status.success?
46
- Either::Right.new(stdout)
47
- else
48
- Either::Left.new("Command #{command} failed!")
49
- end
50
- end
51
- end # World
52
-
53
4
  # Standalone configuration of a mutant execution.
54
5
  #
55
6
  # Does not reference any "external" volatile state. The configuration applied
56
7
  # to current environment is being represented by the Mutant::Env object.
57
8
  class Config
58
9
  include Adamantium::Flat, Anima.new(
10
+ :coverage_criteria,
59
11
  :expression_parser,
60
12
  :fail_fast,
61
13
  :includes,
@@ -63,6 +15,7 @@ module Mutant
63
15
  :isolation,
64
16
  :jobs,
65
17
  :matcher,
18
+ :mutation_timeout,
66
19
  :reporter,
67
20
  :requires,
68
21
  :zombie
@@ -72,30 +25,6 @@ module Mutant
72
25
  define_method(:"#{name}?") { public_send(name) }
73
26
  end
74
27
 
75
- boolean = Transform::Boolean.new
76
- integer = Transform::Primitive.new(Integer)
77
- string = Transform::Primitive.new(String)
78
-
79
- string_array = Transform::Array.new(string)
80
-
81
- TRANSFORM = Transform::Sequence.new(
82
- [
83
- Transform::Exception.new(SystemCallError, :read.to_proc),
84
- Transform::Exception.new(YAML::SyntaxError, YAML.method(:safe_load)),
85
- Transform::Hash.new(
86
- optional: [
87
- Transform::Hash::Key.new('fail_fast', boolean),
88
- Transform::Hash::Key.new('includes', string_array),
89
- Transform::Hash::Key.new('integration', string),
90
- Transform::Hash::Key.new('jobs', integer),
91
- Transform::Hash::Key.new('requires', string_array)
92
- ],
93
- required: []
94
- ),
95
- Transform::Hash::Symbolize.new
96
- ]
97
- )
98
-
99
28
  MORE_THAN_ONE_CONFIG_FILE = <<~'MESSAGE'
100
29
  Found more than one candidate for use as implicit config file: %s
101
30
  MESSAGE
@@ -108,17 +37,62 @@ module Mutant
108
37
 
109
38
  private_constant(*constants(false))
110
39
 
40
+ class CoverageCriteria
41
+ include Anima.new(:process_abort, :test_result, :timeout)
42
+
43
+ DEFAULT = new(
44
+ process_abort: false,
45
+ test_result: true,
46
+ timeout: false
47
+ )
48
+
49
+ TRANSFORM =
50
+ Transform::Sequence.new(
51
+ [
52
+ Transform::Hash.new(
53
+ optional: [
54
+ Transform::Hash::Key.new('process_abort', Transform::BOOLEAN),
55
+ Transform::Hash::Key.new('test_result', Transform::BOOLEAN),
56
+ Transform::Hash::Key.new('timeout', Transform::BOOLEAN)
57
+ ],
58
+ required: []
59
+ ),
60
+ Transform::Hash::Symbolize.new,
61
+ ->(value) { Either::Right.new(DEFAULT.with(**value)) }
62
+ ]
63
+ )
64
+ end # CoverageCriteria
65
+
66
+ # Merge with other config
67
+ #
68
+ # @param [Config] other
69
+ #
70
+ # @return [Config]
71
+ def merge(other)
72
+ other.with(
73
+ fail_fast: fail_fast || other.fail_fast,
74
+ includes: other.includes + includes,
75
+ jobs: other.jobs || jobs,
76
+ integration: other.integration || integration,
77
+ mutation_timeout: other.mutation_timeout || mutation_timeout,
78
+ matcher: matcher.merge(other.matcher),
79
+ requires: other.requires + requires,
80
+ zombie: zombie || other.zombie
81
+ )
82
+ end
83
+
111
84
  # Load config file
112
85
  #
113
86
  # @param [World] world
114
87
  # @param [Config] config
115
88
  #
116
89
  # @return [Either<String,Config>]
117
- def self.load_config_file(world, config)
118
- files = CANDIDATES.map(&world.pathname.method(:new)).select(&:readable?)
90
+ def self.load_config_file(world)
91
+ config = DEFAULT
92
+ files = CANDIDATES.map(&world.pathname.public_method(:new)).select(&:readable?)
119
93
 
120
94
  if files.one?
121
- load_contents(files.first).fmap(&config.method(:with))
95
+ load_contents(files.first).fmap(&config.public_method(:with))
122
96
  elsif files.empty?
123
97
  Either::Right.new(config)
124
98
  else
@@ -129,9 +103,38 @@ module Mutant
129
103
  def self.load_contents(path)
130
104
  Transform::Named
131
105
  .new(path.to_s, TRANSFORM)
132
- .apply(path)
106
+ .call(path)
133
107
  .lmap(&:compact_message)
134
108
  end
135
109
  private_class_method :load_contents
110
+
111
+ # The configuration from the environment
112
+ #
113
+ # @return [Config]
114
+ def self.env
115
+ DEFAULT.with(jobs: Etc.nprocessors)
116
+ end
117
+
118
+ TRANSFORM = Transform::Sequence.new(
119
+ [
120
+ Transform::Exception.new(SystemCallError, :read.to_proc),
121
+ Transform::Exception.new(YAML::SyntaxError, YAML.method(:safe_load)),
122
+ Transform::Hash.new(
123
+ optional: [
124
+ Transform::Hash::Key.new('coverage_criteria', CoverageCriteria::TRANSFORM),
125
+ Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
126
+ Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
127
+ Transform::Hash::Key.new('integration', Transform::STRING),
128
+ Transform::Hash::Key.new('jobs', Transform::INTEGER),
129
+ Transform::Hash::Key.new('mutation_timeout', Transform::FLOAT),
130
+ Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
131
+ ],
132
+ required: []
133
+ ),
134
+ Transform::Hash::Symbolize.new
135
+ ]
136
+ )
137
+
138
+ private_constant(:TRANSFORM)
136
139
  end # Config
137
140
  end # Mutant
@@ -24,10 +24,15 @@ module Mutant
24
24
  # @param [Config] config
25
25
  #
26
26
  # @return [Env]
27
+ #
28
+ # rubocop:disable Metrics/MethodLength
27
29
  def self.empty(world, config)
28
30
  new(
29
31
  config: config,
30
- integration: Integration::Null.new(config),
32
+ integration: Integration::Null.new(
33
+ expression_parser: config.expression_parser,
34
+ timer: world.timer
35
+ ),
31
36
  matchable_scopes: EMPTY_ARRAY,
32
37
  mutations: EMPTY_ARRAY,
33
38
  parser: Parser.new,
@@ -36,6 +41,7 @@ module Mutant
36
41
  world: world
37
42
  )
38
43
  end
44
+ # rubocop:enable Metrics/MethodLength
39
45
 
40
46
  # Kill mutation
41
47
  #
@@ -43,14 +49,14 @@ module Mutant
43
49
  #
44
50
  # @return [Result::Mutation]
45
51
  def kill(mutation)
46
- start = Timer.now
52
+ start = timer.now
47
53
 
48
54
  tests = selections.fetch(mutation.subject)
49
55
 
50
56
  Result::Mutation.new(
51
57
  isolation_result: run_mutation_tests(mutation, tests),
52
58
  mutation: mutation,
53
- runtime: Timer.now - start
59
+ runtime: timer.now - start
54
60
  )
55
61
  end
56
62
 
@@ -127,7 +133,7 @@ module Mutant
127
133
  private
128
134
 
129
135
  def run_mutation_tests(mutation, tests)
130
- config.isolation.call do
136
+ config.isolation.call(config.mutation_timeout) do
131
137
  result = mutation.insert(world.kernel)
132
138
 
133
139
  if result.equal?(Loader::Result::VoidValue.instance)
@@ -138,5 +144,9 @@ module Mutant
138
144
  end
139
145
  end
140
146
 
147
+ def timer
148
+ world.timer
149
+ end
150
+
141
151
  end # Env
142
152
  end # Mutant