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 +