crispr 0.1.3 → 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 +4 -4
- data/.rubocop.yml +4 -1
- data/README.md +1 -41
- data/lib/crispr/mutations/arithmetic.rb +49 -0
- data/lib/crispr/mutations/array.rb +40 -0
- data/lib/crispr/mutations/assignment.rb +47 -0
- data/lib/crispr/mutations/base.rb +19 -0
- data/lib/crispr/mutations/block.rb +35 -0
- data/lib/crispr/mutations/{boolean_mutations.rb → boolean.rb} +4 -2
- data/lib/crispr/mutations/comparison.rb +33 -0
- data/lib/crispr/mutations/conditional.rb +37 -0
- data/lib/crispr/mutations/control_flow.rb +29 -0
- data/lib/crispr/mutations/hash.rb +39 -0
- data/lib/crispr/mutations/literal.rb +47 -0
- data/lib/crispr/mutations/logical.rb +43 -0
- data/lib/crispr/mutations/method_call.rb +39 -0
- data/lib/crispr/mutations/numeric.rb +42 -0
- data/lib/crispr/mutations/range.rb +32 -0
- data/lib/crispr/mutations/regexp.rb +29 -0
- data/lib/crispr/mutations/rescue.rb +37 -0
- data/lib/crispr/mutations/string.rb +32 -0
- data/lib/crispr/mutations/symbol.rb +31 -0
- data/lib/crispr/mutations/ternary.rb +28 -0
- data/lib/crispr/mutations/unary.rb +32 -0
- data/lib/crispr/mutator.rb +49 -4
- data/lib/crispr/runner.rb +4 -5
- data/lib/crispr/version.rb +1 -1
- data/lib/crispr.rb +4 -0
- metadata +31 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd1d78ee623852936e6f40e030ed4759e1f62315a653908c90efa0f905e3e70e
|
4
|
+
data.tar.gz: 30ca300c36a24e0330e9a891ad9f13c933ba95fe34b51478ef285c94777ac129
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cb4b3a428a9e727e6fdc00e55f537223243d2a1e26a86af6059c468e2d1ddeb4dc92bf60e07a0717e066eb0f4969f709c5d2a328211c537daa640640ef34c07c
|
7
|
+
data.tar.gz: 123b8b26be881baac7caac3a1e9c1a6da75b3db0644fbc3c37e791bf80a3676f5c44f63f997512f7945720fbeb6d6a02e89522d898f34a2d508e88e74a13b6c5
|
data/.rubocop.yml
CHANGED
data/README.md
CHANGED
@@ -1,45 +1,5 @@
|
|
1
1
|
# Crispr
|
2
2
|
|
3
|
-
TODO: Delete this and the text below, and describe your gem
|
4
|
-
|
5
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/crispr`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
-
|
7
|
-
## Installation
|
8
|
-
|
9
|
-
TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
|
10
|
-
|
11
|
-
Install the gem and add to the application's Gemfile by executing:
|
12
|
-
|
13
|
-
```bash
|
14
|
-
bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
15
|
-
```
|
16
|
-
|
17
|
-
If bundler is not being used to manage dependencies, install the gem by executing:
|
18
|
-
|
19
|
-
```bash
|
20
|
-
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
|
21
|
-
```
|
22
|
-
|
23
|
-
## Usage
|
24
|
-
|
25
|
-
TODO: Write usage instructions here
|
26
|
-
|
27
|
-
## Development
|
28
|
-
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
-
|
31
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
-
|
33
|
-
## Contributing
|
34
|
-
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/crispr.
|
36
|
-
|
37
|
-
## License
|
38
|
-
|
39
|
-
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
40
|
-
|
41
|
-
# Crispr
|
42
|
-
|
43
3
|
**Crispr** is a mutation testing tool for Ruby. It introduces small mutations into your code to test whether your existing test suite can detect and fail on them. This helps reveal gaps in your test coverage and improve confidence in your codebase.
|
44
4
|
|
45
5
|
## Installation
|
@@ -108,4 +68,4 @@ Bug reports and pull requests are welcome on GitHub at [https://github.com/afsta
|
|
108
68
|
|
109
69
|
## License
|
110
70
|
|
111
|
-
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
|
71
|
+
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Crispr
|
6
|
+
module Mutations
|
7
|
+
# Mutates arithmetic operations such as +, -, *, /, etc.
|
8
|
+
class Arithmetic < Base
|
9
|
+
COMMUTATIVE = {
|
10
|
+
:+ => %i[- * / % **],
|
11
|
+
:* => %i[+ - / % **]
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
NON_COMMUTATIVE = {
|
15
|
+
:- => %i[+ * / % **],
|
16
|
+
:/ => %i[+ - * % **],
|
17
|
+
:% => %i[+ - * / **],
|
18
|
+
:** => %i[+ - * / %]
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
# Returns mutated AST nodes for arithmetic expressions.
|
22
|
+
#
|
23
|
+
# @param node [Parser::AST::Node] the AST node to inspect
|
24
|
+
# @return [Array<Parser::AST::Node>] mutated nodes
|
25
|
+
def mutations_for(node)
|
26
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
27
|
+
return [] unless node.type == :send
|
28
|
+
|
29
|
+
receiver, method_name, *args = node.children
|
30
|
+
return [] unless args.size == 1
|
31
|
+
return [] unless (COMMUTATIVE.keys + NON_COMMUTATIVE.keys).include?(method_name)
|
32
|
+
|
33
|
+
replacements = COMMUTATIVE[method_name] || NON_COMMUTATIVE[method_name]
|
34
|
+
mutations = replacements.map do |op|
|
35
|
+
Parser::AST::Node.new(:send, [receiver, op, args.first])
|
36
|
+
end
|
37
|
+
|
38
|
+
# Additional mutations
|
39
|
+
mutations << receiver
|
40
|
+
mutations << args.first
|
41
|
+
|
42
|
+
# For commutative operators, swap operands
|
43
|
+
mutations << Parser::AST::Node.new(:send, [args.first, method_name, receiver]) if COMMUTATIVE.key?(method_name)
|
44
|
+
|
45
|
+
mutations
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crispr
|
4
|
+
module Mutations
|
5
|
+
# Provides mutations for Ruby array nodes.
|
6
|
+
class Array < Base
|
7
|
+
# Generates mutated versions of an array node.
|
8
|
+
#
|
9
|
+
# @param node [Parser::AST::Node] the array node to mutate
|
10
|
+
# @return [Array<Parser::AST::Node>] list of mutated nodes
|
11
|
+
def mutations_for(node)
|
12
|
+
return [] unless node.is_a?(Parser::AST::Node) && node.type == :array
|
13
|
+
|
14
|
+
mutations = []
|
15
|
+
|
16
|
+
# Remove each element individually
|
17
|
+
node.children.each_with_index do |_, index|
|
18
|
+
new_elements = node.children.dup
|
19
|
+
new_elements.delete_at(index)
|
20
|
+
mutations << s(:array, *new_elements)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add nil to the end
|
24
|
+
mutations << s(:array, *node.children, s(:nil))
|
25
|
+
|
26
|
+
# Replace each element individually with nil
|
27
|
+
node.children.each_with_index do |_, index|
|
28
|
+
new_elements = node.children.dup
|
29
|
+
new_elements[index] = s(:nil)
|
30
|
+
mutations << s(:array, *new_elements)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Replace all elements with nil
|
34
|
+
mutations << s(:array, *::Array.new(node.children.size) { s(:nil) })
|
35
|
+
|
36
|
+
mutations
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# rubocop:disable Lint/BooleanSymbol
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "base"
|
5
|
+
|
6
|
+
module Crispr
|
7
|
+
module Mutations
|
8
|
+
# Mutates local variable assignments like `x = value`
|
9
|
+
class Assignment < Base
|
10
|
+
# Returns mutated AST nodes for assignments
|
11
|
+
#
|
12
|
+
# @param node [Parser::AST::Node] the AST node to inspect
|
13
|
+
# @return [Array<Parser::AST::Node>] mutated nodes
|
14
|
+
def mutations_for(node)
|
15
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
16
|
+
return [] unless node.type == :lvasgn
|
17
|
+
|
18
|
+
var_name, value_node = node.children
|
19
|
+
return [] unless value_node
|
20
|
+
|
21
|
+
mutations = []
|
22
|
+
|
23
|
+
# Remove assignment entirely (i.e., drop it)
|
24
|
+
mutations << nil
|
25
|
+
|
26
|
+
# Common alternative values
|
27
|
+
[s(:nil), s(:int, 0), s(:str, ""), s(:true), s(:false)].each do |replacement|
|
28
|
+
rep_node = replacement.is_a?(Symbol) ? s(replacement) : replacement
|
29
|
+
mutations << s(:lvasgn, var_name, rep_node)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Invert boolean
|
33
|
+
if value_node.type == :true
|
34
|
+
mutations << s(:lvasgn, var_name, s(:false))
|
35
|
+
elsif value_node.type == :false
|
36
|
+
mutations << s(:lvasgn, var_name, s(:true))
|
37
|
+
end
|
38
|
+
|
39
|
+
# Negate numeric
|
40
|
+
mutations << s(:lvasgn, var_name, s(:int, -value_node.children.first)) if value_node.type == :int
|
41
|
+
|
42
|
+
mutations.compact
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
# rubocop:enable Lint/BooleanSymbol
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crispr
|
4
|
+
module Mutations
|
5
|
+
# Abstract base class for all mutation strategies.
|
6
|
+
# Subclasses must implement the `#mutations_for` method.
|
7
|
+
class Base
|
8
|
+
include AST::Sexp
|
9
|
+
|
10
|
+
# Returns an array of mutated AST nodes for a given node.
|
11
|
+
#
|
12
|
+
# @param node [Parser::AST::Node] the node to mutate
|
13
|
+
# @return [Array<Parser::AST::Node>] mutations
|
14
|
+
def mutations_for(node)
|
15
|
+
raise NotImplementedError, "#{self.class} must implement #mutations_for"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crispr
|
4
|
+
module Mutations
|
5
|
+
# Generates mutations for Ruby block nodes.
|
6
|
+
# - Removes all block arguments.
|
7
|
+
# - Removes the block body.
|
8
|
+
# - Replaces the entire block with the method call only.
|
9
|
+
class Block < Base
|
10
|
+
def mutations_for(node)
|
11
|
+
mutations = []
|
12
|
+
|
13
|
+
method_call, args, body = *node
|
14
|
+
|
15
|
+
# Remove all block arguments
|
16
|
+
mutations << s(:block, method_call, s(:args), body) if args.is_a?(Parser::AST::Node) && args.type == :args && args.children.any?
|
17
|
+
|
18
|
+
# Remove the block body
|
19
|
+
mutations << s(:block, method_call, args, nil)
|
20
|
+
|
21
|
+
# Replace block with just the method call
|
22
|
+
mutations << method_call
|
23
|
+
|
24
|
+
# Remove each individual statement if body is a :begin
|
25
|
+
if body&.type == :begin
|
26
|
+
body.children.each do |child|
|
27
|
+
mutations << s(:block, method_call, args, child)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
mutations
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -1,16 +1,18 @@
|
|
1
1
|
# rubocop:disable Lint/BooleanSymbol
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require_relative "base"
|
5
|
+
|
4
6
|
module Crispr
|
5
7
|
module Mutations
|
6
8
|
# Provides boolean-specific AST mutations.
|
7
9
|
# Currently supports toggling `true` to `false` and `false` to `true`.
|
8
|
-
|
10
|
+
class Boolean < Base
|
9
11
|
# Returns a list of stringified mutated forms for the given boolean AST node.
|
10
12
|
#
|
11
13
|
# @param node [Parser::AST::Node] the AST node to inspect
|
12
14
|
# @return [Array<String>] mutated Ruby source code strings
|
13
|
-
def
|
15
|
+
def mutations_for(node)
|
14
16
|
return [] unless node.is_a?(Parser::AST::Node)
|
15
17
|
|
16
18
|
case node.type
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Crispr
|
6
|
+
module Mutations
|
7
|
+
# Defines mutations for comparison operators.
|
8
|
+
# This includes flipping equality and inequality, and reversing inequality directions.
|
9
|
+
class Comparison < Base
|
10
|
+
COMPARISON_FLIPS = {
|
11
|
+
:== => :!=,
|
12
|
+
:!= => :==,
|
13
|
+
:< => :>=,
|
14
|
+
:<= => :>,
|
15
|
+
:> => :<=,
|
16
|
+
:>= => :<
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
# Returns a list of mutated AST nodes where comparisons are flipped.
|
20
|
+
#
|
21
|
+
# @param node [Parser::AST::Node] the AST node to inspect
|
22
|
+
# @return [Array<Parser::AST::Node>] mutated nodes
|
23
|
+
def mutations_for(node)
|
24
|
+
return [] unless node.type == :send
|
25
|
+
return [] unless COMPARISON_FLIPS.key?(node.children[1])
|
26
|
+
|
27
|
+
flipped_operator = COMPARISON_FLIPS[node.children[1]]
|
28
|
+
mutated_node = node.updated(nil, [node.children[0], flipped_operator, *node.children[2..]])
|
29
|
+
[mutated_node]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# rubocop:disable Lint/BooleanSymbol
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Crispr
|
5
|
+
module Mutations
|
6
|
+
# Mutates conditional expressions like `if`, `unless`, etc.
|
7
|
+
#
|
8
|
+
# This mutator generates mutations for the condition part of a conditional expression.
|
9
|
+
# It flips the condition, replaces it with `true`, or replaces it with `false`.
|
10
|
+
class Conditional < Base
|
11
|
+
# Returns a list of mutated forms for the given AST node.
|
12
|
+
#
|
13
|
+
# @param node [Parser::AST::Node] the AST node to inspect
|
14
|
+
# @return [Array<Parser::AST::Node>] mutated AST nodes
|
15
|
+
def mutations_for(node)
|
16
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
17
|
+
return [] unless %i[if unless].include?(node.type)
|
18
|
+
|
19
|
+
condition, then_branch, else_branch = *node
|
20
|
+
mutations = []
|
21
|
+
|
22
|
+
# Flip condition using a logical not
|
23
|
+
flipped_condition = s(:send, condition, :!)
|
24
|
+
mutations << s(node.type, flipped_condition, then_branch, else_branch)
|
25
|
+
|
26
|
+
# Replace condition with `true`
|
27
|
+
mutations << s(node.type, s(:true), then_branch, else_branch)
|
28
|
+
|
29
|
+
# Replace condition with `false`
|
30
|
+
mutations << s(node.type, s(:false), then_branch, else_branch)
|
31
|
+
|
32
|
+
mutations
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
# rubocop:enable Lint/BooleanSymbol
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Crispr
|
6
|
+
module Mutations
|
7
|
+
# Mutates control flow keywords like `next`, `break`, and `return`.
|
8
|
+
class ControlFlow < Base
|
9
|
+
# Returns mutated AST nodes for control flow constructs.
|
10
|
+
#
|
11
|
+
# @param node [Parser::AST::Node] the AST node to inspect
|
12
|
+
# @return [Array<Parser::AST::Node>] mutated nodes
|
13
|
+
def mutations_for(node)
|
14
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
15
|
+
|
16
|
+
case node.type
|
17
|
+
when :next
|
18
|
+
[Parser::AST::Node.new(:break, node.children), Parser::AST::Node.new(:nil)]
|
19
|
+
when :break
|
20
|
+
[Parser::AST::Node.new(:next, node.children), Parser::AST::Node.new(:nil)]
|
21
|
+
when :return
|
22
|
+
[Parser::AST::Node.new(:return, []), Parser::AST::Node.new(:nil)]
|
23
|
+
else
|
24
|
+
[]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crispr
|
4
|
+
module Mutations
|
5
|
+
# Mutations targeting hash literals.
|
6
|
+
class Hash < Base
|
7
|
+
# Returns an array of mutated versions of the given hash node.
|
8
|
+
#
|
9
|
+
# @param node [Parser::AST::Node] the hash AST node
|
10
|
+
# @return [Array<Parser::AST::Node>] mutated AST nodes
|
11
|
+
def mutations_for(node)
|
12
|
+
return [] unless node.is_a?(Parser::AST::Node) && node.type == :hash
|
13
|
+
|
14
|
+
mutations = []
|
15
|
+
|
16
|
+
node.children.each_with_index do |pair, index|
|
17
|
+
# Remove individual pair
|
18
|
+
new_pairs = node.children.dup
|
19
|
+
new_pairs.delete_at(index)
|
20
|
+
mutations << s(:hash, *new_pairs)
|
21
|
+
|
22
|
+
# Replace value with nil
|
23
|
+
next unless pair.type == :pair
|
24
|
+
|
25
|
+
key, _value = *pair
|
26
|
+
new_pair = s(:pair, key, s(:nil))
|
27
|
+
new_pairs = node.children.dup
|
28
|
+
new_pairs[index] = new_pair
|
29
|
+
mutations << s(:hash, *new_pairs)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Add a new nil => nil pair
|
33
|
+
mutations << s(:hash, *(node.children + [s(:pair, s(:nil), s(:nil))]))
|
34
|
+
|
35
|
+
mutations
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# rubocop:disable Lint/BooleanSymbol
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "base"
|
5
|
+
|
6
|
+
module Crispr
|
7
|
+
module Mutations
|
8
|
+
# Mutates basic Ruby literals to edge-case or alternate values.
|
9
|
+
class Literal < Base
|
10
|
+
# Returns mutated AST nodes for literal types.
|
11
|
+
#
|
12
|
+
# @param node [Parser::AST::Node] the AST node to inspect
|
13
|
+
# @return [Array<Parser::AST::Node>] mutated nodes
|
14
|
+
def mutations_for(node)
|
15
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
16
|
+
|
17
|
+
case node.type
|
18
|
+
when :int
|
19
|
+
[replace_literal(node, 0), replace_literal(node, -1), replace_literal(node, 1)].uniq
|
20
|
+
when :str
|
21
|
+
[replace_literal(node, ""), replace_literal(node, "crispr")]
|
22
|
+
when :sym
|
23
|
+
[replace_literal(node, :other)]
|
24
|
+
when :array
|
25
|
+
[Parser::AST::Node.new(:array, [])]
|
26
|
+
when :hash
|
27
|
+
[Parser::AST::Node.new(:hash, [])]
|
28
|
+
when :true
|
29
|
+
[replace_literal(node, false)]
|
30
|
+
when :false
|
31
|
+
[replace_literal(node, true)]
|
32
|
+
when :nil
|
33
|
+
[Parser::AST::Node.new(:str, ["null"])]
|
34
|
+
else
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def replace_literal(original, new_value)
|
42
|
+
Parser::AST::Node.new(original.type, [new_value])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
# rubocop:enable Lint/BooleanSymbol
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Crispr
|
6
|
+
module Mutations
|
7
|
+
# Mutates logical operators like `&&` and `||`.
|
8
|
+
class Logical < Base
|
9
|
+
# Returns mutated AST nodes for logical expressions.
|
10
|
+
#
|
11
|
+
# @param node [Parser::AST::Node] the AST node to inspect
|
12
|
+
# @return [Array<Parser::AST::Node>] mutated nodes
|
13
|
+
def mutations_for(node)
|
14
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
15
|
+
|
16
|
+
case node.type
|
17
|
+
when :and
|
18
|
+
mutate_logical(node, :or)
|
19
|
+
when :or
|
20
|
+
mutate_logical(node, :and)
|
21
|
+
else
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Generate logical mutations by swapping type and extracting operands
|
29
|
+
#
|
30
|
+
# @param node [Parser::AST::Node]
|
31
|
+
# @param opposite [Symbol] the alternate logical operator type
|
32
|
+
# @return [Array<Parser::AST::Node>]
|
33
|
+
def mutate_logical(node, opposite)
|
34
|
+
left, right = node.children
|
35
|
+
[
|
36
|
+
Parser::AST::Node.new(opposite, [left, right]), # swap AND/OR
|
37
|
+
left,
|
38
|
+
right
|
39
|
+
]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crispr
|
4
|
+
module Mutations
|
5
|
+
# Mutates method calls by altering the method name or arguments.
|
6
|
+
class MethodCall < Base
|
7
|
+
# Returns true if the node is a method call (:send).
|
8
|
+
#
|
9
|
+
# @param node [Parser::AST::Node]
|
10
|
+
# @return [Boolean]
|
11
|
+
def match?(node)
|
12
|
+
node.type == :send
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns a list of mutated versions of the given method call.
|
16
|
+
#
|
17
|
+
# @param node [Parser::AST::Node]
|
18
|
+
# @return [Array<Parser::AST::Node>]
|
19
|
+
def mutations_for(node)
|
20
|
+
return [] unless match?(node)
|
21
|
+
|
22
|
+
original_receiver, original_method_name, *args = *node
|
23
|
+
|
24
|
+
mutations = []
|
25
|
+
|
26
|
+
# Replace method name with a placeholder
|
27
|
+
mutations << s(:send, original_receiver, :foo, *args)
|
28
|
+
|
29
|
+
# Add nil as an argument if none exist
|
30
|
+
mutations << s(:send, original_receiver, original_method_name, s(:nil)) if args.empty?
|
31
|
+
|
32
|
+
# Remove first argument if any exist
|
33
|
+
mutations << s(:send, original_receiver, original_method_name, *args[1..]) if args.any?
|
34
|
+
|
35
|
+
mutations
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Crispr
|
6
|
+
module Mutations
|
7
|
+
# The Numeric class defines mutations for integer literals.
|
8
|
+
# It generates alternative integer nodes by applying small transformations,
|
9
|
+
# such as incrementing, decrementing, zeroing, or negating the value.
|
10
|
+
class Numeric < Base
|
11
|
+
# Applies numeric mutations to the given AST node.
|
12
|
+
#
|
13
|
+
# @param node [Parser::AST::Node] the node to mutate
|
14
|
+
# @return [Array<Parser::AST::Node>] an array of mutated nodes
|
15
|
+
def mutations_for(node)
|
16
|
+
return [] unless node.type == :int
|
17
|
+
|
18
|
+
value = node.children[0]
|
19
|
+
|
20
|
+
# Generate a set of numeric mutations
|
21
|
+
mutations = []
|
22
|
+
mutations << replace(node, value + 1)
|
23
|
+
mutations << replace(node, value - 1)
|
24
|
+
mutations << replace(node, 0) unless value.zero?
|
25
|
+
mutations << replace(node, -value) unless value.zero?
|
26
|
+
|
27
|
+
mutations
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
# Creates a new AST node with the given integer value.
|
33
|
+
#
|
34
|
+
# @param node [Parser::AST::Node] the original node
|
35
|
+
# @param new_value [Integer] the new integer value
|
36
|
+
# @return [Parser::AST::Node] the mutated node
|
37
|
+
def replace(node, new_value)
|
38
|
+
Parser::AST::Node.new(:int, [new_value], location: node.location)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crispr
|
4
|
+
module Mutations
|
5
|
+
# Mutation class for handling Ruby range expressions (inclusive and exclusive).
|
6
|
+
# Applies mutations such as flipping range type, replacing with nil, and swapping bounds.
|
7
|
+
class Range < Base
|
8
|
+
# Returns a list of mutated forms of the given range node.
|
9
|
+
#
|
10
|
+
# @param node [Parser::AST::Node] the AST node representing a range
|
11
|
+
# @return [Array<Parser::AST::Node>] mutated AST nodes
|
12
|
+
def mutations_for(node)
|
13
|
+
return [] unless %i[irange erange].include?(node.type)
|
14
|
+
|
15
|
+
left, right = node.children
|
16
|
+
mutations = []
|
17
|
+
|
18
|
+
# Flip the range type (inclusive <-> exclusive)
|
19
|
+
flipped_type = node.type == :irange ? :erange : :irange
|
20
|
+
mutations << s(flipped_type, left, right)
|
21
|
+
|
22
|
+
# Replace with nil
|
23
|
+
mutations << s(:nil)
|
24
|
+
|
25
|
+
# Swap bounds if both sides are literals
|
26
|
+
mutations << s(node.type, right, left) if left&.type == :int && right&.type == :int
|
27
|
+
|
28
|
+
mutations
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crispr
|
4
|
+
module Mutations
|
5
|
+
# Generates mutations for regular expressions
|
6
|
+
class Regexp < Base
|
7
|
+
# Returns a list of mutated forms for the given Regexp AST node.
|
8
|
+
#
|
9
|
+
# @param node [Parser::AST::Node] the AST node to inspect
|
10
|
+
# @return [Array<Parser::AST::Node>] mutated AST nodes
|
11
|
+
def mutations_for(node)
|
12
|
+
return [] unless node.type == :regexp
|
13
|
+
|
14
|
+
parts = node.children[0...-1]
|
15
|
+
options = node.children.last
|
16
|
+
|
17
|
+
mutations = []
|
18
|
+
|
19
|
+
# Remove options if present
|
20
|
+
mutations << s(:regexp, *parts, 0) if options != 0
|
21
|
+
|
22
|
+
# Remove the pattern entirely
|
23
|
+
mutations << s(:regexp, s(:str, ""), options)
|
24
|
+
|
25
|
+
mutations
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crispr
|
4
|
+
module Mutations
|
5
|
+
# Generates mutations for `rescue` nodes.
|
6
|
+
#
|
7
|
+
# Possible mutations include:
|
8
|
+
# - Removing the entire rescue and returning only the main body.
|
9
|
+
# - Replacing the rescued body with `nil` or a string.
|
10
|
+
# - Removing individual rescue clauses from the rescue expression.
|
11
|
+
class Rescue < Base
|
12
|
+
def mutations_for(node)
|
13
|
+
mutations = []
|
14
|
+
|
15
|
+
# Remove the rescue entirely (just return the body before rescue)
|
16
|
+
mutations << node.children[0] if node.children[0]
|
17
|
+
|
18
|
+
# Replace the rescued body with nil
|
19
|
+
mutations << s(:block, s(:rescue, node.children.first, s(:resbody, nil, nil, s(:nil)), nil), nil, nil)
|
20
|
+
|
21
|
+
# Replace the rescued body with a different expression
|
22
|
+
mutations << s(:rescue, s(:str, "error"), *node.children[1..])
|
23
|
+
|
24
|
+
# Remove individual rescue clauses if present
|
25
|
+
if node.children[1..] && !node.children[1..].empty?
|
26
|
+
node.children[1..].each_with_index do |_rescue_clause, i|
|
27
|
+
new_children = node.children.dup
|
28
|
+
new_children.delete_at(i + 1) # +1 to skip the body
|
29
|
+
mutations << s(:rescue, *new_children)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
mutations
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Crispr
|
6
|
+
module Mutations
|
7
|
+
# Mutates string literals like "hello"
|
8
|
+
class String < Base
|
9
|
+
def mutations_for(node)
|
10
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
11
|
+
return [] unless node.type == :str
|
12
|
+
|
13
|
+
original = node.children.first
|
14
|
+
return [] unless original.is_a?(::String)
|
15
|
+
|
16
|
+
mutations = []
|
17
|
+
|
18
|
+
# Common string mutations
|
19
|
+
mutations << s(:str, "")
|
20
|
+
mutations << s(:str, "a")
|
21
|
+
mutations << s(:str, "test")
|
22
|
+
mutations << s(:str, original.reverse) unless original.empty?
|
23
|
+
mutations << s(:str, original.upcase) unless original.upcase == original
|
24
|
+
mutations << s(:str, original.downcase) unless original.downcase == original
|
25
|
+
|
26
|
+
mutations << s(:str, "mutated")
|
27
|
+
mutations << s(:nil)
|
28
|
+
mutations
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crispr
|
4
|
+
module Mutations
|
5
|
+
# This class generates mutations for symbol nodes in the AST.
|
6
|
+
# It provides different mutated versions of a symbol node for mutation testing.
|
7
|
+
class Symbol < Base
|
8
|
+
# Returns a list of mutated versions of a symbol node.
|
9
|
+
#
|
10
|
+
# @param node [Parser::AST::Node]
|
11
|
+
# @return [Array<Parser::AST::Node>]
|
12
|
+
def mutations_for(node)
|
13
|
+
return [] unless node.type == :sym
|
14
|
+
|
15
|
+
value = node.children.first
|
16
|
+
mutations = []
|
17
|
+
|
18
|
+
# Change the symbol to an empty symbol
|
19
|
+
mutations << s(:sym, :"") unless value == :""
|
20
|
+
|
21
|
+
# Change the symbol to a different symbol
|
22
|
+
mutations << s(:sym, :mutated) unless value == :mutated
|
23
|
+
|
24
|
+
# Remove the symbol node entirely (i.e., replace with nil)
|
25
|
+
mutations << s(:nil)
|
26
|
+
|
27
|
+
mutations
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Crispr
|
6
|
+
module Mutations
|
7
|
+
# Mutates ternary expressions like `cond ? a : b`
|
8
|
+
class Ternary < Base
|
9
|
+
# Returns mutated AST nodes for ternary expressions.
|
10
|
+
#
|
11
|
+
# @param node [Parser::AST::Node] the AST node to inspect
|
12
|
+
# @return [Array<Parser::AST::Node>] mutated nodes
|
13
|
+
def mutations_for(node)
|
14
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
15
|
+
return [] unless node.type == :if
|
16
|
+
return [] unless node.loc.respond_to?(:question) && node.loc.question
|
17
|
+
|
18
|
+
cond, if_branch, else_branch = node.children
|
19
|
+
[
|
20
|
+
if_branch,
|
21
|
+
else_branch,
|
22
|
+
cond,
|
23
|
+
Parser::AST::Node.new(:if, [cond, else_branch, if_branch]) # swap branches
|
24
|
+
].compact
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Crispr
|
6
|
+
module Mutations
|
7
|
+
# Mutates unary operator expressions such as !x, -x, +x.
|
8
|
+
class Unary < Base
|
9
|
+
# Returns mutated AST nodes for unary operator nodes.
|
10
|
+
#
|
11
|
+
# @param node [Parser::AST::Node] the AST node to inspect
|
12
|
+
# @return [Array<Parser::AST::Node>] mutated nodes
|
13
|
+
def mutations_for(node)
|
14
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
15
|
+
return [] unless node.type == :send
|
16
|
+
|
17
|
+
receiver, method_name, = node.children
|
18
|
+
|
19
|
+
case method_name
|
20
|
+
when :!
|
21
|
+
[receiver].compact
|
22
|
+
when :-@
|
23
|
+
[Parser::AST::Node.new(:send, [receiver, :+@])]
|
24
|
+
when :+@
|
25
|
+
[Parser::AST::Node.new(:send, [receiver, :-@])]
|
26
|
+
else
|
27
|
+
[]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/crispr/mutator.rb
CHANGED
@@ -2,12 +2,54 @@
|
|
2
2
|
|
3
3
|
require "parser/current"
|
4
4
|
require "unparser"
|
5
|
-
require_relative "mutations/
|
5
|
+
require_relative "mutations/boolean"
|
6
|
+
require_relative "mutations/numeric"
|
7
|
+
require_relative "mutations/comparison"
|
8
|
+
require_relative "mutations/literal"
|
9
|
+
require_relative "mutations/unary"
|
10
|
+
require_relative "mutations/control_flow"
|
11
|
+
require_relative "mutations/conditional"
|
12
|
+
require_relative "mutations/logical"
|
13
|
+
require_relative "mutations/ternary"
|
14
|
+
require_relative "mutations/arithmetic"
|
15
|
+
require_relative "mutations/assignment"
|
16
|
+
require_relative "mutations/method_call"
|
17
|
+
require_relative "mutations/array"
|
18
|
+
require_relative "mutations/hash"
|
19
|
+
require_relative "mutations/range"
|
20
|
+
require_relative "mutations/regexp"
|
21
|
+
require_relative "mutations/symbol"
|
22
|
+
require_relative "mutations/string"
|
23
|
+
require_relative "mutations/block"
|
24
|
+
require_relative "mutations/rescue"
|
6
25
|
|
7
26
|
module Crispr
|
8
27
|
# Mutator performs simple AST mutations on Ruby source code.
|
9
|
-
#
|
28
|
+
# It delegates to multiple mutation strategies (Boolean, Numeric, etc.)
|
10
29
|
class Mutator
|
30
|
+
MUTATORS = [
|
31
|
+
Crispr::Mutations::Boolean.new,
|
32
|
+
Crispr::Mutations::Numeric.new,
|
33
|
+
Crispr::Mutations::Comparison.new,
|
34
|
+
Crispr::Mutations::Literal.new,
|
35
|
+
Crispr::Mutations::Unary.new,
|
36
|
+
Crispr::Mutations::ControlFlow.new,
|
37
|
+
Crispr::Mutations::Conditional.new,
|
38
|
+
Crispr::Mutations::Logical.new,
|
39
|
+
Crispr::Mutations::Ternary.new,
|
40
|
+
Crispr::Mutations::Arithmetic.new,
|
41
|
+
Crispr::Mutations::Assignment.new,
|
42
|
+
Crispr::Mutations::MethodCall.new,
|
43
|
+
Crispr::Mutations::Array.new,
|
44
|
+
Crispr::Mutations::Hash.new,
|
45
|
+
Crispr::Mutations::Range.new,
|
46
|
+
Crispr::Mutations::Regexp.new,
|
47
|
+
Crispr::Mutations::Symbol.new,
|
48
|
+
Crispr::Mutations::String.new,
|
49
|
+
Crispr::Mutations::Block.new,
|
50
|
+
Crispr::Mutations::Rescue.new
|
51
|
+
].freeze
|
52
|
+
|
11
53
|
def initialize(source_code)
|
12
54
|
@source_code = source_code
|
13
55
|
end
|
@@ -24,8 +66,11 @@ module Crispr
|
|
24
66
|
def find_mutations(node)
|
25
67
|
return [] unless node.is_a?(Parser::AST::Node)
|
26
68
|
|
27
|
-
local_mutations =
|
28
|
-
|
69
|
+
local_mutations =
|
70
|
+
MUTATORS.flat_map { |mutator| mutator.mutations_for(node) }
|
71
|
+
|
72
|
+
child_mutations =
|
73
|
+
node.children.flat_map { |child| find_mutations(child) }
|
29
74
|
|
30
75
|
local_mutations + child_mutations
|
31
76
|
end
|
data/lib/crispr/runner.rb
CHANGED
@@ -13,18 +13,17 @@ module Crispr
|
|
13
13
|
# @param mutated_source [String] the mutated version of the file's source code
|
14
14
|
# @param test_path [String, nil] optional path to a specific test file to run
|
15
15
|
# @return [Boolean] true if the mutation was killed (test suite failed), false otherwise
|
16
|
-
def self.run_mutation(path:, mutated_source:, test_path: nil)
|
16
|
+
def self.run_mutation(path:, mutated_source:, test_path: nil, verbose: false)
|
17
17
|
original_source = File.read(path)
|
18
18
|
|
19
19
|
begin
|
20
20
|
File.write(path, mutated_source)
|
21
21
|
|
22
22
|
test_cmd = test_path ? "bundle exec rspec #{test_path}" : "bundle exec rspec"
|
23
|
-
|
24
|
-
killed = !status.success?
|
23
|
+
test_cmd += " > /dev/null 2>&1" unless verbose
|
25
24
|
|
26
|
-
|
27
|
-
|
25
|
+
system(test_cmd)
|
26
|
+
killed = !$CHILD_STATUS.success?
|
28
27
|
|
29
28
|
killed
|
30
29
|
ensure
|
data/lib/crispr/version.rb
CHANGED
data/lib/crispr.rb
CHANGED
@@ -5,6 +5,10 @@ require_relative "crispr/mutator"
|
|
5
5
|
require_relative "crispr/runner"
|
6
6
|
require_relative "crispr/cli"
|
7
7
|
|
8
|
+
require_relative "crispr/mutations/base"
|
9
|
+
require_relative "crispr/mutations/boolean"
|
10
|
+
require_relative "crispr/mutations/numeric"
|
11
|
+
|
8
12
|
module Crispr
|
9
13
|
class Error < StandardError; end
|
10
14
|
# Your code goes here...
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: crispr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron F Stanton
|
@@ -13,30 +13,30 @@ dependencies:
|
|
13
13
|
name: parser
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
15
15
|
requirements:
|
16
|
-
- - "
|
16
|
+
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version:
|
18
|
+
version: 3.3.8
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
|
-
- - "
|
23
|
+
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version:
|
25
|
+
version: 3.3.8
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: unparser
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
|
-
- - "
|
30
|
+
- - "~>"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version:
|
32
|
+
version: 0.8.0
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
|
-
- - "
|
37
|
+
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
39
|
+
version: 0.8.0
|
40
40
|
description: Crispr is a mutation testing tool for Ruby that introduces small code
|
41
41
|
mutations to verify the effectiveness of your test suite.
|
42
42
|
email:
|
@@ -54,7 +54,27 @@ files:
|
|
54
54
|
- bin/crispr
|
55
55
|
- lib/crispr.rb
|
56
56
|
- lib/crispr/cli.rb
|
57
|
-
- lib/crispr/mutations/
|
57
|
+
- lib/crispr/mutations/arithmetic.rb
|
58
|
+
- lib/crispr/mutations/array.rb
|
59
|
+
- lib/crispr/mutations/assignment.rb
|
60
|
+
- lib/crispr/mutations/base.rb
|
61
|
+
- lib/crispr/mutations/block.rb
|
62
|
+
- lib/crispr/mutations/boolean.rb
|
63
|
+
- lib/crispr/mutations/comparison.rb
|
64
|
+
- lib/crispr/mutations/conditional.rb
|
65
|
+
- lib/crispr/mutations/control_flow.rb
|
66
|
+
- lib/crispr/mutations/hash.rb
|
67
|
+
- lib/crispr/mutations/literal.rb
|
68
|
+
- lib/crispr/mutations/logical.rb
|
69
|
+
- lib/crispr/mutations/method_call.rb
|
70
|
+
- lib/crispr/mutations/numeric.rb
|
71
|
+
- lib/crispr/mutations/range.rb
|
72
|
+
- lib/crispr/mutations/regexp.rb
|
73
|
+
- lib/crispr/mutations/rescue.rb
|
74
|
+
- lib/crispr/mutations/string.rb
|
75
|
+
- lib/crispr/mutations/symbol.rb
|
76
|
+
- lib/crispr/mutations/ternary.rb
|
77
|
+
- lib/crispr/mutations/unary.rb
|
58
78
|
- lib/crispr/mutator.rb
|
59
79
|
- lib/crispr/reporter.rb
|
60
80
|
- lib/crispr/runner.rb
|
@@ -76,7 +96,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
96
|
requirements:
|
77
97
|
- - ">="
|
78
98
|
- !ruby/object:Gem::Version
|
79
|
-
version: 3.
|
99
|
+
version: 3.2.0
|
80
100
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
81
101
|
requirements:
|
82
102
|
- - ">="
|