mutant 0.10.16 → 0.10.21

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mutant.rb +2 -0
  3. data/lib/mutant/ast/meta/send.rb +0 -1
  4. data/lib/mutant/ast/types.rb +0 -9
  5. data/lib/mutant/bootstrap.rb +2 -2
  6. data/lib/mutant/cli/command/environment.rb +4 -4
  7. data/lib/mutant/cli/command/environment/run.rb +2 -2
  8. data/lib/mutant/cli/command/environment/show.rb +1 -2
  9. data/lib/mutant/cli/command/environment/subject.rb +39 -0
  10. data/lib/mutant/cli/command/root.rb +1 -1
  11. data/lib/mutant/cli/command/subscription.rb +1 -1
  12. data/lib/mutant/env.rb +8 -6
  13. data/lib/mutant/expression.rb +12 -3
  14. data/lib/mutant/expression/method.rb +33 -8
  15. data/lib/mutant/expression/methods.rb +6 -4
  16. data/lib/mutant/expression/namespace.rb +17 -6
  17. data/lib/mutant/expression/parser.rb +2 -2
  18. data/lib/mutant/integration/null.rb +2 -3
  19. data/lib/mutant/isolation/fork.rb +16 -5
  20. data/lib/mutant/license.rb +1 -1
  21. data/lib/mutant/license/subscription.rb +1 -1
  22. data/lib/mutant/license/subscription/commercial.rb +1 -1
  23. data/lib/mutant/license/subscription/opensource.rb +1 -1
  24. data/lib/mutant/mutator/node/arguments.rb +0 -2
  25. data/lib/mutant/mutator/node/defined.rb +12 -3
  26. data/lib/mutant/mutator/node/literal/symbol.rb +0 -2
  27. data/lib/mutant/mutator/node/procarg_zero.rb +0 -6
  28. data/lib/mutant/mutator/node/send.rb +20 -18
  29. data/lib/mutant/parallel.rb +43 -28
  30. data/lib/mutant/parallel/driver.rb +9 -3
  31. data/lib/mutant/parallel/worker.rb +60 -2
  32. data/lib/mutant/pipe.rb +94 -0
  33. data/lib/mutant/reporter/cli/printer/isolation_result.rb +1 -6
  34. data/lib/mutant/result.rb +9 -6
  35. data/lib/mutant/runner.rb +8 -11
  36. data/lib/mutant/runner/sink.rb +12 -2
  37. data/lib/mutant/test.rb +1 -1
  38. data/lib/mutant/timer.rb +0 -2
  39. data/lib/mutant/transform.rb +0 -2
  40. data/lib/mutant/version.rb +1 -1
  41. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 376273ed7dd5673bc6acc7d2393b3b9d05c5779a33b36fffce4a2fd93742e313
4
- data.tar.gz: 186a12fae490b557d0f48f879017a2faf61b2edfef09cd44d8e11549c34a8526
3
+ metadata.gz: ace60aadd53eaa9d10a47ad5bfd16cc0bd825be0588356fdfacc8e00ee767c20
4
+ data.tar.gz: 89f5a6460423c7ae12a735f55df13aa8e0077e76243a6fb0bf09d31babafc774
5
5
  SHA512:
6
- metadata.gz: 8f2a51ac2f6e515d9e4b96f3ede03773ae20c9038e9b4f31f639e733598cff0d86ed6d7020b727b167b5e518425ae90b97c1b53a5c3c26abbc6c417bf18eacd0
7
- data.tar.gz: bd2084f932c8267845e0371ff267d0acd2c8085684fdb7acb718253777adcd1b8098c5288c6978c4542ca9327bba0f62d8206ec90c8baad73be3fad46e471808
6
+ metadata.gz: 3dd24a1870b332e38dd4e40041d6ce60f1fe23bdf26490ace7bb8f5a285f927880a1898cf6e85d04055723aca9043fa382b19bc86ad29565bafd7a51258b1727
7
+ data.tar.gz: a855387e41f516caf8759eba4e0c0fe10f144dd4d5e9241bf3f41a768d3fde8bcee0ae70ff3b1ddaeeaf97aa8cd38301832c6b963d0c5eff8d245dc721638594
@@ -43,6 +43,7 @@ end # Mutant
43
43
  require 'mutant/bootstrap'
44
44
  require 'mutant/version'
45
45
  require 'mutant/env'
46
+ require 'mutant/pipe'
46
47
  require 'mutant/util'
47
48
  require 'mutant/registry'
48
49
  require 'mutant/ast'
@@ -171,6 +172,7 @@ require 'mutant/cli/command/subscription'
171
172
  require 'mutant/cli/command/environment'
172
173
  require 'mutant/cli/command/environment/run'
173
174
  require 'mutant/cli/command/environment/show'
175
+ require 'mutant/cli/command/environment/subject'
174
176
  require 'mutant/cli/command/root'
175
177
  require 'mutant/runner'
176
178
  require 'mutant/runner/sink'
@@ -13,7 +13,6 @@ module Mutant
13
13
 
14
14
  public :receiver, :selector
15
15
 
16
- INDEX_ASSIGNMENT_SELECTOR = :[]=
17
16
  ATTRIBUTE_ASSIGNMENT_SELECTOR_SUFFIX = '='
18
17
 
19
18
  # Arguments of mutated node
@@ -6,11 +6,6 @@ module Mutant
6
6
  module Types
7
7
  ASSIGNABLE_VARIABLES = Set.new(%i[ivasgn lvasgn cvasgn gvasgn]).freeze
8
8
 
9
- INDEX_ASSIGN_OPERATOR = :[]=
10
-
11
- # Set of nodes that cannot be on the LHS of an assignment
12
- NOT_ASSIGNABLE = Set.new(%i[int float str dstr class module self nil]).freeze
13
-
14
9
  # Set of op-assign types
15
10
  OP_ASSIGN = Set.new(%i[or_asgn and_asgn op_asgn]).freeze
16
11
  # Set of node types that are not valid when emitted standalone
@@ -53,10 +48,6 @@ module Mutant
53
48
  METHOD_OPERATORS - (INDEX_OPERATORS + UNARY_METHOD_OPERATORS)
54
49
  )
55
50
 
56
- OPERATOR_METHODS = Set.new(
57
- METHOD_OPERATORS + INDEX_OPERATORS + UNARY_METHOD_OPERATORS
58
- ).freeze
59
-
60
51
  # Nodes that are NOT handled by mutant.
61
52
  #
62
53
  # not - 1.8 only, mutant does not support 1.8
@@ -30,7 +30,7 @@ module Mutant
30
30
  # @return [Either<String, Env>]
31
31
  #
32
32
  # rubocop:disable Metrics/MethodLength
33
- def self.apply(world, config)
33
+ def self.call(world, config)
34
34
  env = Env
35
35
  .empty(world, config)
36
36
  .tap(&method(:infect))
@@ -109,7 +109,7 @@ module Mutant
109
109
  return
110
110
  end
111
111
 
112
- expression_parser.apply(name).from_right {}
112
+ expression_parser.call(name).from_right {}
113
113
  end
114
114
  private_class_method :expression
115
115
  # rubocop:enable Metrics/MethodLength
@@ -25,7 +25,7 @@ module Mutant
25
25
  def bootstrap
26
26
  Config.load_config_file(world)
27
27
  .fmap(&method(:expand))
28
- .bind { Bootstrap.apply(world, @config) }
28
+ .bind { Bootstrap.call(world, @config) }
29
29
  end
30
30
 
31
31
  def expand(file_config)
@@ -33,7 +33,7 @@ module Mutant
33
33
  end
34
34
 
35
35
  def parse_remaining_arguments(arguments)
36
- Mutant.traverse(@config.expression_parser.public_method(:apply), arguments)
36
+ Mutant.traverse(@config.expression_parser, arguments)
37
37
  .fmap do |match_expressions|
38
38
  matcher(match_expressions: match_expressions)
39
39
  self
@@ -82,10 +82,10 @@ module Mutant
82
82
  parser.separator('Matcher:')
83
83
 
84
84
  parser.on('--ignore-subject EXPRESSION', 'Ignore subjects that match EXPRESSION as prefix') do |pattern|
85
- add_matcher(:ignore_expressions, @config.expression_parser.apply(pattern).from_right)
85
+ add_matcher(:ignore_expressions, @config.expression_parser.call(pattern).from_right)
86
86
  end
87
87
  parser.on('--start-subject EXPRESSION', 'Start mutation testing at a specific subject') do |pattern|
88
- add_matcher(:start_expressions, @config.expression_parser.apply(pattern).from_right)
88
+ add_matcher(:start_expressions, @config.expression_parser.call(pattern).from_right)
89
89
  end
90
90
  parser.on('--since REVISION', 'Only select subjects touched since REVISION') do |revision|
91
91
  add_matcher(
@@ -26,9 +26,9 @@ module Mutant
26
26
  private
27
27
 
28
28
  def action
29
- soft_fail(License.apply(world))
29
+ soft_fail(License.call(world))
30
30
  .bind { bootstrap }
31
- .bind(&Runner.public_method(:apply))
31
+ .bind(&Runner.public_method(:call))
32
32
  .bind(&method(:from_result))
33
33
  end
34
34
 
@@ -12,8 +12,7 @@ module Mutant
12
12
  private
13
13
 
14
14
  def action
15
- bootstrap
16
- .fmap(&method(:report_env))
15
+ bootstrap.fmap(&method(:report_env))
17
16
  end
18
17
 
19
18
  def report_env(env)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module CLI
5
+ class Command
6
+ class Environment
7
+ class Subject < self
8
+ NAME = 'subject'
9
+ SHORT_DESCRIPTION = 'Subject subcommands'
10
+
11
+ class List < self
12
+ NAME = 'list'
13
+ SHORT_DESCRIPTION = 'List subjects'
14
+ SUBCOMMANDS = EMPTY_ARRAY
15
+
16
+ private
17
+
18
+ def action
19
+ bootstrap.fmap(&method(:list_subjects))
20
+ end
21
+
22
+ def list_subjects(env)
23
+ print('Subjects in environment: %d' % env.subjects.length)
24
+ env.subjects.each do |subject|
25
+ print(subject.expression.syntax)
26
+ end
27
+ end
28
+
29
+ def print(message)
30
+ world.stdout.puts(message)
31
+ end
32
+ end
33
+
34
+ SUBCOMMANDS = [List].freeze
35
+ end # Subject
36
+ end # Environment
37
+ end # Command
38
+ end # CLI
39
+ end # Mutant
@@ -4,7 +4,7 @@ module Mutant
4
4
  module CLI
5
5
  class Command
6
6
  class Environment < self
7
- SUBCOMMANDS = [Environment::Show].freeze
7
+ SUBCOMMANDS = [Environment::Subject, Environment::Show].freeze
8
8
  end # Environment
9
9
 
10
10
  class Root < self
@@ -10,7 +10,7 @@ module Mutant
10
10
  private
11
11
 
12
12
  def license
13
- License.apply(world)
13
+ License.call(world)
14
14
  end
15
15
 
16
16
  class Test < self
@@ -43,19 +43,21 @@ module Mutant
43
43
  end
44
44
  # rubocop:enable Metrics/MethodLength
45
45
 
46
- # Kill mutation
46
+ # Cover mutation with specific index
47
47
  #
48
- # @param [Mutation] mutation
48
+ # @param [Integer] mutation_index
49
49
  #
50
- # @return [Result::Mutation]
51
- def kill(mutation)
50
+ # @return [Result::MutationIndex]
51
+ def cover_index(mutation_index)
52
+ mutation = mutations.fetch(mutation_index)
53
+
52
54
  start = timer.now
53
55
 
54
56
  tests = selections.fetch(mutation.subject)
55
57
 
56
- Result::Mutation.new(
58
+ Result::MutationIndex.new(
57
59
  isolation_result: run_mutation_tests(mutation, tests),
58
- mutation: mutation,
60
+ mutation_index: mutation_index,
59
61
  runtime: timer.now - start
60
62
  )
61
63
  end
@@ -4,7 +4,7 @@ module Mutant
4
4
 
5
5
  # Abstract base class for match expression
6
6
  class Expression
7
- include AbstractType, Adamantium::Flat
7
+ include AbstractType
8
8
 
9
9
  fragment = /[A-Za-z][A-Za-z\d_]*/.freeze
10
10
  SCOPE_NAME_PATTERN = /(?<scope_name>#{fragment}(?:#{SCOPE_OPERATOR}#{fragment})*)/.freeze
@@ -12,6 +12,10 @@ module Mutant
12
12
 
13
13
  private_constant(*constants(false))
14
14
 
15
+ def self.new(*)
16
+ super.freeze
17
+ end
18
+
15
19
  # Syntax of expression
16
20
  #
17
21
  # @return [Matcher]
@@ -55,9 +59,14 @@ module Mutant
55
59
  # otherwise
56
60
  def self.try_parse(input)
57
61
  match = self::REGEXP.match(input)
58
- return unless match
62
+ from_match(match) if match
63
+ end
64
+
65
+ def self.from_match(match)
59
66
  names = anima.attribute_names
60
- new(Hash[names.zip(names.map(&match.method(:[])))])
67
+ new(Hash[names.zip(names.map(&match.public_method(:[])))])
61
68
  end
69
+ private_class_method :from_match
70
+
62
71
  end # Expression
63
72
  end # Mutant
@@ -5,6 +5,8 @@ module Mutant
5
5
 
6
6
  # Explicit method expression
7
7
  class Method < self
8
+ extend AST::Sexp
9
+
8
10
  include Anima.new(
9
11
  :method_name,
10
12
  :scope_name,
@@ -18,22 +20,21 @@ module Mutant
18
20
  '#' => [Matcher::Methods::Instance]
19
21
  )
20
22
 
21
- METHOD_NAME_PATTERN = Regexp.union(
22
- /(?<method_name>[A-Za-z_][A-Za-z\d_]*[!?=]?)/,
23
- *AST::Types::OPERATOR_METHODS.map(&:to_s)
24
- ).freeze
23
+ METHOD_NAME_PATTERN = /(?<method_name>.+)/.freeze
25
24
 
26
25
  private_constant(*constants(false))
27
26
 
28
27
  REGEXP = /\A#{SCOPE_NAME_PATTERN}#{SCOPE_SYMBOL_PATTERN}#{METHOD_NAME_PATTERN}\z/.freeze
29
28
 
29
+ def initialize(*)
30
+ super
31
+ @syntax = [scope_name, scope_symbol, method_name].join.freeze
32
+ end
33
+
30
34
  # Syntax of expression
31
35
  #
32
36
  # @return [String]
33
- def syntax
34
- [scope_name, scope_symbol, method_name].join
35
- end
36
- memoize :syntax
37
+ attr_reader :syntax
37
38
 
38
39
  # Matcher for expression
39
40
  #
@@ -47,6 +48,30 @@ module Mutant
47
48
  Matcher::Filter.new(methods_matcher, ->(subject) { subject.expression.eql?(self) })
48
49
  end
49
50
 
51
+ def self.try_parse(input)
52
+ match = REGEXP.match(input) or return
53
+
54
+ from_match(match) if valid_method_name?(match[:method_name])
55
+ end
56
+
57
+ # Test if string is a valid Ruby method name
58
+ #
59
+ # Note that this crazyness is indeed the "correct" solution.
60
+ #
61
+ # See: https://github.com/whitequark/parser/issues/213
62
+ #
63
+ # @param [String]
64
+ #
65
+ # @return [Boolean]
66
+ def self.valid_method_name?(name)
67
+ buffer = ::Parser::Source::Buffer.new(nil, source: "def #{name}; end")
68
+
69
+ ::Parser::CurrentRuby
70
+ .new
71
+ .parse(buffer).eql?(s(:def, name.to_sym, s(:args), nil))
72
+ end
73
+ private_class_method :valid_method_name?
74
+
50
75
  private
51
76
 
52
77
  def scope
@@ -20,13 +20,15 @@ module Mutant
20
20
 
21
21
  REGEXP = /\A#{SCOPE_NAME_PATTERN}#{SCOPE_SYMBOL_PATTERN}\z/.freeze
22
22
 
23
+ def initialize(*)
24
+ super
25
+ @syntax = [scope_name, scope_symbol].join.freeze
26
+ end
27
+
23
28
  # Syntax of expression
24
29
  #
25
30
  # @return [String]
26
- def syntax
27
- [scope_name, scope_symbol].join
28
- end
29
- memoize :syntax
31
+ attr_reader :syntax
30
32
 
31
33
  # Matcher on expression
32
34
  #
@@ -16,6 +16,9 @@ module Mutant
16
16
  # @return [undefined]
17
17
  def initialize(*)
18
18
  super
19
+
20
+ @syntax = "#{scope_name}*"
21
+
19
22
  @recursion_pattern = Regexp.union(
20
23
  /\A#{scope_name}\z/,
21
24
  /\A#{scope_name}::/,
@@ -26,10 +29,7 @@ module Mutant
26
29
  # Syntax for expression
27
30
  #
28
31
  # @return [String]
29
- def syntax
30
- "#{scope_name}*"
31
- end
32
- memoize :syntax
32
+ attr_reader :syntax
33
33
 
34
34
  # Matcher for expression
35
35
  #
@@ -52,7 +52,6 @@ module Mutant
52
52
  0
53
53
  end
54
54
  end
55
-
56
55
  end # Recursive
57
56
 
58
57
  # Exact namespace expression
@@ -67,7 +66,13 @@ module Mutant
67
66
  #
68
67
  # @return [Matcher]
69
68
  def matcher
70
- Matcher::Scope.new(Object.const_get(scope_name))
69
+ scope = find_scope
70
+
71
+ if scope
72
+ Matcher::Scope.new(scope)
73
+ else
74
+ Matcher::Null.new
75
+ end
71
76
  end
72
77
 
73
78
  # Syntax for expression
@@ -76,6 +81,12 @@ module Mutant
76
81
  alias_method :syntax, :scope_name
77
82
  public :syntax
78
83
 
84
+ private
85
+
86
+ def find_scope
87
+ Object.const_get(scope_name)
88
+ rescue NameError # rubocop:disable Lint/SuppressedException
89
+ end
79
90
  end # Exact
80
91
  end # Namespace
81
92
  end # Expression
@@ -5,7 +5,7 @@ module Mutant
5
5
  class Parser
6
6
  include Concord.new(:types)
7
7
 
8
- # Apply expression parsing
8
+ # Parse expression
9
9
  #
10
10
  # @param [String] input
11
11
  #
@@ -14,7 +14,7 @@ module Mutant
14
14
  #
15
15
  # @return [nil]
16
16
  # otherwise
17
- def apply(input)
17
+ def call(input)
18
18
  expressions = expressions(input)
19
19
  case expressions.length
20
20
  when 0
@@ -16,11 +16,10 @@ module Mutant
16
16
  # @param [Enumerable<Mutant::Test>] tests
17
17
  #
18
18
  # @return [Result::Test]
19
- def call(tests)
19
+ def call(_tests)
20
20
  Result::Test.new(
21
21
  passed: true,
22
- runtime: 0.0,
23
- tests: tests
22
+ runtime: 0.0
24
23
  )
25
24
  end
26
25
 
@@ -64,6 +64,7 @@ module Mutant
64
64
  end
65
65
  end # Pipe
66
66
 
67
+ # rubocop:disable Metrics/ClassLength
67
68
  class Parent
68
69
  include(
69
70
  Anima.new(*ATTRIBUTES),
@@ -149,17 +150,26 @@ module Mutant
149
150
 
150
151
  break unless ready
151
152
 
152
- ready.each do |fd|
153
- if fd.eof?
154
- targets.delete(fd)
153
+ ready.each do |target|
154
+ if target.eof?
155
+ targets.delete(target)
155
156
  else
156
- targets.fetch(fd) << fd.read_nonblock(READ_SIZE)
157
+ read_fragment(target, targets.fetch(target))
157
158
  end
158
159
  end
159
160
  end
160
161
  end
161
162
  # rubocop:enable Metrics/MethodLength
162
163
 
164
+ def read_fragment(target, fragments)
165
+ loop do
166
+ result = target.read_nonblock(READ_SIZE, exception: false)
167
+ break unless result.instance_of?(String)
168
+ fragments << result
169
+ break if result.bytesize < READ_SIZE
170
+ end
171
+ end
172
+
163
173
  # rubocop:disable Metrics/MethodLength
164
174
  def terminate_graceful
165
175
  status = nil
@@ -199,6 +209,7 @@ module Mutant
199
209
  @result = defined?(@result) ? @result.add_error(result) : result
200
210
  end
201
211
  end # Parent
212
+ # rubocop:enable Metrics/ClassLength
202
213
 
203
214
  class Child
204
215
  include(
@@ -219,7 +230,7 @@ module Mutant
219
230
 
220
231
  end # Child
221
232
 
222
- private_constant(*(constants(false) - %i[ChildError ForkError]))
233
+ private_constant(*constants(false))
223
234
 
224
235
  # Call block in isolation
225
236
  #
@@ -12,7 +12,7 @@ module Mutant
12
12
  # @return [Either<String,Subscription>]
13
13
  #
14
14
  # @api private
15
- def self.apply(world)
15
+ def self.call(world)
16
16
  load_mutant_license(world)
17
17
  .fmap { license_path(world) }
18
18
  .bind { |path| Subscription.load(world, world.json.load(path)) }
@@ -30,7 +30,7 @@ module Mutant
30
30
  'oss' => Opensource
31
31
  }.fetch(value.fetch('type'))
32
32
  .from_json(value.fetch('contents'))
33
- .apply(world)
33
+ .call(world)
34
34
  end
35
35
 
36
36
  # Subscription self description
@@ -15,7 +15,7 @@ module Mutant
15
15
  new(value.fetch('authors').map(&Author.public_method(:new)).to_set)
16
16
  end
17
17
 
18
- def apply(world)
18
+ def call(world)
19
19
  candidates = candidates(world)
20
20
 
21
21
  if (licensed & candidates).any?
@@ -50,7 +50,7 @@ module Mutant
50
50
  )
51
51
  end
52
52
 
53
- def apply(world)
53
+ def call(world)
54
54
  world
55
55
  .capture_stdout(%w[git remote --verbose])
56
56
  .fmap(&method(:parse_remotes))
@@ -8,8 +8,6 @@ module Mutant
8
8
 
9
9
  handle(:args)
10
10
 
11
- PROCARG = %i[restarg mlhs].freeze
12
-
13
11
  private
14
12
 
15
13
  def dispatch
@@ -13,10 +13,19 @@ module Mutant
13
13
  private
14
14
 
15
15
  def dispatch
16
- emit_singletons
17
- emit(N_TRUE)
16
+ emit(N_NIL)
17
+ emit_instance_variable_mutation
18
+ end
19
+
20
+ def emit_instance_variable_mutation
21
+ return unless n_ivar?(expression)
22
+
23
+ instance_variable_name = Mutant::Util.one(expression.children)
18
24
 
19
- emit_expression_mutations { |node| !n_self?(node) }
25
+ emit(
26
+ s(:send, nil, :instance_variable_defined?,
27
+ s(:sym, instance_variable_name))
28
+ )
20
29
  end
21
30
 
22
31
  end # Defined
@@ -11,8 +11,6 @@ module Mutant
11
11
 
12
12
  children :value
13
13
 
14
- PREFIX = '__mutant__'
15
-
16
14
  private
17
15
 
18
16
  def dispatch
@@ -4,12 +4,6 @@ module Mutant
4
4
  class Mutator
5
5
  class Node
6
6
  class ProcargZero < self
7
- MAP = {
8
- ::Parser::AST::Node => :emit_argument_node_mutations,
9
- Symbol => :emit_argument_symbol_mutations
10
- }.freeze
11
-
12
- private_constant(*constants(false))
13
7
 
14
8
  handle :procarg0
15
9
 
@@ -14,30 +14,32 @@ module Mutant
14
14
  children :receiver, :selector
15
15
 
16
16
  SELECTOR_REPLACEMENTS = IceNine.deep_freeze(
17
- reverse_map: %i[map each],
18
- kind_of?: %i[instance_of?],
17
+ :< => %i[== eql? equal?],
18
+ :<= => %i[< == eql? equal?],
19
+ :== => %i[eql? equal?],
20
+ :> => %i[== eql? equal?],
21
+ :>= => %i[> == eql? equal?],
22
+ __send__: %i[public_send],
23
+ all?: %i[any?],
24
+ any?: %i[all?],
25
+ at: %i[fetch key?],
26
+ eql?: %i[equal?],
27
+ fetch: %i[key?],
28
+ flat_map: %i[map],
29
+ gsub: %i[sub],
19
30
  is_a?: %i[instance_of?],
31
+ kind_of?: %i[instance_of?],
32
+ map: %i[each],
33
+ method: %i[public_method],
20
34
  reverse_each: %i[each],
35
+ reverse_map: %i[map each],
21
36
  reverse_merge: %i[merge],
22
- map: %i[each],
23
- flat_map: %i[map],
24
37
  send: %i[public_send __send__],
25
- __send__: %i[public_send],
26
- method: %i[public_method],
27
- gsub: %i[sub],
28
- eql?: %i[equal?],
29
- to_s: %i[to_str],
30
- to_i: %i[to_int],
31
38
  to_a: %i[to_ary],
32
39
  to_h: %i[to_hash],
33
- at: %i[fetch key?],
34
- fetch: %i[key?],
35
- values_at: %i[fetch_values],
36
- :== => %i[eql? equal?],
37
- :>= => %i[> == eql? equal?],
38
- :<= => %i[< == eql? equal?],
39
- :> => %i[== eql? equal?],
40
- :< => %i[== eql? equal?]
40
+ to_i: %i[to_int],
41
+ to_s: %i[to_str],
42
+ values_at: %i[fetch_values]
41
43
  )
42
44
 
43
45
  RECEIVER_SELECTOR_REPLACEMENTS = IceNine.deep_freeze(
@@ -6,45 +6,61 @@ module Mutant
6
6
 
7
7
  # Run async computation returning driver
8
8
  #
9
+ # @param [World] world
9
10
  # @param [Config] config
10
11
  #
11
12
  # @return [Driver]
12
- def self.async(config)
13
- shared = {
14
- var_active_jobs: shared(Variable::IVar, config, value: Set.new),
15
- var_final: shared(Variable::IVar, config),
16
- var_sink: shared(Variable::IVar, config, value: config.sink)
17
- }
13
+ def self.async(world, config)
14
+ shared = shared_state(world, config)
15
+ workers = workers(world, config, shared)
18
16
 
19
17
  Driver.new(
20
- threads: threads(config, worker(config, **shared)),
18
+ workers: workers,
19
+ threads: threads(world, config, workers),
21
20
  **shared
22
21
  )
23
22
  end
24
23
 
25
- # The worker
26
- #
27
- # @param [Config] config
28
- #
29
- # @return [Worker]
30
- def self.worker(config, **shared)
31
- Worker.new(
32
- processor: config.processor,
33
- var_running: shared(Variable::MVar, config, value: config.jobs),
34
- var_source: shared(Variable::IVar, config, value: config.source),
35
- **shared
36
- )
24
+ def self.workers(world, config, shared)
25
+ Array.new(config.jobs) do |index|
26
+ Worker.start(
27
+ block: config.block,
28
+ index: index,
29
+ process_name: "#{config.process_name}-#{index}",
30
+ world: world,
31
+ **shared
32
+ )
33
+ end
37
34
  end
35
+ private_class_method :workers
36
+
37
+ def self.shared_state(world, config)
38
+ {
39
+ var_active_jobs: shared(Variable::IVar, world, value: Set.new),
40
+ var_final: shared(Variable::IVar, world),
41
+ var_running: shared(Variable::MVar, world, value: config.jobs),
42
+ var_sink: shared(Variable::IVar, world, value: config.sink),
43
+ var_source: shared(Variable::IVar, world, value: config.source)
44
+ }
45
+ end
46
+ private_class_method :shared_state
47
+
48
+ def self.threads(world, config, workers)
49
+ thread = world.thread
38
50
 
39
- def self.threads(config, worker)
40
- Array.new(config.jobs) { config.thread.new(&worker.method(:call)) }
51
+ workers.map do |worker|
52
+ thread.new do
53
+ thread.current.name = "#{config.thread_name}-#{worker.index}"
54
+ worker.call
55
+ end
56
+ end
41
57
  end
42
58
  private_class_method :threads
43
59
 
44
- def self.shared(klass, config, **attributes)
60
+ def self.shared(klass, world, **attributes)
45
61
  klass.new(
46
- condition_variable: config.condition_variable,
47
- mutex: config.mutex,
62
+ condition_variable: world.condition_variable,
63
+ mutex: world.mutex,
48
64
  **attributes
49
65
  )
50
66
  end
@@ -75,13 +91,12 @@ module Mutant
75
91
  # Parallel run configuration
76
92
  class Config
77
93
  include Adamantium::Flat, Anima.new(
78
- :condition_variable,
94
+ :block,
79
95
  :jobs,
80
- :mutex,
81
- :processor,
96
+ :process_name,
82
97
  :sink,
83
98
  :source,
84
- :thread
99
+ :thread_name
85
100
  )
86
101
  end # Config
87
102
 
@@ -8,7 +8,10 @@ module Mutant
8
8
  :threads,
9
9
  :var_active_jobs,
10
10
  :var_final,
11
- :var_sink
11
+ :var_running,
12
+ :var_sink,
13
+ :var_source,
14
+ :workers
12
15
  )
13
16
 
14
17
  private(*anima.attribute_names)
@@ -29,7 +32,10 @@ module Mutant
29
32
 
30
33
  def finalize(status)
31
34
  status.tap do
32
- threads.each(&:join) if status.done?
35
+ if status.done?
36
+ workers.each(&:join)
37
+ threads.each(&:join)
38
+ end
33
39
  end
34
40
  end
35
41
 
@@ -38,7 +44,7 @@ module Mutant
38
44
  var_sink.with do |sink|
39
45
  Status.new(
40
46
  active_jobs: active_jobs.dup.freeze,
41
- done: threads.all? { |thread| !thread.alive? },
47
+ done: threads.all? { |worker| !worker.alive? },
42
48
  payload: sink.status
43
49
  )
44
50
  end
@@ -4,7 +4,10 @@ module Mutant
4
4
  module Parallel
5
5
  class Worker
6
6
  include Adamantium::Flat, Anima.new(
7
- :processor,
7
+ :connection,
8
+ :index,
9
+ :pid,
10
+ :process,
8
11
  :var_active_jobs,
9
12
  :var_final,
10
13
  :var_running,
@@ -14,6 +17,45 @@ module Mutant
14
17
 
15
18
  private(*anima.attribute_names)
16
19
 
20
+ public :index
21
+
22
+ # rubocop:disable Metrics/MethodLength
23
+ # rubocop:disable Metrics/ParameterLists
24
+ def self.start(world:, block:, process_name:, **attributes)
25
+ io = world.io
26
+ process = world.process
27
+
28
+ request = Pipe.from_io(io)
29
+ response = Pipe.from_io(io)
30
+
31
+ pid = process.fork do
32
+ world.thread.current.name = process_name
33
+ world.process.setproctitle(process_name)
34
+
35
+ Child.new(
36
+ block: block,
37
+ connection: Pipe::Connection.from_pipes(
38
+ marshal: world.marshal,
39
+ reader: request,
40
+ writer: response
41
+ )
42
+ ).call
43
+ end
44
+
45
+ new(
46
+ pid: pid,
47
+ process: process,
48
+ connection: Pipe::Connection.from_pipes(
49
+ marshal: world.marshal,
50
+ reader: response,
51
+ writer: request
52
+ ),
53
+ **attributes
54
+ )
55
+ end
56
+ # rubocop:enable Metrics/MethodLength
57
+ # rubocop:enable Metrics/ParameterLists
58
+
17
59
  # Run worker payload
18
60
  #
19
61
  # @return [self]
@@ -23,7 +65,7 @@ module Mutant
23
65
 
24
66
  job_start(job)
25
67
 
26
- result = processor.call(job.payload)
68
+ result = connection.call(job.payload)
27
69
 
28
70
  job_done(job)
29
71
 
@@ -35,6 +77,12 @@ module Mutant
35
77
  self
36
78
  end
37
79
 
80
+ def join
81
+ process.kill('TERM', pid)
82
+ process.wait(pid)
83
+ self
84
+ end
85
+
38
86
  private
39
87
 
40
88
  def next_job
@@ -66,6 +114,16 @@ module Mutant
66
114
  var_final.put(nil) if var_running.modify(&:pred).zero?
67
115
  end
68
116
 
117
+ class Child
118
+ include Anima.new(:block, :connection)
119
+
120
+ def call
121
+ loop do
122
+ connection.send_value(block.call(connection.receive_value))
123
+ end
124
+ end
125
+ end
126
+ private_constant :Child
69
127
  end # Worker
70
128
  end # Parallel
71
129
  end # Mutant
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ # Pipe abstraction
5
+ class Pipe
6
+ include Adamantium::Flat, Anima.new(:reader, :writer)
7
+
8
+ # Run block with pipe in binmode
9
+ #
10
+ # @return [undefined]
11
+ def self.with(io)
12
+ io.pipe(binmode: true) do |(reader, writer)|
13
+ yield new(reader: reader, writer: writer)
14
+ end
15
+ end
16
+
17
+ def self.from_io(io)
18
+ reader, writer = io.pipe(binmode: true)
19
+ new(reader: reader, writer: writer)
20
+ end
21
+
22
+ # Writer end of the pipe
23
+ #
24
+ # @return [IO]
25
+ def to_writer
26
+ reader.close
27
+ writer
28
+ end
29
+
30
+ # Parent reader end of the pipe
31
+ #
32
+ # @return [IO]
33
+ def to_reader
34
+ writer.close
35
+ reader
36
+ end
37
+
38
+ class Connection
39
+ include Anima.new(:marshal, :reader, :writer)
40
+
41
+ Error = Class.new(RuntimeError)
42
+
43
+ class Frame
44
+ include Concord.new(:io)
45
+
46
+ HEADER_FORMAT = 'N'
47
+ MAX_BYTES = (2**32).pred
48
+ HEADER_SIZE = 4
49
+
50
+ def receive_value
51
+ header = read(HEADER_SIZE)
52
+ read(Util.one(header.unpack(HEADER_FORMAT)))
53
+ end
54
+
55
+ def send_value(body)
56
+ bytesize = body.bytesize
57
+
58
+ fail Error, 'message to big' if bytesize > MAX_BYTES
59
+
60
+ io.write([bytesize].pack(HEADER_FORMAT))
61
+ io.write(body)
62
+ end
63
+
64
+ private
65
+
66
+ def read(bytes)
67
+ io.read(bytes) or fail Error, 'Unexpected EOF'
68
+ end
69
+ end
70
+
71
+ def call(payload)
72
+ send_value(payload)
73
+ receive_value
74
+ end
75
+
76
+ def receive_value
77
+ marshal.load(reader.receive_value)
78
+ end
79
+
80
+ def send_value(value)
81
+ writer.send_value(marshal.dump(value))
82
+ self
83
+ end
84
+
85
+ def self.from_pipes(marshal:, reader:, writer:)
86
+ new(
87
+ marshal: marshal,
88
+ reader: Frame.new(reader.to_reader),
89
+ writer: Frame.new(writer.to_writer)
90
+ )
91
+ end
92
+ end
93
+ end # Pipe
94
+ end # Mutant
@@ -12,11 +12,6 @@ module Mutant
12
12
  %s
13
13
  MESSAGE
14
14
 
15
- LOG_MESSAGES = <<~'MESSAGE'
16
- Log messages (combined stderr and stdout):
17
- %s
18
- MESSAGE
19
-
20
15
  EXCEPTION_ERROR_MESSAGE = <<~'MESSAGE'
21
16
  Killing the mutation resulted in an integration error.
22
17
  This is the case when the tests selected for the current mutation
@@ -36,7 +31,7 @@ module Mutant
36
31
  ```
37
32
  MESSAGE
38
33
 
39
- TIMEOUT_ERROR_MESSAGE =<<~'MESSAGE'
34
+ TIMEOUT_ERROR_MESSAGE = <<~'MESSAGE'
40
35
  Mutation analysis ran into the configured timeout of %0.9<timeout>g seconds.
41
36
  MESSAGE
42
37
 
@@ -121,11 +121,7 @@ module Mutant
121
121
 
122
122
  # Test result
123
123
  class Test
124
- include Result, Anima.new(
125
- :passed,
126
- :runtime,
127
- :tests
128
- )
124
+ include Anima.new(:passed, :runtime)
129
125
 
130
126
  class VoidValue < self
131
127
  include Singleton
@@ -137,7 +133,6 @@ module Mutant
137
133
  super(
138
134
  passed: false,
139
135
  runtime: 0.0,
140
- tests: []
141
136
  )
142
137
  end
143
138
  end # VoidValue
@@ -237,6 +232,14 @@ module Mutant
237
232
  end
238
233
  end
239
234
 
235
+ class MutationIndex
236
+ include Anima.new(
237
+ :isolation_result,
238
+ :mutation_index,
239
+ :runtime
240
+ )
241
+ end # MutationIndex
242
+
240
243
  # Mutation result
241
244
  class Mutation
242
245
  include Result, Anima.new(
@@ -6,7 +6,7 @@ module Mutant
6
6
  # Run against env
7
7
  #
8
8
  # @return [Either<String, Result>]
9
- def self.apply(env)
9
+ def self.call(env)
10
10
  reporter(env).start(env)
11
11
 
12
12
  Either::Right.new(run_mutation_analysis(env))
@@ -17,7 +17,7 @@ module Mutant
17
17
 
18
18
  run_driver(
19
19
  reporter,
20
- Parallel.async(mutation_test_config(env))
20
+ Parallel.async(env.world, mutation_test_config(env))
21
21
  ).tap do |result|
22
22
  reporter.report(result)
23
23
  end
@@ -34,16 +34,13 @@ module Mutant
34
34
  private_class_method :run_driver
35
35
 
36
36
  def self.mutation_test_config(env)
37
- world = env.world
38
-
39
37
  Parallel::Config.new(
40
- condition_variable: world.condition_variable,
41
- jobs: env.config.jobs,
42
- mutex: world.mutex,
43
- processor: env.method(:kill),
44
- sink: Sink.new(env),
45
- source: Parallel::Source::Array.new(env.mutations),
46
- thread: world.thread
38
+ block: env.method(:cover_index),
39
+ jobs: env.config.jobs,
40
+ process_name: 'mutant-worker-process',
41
+ sink: Sink.new(env),
42
+ source: Parallel::Source::Array.new(env.mutations.each_index.to_a),
43
+ thread_name: 'mutant-worker-thread'
47
44
  )
48
45
  end
49
46
  private_class_method :mutation_test_config
@@ -34,10 +34,12 @@ module Mutant
34
34
 
35
35
  # Handle mutation finish
36
36
  #
37
- # @param [Result::Mutation] mutation_result
37
+ # @param [Result::MutationIndex] mutation_index_result
38
38
  #
39
39
  # @return [self]
40
- def result(mutation_result)
40
+ def result(mutation_index_result)
41
+ mutation_result = mutation_result(mutation_index_result)
42
+
41
43
  subject = mutation_result.mutation.subject
42
44
 
43
45
  @subject_results[subject] = Result::Subject.new(
@@ -58,6 +60,14 @@ module Mutant
58
60
  )
59
61
  end
60
62
 
63
+ def mutation_result(mutation_index_result)
64
+ Result::Mutation.new(
65
+ isolation_result: mutation_index_result.isolation_result,
66
+ mutation: env.mutations.fetch(mutation_index_result.mutation_index),
67
+ runtime: mutation_index_result.runtime
68
+ )
69
+ end
70
+
61
71
  def previous_coverage_results(subject)
62
72
  subject_result = @subject_results.fetch(subject) { return EMPTY_ARRAY }
63
73
  subject_result.coverage_results
@@ -3,7 +3,7 @@
3
3
  module Mutant
4
4
  # Abstract base class for test that might kill a mutation
5
5
  class Test
6
- include Adamantium::Flat, Anima.new(
6
+ include Anima.new(
7
7
  :expressions,
8
8
  :id
9
9
  )
@@ -54,8 +54,6 @@ module Mutant
54
54
  class None < self
55
55
  include Concord.new
56
56
 
57
- STATUS = Status.new(nil)
58
-
59
57
  # The time left
60
58
  #
61
59
  # @return [Float, nil]
@@ -383,8 +383,6 @@ module Mutant
383
383
  #
384
384
  # @param [Object]
385
385
  #
386
- # ignore :reek:NestedIterators
387
- #
388
386
  # @return [Either<Error, Object>]
389
387
  def call(input)
390
388
  current = input
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mutant
4
4
  # Current mutant version
5
- VERSION = '0.10.16'
5
+ VERSION = '0.10.21'
6
6
  end # Mutant
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mutant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.16
4
+ version: 0.10.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Markus Schirp
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-08 00:00:00.000000000 Z
11
+ date: 2020-12-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: abstract_type
@@ -307,6 +307,7 @@ files:
307
307
  - lib/mutant/cli/command/environment.rb
308
308
  - lib/mutant/cli/command/environment/run.rb
309
309
  - lib/mutant/cli/command/environment/show.rb
310
+ - lib/mutant/cli/command/environment/subject.rb
310
311
  - lib/mutant/cli/command/root.rb
311
312
  - lib/mutant/cli/command/subscription.rb
312
313
  - lib/mutant/config.rb
@@ -411,6 +412,7 @@ files:
411
412
  - lib/mutant/parallel/source.rb
412
413
  - lib/mutant/parallel/worker.rb
413
414
  - lib/mutant/parser.rb
415
+ - lib/mutant/pipe.rb
414
416
  - lib/mutant/range.rb
415
417
  - lib/mutant/registry.rb
416
418
  - lib/mutant/reporter.rb