mutant 0.8.24 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (218) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +3 -3
  3. data/Changelog.md +14 -654
  4. data/Gemfile +13 -0
  5. data/Gemfile.lock +59 -64
  6. data/LICENSE +271 -20
  7. data/README.md +73 -140
  8. data/Rakefile +0 -21
  9. data/bin/mutant +7 -2
  10. data/config/reek.yml +2 -1
  11. data/config/rubocop.yml +5 -9
  12. data/docs/incremental.md +76 -0
  13. data/docs/known-problems.md +0 -14
  14. data/docs/mutant-minitest.md +1 -1
  15. data/docs/mutant-rspec.md +2 -24
  16. data/lib/mutant.rb +45 -53
  17. data/lib/mutant/ast/nodes.rb +0 -2
  18. data/lib/mutant/ast/types.rb +1 -117
  19. data/lib/mutant/base.rb +192 -0
  20. data/lib/mutant/bootstrap.rb +145 -0
  21. data/lib/mutant/cli.rb +68 -54
  22. data/lib/mutant/config.rb +119 -6
  23. data/lib/mutant/env.rb +94 -8
  24. data/lib/mutant/expression.rb +6 -1
  25. data/lib/mutant/expression/parser.rb +9 -31
  26. data/lib/mutant/integration.rb +64 -36
  27. data/lib/mutant/isolation.rb +16 -1
  28. data/lib/mutant/isolation/fork.rb +105 -40
  29. data/lib/mutant/license.rb +34 -0
  30. data/lib/mutant/license/subscription.rb +47 -0
  31. data/lib/mutant/license/subscription/commercial.rb +57 -0
  32. data/lib/mutant/license/subscription/opensource.rb +77 -0
  33. data/lib/mutant/loader.rb +27 -4
  34. data/lib/mutant/matcher.rb +48 -1
  35. data/lib/mutant/matcher/chain.rb +1 -1
  36. data/lib/mutant/matcher/config.rb +0 -2
  37. data/lib/mutant/matcher/filter.rb +1 -1
  38. data/lib/mutant/matcher/method.rb +11 -7
  39. data/lib/mutant/matcher/methods.rb +1 -1
  40. data/lib/mutant/matcher/namespace.rb +1 -1
  41. data/lib/mutant/matcher/null.rb +1 -1
  42. data/lib/mutant/matcher/scope.rb +1 -1
  43. data/lib/mutant/meta/example/dsl.rb +0 -8
  44. data/lib/mutant/mutation.rb +1 -2
  45. data/lib/mutant/mutator/node.rb +2 -9
  46. data/lib/mutant/mutator/node/arguments.rb +1 -1
  47. data/lib/mutant/mutator/node/class.rb +0 -8
  48. data/lib/mutant/mutator/node/define.rb +0 -12
  49. data/lib/mutant/mutator/node/generic.rb +30 -44
  50. data/lib/mutant/mutator/node/index.rb +4 -4
  51. data/lib/mutant/mutator/node/literal/regex.rb +0 -39
  52. data/lib/mutant/mutator/node/send.rb +13 -12
  53. data/lib/mutant/parallel.rb +61 -40
  54. data/lib/mutant/parallel/driver.rb +59 -0
  55. data/lib/mutant/parallel/source.rb +6 -2
  56. data/lib/mutant/parallel/worker.rb +63 -45
  57. data/lib/mutant/range.rb +15 -0
  58. data/lib/mutant/reporter/cli.rb +5 -11
  59. data/lib/mutant/reporter/cli/format.rb +3 -46
  60. data/lib/mutant/reporter/cli/printer/config.rb +5 -6
  61. data/lib/mutant/reporter/cli/printer/env.rb +40 -0
  62. data/lib/mutant/reporter/cli/printer/env_progress.rb +13 -17
  63. data/lib/mutant/reporter/cli/printer/isolation_result.rb +17 -3
  64. data/lib/mutant/reporter/cli/printer/mutation_result.rb +2 -3
  65. data/lib/mutant/reporter/cli/printer/status_progressive.rb +19 -10
  66. data/lib/mutant/repository.rb +0 -65
  67. data/lib/mutant/repository/diff.rb +104 -0
  68. data/lib/mutant/repository/diff/ranges.rb +52 -0
  69. data/lib/mutant/result.rb +16 -7
  70. data/lib/mutant/runner.rb +38 -47
  71. data/lib/mutant/runner/sink.rb +1 -1
  72. data/lib/mutant/selector/null.rb +19 -0
  73. data/lib/mutant/subject.rb +3 -1
  74. data/lib/mutant/subject/method/instance.rb +3 -1
  75. data/lib/mutant/transform.rb +511 -0
  76. data/lib/mutant/variable.rb +282 -0
  77. data/lib/mutant/version.rb +1 -1
  78. data/lib/mutant/warnings.rb +113 -0
  79. data/meta/case.rb +1 -0
  80. data/meta/class.rb +0 -9
  81. data/meta/def.rb +1 -26
  82. data/meta/regexp.rb +10 -20
  83. data/meta/send.rb +14 -46
  84. data/mutant-minitest.gemspec +1 -1
  85. data/mutant-rspec.gemspec +2 -2
  86. data/mutant.gemspec +15 -16
  87. data/mutant.yml +6 -0
  88. data/spec/integration/mutant/isolation/fork_spec.rb +22 -5
  89. data/spec/integration/mutant/minitest_spec.rb +3 -2
  90. data/spec/integration/mutant/rspec_spec.rb +4 -3
  91. data/spec/integrations.yml +16 -13
  92. data/spec/shared/base_behavior.rb +45 -0
  93. data/spec/shared/framework_integration_behavior.rb +43 -14
  94. data/spec/spec_helper.rb +21 -17
  95. data/spec/support/corpus.rb +56 -95
  96. data/spec/support/shared_context.rb +37 -14
  97. data/spec/support/xspec.rb +7 -3
  98. data/spec/unit/mutant/bootstrap_spec.rb +216 -0
  99. data/spec/unit/mutant/cli_spec.rb +173 -117
  100. data/spec/unit/mutant/config_spec.rb +126 -0
  101. data/spec/unit/mutant/either_spec.rb +247 -0
  102. data/spec/unit/mutant/env_spec.rb +162 -40
  103. data/spec/unit/mutant/expression/method_spec.rb +16 -0
  104. data/spec/unit/mutant/expression/parser_spec.rb +29 -33
  105. data/spec/unit/mutant/expression_spec.rb +5 -7
  106. data/spec/unit/mutant/integration_spec.rb +100 -9
  107. data/spec/unit/mutant/isolation/fork_spec.rb +125 -67
  108. data/spec/unit/mutant/isolation/result_spec.rb +33 -1
  109. data/spec/unit/mutant/license_spec.rb +257 -0
  110. data/spec/unit/mutant/loader_spec.rb +50 -11
  111. data/spec/unit/mutant/matcher/compiler_spec.rb +0 -78
  112. data/spec/unit/mutant/matcher/method/instance_spec.rb +55 -11
  113. data/spec/unit/mutant/matcher/method/singleton_spec.rb +12 -2
  114. data/spec/unit/mutant/matcher_spec.rb +102 -0
  115. data/spec/unit/mutant/maybe_spec.rb +60 -0
  116. data/spec/unit/mutant/meta/example/dsl_spec.rb +1 -17
  117. data/spec/unit/mutant/mutation_spec.rb +13 -6
  118. data/spec/unit/mutant/parallel/driver_spec.rb +112 -14
  119. data/spec/unit/mutant/parallel/source/array_spec.rb +25 -17
  120. data/spec/unit/mutant/parallel/worker_spec.rb +182 -44
  121. data/spec/unit/mutant/parallel_spec.rb +105 -8
  122. data/spec/unit/mutant/range_spec.rb +141 -0
  123. data/spec/unit/mutant/reporter/cli/printer/config_spec.rb +7 -21
  124. data/spec/unit/mutant/reporter/cli/printer/env_progress_spec.rb +15 -6
  125. data/spec/unit/mutant/reporter/cli/printer/env_result_spec.rb +10 -2
  126. data/spec/unit/mutant/reporter/cli/printer/isolation_result_spec.rb +12 -4
  127. data/spec/unit/mutant/reporter/cli/printer/mutation_result_spec.rb +31 -2
  128. data/spec/unit/mutant/reporter/cli/printer/status_progressive_spec.rb +4 -4
  129. data/spec/unit/mutant/reporter/cli/printer/subject_result_spec.rb +5 -0
  130. data/spec/unit/mutant/reporter/cli_spec.rb +46 -123
  131. data/spec/unit/mutant/repository/diff/ranges_spec.rb +180 -0
  132. data/spec/unit/mutant/repository/diff_spec.rb +84 -71
  133. data/spec/unit/mutant/require_highjack_spec.rb +1 -1
  134. data/spec/unit/mutant/result/env_spec.rb +39 -9
  135. data/spec/unit/mutant/result/test_spec.rb +14 -0
  136. data/spec/unit/mutant/runner_spec.rb +88 -41
  137. data/spec/unit/mutant/selector/expression_spec.rb +11 -10
  138. data/spec/unit/mutant/selector/null_spec.rb +17 -0
  139. data/spec/unit/mutant/subject/method/instance_spec.rb +44 -5
  140. data/spec/unit/mutant/subject/method/singleton_spec.rb +9 -2
  141. data/spec/unit/mutant/subject_spec.rb +9 -1
  142. data/spec/unit/mutant/transform/array_spec.rb +92 -0
  143. data/spec/unit/mutant/transform/bool_spec.rb +63 -0
  144. data/spec/unit/mutant/transform/error_spec.rb +132 -0
  145. data/spec/unit/mutant/transform/exception_spec.rb +44 -0
  146. data/spec/unit/mutant/transform/hash_spec.rb +236 -0
  147. data/spec/unit/mutant/transform/index_spec.rb +92 -0
  148. data/spec/unit/mutant/transform/named_spec.rb +49 -0
  149. data/spec/unit/mutant/transform/primitive_spec.rb +56 -0
  150. data/spec/unit/mutant/transform/sequence_spec.rb +98 -0
  151. data/spec/unit/mutant/variable_spec.rb +618 -0
  152. data/spec/unit/mutant/warnings_spec.rb +89 -0
  153. data/spec/unit/mutant/world_spec.rb +63 -0
  154. data/test_app/Gemfile.minitest +0 -2
  155. metadata +79 -113
  156. data/.gitattributes +0 -1
  157. data/.ruby-gemset +0 -1
  158. data/config/triage.yml +0 -2
  159. data/lib/mutant/actor.rb +0 -57
  160. data/lib/mutant/actor/env.rb +0 -31
  161. data/lib/mutant/actor/mailbox.rb +0 -34
  162. data/lib/mutant/actor/receiver.rb +0 -42
  163. data/lib/mutant/actor/sender.rb +0 -26
  164. data/lib/mutant/ast/meta/restarg.rb +0 -19
  165. data/lib/mutant/ast/regexp.rb +0 -42
  166. data/lib/mutant/ast/regexp/transformer.rb +0 -187
  167. data/lib/mutant/ast/regexp/transformer/direct.rb +0 -123
  168. data/lib/mutant/ast/regexp/transformer/named_group.rb +0 -59
  169. data/lib/mutant/ast/regexp/transformer/options_group.rb +0 -83
  170. data/lib/mutant/ast/regexp/transformer/quantifier.rb +0 -114
  171. data/lib/mutant/ast/regexp/transformer/recursive.rb +0 -58
  172. data/lib/mutant/ast/regexp/transformer/root.rb +0 -31
  173. data/lib/mutant/ast/regexp/transformer/text.rb +0 -60
  174. data/lib/mutant/env/bootstrap.rb +0 -160
  175. data/lib/mutant/matcher/compiler.rb +0 -60
  176. data/lib/mutant/mutator/node/regexp.rb +0 -35
  177. data/lib/mutant/mutator/node/regexp/alternation_meta.rb +0 -23
  178. data/lib/mutant/mutator/node/regexp/capture_group.rb +0 -28
  179. data/lib/mutant/mutator/node/regexp/character_type.rb +0 -32
  180. data/lib/mutant/mutator/node/regexp/end_of_line_anchor.rb +0 -23
  181. data/lib/mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor.rb +0 -23
  182. data/lib/mutant/mutator/node/regexp/greedy_zero_or_more.rb +0 -27
  183. data/lib/mutant/parallel/master.rb +0 -181
  184. data/lib/mutant/reporter/cli/printer/status.rb +0 -53
  185. data/lib/mutant/reporter/cli/tput.rb +0 -46
  186. data/lib/mutant/warning_filter.rb +0 -61
  187. data/meta/regexp/character_types.rb +0 -23
  188. data/meta/regexp/regexp_alternation_meta.rb +0 -13
  189. data/meta/regexp/regexp_bol_anchor.rb +0 -10
  190. data/meta/regexp/regexp_bos_anchor.rb +0 -18
  191. data/meta/regexp/regexp_capture_group.rb +0 -19
  192. data/meta/regexp/regexp_eol_anchor.rb +0 -10
  193. data/meta/regexp/regexp_eos_anchor.rb +0 -8
  194. data/meta/regexp/regexp_eos_ob_eol_anchor.rb +0 -10
  195. data/meta/regexp/regexp_greedy_zero_or_more.rb +0 -12
  196. data/meta/regexp/regexp_root_expression.rb +0 -10
  197. data/meta/restarg.rb +0 -10
  198. data/spec/support/fake_actor.rb +0 -111
  199. data/spec/support/warning.rb +0 -66
  200. data/spec/unit/mutant/actor/binding_spec.rb +0 -34
  201. data/spec/unit/mutant/actor/env_spec.rb +0 -31
  202. data/spec/unit/mutant/actor/mailbox_spec.rb +0 -28
  203. data/spec/unit/mutant/actor/message_spec.rb +0 -25
  204. data/spec/unit/mutant/actor/receiver_spec.rb +0 -58
  205. data/spec/unit/mutant/actor/sender_spec.rb +0 -24
  206. data/spec/unit/mutant/ast/regexp/parse_spec.rb +0 -19
  207. data/spec/unit/mutant/ast/regexp/transformer/lookup_table/table_spec.rb +0 -21
  208. data/spec/unit/mutant/ast/regexp/transformer/lookup_table_spec.rb +0 -35
  209. data/spec/unit/mutant/ast/regexp/transformer_spec.rb +0 -21
  210. data/spec/unit/mutant/ast/regexp_spec.rb +0 -704
  211. data/spec/unit/mutant/env/bootstrap_spec.rb +0 -188
  212. data/spec/unit/mutant/matcher/compiler/subject_prefix_spec.rb +0 -26
  213. data/spec/unit/mutant/parallel/master_spec.rb +0 -338
  214. data/spec/unit/mutant/reporter/cli/printer/status_spec.rb +0 -121
  215. data/spec/unit/mutant/reporter/cli/tput_spec.rb +0 -50
  216. data/spec/unit/mutant/warning_filter_spec.rb +0 -106
  217. data/spec/unit/mutant_spec.rb +0 -17
  218. data/test_app/Gemfile.rspec3.7 +0 -7
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ # Bootstrap process
5
+ #
6
+ # The role of the boostrap is to take the pure config and apply it against
7
+ # the impure world to produce an environment.
8
+ #
9
+ # env = config interpreted against the world
10
+ module Bootstrap
11
+ include Adamantium::Flat, Anima.new(:config, :parser, :world)
12
+
13
+ SEMANTICS_MESSAGE_FORMAT =
14
+ "%<message>s. Fix your lib to follow normal ruby semantics!\n" \
15
+ '{Module,Class}#name should return resolvable constant name as String or nil'
16
+
17
+ CLASS_NAME_RAISED_EXCEPTION =
18
+ '%<scope_class>s#name from: %<scope>s raised an error: %<exception>s'
19
+
20
+ CLASS_NAME_TYPE_MISMATCH_FORMAT =
21
+ '%<scope_class>s#name from: %<scope>s returned %<name>s'
22
+
23
+ private_constant(*constants(false))
24
+
25
+ # Run Bootstrap
26
+ #
27
+ # @param [World] world
28
+ # @param [Config] config
29
+ #
30
+ # @return [Either<String, Env>]
31
+ #
32
+ # rubocop:disable Metrics/MethodLength
33
+ def self.apply(world, config)
34
+ env = Env
35
+ .empty(world, config)
36
+ .tap(&method(:infect))
37
+ .with(matchable_scopes: matchable_scopes(world, config))
38
+
39
+ subjects = Matcher.from_config(env.config.matcher).call(env)
40
+
41
+ Integration.setup(env).fmap do |integration|
42
+ env.with(
43
+ integration: integration,
44
+ mutations: subjects.flat_map(&:mutations),
45
+ selector: Selector::Expression.new(integration),
46
+ subjects: subjects
47
+ )
48
+ end
49
+ end
50
+ # rubocop:enable Metrics/MethodLength
51
+
52
+ # Infect environment
53
+ #
54
+ # @return [undefined]
55
+ def self.infect(env)
56
+ config, world = env.config, env.world
57
+
58
+ config.includes.each(&world.load_path.method(:<<))
59
+ config.requires.each(&world.kernel.method(:require))
60
+ end
61
+ private_class_method :infect
62
+
63
+ # Matchable scopes
64
+ #
65
+ # @param [World] world
66
+ # @param [Config] config
67
+ #
68
+ # @return [Array<Scope>]
69
+ def self.matchable_scopes(world, config)
70
+ scopes = world.object_space.each_object(Module).each_with_object([]) do |scope, aggregate|
71
+ expression = expression(config.reporter, config.expression_parser, scope) || next
72
+ aggregate << Scope.new(scope, expression)
73
+ end
74
+
75
+ scopes.sort_by { |scope| scope.expression.syntax }
76
+ end
77
+ private_class_method :matchable_scopes
78
+
79
+ # Scope name from scoping object
80
+ #
81
+ # @param [Class, Module] scope
82
+ #
83
+ # @return [String]
84
+ # if scope has a name and does not raise exceptions obtaining it
85
+ #
86
+ # @return [nil]
87
+ # otherwise
88
+ def self.scope_name(reporter, scope)
89
+ scope.name
90
+ rescue => exception
91
+ semantics_warning(
92
+ reporter,
93
+ CLASS_NAME_RAISED_EXCEPTION,
94
+ exception: exception.inspect,
95
+ scope: scope,
96
+ scope_class: scope.class
97
+ )
98
+ nil
99
+ end
100
+ private_class_method :scope_name
101
+
102
+ # Try to turn scope into expression
103
+ #
104
+ # @param [Expression::Parser] expression_parser
105
+ # @param [Class, Module] scope
106
+ #
107
+ # @return [Expression]
108
+ # if scope can be represented in an expression
109
+ #
110
+ # @return [nil]
111
+ # otherwise
112
+ #
113
+ # rubocop:disable Metrics/MethodLength
114
+ #
115
+ # ignore :reek:LongParameterList
116
+ def self.expression(reporter, expression_parser, scope)
117
+ name = scope_name(reporter, scope) or return
118
+
119
+ unless name.instance_of?(String)
120
+ semantics_warning(
121
+ reporter,
122
+ CLASS_NAME_TYPE_MISMATCH_FORMAT,
123
+ name: name,
124
+ scope_class: scope.class,
125
+ scope: scope
126
+ )
127
+ return
128
+ end
129
+
130
+ expression_parser.apply(name).from_right {}
131
+ end
132
+ private_class_method :expression
133
+ # rubocop:enable Metrics/MethodLength
134
+
135
+ # Write a semantics warning
136
+ #
137
+ # @return [undefined]
138
+ #
139
+ # ignore :reek:LongParameterList
140
+ def self.semantics_warning(reporter, format, options)
141
+ reporter.warn(SEMANTICS_MESSAGE_FORMAT % { message: format % options })
142
+ end
143
+ private_class_method :semantics_warning
144
+ end # Bootstrap
145
+ end # Mutant
@@ -3,63 +3,91 @@
3
3
  module Mutant
4
4
  # Commandline parser / runner
5
5
  class CLI
6
- include Adamantium::Flat, Equalizer.new(:config), Procto.call(:config)
6
+ include Concord.new(:world, :config)
7
7
 
8
- # Error failed when CLI argv is invalid
9
- Error = Class.new(RuntimeError)
8
+ private_class_method :new
9
+
10
+ OPTIONS =
11
+ %i[
12
+ add_environment_options
13
+ add_mutation_options
14
+ add_filter_options
15
+ add_debug_options
16
+ ].freeze
17
+
18
+ private_constant(*constants(false))
10
19
 
11
20
  # Run cli with arguments
12
21
  #
13
- # @param [Array<String>] arguments
22
+ # @param [World] world
23
+ # the outside world
24
+ #
25
+ # @param [Config] default_config
26
+ # the default config
27
+ #
28
+ # @param [Array<String>]
29
+ # the user provided arguments
14
30
  #
15
31
  # @return [Boolean]
16
- def self.run(arguments)
17
- Runner.call(Env::Bootstrap.call(call(arguments))).success?
18
- rescue Error => exception
19
- $stderr.puts(exception.message)
20
- false
32
+ #
33
+ # rubocop:disable Style/Semicolon
34
+ #
35
+ # ignore :reek:LongParameterList
36
+ def self.run(world, default_config, arguments)
37
+ License
38
+ .apply(world)
39
+ .apply { Config.load_config_file(world, default_config) }
40
+ .apply { |file_config| apply(world, file_config, arguments) }
41
+ .apply { |cli_config| Bootstrap.apply(world, cli_config) }
42
+ .apply(&Runner.method(:apply))
43
+ .from_right { |error| world.stderr.puts(error); return false }
44
+ .success?
21
45
  end
46
+ # rubocop:enable Style/Semicolon
22
47
 
23
- # Initialize object
48
+ # Parse arguments into config
24
49
  #
25
- # @param [Array<String>]
50
+ # @param [World] world
51
+ # @param [Config] config
52
+ # @param [Array<String>] arguments
26
53
  #
27
- # @return [undefined]
28
- def initialize(arguments)
29
- @config = Config::DEFAULT
30
-
31
- parse(arguments)
54
+ # @return [Either<OptionParser::ParseError, Config>]
55
+ #
56
+ # ignore :reek:LongParameterList
57
+ def self.apply(world, config, arguments)
58
+ Either
59
+ .wrap_error(OptionParser::ParseError) { new(world, config).parse(arguments) }
60
+ .lmap(&:message)
32
61
  end
33
62
 
34
- # Config parsed from CLI
35
- #
36
- # @return [Config]
37
- attr_reader :config
38
-
39
- private
63
+ # Local opt out of option parser defaults
64
+ class OptionParser < ::OptionParser
65
+ # Kill defaults added by option parser that
66
+ # inference with ours under mutation testing.
67
+ define_method(:add_officious) {}
68
+ end # OptionParser
40
69
 
41
70
  # Parse the command-line options
42
71
  #
43
72
  # @param [Array<String>] arguments
44
73
  # Command-line options and arguments to be parsed.
45
74
  #
46
- # @fail [Error]
47
- # An error occurred while parsing the options.
48
- #
49
- # @return [undefined]
75
+ # @return [Config]
50
76
  def parse(arguments)
51
77
  opts = OptionParser.new do |builder|
52
78
  builder.banner = 'usage: mutant [options] MATCH_EXPRESSION ...'
53
- %i[add_environment_options add_mutation_options add_filter_options add_debug_options].each do |name|
79
+ OPTIONS.each do |name|
54
80
  __send__(name, builder)
55
81
  end
56
82
  end
57
83
 
58
- parse_match_expressions(opts.parse!(arguments))
59
- rescue OptionParser::ParseError => error
60
- raise(Error, error)
84
+ parse_match_expressions(opts.parse!(arguments.dup))
85
+
86
+ config
61
87
  end
62
88
 
89
+ private
90
+
63
91
  # Parse matchers
64
92
  #
65
93
  # @param [Array<String>] expressions
@@ -67,7 +95,7 @@ module Mutant
67
95
  # @return [undefined]
68
96
  def parse_match_expressions(expressions)
69
97
  expressions.each do |expression|
70
- add_matcher(:match_expressions, config.expression_parser.(expression))
98
+ add_matcher(:match_expressions, config.expression_parser.apply(expression).from_right)
71
99
  end
72
100
  end
73
101
 
@@ -94,17 +122,6 @@ module Mutant
94
122
  end
95
123
  end
96
124
 
97
- # Use integration
98
- #
99
- # @param [String] name
100
- #
101
- # @return [undefined]
102
- def setup_integration(name)
103
- with(integration: Integration.setup(config.kernel, name))
104
- rescue LoadError
105
- raise Error, "Could not load integration #{name.inspect} (you may want to try installing the gem mutant-#{name})"
106
- end
107
-
108
125
  # Add mutation options
109
126
  #
110
127
  # @param [OptionParser] opts
@@ -114,7 +131,9 @@ module Mutant
114
131
  opts.separator(nil)
115
132
  opts.separator('Options:')
116
133
 
117
- opts.on('--use INTEGRATION', 'Use INTEGRATION to kill mutations', &method(:setup_integration))
134
+ opts.on('--use INTEGRATION', 'Use INTEGRATION to kill mutations') do |name|
135
+ with(integration: name)
136
+ end
118
137
  end
119
138
 
120
139
  # Add filter options
@@ -124,17 +143,13 @@ module Mutant
124
143
  # @return [undefined]
125
144
  def add_filter_options(opts)
126
145
  opts.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
127
- add_matcher(:ignore_expressions, config.expression_parser.(pattern))
146
+ add_matcher(:ignore_expressions, config.expression_parser.apply(pattern).from_right)
128
147
  end
129
148
  opts.on('--since REVISION', 'Only select subjects touched since REVISION') do |revision|
130
149
  add_matcher(
131
150
  :subject_filters,
132
151
  Repository::SubjectFilter.new(
133
- Repository::Diff.new(
134
- config: config,
135
- from: Repository::Diff::HEAD,
136
- to: revision
137
- )
152
+ Repository::Diff.new(to: revision, world: world)
138
153
  )
139
154
  )
140
155
  end
@@ -150,12 +165,12 @@ module Mutant
150
165
  with(fail_fast: true)
151
166
  end
152
167
  opts.on('--version', 'Print mutants version') do
153
- puts("mutant-#{VERSION}")
154
- config.kernel.exit
168
+ world.stdout.puts("mutant-#{VERSION}")
169
+ world.kernel.exit
155
170
  end
156
171
  opts.on_tail('-h', '--help', 'Show this message') do
157
- puts(opts.to_s)
158
- config.kernel.exit
172
+ world.stdout.puts(opts.to_s)
173
+ world.kernel.exit
159
174
  end
160
175
  end
161
176
 
@@ -193,6 +208,5 @@ module Mutant
193
208
  def add_matcher(attribute, value)
194
209
  with(matcher: config.matcher.add(attribute, value))
195
210
  end
196
-
197
211
  end # CLI
198
212
  end # Mutant
@@ -1,6 +1,54 @@
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
+ :io,
10
+ :json,
11
+ :kernel,
12
+ :load_path,
13
+ :marshal,
14
+ :mutex,
15
+ :object_space,
16
+ :open3,
17
+ :pathname,
18
+ :process,
19
+ :stderr,
20
+ :stdout,
21
+ :thread,
22
+ :warnings
23
+ )
24
+
25
+ INSPECT = '#<Mutant::World>'
26
+
27
+ private_constant(*constants(false))
28
+
29
+ # Object inspection
30
+ #
31
+ # @return [String]
32
+ def inspect
33
+ INSPECT
34
+ end
35
+
36
+ # Capture stdout of a command
37
+ #
38
+ # @param [Array<String>] command
39
+ #
40
+ # @return [Either<String,String>]
41
+ def capture_stdout(command)
42
+ stdout, status = open3.capture2(*command, binmode: true)
43
+
44
+ if status.success?
45
+ Either::Right.new(stdout)
46
+ else
47
+ Either::Left.new("Command #{command} failed!")
48
+ end
49
+ end
50
+ end # World
51
+
4
52
  # Standalone configuration of a mutant execution.
5
53
  #
6
54
  # Does not reference any "external" volatile state. The configuration applied
@@ -9,17 +57,13 @@ module Mutant
9
57
  include Adamantium::Flat, Anima.new(
10
58
  :expression_parser,
11
59
  :fail_fast,
12
- :integration,
13
60
  :includes,
61
+ :integration,
14
62
  :isolation,
15
63
  :jobs,
16
- :kernel,
17
- :load_path,
18
64
  :matcher,
19
- :open3,
20
- :pathname,
21
- :requires,
22
65
  :reporter,
66
+ :requires,
23
67
  :zombie
24
68
  )
25
69
 
@@ -27,5 +71,74 @@ module Mutant
27
71
  define_method(:"#{name}?") { public_send(name) }
28
72
  end
29
73
 
74
+ boolean = Transform::Boolean.new
75
+ integer = Transform::Primitive.new(Integer)
76
+ string = Transform::Primitive.new(String)
77
+
78
+ string_array = Transform::Array.new(string)
79
+
80
+ TRANSFORM = Transform::Sequence.new(
81
+ [
82
+ Transform::Exception.new(SystemCallError, :read.to_proc),
83
+ Transform::Exception.new(YAML::SyntaxError, YAML.method(:safe_load)),
84
+ Transform::Hash.new(
85
+ optional: [
86
+ Transform::Hash::Key.new('fail_fast', boolean),
87
+ Transform::Hash::Key.new('includes', string_array),
88
+ Transform::Hash::Key.new('integration', string),
89
+ Transform::Hash::Key.new('jobs', integer),
90
+ Transform::Hash::Key.new('requires', string_array)
91
+ ],
92
+ required: []
93
+ ),
94
+ Transform::Hash::Symbolize.new
95
+ ]
96
+ )
97
+
98
+ MORE_THAN_ONE_CONFIG_FILE = <<~'MESSAGE'
99
+ Found more than one candidate for use as implicit config file: %s
100
+ MESSAGE
101
+
102
+ CANDIDATES = %w[
103
+ .mutant.yml
104
+ config/mutant.yml
105
+ mutant.yml
106
+ ].freeze
107
+
108
+ private_constant(*constants(false))
109
+
110
+ # Load config file
111
+ #
112
+ # @param [World] world
113
+ # @param [Config] config
114
+ #
115
+ # @return [Either<String,Config>]
116
+ def self.load_config_file(world, config)
117
+ files = CANDIDATES.map(&world.pathname.method(:new)).select(&:readable?)
118
+
119
+ if files.one?
120
+ load_contents(files.first).fmap(&config.method(:with))
121
+ elsif files.empty?
122
+ Either::Right.new(config)
123
+ else
124
+ Either::Left.new(MORE_THAN_ONE_CONFIG_FILE % files.join(', '))
125
+ end
126
+ end
127
+
128
+ # Load contents of file
129
+ #
130
+ # @param [Pathname] path
131
+ #
132
+ # @return [Config]
133
+ #
134
+ # @raise [Either<String, Hash{Symbol => Object}>]
135
+ # in case of config file error
136
+ def self.load_contents(path)
137
+ Transform::Named
138
+ .new(path.to_s, TRANSFORM)
139
+ .apply(path)
140
+ .lmap(&:compact_message)
141
+ end
142
+ private_class_method :load_contents
30
143
  end # Config
31
144
  end # Mutant