saboteur 0.2.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +47 -0
- data/LICENSE +21 -0
- data/README.md +145 -0
- data/exe/saboteur +12 -0
- data/lib/saboteur/cli.rb +112 -0
- data/lib/saboteur/config.rb +133 -0
- data/lib/saboteur/engine/boot.rb +83 -0
- data/lib/saboteur/engine/candidates.rb +63 -0
- data/lib/saboteur/engine/coverage_store.rb +99 -0
- data/lib/saboteur/engine/diagnostics.rb +37 -0
- data/lib/saboteur/engine/diff_scope.rb +32 -0
- data/lib/saboteur/engine/distribution.rb +74 -0
- data/lib/saboteur/engine/result_cache.rb +93 -0
- data/lib/saboteur/engine/scheduler.rb +215 -0
- data/lib/saboteur/engine/subject_selection.rb +63 -0
- data/lib/saboteur/engine/suite_files.rb +82 -0
- data/lib/saboteur/engine/verdict_check.rb +78 -0
- data/lib/saboteur/engine.rb +133 -0
- data/lib/saboteur/execution/process_table.rb +177 -0
- data/lib/saboteur/execution/process_tree.rb +99 -0
- data/lib/saboteur/execution/protocol.rb +41 -0
- data/lib/saboteur/execution/reevaluator.rb +144 -0
- data/lib/saboteur/execution/runner.rb +191 -0
- data/lib/saboteur/execution/supervisor.rb +337 -0
- data/lib/saboteur/execution/worker_pool.rb +249 -0
- data/lib/saboteur/frameworks/rails.rb +166 -0
- data/lib/saboteur/hooks.rb +51 -0
- data/lib/saboteur/ignore_list.rb +62 -0
- data/lib/saboteur/integration/base.rb +72 -0
- data/lib/saboteur/integration/rspec.rb +143 -0
- data/lib/saboteur/mutation_source/base.rb +26 -0
- data/lib/saboteur/mutation_source/builtin.rb +232 -0
- data/lib/saboteur/mutation_source/edit.rb +18 -0
- data/lib/saboteur/mutation_source/mutation.rb +28 -0
- data/lib/saboteur/reporter/diff.rb +97 -0
- data/lib/saboteur/reporter/stryker.rb +116 -0
- data/lib/saboteur/reporter/terminal.rb +136 -0
- data/lib/saboteur/result.rb +134 -0
- data/lib/saboteur/selection/coverage_map.rb +113 -0
- data/lib/saboteur/selection/git_diff.rb +64 -0
- data/lib/saboteur/selection/recorder.rb +72 -0
- data/lib/saboteur/subject.rb +166 -0
- data/lib/saboteur/subject_finder.rb +98 -0
- data/lib/saboteur/version.rb +6 -0
- data/lib/saboteur.rb +51 -0
- metadata +130 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "prism"
|
|
5
|
+
require_relative "subject"
|
|
6
|
+
|
|
7
|
+
module Saboteur
|
|
8
|
+
# Finds `def` methods in a Ruby file with Prism.
|
|
9
|
+
#
|
|
10
|
+
# Included: instance methods and `def self.x` inside a class or module,
|
|
11
|
+
# defs inside `class << self`, and defs passed to a receiverless call such
|
|
12
|
+
# as `private def x`. Skipped: top-level defs, defs nested in defs, defs in
|
|
13
|
+
# blocks (`Class.new do`, `included do`), and `def obj.x` for obj != self.
|
|
14
|
+
class SubjectFinder
|
|
15
|
+
Parsed = Data.define(:subjects, :errors)
|
|
16
|
+
|
|
17
|
+
def call(file)
|
|
18
|
+
source = File.binread(file)
|
|
19
|
+
result = Prism.parse(source, filepath: file)
|
|
20
|
+
return Parsed.new(subjects: [], errors: result.errors.map(&:message)) if result.failure?
|
|
21
|
+
|
|
22
|
+
frozen = frozen_literals?(result)
|
|
23
|
+
subjects = []
|
|
24
|
+
walk(result.value, [], subjects, file: file, source: source, frozen: frozen)
|
|
25
|
+
mark_refinements(subjects, result.value)
|
|
26
|
+
Parsed.new(subjects: subjects, errors: [])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def frozen_literals?(result)
|
|
32
|
+
result.magic_comments.any? do |comment|
|
|
33
|
+
comment.key.tr("-", "_").casecmp?("frozen_string_literal") && comment.value.casecmp?("true")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def walk(node, nesting, out, **ctx)
|
|
38
|
+
case node
|
|
39
|
+
when nil, Prism::BlockNode, Prism::LambdaNode
|
|
40
|
+
nil
|
|
41
|
+
when Prism::ModuleNode
|
|
42
|
+
walk(node.body, nesting + [Scope.new(kind: :module, path: node.constant_path.slice)], out, **ctx)
|
|
43
|
+
when Prism::ClassNode
|
|
44
|
+
walk(node.body, nesting + [Scope.new(kind: :class, path: node.constant_path.slice)], out, **ctx)
|
|
45
|
+
when Prism::SingletonClassNode
|
|
46
|
+
walk_singleton_class(node, nesting, out, **ctx)
|
|
47
|
+
when Prism::DefNode
|
|
48
|
+
add_subject(node, nesting, out, **ctx)
|
|
49
|
+
else
|
|
50
|
+
node.compact_child_nodes.each { |child| walk(child, nesting, out, **ctx) }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def walk_singleton_class(node, nesting, out, **ctx)
|
|
55
|
+
return unless node.expression.is_a?(Prism::SelfNode)
|
|
56
|
+
return if nesting.empty? || nesting.last.sclass?
|
|
57
|
+
|
|
58
|
+
walk(node.body, nesting + [Scope.new(kind: :sclass, path: "self")], out, **ctx)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def add_subject(node, nesting, out, file:, source:, frozen:)
|
|
62
|
+
in_sclass = nesting.last&.sclass? || false
|
|
63
|
+
# `def self.x` inside `class << self`, and `def obj.x`, are not subjects.
|
|
64
|
+
return unless node.receiver.nil? || (node.receiver.is_a?(Prism::SelfNode) && !in_sclass)
|
|
65
|
+
|
|
66
|
+
singleton = in_sclass || !node.receiver.nil?
|
|
67
|
+
owner = owner_path(nesting)
|
|
68
|
+
return unless owner
|
|
69
|
+
|
|
70
|
+
out << Subject.new(name: node.name, owner: owner, singleton: singleton, sclass: in_sclass,
|
|
71
|
+
file: file, node: node, file_source: source, nesting: nesting,
|
|
72
|
+
frozen_literals: frozen)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Lexical path of the enclosing class/module. `class ::Foo` restarts at the top.
|
|
76
|
+
def owner_path(nesting)
|
|
77
|
+
parts = nesting.reject(&:sclass?).map(&:path)
|
|
78
|
+
return nil if parts.empty?
|
|
79
|
+
|
|
80
|
+
parts.each_with_object([]) do |path, acc|
|
|
81
|
+
acc.clear if path.start_with?("::")
|
|
82
|
+
acc << path.delete_prefix("::")
|
|
83
|
+
end.join("::")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def mark_refinements(subjects, program)
|
|
87
|
+
return unless uses_refinements?(program)
|
|
88
|
+
|
|
89
|
+
subjects.each { |subject| subject.skip_reason = "refinements" }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def uses_refinements?(node)
|
|
93
|
+
return true if node.is_a?(Prism::CallNode) && node.name == :using && node.receiver.nil?
|
|
94
|
+
|
|
95
|
+
node.compact_child_nodes.any? { |child| uses_refinements?(child) }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/saboteur.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "saboteur/version"
|
|
5
|
+
|
|
6
|
+
module Saboteur
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
# Invalid configuration or input files (exit code 2).
|
|
10
|
+
class ConfigError < Error; end
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# Set while hook files from config load, so they can call Saboteur.hook.
|
|
14
|
+
attr_accessor :current_hooks
|
|
15
|
+
|
|
16
|
+
# Registers a lifecycle hook from a hooks file:
|
|
17
|
+
#
|
|
18
|
+
# Saboteur.hook(:after_fork) { Redis.current.disconnect! }
|
|
19
|
+
# Saboteur.hook(:around_mutant) { |mutation, run| Timecop.freeze { run.call } }
|
|
20
|
+
def hook(name, callable = nil, &)
|
|
21
|
+
raise Error, "Saboteur.hook is only available while hook files load" unless current_hooks
|
|
22
|
+
|
|
23
|
+
current_hooks.register(name, callable, &)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
require_relative "saboteur/hooks"
|
|
29
|
+
require_relative "saboteur/mutation_source/base"
|
|
30
|
+
require_relative "saboteur/mutation_source/builtin"
|
|
31
|
+
require_relative "saboteur/integration/base"
|
|
32
|
+
require_relative "saboteur/subject"
|
|
33
|
+
require_relative "saboteur/subject_finder"
|
|
34
|
+
require_relative "saboteur/execution/protocol"
|
|
35
|
+
require_relative "saboteur/execution/process_table"
|
|
36
|
+
require_relative "saboteur/execution/process_tree"
|
|
37
|
+
require_relative "saboteur/execution/supervisor"
|
|
38
|
+
require_relative "saboteur/execution/reevaluator"
|
|
39
|
+
require_relative "saboteur/execution/runner"
|
|
40
|
+
require_relative "saboteur/execution/worker_pool"
|
|
41
|
+
require_relative "saboteur/selection/coverage_map"
|
|
42
|
+
require_relative "saboteur/selection/recorder"
|
|
43
|
+
require_relative "saboteur/selection/git_diff"
|
|
44
|
+
require_relative "saboteur/result"
|
|
45
|
+
require_relative "saboteur/config"
|
|
46
|
+
require_relative "saboteur/ignore_list"
|
|
47
|
+
require_relative "saboteur/frameworks/rails"
|
|
48
|
+
require_relative "saboteur/reporter/diff"
|
|
49
|
+
require_relative "saboteur/reporter/terminal"
|
|
50
|
+
require_relative "saboteur/reporter/stryker"
|
|
51
|
+
require_relative "saboteur/engine"
|
metadata
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: saboteur
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Vamsi Pavan Mahesh Gunturu
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: fiddle
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: prism
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.2'
|
|
34
|
+
- - "<"
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: '2'
|
|
37
|
+
type: :runtime
|
|
38
|
+
prerelease: false
|
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '1.2'
|
|
44
|
+
- - "<"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '2'
|
|
47
|
+
description: |
|
|
48
|
+
saboteur makes small deliberate changes to your methods, runs only the tests
|
|
49
|
+
that cover each change, and reports the changes no test notices.
|
|
50
|
+
email:
|
|
51
|
+
- gvpmahesh@gmail.com
|
|
52
|
+
executables:
|
|
53
|
+
- saboteur
|
|
54
|
+
extensions: []
|
|
55
|
+
extra_rdoc_files: []
|
|
56
|
+
files:
|
|
57
|
+
- CHANGELOG.md
|
|
58
|
+
- LICENSE
|
|
59
|
+
- README.md
|
|
60
|
+
- exe/saboteur
|
|
61
|
+
- lib/saboteur.rb
|
|
62
|
+
- lib/saboteur/cli.rb
|
|
63
|
+
- lib/saboteur/config.rb
|
|
64
|
+
- lib/saboteur/engine.rb
|
|
65
|
+
- lib/saboteur/engine/boot.rb
|
|
66
|
+
- lib/saboteur/engine/candidates.rb
|
|
67
|
+
- lib/saboteur/engine/coverage_store.rb
|
|
68
|
+
- lib/saboteur/engine/diagnostics.rb
|
|
69
|
+
- lib/saboteur/engine/diff_scope.rb
|
|
70
|
+
- lib/saboteur/engine/distribution.rb
|
|
71
|
+
- lib/saboteur/engine/result_cache.rb
|
|
72
|
+
- lib/saboteur/engine/scheduler.rb
|
|
73
|
+
- lib/saboteur/engine/subject_selection.rb
|
|
74
|
+
- lib/saboteur/engine/suite_files.rb
|
|
75
|
+
- lib/saboteur/engine/verdict_check.rb
|
|
76
|
+
- lib/saboteur/execution/process_table.rb
|
|
77
|
+
- lib/saboteur/execution/process_tree.rb
|
|
78
|
+
- lib/saboteur/execution/protocol.rb
|
|
79
|
+
- lib/saboteur/execution/reevaluator.rb
|
|
80
|
+
- lib/saboteur/execution/runner.rb
|
|
81
|
+
- lib/saboteur/execution/supervisor.rb
|
|
82
|
+
- lib/saboteur/execution/worker_pool.rb
|
|
83
|
+
- lib/saboteur/frameworks/rails.rb
|
|
84
|
+
- lib/saboteur/hooks.rb
|
|
85
|
+
- lib/saboteur/ignore_list.rb
|
|
86
|
+
- lib/saboteur/integration/base.rb
|
|
87
|
+
- lib/saboteur/integration/rspec.rb
|
|
88
|
+
- lib/saboteur/mutation_source/base.rb
|
|
89
|
+
- lib/saboteur/mutation_source/builtin.rb
|
|
90
|
+
- lib/saboteur/mutation_source/edit.rb
|
|
91
|
+
- lib/saboteur/mutation_source/mutation.rb
|
|
92
|
+
- lib/saboteur/reporter/diff.rb
|
|
93
|
+
- lib/saboteur/reporter/stryker.rb
|
|
94
|
+
- lib/saboteur/reporter/terminal.rb
|
|
95
|
+
- lib/saboteur/result.rb
|
|
96
|
+
- lib/saboteur/selection/coverage_map.rb
|
|
97
|
+
- lib/saboteur/selection/git_diff.rb
|
|
98
|
+
- lib/saboteur/selection/recorder.rb
|
|
99
|
+
- lib/saboteur/subject.rb
|
|
100
|
+
- lib/saboteur/subject_finder.rb
|
|
101
|
+
- lib/saboteur/version.rb
|
|
102
|
+
homepage: https://github.com/vamsipavanmahesh/saboteur
|
|
103
|
+
licenses:
|
|
104
|
+
- MIT
|
|
105
|
+
metadata:
|
|
106
|
+
homepage_uri: https://github.com/vamsipavanmahesh/saboteur
|
|
107
|
+
source_code_uri: https://github.com/vamsipavanmahesh/saboteur
|
|
108
|
+
changelog_uri: https://github.com/vamsipavanmahesh/saboteur/blob/main/CHANGELOG.md
|
|
109
|
+
bug_tracker_uri: https://github.com/vamsipavanmahesh/saboteur/issues
|
|
110
|
+
rubygems_mfa_required: 'true'
|
|
111
|
+
post_install_message:
|
|
112
|
+
rdoc_options: []
|
|
113
|
+
require_paths:
|
|
114
|
+
- lib
|
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '3.3'
|
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
125
|
+
requirements: []
|
|
126
|
+
rubygems_version: 3.5.22
|
|
127
|
+
signing_key:
|
|
128
|
+
specification_version: 4
|
|
129
|
+
summary: Mutation testing for Ruby, driven by per-test coverage.
|
|
130
|
+
test_files: []
|