mutant 0.10.4 → 0.10.9

Sign up to get free protection for your applications and to get access to all the features.
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