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
@@ -4,20 +4,39 @@ module Mutant
4
4
  # Abstract base class for mutant environments
5
5
  class Env
6
6
  include Adamantium::Flat, Anima.new(
7
- :actor_env,
8
7
  :config,
9
8
  :integration,
10
9
  :matchable_scopes,
11
10
  :mutations,
12
11
  :parser,
13
12
  :selector,
14
- :subjects
13
+ :subjects,
14
+ :world
15
15
  )
16
16
 
17
17
  SEMANTICS_MESSAGE =
18
18
  "Fix your lib to follow normal ruby semantics!\n" \
19
19
  '{Module,Class}#name should return resolvable constant name as String or nil'
20
20
 
21
+ # Construct minimal empty env
22
+ #
23
+ # @param [World] world
24
+ # @param [Config] config
25
+ #
26
+ # @return [Env]
27
+ def self.empty(world, config)
28
+ new(
29
+ config: config,
30
+ integration: Integration::Null.new(config),
31
+ matchable_scopes: EMPTY_ARRAY,
32
+ mutations: EMPTY_ARRAY,
33
+ parser: Parser.new,
34
+ selector: Selector::Null.new,
35
+ subjects: EMPTY_ARRAY,
36
+ world: world
37
+ )
38
+ end
39
+
21
40
  # Kill mutation
22
41
  #
23
42
  # @param [Mutation] mutation
@@ -26,8 +45,10 @@ module Mutant
26
45
  def kill(mutation)
27
46
  start = Timer.now
28
47
 
48
+ tests = selections.fetch(mutation.subject)
49
+
29
50
  Result::Mutation.new(
30
- isolation_result: run_mutation_tests(mutation),
51
+ isolation_result: run_mutation_tests(mutation, tests),
31
52
  mutation: mutation,
32
53
  runtime: Timer.now - start
33
54
  )
@@ -43,18 +64,83 @@ module Mutant
43
64
  end
44
65
  memoize :selections
45
66
 
67
+ # Emit warning
68
+ #
69
+ # @param [String] warning
70
+ #
71
+ # @return [self]
72
+ def warn(message)
73
+ config.reporter.warn(message)
74
+ self
75
+ end
76
+
77
+ # Selected tests
78
+ #
79
+ # @return [Set<Test>]
80
+ def selected_tests
81
+ selections.values.flatten.to_set
82
+ end
83
+ memoize :selected_tests
84
+
85
+ # Amount of mutations
86
+ #
87
+ # @return [Integer]
88
+ def amount_mutations
89
+ mutations.length
90
+ end
91
+ memoize :amount_mutations
92
+
93
+ # Amount of tests reachable by integration
94
+ #
95
+ # @return [Integer]
96
+ def amount_total_tests
97
+ integration.all_tests.length
98
+ end
99
+ memoize :amount_total_tests
100
+
101
+ # Amount of selected subjects
102
+ #
103
+ # @return [Integer]
104
+ def amount_subjects
105
+ subjects.length
106
+ end
107
+ memoize :amount_subjects
108
+
109
+ # Amount of selected tests
110
+ #
111
+ # @return [Integer]
112
+ def amount_selected_tests
113
+ selected_tests.length
114
+ end
115
+ memoize :amount_selected_tests
116
+
117
+ # Ratio between selected tests and subjects
118
+ #
119
+ # @return [Rational]
120
+ def test_subject_ratio
121
+ return Rational(0) if amount_subjects.zero?
122
+
123
+ Rational(amount_selected_tests, amount_subjects)
124
+ end
125
+ memoize :test_subject_ratio
126
+
46
127
  private
47
128
 
48
129
  # Kill mutation under isolation with integration
49
130
  #
50
- # @param [Isolation] isolation
51
- # @param [Integration] integration
131
+ # @param [Mutation] mutation
132
+ # @param [Array<Test>] test
52
133
  #
53
134
  # @return [Result::Isolation]
54
- def run_mutation_tests(mutation)
135
+ def run_mutation_tests(mutation, tests)
55
136
  config.isolation.call do
56
- mutation.insert(config.kernel)
57
- integration.call(selections.fetch(mutation.subject))
137
+ result = mutation.insert(world.kernel)
138
+
139
+ if result.equal?(Loader::Result::VoidValue.instance)
140
+ Result::Test::VoidValue.instance
141
+ else
142
+ integration.call(tests)
143
+ end
58
144
  end
59
145
  end
60
146
 
@@ -12,6 +12,11 @@ module Mutant
12
12
 
13
13
  private_constant(*constants(false))
14
14
 
15
+ # Syntax of expression
16
+ #
17
+ # @return [Matcher]
18
+ abstract_method :matcher
19
+
15
20
  # Syntax of expression
16
21
  #
17
22
  # @return [String]
@@ -23,7 +28,7 @@ module Mutant
23
28
  #
24
29
  # @return [Integer]
25
30
  def match_length(other)
26
- if eql?(other)
31
+ if syntax.eql?(other.syntax)
27
32
  syntax.length
28
33
  else
29
34
  0
@@ -5,45 +5,24 @@ module Mutant
5
5
  class Parser
6
6
  include Concord.new(:types)
7
7
 
8
- class ParserError < RuntimeError
9
- include AbstractType
10
- end # ParserError
11
-
12
- # Error raised on invalid expressions
13
- class InvalidExpressionError < ParserError; end
14
-
15
- # Error raised on ambiguous expressions
16
- class AmbiguousExpressionError < ParserError; end
17
-
18
- # Parse input into expression or raise
19
- #
20
- # @param [String] syntax
21
- #
22
- # @return [Expression]
23
- # if expression is valid
24
- #
25
- # @raise [ParserError]
26
- # otherwise
27
- def call(input)
28
- try_parse(input) or fail InvalidExpressionError, "Expression: #{input.inspect} is not valid"
29
- end
30
-
31
- # Try to parse input into expression
8
+ # Apply expression parsing
32
9
  #
33
10
  # @param [String] input
34
11
  #
35
- # @return [Expression]
12
+ # @return [Either<String, Expression>]
36
13
  # if expression is valid
37
14
  #
38
15
  # @return [nil]
39
16
  # otherwise
40
- def try_parse(input)
17
+ def apply(input)
41
18
  expressions = expressions(input)
42
19
  case expressions.length
43
- when 0, 1
44
- expressions.first
20
+ when 0
21
+ Either::Left.new("Expression: #{input.inspect} is invalid")
22
+ when 1
23
+ Either::Right.new(expressions.first)
45
24
  else
46
- fail AmbiguousExpressionError, "Ambiguous expression: #{input.inspect}"
25
+ Either::Left.new("Expression: #{input.inspect} is ambiguous")
47
26
  end
48
27
  end
49
28
 
@@ -57,8 +36,7 @@ module Mutant
57
36
  # if expressions can be parsed from input
58
37
  def expressions(input)
59
38
  types.each_with_object([]) do |type, aggregate|
60
- expression = type.try_parse(input)
61
- aggregate << expression if expression
39
+ expression = type.try_parse(input) and aggregate << expression
62
40
  end
63
41
  end
64
42
 
@@ -6,22 +6,76 @@ module Mutant
6
6
  class Integration
7
7
  include AbstractType, Adamantium::Flat, Concord.new(:config)
8
8
 
9
+ LOAD_MESSAGE = <<~'MESSAGE'
10
+ Unable to load integration mutant-%<integration_name>s:
11
+ %<exception>s
12
+ You may have to install the gem mutant-%<integration_name>s!
13
+ MESSAGE
14
+
15
+ CONST_MESSAGE = <<~'MESSAGE'
16
+ Unable to load integration mutant-%<integration_name>s:
17
+ %<exception>s
18
+ This is a bug in the integration you have to report.
19
+ The integration is supposed to define %<constant_name>s!
20
+ MESSAGE
21
+
22
+ private_constant(*constants(false))
23
+
9
24
  # Setup integration
10
25
  #
11
- # Integrations are supposed to define a constant under
12
- # Mutant::Integration named after the capitalized +name+
13
- # parameter.
26
+ # @param env [Bootstrap]
27
+ #
28
+ # @return [Either<String, Integration>]
29
+ def self.setup(env)
30
+ attempt_require(env)
31
+ .apply { attempt_const_get(env) }
32
+ .fmap { |klass| klass.new(env.config).setup }
33
+ end
34
+
35
+ # Attempt to require integration
36
+ #
37
+ # @param env [Bootstrap]
38
+ #
39
+ # @return [Either<String, undefined>]
40
+ #
41
+ # @api private
42
+ #
43
+ # rubocop:disable Style/MultilineBlockChain
44
+ def self.attempt_require(env)
45
+ integration_name = env.config.integration
46
+
47
+ Either.wrap_error(LoadError) do
48
+ env.world.kernel.require("mutant/integration/#{integration_name}")
49
+ end.lmap do |exception|
50
+ LOAD_MESSAGE % {
51
+ exception: exception.inspect,
52
+ integration_name: integration_name
53
+ }
54
+ end
55
+ end
56
+ private_class_method :attempt_require
57
+ # rubocop:enable Style/MultilineBlockChain
58
+
59
+ # Attempt const get
14
60
  #
15
- # This avoids having to maintain a mutable registry.
61
+ # @param env [Boostrap]
16
62
  #
17
- # @param kernel [Kernel]
18
- # @param name [String]
63
+ # @return [Either<String, Class<Integration>>]
19
64
  #
20
- # @return [Class<Integration>]
21
- def self.setup(kernel, name)
22
- kernel.require("mutant/integration/#{name}")
23
- const_get(name.capitalize)
65
+ # @api private
66
+ def self.attempt_const_get(env)
67
+ integration_name = env.config.integration
68
+ constant_name = integration_name.capitalize
69
+
70
+ Either.wrap_error(NameError) { const_get(constant_name) }.lmap do |exception|
71
+ CONST_MESSAGE % {
72
+ constant_name: "#{self}::#{constant_name}",
73
+ exception: exception.inspect,
74
+ integration_name: integration_name
75
+ }
76
+ end
24
77
  end
78
+ private_class_method :attempt_const_get
25
79
 
26
80
  # Perform integration setup
27
81
  #
@@ -50,31 +104,5 @@ module Mutant
50
104
  def expression_parser
51
105
  config.expression_parser
52
106
  end
53
-
54
- # Null integration that never kills a mutation
55
- class Null < self
56
-
57
- # Available tests for integration
58
- #
59
- # @return [Enumerable<Test>]
60
- def all_tests
61
- EMPTY_ARRAY
62
- end
63
-
64
- # Run a collection of tests
65
- #
66
- # @param [Enumerable<Mutant::Test>] tests
67
- #
68
- # @return [Result::Test]
69
- def call(tests)
70
- Result::Test.new(
71
- output: '',
72
- passed: true,
73
- runtime: 0.0,
74
- tests: tests
75
- )
76
- end
77
-
78
- end # Null
79
107
  end # Integration
80
108
  end # Mutant
@@ -9,6 +9,10 @@ module Mutant
9
9
  class Result
10
10
  include AbstractType, Adamantium
11
11
 
12
+ NULL_LOG = ''
13
+
14
+ private_constant(*constants(false))
15
+
12
16
  abstract_method :error
13
17
  abstract_method :next
14
18
  abstract_method :value
@@ -22,6 +26,13 @@ module Mutant
22
26
  ErrorChain.new(error, self)
23
27
  end
24
28
 
29
+ # The log captured from integration
30
+ #
31
+ # @return [String]
32
+ def log
33
+ NULL_LOG
34
+ end
35
+
25
36
  # Test for success
26
37
  #
27
38
  # @return [Boolean]
@@ -31,7 +42,11 @@ module Mutant
31
42
 
32
43
  # Succesful result producing value
33
44
  class Success < self
34
- include Concord::Public.new(:value)
45
+ include Concord::Public.new(:value, :log)
46
+
47
+ def self.new(_value, _log = '')
48
+ super
49
+ end
35
50
  end # Success
36
51
 
37
52
  # Unsuccessful result by unexpected exception
@@ -4,16 +4,15 @@ module Mutant
4
4
  class Isolation
5
5
  # Isolation via the fork(2) systemcall.
6
6
  class Fork < self
7
- include(
8
- Adamantium::Flat,
9
- Anima.new(:devnull, :io, :marshal, :process, :stderr, :stdout)
10
- )
7
+ include(Adamantium::Flat, Concord.new(:world))
11
8
 
12
- ATTRIBUTES = (anima.attribute_names + %i[block reader writer]).freeze
9
+ READ_SIZE = 4096
10
+
11
+ ATTRIBUTES = %i[block log_pipe result_pipe world].freeze
13
12
 
14
13
  # Unsucessful result as child exited nonzero
15
14
  class ChildError < Result
16
- include Concord::Public.new(:value)
15
+ include Concord::Public.new(:value, :log)
17
16
  end # ChildError
18
17
 
19
18
  # Unsucessful result as fork failed
@@ -21,6 +20,36 @@ module Mutant
21
20
  include Equalizer.new
22
21
  end # ForkError
23
22
 
23
+ # Pipe abstraction
24
+ class Pipe
25
+ include Adamantium::Flat, Anima.new(:reader, :writer)
26
+
27
+ # Run block with pipe in binmode
28
+ #
29
+ # @return [undefined]
30
+ def self.with(io)
31
+ io.pipe(binmode: true) do |(reader, writer)|
32
+ yield new(reader: reader, writer: writer)
33
+ end
34
+ end
35
+
36
+ # Child writer end of the pipe
37
+ #
38
+ # @return [IO]
39
+ def child
40
+ reader.close
41
+ writer
42
+ end
43
+
44
+ # Parent reader end of the pipe
45
+ #
46
+ # @return [IO]
47
+ def parent
48
+ writer.close
49
+ reader
50
+ end
51
+ end # Pipe
52
+
24
53
  # ignore :reek:InstanceVariableAssumption
25
54
  class Parent
26
55
  include(
@@ -33,9 +62,6 @@ module Mutant
33
62
 
34
63
  # Parent process
35
64
  #
36
- # @param [IO] reader
37
- # @param [IO] writer
38
- #
39
65
  # @return [Result]
40
66
  def call
41
67
  pid = start_child or return ForkError.new
@@ -51,7 +77,14 @@ module Mutant
51
77
  #
52
78
  # @return [Integer]
53
79
  def start_child
54
- process.fork { Child.call(to_h) }
80
+ world.process.fork do
81
+ Child.call(
82
+ to_h.merge(
83
+ log_pipe: log_pipe.child,
84
+ result_pipe: result_pipe.child
85
+ )
86
+ )
87
+ end
55
88
  end
56
89
 
57
90
  # Read child result
@@ -59,14 +92,46 @@ module Mutant
59
92
  # @param [Integer] pid
60
93
  #
61
94
  # @return [undefined]
95
+ #
96
+ # rubocop:disable Metrics/MethodLength
62
97
  def read_child_result(pid)
63
- writer.close
64
-
65
- add_result(Result::Success.new(marshal.load(reader)))
66
- rescue ArgumentError, EOFError => exception
67
- add_result(Result::Exception.new(exception))
98
+ result_fragments = []
99
+ log_fragments = []
100
+
101
+ read_fragments(
102
+ log_pipe.parent => log_fragments,
103
+ result_pipe.parent => result_fragments
104
+ )
105
+
106
+ begin
107
+ result = world.marshal.load(result_fragments.join)
108
+ rescue ArgumentError => exception
109
+ add_result(Result::Exception.new(exception))
110
+ else
111
+ add_result(Result::Success.new(result, log_fragments.join))
112
+ end
68
113
  ensure
69
- wait_child(pid)
114
+ wait_child(pid, log_fragments)
115
+ end
116
+ # rubocop:enable Metrics/MethodLength
117
+
118
+ # Read fragments
119
+ #
120
+ # @param [Hash{FD => Array<String}] targets
121
+ #
122
+ # @return [undefined]
123
+ def read_fragments(targets)
124
+ until targets.empty?
125
+ ready, = world.io.select(targets.keys)
126
+
127
+ ready.each do |fd|
128
+ if fd.eof?
129
+ targets.delete(fd)
130
+ else
131
+ targets.fetch(fd) << fd.read_nonblock(READ_SIZE)
132
+ end
133
+ end
134
+ end
70
135
  end
71
136
 
72
137
  # Wait for child process
@@ -74,10 +139,12 @@ module Mutant
74
139
  # @param [Integer] pid
75
140
  #
76
141
  # @return [undefined]
77
- def wait_child(pid)
78
- _pid, status = process.wait2(pid)
142
+ def wait_child(pid, log_fragments)
143
+ _pid, status = world.process.wait2(pid)
79
144
 
80
- add_result(ChildError.new(status)) unless status.success?
145
+ unless status.success? # rubocop:disable Style/GuardClause
146
+ add_result(ChildError.new(status, log_fragments.join))
147
+ end
81
148
  end
82
149
 
83
150
  # Add a result
@@ -97,29 +164,14 @@ module Mutant
97
164
 
98
165
  # Handle child process
99
166
  #
100
- # @param [IO] reader
101
- # @param [IO] writer
102
- #
103
167
  # @return [undefined]
104
168
  def call
105
- reader.close
106
- writer.binmode
107
- writer.syswrite(marshal.dump(result(&block)))
108
- writer.close
169
+ world.stderr.reopen(log_pipe)
170
+ world.stdout.reopen(log_pipe)
171
+ result_pipe.syswrite(world.marshal.dump(block.call))
172
+ result_pipe.close
109
173
  end
110
174
 
111
- private
112
-
113
- # The block result computed under silencing
114
- #
115
- # @return [Object]
116
- def result
117
- devnull.call do |null|
118
- stderr.reopen(null)
119
- stdout.reopen(null)
120
- yield
121
- end
122
- end
123
175
  end # Child
124
176
 
125
177
  private_constant(*(constants(false) - %i[ChildError ForkError]))
@@ -128,11 +180,24 @@ module Mutant
128
180
  #
129
181
  # @return [Result]
130
182
  # execution result
183
+ #
184
+ # ignore :reek:NestedIterators
185
+ #
186
+ # rubocop:disable Metrics/MethodLength
131
187
  def call(&block)
132
- io.pipe(binmode: true) do |(reader, writer)|
133
- Parent.call(to_h.merge(block: block, reader: reader, writer: writer))
188
+ io = world.io
189
+ Pipe.with(io) do |result|
190
+ Pipe.with(io) do |log|
191
+ Parent.call(
192
+ block: block,
193
+ log_pipe: log,
194
+ result_pipe: result,
195
+ world: world
196
+ )
197
+ end
134
198
  end
135
199
  end
200
+ # rubocop:enable Metrics/MethodLength
136
201
  end # Fork
137
202
  end # Isolation
138
203
  end # Mutant