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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e22eb1dbacaba9bb6d1e3cb3397f53f1d38eb8c4fec6b8f9296faafb17f1bec
4
- data.tar.gz: 650c04d67c82db6c211a98da8d8e0cfcd01d6b8c442f3425fd0bd34814dd8098
3
+ metadata.gz: 205d5b57e819c5c2e07a085ea24e98907d2f4b669036455840e03381a23e7e69
4
+ data.tar.gz: 23151bd8e26ad94e8e2cf2b914e62aace2e9e851b9b418b54a18341981db7fec
5
5
  SHA512:
6
- metadata.gz: 88583c8997c5c85c7d57fe26c779cd6f7fb86fd96c1559cf97174539f8d0c15d1baa58134752ad3f26d469bfc1cb2d7390dbfb1bd46876e95d6709b286db5fc7
7
- data.tar.gz: df10e617deb154c9568707a4d2649213d3a82ff0244b26c1d61e7409843155e4c668f8462209dcfffc719f6c7c80187713f789d0dce0cc97e41aabe1ea5b7c03
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
- Max: 30
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
- path = argv.shift
15
- unless path && File.exist?(path)
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
- source = File.read(path)
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
- if mutations.empty?
25
- puts "No mutations found."
26
- exit 0
27
- end
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
- mutations.each_with_index do |mutated, index|
30
- puts "Running mutation #{index + 1}/#{mutations.size}..."
31
- killed = Crispr::Runner.run_mutation(path: path, mutated_source: mutated)
32
- puts killed ? "💥 Mutation killed" : "⚠️ Mutation survived"
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
- module BooleanMutations
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 self.mutations_for(node)
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
@@ -2,12 +2,18 @@
2
2
 
3
3
  require "parser/current"
4
4
  require "unparser"
5
- require_relative "mutations/boolean_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
- # Currently, it supports changing `true` literals to `false` and vice versa.
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 = Crispr::Mutations::BooleanMutations.mutations_for(node)
28
- child_mutations = node.children.flat_map { |child| find_mutations(child) }
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
- stdout, stderr, status = Open3.capture3("bundle exec rspec")
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?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crispr
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
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.2
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/boolean_mutations.rb
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