mutant 0.11.28 → 0.11.29

Sign up to get free protection for your applications and to get access to all the features.
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