mutato 0.1.0

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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +13 -0
  3. data/LICENSE +21 -0
  4. data/README.md +218 -0
  5. data/exe/mutato +21 -0
  6. data/lib/mutato/abort.rb +12 -0
  7. data/lib/mutato/annotations.rb +65 -0
  8. data/lib/mutato/arid.rb +104 -0
  9. data/lib/mutato/baseline.rb +51 -0
  10. data/lib/mutato/boot.rb +99 -0
  11. data/lib/mutato/campaign.rb +52 -0
  12. data/lib/mutato/change.rb +49 -0
  13. data/lib/mutato/child.rb +135 -0
  14. data/lib/mutato/cli.rb +138 -0
  15. data/lib/mutato/clock.rb +21 -0
  16. data/lib/mutato/config.rb +39 -0
  17. data/lib/mutato/console.rb +33 -0
  18. data/lib/mutato/coverage_listener/tally.rb +26 -0
  19. data/lib/mutato/coverage_listener.rb +46 -0
  20. data/lib/mutato/covering.rb +45 -0
  21. data/lib/mutato/diff.rb +42 -0
  22. data/lib/mutato/emitter.rb +46 -0
  23. data/lib/mutato/equivalence.rb +30 -0
  24. data/lib/mutato/experience.rb +11 -0
  25. data/lib/mutato/flags.rb +58 -0
  26. data/lib/mutato/generation.rb +36 -0
  27. data/lib/mutato/generator.rb +10 -0
  28. data/lib/mutato/genres/conditions.rb +70 -0
  29. data/lib/mutato/genres/elements.rb +49 -0
  30. data/lib/mutato/genres/genre.rb +23 -0
  31. data/lib/mutato/genres/operators.rb +81 -0
  32. data/lib/mutato/genres/statements.rb +122 -0
  33. data/lib/mutato/genres/swaps.rb +58 -0
  34. data/lib/mutato/genres/values.rb +41 -0
  35. data/lib/mutato/genres.rb +53 -0
  36. data/lib/mutato/group_hooks.rb +45 -0
  37. data/lib/mutato/hits.rb +18 -0
  38. data/lib/mutato/installer/header.rb +40 -0
  39. data/lib/mutato/installer.rb +133 -0
  40. data/lib/mutato/measure.rb +37 -0
  41. data/lib/mutato/mode/control.rb +43 -0
  42. data/lib/mutato/mode/run.rb +33 -0
  43. data/lib/mutato/mode.rb +26 -0
  44. data/lib/mutato/mutation.rb +85 -0
  45. data/lib/mutato/nodes.rb +150 -0
  46. data/lib/mutato/options.rb +33 -0
  47. data/lib/mutato/output.rb +76 -0
  48. data/lib/mutato/privates.rb +46 -0
  49. data/lib/mutato/report.rb +36 -0
  50. data/lib/mutato/rspec_adapter/progress.rb +20 -0
  51. data/lib/mutato/rspec_adapter/result.rb +15 -0
  52. data/lib/mutato/rspec_adapter.rb +129 -0
  53. data/lib/mutato/run.rb +44 -0
  54. data/lib/mutato/selection.rb +100 -0
  55. data/lib/mutato/session.rb +53 -0
  56. data/lib/mutato/skips.rb +54 -0
  57. data/lib/mutato/soft_timeout.rb +8 -0
  58. data/lib/mutato/subject.rb +21 -0
  59. data/lib/mutato/subjects/scope.rb +44 -0
  60. data/lib/mutato/subjects.rb +67 -0
  61. data/lib/mutato/suite.rb +6 -0
  62. data/lib/mutato/trial.rb +93 -0
  63. data/lib/mutato/usage.rb +41 -0
  64. data/lib/mutato/version.rb +6 -0
  65. data/lib/mutato/visitor/cursor.rb +28 -0
  66. data/lib/mutato/visitor/walk.rb +17 -0
  67. data/lib/mutato/visitor.rb +88 -0
  68. data/lib/mutato/watchdog.rb +54 -0
  69. data/lib/mutato.rb +55 -0
  70. metadata +128 -0
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ module Mutato
6
+ # Which methods are only called from logging: a file-wide fact no single method shows.
7
+ class Usage
8
+ def initialize(tree)
9
+ # Names called inside logging, and names called anywhere else.
10
+ @calls = { true => Set.new, false => Set.new }
11
+ @private = Set.new
12
+ scan(tree, in_arid: false)
13
+ end
14
+
15
+ def logging_only?(name)
16
+ @calls.fetch(true).include?(name) && !@calls.fetch(false).include?(name)
17
+ end
18
+
19
+ # A private method only ever called from logging is as arid as the logging. A public one
20
+ # may be called from another file.
21
+ def demoted(subject)
22
+ name = subject.name
23
+ return subject if subject.skip || !logging_only?(name) || !@private.include?(name)
24
+
25
+ subject.with(skip: "only used in logging")
26
+ end
27
+
28
+ private
29
+
30
+ def scan(node, in_arid:)
31
+ in_arid = called(node, in_arid:) if node.is_a?(Prism::CallNode)
32
+ @private.merge(Privates.names(node)) if node.is_a?(Prism::StatementsNode)
33
+ node.compact_child_nodes.each { |child| scan(child, in_arid:) }
34
+ end
35
+
36
+ def called(call, in_arid:)
37
+ @calls.fetch(in_arid) << call.name
38
+ in_arid || Arid.arid_node?(call)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutato
4
+ VERSION = "0.1.0"
5
+ public_constant :VERSION
6
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutato
4
+ class Visitor
5
+ # Inside a block, `outer` stays the statement outside it, whose tests judge the mutant.
6
+ Cursor = Data.define(:statement, :outer, :in_block) do
7
+ def self.start
8
+ new(statement: nil, outer: nil, in_block: false)
9
+ end
10
+
11
+ def at(statement)
12
+ with(statement:, outer: in_block ? outer : statement)
13
+ end
14
+
15
+ def inside_block
16
+ with(in_block: true)
17
+ end
18
+
19
+ def outer_lines(node)
20
+ return unless in_block
21
+
22
+ location = (outer || node).location
23
+ location.start_line..location.end_line
24
+ end
25
+ end
26
+ private_constant :Cursor
27
+ end
28
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutato
4
+ class Visitor
5
+ Walk = Struct.new(:cursor, :deleted, :spared, :dead) do
6
+ def self.start
7
+ new(Cursor.start, identity_set, identity_set, identity_set)
8
+ end
9
+
10
+ # Nodes compare by structure; the walk needs the very node.
11
+ def self.identity_set
12
+ Set.new.compare_by_identity
13
+ end
14
+ end
15
+ private_constant :Walk
16
+ end
17
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "visitor/cursor"
5
+ require_relative "visitor/walk"
6
+
7
+ module Mutato
8
+ class Visitor
9
+ DEFINITIONS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
10
+ private_constant :DEFINITIONS
11
+
12
+ attr_reader :subject, :walk
13
+
14
+ def initialize(subject, emitter)
15
+ @subject = subject
16
+ @emitter = emitter
17
+ @walk = Walk.start
18
+ end
19
+
20
+ # The last statement is the value and stays, unless the caller drops it: `new`, `x.y = 1`.
21
+ def run
22
+ body = subject.node.body
23
+ Genres::Values.new(self).mutate(body)
24
+ statements = Nodes.statements_of(body)
25
+ walk.spared << statements.last if statements && !Nodes.value_dropped?(subject.name)
26
+ visit(body)
27
+ end
28
+
29
+ def source
30
+ @emitter.source
31
+ end
32
+
33
+ def emit(genre, change)
34
+ @emitter.add(change.with(genre:), subject, walk.cursor)
35
+ end
36
+
37
+ def with_statement(statement, &)
38
+ with_cursor(walk.cursor.at(statement), &)
39
+ end
40
+
41
+ def visit(node)
42
+ case node
43
+ when Prism::StatementsNode then visit_statements(node)
44
+ when Prism::BlockNode, Prism::LambdaNode then visit_block(node)
45
+ when Prism::EmbeddedStatementsNode, Prism::ParenthesesNode then visit_expression(node)
46
+ when nil, *DEFINITIONS then nil
47
+ else visit_other(node)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def visit_statements(node)
54
+ Genres::Statements.new(self).mutate(node)
55
+ node.body.each { |statement| visit_statement(statement) }
56
+ end
57
+
58
+ # Nothing inside a store no one reads is mutated either.
59
+ def visit_statement(statement)
60
+ with_statement(statement) { visit(statement) } unless walk.dead.include?(statement)
61
+ end
62
+
63
+ def visit_block(node)
64
+ with_cursor(walk.cursor.inside_block) { visit_children(node) }
65
+ end
66
+
67
+ # Statements inside `#{}` or parentheses are expressions: mutated, never deleted.
68
+ def visit_expression(node)
69
+ node.compact_child_nodes.flat_map(&:body).each { |child| visit(child) }
70
+ end
71
+
72
+ def visit_other(node)
73
+ visit_children(node) unless Genres.mutate(self, node) == :stop
74
+ end
75
+
76
+ def visit_children(node)
77
+ node.compact_child_nodes.each { |child| visit(child) }
78
+ end
79
+
80
+ def with_cursor(cursor)
81
+ previous = walk.cursor
82
+ walk.cursor = cursor
83
+ yield
84
+ ensure
85
+ walk.cursor = previous
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mutato
4
+ # Past the budget it stops the framework and raises; the parent's SIGKILL is the backstop.
5
+ class Watchdog
6
+ def initialize(adapter, budget)
7
+ @adapter = adapter
8
+ @budget = budget
9
+ @expired = false
10
+ @running = nil
11
+ end
12
+
13
+ # A timeout whatever the tests made of it, as RSpec rescues the exception like any other,
14
+ # unless a test had failed before time ran out.
15
+ def run(ids)
16
+ timer = Thread.new { expire }
17
+ judged(guarded(ids))
18
+ ensure
19
+ timer&.kill
20
+ end
21
+
22
+ private
23
+
24
+ def judged(verdict)
25
+ return verdict unless @expired
26
+
27
+ failing = verdict[:failing]
28
+ failing && failing != @running ? verdict : { outcome: :timeout, running: @running }
29
+ end
30
+
31
+ def guarded(ids)
32
+ @adapter.run(ids).outcome
33
+ rescue SoftTimeout
34
+ { outcome: :timeout, running: @running }
35
+ rescue SystemExit, SignalException => error
36
+ exited(error)
37
+ end
38
+
39
+ def expire
40
+ sleep @budget
41
+ @running = @adapter.current
42
+ @expired = true
43
+ @adapter.stop
44
+ Thread.main.raise(SoftTimeout)
45
+ end
46
+
47
+ # A mutant forced an exit path.
48
+ def exited(error)
49
+ puts "#{error.class}: #{error.message}"
50
+ current = @adapter.current
51
+ { outcome: :caught, failing: current, ran: [current].compact }
52
+ end
53
+ end
54
+ end
data/lib/mutato.rb ADDED
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mutato/arid"
4
+ require_relative "mutato/change"
5
+ require_relative "mutato/config"
6
+ require_relative "mutato/emitter"
7
+ require_relative "mutato/generation"
8
+ require_relative "mutato/generator"
9
+ require_relative "mutato/genres"
10
+ require_relative "mutato/installer"
11
+ require_relative "mutato/mutation"
12
+ require_relative "mutato/nodes"
13
+ require_relative "mutato/privates"
14
+ require_relative "mutato/skips"
15
+ require_relative "mutato/subject"
16
+ require_relative "mutato/subjects"
17
+ require_relative "mutato/usage"
18
+ require_relative "mutato/version"
19
+ require_relative "mutato/visitor"
20
+
21
+ module Mutato
22
+ def self.config
23
+ @config ||= Config.new
24
+ end
25
+
26
+ def self.before_fork(&block)
27
+ config.hooks[:before_fork] << block
28
+ end
29
+
30
+ def self.after_fork(&block)
31
+ config.hooks[:after_fork] << block
32
+ end
33
+
34
+ def self.after_boot(&block)
35
+ config.hooks[:after_boot] << block
36
+ end
37
+
38
+ def self.before_mutant(&block)
39
+ config.hooks[:before_mutant] << block
40
+ end
41
+
42
+ def self.skip(pattern, reason)
43
+ config.skip(pattern, reason)
44
+ end
45
+
46
+ def self.arid(*names)
47
+ config.arid(*names)
48
+ end
49
+
50
+ # A forked child must not share the parent's database sockets.
51
+ before_fork { Sequel::DATABASES.each(&:disconnect) if defined?(Sequel) }
52
+ before_fork do
53
+ ActiveRecord::Base.connection_handler.clear_all_connections! if defined?(ActiveRecord)
54
+ end
55
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mutato
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mathias Løkke Madsen
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: prism
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.8'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.8'
26
+ description: |
27
+ Mutates methods with Prism, redefines them in forked children of a
28
+ test process booted once, selects the tests that cover each mutant from
29
+ per-test coverage, and reports the survivors. RSpec.
30
+ executables:
31
+ - mutato
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - CHANGELOG.md
36
+ - LICENSE
37
+ - README.md
38
+ - exe/mutato
39
+ - lib/mutato.rb
40
+ - lib/mutato/abort.rb
41
+ - lib/mutato/annotations.rb
42
+ - lib/mutato/arid.rb
43
+ - lib/mutato/baseline.rb
44
+ - lib/mutato/boot.rb
45
+ - lib/mutato/campaign.rb
46
+ - lib/mutato/change.rb
47
+ - lib/mutato/child.rb
48
+ - lib/mutato/cli.rb
49
+ - lib/mutato/clock.rb
50
+ - lib/mutato/config.rb
51
+ - lib/mutato/console.rb
52
+ - lib/mutato/coverage_listener.rb
53
+ - lib/mutato/coverage_listener/tally.rb
54
+ - lib/mutato/covering.rb
55
+ - lib/mutato/diff.rb
56
+ - lib/mutato/emitter.rb
57
+ - lib/mutato/equivalence.rb
58
+ - lib/mutato/experience.rb
59
+ - lib/mutato/flags.rb
60
+ - lib/mutato/generation.rb
61
+ - lib/mutato/generator.rb
62
+ - lib/mutato/genres.rb
63
+ - lib/mutato/genres/conditions.rb
64
+ - lib/mutato/genres/elements.rb
65
+ - lib/mutato/genres/genre.rb
66
+ - lib/mutato/genres/operators.rb
67
+ - lib/mutato/genres/statements.rb
68
+ - lib/mutato/genres/swaps.rb
69
+ - lib/mutato/genres/values.rb
70
+ - lib/mutato/group_hooks.rb
71
+ - lib/mutato/hits.rb
72
+ - lib/mutato/installer.rb
73
+ - lib/mutato/installer/header.rb
74
+ - lib/mutato/measure.rb
75
+ - lib/mutato/mode.rb
76
+ - lib/mutato/mode/control.rb
77
+ - lib/mutato/mode/run.rb
78
+ - lib/mutato/mutation.rb
79
+ - lib/mutato/nodes.rb
80
+ - lib/mutato/options.rb
81
+ - lib/mutato/output.rb
82
+ - lib/mutato/privates.rb
83
+ - lib/mutato/report.rb
84
+ - lib/mutato/rspec_adapter.rb
85
+ - lib/mutato/rspec_adapter/progress.rb
86
+ - lib/mutato/rspec_adapter/result.rb
87
+ - lib/mutato/run.rb
88
+ - lib/mutato/selection.rb
89
+ - lib/mutato/session.rb
90
+ - lib/mutato/skips.rb
91
+ - lib/mutato/soft_timeout.rb
92
+ - lib/mutato/subject.rb
93
+ - lib/mutato/subjects.rb
94
+ - lib/mutato/subjects/scope.rb
95
+ - lib/mutato/suite.rb
96
+ - lib/mutato/trial.rb
97
+ - lib/mutato/usage.rb
98
+ - lib/mutato/version.rb
99
+ - lib/mutato/visitor.rb
100
+ - lib/mutato/visitor/cursor.rb
101
+ - lib/mutato/visitor/walk.rb
102
+ - lib/mutato/watchdog.rb
103
+ homepage: https://github.com/LQkkeN/mutato
104
+ licenses:
105
+ - MIT
106
+ metadata:
107
+ source_code_uri: https://github.com/LQkkeN/mutato
108
+ changelog_uri: https://github.com/LQkkeN/mutato/blob/main/CHANGELOG.md
109
+ bug_tracker_uri: https://github.com/LQkkeN/mutato/issues
110
+ rubygems_mfa_required: 'true'
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '3.2'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubygems_version: 4.0.20
126
+ specification_version: 4
127
+ summary: 'Mutation testing for Ruby: boot once, fork per mutant'
128
+ test_files: []