mutant 0.10.20 → 0.10.25

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mutant.rb +23 -4
  3. data/lib/mutant/ast/meta/send.rb +0 -1
  4. data/lib/mutant/ast/regexp.rb +37 -0
  5. data/lib/mutant/ast/regexp/transformer.rb +150 -0
  6. data/lib/mutant/ast/regexp/transformer/direct.rb +121 -0
  7. data/lib/mutant/ast/regexp/transformer/named_group.rb +50 -0
  8. data/lib/mutant/ast/regexp/transformer/options_group.rb +68 -0
  9. data/lib/mutant/ast/regexp/transformer/quantifier.rb +90 -0
  10. data/lib/mutant/ast/regexp/transformer/recursive.rb +56 -0
  11. data/lib/mutant/ast/regexp/transformer/root.rb +28 -0
  12. data/lib/mutant/ast/regexp/transformer/text.rb +58 -0
  13. data/lib/mutant/ast/types.rb +115 -11
  14. data/lib/mutant/cli/command/environment.rb +9 -3
  15. data/lib/mutant/config.rb +8 -54
  16. data/lib/mutant/config/coverage_criteria.rb +61 -0
  17. data/lib/mutant/env.rb +8 -6
  18. data/lib/mutant/expression.rb +0 -12
  19. data/lib/mutant/expression/namespace.rb +1 -1
  20. data/lib/mutant/isolation/fork.rb +1 -1
  21. data/lib/mutant/loader.rb +1 -1
  22. data/lib/mutant/matcher.rb +2 -2
  23. data/lib/mutant/matcher/config.rb +27 -6
  24. data/lib/mutant/matcher/method.rb +2 -3
  25. data/lib/mutant/matcher/method/metaclass.rb +1 -1
  26. data/lib/mutant/meta/example/dsl.rb +6 -1
  27. data/lib/mutant/mutator/node/arguments.rb +0 -2
  28. data/lib/mutant/mutator/node/block.rb +5 -1
  29. data/lib/mutant/mutator/node/dynamic_literal.rb +1 -1
  30. data/lib/mutant/mutator/node/kwargs.rb +44 -0
  31. data/lib/mutant/mutator/node/literal/regex.rb +26 -0
  32. data/lib/mutant/mutator/node/literal/symbol.rb +0 -2
  33. data/lib/mutant/mutator/node/regexp.rb +20 -0
  34. data/lib/mutant/mutator/node/regexp/alternation_meta.rb +20 -0
  35. data/lib/mutant/mutator/node/regexp/beginning_of_line_anchor.rb +20 -0
  36. data/lib/mutant/mutator/node/regexp/capture_group.rb +25 -0
  37. data/lib/mutant/mutator/node/regexp/character_type.rb +31 -0
  38. data/lib/mutant/mutator/node/regexp/end_of_line_anchor.rb +20 -0
  39. data/lib/mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor.rb +20 -0
  40. data/lib/mutant/mutator/node/regexp/zero_or_more.rb +34 -0
  41. data/lib/mutant/mutator/node/sclass.rb +1 -1
  42. data/lib/mutant/mutator/node/send.rb +36 -19
  43. data/lib/mutant/parallel.rb +43 -28
  44. data/lib/mutant/parallel/driver.rb +9 -3
  45. data/lib/mutant/parallel/worker.rb +60 -2
  46. data/lib/mutant/pipe.rb +94 -0
  47. data/lib/mutant/reporter/cli/printer/env.rb +1 -1
  48. data/lib/mutant/reporter/cli/printer/isolation_result.rb +1 -6
  49. data/lib/mutant/result.rb +8 -0
  50. data/lib/mutant/runner.rb +7 -10
  51. data/lib/mutant/runner/sink.rb +12 -2
  52. data/lib/mutant/subject.rb +1 -3
  53. data/lib/mutant/subject/method/instance.rb +2 -4
  54. data/lib/mutant/timer.rb +2 -4
  55. data/lib/mutant/transform.rb +25 -2
  56. data/lib/mutant/version.rb +1 -1
  57. data/lib/mutant/world.rb +1 -2
  58. metadata +48 -9
  59. data/lib/mutant/warnings.rb +0 -106
@@ -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
@@ -33,7 +33,7 @@ module Mutant
33
33
  __send__(report, format, __send__(value))
34
34
  end
35
35
  end
36
- end # EnvProgress
36
+ end # Env
37
37
  end # Printer
38
38
  end # CLI
39
39
  end # Reporter
@@ -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
 
@@ -232,6 +232,14 @@ module Mutant
232
232
  end
233
233
  end
234
234
 
235
+ class MutationIndex
236
+ include Anima.new(
237
+ :isolation_result,
238
+ :mutation_index,
239
+ :runtime
240
+ )
241
+ end # MutationIndex
242
+
235
243
  # Mutation result
236
244
  class Mutation
237
245
  include Result, Anima.new(
@@ -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
@@ -4,9 +4,7 @@ module Mutant
4
4
  # Subject of a mutation
5
5
  class Subject
6
6
  include AbstractType, Adamantium::Flat, Enumerable
7
- include Anima.new(:context, :node, :warnings)
8
-
9
- private :warnings
7
+ include Anima.new(:context, :node)
10
8
 
11
9
  # Mutations for this subject
12
10
  #
@@ -13,9 +13,7 @@ module Mutant
13
13
  #
14
14
  # @return [self]
15
15
  def prepare
16
- warnings.call do
17
- scope.public_send(:undef_method, name)
18
- end
16
+ scope.undef_method(name)
19
17
  self
20
18
  end
21
19
 
@@ -52,7 +50,7 @@ module Mutant
52
50
  # rubocop:disable Style/GuardClause
53
51
  if FREEZER_OPTION_VALUES.key?(freezer)
54
52
  [
55
- s(:hash,
53
+ s(:kwargs,
56
54
  s(:pair,
57
55
  s(:sym, :freezer),
58
56
  s(:sym, FREEZER_OPTION_VALUES.fetch(freezer))))
@@ -14,8 +14,8 @@ module Mutant
14
14
  class Deadline
15
15
  include Anima.new(:timer, :allowed_time)
16
16
 
17
- def initialize(**arguments)
18
- super(**arguments)
17
+ def initialize(*arguments)
18
+ super(*arguments)
19
19
  @start_at = timer.now
20
20
  end
21
21
 
@@ -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]
@@ -73,6 +73,31 @@ module Mutant
73
73
  end
74
74
  end # Named
75
75
 
76
+ class Block < self
77
+ include Anima.new(:block, :name)
78
+
79
+ def self.capture(name, &block)
80
+ new(block: block, name: name)
81
+ end
82
+
83
+ def call(input)
84
+ block
85
+ .call(input)
86
+ .lmap do |message|
87
+ Error.new(
88
+ cause: nil,
89
+ input: input,
90
+ message: message,
91
+ transform: self
92
+ )
93
+ end
94
+ end
95
+
96
+ def slug
97
+ name
98
+ end
99
+ end
100
+
76
101
  private
77
102
 
78
103
  def error(cause: nil, input:, message: nil)
@@ -383,8 +408,6 @@ module Mutant
383
408
  #
384
409
  # @param [Object]
385
410
  #
386
- # ignore :reek:NestedIterators
387
- #
388
411
  # @return [Either<Error, Object>]
389
412
  def call(input)
390
413
  current = input
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Mutant
4
4
  # Current mutant version
5
- VERSION = '0.10.20'
5
+ VERSION = '0.10.25'
6
6
  end # Mutant
@@ -20,8 +20,7 @@ module Mutant
20
20
  :stderr,
21
21
  :stdout,
22
22
  :thread,
23
- :timer,
24
- :warnings
23
+ :timer
25
24
  )
26
25
 
27
26
  INSPECT = '#<Mutant::World>'
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.20
4
+ version: 0.10.25
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-16 00:00:00.000000000 Z
11
+ date: 2021-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: abstract_type
@@ -156,14 +156,14 @@ dependencies:
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: 2.7.1
159
+ version: 3.0.0
160
160
  type: :runtime
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: 2.7.1
166
+ version: 3.0.0
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: procto
169
169
  requirement: !ruby/object:Gem::Requirement
@@ -178,20 +178,40 @@ dependencies:
178
178
  - - "~>"
179
179
  - !ruby/object:Gem::Version
180
180
  version: 0.0.2
181
+ - !ruby/object:Gem::Dependency
182
+ name: regexp_parser
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '2.0'
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: 2.0.3
191
+ type: :runtime
192
+ prerelease: false
193
+ version_requirements: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - "~>"
196
+ - !ruby/object:Gem::Version
197
+ version: '2.0'
198
+ - - ">="
199
+ - !ruby/object:Gem::Version
200
+ version: 2.0.3
181
201
  - !ruby/object:Gem::Dependency
182
202
  name: unparser
183
203
  requirement: !ruby/object:Gem::Requirement
184
204
  requirements:
185
205
  - - "~>"
186
206
  - !ruby/object:Gem::Version
187
- version: 0.5.4
207
+ version: 0.5.6
188
208
  type: :runtime
189
209
  prerelease: false
190
210
  version_requirements: !ruby/object:Gem::Requirement
191
211
  requirements:
192
212
  - - "~>"
193
213
  - !ruby/object:Gem::Version
194
- version: 0.5.4
214
+ version: 0.5.6
195
215
  - !ruby/object:Gem::Dependency
196
216
  name: variable
197
217
  requirement: !ruby/object:Gem::Requirement
@@ -268,14 +288,14 @@ dependencies:
268
288
  requirements:
269
289
  - - "~>"
270
290
  - !ruby/object:Gem::Version
271
- version: '1.2'
291
+ version: '1.7'
272
292
  type: :development
273
293
  prerelease: false
274
294
  version_requirements: !ruby/object:Gem::Requirement
275
295
  requirements:
276
296
  - - "~>"
277
297
  - !ruby/object:Gem::Version
278
- version: '1.2'
298
+ version: '1.7'
279
299
  description: Mutation Testing for Ruby.
280
300
  email:
281
301
  - mbj@schirp-dso.com
@@ -299,6 +319,15 @@ files:
299
319
  - lib/mutant/ast/named_children.rb
300
320
  - lib/mutant/ast/node_predicates.rb
301
321
  - lib/mutant/ast/nodes.rb
322
+ - lib/mutant/ast/regexp.rb
323
+ - lib/mutant/ast/regexp/transformer.rb
324
+ - lib/mutant/ast/regexp/transformer/direct.rb
325
+ - lib/mutant/ast/regexp/transformer/named_group.rb
326
+ - lib/mutant/ast/regexp/transformer/options_group.rb
327
+ - lib/mutant/ast/regexp/transformer/quantifier.rb
328
+ - lib/mutant/ast/regexp/transformer/recursive.rb
329
+ - lib/mutant/ast/regexp/transformer/root.rb
330
+ - lib/mutant/ast/regexp/transformer/text.rb
302
331
  - lib/mutant/ast/sexp.rb
303
332
  - lib/mutant/ast/types.rb
304
333
  - lib/mutant/bootstrap.rb
@@ -311,6 +340,7 @@ files:
311
340
  - lib/mutant/cli/command/root.rb
312
341
  - lib/mutant/cli/command/subscription.rb
313
342
  - lib/mutant/config.rb
343
+ - lib/mutant/config/coverage_criteria.rb
314
344
  - lib/mutant/context.rb
315
345
  - lib/mutant/env.rb
316
346
  - lib/mutant/expression.rb
@@ -366,6 +396,7 @@ files:
366
396
  - lib/mutant/mutator/node/generic.rb
367
397
  - lib/mutant/mutator/node/if.rb
368
398
  - lib/mutant/mutator/node/index.rb
399
+ - lib/mutant/mutator/node/kwargs.rb
369
400
  - lib/mutant/mutator/node/kwbegin.rb
370
401
  - lib/mutant/mutator/node/literal.rb
371
402
  - lib/mutant/mutator/node/literal/array.rb
@@ -390,6 +421,14 @@ files:
390
421
  - lib/mutant/mutator/node/op_asgn.rb
391
422
  - lib/mutant/mutator/node/or_asgn.rb
392
423
  - lib/mutant/mutator/node/procarg_zero.rb
424
+ - lib/mutant/mutator/node/regexp.rb
425
+ - lib/mutant/mutator/node/regexp/alternation_meta.rb
426
+ - lib/mutant/mutator/node/regexp/beginning_of_line_anchor.rb
427
+ - lib/mutant/mutator/node/regexp/capture_group.rb
428
+ - lib/mutant/mutator/node/regexp/character_type.rb
429
+ - lib/mutant/mutator/node/regexp/end_of_line_anchor.rb
430
+ - lib/mutant/mutator/node/regexp/end_of_string_or_before_end_of_line_anchor.rb
431
+ - lib/mutant/mutator/node/regexp/zero_or_more.rb
393
432
  - lib/mutant/mutator/node/regopt.rb
394
433
  - lib/mutant/mutator/node/resbody.rb
395
434
  - lib/mutant/mutator/node/rescue.rb
@@ -412,6 +451,7 @@ files:
412
451
  - lib/mutant/parallel/source.rb
413
452
  - lib/mutant/parallel/worker.rb
414
453
  - lib/mutant/parser.rb
454
+ - lib/mutant/pipe.rb
415
455
  - lib/mutant/range.rb
416
456
  - lib/mutant/registry.rb
417
457
  - lib/mutant/reporter.rb
@@ -450,7 +490,6 @@ files:
450
490
  - lib/mutant/transform.rb
451
491
  - lib/mutant/util.rb
452
492
  - lib/mutant/version.rb
453
- - lib/mutant/warnings.rb
454
493
  - lib/mutant/world.rb
455
494
  - lib/mutant/zombifier.rb
456
495
  homepage: https://github.com/mbj/mutant