mutant 0.17.0 → 0.17.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 752a1938022756ae4d788946052f227269843741bafc857cb9be24bd94b5a272
4
- data.tar.gz: b7f0ec5bc7a221c9503c923d20781a061826826acb3caf605a4601e77a808bb5
3
+ metadata.gz: fd77b62cd208529f1fd030e87ffbce9c4df69cf18c097f7895ddd76204f8577d
4
+ data.tar.gz: f4ce398e717e5a8d34aa2ed57eafea5358cdb7af02fb8dc1dbcca2b862f754cc
5
5
  SHA512:
6
- metadata.gz: ae4b675e810a9b94f340e9888722eb1a72b78f8bcb008dd0dec44919a3f5d4ef3d440f117c4c159db59528a6362b3b53ea6e0ec808833798d7f9f4ca1c0ab633
7
- data.tar.gz: a95c6b2b83be6d672284bc6139cd8f37fb29a1a02e5babc3a5c304b6a6eeba143b37d68b6dee39c5de6355c93800435443e9abdf68e4ffd301cdb60b334d9dd5
6
+ metadata.gz: c6dbb4139015cfe33c91b7c802004aca5d9015299d517552c56bcaa4cc81fa40b5a5f8e39bd17319ff0876da90b2cc7164566c0c65221f6dd0de1758fe767829
7
+ data.tar.gz: caa47a0c66816fe06b76bf0da184d9b91404abceb93eec83d374c42f1d2c3c51ae578e57b98f00b9fb431ffcc770cb864aabfe203bdd9400b30a77fdf8acade3
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.17.0
1
+ 0.17.1
@@ -177,6 +177,11 @@ module Mutant
177
177
  fixed: EMPTY_ARRAY,
178
178
  variable: Node::Variable::Descendants.new(name: :members, range: 0..)
179
179
  ),
180
+ Node.new(
181
+ type: :array_pattern_with_tail,
182
+ fixed: EMPTY_ARRAY,
183
+ variable: Node::Variable::Descendants.new(name: :members, range: 0..)
184
+ ),
180
185
  Node.new(
181
186
  type: :back_ref,
182
187
  fixed: Node.fixed([[Node::Fixed::Attribute, :name]]),
@@ -381,6 +386,11 @@ module Mutant
381
386
  fixed: EMPTY_ARRAY,
382
387
  variable: nil
383
388
  ),
389
+ Node.new(
390
+ type: :find_pattern,
391
+ fixed: EMPTY_ARRAY,
392
+ variable: Node::Variable::Descendants.new(name: :members, range: 0..)
393
+ ),
384
394
  Node.new(
385
395
  type: :float,
386
396
  fixed: Node.fixed([[Node::Fixed::Attribute, :value]]),
@@ -442,6 +452,11 @@ module Mutant
442
452
  fixed: EMPTY_ARRAY,
443
453
  variable: Node::Variable::Descendants.new(name: :members, range: 0..)
444
454
  ),
455
+ Node.new(
456
+ type: :hash_pattern,
457
+ fixed: EMPTY_ARRAY,
458
+ variable: Node::Variable::Descendants.new(name: :members, range: 0..)
459
+ ),
445
460
  Node.new(
446
461
  type: :index,
447
462
  fixed: Node.fixed([[Node::Fixed::Descendant, :receiver]]),
@@ -505,6 +520,17 @@ module Mutant
505
520
  ),
506
521
  variable: nil
507
522
  ),
523
+ Node.new(
524
+ type: :itblock,
525
+ fixed: Node.fixed(
526
+ [
527
+ [Node::Fixed::Descendant, :receiver],
528
+ [Node::Fixed::Attribute, :parameter],
529
+ [Node::Fixed::Descendant, :body]
530
+ ]
531
+ ),
532
+ variable: nil
533
+ ),
508
534
  Node.new(
509
535
  type: :ivar,
510
536
  fixed: Node.fixed([[Node::Fixed::Attribute, :name]]),
@@ -535,6 +561,11 @@ module Mutant
535
561
  fixed: EMPTY_ARRAY,
536
562
  variable: Node::Variable::Descendants.new(name: :members, range: 0..)
537
563
  ),
564
+ Node.new(
565
+ type: :kwnilarg,
566
+ fixed: EMPTY_ARRAY,
567
+ variable: nil
568
+ ),
538
569
  Node.new(
539
570
  type: :kwoptarg,
540
571
  fixed: Node.fixed(
@@ -575,11 +606,36 @@ module Mutant
575
606
  ),
576
607
  variable: nil
577
608
  ),
609
+ Node.new(
610
+ type: :match_alt,
611
+ fixed: Node.fixed(
612
+ [
613
+ [Node::Fixed::Descendant, :left],
614
+ [Node::Fixed::Descendant, :right]
615
+ ]
616
+ ),
617
+ variable: nil
618
+ ),
619
+ Node.new(
620
+ type: :match_as,
621
+ fixed: Node.fixed(
622
+ [
623
+ [Node::Fixed::Descendant, :pattern],
624
+ [Node::Fixed::Descendant, :target]
625
+ ]
626
+ ),
627
+ variable: nil
628
+ ),
578
629
  Node.new(
579
630
  type: :match_current_line,
580
631
  fixed: Node.fixed([[Node::Fixed::Descendant, :pattern]]),
581
632
  variable: nil
582
633
  ),
634
+ Node.new(
635
+ type: :match_nil_pattern,
636
+ fixed: EMPTY_ARRAY,
637
+ variable: nil
638
+ ),
583
639
  Node.new(
584
640
  type: :match_pattern,
585
641
  fixed: Node.fixed(
@@ -590,6 +646,16 @@ module Mutant
590
646
  ),
591
647
  variable: nil
592
648
  ),
649
+ Node.new(
650
+ type: :match_pattern_p,
651
+ fixed: Node.fixed(
652
+ [
653
+ [Node::Fixed::Descendant, :target],
654
+ [Node::Fixed::Descendant, :pattern]
655
+ ]
656
+ ),
657
+ variable: nil
658
+ ),
593
659
  Node.new(
594
660
  type: :match_rest,
595
661
  fixed: Node.fixed([[Node::Fixed::Descendant, :value]]),
@@ -794,6 +860,11 @@ module Mutant
794
860
  ),
795
861
  variable: nil
796
862
  ),
863
+ Node.new(
864
+ type: :pin,
865
+ fixed: Node.fixed([[Node::Fixed::Descendant, :value]]),
866
+ variable: nil
867
+ ),
797
868
  Node.new(
798
869
  type: :postexe,
799
870
  fixed: Node.fixed([[Node::Fixed::Descendant, :body]]),
@@ -7,23 +7,29 @@ module Mutant
7
7
  NAME = 'session'
8
8
  SHORT_DESCRIPTION = 'Session history subcommands'
9
9
 
10
- RESULTS_DIR = '.mutant/results'
10
+ RESULTS_DIR = Result::SessionFiles::DIRECTORY
11
11
 
12
- private
12
+ GZIP_MAGIC = [0x1F, 0x8B].pack('C2')
13
13
 
14
- def session_files
15
- dir = world.pathname.new(RESULTS_DIR)
14
+ private_constant(:GZIP_MAGIC)
16
15
 
17
- return [] unless dir.directory?
16
+ private
18
17
 
19
- dir.glob('*.json')
18
+ def session_files
19
+ Result::SessionFiles.new(world:).paths
20
20
  end
21
21
 
22
22
  def load_session_file(path)
23
- world.parse_json(path.read)
23
+ world.parse_json(read_session_file(path))
24
24
  .bind(&Result::Session::CODEC.load_transform.public_method(:call))
25
25
  end
26
26
 
27
+ def read_session_file(path)
28
+ content = path.binread
29
+
30
+ content.start_with?(GZIP_MAGIC) ? Zlib.gunzip(content) : content
31
+ end
32
+
27
33
  # Shared base for commands that operate on a session
28
34
  class SessionCommand < self
29
35
  OPTIONS = %i[add_session_id_option].freeze
@@ -68,7 +74,9 @@ module Mutant
68
74
 
69
75
  def resolve_session_path
70
76
  if @session_id
71
- world.pathname.new("#{RESULTS_DIR}/#{@session_id}.json")
77
+ compressed = world.pathname.new("#{RESULTS_DIR}/#{@session_id}.json.gz")
78
+
79
+ compressed.file? ? compressed : world.pathname.new("#{RESULTS_DIR}/#{@session_id}.json")
72
80
  else
73
81
  session_files.last
74
82
  end
@@ -131,9 +139,9 @@ module Mutant
131
139
  end
132
140
 
133
141
  def colorize_unsupported(path)
134
- session_id = path.basename('.json')
142
+ session_id = path.basename.to_s.delete_suffix('.gz').delete_suffix('.json')
135
143
 
136
- Unparser::Color::RED.format(INCOMPATIBLE.ljust(54)) + session_id.to_s
144
+ Unparser::Color::RED.format(INCOMPATIBLE.ljust(54)) + session_id
137
145
  end
138
146
  end # List
139
147
 
@@ -230,7 +238,7 @@ module Mutant
230
238
  SUBCOMMANDS = [].freeze
231
239
  OPTIONS = %i[add_gc_options].freeze
232
240
 
233
- DEFAULT_KEEP = 100
241
+ DEFAULT_KEEP = Result::SessionFiles::KEEP
234
242
 
235
243
  def initialize(*)
236
244
  super
@@ -18,6 +18,8 @@ module Mutant
18
18
  evaluator =
19
19
  if memoized_method?(scope, target_method.name)
20
20
  Evaluator::Memoized
21
+ elsif module_function?(scope, target_method)
22
+ Evaluator::ModuleFunction
21
23
  else
22
24
  Evaluator
23
25
  end
@@ -39,6 +41,25 @@ module Mutant
39
41
  end
40
42
  private_class_method :memoized_method?
41
43
 
44
+ # Test if method was exposed via module_function
45
+ #
46
+ # module_function makes the instance method private and copies it
47
+ # onto the singleton class, where it keeps the source location.
48
+ #
49
+ # @param [Scope] scope
50
+ # @param [UnboundMethod] target_method
51
+ #
52
+ # @return [Boolean]
53
+ def self.module_function?(scope, target_method)
54
+ raw = scope.raw
55
+ method_name = target_method.name
56
+
57
+ raw.private_method_defined?(method_name) &&
58
+ raw.singleton_class.public_method_defined?(method_name) &&
59
+ raw.singleton_class.instance_method(method_name).source_location.eql?(target_method.source_location)
60
+ end
61
+ private_class_method :module_function?
62
+
42
63
  # Instance method specific evaluator
43
64
  class Evaluator < Evaluator
44
65
  MATCH_NODE_TYPE = :def
@@ -75,6 +96,11 @@ module Mutant
75
96
  end
76
97
 
77
98
  end # Memoized
99
+
100
+ # Evaluator specialized for module_function instance methods
101
+ class ModuleFunction < self
102
+ SUBJECT_CLASS = Subject::Method::Instance::ModuleFunction
103
+ end # ModuleFunction
78
104
  end # Evaluator
79
105
 
80
106
  private_constant(*constants(false))
@@ -19,6 +19,7 @@ module Mutant
19
19
  .record(:analysis) { run_driver(reporter, async_driver(env)) }
20
20
  .tap { |result| env.record(:report) { reporter.report(result) } }
21
21
  .tap { |result| Result::JSONWriter.new(env:, result:).call }
22
+ .tap { Result::SessionFiles.new(world: env.world).prune(Result::SessionFiles::KEEP) }
22
23
  end
23
24
  private_class_method :run_mutation_analysis
24
25
 
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ class Mutator
5
+ class Node
6
+ class Itblock < self
7
+
8
+ handle(:itblock)
9
+
10
+ children :receiver, :parameter, :body
11
+
12
+ private
13
+
14
+ def dispatch
15
+ emit_nil
16
+ emit_receiver_mutations(&method(:n_send?))
17
+ emit_body_mutations(&method(:references_it?))
18
+ end
19
+
20
+ def references_it?(node)
21
+ return true if node.eql?(s(:lvar, :it))
22
+
23
+ children = owns_it?(node) ? [node.children.first] : node.children
24
+
25
+ children.any? do |child|
26
+ child.instance_of?(::Parser::AST::Node) && references_it?(child)
27
+ end
28
+ end
29
+
30
+ def owns_it?(node)
31
+ n_block?(node) || n_itblock?(node) || n_def?(node) || n_defs?(node)
32
+ end
33
+ end # Itblock
34
+ end # Node
35
+ end # Mutator
36
+ end # Mutant
@@ -2,26 +2,30 @@
2
2
 
3
3
  module Mutant
4
4
  module Result
5
- # Write result JSON to .mutant/results/
5
+ # Write result JSON to .mutant/results/, gzip compressed
6
6
  class JSONWriter
7
7
  include Anima.new(:env, :result)
8
8
 
9
- RESULTS_DIR = '.mutant/results'
9
+ # Log stored in place of the log of a covered mutation
10
+ EMPTY_LOG = LogCapture::String.new(content: '')
10
11
 
11
12
  # Write result JSON file
12
13
  #
14
+ # The JSON repeats the source, diff, and tests of each mutation,
15
+ # and compresses about twentyfold.
16
+ #
13
17
  # Written to a temporary file first and renamed into place, so a
14
18
  # concurrent reader of the session file never observes a partial
15
19
  # document. This matters once the file is rewritten during a run.
16
20
  #
17
21
  # @return [Pathname]
18
22
  def call
19
- dir = env.world.pathname.new(RESULTS_DIR)
23
+ dir = env.world.pathname.new(SessionFiles::DIRECTORY)
20
24
  dir.mkpath
21
25
 
22
- path = dir.join("#{SESSION_ID}.json")
23
- tmp_path = dir.join("#{SESSION_ID}.json.tmp")
24
- tmp_path.write(json)
26
+ path = dir.join("#{SESSION_ID}.json.gz")
27
+ tmp_path = dir.join("#{SESSION_ID}.json.gz.tmp")
28
+ tmp_path.binwrite(Zlib.gzip(json))
25
29
  tmp_path.rename(path)
26
30
 
27
31
  path
@@ -41,7 +45,29 @@ module Mutant
41
45
  ruby_version: RUBY_VERSION,
42
46
  runtime: result.runtime,
43
47
  session_id: SESSION_ID,
44
- subject_results: result.subject_results
48
+ subject_results: result.subject_results.map(&method(:without_covered_logs))
49
+ )
50
+ end
51
+
52
+ def without_covered_logs(subject_result)
53
+ subject_result.with(
54
+ coverage_results: subject_result.coverage_results.map(&method(:without_log))
55
+ )
56
+ end
57
+
58
+ # The log of a covered mutation is the output of the tests that
59
+ # killed it. The session subcommands only report alive mutations,
60
+ # so nothing reads it back, and on a suite whose failures print
61
+ # large values it is most of the session file.
62
+ def without_log(coverage_result)
63
+ return coverage_result unless coverage_result.success?
64
+
65
+ mutation_result = coverage_result.mutation_result
66
+
67
+ coverage_result.with(
68
+ mutation_result: mutation_result.with(
69
+ isolation_result: mutation_result.isolation_result.with(log: EMPTY_LOG)
70
+ )
45
71
  )
46
72
  end
47
73
  end # JSONWriter
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutant
4
+ module Result
5
+ # The session files of past runs in .mutant/results/
6
+ class SessionFiles
7
+ include Anima.new(:world)
8
+
9
+ DIRECTORY = '.mutant/results'
10
+
11
+ # Sessions this version writes gzip compressed, and those earlier
12
+ # versions wrote as plain JSON. The plain ones are listed first,
13
+ # each kind oldest first, as the earlier versions wrote them before
14
+ # this one wrote any.
15
+ PATTERN = '*.json{,.gz}'
16
+
17
+ # Sessions a run keeps, and `mutant session gc` keeps by default
18
+ KEEP = 100
19
+
20
+ # The session files, oldest first
21
+ #
22
+ # @return [Array<Pathname>]
23
+ def paths
24
+ dir = world.pathname.new(DIRECTORY)
25
+
26
+ return EMPTY_ARRAY unless dir.directory?
27
+
28
+ dir.glob(PATTERN)
29
+ end
30
+
31
+ # Delete all but the most recent sessions
32
+ #
33
+ # A session written alongside this one by a concurrent run may
34
+ # have been deleted by that run already.
35
+ #
36
+ # @param [Integer] keep
37
+ #
38
+ # @return [Array<Pathname>]
39
+ # the deleted session files
40
+ def prune(keep)
41
+ excess = paths.reverse.drop(keep)
42
+
43
+ excess.each do |path|
44
+ path.delete
45
+ rescue Errno::ENOENT
46
+ nil
47
+ end
48
+ end
49
+ end # SessionFiles
50
+ end # Result
51
+ end # Mutant
@@ -38,6 +38,28 @@ module Mutant
38
38
  s(:begin, mutant, s(:send, nil, :memoize, s(:sym, name)))
39
39
  end
40
40
  end # Memoized
41
+
42
+ # Subject for instance methods exposed via module_function
43
+ #
44
+ # module_function copies the method onto the module's singleton
45
+ # class. Redefining only the instance method would leave that copy,
46
+ # which callers use, unmutated.
47
+ class ModuleFunction < self
48
+
49
+ # Prepare subject for mutation insertion
50
+ #
51
+ # @return [self]
52
+ def prepare
53
+ scope.raw.singleton_class.undef_method(name)
54
+
55
+ super
56
+ end
57
+
58
+ # Copy the inserted method onto the singleton class again
59
+ #
60
+ # @return [self]
61
+ def post_insert = tap { scope.raw.__send__(:module_function, name) }
62
+ end # ModuleFunction
41
63
  end # Instance
42
64
  end # Method
43
65
  end # Subject
data/lib/mutant.rb CHANGED
@@ -42,6 +42,7 @@ module Mutant
42
42
  stringio
43
43
  unparser
44
44
  yaml
45
+ zlib
45
46
  ].each { |name| require(name) }
46
47
  end
47
48
 
@@ -187,6 +188,7 @@ module Mutant
187
188
  require 'mutant/mutator/node/procarg_zero'
188
189
  require 'mutant/mutator/node/kwargs'
189
190
  require 'mutant/mutator/node/numblock'
191
+ require 'mutant/mutator/node/itblock' if RUBY_VERSION > '3.4.0'
190
192
  require 'mutant/mutator/node/const_pattern'
191
193
  require 'mutant/mutator/regexp'
192
194
  require 'mutant/loader'
@@ -249,6 +251,7 @@ module Mutant
249
251
  require 'mutant/mutation/runner/sink'
250
252
  require 'mutant/result'
251
253
  require 'mutant/result/session'
254
+ require 'mutant/result/session_files'
252
255
  require 'mutant/result/json_writer'
253
256
  require 'mutant/reporter'
254
257
  require 'mutant/reporter/null'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mutant
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.17.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Markus Schirp
@@ -304,6 +304,7 @@ files:
304
304
  - lib/mutant/mutator/node/if.rb
305
305
  - lib/mutant/mutator/node/in_pattern.rb
306
306
  - lib/mutant/mutator/node/index.rb
307
+ - lib/mutant/mutator/node/itblock.rb
307
308
  - lib/mutant/mutator/node/kwargs.rb
308
309
  - lib/mutant/mutator/node/kwbegin.rb
309
310
  - lib/mutant/mutator/node/literal.rb
@@ -389,6 +390,7 @@ files:
389
390
  - lib/mutant/result/json_writer.rb
390
391
  - lib/mutant/result/process_status.rb
391
392
  - lib/mutant/result/session.rb
393
+ - lib/mutant/result/session_files.rb
392
394
  - lib/mutant/result/test.rb
393
395
  - lib/mutant/scope.rb
394
396
  - lib/mutant/segment.rb
@@ -435,7 +437,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
435
437
  - !ruby/object:Gem::Version
436
438
  version: '0'
437
439
  requirements: []
438
- rubygems_version: 4.0.6
440
+ rubygems_version: 4.0.20
439
441
  specification_version: 4
440
442
  summary: ''
441
443
  test_files: []