crispr 0.1.2 → 0.1.4
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 +13 -1
- data/lib/crispr/cli.rb +29 -14
- data/lib/crispr/mutations/base.rb +17 -0
- data/lib/crispr/mutations/{boolean_mutations.rb → boolean.rb} +4 -2
- data/lib/crispr/mutations/numeric.rb +42 -0
- data/lib/crispr/mutator.rb +13 -4
- data/lib/crispr/reporter.rb +41 -0
- data/lib/crispr/runner.rb +4 -2
- data/lib/crispr/version.rb +1 -1
- data/lib/crispr.rb +4 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 205d5b57e819c5c2e07a085ea24e98907d2f4b669036455840e03381a23e7e69
|
4
|
+
data.tar.gz: 23151bd8e26ad94e8e2cf2b914e62aace2e9e851b9b418b54a18341981db7fec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c48c9bbe4748591fc70b8b9c455f91b3c90bffc912843534dbe47ea7359f77fad1a19af270681e47659d5d485f168fb42236bfa78b71e82f9f4393a722b6cc3
|
7
|
+
data.tar.gz: e541dc13fc8fe3da01a1fc550960ba3c922cc65af0c6b8f59d9afa356c1e182bc5e28e8af5537d1ada229064ba9c5a4941217afaade3f8f0766a38ea5de1c872
|
data/.rubocop.yml
CHANGED
@@ -6,15 +6,27 @@ AllCops:
|
|
6
6
|
NewCops: enable
|
7
7
|
TargetRubyVersion: 3.1
|
8
8
|
|
9
|
+
Layout/LineLength:
|
10
|
+
Enabled: false
|
11
|
+
|
9
12
|
Metrics/AbcSize:
|
10
|
-
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
Metrics/CyclomaticComplexity:
|
16
|
+
Enabled: false
|
11
17
|
|
12
18
|
Metrics/MethodLength:
|
13
19
|
Max: 100
|
14
20
|
|
21
|
+
Metrics/PerceivedComplexity:
|
22
|
+
Enabled: false
|
23
|
+
|
15
24
|
RSpec/ExampleLength:
|
16
25
|
Max: 20
|
17
26
|
|
27
|
+
RSpec/MultipleExpectations:
|
28
|
+
Enabled: false
|
29
|
+
|
18
30
|
Style/StringLiterals:
|
19
31
|
EnforcedStyle: double_quotes
|
20
32
|
|
data/lib/crispr/cli.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "crispr/mutator"
|
4
4
|
require "crispr/runner"
|
5
|
+
require "crispr/reporter"
|
5
6
|
|
6
7
|
module Crispr
|
7
8
|
# Provides the command-line interface for the Crispr gem.
|
@@ -11,26 +12,40 @@ module Crispr
|
|
11
12
|
command = argv.shift
|
12
13
|
case command
|
13
14
|
when "run"
|
14
|
-
|
15
|
-
unless
|
16
|
-
puts "Error: Please specify a valid Ruby file to mutate."
|
15
|
+
input_path = argv.shift
|
16
|
+
unless input_path && File.exist?(input_path)
|
17
|
+
puts "Error: Please specify a valid Ruby file or directory to mutate."
|
17
18
|
exit 1
|
18
19
|
end
|
19
20
|
|
20
|
-
|
21
|
-
mutator = Crispr::Mutator.new(source)
|
22
|
-
mutations = mutator.mutations
|
21
|
+
paths = File.directory?(input_path) ? Dir.glob("#{input_path}/**/*.rb") : [input_path]
|
23
22
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
23
|
+
reporter = Crispr::Reporter.new
|
24
|
+
|
25
|
+
paths.each do |path|
|
26
|
+
source = File.read(path)
|
27
|
+
mutator = Crispr::Mutator.new(source)
|
28
|
+
mutations = mutator.mutations
|
28
29
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
if mutations.empty?
|
31
|
+
puts "No mutations found in #{path}."
|
32
|
+
next
|
33
|
+
end
|
34
|
+
|
35
|
+
mutations.each_with_index do |mutated, index|
|
36
|
+
puts "Running mutation #{index + 1}/#{mutations.size} on #{path}..."
|
37
|
+
killed = Crispr::Runner.run_mutation(path: path, mutated_source: mutated)
|
38
|
+
reporter.record(killed: killed)
|
39
|
+
puts killed ? "💥 Mutation killed" : "⚠️ Mutation survived"
|
40
|
+
end
|
33
41
|
end
|
42
|
+
|
43
|
+
summary = reporter.summary
|
44
|
+
puts
|
45
|
+
puts "Mutations: #{summary[:mutations]}"
|
46
|
+
puts "💥 Killed: #{summary[:killed]}"
|
47
|
+
puts "⚠️ Survived: #{summary[:survived]}"
|
48
|
+
puts "Score: #{summary[:score]}%"
|
34
49
|
else
|
35
50
|
puts "Usage: crispr run path/to/file.rb"
|
36
51
|
exit 1
|
@@ -0,0 +1,17 @@
|
|
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
|
+
# Returns an array of mutated AST nodes for a given node.
|
9
|
+
#
|
10
|
+
# @param node [Parser::AST::Node] the node to mutate
|
11
|
+
# @return [Array<Parser::AST::Node>] mutations
|
12
|
+
def mutations_for(node)
|
13
|
+
raise NotImplementedError, "#{self.class} must implement #mutations_for"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
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,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
|
data/lib/crispr/mutator.rb
CHANGED
@@ -2,12 +2,18 @@
|
|
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"
|
6
7
|
|
7
8
|
module Crispr
|
8
9
|
# Mutator performs simple AST mutations on Ruby source code.
|
9
|
-
#
|
10
|
+
# It delegates to multiple mutation strategies (Boolean, Numeric, etc.)
|
10
11
|
class Mutator
|
12
|
+
MUTATORS = [
|
13
|
+
Crispr::Mutations::Boolean.new,
|
14
|
+
Crispr::Mutations::Numeric.new
|
15
|
+
].freeze
|
16
|
+
|
11
17
|
def initialize(source_code)
|
12
18
|
@source_code = source_code
|
13
19
|
end
|
@@ -24,8 +30,11 @@ module Crispr
|
|
24
30
|
def find_mutations(node)
|
25
31
|
return [] unless node.is_a?(Parser::AST::Node)
|
26
32
|
|
27
|
-
local_mutations =
|
28
|
-
|
33
|
+
local_mutations =
|
34
|
+
MUTATORS.flat_map { |mutator| mutator.mutations_for(node) }
|
35
|
+
|
36
|
+
child_mutations =
|
37
|
+
node.children.flat_map { |child| find_mutations(child) }
|
29
38
|
|
30
39
|
local_mutations + child_mutations
|
31
40
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crispr
|
4
|
+
# Reporter collects mutation testing results and prints a summary.
|
5
|
+
class Reporter
|
6
|
+
# Initializes a new Reporter.
|
7
|
+
def initialize
|
8
|
+
@killed = 0
|
9
|
+
@survived = 0
|
10
|
+
end
|
11
|
+
|
12
|
+
# Records the result of a mutation test.
|
13
|
+
#
|
14
|
+
# @param killed [Boolean] whether the mutation was killed
|
15
|
+
# @return [void]
|
16
|
+
def record(killed:)
|
17
|
+
killed ? @killed += 1 : @survived += 1
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the mutation score as a percentage.
|
21
|
+
#
|
22
|
+
# @return [Float] the mutation score
|
23
|
+
def score
|
24
|
+
total = @killed + @survived
|
25
|
+
total.zero? ? 0.0 : (@killed.to_f / total * 100).round(2)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns a summary of mutation results.
|
29
|
+
#
|
30
|
+
# @return [Hash] summary statistics including totals and score
|
31
|
+
def summary
|
32
|
+
total = @killed + @survived
|
33
|
+
{
|
34
|
+
mutations: total,
|
35
|
+
killed: @killed,
|
36
|
+
survived: @survived,
|
37
|
+
score: score
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/crispr/runner.rb
CHANGED
@@ -11,14 +11,16 @@ module Crispr
|
|
11
11
|
#
|
12
12
|
# @param path [String] the path to the source file to mutate
|
13
13
|
# @param mutated_source [String] the mutated version of the file's source code
|
14
|
+
# @param test_path [String, nil] optional path to a specific test file to run
|
14
15
|
# @return [Boolean] true if the mutation was killed (test suite failed), false otherwise
|
15
|
-
def self.run_mutation(path:, mutated_source:)
|
16
|
+
def self.run_mutation(path:, mutated_source:, test_path: nil)
|
16
17
|
original_source = File.read(path)
|
17
18
|
|
18
19
|
begin
|
19
20
|
File.write(path, mutated_source)
|
20
21
|
|
21
|
-
|
22
|
+
test_cmd = test_path ? "bundle exec rspec #{test_path}" : "bundle exec rspec"
|
23
|
+
stdout, stderr, status = Open3.capture3(test_cmd)
|
22
24
|
killed = !status.success?
|
23
25
|
|
24
26
|
puts stdout unless status.success?
|
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.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron F Stanton
|
@@ -54,8 +54,11 @@ files:
|
|
54
54
|
- bin/crispr
|
55
55
|
- lib/crispr.rb
|
56
56
|
- lib/crispr/cli.rb
|
57
|
-
- lib/crispr/mutations/
|
57
|
+
- lib/crispr/mutations/base.rb
|
58
|
+
- lib/crispr/mutations/boolean.rb
|
59
|
+
- lib/crispr/mutations/numeric.rb
|
58
60
|
- lib/crispr/mutator.rb
|
61
|
+
- lib/crispr/reporter.rb
|
59
62
|
- lib/crispr/runner.rb
|
60
63
|
- lib/crispr/version.rb
|
61
64
|
- sig/crispr.rbs
|