diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..14fe26c
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,10 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+indent_style = space
+indent_size = 4
+
diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml
new file mode 100644
index 0000000..f285c7b
--- /dev/null
+++ b/.github/workflows/python.yml
@@ -0,0 +1,95 @@
+name: Python package
+
+on: [push]
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.10"]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install pip-tools
+ pip-sync requirements-dev.txt
+ - name: Lint with pycdestyle, isort, and pyright
+ run: |
+ sh lint.sh
+
+ unittest:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.10"]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install racket
+ run: |
+ sudo add-apt-repository ppa:plt/racket
+ sudo apt-get update
+ sudo apt-get install racket
+ racket --version
+ - name: Install prod dependencies only
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install pip-tools
+ pip-sync requirements.txt
+ - name: Run unit tests
+ run: |
+ python3 -m unittest discover
+
+ e2etest:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.10"]
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install racket
+ run: |
+ sudo add-apt-repository ppa:plt/racket
+ sudo apt-get update
+ sudo apt-get install racket
+ racket --version
+ raco pkg install al2-test-runner
+ - uses: actions/checkout@v3
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Build and install package
+ run: |
+ python3 -m pip install --upgrade pip
+ python3 -m pip install build
+ python3 -m build
+ python3 -m pip install $(ls -1 dist/*.whl | tail -n 1)
+ - name: Run mutation analysis e2e tests
+ working-directory: racket_mutation_analysis/mutation_analysis/e2e
+ run: |
+ ./run_e2e_test.sh tests/all_tests_all_mutants 0
+ ./run_e2e_test.sh tests/exclude_test_all_mutants 0
+ ./run_e2e_test.sh tests/include_test_all_mutants 1
+ ./run_e2e_test.sh tests/include_test_exclude_mutants 1
+ ./run_e2e_test.sh tests/include_test_include_mutants 1
+ ./run_e2e_test.sh tests/include_test_include_mutants2 0
+ ./run_e2e_test.sh tests/skip_false_positive_check 0
+ ./run_e2e_test.sh tests/stop_after_false_positive_check_0_exit_status 0
+ ./run_e2e_test.sh tests/stop_after_false_positive_check_1_exit_status 1
diff --git a/.gitignore b/.gitignore
index b6e4761..8b76422 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,11 @@ dmypy.json
# Pyre type checker
.pyre/
+
+**/.DS_Store
+
+cmp.rkd
+cond.skt
+sample.rkt
+.vscode/settings.json
+racket_mutation_analysis/mutation_analysis/e2e/actual_output.txt
diff --git a/README.md b/README.md
index cf2b8f4..25b25ab 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,57 @@
-# MACKET
-A mutation analysis tool for Racket
+# Racket Mutation Testing
+
+## Installing the Package
+Install the latest Python wheel as follows (requires Python >= 3.10):
+```
+python3 -m pip install $(ls -1 dist/*.whl | tail -n 1)
+```
+
+## Basic Usage
+First, create an empty phase command script by running:
+```
+generate-mutants --init
+```
+Replace {instructor-solution} and {hw-name} to specify the file to seed mutants in
+and the file to record the generated mutants in.
+
+Then, edit the generated script "mutation_commands.sh" to include the commands
+needed for each phase. The `assignment_template` directory an example
+of what this could look like for a racket assignment.
+
+Next, generate the mutants with:
+```
+generate-mutants {instructor-solution}.rkt {hw-name}-mutants.yml
+```
+
+And finally run the mutants with:
+```
+run-mutants --run_tests_in_one_batch {hw-name}-mutants.yml
+```
+
+## Dev Setup
+Requires python3.10 (with virtual environments) and [editorconfig](https://editorconfig.org/).
+
+After installing python3.10, create a virtual environment and install dependencies:
+```
+python3.10 -m venv venv
+source venv/bin/activate
+
+python -m pip install --upgrade pip
+python -m pip install pip-tools
+
+pip-sync requirements-dev.txt
+```
+
+## Linters
+To run pycodestyle, isort, and pyright:
+```
+./lint.sh
+```
+
+## Racket AST and Visitor
+AST node classes can be found at the beginning of racket_ast/scheme_reader.py, and visitor classes
+are in racket_ast/visitor.py. An example use of the visitor class can be found in read_write_test.py.
+The `main` function file reads a file from stdin or the command line, parses the AST (by calling
+`racket_ast.scheme_reader.read_file`), and invokes a `ToStrVisitor` on the AST, which prints
+the AST back out to stdout (discarding comments and replacing tabs with a single space, but
+otherwise preserving the source code structure).
diff --git a/assignment_template/.editorconfig b/assignment_template/.editorconfig
new file mode 100644
index 0000000..0db3d3e
--- /dev/null
+++ b/assignment_template/.editorconfig
@@ -0,0 +1,16 @@
+root = true
+
+[*]
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+charset = utf-8
+indent_style = space
+indent_size = 4
+
+[*.{html,yml,yaml}]
+indent_size = 2
+
+# Tab indentation (no size specified)
+[Makefile]
+indent_style = tab
diff --git a/assignment_template/README.md b/assignment_template/README.md
new file mode 100644
index 0000000..018d08c
--- /dev/null
+++ b/assignment_template/README.md
@@ -0,0 +1,3 @@
+# Racket Assignment Template
+Contains some scripts to generate an empty Racket assignment in which students have to implement test cases using `check-expect`.
+To get started, run `bash init_project.sh {hw_name}`, replacing {hw_name} with the name of the assignment, e.g., "hw3". Then follow the instructions printed by that script.
diff --git a/assignment_template/directivestotest b/assignment_template/directivestotest
new file mode 100644
index 0000000..bc4ebda
--- /dev/null
+++ b/assignment_template/directivestotest
@@ -0,0 +1,2 @@
+#lang htdp/bsl
+#reader(lib "htdp-beginner-reader.ss" "lang")((modname hw3-master) (read-case-sensitive #t) (teachpacks ()) (htdp-settings #(#t constructor repeating-decimal #f #t none #f () #f)))
diff --git a/assignment_template/init_project.sh b/assignment_template/init_project.sh
new file mode 100755
index 0000000..82b6b2a
--- /dev/null
+++ b/assignment_template/init_project.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+set -e
+
+if [ -z $1 ]; then
+ echo "Usage: bash $0 hw_name"
+ exit 1
+fi
+
+# The name of the file that students should submit without a file extension.
+# e.g. hw1, hw2, etc.
+# A new directory with this name will be created, and an assignment template
+# will be initialized in that directory using this name where necessary.
+hw_name=$1
+
+if [ -e $hw_name ]; then
+ echo "A file or directory with the name \"$hw_name\" already exists."
+ echo "Exiting."
+ exit 1
+fi
+
+mkdir $hw_name
+cp -r preprocessing $hw_name
+
+for file in template/*; do
+ sed "s/hwX/$hw_name/g" $file > $hw_name/$(echo $(basename $file) | sed "s/hwX/$hw_name/g")
+done
+
+
+echo "Project initialized. Next steps:"
+echo "= Run 'cd $hw_name'"
+echo "- Edit reader-directive.txt to contain the reader or lang directive needed for this assignment."
+echo "- Add the assignment's public symbols to public-symbols.rkt"
+echo "- Run 'racket public-symbols.rkt' and put the printed code at the bottom of $hw_name-instructor-solution.rkt"
+echo "- Add your instructor solution and test cases (check-expects) to $hw_name-instructor-solution.rkt"
+echo "- Run 'bash extract_instructor_tests.bash' to extract instructor test cases from $hw_name-instructor-solution.rkt into $hw_name-instructor-tests.rkt"
+echo "- Run 'bash generate_mutants.bash"
+echo "- Run 'make'"
diff --git a/assignment_template/preprocessing/extract_check_expects.rkt b/assignment_template/preprocessing/extract_check_expects.rkt
new file mode 100644
index 0000000..33af483
--- /dev/null
+++ b/assignment_template/preprocessing/extract_check_expects.rkt
@@ -0,0 +1,64 @@
+#lang racket
+(require 2htdp/batch-io)
+
+;; returns the list of objects of parsed file to be written out to file
+(define (parse-args file num)
+ (let
+ ([current_line (read-syntax file file)])
+ (if (not (eof-object? current_line))
+ (let [(datum (syntax->datum current_line))]
+ (if (and (list? datum) (eq? 'check-expect (car datum)))
+ (begin
+ (display " (test-case ")
+ (display "\"Test ")
+ (display num)
+ (display " line ")
+ (display (syntax-line current_line))
+ (display "\"")
+ (newline)
+ (display " ")
+ (writeln (list 'check-equal? (second datum) (third datum)))
+ (displayln " )")
+ (parse-args file (+ 1 num))
+ )
+
+ (parse-args file num)
+ )
+ )
+ '()
+ )
+ )
+)
+
+(define (main-helper file_name output)
+ (let ([file (open-input-file file_name)])
+ (begin
+ (port-count-lines! file)
+ (newline)
+
+ (displayln "(define test-suite-wrapper" )
+ (displayln " (test-suite \"Test Suite\"" )
+ (parse-args file 1)
+ (displayln " )")
+ (displayln ")")
+ (newline)
+ )
+ )
+)
+
+;;writes each test to a separate line at given location
+(define (test-separator lot output_file)
+ (cond
+ [(empty? lot) (values)]
+ [(cons? lot) (begin
+ (writeln (first lot) output_file)
+ ;;(newline output_file)
+ (test-separator (rest lot) output_file))])
+)
+
+(command-line #:program "Racket check-expect extractor" #:args (input_file)
+ (begin
+ (read-accept-reader #t)
+ (main-helper input_file '())
+ )
+)
diff --git a/assignment_template/preprocessing/extract_instructor_tests.bash b/assignment_template/preprocessing/extract_instructor_tests.bash
new file mode 100644
index 0000000..18f56db
--- /dev/null
+++ b/assignment_template/preprocessing/extract_instructor_tests.bash
@@ -0,0 +1,28 @@
+# Extract check-expects from the instructor solution into rackunit test cases.
+# Prints the resulting code to stdout.
+# The extracted tests can be used to evaluate the correctness of student
+# implementations.
+
+# The name of the file that students should submit without a file extension.
+# e.g. hw1, hw2, etc.
+# This name will be interpolated into the file names of the instructor solution
+# (e.g. hw1-instructor-solution.rkt), student implementation (e.g. hw1.rkt), and
+# instructor test cases (e.g. hw1-instructor-tests.rkt).
+hw_name=$1
+# The reader or lang special form that should be used in this assignment.
+# When check-expects are extracted from the student implementation and written
+# to a file containing only test cases, this reader or lang special form will
+# be placed at the top of the file.
+reader_directive=$2
+
+
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+cat <(echo $reader_directive) \
+ <(echo '(require rackunit)') \
+ <(echo '(require al2-test-runner)') \
+ <(echo '(require "./test-runner.rkt")') \
+ <(echo "(require \"./$hw_name.rkt\")") \
+ <(racket $SCRIPT_DIR/extract_requires.rkt <(sed -r 's/(#reader.*)|(#lang.*)//' $hw_name-instructor-solution.rkt)) \
+ <(racket $SCRIPT_DIR/extract_private_defs_instructor.rkt <(sed -r 's/(#reader.*)|(#lang.*)//' $hw_name-instructor-solution.rkt)) \
+ <(racket $SCRIPT_DIR/extract_check_expects.rkt <(sed -r 's/(#reader.*)|(#lang.*)//' $hw_name-instructor-solution.rkt)) \
+ <(echo '(test-runner-cmd-line test-suite-wrapper)')
diff --git a/assignment_template/preprocessing/extract_private_defs.rkt b/assignment_template/preprocessing/extract_private_defs.rkt
new file mode 100644
index 0000000..8c6395e
--- /dev/null
+++ b/assignment_template/preprocessing/extract_private_defs.rkt
@@ -0,0 +1,40 @@
+#lang racket
+
+(define (extract-private-defs file_name public-def-symbols public-struct-symbols)
+ (let ([file (open-input-file file_name)] )
+ (extract-defs-helper file public-def-symbols public-struct-symbols)
+ )
+)
+
+;; returns the list of objects of parsed file to be written out to file
+(define (extract-defs-helper file public-def-symbols public-struct-symbols)
+ (let
+ ([current_line (read-syntax file file)])
+ (unless (eof-object? current_line)
+ (let [(datum (syntax->datum current_line))]
+ (match datum
+ [
+ (list (quote define) (var const_name) _) #:when (and (symbol? const_name) (not (member const_name public-def-symbols)))
+ (writeln datum)
+ ]
+ [
+ (list-rest
+ (quote define)
+ (list-rest (var func_name) _)
+ _
+ ) #:when (not (member func_name public-def-symbols))
+ (writeln datum)
+ ]
+ [
+ (list (quote define-struct) (var const_name) _) #:when (and (symbol? const_name) (not (member const_name public-struct-symbols)))
+ (writeln datum)
+ ]
+ [_ '()]
+ )
+ )
+ (extract-defs-helper file public-def-symbols public-struct-symbols)
+ )
+ )
+)
+
+(provide extract-private-defs)
diff --git a/assignment_template/preprocessing/extract_private_defs_instructor.rkt b/assignment_template/preprocessing/extract_private_defs_instructor.rkt
new file mode 100644
index 0000000..942ba40
--- /dev/null
+++ b/assignment_template/preprocessing/extract_private_defs_instructor.rkt
@@ -0,0 +1,15 @@
+#lang racket
+
+;; To be used in local project setup context (i.e., with preprocessing scripts in a subdirectory).
+;; Extracts non-public defs (constants, helper methods, structs) that must accompany
+;; the instructor check-expects when extracted from the instructor solution.
+
+(require "./extract_private_defs.rkt")
+(require "../public-symbols.rkt")
+
+(command-line #:program "Racket instructor private defs extractor" #:args (input_file)
+ (begin
+ (read-accept-reader #t)
+ (extract-private-defs input_file public-def-symbols public-struct-symbols)
+ )
+)
diff --git a/assignment_template/preprocessing/extract_private_defs_student.rkt b/assignment_template/preprocessing/extract_private_defs_student.rkt
new file mode 100644
index 0000000..9d78b48
--- /dev/null
+++ b/assignment_template/preprocessing/extract_private_defs_student.rkt
@@ -0,0 +1,15 @@
+#lang racket
+
+;; To be used only in autograding context (i.e., all files in one flat directory)
+;; Extracts non-public defs (constants, helper methods, structs) needed to run
+;; student test cases during mutation testing.
+
+(require "./extract_private_defs.rkt")
+(require "./public-symbols.rkt")
+
+(command-line #:program "Racket student private defs extractor" #:args (input_file)
+ (begin
+ (read-accept-reader #t)
+ (extract-private-defs input_file public-def-symbols public-struct-symbols)
+ )
+)
diff --git a/assignment_template/preprocessing/extract_requires.rkt b/assignment_template/preprocessing/extract_requires.rkt
new file mode 100644
index 0000000..6641d4a
--- /dev/null
+++ b/assignment_template/preprocessing/extract_requires.rkt
@@ -0,0 +1,35 @@
+#lang racket
+(require 2htdp/batch-io)
+
+(define (parse-args file)
+ (let
+ ([current_line (read-syntax file file)])
+ (unless (eof-object? current_line)
+ (let [(datum (syntax->datum current_line))]
+ (match datum
+ [
+ (list-rest (quote require) _)
+ (begin
+ (writeln datum)
+ (parse-args file)
+ )
+ ]
+ [_ (parse-args file)]
+ )
+ )
+ )
+ )
+)
+
+(define (main-helper file_name output)
+ (let ([file (open-input-file file_name)] )
+ (parse-args file)
+ )
+)
+
+(command-line #:program "Racket requires extractor" #:args (input_file)
+ (begin
+ (read-accept-reader #t)
+ (main-helper input_file '())
+ )
+)
diff --git a/assignment_template/preprocessing/extract_solution_defs.rkt b/assignment_template/preprocessing/extract_solution_defs.rkt
new file mode 100644
index 0000000..61497a5
--- /dev/null
+++ b/assignment_template/preprocessing/extract_solution_defs.rkt
@@ -0,0 +1,45 @@
+#lang racket
+(require 2htdp/batch-io)
+
+(define (extract-solution-defs-helper file)
+ (let
+ ([current_line (read-syntax file file)])
+ (unless (eof-object? current_line)
+ (let [(datum (syntax->datum current_line))]
+ (match datum
+ [
+ (list (quote define) (var const_name) _) #:when (symbol? const_name)
+ (writeln datum)
+ ]
+ [
+ (list-rest
+ (quote define)
+ (list-rest (var func_name) _)
+ _
+ )
+ (writeln datum)
+ ]
+ [
+ (list (quote define-struct) (var struct_name) _)
+ (writeln datum)
+ ]
+ [_ '()]
+ )
+ )
+ (extract-solution-defs-helper file)
+ )
+ )
+)
+
+(define (extract-solution-defs file_name output)
+ (let ([file (open-input-file file_name)] )
+ (extract-solution-defs-helper file)
+ )
+)
+
+(command-line #:program "Racket solutions definitions extractor" #:args (input_file)
+ (begin
+ (read-accept-reader #t)
+ (extract-solution-defs input_file '())
+ )
+)
diff --git a/assignment_template/preprocessing/extract_student_tests.bash b/assignment_template/preprocessing/extract_student_tests.bash
new file mode 100644
index 0000000..d3aeeff
--- /dev/null
+++ b/assignment_template/preprocessing/extract_student_tests.bash
@@ -0,0 +1,27 @@
+# Extract check-expects from a student implementation into rackunit test cases.
+# Prints the resulting code to stdout.
+# The extracted student tests can be evaluated using mutation testing.
+
+# The name of the file that students should submit without a file extension.
+# e.g. hw1, hw2, etc.
+# This name will be interpolated into the file names of the instructor solution
+# (e.g. hw1-instructor-solution.rkt), student implementation (e.g. hw1.rkt), and
+# student test cases (e.g. hw1-tests.rkt).
+hw_name=$1
+# The reader or lang special form that should be used in this assignment.
+# When check-expects are extracted from the student implementation and written
+# to a file containing only test cases, this reader or lang special form will
+# be placed at the top of the file.
+reader_directive=$2
+
+
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+cat <(echo $reader_directive) \
+ <(echo '(require rackunit)') \
+ <(echo '(require al2-test-runner)') \
+ <(echo '(require "./test-runner.rkt")') \
+ <(echo "(require \"./$hw_name-instructor-solution.rkt\")") \
+ <(racket $SCRIPT_DIR/extract_requires.rkt <(sed -r 's/(#reader.*)|(#lang.*)//' $hw_name-instructor-solution.rkt)) \
+ <(racket $SCRIPT_DIR/extract_private_defs_student.rkt <(sed -r 's/(#reader.*)|(#lang.*)//' $hw_name.rkt)) \
+ <(racket $SCRIPT_DIR/extract_check_expects.rkt <(sed -r 's/(#reader.*)|(#lang.*)//' $hw_name.rkt)) \
+ <(echo '(test-runner-cmd-line test-suite-wrapper)')
diff --git a/assignment_template/preprocessing/prepare_mutants.bash b/assignment_template/preprocessing/prepare_mutants.bash
new file mode 100644
index 0000000..72b6f58
--- /dev/null
+++ b/assignment_template/preprocessing/prepare_mutants.bash
@@ -0,0 +1,23 @@
+# Creates the instructor solution file minus what should not be mutated.
+# Creates a file with the resulting file to a new subdirectory called
+# _mutant_gen.
+# This file will be used with the mutant generator to create the mutants
+# to analyze the student test cases with.
+
+# The name of the file that students should submit without a file extension.
+# e.g. hw1, hw2, etc.
+# This name will be interpolated into the file names of the instructor solution
+# (e.g. hw1-instructor-solution.rkt), student implementation (e.g. hw1.rkt), and
+# instructor test cases (e.g. hw1-instructor-tests.rkt).
+hw_name=$1
+# The reader or lang special form that should be used in this assignment.
+# When check-expects are extracted from the student implementation and written
+# to a file containing only test cases, this reader or lang special form will
+# be placed at the top of the file.
+reader_directive=$2
+mkdir -p _mutant_gen
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+cat <(echo "$(grep -E "(#lang|#reader)" reader-directive.txt)") \
+ <(racket $SCRIPT_DIR/extract_requires.rkt <(sed -r 's,(#reader.*)|(#lang.*),,;s,require "./provide-hack.rkt",require "../provide-hack.rkt",' $hw_name-instructor-solution.rkt)) \
+ <(racket $SCRIPT_DIR/extract_solution_defs.rkt <(sed -r 's/(#reader.*)|(#lang.*)//' $hw_name-instructor-solution.rkt) ) \
+ <(racket public-symbols.rkt) > _mutant_gen/$hw_name-instructor-solution.rkt
diff --git a/assignment_template/preprocessing/prepare_student_impl.bash b/assignment_template/preprocessing/prepare_student_impl.bash
new file mode 100644
index 0000000..6230068
--- /dev/null
+++ b/assignment_template/preprocessing/prepare_student_impl.bash
@@ -0,0 +1,22 @@
+# Processes a student-submitted racket file and prints to stdout a version
+# of that file containing the `require`s and `provide`s needed to evaluate the
+# implementation with the instructor test suite.
+
+# The name of the file that students should submit without a file extension.
+# e.g. hw1, hw2, etc.
+# This name will be interpolated into the file names of the instructor solution
+# (e.g. hw1-instructor-solution.rkt), student implementation (e.g. hw1.rkt), and
+# student test cases (e.g. hw1-tests.rkt).
+hw_name=$1
+# The reader or lang special form that should be used in this assignment.
+# When check-expects are extracted from the student implementation and written
+# to a file containing only test cases, this reader or lang special form will
+# be placed at the top of the file.
+reader_directive=$2
+
+
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+cat <(echo $reader_directive) \
+ <(racket $SCRIPT_DIR/extract_requires.rkt <(sed -r 's/(#reader.*)|(#lang.*)//' $hw_name-instructor-solution.rkt)) \
+ <(sed -r 's/(#reader.*)|(#lang.*)//' $hw_name.rkt) \
+ <(racket public-symbols.rkt)
diff --git a/assignment_template/template/Makefile b/assignment_template/template/Makefile
new file mode 100644
index 0000000..9fdae72
--- /dev/null
+++ b/assignment_template/template/Makefile
@@ -0,0 +1,38 @@
+# Run make sol=/path/to/hwX.rkt to use a different implementation file (e.g., from a student).
+sol := hwX.rkt
+
+all: test mutation
+
+# Yes, we want hwX hardcoded in the recipe below
+test: setup hwX-instructor-tests.rkt
+ cp hwX-instructor-tests.rkt _working_dir
+ cd _working_dir; \
+ bash prepare_student_impl.bash \
+ hwX "$$(grep -E "(#lang|#reader)" reader-directive.txt)" \
+ > hwX-temp.rkt; \
+ mv hwX-temp.rkt hwX.rkt; \
+ raco test hwX-instructor-tests.rkt
+
+mutation: setup
+ cp hwX-mutants.yml _working_dir
+ cd _working_dir; \
+ bash extract_student_tests.bash \
+ hwX "$$(grep -E "(#lang|#reader)" reader-directive.txt)" \
+ > hwX-tests.rkt; \
+ run-mutants --run_tests_in_one_batch hwX-mutants.yml; \
+ echo "HTML report available in hwX/_working_dir/mutants.html"
+
+setup:
+ mkdir -p _working_dir
+ cp *.rkt mutation_commands.sh preprocessing/* reader-directive.txt _working_dir
+ cp $(sol) _working_dir/hwX.rkt
+
+hwX-instructor-tests.rkt:
+ bash preprocessing/extract_instructor_tests.bash hwX \
+ "$$(grep -E "(#lang|#reader)" reader-directive.txt)" \
+ > hwX-instructor-tests.rkt
+
+clean:
+ rm -rf _working_dir
+
+.PHONY: all test mutation setup clean
diff --git a/assignment_template/template/extract_instructor_tests.bash b/assignment_template/template/extract_instructor_tests.bash
new file mode 100644
index 0000000..cd219df
--- /dev/null
+++ b/assignment_template/template/extract_instructor_tests.bash
@@ -0,0 +1,3 @@
+bash preprocessing/extract_instructor_tests.bash hwX \
+ "$(grep -E "(#lang|#reader)" reader-directive.txt)" \
+ > hwX-instructor-tests.rkt
diff --git a/assignment_template/template/generate_mutants.bash b/assignment_template/template/generate_mutants.bash
new file mode 100644
index 0000000..9226135
--- /dev/null
+++ b/assignment_template/template/generate_mutants.bash
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+bash preprocessing/prepare_mutants.bash hwX reader_directive.txt
+generate-mutants -p _mutant_gen _mutant_gen/hwX-instructor-solution.rkt hwX-mutants.yml
diff --git a/assignment_template/template/hwX-instructor-solution.rkt b/assignment_template/template/hwX-instructor-solution.rkt
new file mode 100644
index 0000000..9d28353
--- /dev/null
+++ b/assignment_template/template/hwX-instructor-solution.rkt
@@ -0,0 +1,32 @@
+#reader(lib "htdp-intermediate-lambda-reader.ss" "lang")((modname hw2) (read-case-sensitive #t) (teachpacks ()) (htdp-settings #(#t constructor repeating-decimal #f #t none #f () #f)))
+;; DO NOT REMOVE THIS IMPORT. It is needed for `provide` to be usable under
+;; racket language subsets.
+(require "./provide-hack.rkt")
+
+(check-expect (plus 1 2) 3)
+(check-expect (plus 3 4) (+ 3 4))
+(define (plus a b)
+ (+ a b)
+)
+
+(check-expect (minus 1 2) -1)
+(check-expect (minus 5 5) 0)
+(define (minus a b)
+ (- (helper a) b)
+)
+
+(define (helper a)
+ a
+)
+
+(define-struct thing [field1 field2])
+(define THING1 (make-thing "field1" #false))
+(check-expect (thing? THING1) #true)
+(check-expect (thing-field1 THING1) "field1")
+
+;; Replace this provide form with the code generated by running "racket public-symbols.rkt"
+(provide
+ plus
+ minus
+ (struct-out thing)
+)
diff --git a/assignment_template/template/hwX.rkt b/assignment_template/template/hwX.rkt
new file mode 100644
index 0000000..0f9afb5
--- /dev/null
+++ b/assignment_template/template/hwX.rkt
@@ -0,0 +1,23 @@
+;; Sample student implementation for template assignment. It's test cases
+;; are inadequate and should not receive a high mutation score.
+
+#reader(lib "htdp-intermediate-lambda-reader.ss" "lang")((modname hw2) (read-case-sensitive #t) (teachpacks ()) (htdp-settings #(#t constructor repeating-decimal #f #t none #f () #f)))
+
+(check-expect (plus 0 0) 0)
+(define (plus a b)
+ (+ a b)
+)
+
+(define (minus a b)
+ (- (helper a) b)
+)
+(check-expect (minus 0 0) 0)
+
+(define (helper a)
+ a
+)
+
+(define-struct thing [field1 field2])
+(define THING1 (make-thing "field1" #false))
+(check-expect (thing? THING1) #true)
+(check-expect (thing-field1 THING1) "field1")
diff --git a/assignment_template/template/mutation_commands.sh b/assignment_template/template/mutation_commands.sh
new file mode 100644
index 0000000..7d198de
--- /dev/null
+++ b/assignment_template/template/mutation_commands.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+set -e # Script will exit nonzero if any subcommands fail.
+
+subcmd=$1
+if [ $subcmd = "setup" ]; then
+ # Add any setup that needs to be run once before any other steps are taken.
+ true
+elif [ $subcmd = "discover_tests" ]; then
+ # Prints a newline-separated list of test case names.
+ grep "(test-case" hwX-tests.rkt | sed 's/(test-case//g' | sed 's/"//g'
+elif [ $subcmd = "run_test" ]; then
+ test_name=$2
+ # Runs the test called $test_name.
+ raco test ++arg -t ++arg "Test Suite" ++arg "$test_name" hwX-tests.rkt
+elif [ $subcmd = "run_test_batch" ]; then
+ args="raco test "
+ for test in "${@:2}"; do
+ args+="$arg ++arg -t ++arg 'Test Suite' ++arg '$test' "
+ done
+ args+="hwX-tests.rkt"
+ eval $args
+fi
diff --git a/assignment_template/template/provide-hack.rkt b/assignment_template/template/provide-hack.rkt
new file mode 100644
index 0000000..1a8747d
--- /dev/null
+++ b/assignment_template/template/provide-hack.rkt
@@ -0,0 +1,5 @@
+#lang racket
+
+;; This is needed for `provide` to be available when using racket sublanguages.
+
+(provide provide struct-out)
diff --git a/assignment_template/template/public-symbols.rkt b/assignment_template/template/public-symbols.rkt
new file mode 100644
index 0000000..ecb4bbe
--- /dev/null
+++ b/assignment_template/template/public-symbols.rkt
@@ -0,0 +1,48 @@
+#lang racket
+;; Populate the lists below with top-level symbols that students are required to
+;; define in their implementation. This is used to distinguish between required
+;; symbols and student defined helpers used in their test cases when extracting
+;; check-expects into test cases.
+;;
+;; NOTE: You will also need to `provide` these symbols in hwX-instructor-solution.rkt.
+;; The struct names should be passed to `struct-out`. For example:
+;; ```hwX-instructor-solution.rkt
+;; ...
+;; (provide
+;; plus
+;; minus
+;; (struct-out thing)
+;; )
+;; ```
+;;
+;; For convenience, you can generate the needed provide form by running this module:
+;; racket public-symbols.rkt
+;; Add the printed code to the bottom of hwX-instructor-solution.rkt
+
+;; Put function/constant names here
+(define public-def-symbols
+ (list
+ 'plus
+ 'minus
+ )
+)
+
+;; Put struct names here
+(define public-struct-symbols
+ (list
+ 'thing
+ )
+)
+
+;; DO NOT EDIT BELOW
+
+(provide public-def-symbols public-struct-symbols)
+
+(module+ main
+ (displayln
+ `(provide
+ ,@public-def-symbols
+ ,@(map (lambda (struct-symbol) `(struct-out ,struct-symbol)) public-struct-symbols)
+ )
+ )
+)
diff --git a/assignment_template/template/reader-directive.txt b/assignment_template/template/reader-directive.txt
new file mode 100644
index 0000000..1e288eb
--- /dev/null
+++ b/assignment_template/template/reader-directive.txt
@@ -0,0 +1,3 @@
+;; Replace the line below with the reader or lang special form needed for this assignment.
+;; Do not add anything else to this file.
+#lang htdp/bsl
diff --git a/assignment_template/template/test-runner.rkt b/assignment_template/template/test-runner.rkt
new file mode 100644
index 0000000..0457589
--- /dev/null
+++ b/assignment_template/template/test-runner.rkt
@@ -0,0 +1,27 @@
+#lang racket
+
+(require rackunit)
+(require al2-test-runner)
+
+(define tests-to-run (make-hash))
+
+(define (test-runner-cmd-line test-suite-wrapper)
+ (command-line #:program "Test Runner"
+ #:multi
+ [
+ ("-t" "--include-test") suite_name test_name "Name of test suite and test case to run"
+ (hash-set! tests-to-run suite_name
+ (append (hash-ref tests-to-run suite_name (list)) (list test_name))
+ )
+ ]
+ #:args ()
+ (let ((requested_tests (hash-map tests-to-run (lambda (key value) (cons key value)))))
+ (if (empty? requested_tests)
+ (run-tests test-suite-wrapper)
+ (run-tests #:only requested_tests test-suite-wrapper)
+ )
+ )
+ )
+)
+
+(provide test-runner-cmd-line)
diff --git a/lint.sh b/lint.sh
new file mode 100755
index 0000000..41f2b65
--- /dev/null
+++ b/lint.sh
@@ -0,0 +1,3 @@
+pycodestyle --exclude venv . \
+&& isort --check --diff --skip venv . \
+&& pyright
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..81b1278
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,32 @@
+[project]
+name = "racket_mutation_analysis"
+version = "0.0.1"
+authors = [
+ { name="James Perretta", email="perretta.j@northeastern.edu" },
+ { name="Bambi Zhuang", email="zhuang.ba@northeastern.edu" },
+]
+description = "Utilities for conducting mutation analysis on simple Racket programs."
+readme = "README.md"
+requires-python = ">=3.10"
+classifiers = [
+ "Programming Language :: Python :: 3",
+]
+dependencies = ["pyyaml", "jsonschema"]
+
+[tool.setuptools.packages.find]
+include = ["racket_mutation_analysis*"]
+
+[project.urls]
+
+[project.scripts]
+generate-mutants = "racket_mutation_analysis.racket_mutation.main:main"
+run-mutants = "racket_mutation_analysis.mutation_analysis.main:main"
+
+[project.optional-dependencies]
+dev = [
+ "isort",
+ "pyright",
+ "pycodestyle",
+
+ "build",
+]
diff --git a/racket_mutation_analysis/__init__.py b/racket_mutation_analysis/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/racket_mutation_analysis/mutation_analysis/__init__.py b/racket_mutation_analysis/mutation_analysis/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/hw-instructor-solution.rkt b/racket_mutation_analysis/mutation_analysis/e2e/hw-instructor-solution.rkt
new file mode 100644
index 0000000..123ae71
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/hw-instructor-solution.rkt
@@ -0,0 +1,7 @@
+#lang racket
+
+(define (add a b) (+ a b))
+
+; (check-expect (+ 1 2) 3)
+
+(provide (all-defined-out))
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/hw-student-tests.rkt b/racket_mutation_analysis/mutation_analysis/e2e/hw-student-tests.rkt
new file mode 100644
index 0000000..8b0c038
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/hw-student-tests.rkt
@@ -0,0 +1,38 @@
+#lang racket
+(require "./hw-instructor-solution.rkt")
+(require rackunit al2-test-runner)
+
+
+(define test-suite-wrapper
+ (test-suite "Test Suite"
+ (test-case "Regular test"
+ (check-equal? (add 1 2) 3)
+ )
+
+ (test-case "Regular test 2"
+ (check-equal? (add 0 2) 2)
+ )
+
+ (test-case "Regular test 3"
+ (check-equal? (add 2 0) 2)
+ )
+
+ (test-case "False positive test"
+ (check-equal? (add 1 2) 5)
+ )
+
+ (test-case "Timeout test"
+ (infloop1)
+ )
+ )
+)
+
+(define (infloop1) (infloop2))
+(define (infloop2) (infloop1))
+
+(command-line #:program "Test Runner" #:args (test_name)
+ (run-tests
+ #:only `(("Test Suite" ,test_name))
+ test-suite-wrapper
+ )
+)
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/mutants.yml b/racket_mutation_analysis/mutation_analysis/e2e/mutants.yml
new file mode 100644
index 0000000..2365fcb
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/mutants.yml
@@ -0,0 +1,74 @@
+hw-instructor-solution.rkt:
+ mutants:
+ - id: '1'
+ location:
+ end:
+ column: 20
+ line: 2
+ start:
+ column: 19
+ line: 2
+ mutated_code: '#lang racket
+
+
+ (define (add a b) (- a b))
+
+
+ ; (check-expect (+ 1 2) 3)
+
+
+ (provide (all-defined-out))
+
+ '
+ mutator_name: ArithmeticMutator
+ replacement: '-'
+ - id: '2'
+ location: &id001
+ end:
+ column: 25
+ line: 2
+ start:
+ column: 18
+ line: 2
+ mutated_code: '#lang racket
+
+
+ (define (add a b) a)
+
+
+ ; (check-expect (+ 1 2) 3)
+
+
+ (provide (all-defined-out))
+
+ '
+ mutator_name: ArithmeticDeletionMutator
+ replacement: a
+ - id: '3'
+ location: *id001
+ mutated_code: '#lang racket
+
+
+ (define (add a b) b)
+
+
+ ; (check-expect (+ 1 2) 3)
+
+
+ (provide (all-defined-out))
+
+ '
+ mutator_name: ArithmeticDeletionMutator
+ replacement: b
+ original: '#lang racket
+
+
+ (define (add a b) (+ a b))
+
+
+ ; (check-expect (+ 1 2) 3)
+
+
+ (provide (all-defined-out))
+
+ '
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/mutation_commands.sh b/racket_mutation_analysis/mutation_analysis/e2e/mutation_commands.sh
new file mode 100644
index 0000000..b3adc12
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/mutation_commands.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+set -e # Script will exit nonzero if any subcommands fail.
+
+subcmd=$1
+if [ $subcmd = "setup" ]; then
+ # Add any setup that needs to be run once before any other steps are taken.
+ echo "Hello"
+elif [ $subcmd = "discover_tests" ]; then
+ # Add a command that prints a newline-separated list of test case names.
+ echo "Regular test"
+ echo "Regular test 2"
+ echo "Regular test 3"
+ echo "False positive test"
+ echo "Timeout test"
+elif [ $subcmd = "run_test" ]; then
+ test_name=$2
+ # Add a command that runs the test called $test_name.
+ raco test ++arg "$test_name" hw-student-tests.rkt
+fi
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/run_e2e_test.sh b/racket_mutation_analysis/mutation_analysis/e2e/run_e2e_test.sh
new file mode 100755
index 0000000..17a3cd4
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/run_e2e_test.sh
@@ -0,0 +1,13 @@
+test_name=$1
+expected_status=$2
+
+echo "========== Running test $test_name ==========="
+
+bash -c "run-mutants mutants.yml -p . --timeout 3 $(cat $test_name.args) &> $test_name.out"
+status="$?"
+
+diff <(sed -r 's/[0-9][0-9]?\.[0-9][0-9]? ms//g' $test_name.out.correct) <(sed -r 's/[0-9][0-9]?\.[0-9][0-9]? ms//g' $test_name.out) || exit 1
+if [ $status != $expected_status ]; then
+ echo "Expected exit status to be $expected_status, but it was $status"
+ exit 1
+fi
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/all_tests_all_mutants.args b/racket_mutation_analysis/mutation_analysis/e2e/tests/all_tests_all_mutants.args
new file mode 100644
index 0000000..e69de29
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/all_tests_all_mutants.out.correct b/racket_mutation_analysis/mutation_analysis/e2e/tests/all_tests_all_mutants.out.correct
new file mode 100644
index 0000000..8ea7726
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/all_tests_all_mutants.out.correct
@@ -0,0 +1,299 @@
+=================== Running setup ===================
+---stdout---
+Hello
+
+=================== Running test case discovery ===================
+---stdout---
+Regular test
+Regular test 2
+Regular test 3
+False positive test
+Timeout test
+
+=================== Checking for false positives ===================
+---- Checking test case "Regular test" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test"
+
+*** Testsuite Test Suite
+ Regular test: ok (0.05 ms)
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Checking test case "Regular test 2" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: ok (0.05 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Checking test case "Regular test 3" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 3"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: ok (0.05 ms)
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Checking test case "False positive test" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "False positive test"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: fail (0.07 ms)
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.07 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+False positive test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:21:12
+actual: 3
+expected: 5
+--------------------
+1/1 test failures
+
+*** FALSE POSITIVE found in test case "False positive test" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'False positive test']' returned non-zero exit status 1.
+---- Checking test case "Timeout test" ----
+---stdout---
+b'raco test: "hw-student-tests.rkt" "Timeout test"\n\n*** Testsuite Test Suite\n\tRegular test: skipped\n\tRegular test 2: skipped\n\tRegular test 3: skipped\n\tFalse positive test: skipped\n\tTimeout test: '
+*** FALSE POSITIVE found in test case "Timeout test" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Timeout test']' timed out after 3 seconds
+=================== Running remaining tests against mutants ===================
+---- Mutant: "1", Test case: "Regular test" ----
+*** Test case "Regular test" DETECTED mutant "1" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test"
+
+*** Testsuite Test Suite
+ Regular test: fail (0.08 ms)
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.08 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:9:12
+actual: -1
+expected: 3
+--------------------
+1/1 test failures
+
+---- Mutant: "1", Test case: "Regular test 2" ----
+*** Test case "Regular test 2" DETECTED mutant "1" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test 2']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: fail (0.07 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.07 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test 2
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:13:12
+actual: -2
+expected: 2
+--------------------
+1/1 test failures
+
+---- Mutant: "1", Test case: "Regular test 3" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 3"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: ok (0.05 ms)
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Mutant: "2", Test case: "Regular test" ----
+*** Test case "Regular test" DETECTED mutant "2" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test"
+
+*** Testsuite Test Suite
+ Regular test: fail (0.08 ms)
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.08 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:9:12
+actual: 1
+expected: 3
+--------------------
+1/1 test failures
+
+---- Mutant: "2", Test case: "Regular test 2" ----
+*** Test case "Regular test 2" DETECTED mutant "2" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test 2']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: fail (0.08 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.08 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test 2
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:13:12
+actual: 0
+expected: 2
+--------------------
+1/1 test failures
+
+---- Mutant: "2", Test case: "Regular test 3" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 3"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: ok (0.05 ms)
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Mutant: "3", Test case: "Regular test" ----
+*** Test case "Regular test" DETECTED mutant "3" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test"
+
+*** Testsuite Test Suite
+ Regular test: fail (0.08 ms)
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.08 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:9:12
+actual: 2
+expected: 3
+--------------------
+1/1 test failures
+
+---- Mutant: "3", Test case: "Regular test 2" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: ok (0.04 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.04 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Mutant: "3", Test case: "Regular test 3" ----
+*** Test case "Regular test 3" DETECTED mutant "3" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test 3']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 3"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: fail (0.07 ms)
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.07 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test 3
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:17:12
+actual: 0
+expected: 2
+--------------------
+1/1 test failures
+
+
+========================== SUMMARY ========================
+Mutant 1: Killed
+ Detected by: Regular test, Regular test 2
+Mutant 2: Killed
+ Detected by: Regular test, Regular test 2
+Mutant 3: Killed
+ Detected by: Regular test, Regular test 3
+
+Total mutants: 3
+# Detected: 3
+# Undetected: 0
+# Skipped: 0
+Mutation score: 100.00%
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/exclude_test_all_mutants.args b/racket_mutation_analysis/mutation_analysis/e2e/tests/exclude_test_all_mutants.args
new file mode 100644
index 0000000..e8ee1a4
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/exclude_test_all_mutants.args
@@ -0,0 +1 @@
+--et "Regular test 2"
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/exclude_test_all_mutants.out.correct b/racket_mutation_analysis/mutation_analysis/e2e/tests/exclude_test_all_mutants.out.correct
new file mode 100644
index 0000000..7b549a2
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/exclude_test_all_mutants.out.correct
@@ -0,0 +1,219 @@
+=================== Running setup ===================
+---stdout---
+Hello
+
+=================== Running test case discovery ===================
+---stdout---
+Regular test
+Regular test 2
+Regular test 3
+False positive test
+Timeout test
+
+=================== Checking for false positives ===================
+---- Checking test case "Regular test" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test"
+
+*** Testsuite Test Suite
+ Regular test: ok (0.05 ms)
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Checking test case "Regular test 3" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 3"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: ok (0.05 ms)
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Checking test case "False positive test" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "False positive test"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: fail (0.07 ms)
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.07 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+False positive test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:21:12
+actual: 3
+expected: 5
+--------------------
+1/1 test failures
+
+*** FALSE POSITIVE found in test case "False positive test" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'False positive test']' returned non-zero exit status 1.
+---- Checking test case "Timeout test" ----
+---stdout---
+b'raco test: "hw-student-tests.rkt" "Timeout test"\n\n*** Testsuite Test Suite\n\tRegular test: skipped\n\tRegular test 2: skipped\n\tRegular test 3: skipped\n\tFalse positive test: skipped\n\tTimeout test: '
+*** FALSE POSITIVE found in test case "Timeout test" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Timeout test']' timed out after 3 seconds
+=================== Running remaining tests against mutants ===================
+---- Mutant: "1", Test case: "Regular test" ----
+*** Test case "Regular test" DETECTED mutant "1" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test"
+
+*** Testsuite Test Suite
+ Regular test: fail (0.08 ms)
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.08 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:9:12
+actual: -1
+expected: 3
+--------------------
+1/1 test failures
+
+---- Mutant: "1", Test case: "Regular test 3" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 3"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: ok (0.05 ms)
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Mutant: "2", Test case: "Regular test" ----
+*** Test case "Regular test" DETECTED mutant "2" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test"
+
+*** Testsuite Test Suite
+ Regular test: fail (0.08 ms)
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.08 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:9:12
+actual: 1
+expected: 3
+--------------------
+1/1 test failures
+
+---- Mutant: "2", Test case: "Regular test 3" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 3"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: ok (0.05 ms)
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Mutant: "3", Test case: "Regular test" ----
+*** Test case "Regular test" DETECTED mutant "3" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test"
+
+*** Testsuite Test Suite
+ Regular test: fail (0.08 ms)
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.08 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:9:12
+actual: 2
+expected: 3
+--------------------
+1/1 test failures
+
+---- Mutant: "3", Test case: "Regular test 3" ----
+*** Test case "Regular test 3" DETECTED mutant "3" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test 3']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 3"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: fail (0.08 ms)
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.08 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test 3
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:17:12
+actual: 0
+expected: 2
+--------------------
+1/1 test failures
+
+
+========================== SUMMARY ========================
+Mutant 1: Killed
+ Detected by: Regular test
+Mutant 2: Killed
+ Detected by: Regular test
+Mutant 3: Killed
+ Detected by: Regular test, Regular test 3
+
+Total mutants: 3
+# Detected: 3
+# Undetected: 0
+# Skipped: 0
+Mutation score: 100.00%
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_all_mutants.args b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_all_mutants.args
new file mode 100644
index 0000000..73a4083
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_all_mutants.args
@@ -0,0 +1 @@
+--it "Regular test 2"
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_all_mutants.out.correct b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_all_mutants.out.correct
new file mode 100644
index 0000000..47ba05e
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_all_mutants.out.correct
@@ -0,0 +1,107 @@
+=================== Running setup ===================
+---stdout---
+Hello
+
+=================== Running test case discovery ===================
+---stdout---
+Regular test
+Regular test 2
+Regular test 3
+False positive test
+Timeout test
+
+=================== Checking for false positives ===================
+---- Checking test case "Regular test 2" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: ok (0.05 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+=================== Running remaining tests against mutants ===================
+---- Mutant: "1", Test case: "Regular test 2" ----
+*** Test case "Regular test 2" DETECTED mutant "1" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test 2']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: fail (0.07 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.07 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test 2
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:13:12
+actual: -2
+expected: 2
+--------------------
+1/1 test failures
+
+---- Mutant: "2", Test case: "Regular test 2" ----
+*** Test case "Regular test 2" DETECTED mutant "2" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test 2']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: fail (0.08 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.08 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test 2
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:13:12
+actual: 0
+expected: 2
+--------------------
+1/1 test failures
+
+---- Mutant: "3", Test case: "Regular test 2" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: ok (0.05 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+
+========================== SUMMARY ========================
+Mutant 1: Killed
+ Detected by: Regular test 2
+Mutant 2: Killed
+ Detected by: Regular test 2
+Mutant 3: Survived
+
+Total mutants: 3
+# Detected: 2
+# Undetected: 1
+# Skipped: 0
+Mutation score: 66.67%
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_exclude_mutants.args b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_exclude_mutants.args
new file mode 100644
index 0000000..4f176cd
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_exclude_mutants.args
@@ -0,0 +1 @@
+--it "Regular test 2" --em 2
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_exclude_mutants.out.correct b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_exclude_mutants.out.correct
new file mode 100644
index 0000000..0721eb3
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_exclude_mutants.out.correct
@@ -0,0 +1,80 @@
+=================== Running setup ===================
+---stdout---
+Hello
+
+=================== Running test case discovery ===================
+---stdout---
+Regular test
+Regular test 2
+Regular test 3
+False positive test
+Timeout test
+
+=================== Checking for false positives ===================
+---- Checking test case "Regular test 2" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: ok (0.05 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+=================== Running remaining tests against mutants ===================
+---- Mutant: "1", Test case: "Regular test 2" ----
+*** Test case "Regular test 2" DETECTED mutant "1" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test 2']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: fail (0.08 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.08 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test 2
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:13:12
+actual: -2
+expected: 2
+--------------------
+1/1 test failures
+
+---- Mutant: "3", Test case: "Regular test 2" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: ok (0.05 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+
+========================== SUMMARY ========================
+Mutant 1: Killed
+ Detected by: Regular test 2
+Mutant 2: Ignored
+Mutant 3: Survived
+
+Total mutants: 3
+# Detected: 1
+# Undetected: 1
+# Skipped: 1
+Mutation score: 50.00%
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants.args b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants.args
new file mode 100644
index 0000000..bc7901b
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants.args
@@ -0,0 +1 @@
+--it "Regular test 2" --im 3
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants.out.correct b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants.out.correct
new file mode 100644
index 0000000..4b00d3d
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants.out.correct
@@ -0,0 +1,53 @@
+=================== Running setup ===================
+---stdout---
+Hello
+
+=================== Running test case discovery ===================
+---stdout---
+Regular test
+Regular test 2
+Regular test 3
+False positive test
+Timeout test
+
+=================== Checking for false positives ===================
+---- Checking test case "Regular test 2" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: ok (0.08 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.08 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+=================== Running remaining tests against mutants ===================
+---- Mutant: "3", Test case: "Regular test 2" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: ok (0.05 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+
+========================== SUMMARY ========================
+Mutant 1: Ignored
+Mutant 2: Ignored
+Mutant 3: Survived
+
+Total mutants: 3
+# Detected: 0
+# Undetected: 1
+# Skipped: 2
+Mutation score: 0.00%
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants2.args b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants2.args
new file mode 100644
index 0000000..9b79a6d
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants2.args
@@ -0,0 +1 @@
+--it "Regular test 2" --im 2
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants2.out.correct b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants2.out.correct
new file mode 100644
index 0000000..f4ef0f5
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/include_test_include_mutants2.out.correct
@@ -0,0 +1,66 @@
+=================== Running setup ===================
+---stdout---
+Hello
+
+=================== Running test case discovery ===================
+---stdout---
+Regular test
+Regular test 2
+Regular test 3
+False positive test
+Timeout test
+
+=================== Checking for false positives ===================
+---- Checking test case "Regular test 2" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: ok (0.05 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+=================== Running remaining tests against mutants ===================
+---- Mutant: "2", Test case: "Regular test 2" ----
+*** Test case "Regular test 2" DETECTED mutant "2" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Regular test 2']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: fail (0.07 ms)
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.07 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+Regular test 2
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:13:12
+actual: 0
+expected: 2
+--------------------
+1/1 test failures
+
+
+========================== SUMMARY ========================
+Mutant 1: Ignored
+Mutant 2: Killed
+ Detected by: Regular test 2
+Mutant 3: Ignored
+
+Total mutants: 3
+# Detected: 1
+# Undetected: 0
+# Skipped: 2
+Mutation score: 100.00%
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/skip_false_positive_check.args b/racket_mutation_analysis/mutation_analysis/e2e/tests/skip_false_positive_check.args
new file mode 100644
index 0000000..48b8b0b
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/skip_false_positive_check.args
@@ -0,0 +1 @@
+--it "False positive test" --skip_false_positive_check
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/skip_false_positive_check.out.correct b/racket_mutation_analysis/mutation_analysis/e2e/tests/skip_false_positive_check.out.correct
new file mode 100644
index 0000000..0f9beda
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/skip_false_positive_check.out.correct
@@ -0,0 +1,105 @@
+=================== Running setup ===================
+---stdout---
+Hello
+
+=================== Running test case discovery ===================
+---stdout---
+Regular test
+Regular test 2
+Regular test 3
+False positive test
+Timeout test
+
+=================== Running remaining tests against mutants ===================
+---- Mutant: "1", Test case: "False positive test" ----
+*** Test case "False positive test" DETECTED mutant "1" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'False positive test']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "False positive test"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: fail (0.1 ms)
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.1 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+False positive test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:21:12
+actual: -1
+expected: 5
+--------------------
+1/1 test failures
+
+---- Mutant: "2", Test case: "False positive test" ----
+*** Test case "False positive test" DETECTED mutant "2" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'False positive test']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "False positive test"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: fail (0.07 ms)
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.07 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+False positive test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:21:12
+actual: 1
+expected: 5
+--------------------
+1/1 test failures
+
+---- Mutant: "3", Test case: "False positive test" ----
+*** Test case "False positive test" DETECTED mutant "3" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'False positive test']' returned non-zero exit status 1.
+---stdout---
+raco test: "hw-student-tests.rkt" "False positive test"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: fail (0.07 ms)
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.07 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+False positive test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:21:12
+actual: 2
+expected: 5
+--------------------
+1/1 test failures
+
+
+========================== SUMMARY ========================
+Mutant 1: Killed
+ Detected by: False positive test
+Mutant 2: Killed
+ Detected by: False positive test
+Mutant 3: Killed
+ Detected by: False positive test
+
+Total mutants: 3
+# Detected: 3
+# Undetected: 0
+# Skipped: 0
+Mutation score: 100.00%
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_0_exit_status.args b/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_0_exit_status.args
new file mode 100644
index 0000000..3dfc468
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_0_exit_status.args
@@ -0,0 +1 @@
+--it "Regular test" --stop_after_false_positive_check
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_0_exit_status.out.correct b/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_0_exit_status.out.correct
new file mode 100644
index 0000000..43e0ca5
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_0_exit_status.out.correct
@@ -0,0 +1,28 @@
+=================== Running setup ===================
+---stdout---
+Hello
+
+=================== Running test case discovery ===================
+---stdout---
+Regular test
+Regular test 2
+Regular test 3
+False positive test
+Timeout test
+
+=================== Checking for false positives ===================
+---- Checking test case "Regular test" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test"
+
+*** Testsuite Test Suite
+ Regular test: ok ()
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+No false positives detected
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_1_exit_status.args b/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_1_exit_status.args
new file mode 100644
index 0000000..be8a5d0
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_1_exit_status.args
@@ -0,0 +1 @@
+--stop_after_false_positive_check
diff --git a/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_1_exit_status.out.correct b/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_1_exit_status.out.correct
new file mode 100644
index 0000000..a6c541d
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/e2e/tests/stop_after_false_positive_check_1_exit_status.out.correct
@@ -0,0 +1,89 @@
+=================== Running setup ===================
+---stdout---
+Hello
+
+=================== Running test case discovery ===================
+---stdout---
+Regular test
+Regular test 2
+Regular test 3
+False positive test
+Timeout test
+
+=================== Checking for false positives ===================
+---- Checking test case "Regular test" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test"
+
+*** Testsuite Test Suite
+ Regular test: ok ()
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Checking test case "Regular test 2" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 2"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: ok ()
+ Regular test 3: skipped
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Checking test case "Regular test 3" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "Regular test 3"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: ok ()
+ False positive test: skipped
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 0 ; errors: 0 ; skipped: 4
+1 test passed
+
+---- Checking test case "False positive test" ----
+---stdout---
+raco test: "hw-student-tests.rkt" "False positive test"
+
+*** Testsuite Test Suite
+ Regular test: skipped
+ Regular test 2: skipped
+ Regular test 3: skipped
+ False positive test: fail ()
+ Timeout test: skipped
+*** Testsuite Test Suite completed in 0.05 ms
+*** Total tests: 5 ; failures: 1 ; errors: 0 ; skipped: 4
+
+---stderr---
+--------------------
+False positive test
+FAILURE
+name: check-equal?
+location: hw-student-tests.rkt:21:12
+actual: 3
+expected: 5
+--------------------
+1/1 test failures
+
+*** FALSE POSITIVE found in test case "False positive test" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'False positive test']' returned non-zero exit status 1.
+---- Checking test case "Timeout test" ----
+---stdout---
+b'raco test: "hw-student-tests.rkt" "Timeout test"\n\n*** Testsuite Test Suite\n\tRegular test: skipped\n\tRegular test 2: skipped\n\tRegular test 3: skipped\n\tFalse positive test: skipped\n\tTimeout test: '
+*** FALSE POSITIVE found in test case "Timeout test" ***
+Command '['bash', 'mutation_commands.sh', 'run_test', 'Timeout test']' timed out after 3 seconds
+False positives found in tests:
+False positive test
+ Timeout test
diff --git a/racket_mutation_analysis/mutation_analysis/main.py b/racket_mutation_analysis/mutation_analysis/main.py
new file mode 100644
index 0000000..a5b19d1
--- /dev/null
+++ b/racket_mutation_analysis/mutation_analysis/main.py
@@ -0,0 +1,512 @@
+import argparse
+import json
+import os
+import shutil
+import subprocess
+import uuid
+from collections import Counter
+from collections.abc import Collection, Sequence
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import TypeAlias
+
+import yaml
+
+from racket_mutation_analysis.racket_mutation.schema import (
+ FileResultDictionary, GeneratedMutants, Location, Mutant, MutantResult, MutantStatus,
+ MutationTestResult, Position
+)
+
+
+class StopAfterFalsePositivesCheck(Exception):
+ def __init__(self, false_positives: Sequence[str]):
+ self.false_positives = false_positives
+
+
+def main():
+ args = parse_args()
+ if args.init:
+ with open('mutation_commands.sh', 'w') as f:
+ f.write(MUTATION_CMD_SKELETON)
+ exit(0)
+
+ with open(args.mutants_file) as f:
+ generated_mutants: GeneratedMutants = yaml.load(f, yaml.Loader)
+
+ working_dir = f'mutation_working_dir_{uuid.uuid4().hex}'
+ shutil.copytree(
+ args.project_root,
+ working_dir,
+ ignore=shutil.ignore_patterns('mutation_working_dir_*', *args.ignore_pattern)
+ )
+
+ try:
+ with ChangeDirectory(working_dir):
+ runner = MutantRunner(
+ generated_mutants,
+ timeout=None if args.timeout < 0 else args.timeout,
+ stop_after_false_positive_check=args.stop_after_false_positive_check,
+ skip_false_positive_check=args.skip_false_positive_check,
+ run_tests_in_one_batch=args.run_tests_in_one_batch,
+ failfast=args.failfast,
+ include_tests=[] if args.include_test is None else args.include_test,
+ exclude_tests=[] if args.exclude_test is None else args.exclude_test,
+ include_mutants=[] if args.include_mutant is None else args.include_mutant,
+ exclude_mutants=[] if args.exclude_mutant is None else args.exclude_mutant,
+ )
+ file_results = runner.run()
+ except StopAfterFalsePositivesCheck as e:
+ if not args.keep_working_dir:
+ shutil.rmtree(working_dir)
+
+ if e.false_positives:
+ print('False positives found in tests:')
+ print('\n\t'.join(e.false_positives))
+ exit(1)
+ else:
+ print('No false positives detected')
+ exit(0)
+
+ assert isinstance(args.threshold, int)
+ mutation_analysis_result: MutationTestResult = {
+ 'schemaVersion': '1',
+ 'thresholds': {'high': args.threshold, 'low': args.threshold},
+ 'projectRoot': str(Path(args.project_root).absolute()),
+ 'files': file_results,
+ }
+ mutation_score = make_reports(mutation_analysis_result)
+
+ if not args.keep_working_dir:
+ shutil.rmtree(working_dir)
+
+ if mutation_score < args.threshold:
+ exit(1)
+
+
+def parse_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('mutants_file', help='A YAML file that contains mutant definitions.')
+
+ parser.add_argument(
+ '--init', action='store_true',
+ help=f'When specified, creates a file called {MUTATION_CMD_SCRIPT_NAME} that will '
+ 'be run at different stages of mutation analysis and then exits. '
+ 'The user should add appropriate commands for each stage of mutation '
+ 'analysis to this file.')
+
+ parser.add_argument('--project_root', '-p', default='.')
+ parser.add_argument(
+ '--ignore_pattern', '-i', action='append', default=[],
+ help='Glob-style patterns of files within project root that should NOT '
+ 'be copied into the temporary working directory.')
+
+ parser.add_argument(
+ '--timeout', '-t', default=10, type=int,
+ help='Time limit in seconds for commands (e.g., checking a test for false positives, '
+ 'running a test case against a mutant). Set to -1 for no time limit. '
+ 'Note that no time limit is placed on the setup step.'
+ )
+
+ false_positive_phase_args = parser.add_mutually_exclusive_group()
+ false_positive_phase_args.add_argument(
+ '--stop_after_false_positive_check', '--stop_afp', action='store_true', default=False,
+ help='When specified, mutation analysis will stop after checking for false positives. '
+ 'If any false positives were detected, the program will exit nonzero.'
+ )
+ false_positive_phase_args.add_argument(
+ '--skip_false_positive_check', '--sfp', action='store_true', default=False,
+ help='When specified, test cases will not be checked for false positives '
+ 'before being run against mutants.'
+ )
+
+ test_running_args = parser.add_mutually_exclusive_group()
+ test_running_args.add_argument(
+ '--run_tests_in_one_batch', '-b', action='store_true', default=False,
+ help='When specified, all valid tests will be run against a mutant in a '
+ 'single command instead of one at a time.'
+ )
+ test_running_args.add_argument(
+ '--failfast', '-f', action='store_true', default=False,
+ help='When specified, mutants will be marked as detected as soon as the first '
+ 'case fails against it, and no more tests will be run against that mutant.'
+ )
+ parser.add_argument(
+ '--keep_working_dir', '-k', action='store_true', default=False,
+ help='When specified, the temporary working directory will not be deleted '
+ 'after mutation analysis finishes.'
+ )
+
+ parser.add_argument(
+ '--include_test', '--it', action='append',
+ help='Run only the test case(s) listed here '
+ '(this argument can be specified multiple times).'
+ )
+ parser.add_argument(
+ '--exclude_test', '--et', action='append',
+ help='Do not run the test case(s) listed here '
+ '(this argument can be specified multiple times).'
+ )
+
+ parser.add_argument(
+ '--include_mutant', '--im', action='append',
+ help='Run only the mutant(s) listed here '
+ '(this argument can be specified multiple times).'
+ )
+ parser.add_argument(
+ '--exclude_mutant', '--em', action='append',
+ help='Do not run the mutant(s) listed here '
+ '(this argument can be specified multiple times).'
+ )
+
+ parser.add_argument(
+ '--threshold', '--th', default=100, type=int,
+ help='Exit nonzero if the mutation score is lower than this percentage.'
+ )
+
+ return parser.parse_args()
+
+
+def make_reports(result: MutationTestResult) -> float: # Return the mutation score %
+ print('\n========================== SUMMARY ========================')
+ status_counts = Counter()
+ for filename, file_result in result['files'].items():
+ for mutant_result in file_result['mutants']:
+ status_counts.update([mutant_result['status']])
+ print(f'Mutant {mutant_result["id"]}: {mutant_result["status"]}')
+ if mutant_result['status'] == 'Killed':
+ print('\tDetected by:', ', '.join(mutant_result['killedBy']))
+
+ num_mutants = sum(status_counts.values())
+ num_detected = status_counts["Killed"] + status_counts["Timeout"]
+ print()
+ print(f'Total mutants: {num_mutants}')
+ print(f'# Detected: {num_detected}')
+ print(f'# Undetected: {status_counts["Survived"]}')
+ print(f'# Skipped: {status_counts["Ignored"]}')
+ mutation_score = (num_detected / (num_mutants - status_counts['Ignored'])) * 100
+ print(f'Mutation score: {mutation_score:.2f}%')
+
+ json_str = json.dumps(result, indent=2)
+ with open('mutants.json', 'w') as f:
+ f.write(json_str)
+
+ with open('mutants.html', 'w') as f:
+ f.write(f'''
+
+
+
+
+
+ Document
+
+
+
+
+ Your browser does not support custom elements. Please use a modern browser.
+
+
+
+
+''')
+
+ return mutation_score
+
+
+class MutantRunner:
+ def __init__(
+ self,
+ generated_mutants: GeneratedMutants,
+ *,
+ timeout: int | None,
+ skip_false_positive_check: bool,
+ stop_after_false_positive_check: bool,
+ run_tests_in_one_batch: bool,
+ failfast: bool,
+ include_tests: Collection[str],
+ exclude_tests: Collection[str],
+ include_mutants: Collection[str],
+ exclude_mutants: Collection[str],
+ ):
+ self.generated_mutants = generated_mutants
+ self.timeout = timeout
+ self.skip_false_positive_check = skip_false_positive_check
+ self.stop_after_false_positive_check = stop_after_false_positive_check
+ self.run_tests_in_one_batch = run_tests_in_one_batch
+ self.failfast = failfast
+ self.include_tests = set(include_tests)
+ self.exclude_tests = set(exclude_tests)
+ self.include_mutants = set(include_mutants)
+ self.exclude_mutants = set(exclude_mutants)
+
+ def run(self):
+ self._setup()
+ test_names = self._filter_test_names(self._discover_test_case_names())
+ if self.skip_false_positive_check:
+ false_positives: list[str] = []
+ else:
+ false_positives = self._false_positives_check(test_names)
+
+ if self.stop_after_false_positive_check:
+ raise StopAfterFalsePositivesCheck(false_positives)
+
+ return self._run_tests_with_mutants(
+ [name for name in test_names if name not in false_positives]
+ )
+
+ def _setup(self):
+ print('=================== Running setup ===================', flush=True)
+ result = subprocess.run(
+ ['bash', MUTATION_CMD_SCRIPT_NAME, 'setup'],
+ capture_output=True, errors='surrogateescape'
+ )
+ print_subprocess_output(result)
+
+ if result.returncode != 0:
+ print(f'The setup command exited with status {result.returncode}. Exiting.')
+ exit(1)
+
+ def _discover_test_case_names(self):
+ print('=================== Running test case discovery ===================', flush=True)
+ try:
+ result = subprocess.run(
+ ['bash', MUTATION_CMD_SCRIPT_NAME, 'discover_tests'],
+ timeout=self.timeout,
+ capture_output=True,
+ errors='surrogateescape',
+ check=True,
+ )
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError) as e:
+ print_subprocess_output(e)
+ print('Error in test case discovery.', str(e), 'Exiting.')
+ exit(1)
+
+ print_subprocess_output(result)
+ return [name.strip() for name in result.stdout.splitlines()]
+
+ def _filter_test_names(self, test_names: Sequence[str]) -> Sequence[str]:
+ if self.include_tests:
+ test_names = [name for name in test_names if name in self.include_tests]
+ if self.exclude_tests:
+ test_names = [name for name in test_names if name not in self.exclude_tests]
+
+ return test_names
+
+ def _false_positives_check(self, test_names: Sequence[str]):
+ print('=================== Checking for false positives ===================', flush=True)
+ false_positives: list[str] = []
+ for test_name in test_names:
+ print(f'---- Checking test case "{test_name}" ----')
+ try:
+ result = subprocess.run(
+ ['bash', MUTATION_CMD_SCRIPT_NAME, 'run_test', test_name],
+ check=True,
+ timeout=self.timeout,
+ capture_output=True,
+ errors='surrogateescape'
+ )
+ print_subprocess_output(result)
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError) as e:
+ print_subprocess_output(e)
+ print(f'*** FALSE POSITIVE found in test case "{test_name}" ***')
+ print(str(e), flush=True)
+ false_positives.append(test_name)
+
+ return false_positives
+
+ def _run_tests_with_mutants(self, test_names: Sequence[str]):
+ print('=================== Running remaining tests against mutants ===================',
+ flush=True)
+ file_results: dict[str, FileResultDictionary] = {}
+ for filename, mutations in self.generated_mutants.items():
+ mutant_results: list[MutantResult] = []
+ for mutant in mutations['mutants']:
+ if (mutant['id'] in self.exclude_mutants
+ or self.include_mutants and mutant['id'] not in self.include_mutants):
+ mutant_results.append({
+ 'id': mutant['id'],
+ 'location': location_to_one_index(mutant['location']),
+ 'mutatorName': mutant['mutator_name'],
+ 'replacement': mutant['replacement'],
+ 'status': 'Ignored',
+ 'killedBy': [],
+ })
+ continue
+
+ with InjectMutant(
+ filename, mutant=mutant['mutated_code'], original=mutations['original']
+ ):
+ if self.run_tests_in_one_batch:
+ result_info = self._run_tests_in_batch(mutant, test_names)
+
+ else:
+ result_info = self._run_tests_one_by_one(mutant, test_names)
+
+ mutant_results.append({
+ 'id': mutant['id'],
+ 'location': location_to_one_index(mutant['location']),
+ 'mutatorName': mutant['mutator_name'],
+ 'replacement': mutant['replacement'],
+ 'status': result_info.status,
+ 'killedBy': result_info.detected_by_tests,
+ })
+
+ file_results[filename] = {
+ 'language': 'racket',
+ 'source': mutations['original'],
+ 'mutants': mutant_results
+ }
+
+ return file_results
+
+ def _run_tests_one_by_one(self, mutant: Mutant, test_names: Sequence[str]):
+ detected_by_tests: list[str] = []
+ timed_out: bool | None = None
+ for test_name in test_names:
+ print(f'---- Mutant: "{mutant["id"]}", Test case: "{test_name}" ----',
+ flush=True)
+ try:
+ result = subprocess.run(
+ ['bash', MUTATION_CMD_SCRIPT_NAME, 'run_test', test_name],
+ check=True,
+ timeout=self.timeout,
+ capture_output=True,
+ errors='surrogateescape'
+ )
+ print_subprocess_output(result)
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError) as e:
+ print(f'*** Test case "{test_name}" '
+ f'DETECTED mutant "{mutant["id"]}" ***')
+ print(str(e))
+ print_subprocess_output(e)
+ detected_by_tests.append(test_name)
+
+ if timed_out is None:
+ timed_out = isinstance(e, subprocess.TimeoutExpired)
+
+ if self.failfast:
+ break
+
+ if timed_out:
+ status = 'Timeout'
+ elif detected_by_tests:
+ status = 'Killed'
+ else:
+ status = 'Survived'
+
+ return self._MutantDetectionInfo(status, detected_by_tests)
+
+ def _run_tests_in_batch(self, mutant: Mutant, test_names: Sequence[str]):
+ status: MutantStatus
+ print(f'---- Mutant: "{mutant["id"]}", All valid tests ----',
+ flush=True)
+ try:
+ result = subprocess.run(
+ ['bash', MUTATION_CMD_SCRIPT_NAME, 'run_test_batch', *test_names],
+ check=True,
+ timeout=self.timeout,
+ capture_output=True,
+ errors='surrogateescape'
+ )
+ print_subprocess_output(result)
+ status = 'Survived'
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError) as e:
+ print(f'*** Mutant "{mutant["id"]}" DETECTED ***')
+ print(str(e))
+ print_subprocess_output(e)
+
+ status = 'Timeout' if isinstance(e, subprocess.TimeoutExpired) else 'Killed'
+
+ return self._MutantDetectionInfo(status)
+
+ @dataclass
+ class _MutantDetectionInfo:
+ status: MutantStatus
+ detected_by_tests: list[str] = field(default_factory=list)
+
+
+SubprocessResult: TypeAlias = (
+ subprocess.CompletedProcess | subprocess.CalledProcessError | subprocess.TimeoutExpired
+)
+
+
+def print_subprocess_output(result: SubprocessResult):
+ if result.stdout:
+ print('---stdout---')
+ print(result.stdout, flush=True)
+
+ if result.stderr:
+ print('---stderr---')
+ print(result.stderr, flush=True)
+
+
+MUTATION_CMD_SCRIPT_NAME = 'mutation_commands.sh'
+MUTATION_CMD_SKELETON = '''#!/bin/bash
+set -e # Script will exit nonzero if any subcommands fail.
+
+subcmd=$1
+if [ $subcmd = "setup" ]; then
+ # Add any setup that needs to be run once before any other steps are taken.
+elif [ $subcmd = "discover_tests" ]; then
+ # Add a command that prints a newline-separated list of test case names.
+elif [ $subcmd = "run_test" ]; then
+ test_name=$2
+ # Add a command that runs the test called $test_name.
+fi
+'''
+
+
+class ChangeDirectory:
+ """
+ Enables moving into and out of a given directory using "with" statements.
+ """
+
+ def __init__(self, new_dir: str):
+ self._original_dir = os.getcwd()
+ self._new_dir = new_dir
+
+ def __enter__(self) -> None:
+ os.chdir(self._new_dir)
+
+ def __exit__(self, *args: object) -> None:
+ os.chdir(self._original_dir)
+
+
+class InjectMutant:
+ """
+ Context manager for replacing a file with a mutated version, then restoring the original file.
+ """
+
+ def __init__(self, filename: str, *, mutant: str, original: str):
+ self.filename = filename
+ self.original = original
+ self.mutant = mutant
+
+ def __enter__(self) -> None:
+ with open(self.filename, 'w') as f:
+ f.write(self.mutant)
+
+ def __exit__(self, *args: object) -> None:
+ with open(self.filename, 'w') as f:
+ f.write(self.original)
+
+
+def location_to_one_index(location: Location) -> Location:
+ return {
+ 'start': position_to_one_index(location['start']),
+ 'end': position_to_one_index(location['end']),
+ }
+
+
+def position_to_one_index(position: Position) -> Position:
+ return {
+ 'column': position['column'] + 1,
+ 'line': position['line'] + 1,
+ }
+
+
+if __name__ == '__main__':
+ main()
diff --git a/racket_mutation_analysis/racket_ast/__init__.py b/racket_mutation_analysis/racket_ast/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/racket_mutation_analysis/racket_ast/buffer.py b/racket_mutation_analysis/racket_ast/buffer.py
new file mode 100644
index 0000000..03a3d77
--- /dev/null
+++ b/racket_mutation_analysis/racket_ast/buffer.py
@@ -0,0 +1,141 @@
+# NOTE: This module has been modified, and the docstrings have not yet been
+# updated. We've added type annotations in places where we've modified
+# the behavior of functions.
+
+"""The buffer module assists in iterating through lines and tokens.
+
+Project UID 2d6261568f83a98aa474c0a2b04179ce000b9a48
+"""
+
+from __future__ import annotations
+
+import importlib
+import math
+from typing import Iterator, Sequence
+
+from .scheme_tokens import Token
+
+importlib.import_module('readline') # for input history
+
+
+class Buffer:
+ """Provides a way of accessing a sequence of tokens across lines.
+
+ The constructor takes an iterator, called "the source", that
+ returns the next line of tokens as a list each time it is queried,
+ or None to indicate the end of data.
+
+ The Buffer in effect concatenates the sequences returned from its
+ source and then supplies the items from them one at a time through
+ its pop() method, calling the source for more sequences of items
+ only when needed.
+
+ In addition, Buffer provides a current method to look at the next
+ item to be supplied, without sequencing past it.
+
+ The __str__ method prints all tokens read so far, up to the end of
+ the current line, and marks the current token with >>.
+
+ >>> buf = Buffer(iter([['(', '+'], [15], [12, ')']]))
+ >>> buf.pop()
+ '('
+ >>> buf.pop()
+ '+'
+ >>> buf.current()
+ 15
+ >>> print(buf)
+ 1: ( +
+ 2: >> 15
+ >>> buf.pop()
+ 15
+ >>> buf.current()
+ 12
+ >>> buf.pop()
+ 12
+ >>> print(buf)
+ 1: ( +
+ 2: 15
+ 3: 12 >> )
+ >>> buf.pop()
+ ')'
+ >>> print(buf)
+ 1: ( +
+ 2: 15
+ 3: 12 ) >>
+ >>> buf.pop() # returns None
+ """
+
+ def __init__(self, source: Iterator[Sequence[Token]]):
+ """Initialize this Buffer with the give source iterable."""
+ self.index = 0
+ self.lines = []
+ self.source = source
+ self.current_line: Sequence[Token] = ()
+ self.current()
+
+ def pop(self):
+ """Remove the next item from self and return it.
+
+ If self has exhausted its source, returns None.
+ """
+ current = self.current()
+ self.index += 1
+ return current
+
+ @property
+ def more_on_line(self):
+ """Return whether more data remains on the current line."""
+ return self.index < len(self.current_line)
+
+ def current(self):
+ """Return the current element, or None if none exists."""
+ while not self.more_on_line:
+ self.index = 0
+ try:
+ self.current_line = next(self.source)
+ self.lines.append(self.current_line)
+ except StopIteration:
+ self.current_line = ()
+ return None
+ return self.current_line[self.index]
+
+ def __str__(self):
+ """Return recently read contents.
+
+ The current element is marked with >>.
+ """
+ # Format string for right-justified line numbers
+ count = len(self.lines)
+ msg = '{0:>' + str(math.floor(math.log10(count)) + 1) + '}: '
+
+ # Up to three previous lines and current line are included in
+ # output
+ result = ''
+ for i in range(max(0, count - 4), count - 1):
+ result += (msg.format(i + 1)
+ + ' '.join(map(str, self.lines[i]))
+ + '\n')
+ result += msg.format(count)
+ result += ' '.join(map(str, self.current_line[:self.index]))
+ result += ' >> '
+ result += ' '.join(map(str, self.current_line[self.index:]))
+ return result.strip()
+
+
+def make_input_reader(prompt):
+ """Make an iterable over user input, with the given prompt."""
+ while True:
+ yield input(prompt)
+ prompt = ' ' * len(prompt)
+
+
+def make_line_reader(lines, prompt, comment=';'):
+ """Make an iterable that prints lines after a prompt."""
+ while lines:
+ line = lines.pop(0).strip('\n')
+ if (prompt is not None and line != ''
+ and not line.lstrip().startswith(comment)):
+ print(prompt + line)
+ prompt = ' ' * len(prompt)
+ yield line
+ raise EOFError
diff --git a/racket_mutation_analysis/racket_ast/scheme_reader.py b/racket_mutation_analysis/racket_ast/scheme_reader.py
new file mode 100644
index 0000000..2abf310
--- /dev/null
+++ b/racket_mutation_analysis/racket_ast/scheme_reader.py
@@ -0,0 +1,321 @@
+# NOTE: This module has been modified, and the docstrings have not yet been
+# updated. We've added type annotations in places where we've modified
+# the behavior of functions.
+
+"""This module implements a parser for Scheme expressions.
+
+Pairs and lists are defined in scheme_core.py, as well as a
+representation for an unspecified value. Other data types in Scheme
+are represented by their corresponding type in Python:
+ number: int or float
+ symbol: string
+ string: quoted string
+ boolean: bool
+
+Project UID 2d6261568f83a98aa474c0a2b04179ce000b9a48
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Final, Literal, Sequence, TextIO, Type, TypeAlias
+
+from .buffer import Buffer
+from .scheme_tokens import PUNCTUATORS, Location, Token, Tokenizer
+
+
+@dataclass
+class ASTNode:
+ start_loc: Location
+ end_loc: Location
+
+ @property
+ def children(self) -> Sequence[ASTNode]:
+ return []
+
+
+@dataclass
+class Program(ASTNode):
+ expressions: Sequence[Expression]
+ # Dict of {line #: comment token}
+ comments: dict[int, Token]
+
+ @property
+ def children(self) -> Sequence[ASTNode]:
+ return self.expressions
+
+
+@dataclass
+class Expression(ASTNode):
+ pass
+
+
+@dataclass
+class SList(Expression):
+ items: Sequence[Expression]
+
+ opening_bracket: str = '('
+ closing_bracket: str = ')'
+
+ @property
+ def is_nil(self) -> bool:
+ return len(self.items) == 0
+
+ @property
+ def children(self) -> Sequence[ASTNode]:
+ return self.items
+
+
+@dataclass
+class Pair(Expression):
+ first: Expression
+ second: Expression
+
+ @property
+ def children(self) -> Sequence[ASTNode]:
+ return (self.first, self.second)
+
+
+@dataclass
+class QuotedExpr(Expression):
+ expr: Expression
+
+ @property
+ def children(self) -> Sequence[ASTNode]:
+ return (self.expr,)
+
+
+@dataclass
+class QuasiQuotedExpr(Expression):
+ expr: Expression
+
+ @property
+ def children(self) -> Sequence[ASTNode]:
+ return (self.expr,)
+
+
+@dataclass
+class UnquotedExpr(Expression):
+ expr: Expression
+
+ @property
+ def children(self) -> Sequence[ASTNode]:
+ return (self.expr,)
+
+
+@dataclass
+class UnquoteSplicingExpr(Expression):
+ expr: Expression
+
+ @property
+ def children(self) -> Sequence[ASTNode]:
+ return (self.expr,)
+
+
+@dataclass
+class Identifier(Expression):
+ name: str
+
+
+@dataclass
+class LiteralVal(Expression):
+ value: str
+
+
+@dataclass
+class String(LiteralVal):
+ pass
+
+
+@dataclass
+class Boolean(LiteralVal):
+ pass
+
+
+@dataclass
+class Character(LiteralVal):
+ pass
+
+
+@dataclass
+class Number(LiteralVal):
+ pass
+
+
+def token_to_node(token: Token) -> Expression:
+ match(token['token_type']):
+ case 'identifier':
+ return Identifier(start_loc=token['start'], end_loc=token['end'], name=token['token'])
+ case 'string':
+ return String(start_loc=token['start'], end_loc=token['end'], value=token['token'])
+ case 'boolean':
+ return Boolean(start_loc=token['start'], end_loc=token['end'], value=token['token'])
+ case 'character':
+ return Character(start_loc=token['start'], end_loc=token['end'], value=token['token'])
+ case 'number':
+ return Number(start_loc=token['start'], end_loc=token['end'], value=token['token'])
+ case 'reader_directive':
+ return Identifier(start_loc=token['start'], end_loc=token['end'], name=token['token'])
+ case _:
+ assert False, f'Unexpected token type {token["token_type"]}'
+
+
+# Scheme list parser
+
+QuoteExprClass: TypeAlias = (
+ Type[QuotedExpr] | Type[QuasiQuotedExpr] | Type[UnquotedExpr] | Type[UnquoteSplicingExpr]
+)
+
+# Quotation markers
+QUOTES: dict[str, QuoteExprClass] = {
+ "'": QuotedExpr,
+ '`': QuasiQuotedExpr,
+ ',': UnquotedExpr,
+ ',@': UnquoteSplicingExpr
+}
+
+
+ClosingBracket: TypeAlias = Literal[')', ']']
+MATCHING_BRACKETS: Final[dict[str, ClosingBracket]] = {
+ '(': ')',
+ '[': ']',
+ '#(': ')',
+}
+
+
+class SchemeReader:
+ def __init__(self):
+ # Dict of {line #: comment token}
+ self.comments: dict[int, Token] = {}
+
+ def read_file(self, file_: TextIO) -> Program:
+ expressions = []
+ tokens = list(Tokenizer().tokenize_lines(file_))
+ src = Buffer(iter(tokens))
+ while src.current() is not None:
+ while src.more_on_line:
+ expression = self.scheme_read(src)
+ expressions.append(expression)
+
+ return Program(
+ start_loc=expressions[0].start_loc,
+ end_loc=expressions[0].end_loc,
+ expressions=expressions,
+ comments=self.comments,
+ )
+
+ def scheme_read(self, src: Buffer) -> Expression:
+ """Read the next expression from SRC, a Buffer of tokens."""
+ token = src.pop()
+ if token is None:
+ raise EOFError
+
+ if token['token_type'] == 'comment':
+ self.comments[token['start']['line']] = token
+ return self.scheme_read(src)
+
+ val = token['token']
+ start_loc = token['start']
+ if val not in PUNCTUATORS:
+ return token_to_node(token)
+ if val in QUOTES:
+ # SKELETON pass # fill in your solution here
+ # BEGIN SOLUTION
+ quoted_expr = self.scheme_read(src)
+ return QUOTES[val](start_loc=start_loc, end_loc=quoted_expr.end_loc, expr=quoted_expr)
+ # END SOLUTION
+ if val in MATCHING_BRACKETS:
+ opening_bracket = val
+ expected_closing_bracket = MATCHING_BRACKETS[opening_bracket]
+ current_token = src.current()
+ if current_token is None:
+ raise SyntaxError('unexpected end of file')
+
+ # Empty list
+ if current_token['token'] == expected_closing_bracket:
+ closing_paren = src.pop()
+ assert closing_paren is not None
+ return SList(
+ start_loc=start_loc, end_loc=closing_paren['end'], items=tuple(),
+ opening_bracket=opening_bracket, closing_bracket=closing_paren['token'],
+ )
+
+ head = self.read_head(src)
+
+ current_token = src.current()
+ if current_token is None:
+ raise SyntaxError('unexpected end of file')
+ if current_token['token'] == '.':
+ src.pop()
+ tail = self.scheme_read(src)
+ if src.current() is None:
+ raise SyntaxError('unexpected end of file')
+
+ closing_token = src.pop()
+ assert closing_token is not None
+ if closing_token['token'] != expected_closing_bracket:
+ if closing_token in MATCHING_BRACKETS:
+ raise SyntaxError(
+ f'Bracket mismatch: expected {expected_closing_bracket} '
+ f'but got {closing_token["token"]}'
+ )
+ raise SyntaxError('Expected one element after .')
+
+ return Pair(
+ start_loc=start_loc, end_loc=closing_token['end'],
+ first=head, second=tail
+ )
+
+ tail, closing_paren = self.read_tail(src, expected_closing_bracket)
+ return SList(
+ start_loc=start_loc, end_loc=closing_paren['end'], items=[head, *tail],
+ opening_bracket=opening_bracket, closing_bracket=closing_paren['token'],
+ )
+
+ raise SyntaxError('unexpected token: {0}'.format(val))
+
+ def read_head(self, src: Buffer) -> Expression:
+ """
+ Read an expression to be used as the head of a list or pair.
+ Return None and leave the closing paren in the buffer if the next token closes the list.
+ """
+ try:
+ current_token = src.current()
+ if current_token is None:
+ raise SyntaxError('unexpected end of file')
+ current_val = current_token['token']
+
+ if current_val == '.':
+ raise SyntaxError('. must have at least one element before it')
+
+ expr = self.scheme_read(src)
+ if src.current() is None:
+ raise SyntaxError('unexpected end of file')
+
+ return expr
+ except EOFError as exc:
+ raise SyntaxError('unexpected end of file') from exc
+
+ def read_tail(
+ self,
+ src: Buffer, expected_closing_bracket: ClosingBracket
+ ) -> tuple[list[Expression], Token]:
+ """Return the remainder of a list in SRC.
+
+ Return a tuple of (expressions, closing paren/bracket token).
+ """
+ try:
+ current_token = src.current()
+ if current_token is None:
+ raise SyntaxError('unexpected end of file')
+ current_val = current_token['token']
+ # Empty list
+ if current_val == expected_closing_bracket:
+ src.pop()
+ return [], current_token
+
+ first = self.scheme_read(src)
+ rest, closing_paren = self.read_tail(src, expected_closing_bracket)
+ return [first, *rest], closing_paren
+ except EOFError as exc:
+ raise SyntaxError('unexpected end of file') from exc
diff --git a/racket_mutation_analysis/racket_ast/scheme_tokens.py b/racket_mutation_analysis/racket_ast/scheme_tokens.py
new file mode 100644
index 0000000..ba124dd
--- /dev/null
+++ b/racket_mutation_analysis/racket_ast/scheme_tokens.py
@@ -0,0 +1,396 @@
+# NOTE: This module has been modified, and the docstrings have not yet been
+# updated. We've added type annotations in places where we've modified
+# the behavior of functions.
+
+"""Lexer for Scheme.
+
+This module provides a Tokenizer class with tokenize_line and
+tokenize_lines methods for converting (iterators producing) strings
+into (iterators producing) lists of tokens. A token may be:
+
+ * A number (represented as an int, float, complex, or Fraction)
+ * A boolean (represented as a bool)
+ * A character (represented as a string)
+ * A symbol (represented as a string)
+ * A string (represented as a quoted string)
+ * A punctuator, including parentheses, dots, and single quotes
+
+Author: Amir Kamil
+Small portions of code were derived from the Scheme interpreter
+project in the Composing Programs text by John DeNero.
+
+Project UID 2d6261568f83a98aa474c0a2b04179ce000b9a48
+"""
+
+from __future__ import annotations
+
+import cmath
+import fractions
+import itertools
+import re
+import sys
+from typing import Literal, TypeAlias, TypedDict
+
+_WHITESPACE = set(' \t\n\r')
+PUNCTUATORS = set("()'`,.[]") | {',@', '#('}
+DELIMITERS = _WHITESPACE | {'(', ')', '[', ']', '"', ';'}
+
+# Simple tokens
+STRING_CHARS = r'(\\\\|\\"|[^\\])'
+RAW_STRING = re.compile(fr'"{STRING_CHARS}*?"')
+STRING_START = re.compile(fr'"{STRING_CHARS}*\n')
+STRING_END = re.compile(fr'{STRING_CHARS}*?"')
+STRING_ESCAPE = re.compile(r'\\(.)', flags=re.DOTALL)
+BOOLEAN = re.compile(r'#[tTfF]')
+CHARACTER = re.compile(r'#\\[sS][pP][aA][cC][eE]|'
+ r'#\\[nN][eE][wW][lL][iI][nN][eE]|'
+ r'#\\.', flags=re.DOTALL)
+INITIAL = r'a-zA-Z!\$%&\*/:<=>\?\^_~'
+SUBSEQUENT = INITIAL + r'0-9\+\-\.@'
+IDENTIFIER = re.compile(fr'[{INITIAL}][{SUBSEQUENT}]*|\+|\-|\.\.\.')
+PUNCTUATOR = re.compile(r"#\(|,@|[()\[\]'`,.]")
+COMMENT = re.compile(r';.*')
+
+
+# Numbers
+EXACTNESS = r'(#[eEiI])'
+PREFIX_2 = fr'(#[bB]{EXACTNESS}?|{EXACTNESS}#[bB])'
+PREFIX_8 = fr'(#[oO]{EXACTNESS}?|{EXACTNESS}#[oO])'
+PREFIX_10 = fr'({EXACTNESS}#[dD]|(#[dD])?{EXACTNESS}?)'
+PREFIX_16 = fr'(#[xX]{EXACTNESS}?|{EXACTNESS}#[xX])'
+EXPONENT_MARKER = r'[eEsSfFdDlL]'
+SIGN = r'[+-]?'
+SUFFIX = fr'({EXPONENT_MARKER}{SIGN}[0-9]+)?'
+UINTEGER_2 = r'[01]+#*'
+UINTEGER_8 = r'[0-7]+#*'
+UINTEGER_10 = r'[0-9]+#*'
+UINTEGER_16 = r'[0-9a-fA-F]+#*'
+DECIMAL_10 = (fr'(\.[0-9]+#*{SUFFIX}|'
+ fr'[0-9]+\.[0-9]*#*{SUFFIX}|'
+ fr'[0-9]+#+\.#*{SUFFIX}|'
+ fr'{UINTEGER_10}{SUFFIX})')
+UREAL_2 = fr'({UINTEGER_2}/{UINTEGER_2}|{UINTEGER_2})'
+UREAL_8 = fr'({UINTEGER_8}/{UINTEGER_8}|{UINTEGER_8})'
+UREAL_10 = (fr'({UINTEGER_10}/{UINTEGER_10}|{DECIMAL_10}|'
+ fr'{UINTEGER_10})')
+UREAL_16 = fr'({UINTEGER_16}/{UINTEGER_16}|{UINTEGER_16})'
+REAL_2 = fr'{SIGN}{UREAL_2}'
+REAL_8 = fr'{SIGN}{UREAL_8}'
+REAL_10 = fr'{SIGN}{UREAL_10}'
+REAL_16 = fr'{SIGN}{UREAL_16}'
+COMPLEX_2 = (fr'({REAL_2}@{REAL_2}|{REAL_2}[+-]{UREAL_2}?i|'
+ fr'[+-]{UREAL_2}?i|{REAL_2})')
+COMPLEX_8 = (fr'({REAL_8}@{REAL_8}|{REAL_8}[+-]{UREAL_8}?i|'
+ fr'[+-]{UREAL_8}?i|{REAL_8})')
+COMPLEX_10 = (fr'({REAL_10}@{REAL_10}|{REAL_10}[+-]{UREAL_10}?i|'
+ fr'[+-]{UREAL_10}?i|{REAL_10})')
+COMPLEX_16 = (fr'({REAL_16}@{REAL_16}|{REAL_16}[+-]{UREAL_16}?i|'
+ fr'[+-]{UREAL_16}?i|{REAL_16})')
+NUMBER = re.compile(fr'({PREFIX_2}{COMPLEX_2}|'
+ fr'{PREFIX_8}{COMPLEX_8}|'
+ fr'{PREFIX_10}{COMPLEX_10}|'
+ fr'{PREFIX_16}{COMPLEX_16})')
+
+# Number utilities
+PREFIX = re.compile(r'#[bodx](#[ei])?|#[ei](#[bodx])?')
+RADIX = re.compile(r'#[bodx]')
+RADIX_MAP = {'#b': 2, '#o': 8, '#d': 10, '#x': 16}
+EXACTNESS_LOWER = re.compile(r'#[ei]')
+EXPONENT = re.compile(r'[esdfl]')
+
+READER_DIRECTIVE = re.compile(r'(#reader)|(#lang)')
+
+# Token precedence order
+TOKEN_PATTERNS = (RAW_STRING, STRING_START, READER_DIRECTIVE, BOOLEAN, CHARACTER,
+ NUMBER, IDENTIFIER, PUNCTUATOR, COMMENT)
+
+
+def process_rational(token_text, radix=10, inexact=False):
+ """Convert a rational or integer literal to a number.
+
+ If inexact is true, returns a float. Otherwise returns an int
+ or Fraction.
+ """
+ # Fractions
+ if '/' in token_text:
+ value = fractions.Fraction(*(int(num, radix)
+ for num in
+ token_text.split('/')))
+ if inexact:
+ return float(value)
+ return (value.numerator if value.denominator == 1
+ else value)
+
+ # Integers
+ value = int(token_text, radix)
+ return float(value) if inexact else value
+
+
+def process_number(token_text):
+ """Convert a numeric literal to its associated value.
+
+ Handles floating-point numbers in base 10 and fractions and
+ integers in bases 2, 8, 10, and 16. Handles exact and inexact
+ prefixes. Returns a float for inexact literals and an int or
+ Fraction for an exact literal.
+ """
+ token_text = token_text.lower()
+
+ prefix_match = PREFIX.match(token_text)
+ prefix = ''
+ if prefix_match:
+ # Strip prefix
+ token_text = token_text[prefix_match.span()[1]:]
+ prefix = prefix_match.group()
+
+ # Determine exactness and strip it
+ inexact = '#i' in prefix
+ exact = '#e' in prefix
+ prefix = EXACTNESS_LOWER.sub('', prefix)
+
+ # Determine radix
+ radix_match = RADIX.match(prefix)
+ radix = 10
+ if radix_match:
+ radix = RADIX_MAP[radix_match.group()]
+
+ # Convert all remaining hash symbols to zeros
+ token_text = token_text.replace('#', '0')
+
+ # Floating-point literals
+ if '.' in token_text or 'e' in token_text:
+ # Convert all (now lower-case) exponent markers to e
+ token_text = EXPONENT.sub('e', token_text)
+
+ # Complex numbers, always inexact
+ if token_text[-1] == 'i':
+ return complex(token_text[:-1] + 'j')
+ # Polar notation
+ if '@' in token_text:
+ partitioned = token_text.partition('@')
+ return cmath.rect(float(partitioned[0]),
+ float(partitioned[2]))
+
+ # Real numbers
+ return (fractions.Fraction(token_text) if exact
+ else float(token_text))
+
+ # Complex numbers
+ if token_text[-1] == 'i':
+ # Find rightmost sign
+ split = max(token_text.rfind('+'), token_text.rfind('-'))
+ real_text = token_text[:split]
+ imag_text = token_text[split: -1]
+ if not real_text:
+ real_text = '0'
+ if len(imag_text) == 1:
+ imag_text += '1'
+ return complex(process_rational(real_text, radix), # type: ignore
+ process_rational(imag_text, radix)) # type: ignore
+ # Polar notation
+ if '@' in token_text:
+ partitioned = token_text.partition('@')
+ return cmath.rect(process_rational(partitioned[0], radix), # type: ignore
+ process_rational(partitioned[2], radix)) # type: ignore
+
+ # Rationals and integers
+ return process_rational(token_text, radix, inexact)
+
+
+def check_termination(token_text, pattern, next_char):
+ """Check if token must be implicitly terminated but is not."""
+ undelimited = False
+ if pattern in (IDENTIFIER, NUMBER):
+ undelimited = next_char not in DELIMITERS
+ elif pattern is CHARACTER:
+ undelimited = (len(token_text) == 3
+ and token_text[2].isalpha()
+ and next_char not in DELIMITERS)
+ elif pattern is PUNCTUATOR:
+ undelimited = (token_text == '.'
+ and next_char not in DELIMITERS)
+ if undelimited:
+ print(f'warning: token {token_text} should be terminated by '
+ f'a delimiter instead of {next_char}', file=sys.stderr)
+
+
+TokenType: TypeAlias = Literal[
+ 'string', 'boolean', 'character', 'number', 'identifier',
+ 'comment', 'empty', 'punctuator', 'reader_directive',
+]
+
+
+class Token(TypedDict):
+ token_type: TokenType
+ token: str
+ start: Location
+ end: Location
+
+
+class Location(TypedDict):
+ line: int
+ column: int
+
+
+class Tokenizer:
+ """Lexes input data representing Scheme source code."""
+
+ def __init__(self):
+ """Initialize this tokenizer to be empty."""
+ self.partial_string: str | None = None
+ self.partial_string_start: Location | None = None
+
+ def _process_token(
+ self, token_text, pattern, start_loc: Location | None = None
+ ) -> tuple[str, TokenType]:
+ """Process the token, where pattern is what the text matched.
+
+ For strings, replaces escape sequences.
+ For booleans, converts them to the corresponding Python value/
+ For decimals, coverts them to number format.
+ For fractions, converts them to Fraction objects.
+ For identifiers, converts them to lowercase.
+ """
+ token_text = token_text
+ if pattern is RAW_STRING:
+ return STRING_ESCAPE.sub(r'\1', token_text), 'string'
+ elif pattern is STRING_START:
+ self.partial_string = token_text
+ assert start_loc is not None
+ self.partial_string_start = start_loc
+ return '', 'empty'
+ elif pattern is BOOLEAN:
+ return token_text, 'boolean'
+ elif pattern is CHARACTER:
+ if len(token_text) > 3: # space and newline escapes
+ return token_text, 'character'
+ elif pattern is NUMBER:
+ return token_text, 'number'
+ elif pattern is IDENTIFIER:
+ return token_text, 'identifier'
+ elif pattern is COMMENT:
+ return token_text, 'comment'
+ elif pattern is READER_DIRECTIVE:
+ return token_text, 'reader_directive'
+ return token_text, 'punctuator'
+
+ def _next_token(self, line, position, line_num) -> Token:
+ """Return the next token in line after the given position.
+
+ Produces a tuple (token, position'), where token is the next
+ substring of line at or after the given position that could be
+ a token (assuming it passes a validity check), and position'
+ is the position in line following that token. Returns ('',
+ len(line)) when there are no more tokens.
+
+ For multiline strings, the string read so far is in
+ self.partial_string.
+ """
+ # Check for and handle multiline string
+ if self.partial_string:
+ assert position == 0, line + f' ({position})'
+ match = STRING_END.match(line)
+ if match:
+ token_text = self.partial_string + match.group()
+ assert self.partial_string_start is not None
+ token_val, token_type = self._process_token(
+ token_text, RAW_STRING, start_loc=self.partial_string_start
+ )
+ token: Token = {
+ 'token': token_val,
+ 'token_type': token_type,
+ 'start': self.partial_string_start,
+ 'end': {'line': line_num, 'column': match.span()[1]}
+ }
+ self.partial_string = None
+ self.partial_string_start = None
+ return token
+ self.partial_string += line
+ return {
+ 'token': '',
+ 'token_type': 'empty',
+ 'start': {'line': line_num, 'column': 0},
+ 'end': {'line': line_num, 'column': len(line)},
+ }
+
+ # Discard leading whitespace
+ while position < len(line) and line[position] in _WHITESPACE:
+ position += 1
+
+ if position == len(line):
+ return {
+ 'token': '',
+ 'token_type': 'empty',
+ 'start': {'line': line_num, 'column': -1},
+ 'end': {'line': line_num, 'column': position},
+ }
+
+ text = line[position:]
+ # Attempt to match each token type in order
+ for pattern in TOKEN_PATTERNS:
+ match = pattern.match(text)
+ if match:
+ start_col = position + match.span()[0]
+ start_loc: Location = {'line': line_num, 'column': start_col}
+ token_val, token_type = self._process_token(
+ match.group(), pattern, start_loc=start_loc)
+ position += match.span()[1]
+ check_termination(match.group(), pattern,
+ line[position: position + 1])
+ return {
+ 'token': token_val,
+ 'token_type': token_type,
+ 'start': start_loc,
+ 'end': {'line': line_num, 'column': position},
+ }
+
+ # Did not match any token type
+ raise SyntaxError(f'invalid token: {text}')
+
+ def tokenize_line(self, line_num, line) -> list[Token]:
+ """Return a list of the Scheme tokens on line.
+
+ Excludes comments and whitespace.
+ """
+ # Some forms of input strip the trailing newline
+ if line[-1:] != '\n':
+ line += '\n'
+
+ result = []
+ token = self._next_token(line, 0, line_num)
+ while token['token'] != '':
+ result.append(token)
+ # print(token)
+ token = self._next_token(line, token['end']['column'], line_num)
+
+ return result
+
+ def tokenize_lines(self, input_data):
+ """Produce an iterator over lists of the tokens in the input.
+
+ A list is returned for each line of the iterable input
+ sequence.
+ """
+ return itertools.starmap(self.tokenize_line, enumerate(input_data))
+
+
+def count_tokens(input_data):
+ """Count the number of non-punctuator tokens in the input."""
+ return len(
+ list(
+ filter(
+ lambda x: x['token'] not in PUNCTUATORS,
+ itertools.chain(*Tokenizer().tokenize_lines(input_data))
+ )
+ )
+ )
+
+
+def main():
+ """Count the tokens in standard input."""
+ with open(sys.argv[1], 'r') if len(sys.argv) > 1 else sys.stdin as f:
+ print('counted', count_tokens(f), 'non-punctuator tokens')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/racket_mutation_analysis/racket_ast/string_visitor.py b/racket_mutation_analysis/racket_ast/string_visitor.py
new file mode 100644
index 0000000..7e121d9
--- /dev/null
+++ b/racket_mutation_analysis/racket_ast/string_visitor.py
@@ -0,0 +1,93 @@
+from racket_mutation_analysis.racket_ast.scheme_reader import (
+ ASTNode, Identifier, LiteralVal, Pair, Program, QuasiQuotedExpr, QuotedExpr, SList,
+ UnquotedExpr, UnquoteSplicingExpr
+)
+from racket_mutation_analysis.racket_ast.scheme_tokens import Location
+from racket_mutation_analysis.racket_ast.visitor import Visitor
+
+
+class ToStrVisitor(Visitor):
+ def __init__(self):
+ self.current_line = 0
+ self.current_col = 0
+
+ self.result = ''
+
+ self.program: Program | None = None
+
+ def _pad_until(self, loc: Location):
+ while self.current_line < loc['line']:
+ if self.program is not None and self.current_line in self.program.comments:
+ comment = self.program.comments[self.current_line]
+ self._pad_until(comment['start'])
+ self.result += comment['token']
+
+ self.result += '\n'
+ self.current_line += 1
+ self.current_col = 0
+
+ while self.current_col < loc['column']:
+ self.result += ' '
+ self.current_col += 1
+
+ def visit_ASTNode(self, node: ASTNode):
+ if self.program is None and isinstance(node, Program):
+ self.program = node
+
+ self._pad_until(node.start_loc)
+ super().visit_ASTNode(node)
+
+ def visit_SList(self, node: SList):
+ self.result += node.opening_bracket
+ self.current_col += len(node.opening_bracket)
+
+ super().visit_SList(node)
+
+ # The end column stored in the node includes the closing ), but we
+ # still need to print it ourselves.
+ self._pad_until({'line': node.end_loc['line'], 'column': node.end_loc['column'] - 1})
+ self.result += node.closing_bracket
+ self.current_col += 1
+
+ def visit_Pair(self, node: Pair):
+ self.result += '('
+ self.current_col += 1
+
+ self.visit_ASTNode(node.first)
+ self.result += '.'
+ self.current_col += 1
+ self.visit_ASTNode(node.second)
+
+ # The end column stored in the node includes the closing ), but we
+ # still need to print it ourselves.
+ self._pad_until({'line': node.end_loc['line'], 'column': node.end_loc['column'] - 1})
+ self.result += ')'
+ self.current_col += 1
+
+ def visit_QuotedExpr(self, node: QuotedExpr):
+ self.result += "'"
+ self.current_col += 1
+ super().visit_QuotedExpr(node)
+
+ def visit_QuasiQuotedExpr(self, node: QuasiQuotedExpr):
+ self.result += '`'
+ self.current_col += 1
+ super().visit_QuasiQuotedExpr(node)
+
+ def visit_UnquotedExpr(self, node: UnquotedExpr):
+ self.result += ','
+ self.current_col += 1
+ super().visit_UnquotedExpr(node)
+
+ def visit_UnquoteSplicingExpr(self, node: UnquoteSplicingExpr):
+ self.result += ',@'
+ self.current_col += 2
+ super().visit_UnquoteSplicingExpr(node)
+
+ def visit_Identifier(self, node: Identifier):
+ self.result += node.name
+ self.current_col += len(node.name)
+
+ def visit_LiteralVal(self, node: LiteralVal):
+ self.result += node.value
+ self.current_col += len(node.value)
diff --git a/racket_mutation_analysis/racket_ast/utils.py b/racket_mutation_analysis/racket_ast/utils.py
new file mode 100644
index 0000000..2dac7d9
--- /dev/null
+++ b/racket_mutation_analysis/racket_ast/utils.py
@@ -0,0 +1,27 @@
+from typing import Container
+
+from .scheme_reader import Identifier, SList
+
+
+def is_call_to(slist: SList, func_name: str) -> bool:
+ """
+ Returns True if the given SList is a call to a function named
+ `func_name`.
+ """
+ if len(slist.items) == 0:
+ return False
+
+ first = slist.items[0]
+ return isinstance(first, Identifier) and first.name == func_name
+
+
+def is_call_to_one_of(slist: SList, func_names: Container[str]) -> bool:
+ """
+ Returns True if the given SList is a call to one of the functions
+ contained in `func_names`.
+ """
+ if len(slist.items) == 0:
+ return False
+
+ first = slist.items[0]
+ return isinstance(first, Identifier) and first.name in func_names
diff --git a/racket_mutation_analysis/racket_ast/visitor.py b/racket_mutation_analysis/racket_ast/visitor.py
new file mode 100644
index 0000000..da86ba5
--- /dev/null
+++ b/racket_mutation_analysis/racket_ast/visitor.py
@@ -0,0 +1,95 @@
+from .scheme_reader import (
+ ASTNode, Boolean, Character, Expression, Identifier, LiteralVal, Number, Pair, Program,
+ QuasiQuotedExpr, QuotedExpr, SList, String, UnquotedExpr, UnquoteSplicingExpr
+)
+
+
+class Visitor:
+ def visit(self, node: ASTNode):
+ self.visit_ASTNode(node)
+
+ def visit_ASTNode(self, node: ASTNode):
+ match(node):
+ case Program():
+ self.visit_Program(node)
+ case Expression():
+ self.visit_Expression(node)
+ case _:
+ assert False
+
+ def visit_Program(self, node: Program):
+ for child in node.children:
+ self.visit_ASTNode(child)
+
+ def visit_Expression(self, node: Expression):
+ match(node):
+ case SList():
+ self.visit_SList(node)
+ case Pair():
+ self.visit_Pair(node)
+ case QuotedExpr():
+ self.visit_QuotedExpr(node)
+ case QuasiQuotedExpr():
+ self.visit_QuasiQuotedExpr(node)
+ case UnquotedExpr():
+ self.visit_UnquotedExpr(node)
+ case UnquoteSplicingExpr():
+ self.visit_UnquoteSplicingExpr(node)
+ case Identifier():
+ self.visit_Identifier(node)
+ case LiteralVal():
+ self.visit_LiteralVal(node)
+ case _:
+ assert False
+
+ def visit_SList(self, node: SList):
+ for child in node.children:
+ self.visit_ASTNode(child)
+
+ def visit_Pair(self, node: Pair):
+ for child in node.children:
+ self.visit_ASTNode(child)
+
+ def visit_QuotedExpr(self, node: QuotedExpr):
+ for child in node.children:
+ self.visit_ASTNode(child)
+
+ def visit_QuasiQuotedExpr(self, node: QuasiQuotedExpr):
+ for child in node.children:
+ self.visit_ASTNode(child)
+
+ def visit_UnquotedExpr(self, node: UnquotedExpr):
+ for child in node.children:
+ self.visit_ASTNode(child)
+
+ def visit_UnquoteSplicingExpr(self, node: UnquoteSplicingExpr):
+ for child in node.children:
+ self.visit_ASTNode(child)
+
+ def visit_Identifier(self, node: Identifier):
+ pass
+
+ def visit_LiteralVal(self, node: LiteralVal):
+ match(node):
+ case String():
+ self.visit_String(node)
+ case Boolean():
+ self.visit_Boolean(node)
+ case Character():
+ self.visit_Character(node)
+ case Number():
+ self.visit_Number(node)
+ case _:
+ assert False
+
+ def visit_String(self, node: String):
+ pass
+
+ def visit_Boolean(self, node: Boolean):
+ pass
+
+ def visit_Character(self, node: Character):
+ pass
+
+ def visit_Number(self, node: Number):
+ pass
diff --git a/racket_mutation_analysis/racket_mutation/__init__.py b/racket_mutation_analysis/racket_mutation/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/racket_mutation_analysis/racket_mutation/main.py b/racket_mutation_analysis/racket_mutation/main.py
new file mode 100644
index 0000000..ca062bd
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/main.py
@@ -0,0 +1,64 @@
+import argparse
+from pathlib import Path
+
+import yaml
+
+from racket_mutation_analysis.racket_ast.scheme_reader import SchemeReader
+from racket_mutation_analysis.racket_mutation.mutant_generator import MutantGenerator
+from racket_mutation_analysis.racket_mutation.mutators import MUTATOR_CLASSES
+from racket_mutation_analysis.racket_mutation.schema import AppliedMutant, GeneratedMutants
+
+
+def main():
+ args = parse_args()
+
+ yaml_dict: GeneratedMutants = {}
+
+ for input_file in args.input_files:
+ with open(input_file) as f:
+ ast = SchemeReader().read_file(f)
+
+ with open(input_file) as f:
+ file_text = f.read()
+ code = file_text.splitlines(keepends=True)
+
+ if not args.mutator:
+ desired_mutators = list(MUTATOR_CLASSES.values())
+ else:
+ desired_mutators = [
+ mutator_class for mutator_name, mutator_class in MUTATOR_CLASSES.items()
+ if mutator_name in args.mutator
+ ]
+
+ mutant_generator = MutantGenerator(ast, code, desired_mutators)
+ mutants = mutant_generator.run()
+
+ path = Path(input_file).relative_to(args.project_root)
+ yaml_dict[str(path)] = {
+ 'original': file_text,
+ 'mutants': [
+ AppliedMutant(mutated_code=mutated_code, **mutant)
+ for mutant, mutated_code in mutants
+ ]
+ }
+
+ with open(args.output_file, 'w') as f:
+ f.write(yaml.dump(yaml_dict, width=float('inf'))) # type: ignore
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(
+ "Command line interface for Racket mutant generation")
+ # input file
+ parser.add_argument('input_files', nargs='+')
+ # output YAML file name
+ parser.add_argument('output_file')
+ # desired mutator(s)
+ parser.add_argument('--mutator', '-m', action='append', choices=list(MUTATOR_CLASSES))
+ parser.add_argument('--project_root', '-p', default='.')
+
+ return parser.parse_args()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/racket_mutation_analysis/racket_mutation/mutant_generator.py b/racket_mutation_analysis/racket_mutation/mutant_generator.py
new file mode 100644
index 0000000..17fe1ad
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutant_generator.py
@@ -0,0 +1,54 @@
+from typing import Sequence, Type
+
+from racket_mutation_analysis.racket_ast.scheme_reader import Program
+from racket_mutation_analysis.racket_mutation.mutators.mutator import MutatorVisitor
+from racket_mutation_analysis.racket_mutation.schema import Mutant
+
+
+class MutantGenerator:
+
+ def __init__(self, tree: Program, input_file: list[str],
+ mutator_classes: Sequence[Type[MutatorVisitor]]) -> None:
+ self.ast = tree
+ self.input_file = input_file
+ self.mutations: list[Mutant] = []
+ self.mutator_classes = mutator_classes
+
+ # Applies each visitor to the given AST
+ def run(self) -> list[tuple[Mutant, str]]:
+ mutants: list[tuple[Mutant, str]] = []
+
+ for mutator_class in self.mutator_classes:
+ mutator = mutator_class()
+ mutator.visit(self.ast)
+ self.mutations += mutator.list_mutants
+
+ # For each Mutant in list, create a new mutated string
+ for mutant in self.mutations:
+ new_string = '' # reset new mutant string
+ start_line_num = mutant['location']['start']['line']
+ start_col_num = mutant['location']['start']['column']
+ end_line_num = mutant['location']['end']['line']
+ end_col_num = mutant['location']['end']['column']
+
+ line = 0
+ while line < len(self.input_file):
+ if line == start_line_num == end_line_num:
+ new_string += self.input_file[line][0:start_col_num]
+ new_string += mutant['replacement']
+ new_string += self.input_file[line][end_col_num:]
+ line += 1
+ elif line == start_line_num:
+ new_string += self.input_file[line][0:start_col_num]
+ new_string += mutant['replacement']
+ line = end_line_num
+ elif line == end_line_num:
+ new_string += self.input_file[line][end_col_num:]
+ line += 1
+ else: # there is no mutant in this line
+ new_string += self.input_file[line]
+ line += 1
+
+ mutants.append((mutant, new_string))
+
+ return mutants
diff --git a/racket_mutation_analysis/racket_mutation/mutators/__init__.py b/racket_mutation_analysis/racket_mutation/mutators/__init__.py
new file mode 100644
index 0000000..896d5b3
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/__init__.py
@@ -0,0 +1,34 @@
+from typing import Final, Mapping, Type
+
+from .arithmetic_deletion import ArithmeticDeletionMutator
+from .arithmetic_mut import ArithmeticMutator
+from .bool_func_to_bool import BoolFuncToBoolMutator
+from .cond_mut import CondMutator
+from .empty_list import EmptyListMutator
+from .empty_string import EmptyStringMutator
+from .flip_bools import FlipBooleansMutator
+from .flip_num_sign import FlipNumSignMutator
+from .homework_mutator import Homework4and5Mutator
+from .if_mut import IfMutator
+from .logical_mut import LogicalMutator
+from .mutator import MutatorVisitor
+from .num_comparison_mutator import NumberComparisonMutator
+from .num_literals_mut import NumLiteralsMutator
+from .wrap_with_not import WrapWithNotMutator
+
+MUTATOR_CLASSES: Final[Mapping[str, Type[MutatorVisitor]]] = {
+ ArithmeticMutator.__name__: ArithmeticMutator,
+ LogicalMutator.__name__: LogicalMutator,
+ IfMutator.__name__: IfMutator,
+ CondMutator.__name__: CondMutator,
+ WrapWithNotMutator.__name__: WrapWithNotMutator,
+ BoolFuncToBoolMutator.__name__: BoolFuncToBoolMutator,
+ FlipNumSignMutator.__name__: FlipNumSignMutator,
+ ArithmeticDeletionMutator.__name__: ArithmeticDeletionMutator,
+ NumberComparisonMutator.__name__: NumberComparisonMutator,
+ FlipBooleansMutator.__name__: FlipBooleansMutator,
+ EmptyListMutator.__name__: EmptyListMutator,
+ EmptyStringMutator.__name__: EmptyStringMutator,
+ NumLiteralsMutator.__name__: NumLiteralsMutator,
+ Homework4and5Mutator.__name__: Homework4and5Mutator
+}
diff --git a/racket_mutation_analysis/racket_mutation/mutators/arithmetic_deletion.py b/racket_mutation_analysis/racket_mutation/mutators/arithmetic_deletion.py
new file mode 100644
index 0000000..53160f1
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/arithmetic_deletion.py
@@ -0,0 +1,39 @@
+from typing import Final
+
+from racket_mutation_analysis.racket_ast.scheme_reader import Identifier, SList
+from racket_mutation_analysis.racket_ast.string_visitor import ToStrVisitor
+from racket_mutation_analysis.racket_ast.utils import is_call_to_one_of
+
+from ..schema import Location, Position
+from .mutator import MutatorVisitor
+
+
+class ArithmeticDeletionMutator(MutatorVisitor):
+ _operators: Final = {
+ '+', '-', '*', '/'
+ }
+
+ def visit_SList(self, node: SList):
+ if not is_call_to_one_of(node, self._operators):
+ super().visit_SList(node)
+ return
+
+ first_node = node.items[0]
+ last_node = node.items[len(node.items) - 1]
+ new_start: Position = {'line': first_node.start_loc['line'],
+ 'column': first_node.start_loc['column'] - 1}
+ new_end: Position = {'line': last_node.end_loc['line'],
+ 'column': last_node.end_loc['column'] + 1}
+ new_location: Location = {'start': new_start, 'end': new_end}
+
+ for expr_node in node.items[1:]:
+ str_visitor = ToStrVisitor()
+ str_visitor.visit(expr_node)
+ self.list_mutants.append({
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': str_visitor.result.strip()
+ })
+
+ super().visit_SList(node)
diff --git a/racket_mutation_analysis/racket_mutation/mutators/arithmetic_mut.py b/racket_mutation_analysis/racket_mutation/mutators/arithmetic_mut.py
new file mode 100644
index 0000000..434452b
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/arithmetic_mut.py
@@ -0,0 +1,30 @@
+from typing import Final
+
+from racket_mutation_analysis.racket_ast.scheme_reader import Identifier, SList
+from racket_mutation_analysis.racket_ast.utils import is_call_to_one_of
+
+from ..schema import Location
+from .mutator import MutatorVisitor
+
+
+class ArithmeticMutator(MutatorVisitor):
+ _operator_substitutions: Final = {
+ '+': '-',
+ '-': '+',
+ '*': '/',
+ '/': '*',
+ }
+
+ def visit_SList(self, node: SList):
+ if is_call_to_one_of(node, self._operator_substitutions):
+ first_node = node.items[0]
+ assert isinstance(first_node, Identifier)
+ new_location: Location = {'start': first_node.start_loc, 'end': first_node.end_loc}
+ self.list_mutants.append({
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': self._operator_substitutions[first_node.name]
+ })
+
+ super().visit_SList(node)
diff --git a/racket_mutation_analysis/racket_mutation/mutators/bool_func_to_bool.py b/racket_mutation_analysis/racket_mutation/mutators/bool_func_to_bool.py
new file mode 100644
index 0000000..ba54a60
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/bool_func_to_bool.py
@@ -0,0 +1,45 @@
+from typing import Final
+
+from racket_mutation_analysis.racket_ast.scheme_reader import Identifier, SList
+from racket_mutation_analysis.racket_ast.utils import is_call_to_one_of
+
+from ..schema import Location, Mutant, Position
+from .mutator import MutatorVisitor
+
+
+class BoolFuncToBoolMutator(MutatorVisitor):
+ _bool_funcs: Final = {
+ 'and', 'or', 'not', '>', '>=',
+ '==', '<', '<=', 'nand', 'nor',
+ 'andmap', 'ormap'
+ }
+
+ def visit_SList(self, node: SList):
+ match(node.items):
+ case [Identifier(name=name) as first_node, *_] \
+ if name in self._bool_funcs or name.endswith('?'):
+ last_node = node.items[len(node.items) - 1]
+
+ new_start_position: Position = {
+ 'line': first_node.start_loc['line'],
+ 'column': first_node.start_loc['column'] - 1}
+ new_end_position: Position = {
+ 'line': last_node.end_loc['line'],
+ 'column': last_node.end_loc['column'] + 1}
+ new_location: Location = {'start': new_start_position,
+ 'end': new_end_position}
+
+ new_mutant_true: Mutant = {
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': '#t'}
+ new_mutant_false: Mutant = {
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': '#f'}
+ self.list_mutants.append(new_mutant_true)
+ self.list_mutants.append(new_mutant_false)
+
+ super().visit_SList(node)
diff --git a/racket_mutation_analysis/racket_mutation/mutators/bool_params_to_bool.py b/racket_mutation_analysis/racket_mutation/mutators/bool_params_to_bool.py
new file mode 100644
index 0000000..28d032f
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/bool_params_to_bool.py
@@ -0,0 +1,47 @@
+from typing import Final
+
+from racket_mutation_analysis.racket_ast.scheme_reader import Identifier, SList
+
+from ..schema import Location, Mutant, Position
+from .mutator import MutatorVisitor
+
+
+class BoolParamsToBool(MutatorVisitor):
+ _bool_param_funcs: Final = {
+ 'and', 'or', 'not'
+ }
+
+ def visit_SList(self, node: SList):
+ first_node = node.items[0]
+
+ if (not isinstance(first_node, Identifier)
+ or first_node.name not in self._bool_param_funcs):
+ super().visit_SList(node)
+ return
+
+ for param in node.items[1:]:
+ new_start_position: Position = {
+ 'line': param.start_loc['line'],
+ 'column': param.start_loc['column']}
+ new_end_position: Position = {
+ 'line': param.end_loc['line'],
+ 'column': param.end_loc['column']}
+ new_location: Location = {
+ 'start': new_start_position,
+ 'end': new_end_position}
+
+ new_mutant_true: Mutant = {
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': '#t'}
+ new_mutant_false: Mutant = {
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': '#f'}
+ self.list_mutants.append(new_mutant_true)
+ self.list_mutants.append(new_mutant_false)
+
+ for node_in_list in node.items:
+ self.visit_ASTNode(node_in_list)
diff --git a/racket_mutation_analysis/racket_mutation/mutators/cond_mut.py b/racket_mutation_analysis/racket_mutation/mutators/cond_mut.py
new file mode 100644
index 0000000..b9ac9ac
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/cond_mut.py
@@ -0,0 +1,36 @@
+from racket_mutation_analysis.racket_ast.scheme_reader import Identifier, SList
+from racket_mutation_analysis.racket_ast.utils import is_call_to
+
+from ..schema import Location, Mutant
+from .mutator import MutatorVisitor
+
+
+class CondMutator(MutatorVisitor):
+
+ def visit_SList(self, node: SList):
+ if not is_call_to(node, 'cond'):
+ super().visit_SList(node)
+ return
+
+ # iterate through cond branches
+ for statement in node.items:
+ # checking if valid cond branches (two parts)
+ if len(statement.children) != 2:
+ continue
+ predicate = statement.children[0]
+ # skip else case
+ if isinstance(predicate, Identifier) and predicate.name == 'else':
+ continue
+ new_location: Location = {'start': predicate.start_loc, 'end': predicate.end_loc}
+
+ new_mutant_true: Mutant = {
+ 'id': self._get_next_mutant_id(), 'location': new_location,
+ 'mutator_name': self.get_mutator_name(), 'replacement': '#t'}
+ new_mutant_false: Mutant = {
+ 'id': self._get_next_mutant_id(), 'location': new_location,
+ 'mutator_name': self.get_mutator_name(), 'replacement': '#f'}
+ self.list_mutants.append(new_mutant_true)
+ self.list_mutants.append(new_mutant_false)
+
+ for node_in_list in node.items:
+ self.visit_ASTNode(node_in_list)
diff --git a/racket_mutation_analysis/racket_mutation/mutators/empty_list.py b/racket_mutation_analysis/racket_mutation/mutators/empty_list.py
new file mode 100644
index 0000000..f7b6af9
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/empty_list.py
@@ -0,0 +1,39 @@
+from typing import Final
+
+from racket_mutation_analysis.racket_ast.scheme_reader import Identifier, SList
+
+from ..schema import Location, Position
+from .mutator import MutatorVisitor
+
+
+class EmptyListMutator(MutatorVisitor):
+ _list_identifiers = [
+ 'list', 'cons'
+ ]
+
+ def visit_SList(self, node: SList):
+ if (len(node.items) < 2):
+ super().visit_SList(node)
+ return
+
+ first_node = node.items[0]
+ last_node = node.items[len(node.items) - 1]
+ new_start_position: Position = {
+ 'line': first_node.start_loc['line'],
+ 'column': first_node.start_loc['column'] - 1}
+ new_end_position: Position = {
+ 'line': last_node.end_loc['line'],
+ 'column': last_node.end_loc['column'] + 1}
+ new_location: Location = {'start': new_start_position,
+ 'end': new_end_position}
+
+ if (isinstance(first_node, Identifier)
+ and (first_node.name in self._list_identifiers)):
+ self.list_mutants.append({
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': '\'()'
+ })
+
+ super().visit_SList(node)
diff --git a/racket_mutation_analysis/racket_mutation/mutators/empty_string.py b/racket_mutation_analysis/racket_mutation/mutators/empty_string.py
new file mode 100644
index 0000000..28f6acc
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/empty_string.py
@@ -0,0 +1,16 @@
+from racket_mutation_analysis.racket_ast.scheme_reader import String
+from racket_mutation_analysis.racket_mutation.schema import Location
+
+from .mutator import MutatorVisitor
+
+
+class EmptyStringMutator(MutatorVisitor):
+
+ def visit_String(self, node: String):
+ new_location: Location = {'start': node.start_loc, 'end': node.end_loc}
+ self.list_mutants.append({
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': '\"\"'
+ })
diff --git a/racket_mutation_analysis/racket_mutation/mutators/flip_bools.py b/racket_mutation_analysis/racket_mutation/mutators/flip_bools.py
new file mode 100644
index 0000000..bf2e17b
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/flip_bools.py
@@ -0,0 +1,21 @@
+from racket_mutation_analysis.racket_ast.scheme_reader import Number
+
+from ..schema import Location
+from .mutator import MutatorVisitor
+
+
+class FlipBooleansMutator(MutatorVisitor):
+
+ def visit_Boolean(self, node: Number):
+ _boolean_key = {
+ '#t': '#f',
+ '#f': '#t'
+ }
+
+ new_location: Location = {'start': node.start_loc, 'end': node.end_loc}
+ self.list_mutants.append({
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': _boolean_key[node.value]
+ })
diff --git a/racket_mutation_analysis/racket_mutation/mutators/flip_num_sign.py b/racket_mutation_analysis/racket_mutation/mutators/flip_num_sign.py
new file mode 100644
index 0000000..b046f8f
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/flip_num_sign.py
@@ -0,0 +1,16 @@
+from racket_mutation_analysis.racket_ast.scheme_reader import Number
+
+from ..schema import Location
+from .mutator import MutatorVisitor
+
+
+class FlipNumSignMutator(MutatorVisitor):
+
+ def visit_Number(self, node: Number):
+ new_location: Location = {'start': node.start_loc, 'end': node.end_loc}
+ self.list_mutants.append({
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': '(- ' + node.value + ')'
+ })
diff --git a/racket_mutation_analysis/racket_mutation/mutators/homework_mutator.py b/racket_mutation_analysis/racket_mutation/mutators/homework_mutator.py
new file mode 100644
index 0000000..5c780c8
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/homework_mutator.py
@@ -0,0 +1,71 @@
+from typing import Final
+
+from racket_mutation_analysis.racket_ast.scheme_reader import Identifier, SList
+from racket_mutation_analysis.racket_ast.utils import is_call_to_one_of
+from racket_mutation_analysis.racket_mutation.schema import Location, Mutant, Position
+
+from .mutator import MutatorVisitor
+
+
+class Homework4and5Mutator(MutatorVisitor):
+ # methods with second parameters of [List-of X] data type
+ _second_param_list_funcs: Final = {
+ 'take-n', 'drop-n', 'take-while',
+ 'drop-while', 'group-by',
+ }
+ # methods that return [List-of X] data type
+ _return_list_funcs: Final = {
+ 'take-n', 'drop-n', 'take-while', 'drop-while'
+ }
+ # functionally switchable methods
+ _switchable_func: Final = {
+ 'take-n': 'drop-n',
+ 'drop-n': 'take-n',
+ 'take-while': 'drop-while',
+ 'drop-while': 'take-while'
+ }
+
+ def visit_SList(self, node: SList):
+
+ if is_call_to_one_of(node, self._second_param_list_funcs) and len(node.items) >= 3:
+ first_node = node.items[0]
+ third_node = node.items[2]
+ empty_list_new_mutant: Mutant = {
+ 'id': self._get_next_mutant_id(),
+ 'location': {'start': third_node.start_loc,
+ 'end': third_node.end_loc},
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': '\'()'}
+ self.list_mutants.append(empty_list_new_mutant)
+
+ if is_call_to_one_of(node, self._return_list_funcs):
+ first_node = node.items[0]
+ last_node = node.items[len(node.items) - 1]
+ new_start_position: Position = {
+ 'line': first_node.start_loc['line'],
+ 'column': first_node.start_loc['column'] - 1}
+ new_end_position: Position = {
+ 'line': last_node.end_loc['line'],
+ 'column': last_node.end_loc['column'] + 1}
+ new_location: Location = {'start': new_start_position,
+ 'end': new_end_position}
+
+ empty_list_return_new_mutant: Mutant = {
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': '\'()'}
+ self.list_mutants.append(empty_list_return_new_mutant)
+
+ if is_call_to_one_of(node, self._switchable_func):
+ first_node = node.items[0]
+ assert isinstance(first_node, Identifier)
+ switchable_func_new_mutant: Mutant = {
+ 'id': self._get_next_mutant_id(),
+ 'location': {'start': first_node.start_loc,
+ 'end': first_node.end_loc},
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': self._switchable_func[first_node.name]}
+ self.list_mutants.append(switchable_func_new_mutant)
+
+ super().visit_SList(node)
diff --git a/racket_mutation_analysis/racket_mutation/mutators/if_mut.py b/racket_mutation_analysis/racket_mutation/mutators/if_mut.py
new file mode 100644
index 0000000..a09cf68
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/if_mut.py
@@ -0,0 +1,33 @@
+from racket_mutation_analysis.racket_ast.scheme_reader import Identifier, SList
+from racket_mutation_analysis.racket_ast.utils import is_call_to
+
+from ..schema import Location, Mutant
+from .mutator import MutatorVisitor
+
+
+class IfMutator(MutatorVisitor):
+
+ def visit_SList(self, node: SList):
+ # length is four (if, boolean statement, first case, second case)
+ if not is_call_to(node, 'if') or len(node.items) != 4:
+ super().visit_SList(node)
+ return
+
+ boolean_node = node.items[1]
+ new_location: Location = {'start': boolean_node.start_loc, 'end': boolean_node.end_loc}
+
+ new_mutant_false: Mutant = {
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': '#f'}
+ new_mutant_true: Mutant = {
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': '#t'}
+ self.list_mutants.append(new_mutant_true)
+ self.list_mutants.append(new_mutant_false)
+
+ for node_in_list in node.items:
+ self.visit_ASTNode(node_in_list)
diff --git a/racket_mutation_analysis/racket_mutation/mutators/logical_mut.py b/racket_mutation_analysis/racket_mutation/mutators/logical_mut.py
new file mode 100644
index 0000000..3704f0a
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/logical_mut.py
@@ -0,0 +1,28 @@
+from typing import Final
+
+from racket_mutation_analysis.racket_ast.scheme_reader import Identifier, SList
+from racket_mutation_analysis.racket_ast.utils import is_call_to_one_of
+
+from ..schema import Location, Mutant
+from .mutator import MutatorVisitor
+
+
+class LogicalMutator(MutatorVisitor):
+ _logical_substitutions: Final = {
+ 'and': 'or',
+ 'or': 'and'
+ }
+
+ def visit_SList(self, node: SList):
+ if is_call_to_one_of(node, self._logical_substitutions):
+ first_node = node.items[0]
+ assert isinstance(first_node, Identifier)
+ new_location: Location = {'start': first_node.start_loc, 'end': first_node.end_loc}
+ new_mutant: Mutant = {
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': self._logical_substitutions[first_node.name]}
+ self.list_mutants.append(new_mutant)
+
+ super().visit_SList(node)
diff --git a/racket_mutation_analysis/racket_mutation/mutators/mutator.py b/racket_mutation_analysis/racket_mutation/mutators/mutator.py
new file mode 100644
index 0000000..f916451
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/mutator.py
@@ -0,0 +1,22 @@
+import itertools
+from abc import ABCMeta
+from typing import Final
+
+from racket_mutation_analysis.racket_ast.visitor import Visitor
+
+from ..schema import Mutant
+
+
+class MutatorVisitor(Visitor, metaclass=ABCMeta):
+ _mutant_counter: Final = itertools.count(1)
+
+ def __init__(self) -> None:
+ self.list_mutants: list[Mutant] = []
+
+ @classmethod
+ def get_mutator_name(cls) -> str:
+ return cls.__name__
+
+ @classmethod
+ def _get_next_mutant_id(cls) -> str:
+ return str(next(cls._mutant_counter))
diff --git a/racket_mutation_analysis/racket_mutation/mutators/num_comparison_mutator.py b/racket_mutation_analysis/racket_mutation/mutators/num_comparison_mutator.py
new file mode 100644
index 0000000..138e404
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/num_comparison_mutator.py
@@ -0,0 +1,36 @@
+import time
+from typing import Final, List
+
+from racket_mutation_analysis.racket_ast.scheme_reader import Identifier, SList
+from racket_mutation_analysis.racket_ast.string_visitor import ToStrVisitor
+from racket_mutation_analysis.racket_ast.utils import is_call_to_one_of
+
+from ..schema import Location
+from .mutator import MutatorVisitor
+
+
+class NumberComparisonMutator(MutatorVisitor):
+ _comparisons: List[str] = [
+ '>', '>=', '<', '<=', '='
+ ]
+
+ def visit_SList(self, node: SList):
+ if not is_call_to_one_of(node, self._comparisons):
+ super().visit_SList(node)
+ return
+
+ first_node = node.items[0]
+ new_location: Location = {'start': first_node.start_loc, 'end': first_node.end_loc}
+
+ string_visitor = ToStrVisitor()
+ string_visitor.visit(first_node)
+ string_rep_of_first_node = string_visitor.result.strip()
+
+ for new_operator in self._comparisons:
+ if (new_operator != string_rep_of_first_node):
+ self.list_mutants.append({
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': new_operator
+ })
diff --git a/racket_mutation_analysis/racket_mutation/mutators/num_literals_mut.py b/racket_mutation_analysis/racket_mutation/mutators/num_literals_mut.py
new file mode 100644
index 0000000..00c76e3
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/num_literals_mut.py
@@ -0,0 +1,33 @@
+from racket_mutation_analysis.racket_ast.scheme_reader import Number
+from racket_mutation_analysis.racket_mutation.schema import Location
+
+from .mutator import MutatorVisitor
+
+
+class NumLiteralsMutator(MutatorVisitor):
+ _replacements = [
+ '0', '-1', '1'
+ ]
+
+ def visit_Number(self, node: Number):
+ new_location: Location = {'start': node.start_loc, 'end': node.end_loc}
+ for new_num in self._replacements:
+ if (new_num != int(float(node.value))):
+ self.list_mutants.append({
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': new_num
+ })
+ self.list_mutants.append({
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': str(float(node.value) - 1)
+ })
+ self.list_mutants.append({
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': str(float(node.value) + 1)
+ })
diff --git a/racket_mutation_analysis/racket_mutation/mutators/wrap_with_not.py b/racket_mutation_analysis/racket_mutation/mutators/wrap_with_not.py
new file mode 100644
index 0000000..b03c92a
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/mutators/wrap_with_not.py
@@ -0,0 +1,45 @@
+from typing import Final
+
+from racket_mutation_analysis.racket_ast.scheme_reader import Identifier, LiteralVal, SList
+from racket_mutation_analysis.racket_ast.string_visitor import ToStrVisitor
+from racket_mutation_analysis.racket_ast.utils import is_call_to_one_of
+
+from ..schema import Location, Position
+from .mutator import MutatorVisitor
+
+
+class WrapWithNotMutator(MutatorVisitor):
+ _substitutions: Final = {
+ 'and', 'or'
+ }
+
+ def visit_SList(self, node: SList):
+ if not is_call_to_one_of(node, self._substitutions):
+ super().visit_SList(node)
+ return
+
+ first_node = node.items[0]
+ last_node = node.items[len(node.items) - 1]
+ new_start_position: Position = {
+ 'line': first_node.start_loc['line'],
+ 'column': first_node.start_loc['column']}
+ new_end_position: Position = {
+ 'line': last_node.end_loc['line'],
+ 'column': last_node.end_loc['column']}
+ new_location: Location = {'start': new_start_position,
+ 'end': new_end_position}
+
+ node_str = ''
+ str_visitor = ToStrVisitor()
+ for child in node.children:
+ str_visitor.visit_ASTNode(child)
+ node_str += str_visitor.result
+
+ self.list_mutants.append({
+ 'id': self._get_next_mutant_id(),
+ 'location': new_location,
+ 'mutator_name': self.get_mutator_name(),
+ 'replacement': 'not (' + node_str.strip() + ')'
+ })
+
+ super().visit_SList(node)
diff --git a/racket_mutation_analysis/racket_mutation/schema.py b/racket_mutation_analysis/racket_mutation/schema.py
new file mode 100644
index 0000000..d8e85c6
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/schema.py
@@ -0,0 +1,77 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Literal, TypeAlias, TypedDict
+
+if TYPE_CHECKING:
+ from typing_extensions import Required
+
+
+# Dict of file paths to Mutations
+GeneratedMutants: TypeAlias = dict[str, 'Mutations']
+
+
+class Mutations(TypedDict):
+ # The original, unmutated file contents
+ original: str
+ mutants: list[AppliedMutant]
+
+
+class Mutant(TypedDict):
+ id: str
+ location: Location
+ mutator_name: str
+ replacement: str
+
+
+class AppliedMutant(Mutant):
+ mutated_code: str
+
+
+class Location(TypedDict):
+ start: Position
+ end: Position
+
+
+class Position(TypedDict):
+ line: int
+ column: int
+
+
+# From https://github.com/stryker-mutator/mutation-testing-elements/blob
+# /master/packages/report-schema/src/mutation-testing-report-schema.json
+# Note that we're making some keys required here even if they aren't
+# required by the spec. This will simplify type checking for our use case.
+
+
+class MutationTestResult(TypedDict, total=False):
+ schemaVersion: Required[str]
+ thresholds: Required[Thresholds]
+ files: Required[dict[str, FileResultDictionary]]
+ projectRoot: str
+
+
+class Thresholds(TypedDict):
+ high: int
+ low: int
+
+
+class FileResultDictionary(TypedDict):
+ language: str
+ source: str
+ mutants: list[MutantResult]
+
+
+class MutantResult(TypedDict):
+ id: str
+ mutatorName: str
+ location: Location
+ status: MutantStatus
+ mutatorName: str
+ replacement: str
+ killedBy: list[str]
+
+
+MutantStatus: TypeAlias = Literal[
+ "Killed", "Survived", "NoCoverage", "CompileError",
+ "RuntimeError", "Timeout", "Ignored"
+]
diff --git a/racket_mutation_analysis/racket_mutation/test/__init__.py b/racket_mutation_analysis/racket_mutation/test/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/racket_mutation_analysis/racket_mutation/test/mutant_base_test.py b/racket_mutation_analysis/racket_mutation/test/mutant_base_test.py
new file mode 100644
index 0000000..68da0f3
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/mutant_base_test.py
@@ -0,0 +1,56 @@
+import io
+import subprocess
+import tempfile
+import unittest
+from typing import List, Sequence, Type
+
+from racket_mutation_analysis.racket_ast.scheme_reader import SchemeReader
+from racket_mutation_analysis.racket_mutation.mutant_generator import MutantGenerator
+from racket_mutation_analysis.racket_mutation.mutators.mutator import MutatorVisitor
+
+
+class MutantBaseTest(unittest.TestCase):
+ def setUp(self):
+ super().setUp()
+ self.maxDiff = None
+
+ # compares generated mutants for given input to given expected mutants
+ def mutant_diff_test(self,
+ program_src: str, expected: List[str],
+ mutator_classes: Sequence[Type[MutatorVisitor]]):
+
+ actual = self.generate_mutants(program_src, mutator_classes)
+ self.assertEqual(expected, actual)
+
+ # generates mutants for the given input with the given mutators
+ def generate_mutants(self,
+ program_src: str,
+ mutator_classes: Sequence[Type[MutatorVisitor]]):
+
+ ast = SchemeReader().read_file(io.StringIO(program_src))
+ input_array = program_src.splitlines(keepends=True)
+ mutant_generator = MutantGenerator(ast, input_array, mutator_classes)
+ actual_tuple = mutant_generator.run()
+ actual = []
+
+ for mutant, mutated_file in actual_tuple:
+ actual.append(mutated_file)
+
+ return actual
+
+ def complete_test(self,
+ program_src: str,
+ mutator_classes: Sequence[Type[MutatorVisitor]],
+ # first list is mutated programs, second is expected outputs
+ expected: List[tuple[str, str]],
+ test_case: str = ''):
+ actual = self.generate_mutants(program_src, mutator_classes)
+ self.assertEqual([item[0] for item in expected], actual)
+
+ for mutated_program, expected_output in expected:
+ with tempfile.NamedTemporaryFile() as temp:
+ temp.write(b'#lang racket\n' + mutated_program.encode('utf-8')
+ + b'\n' + test_case.encode('utf-8'))
+ temp.seek(0)
+ output = subprocess.run(['racket', temp.name], text=True, capture_output=True)
+ self.assertEqual(expected_output, output.stdout.strip())
diff --git a/racket_mutation_analysis/racket_mutation/test/test_arithmetic_deletion.py b/racket_mutation_analysis/racket_mutation/test/test_arithmetic_deletion.py
new file mode 100644
index 0000000..16417d7
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_arithmetic_deletion.py
@@ -0,0 +1,71 @@
+from racket_mutation_analysis.racket_mutation.mutators.arithmetic_deletion import (
+ ArithmeticDeletionMutator
+)
+
+from . import mutant_base_test
+
+
+class TestArithmeticDeletionMutator(mutant_base_test.MutantBaseTest):
+ def test_addition_deletion(self):
+ self.mutant_diff_test(
+ '(+ 1 2)',
+ ['1',
+ '2'],
+ [ArithmeticDeletionMutator],
+ )
+
+ def test_many_addition_deletion(self):
+ self.mutant_diff_test(
+ '(+ f (+ (+ (+ a b) c) d e))',
+ ['f',
+ '(+ (+ (+ a b) c) d e)',
+ '(+ f (+ (+ a b) c))',
+ '(+ f d)',
+ '(+ f e)',
+ '(+ f (+ (+ a b) d e))',
+ '(+ f (+ c d e))',
+ '(+ f (+ (+ a c) d e))',
+ '(+ f (+ (+ b c) d e))'],
+ [ArithmeticDeletionMutator]
+ )
+
+ def test_mult_and_div_deletion(self):
+ self.mutant_diff_test(
+ '(/ (* 5 b) 1)',
+ ['(* 5 b)', '1', '(/ 5 1)', '(/ b 1)'],
+ [ArithmeticDeletionMutator]
+ )
+
+ def test_all_arithmetic_deletion(self):
+ self.mutant_diff_test(
+ '(define (math a b c d)\n (* (/ c (+ a b)) d))',
+ ['(define (math a b c d)\n (/ c (+ a b)))',
+ '(define (math a b c d)\n d)',
+ '(define (math a b c d)\n (* c d))',
+ '(define (math a b c d)\n (* (+ a b) d))',
+ '(define (math a b c d)\n (* (/ c a) d))',
+ '(define (math a b c d)\n (* (/ c b) d))'],
+ [ArithmeticDeletionMutator]
+ )
+
+ def test_all_arithmetic_2_deletion(self):
+ self.mutant_diff_test(
+ '(define (foo a b c) (- 6 (/ (+ 1 2 (* 3 4)) 5)))',
+ ['(define (foo a b c) 6)',
+ '(define (foo a b c) (/ (+ 1 2 (* 3 4)) 5))',
+ '(define (foo a b c) (- 6 (+ 1 2 (* 3 4))))',
+ '(define (foo a b c) (- 6 5))',
+ '(define (foo a b c) (- 6 (/ 1 5)))',
+ '(define (foo a b c) (- 6 (/ 2 5)))',
+ '(define (foo a b c) (- 6 (/ (* 3 4) 5)))',
+ '(define (foo a b c) (- 6 (/ (+ 1 2 3) 5)))',
+ '(define (foo a b c) (- 6 (/ (+ 1 2 4) 5)))'],
+ [ArithmeticDeletionMutator]
+ )
+
+ def test_empty_list_handling(self) -> None:
+ self.mutant_diff_test(
+ "'()",
+ [],
+ [ArithmeticDeletionMutator]
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_arithmetic_mutators.py b/racket_mutation_analysis/racket_mutation/test/test_arithmetic_mutators.py
new file mode 100644
index 0000000..811da23
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_arithmetic_mutators.py
@@ -0,0 +1,146 @@
+from racket_mutation_analysis.racket_mutation.mutators.arithmetic_mut import ArithmeticMutator
+
+from . import mutant_base_test
+
+
+class TestArithmeticOpMutators(mutant_base_test.MutantBaseTest):
+ def test_addition(self):
+ self.mutant_diff_test(
+ '(+ 1 2)',
+ ['(- 1 2)'],
+ [ArithmeticMutator],
+ )
+
+ def test_many_addition(self):
+ self.mutant_diff_test(
+ '(+ f (+ (+ (a b) c) d e))',
+ ['(- f (+ (+ (a b) c) d e))',
+ '(+ f (- (+ (a b) c) d e))',
+ '(+ f (+ (- (a b) c) d e))'],
+ [ArithmeticMutator]
+ )
+
+ def test_subtraction(self):
+ self.mutant_diff_test(
+ '(- a b)',
+ ['(+ a b)'],
+ [ArithmeticMutator],
+ )
+
+ def test_many_subtraction(self):
+ self.mutant_diff_test(
+ '(- f (- (- (a b) c) d e))',
+ ['(+ f (- (- (a b) c) d e))',
+ '(- f (+ (- (a b) c) d e))',
+ '(- f (- (+ (a b) c) d e))'],
+ [ArithmeticMutator]
+ )
+
+ def test_multiplication(self):
+ self.mutant_diff_test(
+ '(* a b)',
+ ['(/ a b)'],
+ [ArithmeticMutator],
+ )
+
+ def test_many_multiplication(self):
+ self.mutant_diff_test(
+ '(* f (* (* (a b) c) d e))',
+ ['(/ f (* (* (a b) c) d e))',
+ '(* f (/ (* (a b) c) d e))',
+ '(* f (* (/ (a b) c) d e))', ],
+ [ArithmeticMutator]
+ )
+
+ def test_division(self):
+ self.mutant_diff_test(
+ '(/ 1 2)',
+ ['(* 1 2)'],
+ [ArithmeticMutator],
+ )
+
+ def test_many_division(self):
+ self.mutant_diff_test(
+ '(/ f (/ (/ (a b) c) d e))',
+ ['(* f (/ (/ (a b) c) d e))',
+ '(/ f (* (/ (a b) c) d e))',
+ '(/ f (/ (* (a b) c) d e))', ],
+ [ArithmeticMutator]
+ )
+
+ def test_add_and_sub(self):
+ self.mutant_diff_test(
+ '(+ (- 5 b))',
+ ['(- (- 5 b))', '(+ (+ 5 b))'],
+ [ArithmeticMutator]
+ )
+
+ def test_mult_and_div(self):
+ self.mutant_diff_test(
+ '(/ (* 5 b))',
+ ['(* (* 5 b))', '(/ (/ 5 b))'],
+ [ArithmeticMutator]
+ )
+
+ def test_all_arithmetic(self):
+ self.mutant_diff_test(
+ '(define (math a b c d)\n (* (/ c (+ a b)) d))',
+ ['(define (math a b c d)\n (/ (/ c (+ a b)) d))',
+ '(define (math a b c d)\n (* (* c (+ a b)) d))',
+ '(define (math a b c d)\n (* (/ c (- a b)) d))'],
+ [ArithmeticMutator]
+ )
+
+ # mutators are applied outside in
+ def test_all_arithmetic_2(self):
+ self.mutant_diff_test(
+ '(define (foo a b c) (- 6 (/ (+ 1 2 (* 3 4)) 5)))',
+ ['(define (foo a b c) (+ 6 (/ (+ 1 2 (* 3 4)) 5)))',
+ '(define (foo a b c) (- 6 (* (+ 1 2 (* 3 4)) 5)))',
+ '(define (foo a b c) (- 6 (/ (- 1 2 (* 3 4)) 5)))',
+ '(define (foo a b c) (- 6 (/ (+ 1 2 (/ 3 4)) 5)))'],
+ [ArithmeticMutator]
+ )
+
+ def test_run_simple_arithmetic(self):
+ self.complete_test(
+ '(+ 1 2)',
+ [ArithmeticMutator],
+ [('(- 1 2)', '-1')]
+ )
+
+ def test_run_simple_arithmetic_method(self):
+ self.complete_test(
+ '(define (foo a b) \n (+ a b))',
+ [ArithmeticMutator],
+ [('(define (foo a b) \n (- a b))', '-1')],
+ '(foo 1 2)'
+ )
+
+ def test_all_run_arithmetic(self):
+ self.complete_test(
+ '(define (math a b c d)\n (* (/ c (+ a b)) d))',
+ [ArithmeticMutator],
+ [('(define (math a b c d)\n (/ (/ c (+ a b)) d))', '1/4'),
+ ('(define (math a b c d)\n (* (* c (+ a b)) d))', '36'),
+ ('(define (math a b c d)\n (* (/ c (- a b)) d))', '-12')],
+ '(math 1 2 3 4)'
+ )
+
+ def test_all_2_run_arithmetic(self):
+ self.complete_test(
+ '(define (foo a b c) (- 6 (/ (+ 1 2 (* 3 4)) 5)))',
+ [ArithmeticMutator],
+ [('(define (foo a b c) (+ 6 (/ (+ 1 2 (* 3 4)) 5)))', '9'),
+ ('(define (foo a b c) (- 6 (* (+ 1 2 (* 3 4)) 5)))', '-69'),
+ ('(define (foo a b c) (- 6 (/ (- 1 2 (* 3 4)) 5)))', '43/5'),
+ ('(define (foo a b c) (- 6 (/ (+ 1 2 (/ 3 4)) 5)))', '21/4')],
+ '(foo 1 2 3)'
+ )
+
+ def test_empty_list_handling(self) -> None:
+ self.mutant_diff_test(
+ "'()",
+ [],
+ [ArithmeticMutator]
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_bool_func_to_bool.py b/racket_mutation_analysis/racket_mutation/test/test_bool_func_to_bool.py
new file mode 100644
index 0000000..390816f
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_bool_func_to_bool.py
@@ -0,0 +1,83 @@
+from racket_mutation_analysis.racket_mutation.mutators.bool_func_to_bool import (
+ BoolFuncToBoolMutator
+)
+
+from . import mutant_base_test
+
+
+class TestBoolFuncToBool(mutant_base_test.MutantBaseTest):
+ def test_one_bool_func(self):
+ self.mutant_diff_test(
+ '(and a b)',
+ ['#t', '#f'],
+ [BoolFuncToBoolMutator],
+ )
+
+ def test_many_bool_func(self):
+ self.mutant_diff_test(
+ '(and (not #t) (or #f #t))',
+ ['#t', '#f',
+ '(and #t (or #f #t))',
+ '(and #f (or #f #t))',
+ '(and (not #t) #t)',
+ '(and (not #t) #f)'],
+ [BoolFuncToBoolMutator]
+ )
+
+ def test_defined_predicate(self):
+ self.mutant_diff_test(
+ '(string=? hi bye)',
+ ['#t', '#f'],
+ [BoolFuncToBoolMutator]
+ )
+
+ def test_defined_predicate_in_func(self):
+ self.mutant_diff_test(
+ '(define (foo a b) (if (string=? a b) a b))',
+ ['(define (foo a b) (if #t a b))',
+ '(define (foo a b) (if #f a b))'],
+ [BoolFuncToBoolMutator],
+ )
+
+ def test_new_predicate(self):
+ self.mutant_diff_test(
+ '(if (car? c) 3 4)',
+ ['(if #t 3 4)',
+ '(if #f 3 4)'],
+ [BoolFuncToBoolMutator]
+ )
+
+ def test_one_bool_func_run(self):
+ self.complete_test(
+ '(and #t #f)',
+ [BoolFuncToBoolMutator],
+ [('#t', '#t'),
+ ('#f', '#f')],
+ )
+
+ def test_many_bool_func_run(self):
+ self.complete_test(
+ '(and (not #t) (or #f #t))',
+ [BoolFuncToBoolMutator],
+ [('#t', '#t'),
+ ('#f', '#f'),
+ ('(and #t (or #f #t))', '#t'),
+ ('(and #f (or #f #t))', '#f'),
+ ('(and (not #t) #t)', '#f'),
+ ('(and (not #t) #f)', '#f')]
+ )
+
+ def test_defined_predicate_run(self):
+ self.complete_test(
+ '(string=? \"hi\" \"bye\")',
+ [BoolFuncToBoolMutator],
+ [('#t', '#t'),
+ ('#f', '#f')]
+ )
+
+ def test_empty_list_handling(self) -> None:
+ self.mutant_diff_test(
+ "'()",
+ [],
+ [BoolFuncToBoolMutator]
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_bool_params_to_bool.py b/racket_mutation_analysis/racket_mutation/test/test_bool_params_to_bool.py
new file mode 100644
index 0000000..6aa3606
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_bool_params_to_bool.py
@@ -0,0 +1,84 @@
+from racket_mutation_analysis.racket_mutation.mutators.bool_params_to_bool import BoolParamsToBool
+
+from . import mutant_base_test
+
+
+class TestBoolParamsToBool(mutant_base_test.MutantBaseTest):
+ def test_and_bool_param(self):
+ self.mutant_diff_test(
+ '(and a b)',
+ ['(and #t b)',
+ '(and #f b)',
+ '(and a #t)',
+ '(and a #f)'],
+ [BoolParamsToBool])
+
+ def test_or_mutator_param(self):
+ self.mutant_diff_test(
+ '(or a b)',
+ ['(or #t b)',
+ '(or #f b)',
+ '(or a #t)',
+ '(or a #f)'],
+ [BoolParamsToBool])
+
+ def test_complete_bool_param(self):
+ self.mutant_diff_test(
+ '(define (logical a b c) (or (and a b) c))',
+ ['(define (logical a b c) (or #t c))',
+ '(define (logical a b c) (or #f c))',
+ '(define (logical a b c) (or (and a b) #t))',
+ '(define (logical a b c) (or (and a b) #f))',
+ '(define (logical a b c) (or (and #t b) c))',
+ '(define (logical a b c) (or (and #f b) c))',
+ '(define (logical a b c) (or (and a #t) c))',
+ '(define (logical a b c) (or (and a #f) c))'],
+ [BoolParamsToBool])
+
+ def test_non_applicable_mutator_bool_param(self):
+ self.mutant_diff_test(
+ '(+ 1 2)',
+ [],
+ [BoolParamsToBool])
+
+ def test_complete_bool_param_run1(self):
+ self.complete_test(
+ '(define (logical a b c) (or (and a b) c))',
+ [BoolParamsToBool],
+ [('(define (logical a b c) (or #t c))', '#t'),
+ ('(define (logical a b c) (or #f c))', '#t'),
+ ('(define (logical a b c) (or (and a b) #t))', '#t'),
+ ('(define (logical a b c) (or (and a b) #f))', '#t'),
+ ('(define (logical a b c) (or (and #t b) c))', '#t'),
+ ('(define (logical a b c) (or (and #f b) c))', '#t'),
+ ('(define (logical a b c) (or (and a #t) c))', '#t'),
+ ('(define (logical a b c) (or (and a #f) c))', '#t')],
+ '(logical #t #t #t)')
+
+ def test_complete_bool_param_run2(self):
+ self.complete_test(
+ '(define (logical a b c) (or (and a b) c))',
+ [BoolParamsToBool],
+ [('(define (logical a b c) (or #t c))', '#t'),
+ ('(define (logical a b c) (or #f c))', '#t'),
+ ('(define (logical a b c) (or (and a b) #t))', '#t'),
+ ('(define (logical a b c) (or (and a b) #f))', '#f'),
+ ('(define (logical a b c) (or (and #t b) c))', '#t'),
+ ('(define (logical a b c) (or (and #f b) c))', '#t'),
+ ('(define (logical a b c) (or (and a #t) c))', '#t'),
+ ('(define (logical a b c) (or (and a #f) c))', '#t')],
+ '(logical #t #f #t)')
+
+ def test_complete_bool_param_run3(self):
+ self.complete_test(
+ '(define (logical a b c) (or (and a b) c))',
+ [BoolParamsToBool],
+ [('(define (logical a b c) (or #t c))', '#t'),
+ ('(define (logical a b c) (or #f c))', '#f'),
+ ('(define (logical a b c) (or (and a b) #t))', '#t'),
+ ('(define (logical a b c) (or (and a b) #f))', '#t'),
+ ('(define (logical a b c) (or (and #t b) c))', '#t'),
+ ('(define (logical a b c) (or (and #f b) c))', '#f'),
+ ('(define (logical a b c) (or (and a #t) c))', '#t'),
+ ('(define (logical a b c) (or (and a #f) c))', '#f')],
+ '(logical #t #t #f)')
diff --git a/racket_mutation_analysis/racket_mutation/test/test_cond_mutators.py b/racket_mutation_analysis/racket_mutation/test/test_cond_mutators.py
new file mode 100644
index 0000000..dc623ae
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_cond_mutators.py
@@ -0,0 +1,105 @@
+from racket_mutation_analysis.racket_mutation.mutators.cond_mut import CondMutator
+
+from . import mutant_base_test
+
+
+class TestCondMutators(mutant_base_test.MutantBaseTest):
+ def test_cond_only_else_mutator(self):
+ self.mutant_diff_test(
+ '(cond[else 2])',
+ [],
+ [CondMutator]
+ )
+
+ def test_series_cond_expression_mutator(self):
+ self.mutant_diff_test(
+ '(cond[(< a b) 2][(> a b) 3][(= a b) 4])',
+ ['(cond[#t 2][(> a b) 3][(= a b) 4])', '(cond[#f 2][(> a b) 3][(= a b) 4])',
+ '(cond[(< a b) 2][#t 3][(= a b) 4])', '(cond[(< a b) 2][#f 3][(= a b) 4])',
+ '(cond[(< a b) 2][(> a b) 3][#t 4])', '(cond[(< a b) 2][(> a b) 3][#f 4])'],
+ [CondMutator]
+ )
+
+ def test_cond_true_mutator(self):
+ self.mutant_diff_test(
+ '(cond[#t 2][else #f])',
+ ['(cond[#t 2][else #f])',
+ '(cond[#f 2][else #f])'],
+ [CondMutator]
+ )
+
+ def test_cond_false_mutator(self):
+ self.mutant_diff_test(
+ '(cond [#f #t] [else 2])',
+ ['(cond [#t #t] [else 2])',
+ '(cond [#f #t] [else 2])'],
+ [CondMutator]
+ )
+
+ def test_cond_expression_mutator(self):
+ self.mutant_diff_test(
+ '(cond[(= a b) 2][else #t])',
+ ['(cond[#t 2][else #t])', '(cond[#f 2][else #t])'],
+ [CondMutator]
+ )
+
+ def test_complete_cond(self):
+ self.mutant_diff_test(
+ '(define (foo a b c) (cond [(> a b) a] [(> b a) b] [else c]))',
+ ['(define (foo a b c) (cond [#t a] [(> b a) b] [else c]))',
+ '(define (foo a b c) (cond [#f a] [(> b a) b] [else c]))',
+ '(define (foo a b c) (cond [(> a b) a] [#t b] [else c]))',
+ '(define (foo a b c) (cond [(> a b) a] [#f b] [else c]))'],
+ [CondMutator]
+ )
+
+ def test_complete(self):
+ self.mutant_diff_test(
+ '(define (foo a b c)\n(cond\n[(> a b) a]\n[(> b a) b]\n[else c]))',
+ ['(define (foo a b c)\n(cond\n[#t a]\n[(> b a) b]\n[else c]))',
+ '(define (foo a b c)\n(cond\n[#f a]\n[(> b a) b]\n[else c]))',
+ '(define (foo a b c)\n(cond\n[(> a b) a]\n[#t b]\n[else c]))',
+ '(define (foo a b c)\n(cond\n[(> a b) a]\n[#f b]\n[else c]))'],
+ [CondMutator]
+ )
+
+ def test_non_applicable_mutator(self):
+ self.mutant_diff_test(
+ '(if #t 2 3)',
+ [],
+ [CondMutator]
+ )
+
+ def test_empty_list_handling(self) -> None:
+ self.mutant_diff_test(
+ "'()",
+ [],
+ [CondMutator]
+ )
+
+ def test_cond_true_mutator_run(self):
+ self.complete_test(
+ '(cond[#t 2][else #f])',
+ [CondMutator],
+ [('(cond[#t 2][else #f])', '2'),
+ ('(cond[#f 2][else #f])', '#f')]
+ )
+
+ def test_cond_false_mutator_run(self):
+ self.complete_test(
+ '(cond [#f #t] [else 2])',
+ [CondMutator],
+ [('(cond [#t #t] [else 2])', '#t'),
+ ('(cond [#f #t] [else 2])', '2')]
+ )
+
+ def test_complete_cond_run(self):
+ self.complete_test(
+ '(define (foo a b c) (cond [(> a b) a] [(> b a) b] [else c]))',
+ [CondMutator],
+ [('(define (foo a b c) (cond [#t a] [(> b a) b] [else c]))', '1'),
+ ('(define (foo a b c) (cond [#f a] [(> b a) b] [else c]))', '2'),
+ ('(define (foo a b c) (cond [(> a b) a] [#t b] [else c]))', '2'),
+ ('(define (foo a b c) (cond [(> a b) a] [#f b] [else c]))', '3')],
+ '(foo 1 2 3)'
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_empty_list.py b/racket_mutation_analysis/racket_mutation/test/test_empty_list.py
new file mode 100644
index 0000000..4845755
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_empty_list.py
@@ -0,0 +1,47 @@
+from racket_mutation_analysis.racket_mutation.mutators.empty_list import EmptyListMutator
+
+from . import mutant_base_test
+
+
+class TestEmptyListMutator(mutant_base_test.MutantBaseTest):
+ def test_list_only(self):
+ self.mutant_diff_test(
+ '(list 1 2)',
+ ['\'()'],
+ [EmptyListMutator],
+ )
+
+ def test_list_only_with_cons(self):
+ self.mutant_diff_test(
+ '(cons 1 2)',
+ ['\'()'],
+ [EmptyListMutator],
+ )
+
+ def test_already_empty_list(self):
+ self.mutant_diff_test(
+ '\'()',
+ [],
+ [EmptyListMutator]
+ )
+
+ def test_non_applciable_empty_list(self):
+ self.mutant_diff_test(
+ '(+ 1 2)',
+ [],
+ [EmptyListMutator]
+ )
+
+ def test_list_only_run(self):
+ self.complete_test(
+ '(list 1 2)',
+ [EmptyListMutator],
+ [('\'()', '\'()')]
+ )
+
+ def test_list_only_with_cons_run(self):
+ self.complete_test(
+ '(cons 1 2)',
+ [EmptyListMutator],
+ [('\'()', '\'()')]
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_empty_string.py b/racket_mutation_analysis/racket_mutation/test/test_empty_string.py
new file mode 100644
index 0000000..d6dc6ce
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_empty_string.py
@@ -0,0 +1,26 @@
+from racket_mutation_analysis.racket_mutation.mutators.empty_string import EmptyStringMutator
+
+from . import mutant_base_test
+
+
+class TestEmptyStringMutator(mutant_base_test.MutantBaseTest):
+ def test_hello_empty_string(self):
+ self.mutant_diff_test(
+ '\"hello\"',
+ ['\"\"'],
+ [EmptyStringMutator],
+ )
+
+ def test_already_empty_string(self):
+ self.mutant_diff_test(
+ '\"\"',
+ ['\"\"'],
+ [EmptyStringMutator]
+ )
+
+ def test_run_simple_arithmetic_flip_sign(self):
+ self.complete_test(
+ '\"hello\"',
+ [EmptyStringMutator],
+ [('\"\"', '\"\"')]
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_flip_bools.py b/racket_mutation_analysis/racket_mutation/test/test_flip_bools.py
new file mode 100644
index 0000000..328306d
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_flip_bools.py
@@ -0,0 +1,35 @@
+from racket_mutation_analysis.racket_mutation.mutators.flip_bools import FlipBooleansMutator
+
+from . import mutant_base_test
+
+
+class TestFlipNumSignMutator(mutant_base_test.MutantBaseTest):
+ def test_true_flip(self):
+ self.mutant_diff_test(
+ '#t',
+ ['#f'],
+ [FlipBooleansMutator],
+ )
+
+ def test_false_flip(self):
+ self.mutant_diff_test(
+ '#f',
+ ['#t'],
+ [FlipBooleansMutator]
+ )
+
+ def test_run_unapplicable_boolean_flip(self):
+ self.mutant_diff_test(
+ '(/ 4.0 2.0)',
+ [],
+ [FlipBooleansMutator]
+ )
+
+ def test_run_simple_expression_flip_booleans(self):
+ self.complete_test(
+ '(if #t #f #t)',
+ [FlipBooleansMutator],
+ [('(if #f #f #t)', '#t'),
+ ('(if #t #t #t)', '#t'),
+ ('(if #t #f #f)', '#f')]
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_flip_num_sign.py b/racket_mutation_analysis/racket_mutation/test/test_flip_num_sign.py
new file mode 100644
index 0000000..a32162a
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_flip_num_sign.py
@@ -0,0 +1,36 @@
+from racket_mutation_analysis.racket_mutation.mutators.flip_num_sign import FlipNumSignMutator
+
+from . import mutant_base_test
+
+
+class TestFlipNumSignMutator(mutant_base_test.MutantBaseTest):
+ def test_addition_flip_num(self):
+ self.mutant_diff_test(
+ '(+ 1 2)',
+ ['(+ (- 1) 2)',
+ '(+ 1 (- 2))'],
+ [FlipNumSignMutator],
+ )
+
+ def test_non_literal_flip_sign(self):
+ self.mutant_diff_test(
+ '(/ a b)',
+ [],
+ [FlipNumSignMutator]
+ )
+
+ def test_run_simple_arithmetic_flip_sign(self):
+ self.complete_test(
+ '(+ 1 2)',
+ [FlipNumSignMutator],
+ [('(+ (- 1) 2)', '1'),
+ ('(+ 1 (- 2))', '-1')]
+ )
+
+ def test_run_simple_floating_point_flip_sign(self):
+ self.complete_test(
+ '(/ 4.0 2.0)',
+ [FlipNumSignMutator],
+ [('(/ (- 4.0) 2.0)', '-2.0'),
+ ('(/ 4.0 (- 2.0))', '-2.0')]
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_homework_mutator.py b/racket_mutation_analysis/racket_mutation/test/test_homework_mutator.py
new file mode 100644
index 0000000..9ea3d58
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_homework_mutator.py
@@ -0,0 +1,55 @@
+from racket_mutation_analysis.racket_mutation.mutators.homework_mutator import Homework4and5Mutator
+
+from . import mutant_base_test
+
+
+class TestArithmeticDeletionMutator(mutant_base_test.MutantBaseTest):
+ def test_mutants_for_take_n(self):
+ self.mutant_diff_test(
+ '(take-n 5 (list 1 2 3))',
+ ["(take-n 5 '())",
+ "'()",
+ '(drop-n 5 (list 1 2 3))'],
+ [Homework4and5Mutator],
+ )
+
+ def test_mutants_for_drop_n(self):
+ self.mutant_diff_test(
+ '(drop-n 5 (list 1 2 3))',
+ ["(drop-n 5 '())",
+ "'()",
+ '(take-n 5 (list 1 2 3))'],
+ [Homework4and5Mutator],
+ )
+
+ def test_mutants_for_take_while(self):
+ self.mutant_diff_test(
+ '(take-while even? (list 1 2 3))',
+ ["(take-while even? '())",
+ "'()",
+ '(drop-while even? (list 1 2 3))'],
+ [Homework4and5Mutator],
+ )
+
+ def test_mutants_for_drop_while(self):
+ self.mutant_diff_test(
+ '(drop-while even? (list 1 2 3))',
+ ["(drop-while even? '())",
+ "'()",
+ '(take-while even? (list 1 2 3))'],
+ [Homework4and5Mutator],
+ )
+
+ def test_mutants_for_group_by(self):
+ self.mutant_diff_test(
+ '(group-by = (list 1 2 3))',
+ ["(group-by = '())"],
+ [Homework4and5Mutator],
+ )
+
+ def test_empty_list_handling(self) -> None:
+ self.mutant_diff_test(
+ "'()",
+ [],
+ [Homework4and5Mutator]
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_if_mutators.py b/racket_mutation_analysis/racket_mutation/test/test_if_mutators.py
new file mode 100644
index 0000000..62de996
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_if_mutators.py
@@ -0,0 +1,163 @@
+from racket_mutation_analysis.racket_mutation.mutators.if_mut import IfMutator
+
+from . import mutant_base_test
+
+
+class TestIfMutators(mutant_base_test.MutantBaseTest):
+ def test_if_true_mutator(self):
+ self.mutant_diff_test(
+ '(if #t 2 3)',
+ ['(if #t 2 3)',
+ '(if #f 2 3)'],
+ [IfMutator]
+ )
+
+ def test_if_false_mutator(self):
+ self.mutant_diff_test(
+ '(if #f #t #f)',
+ ['(if #t #t #f)',
+ '(if #f #t #f)'],
+ [IfMutator]
+ )
+
+ def test_nested_if_mutator(self):
+ self.mutant_diff_test(
+ '(if (if (= 1 2) #t #f) #t #f)',
+ ['(if #t #t #f)',
+ '(if #f #t #f)',
+ '(if (if #t #t #f) #t #f)',
+ '(if (if #f #t #f) #t #f)'],
+ [IfMutator]
+ )
+
+ def test_nested_if_mutator2(self):
+ self.mutant_diff_test(
+ '(if (if (= a b) #t #f) 3 4)',
+ ['(if #t 3 4)', '(if #f 3 4)',
+ '(if (if #t #t #f) 3 4)', '(if (if #f #t #f) 3 4)'],
+ [IfMutator]
+ )
+
+ def test_complete_if(self):
+ self.mutant_diff_test(
+ '(define (foo a b) (if (= a b) #t #f))',
+ ['(define (foo a b) (if #t #t #f))',
+ '(define (foo a b) (if #f #t #f))'],
+ [IfMutator]
+ )
+
+ def test_complete_if_2(self):
+ self.mutant_diff_test(
+ '(define (foo a b c) (if (> a b) (+ a b c) (- a b c)))',
+ ['(define (foo a b c) (if #t (+ a b c) (- a b c)))',
+ '(define (foo a b c) (if #f (+ a b c) (- a b c)))'],
+ [IfMutator]
+ )
+
+ def test_already_boolean(self):
+ self.mutant_diff_test(
+ '(define (foo a b c) (if #t (+ a b c) (- a b c)))',
+ ['(define (foo a b c) (if #t (+ a b c) (- a b c)))',
+ '(define (foo a b c) (if #f (+ a b c) (- a b c)))'],
+ [IfMutator]
+ )
+
+ def test_multi_line(self):
+ self.mutant_diff_test(
+ '(define (foo a b)\n (if (= a b) a b))',
+ ['(define (foo a b)\n (if #t a b))',
+ '(define (foo a b)\n (if #f a b))'],
+ [IfMutator]
+ )
+
+ def test_multi_line2(self):
+ self.mutant_diff_test(
+ '(define (foo a b c d)\n (if\n(=c d)\na\nb))',
+ ['(define (foo a b c d)\n (if\n#t\na\nb))',
+ '(define (foo a b c d)\n (if\n#f\na\nb))'],
+ [IfMutator]
+ )
+
+ def test_mutant_across_lines_if(self):
+ self.mutant_diff_test(
+ '(define (foo a b c) (if (and\na\nb\nc)\na b))',
+ ['(define (foo a b c) (if #t\na b))',
+ '(define (foo a b c) (if #f\na b))'],
+ [IfMutator]
+ )
+
+ def test_non_applicable_mutator(self):
+ self.mutant_diff_test(
+ '(and #t #f)',
+ [],
+ [IfMutator])
+
+ def test_empty_list_handling(self) -> None:
+ self.mutant_diff_test(
+ "'()",
+ [],
+ [IfMutator]
+ )
+
+ def test_if_true_mutator_run(self):
+ self.complete_test(
+ '(if #t 2 3)',
+ [IfMutator],
+ [('(if #t 2 3)', '2'),
+ ('(if #f 2 3)', '3')]
+ )
+
+ def test_if_false_mutator_run(self):
+ self.complete_test(
+ '(if #f #t #f)',
+ [IfMutator],
+ [('(if #t #t #f)', '#t'),
+ ('(if #f #t #f)', '#f')]
+ )
+
+ def test_nested_if_mutator_run(self):
+ self.complete_test(
+ '(if (if (= 1 2) #t #f) #t #f)',
+ [IfMutator],
+ [('(if #t #t #f)', '#t'),
+ ('(if #f #t #f)', '#f'),
+ ('(if (if #t #t #f) #t #f)', '#t'),
+ ('(if (if #f #t #f) #t #f)', '#f')]
+ )
+
+ def test_nested_if_mutator2_run(self):
+ self.complete_test(
+ '(if (if (= a b) #t #f) 3 4)',
+ [IfMutator],
+ [('(if #t 3 4)', '3'),
+ ('(if #f 3 4)', '4'),
+ ('(if (if #t #t #f) 3 4)', '3'),
+ ('(if (if #f #t #f) 3 4)', '4')],
+ )
+
+ def test_complete_if_run(self):
+ self.complete_test(
+ '(define (foo a b) (if (= a b) #t #f))',
+ [IfMutator],
+ [('(define (foo a b) (if #t #t #f))', '#t'),
+ ('(define (foo a b) (if #f #t #f))', '#f')],
+ '(foo 1 2)'
+ )
+
+ def test_complete_if_2_run(self):
+ self.complete_test(
+ '(define (foo a b c) (if (> a b) (+ a b c) (- a b c)))',
+ [IfMutator],
+ [('(define (foo a b c) (if #t (+ a b c) (- a b c)))', '6'),
+ ('(define (foo a b c) (if #f (+ a b c) (- a b c)))', '-4')],
+ '(foo 1 2 3)'
+ )
+
+ def test_multi_line2_run(self):
+ self.complete_test(
+ '(define (foo a b c d)\n (if\n(=c d)\na\nb))',
+ [IfMutator],
+ [('(define (foo a b c d)\n (if\n#t\na\nb))', '1'),
+ ('(define (foo a b c d)\n (if\n#f\na\nb))', '2')],
+ '(foo 1 2 3 4)'
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_logical_mutators.py b/racket_mutation_analysis/racket_mutation/test/test_logical_mutators.py
new file mode 100644
index 0000000..76a53cb
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_logical_mutators.py
@@ -0,0 +1,79 @@
+from racket_mutation_analysis.racket_mutation.mutators.logical_mut import LogicalMutator
+
+from . import mutant_base_test
+
+
+class TestLogicalOperators(mutant_base_test.MutantBaseTest):
+ def test_and_mutator(self):
+ self.mutant_diff_test(
+ '(and #t #f)',
+ ['(or #t #f)'],
+ [LogicalMutator])
+
+ def test_or_mutator(self):
+ self.mutant_diff_test(
+ '(or a b)',
+ ['(and a b)'],
+ [LogicalMutator])
+
+ def test_and_or_mutators(self):
+ self.mutant_diff_test(
+ '(and (or (and (or #t #f) #t) #f) #t)',
+ ['(or (or (and (or #t #f) #t) #f) #t)',
+ '(and (and (and (or #t #f) #t) #f) #t)',
+ '(and (or (or (or #t #f) #t) #f) #t)',
+ '(and (or (and (and #t #f) #t) #f) #t)'],
+ [LogicalMutator])
+
+ def test_and_or_mutators_multi_line(self):
+ self.mutant_diff_test(
+ '(and (or \n(and (or \n#t #f) #t) #f) #t)',
+ ['(or (or \n(and (or \n#t #f) #t) #f) #t)',
+ '(and (and \n(and (or \n#t #f) #t) #f) #t)',
+ '(and (or \n(or (or \n#t #f) #t) #f) #t)',
+ '(and (or \n(and (and \n#t #f) #t) #f) #t)'],
+ [LogicalMutator])
+
+ def test_complete(self):
+ self.mutant_diff_test(
+ '(define (logical a b c) (or (and a b) c))',
+ ['(define (logical a b c) (and (and a b) c))',
+ '(define (logical a b c) (or (or a b) c))'],
+ [LogicalMutator])
+
+ def test_non_applicable_mutator(self):
+ self.mutant_diff_test(
+ '(+ 1 2)',
+ [],
+ [LogicalMutator])
+
+ def test_and_mutator_run(self):
+ self.complete_test(
+ '(and #t #f)',
+ [LogicalMutator],
+ [('(or #t #f)', '#t')])
+
+ def test_and_or_mutators_run(self):
+ self.complete_test(
+ '(and (or (and (or #t #f) #t) #f) #t)',
+ [LogicalMutator],
+ [('(or (or (and (or #t #f) #t) #f) #t)', '#t'),
+ ('(and (and (and (or #t #f) #t) #f) #t)', '#f'),
+ ('(and (or (or (or #t #f) #t) #f) #t)', '#t'),
+ ('(and (or (and (and #t #f) #t) #f) #t)', '#f')])
+
+ def test_complete_logical_run(self):
+ self.complete_test(
+ '(define (logical a b c) (or (and a b) c))',
+ [LogicalMutator],
+ [('(define (logical a b c) (and (and a b) c))', '#f'),
+ ('(define (logical a b c) (or (or a b) c))', '#t')],
+ '(logical #t #f #t)'
+ )
+
+ def test_empty_list_handling(self) -> None:
+ self.mutant_diff_test(
+ "'()",
+ [],
+ [LogicalMutator]
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_num_comparison.py b/racket_mutation_analysis/racket_mutation/test/test_num_comparison.py
new file mode 100644
index 0000000..e4abf8f
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_num_comparison.py
@@ -0,0 +1,94 @@
+from racket_mutation_analysis.racket_mutation.mutators.num_comparison_mutator import (
+ NumberComparisonMutator
+)
+
+from . import mutant_base_test
+
+
+class TestNumberComparisonMutator(mutant_base_test.MutantBaseTest):
+ def test_greater_than_replacements(self):
+ self.mutant_diff_test(
+ '(> 1 2)',
+ ['(>= 1 2)',
+ '(< 1 2)',
+ '(<= 1 2)',
+ '(= 1 2)'],
+ [NumberComparisonMutator],
+ )
+
+ def test_greater_than__or_equal_to_replacements(self):
+ self.mutant_diff_test(
+ '(>= 1 2)',
+ ['(> 1 2)',
+ '(< 1 2)',
+ '(<= 1 2)',
+ '(= 1 2)'],
+ [NumberComparisonMutator],
+ )
+
+ def test_less_than_replacements(self):
+ self.mutant_diff_test(
+ '(< 1 2)',
+ ['(> 1 2)',
+ '(>= 1 2)',
+ '(<= 1 2)',
+ '(= 1 2)'],
+ [NumberComparisonMutator],
+ )
+
+ def test_less_than__or_equal_to_replacements(self):
+ self.mutant_diff_test(
+ '(<= 1 2)',
+ ['(> 1 2)',
+ '(>= 1 2)',
+ '(< 1 2)',
+ '(= 1 2)'],
+ [NumberComparisonMutator],
+ )
+
+ def test_greater_than_replacements_run(self):
+ self.complete_test(
+ '(> 1 2)',
+ [NumberComparisonMutator],
+ [('(>= 1 2)', '#f'),
+ ('(< 1 2)', '#t'),
+ ('(<= 1 2)', '#t'),
+ ('(= 1 2)', '#f')]
+ )
+
+ def test_greater_than__or_equal_to_replacements_run(self):
+ self.complete_test(
+ '(>= 1 2)',
+ [NumberComparisonMutator],
+ [('(> 1 2)', '#f'),
+ ('(< 1 2)', '#t'),
+ ('(<= 1 2)', '#t'),
+ ('(= 1 2)', '#f')]
+ )
+
+ def test_less_than_replacements_run(self):
+ self.complete_test(
+ '(< 1 2)',
+ [NumberComparisonMutator],
+ [('(> 1 2)', '#f'),
+ ('(>= 1 2)', '#f'),
+ ('(<= 1 2)', '#t'),
+ ('(= 1 2)', '#f')]
+ )
+
+ def test_less_than__or_equal_to_replacements_run(self):
+ self.complete_test(
+ '(<= 1 2)',
+ [NumberComparisonMutator],
+ [('(> 1 2)', '#f'),
+ ('(>= 1 2)', '#f'),
+ ('(< 1 2)', '#t'),
+ ('(= 1 2)', '#f')]
+ )
+
+ def test_empty_list_handling(self) -> None:
+ self.mutant_diff_test(
+ "'()",
+ [],
+ [NumberComparisonMutator]
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_num_literals.py b/racket_mutation_analysis/racket_mutation/test/test_num_literals.py
new file mode 100644
index 0000000..025e38d
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_num_literals.py
@@ -0,0 +1,51 @@
+from racket_mutation_analysis.racket_mutation.mutators.num_literals_mut import NumLiteralsMutator
+
+from . import mutant_base_test
+
+
+class TestNumLiteralsMutator(mutant_base_test.MutantBaseTest):
+ def test_one_num_literal(self):
+ self.mutant_diff_test(
+ '2',
+ ['0', '-1', '1', '1.0', '3.0'],
+ [NumLiteralsMutator],
+ )
+
+ def test_non_applicable_num_literal(self):
+ self.mutant_diff_test(
+ '(/ a b)',
+ [],
+ [NumLiteralsMutator]
+ )
+
+ def test_run_simple_arithmetic_num_literals_run(self):
+ self.complete_test(
+ '(+ 1 2)',
+ [NumLiteralsMutator],
+ [('(+ 0 2)', '2'),
+ ('(+ -1 2)', '1'),
+ ('(+ 1 2)', '3'),
+ ('(+ 0.0 2)', '2.0'),
+ ('(+ 2.0 2)', '4.0'),
+ ('(+ 1 0)', '1'),
+ ('(+ 1 -1)', '0'),
+ ('(+ 1 1)', '2'),
+ ('(+ 1 1.0)', '2.0'),
+ ('(+ 1 3.0)', '4.0')]
+ )
+
+ def test_run_simple_floating_point_num_literals_run(self):
+ self.complete_test(
+ '(* 4.0 2.0)',
+ [NumLiteralsMutator],
+ [('(* 0 2.0)', '0'),
+ ('(* -1 2.0)', '-2.0'),
+ ('(* 1 2.0)', '2.0'),
+ ('(* 3.0 2.0)', '6.0'),
+ ('(* 5.0 2.0)', '10.0'),
+ ('(* 4.0 0)', '0'),
+ ('(* 4.0 -1)', '-4.0'),
+ ('(* 4.0 1)', '4.0'),
+ ('(* 4.0 1.0)', '4.0'),
+ ('(* 4.0 3.0)', '12.0')]
+ )
diff --git a/racket_mutation_analysis/racket_mutation/test/test_wrap_with_not.py b/racket_mutation_analysis/racket_mutation/test/test_wrap_with_not.py
new file mode 100644
index 0000000..e0bcd83
--- /dev/null
+++ b/racket_mutation_analysis/racket_mutation/test/test_wrap_with_not.py
@@ -0,0 +1,69 @@
+from racket_mutation_analysis.racket_mutation.mutators.wrap_with_not import WrapWithNotMutator
+
+from . import mutant_base_test
+
+
+class TestWrapWithNotMutator(mutant_base_test.MutantBaseTest):
+ def test_wrap_and_with_not(self):
+ self.mutant_diff_test(
+ '(and #t #f)',
+ ['(not (and #t #f))'],
+ [WrapWithNotMutator])
+
+ def test_wrap_or_with_not(self):
+ self.mutant_diff_test(
+ '(or a b)',
+ ['(not (or a b))'],
+ [WrapWithNotMutator])
+
+ def test_and_or_mutators(self):
+ self.mutant_diff_test(
+ '(and (or (and (or #t #f) #t) #f) #t)',
+ ['(not (and (or (and (or #t #f) #t) #f) #t))',
+ '(and (not (or (and (or #t #f) #t) #f)) #t)',
+ '(and (or (not (and (or #t #f) #t)) #f) #t)',
+ '(and (or (and (not (or #t #f)) #t) #f) #t)'],
+ [WrapWithNotMutator])
+
+ def test_complete_wrap_with_not(self):
+ self.mutant_diff_test(
+ '(define (logical a b c) (or (and a b) c))',
+ ['(define (logical a b c) (not (or (and a b) c)))',
+ '(define (logical a b c) (or (not (and a b)) c))'],
+ [WrapWithNotMutator])
+
+ def test_non_applicable_mutator_wrap_not(self):
+ self.mutant_diff_test(
+ '(+ 1 2)',
+ [],
+ [WrapWithNotMutator])
+
+ def test_empty_list_handling(self) -> None:
+ self.mutant_diff_test(
+ "'()",
+ [],
+ [WrapWithNotMutator]
+ )
+
+ def test_wrap_and_mutator_run(self):
+ self.complete_test(
+ '(and #t #f)',
+ [WrapWithNotMutator],
+ [('(not (and #t #f))', '#t')])
+
+ def test_and_or_wrap_mutators_run(self):
+ self.complete_test(
+ '(and (or (and (or #t #f) #t) #f) #t)',
+ [WrapWithNotMutator],
+ [('(not (and (or (and (or #t #f) #t) #f) #t))', '#f'),
+ ('(and (not (or (and (or #t #f) #t) #f)) #t)', '#f'),
+ ('(and (or (not (and (or #t #f) #t)) #f) #t)', '#f'),
+ ('(and (or (and (not (or #t #f)) #t) #f) #t)', '#f')])
+
+ def test_complete_wrap_run(self):
+ self.complete_test(
+ '(define (logical a b c) (or (and a b) c))',
+ [WrapWithNotMutator],
+ [('(define (logical a b c) (not (or (and a b) c)))', '#f'),
+ ('(define (logical a b c) (or (not (and a b)) c))', '#t')],
+ '(logical #t #f #t)')
diff --git a/read_write_test.py b/read_write_test.py
new file mode 100644
index 0000000..5db823e
--- /dev/null
+++ b/read_write_test.py
@@ -0,0 +1,39 @@
+"""
+Use this for testing our parser. Parses a Racket program and writes
+it back out to standard out. Whitespace is somewhat preserved (line break
+placement is preserved, but not the specific line ending characters;
+tabs are replaced with a single space).
+Comments are removed and whitespace is left in their place.
+
+Note that to generate mutants, we don't actually need to modify the AST.
+We only need to identify the node to mutate and record its location and the
+text to substitute.
+"""
+
+import argparse
+import sys
+
+from racket_mutation_analysis.racket_ast.scheme_reader import SchemeReader
+from racket_mutation_analysis.racket_ast.string_visitor import ToStrVisitor
+
+
+def main():
+ """Run a read-print loop for Scheme expressions."""
+ args = parse_args()
+ with sys.stdin if args.input_file == '-' else open(args.input_file) as f:
+ ast = SchemeReader().read_file(f)
+
+ visitor = ToStrVisitor()
+ visitor.visit(ast)
+ print(visitor.result)
+
+
+def parse_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('input_file', default='-')
+
+ return parser.parse_args()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..ab3b6ac
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,34 @@
+#
+# This file is autogenerated by pip-compile with python 3.10
+# To update, run:
+#
+# pip-compile --extra=dev --output-file=requirements-dev.txt pyproject.toml
+#
+attrs==22.1.0
+ # via jsonschema
+build==0.8.0
+ # via racket-mutation-analysis (pyproject.toml)
+isort==5.10.1
+ # via racket-mutation-analysis (pyproject.toml)
+jsonschema==4.16.0
+ # via racket-mutation-analysis (pyproject.toml)
+nodeenv==1.6.0
+ # via pyright
+packaging==21.3
+ # via build
+pep517==0.13.0
+ # via build
+pycodestyle==2.8.0
+ # via racket-mutation-analysis (pyproject.toml)
+pyparsing==3.0.9
+ # via packaging
+pyright==1.1.254
+ # via racket-mutation-analysis (pyproject.toml)
+pyrsistent==0.18.1
+ # via jsonschema
+pyyaml==6.0
+ # via racket-mutation-analysis (pyproject.toml)
+tomli==2.0.1
+ # via
+ # build
+ # pep517
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..27830a4
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,19 @@
+#
+# This file is autogenerated by pip-compile with python 3.10
+# To update, run:
+#
+# pip-compile pyproject.toml
+#
+attrs==22.1.0
+ # via jsonschema
+jsonschema==3.2.0
+ # via racket-mutation-analysis (pyproject.toml)
+pyrsistent==0.18.0
+ # via jsonschema
+pyyaml==6.0
+ # via racket-mutation-analysis (pyproject.toml)
+six==1.16.0
+ # via jsonschema
+
+# The following packages are considered to be unsafe in a requirements file:
+# setuptools
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..e964805
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,12 @@
+[pycodestyle]
+ignore = W503,E133
+max-line-length = 99
+
+# [pydocstyle]
+# ignore = D1,D200,D203,D204,D205,D212,D4
+
+[isort]
+multi_line_output = 5
+use_parentheses = True
+line_length = 99
+