mutant 0.11.28 → 0.11.29

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mutant/ast.rb +8 -9
  3. data/lib/mutant/bootstrap.rb +39 -13
  4. data/lib/mutant/cli/command/environment/test.rb +38 -1
  5. data/lib/mutant/cli/command/environment.rb +11 -0
  6. data/lib/mutant/context.rb +31 -61
  7. data/lib/mutant/env.rb +8 -1
  8. data/lib/mutant/expression/method.rb +4 -1
  9. data/lib/mutant/expression/methods.rb +4 -1
  10. data/lib/mutant/expression/namespace.rb +4 -4
  11. data/lib/mutant/hooks.rb +1 -0
  12. data/lib/mutant/integration/null.rb +1 -0
  13. data/lib/mutant/integration.rb +5 -1
  14. data/lib/mutant/matcher/descendants.rb +1 -1
  15. data/lib/mutant/matcher/method/instance.rb +5 -4
  16. data/lib/mutant/matcher/method/metaclass.rb +1 -1
  17. data/lib/mutant/matcher/method/singleton.rb +1 -1
  18. data/lib/mutant/matcher/method.rb +30 -4
  19. data/lib/mutant/matcher/methods.rb +8 -7
  20. data/lib/mutant/matcher/namespace.rb +1 -1
  21. data/lib/mutant/meta/example.rb +12 -2
  22. data/lib/mutant/mutation/runner/sink.rb +7 -3
  23. data/lib/mutant/mutation/runner.rb +2 -3
  24. data/lib/mutant/parallel/connection.rb +178 -0
  25. data/lib/mutant/parallel/pipe.rb +39 -0
  26. data/lib/mutant/parallel/worker.rb +42 -14
  27. data/lib/mutant/parallel.rb +18 -7
  28. data/lib/mutant/reporter/cli/format.rb +19 -2
  29. data/lib/mutant/reporter/cli/printer/test.rb +138 -0
  30. data/lib/mutant/reporter/cli.rb +33 -4
  31. data/lib/mutant/reporter.rb +22 -1
  32. data/lib/mutant/result.rb +53 -2
  33. data/lib/mutant/scope.rb +41 -1
  34. data/lib/mutant/subject/method/instance.rb +3 -2
  35. data/lib/mutant/subject/method/metaclass.rb +1 -1
  36. data/lib/mutant/subject/method/singleton.rb +2 -2
  37. data/lib/mutant/subject/method.rb +1 -1
  38. data/lib/mutant/test/runner/sink.rb +51 -0
  39. data/lib/mutant/test/runner.rb +62 -0
  40. data/lib/mutant/timer.rb +9 -0
  41. data/lib/mutant/version.rb +1 -1
  42. data/lib/mutant/world.rb +2 -0
  43. data/lib/mutant.rb +7 -1
  44. metadata +9 -19
  45. data/lib/mutant/pipe.rb +0 -96
@@ -39,8 +39,9 @@ module Mutant
39
39
  # @return [Context]
40
40
  def context
41
41
  Context.new(
42
- scope: Object,
43
- source_path: location.path
42
+ constant_scope: Context::ConstantScope::None.new,
43
+ scope: scope,
44
+ source_path: location.path
44
45
  )
45
46
  end
46
47
 
@@ -65,6 +66,15 @@ module Mutant
65
66
  end
66
67
  memoize :generated
67
68
 
69
+ private
70
+
71
+ def scope
72
+ Scope.new(
73
+ expression: Expression::Namespace::Exact.new(scope_name: 'Object'),
74
+ raw: Object
75
+ )
76
+ end
77
+
68
78
  end # Example
69
79
  end # Meta
70
80
  end # Mutant
@@ -4,6 +4,8 @@ module Mutant
4
4
  class Mutation
5
5
  module Runner
6
6
  class Sink
7
+ include Parallel::Sink
8
+
7
9
  include Anima.new(:env)
8
10
 
9
11
  # Initialize object
@@ -35,11 +37,13 @@ module Mutant
35
37
 
36
38
  # Handle mutation finish
37
39
  #
38
- # @param [Result::MutationIndex] mutation_index_result
40
+ # @param [Parallel::Response] response
39
41
  #
40
42
  # @return [self]
41
- def result(mutation_index_result)
42
- mutation_result = mutation_result(mutation_index_result)
43
+ def response(response)
44
+ fail response.error if response.error
45
+
46
+ mutation_result = mutation_result(response.result)
43
47
 
44
48
  subject = mutation_result.mutation.subject
45
49
 
@@ -15,8 +15,6 @@ module Mutant
15
15
  def self.run_mutation_analysis(env)
16
16
  reporter = reporter(env)
17
17
 
18
- env.world.process_warmup
19
-
20
18
  env
21
19
  .record(:analysis) { run_driver(reporter, async_driver(env)) }
22
20
  .tap { |result| env.record(:report) { reporter.report(result) } }
@@ -24,7 +22,7 @@ module Mutant
24
22
  private_class_method :run_mutation_analysis
25
23
 
26
24
  def self.async_driver(env)
27
- Parallel.async(env.world, mutation_test_config(env))
25
+ Parallel.async(world: env.world, config: mutation_test_config(env))
28
26
  end
29
27
  private_class_method :async_driver
30
28
 
@@ -49,6 +47,7 @@ module Mutant
49
47
  process_name: 'mutant-worker-process',
50
48
  sink: Sink.new(env: env),
51
49
  source: Parallel::Source::Array.new(jobs: env.mutations.each_index.to_a),
50
+ timeout: nil,
52
51
  thread_name: 'mutant-worker-thread'
53
52
  )
54
53
  end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module Parallel
5
+ class Connection
6
+ include Anima.new(:marshal, :reader, :writer)
7
+
8
+ Error = Class.new(RuntimeError)
9
+
10
+ HEADER_FORMAT = 'N'
11
+ HEADER_SIZE = 4
12
+ MAX_BYTES = (2**32).pred
13
+
14
+ class Reader
15
+ include Anima.new(:deadline, :io, :marshal, :response_reader, :log_reader)
16
+
17
+ private(*anima.attribute_names)
18
+
19
+ private_class_method :new
20
+
21
+ attr_reader :log
22
+
23
+ def error
24
+ @errors.first
25
+ end
26
+
27
+ def result
28
+ @results.first
29
+ end
30
+
31
+ def initialize(*)
32
+ super
33
+
34
+ @buffer = +''
35
+ @log = +''
36
+
37
+ # Array of size max 1 as surrogate for
38
+ # terrible default nil ivars.
39
+ @errors = []
40
+ @lengths = []
41
+ @results = []
42
+ end
43
+
44
+ def self.read_response(job:, **attributes)
45
+ reader = new(**attributes).read_till_final
46
+
47
+ Response.new(
48
+ error: reader.error,
49
+ job: job,
50
+ log: reader.log,
51
+ result: reader.result
52
+ )
53
+ end
54
+
55
+ # rubocop:disable Metrics/MethodLength
56
+ def read_till_final
57
+ readers = [response_reader, log_reader]
58
+
59
+ until !@results.empty? || error
60
+ status = deadline.status
61
+
62
+ break timeout unless status.ok?
63
+
64
+ reads, _others = io.select(readers, nil, nil, status.time_left)
65
+
66
+ break timeout unless reads
67
+
68
+ reads.each do |ready|
69
+ if ready.equal?(response_reader)
70
+ advance_result
71
+ else
72
+ advance_log
73
+ end
74
+ end
75
+ end
76
+
77
+ self
78
+ end
79
+ # rubocop:enable Metrics/MethodLength
80
+
81
+ private
82
+
83
+ def timeout
84
+ @errors << Timeout::Error
85
+ end
86
+
87
+ def advance_result
88
+ if length
89
+ if read_buffer(length)
90
+ @results << marshal.load(@buffer)
91
+ end
92
+ elsif read_buffer(HEADER_SIZE)
93
+ @lengths << Util.one(@buffer.unpack(HEADER_FORMAT))
94
+ @buffer = +''
95
+ end
96
+ end
97
+
98
+ def length
99
+ @lengths.first
100
+ end
101
+
102
+ def advance_log
103
+ with_nonblock_read(io: log_reader, max_bytes: 4096, &log.public_method(:<<))
104
+ end
105
+
106
+ def read_buffer(max_bytes)
107
+ with_nonblock_read(
108
+ io: response_reader,
109
+ max_bytes: max_bytes - @buffer.bytesize
110
+ ) do |chunk|
111
+ @buffer << chunk
112
+ @buffer.bytesize.equal?(max_bytes)
113
+ end
114
+ end
115
+
116
+ # rubocop:disable Metrics/MethodLength
117
+ def with_nonblock_read(io:, max_bytes:)
118
+ io.binmode
119
+
120
+ chunk = io.read_nonblock(max_bytes, exception: false)
121
+
122
+ case chunk
123
+ when nil
124
+ @errors << EOFError
125
+ false
126
+ when String
127
+ yield chunk
128
+ else
129
+ fail "Unexpected nonblocking read return: #{chunk.inspect}"
130
+ end
131
+ end
132
+ # rubocop:enable Metrics/MethodLength
133
+ end
134
+
135
+ class Frame
136
+ include Anima.new(:io)
137
+
138
+ def receive_value
139
+ read(Util.one(read(HEADER_SIZE).unpack(HEADER_FORMAT)))
140
+ end
141
+
142
+ def send_value(body)
143
+ bytesize = body.bytesize
144
+
145
+ fail Error, 'message to big' if bytesize > MAX_BYTES
146
+
147
+ io.binmode
148
+ io.write([bytesize].pack(HEADER_FORMAT))
149
+ io.write(body)
150
+ end
151
+
152
+ private
153
+
154
+ def read(bytes)
155
+ io.binmode
156
+ io.read(bytes) or fail Error, 'Unexpected EOF'
157
+ end
158
+ end
159
+
160
+ def receive_value
161
+ marshal.load(reader.receive_value)
162
+ end
163
+
164
+ def send_value(value)
165
+ writer.send_value(marshal.dump(value))
166
+ self
167
+ end
168
+
169
+ def self.from_pipes(marshal:, reader:, writer:)
170
+ new(
171
+ marshal: marshal,
172
+ reader: Frame.new(io: reader.to_reader),
173
+ writer: Frame.new(io: writer.to_writer)
174
+ )
175
+ end
176
+ end # Connection
177
+ end # Parallel
178
+ end # Mutant
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module Parallel
5
+ class Pipe
6
+ include Adamantium, 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
+ end # Pipe
38
+ end # Parallel
39
+ end # Mutant
@@ -9,6 +9,7 @@ module Mutant
9
9
  :index,
10
10
  :on_process_start,
11
11
  :process_name,
12
+ :timeout,
12
13
  :var_active_jobs,
13
14
  :var_final,
14
15
  :var_running,
@@ -18,38 +19,49 @@ module Mutant
18
19
  )
19
20
  end
20
21
 
21
- include Adamantium, Anima.new(:connection, :config, :pid)
22
+ include Adamantium, Anima.new(:config, :connection, :log_reader, :pid, :response_reader)
22
23
 
23
24
  def self.start(**attributes)
24
25
  start_config(Config.new(**attributes))
25
26
  end
26
27
 
27
28
  # rubocop:disable Metrics/MethodLength
29
+ # rubocop:disable Metrics/AbcSize
28
30
  def self.start_config(config)
29
31
  world = config.world
30
32
  io = world.io
31
33
  marshal = world.marshal
32
34
 
33
- request = Pipe.from_io(io)
34
- response = Pipe.from_io(io)
35
+ log, request, response = Pipe.from_io(io), Pipe.from_io(io), Pipe.from_io(io)
35
36
 
36
37
  pid = world.process.fork do
38
+ log_writer = log.to_writer
39
+
40
+ world.stderr.reopen(log_writer)
41
+ world.stdout.reopen(log_writer)
42
+
37
43
  run_child(
38
44
  config: config,
39
- connection: Pipe::Connection.from_pipes(marshal: marshal, reader: request, writer: response)
45
+ connection: Connection.from_pipes(marshal: marshal, reader: request, writer: response),
46
+ log_writer: log_writer
40
47
  )
41
48
  end
42
49
 
50
+ connection = Connection.from_pipes(marshal: marshal, reader: response, writer: request)
51
+
43
52
  new(
44
- pid: pid,
45
- config: config,
46
- connection: Pipe::Connection.from_pipes(marshal: marshal, reader: response, writer: request)
53
+ config: config,
54
+ connection: connection,
55
+ log_reader: log.to_reader,
56
+ response_reader: connection.reader.io,
57
+ pid: pid
47
58
  )
48
59
  end
49
60
  private_class_method :start_config
61
+ # rubocop:enable Metrics/AbcSize
50
62
  # rubocop:enable Metrics/MethodLength
51
63
 
52
- def self.run_child(config:, connection:)
64
+ def self.run_child(config:, connection:, log_writer:)
53
65
  world = config.world
54
66
 
55
67
  world.thread.current.name = config.process_name
@@ -58,7 +70,9 @@ module Mutant
58
70
  config.on_process_start.call(index: config.index)
59
71
 
60
72
  loop do
61
- connection.send_value(config.block.call(connection.receive_value))
73
+ value = config.block.call(connection.receive_value)
74
+ log_writer.flush
75
+ connection.send_value(value)
62
76
  end
63
77
  end
64
78
  private_class_method :run_child
@@ -67,26 +81,40 @@ module Mutant
67
81
  config.index
68
82
  end
69
83
 
70
- # Run worker payload
84
+ # Run worker loop
71
85
  #
72
86
  # @return [self]
87
+ #
88
+ # rubocop:disable Metrics/MethodLength
89
+ # rubocop:disable Metrics/AbcSize
73
90
  def call
74
91
  loop do
75
92
  job = next_job or break
76
93
 
77
94
  job_start(job)
78
95
 
79
- result = connection.call(job.payload)
96
+ connection.send_value(job.payload)
97
+
98
+ response = Connection::Reader.read_response(
99
+ deadline: config.world.deadline(config.timeout),
100
+ io: config.world.io,
101
+ job: job,
102
+ log_reader: log_reader,
103
+ marshal: config.world.marshal,
104
+ response_reader: response_reader
105
+ )
80
106
 
81
107
  job_done(job)
82
108
 
83
- break if add_result(result)
109
+ break if add_response(response) || response.error
84
110
  end
85
111
 
86
112
  finalize
87
113
 
88
114
  self
89
115
  end
116
+ # rubocop:enable Metrics/AbcSize
117
+ # rubocop:enable Metrics/MethodLength
90
118
 
91
119
  def signal
92
120
  process.kill('TERM', pid)
@@ -110,9 +138,9 @@ module Mutant
110
138
  end
111
139
  end
112
140
 
113
- def add_result(result)
141
+ def add_response(response)
114
142
  config.var_sink.with do |sink|
115
- sink.result(result)
143
+ sink.response(response)
116
144
  sink.stop?
117
145
  end
118
146
  end
@@ -9,8 +9,11 @@ module Mutant
9
9
  # @param [Config] config
10
10
  #
11
11
  # @return [Driver]
12
- def self.async(world, config)
13
- shared = shared_state(world, config)
12
+ def self.async(config:, world:)
13
+ shared = shared_state(world, config)
14
+
15
+ world.process_warmup
16
+
14
17
  workers = workers(world, config, shared)
15
18
 
16
19
  Driver.new(
@@ -20,6 +23,7 @@ module Mutant
20
23
  )
21
24
  end
22
25
 
26
+ # rubocop:disable Metric/MethodLength
23
27
  def self.workers(world, config, shared)
24
28
  Array.new(config.jobs) do |index|
25
29
  Worker.start(
@@ -27,12 +31,14 @@ module Mutant
27
31
  index: index,
28
32
  on_process_start: config.on_process_start,
29
33
  process_name: "#{config.process_name}-#{index}",
34
+ timeout: config.timeout,
30
35
  world: world,
31
36
  **shared
32
37
  )
33
38
  end
34
39
  end
35
40
  private_class_method :workers
41
+ # rubocop:enable Metric/MethodLength
36
42
 
37
43
  def self.shared_state(world, config)
38
44
  {
@@ -66,16 +72,16 @@ module Mutant
66
72
  end
67
73
  private_class_method :shared
68
74
 
69
- # Job result sink
70
- class Sink
75
+ # Job result sink signature
76
+ module Sink
71
77
  include AbstractType
72
78
 
73
79
  # Process job result
74
80
  #
75
- # @param [Object]
81
+ # @param [Response]
76
82
  #
77
83
  # @return [self]
78
- abstract_method :result
84
+ abstract_method :response
79
85
 
80
86
  # The sink status
81
87
  #
@@ -97,10 +103,15 @@ module Mutant
97
103
  :process_name,
98
104
  :sink,
99
105
  :source,
100
- :thread_name
106
+ :thread_name,
107
+ :timeout
101
108
  )
102
109
  end # Config
103
110
 
111
+ class Response
112
+ include Anima.new(:error, :job, :log, :result)
113
+ end
114
+
104
115
  # Parallel execution status
105
116
  class Status
106
117
  include Adamantium, Anima.new(
@@ -18,11 +18,14 @@ module Mutant
18
18
 
19
19
  # Progress representation
20
20
  #
21
- # @param [Runner::Status] status
22
- #
23
21
  # @return [String]
24
22
  abstract_method :progress
25
23
 
24
+ # Progress representation
25
+ #
26
+ # @return [String]
27
+ abstract_method :test_progress
28
+
26
29
  # Report delay in seconds
27
30
  #
28
31
  # @return [Float]
@@ -69,6 +72,13 @@ module Mutant
69
72
  format(Printer::Env, env)
70
73
  end
71
74
 
75
+ # Test start representation
76
+ #
77
+ # @return [String]
78
+ def test_start(env)
79
+ format(Printer::Test::Env, env)
80
+ end
81
+
72
82
  # Progress representation
73
83
  #
74
84
  # @return [String]
@@ -76,6 +86,13 @@ module Mutant
76
86
  format(Printer::StatusProgressive, status)
77
87
  end
78
88
 
89
+ # Progress representation
90
+ #
91
+ # @return [String]
92
+ def test_progress(status)
93
+ format(Printer::Test::StatusProgressive, status)
94
+ end
95
+
79
96
  private
80
97
 
81
98
  def new_buffer
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Reporter
5
+ class CLI
6
+ class Printer
7
+ class Test < self
8
+ # Printer for test config
9
+ class Config < self
10
+
11
+ # Report configuration
12
+ #
13
+ # @param [Mutant::Config] config
14
+ #
15
+ # @return [undefined]
16
+ #
17
+ def run
18
+ info('Fail-Fast: %s', object.fail_fast)
19
+ info('Integration: %s', object.integration.name || 'null')
20
+ info('Jobs: %s', object.jobs || 'auto')
21
+ end
22
+ end # Config
23
+
24
+ # Env printer
25
+ class Env < self
26
+ delegate(
27
+ :amount_available_tests,
28
+ :amount_selected_tests,
29
+ :amount_all_tests,
30
+ :config
31
+ )
32
+
33
+ FORMATS = [
34
+ ].each(&:freeze)
35
+
36
+ # Run printer
37
+ #
38
+ # @return [undefined]
39
+ def run
40
+ info('Test environment:')
41
+ visit(Config, config)
42
+ info('Tests: %s', amount_all_tests)
43
+ end
44
+ end # Env
45
+
46
+ # Full env result reporter
47
+ class EnvResult < self
48
+ delegate(
49
+ :amount_test_results,
50
+ :amount_tests_failed,
51
+ :amount_tests_success,
52
+ :runtime,
53
+ :testtime
54
+ )
55
+
56
+ FORMATS = [
57
+ [:info, 'Test-Results: %0d', :amount_test_results ],
58
+ [:info, 'Test-Failed: %0d', :amount_tests_failed ],
59
+ [:info, 'Test-Success: %0d', :amount_tests_success ],
60
+ [:info, 'Runtime: %0.2fs', :runtime ],
61
+ [:info, 'Testtime: %0.2fs', :testtime ],
62
+ [:info, 'Efficiency: %0.2f%%', :efficiency_percent ]
63
+ ].each(&:freeze)
64
+
65
+ private_constant(*constants(false))
66
+
67
+ # Run printer
68
+ #
69
+ # @return [undefined]
70
+ def run
71
+ visit_collection(Result, object.failed_test_results)
72
+ visit(Env, object.env)
73
+ FORMATS.each do |report, format, value|
74
+ __send__(report, format, __send__(value))
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def efficiency_percent
81
+ (testtime / runtime) * 100
82
+ end
83
+ end # EnvResult
84
+
85
+ # Reporter for test results
86
+ class Result < self
87
+
88
+ # Run report printer
89
+ #
90
+ # @return [undefined]
91
+ def run
92
+ puts(object.output)
93
+ end
94
+
95
+ end # Result
96
+
97
+ # Reporter for progressive output format on scheduler Status objects
98
+ class StatusProgressive < self
99
+ FORMAT = 'progress: %02d/%02d failed: %d runtime: %0.02fs testtime: %0.02fs tests/s: %0.02f'
100
+
101
+ delegate(
102
+ :amount_test_results,
103
+ :amount_tests,
104
+ :amount_tests_failed,
105
+ :testtime,
106
+ :runtime
107
+ )
108
+
109
+ # Run printer
110
+ #
111
+ # @return [undefined]
112
+ def run
113
+ status(
114
+ FORMAT,
115
+ amount_test_results,
116
+ amount_tests,
117
+ amount_tests_failed,
118
+ runtime,
119
+ testtime,
120
+ tests_per_second
121
+ )
122
+ end
123
+
124
+ private
125
+
126
+ def object
127
+ super().payload
128
+ end
129
+
130
+ def tests_per_second
131
+ amount_test_results / runtime
132
+ end
133
+ end # StatusProgressive
134
+ end # Test
135
+ end # Printer
136
+ end # CLI
137
+ end # Reporter
138
+ end # Mutant