mutant 0.10.0 → 0.10.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) 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 +78 -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.rb +1 -1
  14. data/lib/mutant/license/subscription/commercial.rb +2 -3
  15. data/lib/mutant/license/subscription/opensource.rb +2 -2
  16. data/lib/mutant/matcher/config.rb +13 -0
  17. data/lib/mutant/matcher/method/instance.rb +0 -2
  18. data/lib/mutant/mutator/node/send.rb +1 -1
  19. data/lib/mutant/parallel.rb +0 -1
  20. data/lib/mutant/parallel/worker.rb +0 -2
  21. data/lib/mutant/reporter/cli.rb +0 -2
  22. data/lib/mutant/reporter/cli/printer/config.rb +9 -5
  23. data/lib/mutant/reporter/cli/printer/coverage_result.rb +19 -0
  24. data/lib/mutant/reporter/cli/printer/env_progress.rb +2 -0
  25. data/lib/mutant/reporter/cli/printer/isolation_result.rb +19 -35
  26. data/lib/mutant/reporter/cli/printer/mutation_result.rb +4 -9
  27. data/lib/mutant/reporter/cli/printer/subject_result.rb +2 -2
  28. data/lib/mutant/result.rb +81 -30
  29. data/lib/mutant/runner/sink.rb +12 -5
  30. data/lib/mutant/selector/expression.rb +3 -1
  31. data/lib/mutant/test.rb +1 -1
  32. data/lib/mutant/timer.rb +60 -11
  33. data/lib/mutant/transform.rb +25 -21
  34. data/lib/mutant/version.rb +1 -1
  35. data/lib/mutant/warnings.rb +0 -1
  36. data/lib/mutant/world.rb +67 -0
  37. metadata +13 -15
  38. data/lib/mutant/minitest/coverage.rb +0 -53
  39. data/lib/mutant/reporter/cli/printer/mutation_progress_result.rb +0 -28
  40. data/lib/mutant/reporter/cli/printer/subject_progress.rb +0 -58
  41. 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: fd38b14761b4836ca3ce54a9bd8ea7a25d1f4dac6506b08ab6f6c64227a0f71d
4
- data.tar.gz: dbed2538feb3240195e76b8b10895ea01ba7dc827820a6106d00fa4a55da97d1
3
+ metadata.gz: defe26105634d28a81b10e876713ab4e29a53a5d78ac8fe7e2d8a44d32b644c6
4
+ data.tar.gz: fad9645931798ed165a249e8e8d4a1c032896ceba6b823a1464ff2f60a7cadbb
5
5
  SHA512:
6
- metadata.gz: c70d6881956ace0d359b13dcfd41ef4eb92dbabcf5ee90d913790b8a09bb1b12877e32c85453d3be97d9fd2a1523a0af307ebf7318550cd0e3413fc8d9ecb765
7
- data.tar.gz: 27b59ee208993bfc5b128269658fc4465782052613b0641c9647ace34cc10b956c72c8e05a5b8a6f86af77112589295e3289e53192d973500fe43ceed84180fc
6
+ metadata.gz: 79da9f75f18510bbf81e43e2b98e43c4b92ddf2e6cb68b66828f9f4240f7d3e8747d6e475130f83a2d1edec8c1dda43a34f48524bd7439372e545a046099c28d
7
+ data.tar.gz: 7d8aac2776eca84aff051b7a146863bb271c5df4ec7dc344f041d3a22a17b4d87f9f48d5201e2dceec30f548adc7135d443ee78bc00764a9399fb677f2cf0aa5
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,60 @@ module Mutant
108
37
 
109
38
  private_constant(*constants(false))
110
39
 
40
+ class CoverageCriteria
41
+ include Anima.new(:timeout, :test_result)
42
+
43
+ DEFAULT = new(
44
+ timeout: false,
45
+ test_result: true
46
+ )
47
+
48
+ TRANSFORM =
49
+ Transform::Sequence.new(
50
+ [
51
+ Transform::Hash.new(
52
+ optional: [
53
+ Transform::Hash::Key.new('timeout', Transform::BOOLEAN),
54
+ Transform::Hash::Key.new('test_result', Transform::BOOLEAN)
55
+ ],
56
+ required: []
57
+ ),
58
+ Transform::Hash::Symbolize.new,
59
+ ->(value) { Either::Right.new(DEFAULT.with(**value)) }
60
+ ]
61
+ )
62
+ end # CoverageCriteria
63
+
64
+ # Merge with other config
65
+ #
66
+ # @param [Config] other
67
+ #
68
+ # @return [Config]
69
+ def merge(other)
70
+ other.with(
71
+ fail_fast: fail_fast || other.fail_fast,
72
+ includes: other.includes + includes,
73
+ jobs: other.jobs || jobs,
74
+ integration: other.integration || integration,
75
+ mutation_timeout: other.mutation_timeout || mutation_timeout,
76
+ matcher: matcher.merge(other.matcher),
77
+ requires: other.requires + requires,
78
+ zombie: zombie || other.zombie
79
+ )
80
+ end
81
+
111
82
  # Load config file
112
83
  #
113
84
  # @param [World] world
114
85
  # @param [Config] config
115
86
  #
116
87
  # @return [Either<String,Config>]
117
- def self.load_config_file(world, config)
118
- files = CANDIDATES.map(&world.pathname.method(:new)).select(&:readable?)
88
+ def self.load_config_file(world)
89
+ config = DEFAULT
90
+ files = CANDIDATES.map(&world.pathname.public_method(:new)).select(&:readable?)
119
91
 
120
92
  if files.one?
121
- load_contents(files.first).fmap(&config.method(:with))
93
+ load_contents(files.first).fmap(&config.public_method(:with))
122
94
  elsif files.empty?
123
95
  Either::Right.new(config)
124
96
  else
@@ -129,9 +101,38 @@ module Mutant
129
101
  def self.load_contents(path)
130
102
  Transform::Named
131
103
  .new(path.to_s, TRANSFORM)
132
- .apply(path)
104
+ .call(path)
133
105
  .lmap(&:compact_message)
134
106
  end
135
107
  private_class_method :load_contents
108
+
109
+ # The configuration from the environment
110
+ #
111
+ # @return [Config]
112
+ def self.env
113
+ DEFAULT.with(jobs: Etc.nprocessors)
114
+ end
115
+
116
+ TRANSFORM = Transform::Sequence.new(
117
+ [
118
+ Transform::Exception.new(SystemCallError, :read.to_proc),
119
+ Transform::Exception.new(YAML::SyntaxError, YAML.method(:safe_load)),
120
+ Transform::Hash.new(
121
+ optional: [
122
+ Transform::Hash::Key.new('coverage_criteria', CoverageCriteria::TRANSFORM),
123
+ Transform::Hash::Key.new('fail_fast', Transform::BOOLEAN),
124
+ Transform::Hash::Key.new('includes', Transform::STRING_ARRAY),
125
+ Transform::Hash::Key.new('integration', Transform::STRING),
126
+ Transform::Hash::Key.new('jobs', Transform::INTEGER),
127
+ Transform::Hash::Key.new('mutation_timeout', Transform::FLOAT),
128
+ Transform::Hash::Key.new('requires', Transform::STRING_ARRAY)
129
+ ],
130
+ required: []
131
+ ),
132
+ Transform::Hash::Symbolize.new
133
+ ]
134
+ )
135
+
136
+ private_constant(:TRANSFORM)
136
137
  end # Config
137
138
  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