crispr 0.1.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 205d5b57e819c5c2e07a085ea24e98907d2f4b669036455840e03381a23e7e69
4
- data.tar.gz: 23151bd8e26ad94e8e2cf2b914e62aace2e9e851b9b418b54a18341981db7fec
3
+ metadata.gz: fd1d78ee623852936e6f40e030ed4759e1f62315a653908c90efa0f905e3e70e
4
+ data.tar.gz: 30ca300c36a24e0330e9a891ad9f13c933ba95fe34b51478ef285c94777ac129
5
5
  SHA512:
6
- metadata.gz: 2c48c9bbe4748591fc70b8b9c455f91b3c90bffc912843534dbe47ea7359f77fad1a19af270681e47659d5d485f168fb42236bfa78b71e82f9f4393a722b6cc3
7
- data.tar.gz: e541dc13fc8fe3da01a1fc550960ba3c922cc65af0c6b8f59d9afa356c1e182bc5e28e8af5537d1ada229064ba9c5a4941217afaade3f8f0766a38ea5de1c872
6
+ metadata.gz: cb4b3a428a9e727e6fdc00e55f537223243d2a1e26a86af6059c468e2d1ddeb4dc92bf60e07a0717e066eb0f4969f709c5d2a328211c537daa640640ef34c07c
7
+ data.tar.gz: 123b8b26be881baac7caac3a1e9c1a6da75b3db0644fbc3c37e791bf80a3676f5c44f63f997512f7945720fbeb6d6a02e89522d898f34a2d508e88e74a13b6c5
data/.rubocop.yml CHANGED
@@ -4,7 +4,7 @@ plugins:
4
4
 
5
5
  AllCops:
6
6
  NewCops: enable
7
- TargetRubyVersion: 3.1
7
+ TargetRubyVersion: 3.2
8
8
 
9
9
  Layout/LineLength:
10
10
  Enabled: false
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
@@ -5,6 +5,8 @@ module Crispr
5
5
  # Abstract base class for all mutation strategies.
6
6
  # Subclasses must implement the `#mutations_for` method.
7
7
  class Base
8
+ include AST::Sexp
9
+
8
10
  # Returns an array of mutated AST nodes for a given node.
9
11
  #
10
12
  # @param node [Parser::AST::Node] the node to mutate
@@ -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
@@ -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,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
@@ -4,6 +4,24 @@ require "parser/current"
4
4
  require "unparser"
5
5
  require_relative "mutations/boolean"
6
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"
7
25
 
8
26
  module Crispr
9
27
  # Mutator performs simple AST mutations on Ruby source code.
@@ -11,7 +29,25 @@ module Crispr
11
29
  class Mutator
12
30
  MUTATORS = [
13
31
  Crispr::Mutations::Boolean.new,
14
- Crispr::Mutations::Numeric.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
15
51
  ].freeze
16
52
 
17
53
  def initialize(source_code)
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
- stdout, stderr, status = Open3.capture3(test_cmd)
24
- killed = !status.success?
23
+ test_cmd += " > /dev/null 2>&1" unless verbose
25
24
 
26
- puts stdout unless status.success?
27
- puts stderr unless status.success?
25
+ system(test_cmd)
26
+ killed = !$CHILD_STATUS.success?
28
27
 
29
28
  killed
30
29
  ensure
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crispr
4
- VERSION = "0.1.4"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.4
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: '0'
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: '0'
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: '0'
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: '0'
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,9 +54,27 @@ files:
54
54
  - bin/crispr
55
55
  - lib/crispr.rb
56
56
  - lib/crispr/cli.rb
57
+ - lib/crispr/mutations/arithmetic.rb
58
+ - lib/crispr/mutations/array.rb
59
+ - lib/crispr/mutations/assignment.rb
57
60
  - lib/crispr/mutations/base.rb
61
+ - lib/crispr/mutations/block.rb
58
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
59
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
60
78
  - lib/crispr/mutator.rb
61
79
  - lib/crispr/reporter.rb
62
80
  - lib/crispr/runner.rb
@@ -78,7 +96,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
78
96
  requirements:
79
97
  - - ">="
80
98
  - !ruby/object:Gem::Version
81
- version: 3.1.0
99
+ version: 3.2.0
82
100
  required_rubygems_version: !ruby/object:Gem::Requirement
83
101
  requirements:
84
102
  - - ">="