diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5a8072d --- /dev/null +++ b/Makefile @@ -0,0 +1,249 @@ +SHELL := /bin/bash +.ONESHELL: + +PYTHON ?= python3 +VENV ?= .venv +PIP := $(VENV)/bin/pip +MUTATE := $(VENV)/bin/mutate + +TMP_DIR := tmp +TACT_OUT := /tmp/tact-out +TOLK_JSON := /tmp/tolk-out.json + +# Tolk CLI via npx (TON) +TOLK_NPX := npx -y @ton/tolk-js +TOLK_CMD := $(TOLK_NPX) --output-json $(TOLK_JSON) MUTANT + +.PHONY: help doctor venv install-um tolk-install tolk-check tact-check \ + mutate-tact mutate-tolk mutate-tact-only mutate-tolk-only clean + +help: + @echo "Targets:" + @echo " make doctor - show toolchain availability" + @echo " make venv - create venv" + @echo " make install-um - install universalmutator into venv (editable)" + @echo " make tolk-install - install Tolk CLI wrapper (@ton/tolk-js) globally" + @echo " make tolk-check - compile examples/foo.tolk via npx" + @echo " make tact-check - compile examples/foo.tact via tact" + @echo " make mutate-tact - mutate Tact with compilation check" + @echo " make mutate-tolk - mutate Tolk with compilation check (via npx)" + @echo " make mutate-*-only - use only .rules (no universal/c_like)" + @echo " make clean - remove tmp mutants" + +doctor: + @echo "python: $$($(PYTHON) --version 2>/dev/null || true)" + @echo "venv: $(VENV)" + @echo "node: $$(node --version 2>/dev/null || echo 'NOT FOUND')" + @echo "npm: $$(npm --version 2>/dev/null || echo 'NOT FOUND')" + @echo "tact: $$(command -v tact >/dev/null && tact --version 2>/dev/null || echo 'NOT FOUND')" + @echo "npx @ton/tolk-js: $$( $(TOLK_NPX) --help >/dev/null 2>&1 && echo 'OK' || echo 'NOT FOUND (run make tolk-install or use npx with internet)' )" + @echo "mutate: $$(test -x $(MUTATE) && echo 'OK' || echo 'NOT FOUND (run make install-um)')" + +venv: + $(PYTHON) -m venv $(VENV) + $(PIP) install -U pip + +# Editable install. If you're offline, this may still work because it's local, +# but build isolation may try to pull setuptools; use --no-build-isolation. +install-um: venv + $(PIP) install -e . --no-build-isolation --no-deps || true + @echo "If 'mutate' is still missing, run: source $(VENV)/bin/activate && python -m universalmutator.genmutants --help" + +# Optional: global install of tolk-js (still runs via npx) +tolk-install: + npm i -g @ton/tolk-js + +tolk-check: + $(TOLK_NPX) --output-json $(TOLK_JSON) examples/foo.tolk + @echo "OK: wrote $(TOLK_JSON)" + +tact-check: + tact examples/foo.tact --output $(TACT_OUT) + @echo "OK: wrote $(TACT_OUT)" + +mutate-tact: + mkdir -p $(TMP_DIR)/mutants_tact + $(MUTATE) examples/foo.tact tact \ + --cmd "tact MUTANT --output $(TACT_OUT)" \ + --mutantDir $(TMP_DIR)/mutants_tact + +mutate-tact-only: + mkdir -p $(TMP_DIR)/mutants_tact_only + $(MUTATE) examples/foo.tact tact \ + --only tact.rules --noCheck \ + --mutantDir $(TMP_DIR)/mutants_tact_only + +mutate-tolk: + mkdir -p tmp/mutants_tolk + mutate examples/foo.tolk tolk \ + --cmd "npx -y @ton/tolk-js --output-json /tmp/tolk-out.json examples/foo.tolk" \ + --mutantDir tmp/mutants_tolk + + +mutate-tolk-only: + mkdir -p tmp/mutants_tolk_only + mutate examples/foo.tolk tolk \ + --cmd "npx -y @ton/tolk-js --only tolk.rules --noCheck \ + --mutantDir --output-json /tmp/tolk-out.json examples/foo.tolk" \ + --mutantDir tmp/mutants_tolk_only + +clean: + rm -rf $(TMP_DIR)/mutants_tact* $(TMP_DIR)/mutants_tolk* + rm -f $(TOLK_JSON) + + + +FILE ?= +LOG_DIR := tmp/compile_logs +TACT_OUT_DIR := /tmp/tact-out +TOLK_JSON := /tmp/tolk-out.json + +TACT ?= tact +TOLK_NPX := npx -y @ton/tolk-js + +.PHONY: tact-validate tolk-validate + +tact-validate: + @bash -euo pipefail -c '\ + FILE="$(FILE)"; \ + if [ -z "$$FILE" ]; then \ + echo "Usage: make tact-validate FILE=path/to/file.tact"; exit 2; \ + fi; \ + mkdir -p "$(LOG_DIR)" "$(TACT_OUT_DIR)"; \ + name="$${FILE##*/}"; \ + LOG="$(LOG_DIR)/tact_$${name}.log"; \ + echo "Compiling Tact: $$FILE"; \ + if "$(TACT)" "$$FILE" --output "$(TACT_OUT_DIR)" >"$$LOG" 2>&1; then \ + echo "OK. Output: $(TACT_OUT_DIR)"; \ + echo "Log: $$LOG"; \ + else \ + rc="$$?"; \ + echo "FAILED (exit $$rc). Log: $$LOG"; \ + echo "---- error context (best effort) ----"; \ + awk "BEGIN{p=0} /Error:/{p=1} {if(p) print}" "$$LOG" | sed -n "1,160p" || true; \ + echo "---- tail ----"; \ + tail -n 80 "$$LOG" || true; \ + exit "$$rc"; \ + fi' + +tolk-validate: + @bash -euo pipefail -c '\ + FILE="$(FILE)"; \ + if [ -z "$$FILE" ]; then \ + echo "Usage: make tolk-validate FILE=path/to/file.tolk"; exit 2; \ + fi; \ + mkdir -p "$(LOG_DIR)"; \ + name="$${FILE##*/}"; \ + LOG="$(LOG_DIR)/tolk_$${name}.log"; \ + rm -f "$(TOLK_JSON)" >/dev/null 2>&1 || true; \ + echo "Compiling Tolk (via npx @ton/tolk-js): $$FILE"; \ + if npx -y @ton/tolk-js --output-json "$(TOLK_JSON)" "$$FILE" >"$$LOG" 2>&1; then \ + echo "OK. JSON: $(TOLK_JSON)"; \ + echo "Log: $$LOG"; \ + else \ + rc="$$?"; \ + echo "FAILED (exit $$rc). Log: $$LOG"; \ + echo "---- error context (best effort) ----"; \ + grep -nEi "error|failed|exception" "$$LOG" | head -n 60 || true; \ + echo "---- tail ----"; \ + tail -n 120 "$$LOG" || true; \ + exit "$$rc"; \ + fi' + + +# ----------------------------- +# FunC (func) toolchain targets +# ----------------------------- + +# FunC outputs +FUNC_FIF := /tmp/func-out.fif +FUNC_CELL := /tmp/func-out.cell +FILE_DEFAULT_FUNC := examples/foo.fc + +.PHONY: func-install func-check func-validate func-build + +func-install: + @bash -euo pipefail -c '\ + npm i -g ton-compiler; \ + echo "OK: ton-compiler installed. Try: ton-compiler --help"' + +# ✅ default check = validate (Fift-only) +func-check: + @$(MAKE) func-validate FILE=$(FILE_DEFAULT_FUNC) + +# ✅ Single-file validate: FunC -> Fift (no main required) +func-validate: + @bash -euo pipefail -c '\ + FILE="$(FILE)"; \ + if [ -z "$$FILE" ]; then echo "Usage: make func-validate FILE=path/to/file.fc"; exit 2; fi; \ + if [ ! -f "$$FILE" ]; then echo "File not found: $$FILE"; exit 2; fi; \ + mkdir -p tmp/compile_logs; \ + LOG="tmp/compile_logs/func_$${FILE##*/}.log"; \ + rm -f "$(FUNC_FIF)" >/dev/null 2>&1 || true; \ + echo "Compiling FunC -> Fift (validate): $$FILE"; \ + if command -v ton-compiler >/dev/null; then \ + ton-compiler --input "$$FILE" --output-fift "$(FUNC_FIF)" >"$$LOG" 2>&1; \ + else \ + npx -y ton-compiler --input "$$FILE" --output-fift "$(FUNC_FIF)" >"$$LOG" 2>&1; \ + fi; \ + RC="$$?"; \ + if [ "$$RC" -eq 0 ]; then \ + echo "OK. FIF: $(FUNC_FIF)"; echo "Log: $$LOG"; \ + else \ + echo "FAILED (exit $$RC). Log: $$LOG"; \ + grep -nEi "error|fatal|failed|line|parse|undefined" "$$LOG" | head -n 120 || true; \ + tail -n 160 "$$LOG" || true; \ + exit "$$RC"; \ + fi' + +# ✅ Full build: FunC -> cell (+fift). Requires main. +func-build: + @bash -euo pipefail -c '\ + FILE="$(FILE)"; \ + if [ -z "$$FILE" ]; then echo "Usage: make func-build FILE=path/to/file.fc"; exit 2; fi; \ + if [ ! -f "$$FILE" ]; then echo "File not found: $$FILE"; exit 2; fi; \ + mkdir -p tmp/compile_logs; \ + LOG="tmp/compile_logs/func_build_$${FILE##*/}.log"; \ + rm -f "$(FUNC_CELL)" "$(FUNC_FIF)" >/dev/null 2>&1 || true; \ + echo "Compiling FunC -> Cell: $$FILE"; \ + if command -v ton-compiler >/dev/null; then \ + ton-compiler --input "$$FILE" --output "$(FUNC_CELL)" --output-fift "$(FUNC_FIF)" >"$$LOG" 2>&1; \ + else \ + npx -y ton-compiler --input "$$FILE" --output "$(FUNC_CELL)" --output-fift "$(FUNC_FIF)" >"$$LOG" 2>&1; \ + fi; \ + RC="$$?"; \ + if [ "$$RC" -eq 0 ]; then \ + echo "OK. CELL: $(FUNC_CELL)"; echo "FIF: $(FUNC_FIF)"; echo "Log: $$LOG"; \ + else \ + echo "FAILED (exit $$RC). Log: $$LOG"; \ + grep -nEi "main|procedure|entry|error|fatal|failed" "$$LOG" | head -n 160 || true; \ + tail -n 200 "$$LOG" || true; \ + exit "$$RC"; \ + fi' + +.PHONY: mutate-func + +# Usage: +# make mutate-func FILE=path/to/file.fc +# Mutants go to: tmp/mutants_func +mutate-func: + @bash -euo pipefail -c '\ + FILE="$(FILE)"; \ + if [ -z "$$FILE" ]; then \ + echo "Usage: make mutate-func FILE=path/to/file.fc"; exit 2; \ + fi; \ + if [ ! -f "$$FILE" ]; then \ + echo "File not found: $$FILE"; exit 2; \ + fi; \ + mkdir -p tmp/mutants_func; \ + echo "Mutating FunC with compile-check (Fift-only): $$FILE"; \ + .venv/bin/mutate "$$FILE" func \ + --cmd "bash -lc '\''rm -f $(FUNC_FIF) >/dev/null 2>&1 || true; \ + if command -v ton-compiler >/dev/null; then \ + ton-compiler --input \"$$FILE\" --output-fift $(FUNC_FIF); \ + else \ + npx -y ton-compiler --input \"$$FILE\" --output-fift $(FUNC_FIF); \ + fi'\''" \ + --mutantDir tmp/mutants_func' + + diff --git a/README.md b/README.md index 6c5cab7..17369ce 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,222 @@ +# UniversalMutator + TON (Tact / FunC / Tolk) + +Это форк `universalmutator`, в который добавлена поддержка **трёх TON-языков**: + +* **Tact** (`.tact`) +* **FunC** (`.fc`, `.func`) +* **Tolk** (`.tolk`) + +Идея простая: UniversalMutator уже упоминается в официальной документации Solidity как один из ресурсов, а добавить новый язык здесь обычно сводится к: + +1. написанию наборов regex-правил (`*.rules`) +2. добавлению handler’а (Python), который умеет валидировать мутанта (например, компиляцией) + + +## Что добавлено в этом форке + +### 1) Новые языки и расширения + +Добавлены соответствия расширений файлов и новые handlers: + +* `universalmutator/tact_handler.py` +* `universalmutator/func_handler.py` +* `universalmutator/tolk_handler.py` + +А также обновлена логика выбора языка по расширению (чтобы `mutate file.tact tact` работало так же, как для остальных языков). + +### 2) Правила мутаций для TON-языков + +Добавлены файлы правил (регулярки мутаций): + +* `universalmutator/static/tact.rules` +* `universalmutator/static/func.rules` +* `universalmutator/static/tolk.rules` + +В основе — типовые мутации (операторы сравнения/арифметика/логика/константы), плюс несколько точечных мутаций под синтаксис языков. + +### 3) Compile-check для валидности мутантов + +UniversalMutator сам по себе “делает мутантов” на основе правил, а валидность мутанта (компилируется/нет) зависит от того, какой handler используется. + +В этом форке для TON-языков рекомендуется валидировать мутантов **через компиляцию**: + +* **Tact** — через `tact` CLI +* **Tolk** — через `npx @ton/tolk-js` (WASM-компилятор) +* **FunC** — через `ton-compiler` (CLI, удобно для CI и локальных прогонов) + + +## Где находятся инструкции + +* Основные команды запуска собраны в **`Makefile`** в корне репозитория. +* Логи компиляции и диагностические выводы складываются в: + + * `tmp/compile_logs/` +* Сгенерированные мутанты складываются в: + + * `tmp/mutants_tact/` + * `tmp/mutants_tolk/` + * `tmp/mutants_func/` + + +## Быстрый старт + +### 0) Проверить окружение + +```bash +make doctor +``` + +### 1) Python окружение и установка проекта + +```bash +python3 -m venv .venv +source .venv/bin/activate + +# локальная установка форка +pip install -e . --no-build-isolation --no-deps +``` + +Если окружение без доступа в интернет или pip пытается тянуть build-зависимости, используй флаг `--no-build-isolation`. + + +## Установка компиляторов + +### Tact + +Нужен установленный `tact` (CLI должен быть доступен как команда `tact`). +Проверка: + +```bash +tact --help +``` + +### Tolk + +Нужны `node`, `npm`, `npx`. +Проверка: + +```bash +node -v +npm -v +npx -v +``` + +Компиляция Tolk выполняется через: + +```bash +npx -y @ton/tolk-js --help +``` + +### FunC + +Рекомендуемый CLI: + +```bash +npm i -g ton-compiler +ton-compiler --help +``` + + +## Валидация одиночных файлов + +Эти команды компилируют один файл и показывают ошибку (а полный лог кладут в `tmp/compile_logs/`). + +### Tact + +```bash +make tact-validate FILE=examples/foo.tact +``` + +### Tolk + +```bash +make tolk-validate FILE=examples/foo.tolk +``` + +### FunC + +Для FunC есть важный нюанс: сборка в `.cell` часто требует `main`, будь бдительным когда выбираешь код для мутирования + +```bash +make func-validate FILE=examples/foo.fc +``` + + +## Генерация мутантов (mutation testing) + +Смысл: UniversalMutator генерирует мутанты, и для каждого мутанта запускает compile-check. +Если компиляция проходит — мутант считается `VALID` и сохраняется. + +### Tact + +```bash +make mutate-tact FILE=examples/foo.tact +``` + +или напрямую: + +```bash +mutate examples/foo.tact tact \ + --cmd "tact MUTANT --output /tmp/tact-out" \ + --mutantDir tmp/mutants_tact +``` + +### Tolk + +Для Tolk часто есть `import`, и компилятору важен контекст расположения файла. +Поэтому рекомендуемый режим — компилировать фиксированный путь исходника, без `MUTANT` в команде, чтобы UniversalMutator подменял файл на месте: + +```bash +make mutate-tolk FILE=examples/foo.tolk +``` + +или напрямую: + +```bash +mutate examples/foo.tolk tolk \ + --cmd "npx -y @ton/tolk-js --output-json /tmp/tolk-out.json examples/foo.tolk" \ + --mutantDir tmp/mutants_tolk +``` + +### FunC (Fift-only compile-check) + +```bash +make mutate-func FILE=examples/foo.fc +``` + +или напрямую: + +```bash +mutate examples/foo.fc func \ + --cmd 'bash -lc "rm -f /tmp/func-out.fif; (command -v ton-compiler >/dev/null && ton-compiler || npx -y ton-compiler) --input examples/foo.fc --output-fift /tmp/func-out.fif"' \ + --mutantDir tmp/mutants_func +``` + +## Как понять, что всё работает правильно + +После запуска `mutate` можно увидеть статистику по мере выполнения: + +* сколько `VALID` (сохранились в `tmp/mutants_*`) +* сколько `INVALID` (не компилируются) +* процент валидных мутантов + +Практически всегда будут `INVALID` (особенно от “универсальных” правил), но цель — держать валидность достаточно высокой, чтобы мутации были полезны. + +## Заметки по реализации + +Из основного с чем столкнулся: +* Для начала дефолтный проект, который требовал зависимости pkg_resources, который уже не найти (заменил на pkgutil) +* После проблема с импортом T из re, ее тоже поправил +* Дальше в целом все было ок, за исключением поиска пакетов с компиляторами и их правильный запуск +* По сути, вайбкод вперемешку с моими фиксами, идеями и прочим, ГПТ помог значительно и ускорил весь процесс + +### Что улучшить дальше + +* TODO: поднять % валидных мутантов для Tact/Tolk/FunC без влияния на другие языки (break/continue спонтанно подставляется, что сразу делает код на этих трех языках не исполняемым) +* TODO: расширить набор “полезных” мутаций под синтаксис TON-языков +* TODO: добавить CI job, который прогоняет smoke-test на примерах + +# DEFAULT TEXT BELOW This is a tool based on source-based rewrite of code lines for mutation generation, including multi-language rules aided by special rules for languages or even projects. Originally, the approach used only regular expressions, treating code as text. However, there is also a mode that can use the [Comby](https://github.com/comby-tools/comby) tool diff --git a/examples/analyze.out b/examples/analyze.out new file mode 100644 index 0000000..c66e3d1 --- /dev/null +++ b/examples/analyze.out @@ -0,0 +1,1055 @@ +ANALYZING foo.py +COMMAND: ** ['python foo.py'] ** +================================================================================ +#1: [0.0s 0.0% DONE] +MUTANT: ./foo.mutant.47.py +*** Original +--- Mutant +*************** +*** 9,15 **** + return x + + def main(): +! y = 4 + v = myfunction(y) + assert v==10 + +--- 9,15 ---- + return x + + def main(): +! y = 1 + v = myfunction(y) + assert v==10 + + + +RUNNING ./foo.mutant.47.py... +1 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.47.py KILLED IN 0.17923378944396973 (RETURN CODE 1) + RUNNING SCORE: 1.0 +================================================================================ +#2: [0.19s 1.41% DONE] +MUTANT: ./foo.mutant.8.py +*** Original +--- Mutant +*************** +*** 1,7 **** + from __future__ import print_function + + def myfunction(x): +! if (x < 6): + print(x) + x = 20 + while (x > (10-1)): +--- 1,7 ---- + from __future__ import print_function + + def myfunction(x): +! if (x < -1): + print(x) + x = 20 + while (x > (10-1)): + + +RUNNING ./foo.mutant.8.py... +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.8.py KILLED IN 0.11300516128540039 (RETURN CODE 1) + RUNNING SCORE: 1.0 +================================================================================ +#3: [0.3s 2.82% DONE] +MUTANT: ./foo.mutant.0.py +*** Original +--- Mutant +*************** +*** 1,4 **** +! from __future__ import print_function + + def myfunction(x): + if (x < 6): +--- 1,4 ---- +! pass + + def myfunction(x): + if (x < 6): + + +RUNNING ./foo.mutant.0.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.0.py KILLED IN 0.12555956840515137 (RETURN CODE 1) + RUNNING SCORE: 1.0 +================================================================================ +#4: [0.43s 4.23% DONE] +MUTANT: ./foo.mutant.13.py +*** Original +--- Mutant +*************** +*** 3,9 **** + def myfunction(x): + if (x < 6): + print(x) +! x = 20 + while (x > (10-1)): + x -= 1 + return x +--- 3,9 ---- + def myfunction(x): + if (x < 6): + print(x) +! x = 0 + while (x > (10-1)): + x -= 1 + return x + + +RUNNING ./foo.mutant.13.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.13.py KILLED IN 0.09709763526916504 (RETURN CODE 1) + RUNNING SCORE: 1.0 +================================================================================ +#5: [0.54s 5.63% DONE] +MUTANT: ./foo.mutant.20.py +*** Original +--- Mutant +*************** +*** 4,10 **** + if (x < 6): + print(x) + x = 20 +! while (x > (10-1)): + x -= 1 + return x + +--- 4,10 ---- + if (x < 6): + print(x) + x = 20 +! while (x > (10*1)): + x -= 1 + return x + + + +RUNNING ./foo.mutant.20.py... +4 +./foo.mutant.20.py NOT KILLED + RUNNING SCORE: 0.8 +================================================================================ +#6: [0.74s 7.04% DONE] +MUTANT: ./foo.mutant.58.py +*** Original +--- Mutant +*************** +*** 11,17 **** + def main(): + y = 4 + v = myfunction(y) +! assert v==10 + + if __name__ == '__main__': + main() +--- 11,17 ---- + def main(): + y = 4 + v = myfunction(y) +! assert v==0 + + if __name__ == '__main__': + main() + + +RUNNING ./foo.mutant.58.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==0 + ^^^^ +AssertionError +./foo.mutant.58.py KILLED IN 0.11405229568481445 (RETURN CODE 1) + RUNNING SCORE: 0.8333333333333334 +================================================================================ +#7: [0.86s 8.45% DONE] +MUTANT: ./foo.mutant.64.py +*** Original +--- Mutant +*************** +*** 13,17 **** + v = myfunction(y) + assert v==10 + +! if __name__ == '__main__': + main() +--- 13,17 ---- + v = myfunction(y) + assert v==10 + +! if __name__ != '__main__': + main() + + +RUNNING ./foo.mutant.64.py... +./foo.mutant.64.py NOT KILLED + RUNNING SCORE: 0.7142857142857143 +================================================================================ +#8: [0.97s 9.86% DONE] +MUTANT: ./foo.mutant.34.py +*** Original +--- Mutant +*************** +*** 5,11 **** + print(x) + x = 20 + while (x > (10-1)): +! x -= 1 + return x + + def main(): +--- 5,11 ---- + print(x) + x = 20 + while (x > (10-1)): +! x *= 1 + return x + + def main(): + + +RUNNING ./foo.mutant.34.py... + +HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION) +./foo.mutant.34.py KILLED IN 5.077595233917236 (RETURN CODE None) + RUNNING SCORE: 0.75 +================================================================================ +#9: [6.06s 11.27% DONE] +MUTANT: ./foo.mutant.66.py +*** Original +--- Mutant +*************** +*** 13,17 **** + v = myfunction(y) + assert v==10 + +! if __name__ == '__main__': + main() +--- 13,17 ---- + v = myfunction(y) + assert v==10 + +! if __name__ >= '__main__': + main() + + +RUNNING ./foo.mutant.66.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.66.py KILLED IN 0.12610316276550293 (RETURN CODE 1) + RUNNING SCORE: 0.7777777777777778 +================================================================================ +#10: [6.19s 12.68% DONE] +MUTANT: ./foo.mutant.18.py +*** Original +--- Mutant +*************** +*** 3,9 **** + def myfunction(x): + if (x < 6): + print(x) +! x = 20 + while (x > (10-1)): + x -= 1 + return x +--- 3,9 ---- + def myfunction(x): + if (x < 6): + print(x) +! pass + while (x > (10-1)): + x -= 1 + return x + + +RUNNING ./foo.mutant.18.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.18.py KILLED IN 0.11788296699523926 (RETURN CODE 1) + RUNNING SCORE: 0.8 +================================================================================ +#11: [6.33s 14.08% DONE] +MUTANT: ./foo.mutant.30.py +*** Original +--- Mutant +*************** +*** 4,10 **** + if (x < 6): + print(x) + x = 20 +! while (x > (10-1)): + x -= 1 + return x + +--- 4,10 ---- + if (x < 6): + print(x) + x = 20 +! while (x > (10-(1+1))): + x -= 1 + return x + + + +RUNNING ./foo.mutant.30.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.30.py KILLED IN 0.15216326713562012 (RETURN CODE 1) + RUNNING SCORE: 0.8181818181818182 +================================================================================ +#12: [6.49s 15.49% DONE] +MUTANT: ./foo.mutant.68.py +*** Original +--- Mutant +*************** +*** 13,17 **** + v = myfunction(y) + assert v==10 + +! if __name__ == '__main__': + main() +--- 13,17 ---- + v = myfunction(y) + assert v==10 + +! if __name__ < '__main__': + main() + + +RUNNING ./foo.mutant.68.py... +./foo.mutant.68.py NOT KILLED + RUNNING SCORE: 0.75 +================================================================================ +#13: [6.64s 16.9% DONE] +MUTANT: ./foo.mutant.36.py +*** Original +--- Mutant +*************** +*** 5,11 **** + print(x) + x = 20 + while (x > (10-1)): +! x -= 1 + return x + + def main(): +--- 5,11 ---- + print(x) + x = 20 + while (x > (10-1)): +! x %= 1 + return x + + def main(): + + +RUNNING ./foo.mutant.36.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.36.py KILLED IN 0.11724424362182617 (RETURN CODE 1) + RUNNING SCORE: 0.7692307692307693 +================================================================================ +#14: [6.76s 18.31% DONE] +MUTANT: ./foo.mutant.59.py +*** Original +--- Mutant +*************** +*** 11,17 **** + def main(): + y = 4 + v = myfunction(y) +! assert v==10 + + if __name__ == '__main__': + main() +--- 11,17 ---- + def main(): + y = 4 + v = myfunction(y) +! assert v==1 + + if __name__ == '__main__': + main() + + +RUNNING ./foo.mutant.59.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==1 + ^^^^ +AssertionError +./foo.mutant.59.py KILLED IN 0.1158909797668457 (RETURN CODE 1) + RUNNING SCORE: 0.7857142857142857 +================================================================================ +#15: [6.9s 19.72% DONE] +MUTANT: ./foo.mutant.23.py +*** Original +--- Mutant +*************** +*** 4,10 **** + if (x < 6): + print(x) + x = 20 +! while (x > (10-1)): + x -= 1 + return x + +--- 4,10 ---- + if (x < 6): + print(x) + x = 20 +! while (x == (10-1)): + x -= 1 + return x + + + +RUNNING ./foo.mutant.23.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.23.py KILLED IN 0.1274251937866211 (RETURN CODE 1) + RUNNING SCORE: 0.8 +================================================================================ +#16: [7.04s 21.13% DONE] +MUTANT: ./foo.mutant.46.py +*** Original +--- Mutant +*************** +*** 9,15 **** + return x + + def main(): +! y = 4 + v = myfunction(y) + assert v==10 + +--- 9,15 ---- + return x + + def main(): +! y = 0 + v = myfunction(y) + assert v==10 + + + +RUNNING ./foo.mutant.46.py... +0 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.46.py KILLED IN 0.11632299423217773 (RETURN CODE 1) + RUNNING SCORE: 0.8125 +================================================================================ +#17: [7.2s 22.54% DONE] +MUTANT: ./foo.mutant.9.py +*** Original +--- Mutant +*************** +*** 1,7 **** + from __future__ import print_function + + def myfunction(x): +! if (x < 6): + print(x) + x = 20 + while (x > (10-1)): +--- 1,7 ---- + from __future__ import print_function + + def myfunction(x): +! if (x < (6+1)): + print(x) + x = 20 + while (x > (10-1)): + + +RUNNING ./foo.mutant.9.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.9.py KILLED IN 0.11342883110046387 (RETURN CODE 1) + RUNNING SCORE: 0.8235294117647058 +================================================================================ +#18: [7.32s 23.94% DONE] +MUTANT: ./foo.mutant.1.py +*** Original +--- Mutant +*************** +*** 1,7 **** + from __future__ import print_function + + def myfunction(x): +! if (x < 6): + print(x) + x = 20 + while (x > (10-1)): +--- 1,7 ---- + from __future__ import print_function + + def myfunction(x): +! if (x > 6): + print(x) + x = 20 + while (x > (10-1)): + + +RUNNING ./foo.mutant.1.py... +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.1.py KILLED IN 0.11642646789550781 (RETURN CODE 1) + RUNNING SCORE: 0.8333333333333334 +================================================================================ +#19: [7.44s 25.35% DONE] +MUTANT: ./foo.mutant.39.py +*** Original +--- Mutant +*************** +*** 5,11 **** + print(x) + x = 20 + while (x > (10-1)): +! x -= 1 + return x + + def main(): +--- 5,11 ---- + print(x) + x = 20 + while (x > (10-1)): +! x -= 0 + return x + + def main(): + + +RUNNING ./foo.mutant.39.py... + +HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION) +./foo.mutant.39.py KILLED IN 5.062973737716675 (RETURN CODE None) + RUNNING SCORE: 0.8421052631578947 +================================================================================ +#20: [12.51s 26.76% DONE] +MUTANT: ./foo.mutant.40.py +*** Original +--- Mutant +*************** +*** 5,11 **** + print(x) + x = 20 + while (x > (10-1)): +! x -= 1 + return x + + def main(): +--- 5,11 ---- + print(x) + x = 20 + while (x > (10-1)): +! x -= -1 + return x + + def main(): + + +RUNNING ./foo.mutant.40.py... + +HAD TO TERMINATE ANALYSIS (TIMEOUT OR EXCEPTION) +./foo.mutant.40.py KILLED IN 5.099248647689819 (RETURN CODE None) + RUNNING SCORE: 0.85 +================================================================================ +#21: [17.62s 28.17% DONE] +MUTANT: ./foo.mutant.5.py +*** Original +--- Mutant +*************** +*** 1,7 **** + from __future__ import print_function + + def myfunction(x): +! if (x < 6): + print(x) + x = 20 + while (x > (10-1)): +--- 1,7 ---- + from __future__ import print_function + + def myfunction(x): +! if (x != 6): + print(x) + x = 20 + while (x > (10-1)): + + +RUNNING ./foo.mutant.5.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.5.py KILLED IN 0.06272053718566895 (RETURN CODE 1) + RUNNING SCORE: 0.8571428571428571 +================================================================================ +#22: [17.69s 29.58% DONE] +MUTANT: ./foo.mutant.55.py +*** Original +--- Mutant +*************** +*** 11,17 **** + def main(): + y = 4 + v = myfunction(y) +! assert v==10 + + if __name__ == '__main__': + main() +--- 11,17 ---- + def main(): + y = 4 + v = myfunction(y) +! assert v>=10 + + if __name__ == '__main__': + main() + + +RUNNING ./foo.mutant.55.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v>=10 + ^^^^^ +AssertionError +./foo.mutant.55.py KILLED IN 0.1637721061706543 (RETURN CODE 1) + RUNNING SCORE: 0.8636363636363636 +================================================================================ +#23: [17.86s 30.99% DONE] +MUTANT: ./foo.mutant.6.py +*** Original +--- Mutant +*************** +*** 1,7 **** + from __future__ import print_function + + def myfunction(x): +! if (x < 6): + print(x) + x = 20 + while (x > (10-1)): +--- 1,7 ---- + from __future__ import print_function + + def myfunction(x): +! if (x < 0): + print(x) + x = 20 + while (x > (10-1)): + + +RUNNING ./foo.mutant.6.py... +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.6.py KILLED IN 0.062036752700805664 (RETURN CODE 1) + RUNNING SCORE: 0.8695652173913043 +================================================================================ +#24: [17.93s 32.39% DONE] +MUTANT: ./foo.mutant.65.py +*** Original +--- Mutant +*************** +*** 13,17 **** + v = myfunction(y) + assert v==10 + +! if __name__ == '__main__': + main() +--- 13,17 ---- + v = myfunction(y) + assert v==10 + +! if __name__ <= '__main__': + main() + + +RUNNING ./foo.mutant.65.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.65.py KILLED IN 0.1142740249633789 (RETURN CODE 1) + RUNNING SCORE: 0.875 +================================================================================ +#25: [18.05s 33.8% DONE] +MUTANT: ./foo.mutant.37.py +*** Original +--- Mutant +*************** +*** 5,11 **** + print(x) + x = 20 + while (x > (10-1)): +! x -= 1 + return x + + def main(): +--- 5,11 ---- + print(x) + x = 20 + while (x > (10-1)): +! x =- 1 + return x + + def main(): + + +RUNNING ./foo.mutant.37.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.37.py KILLED IN 0.11506485939025879 (RETURN CODE 1) + RUNNING SCORE: 0.88 +================================================================================ +#26: [18.2s 35.21% DONE] +MUTANT: ./foo.mutant.56.py +*** Original +--- Mutant +*************** +*** 11,17 **** + def main(): + y = 4 + v = myfunction(y) +! assert v==10 + + if __name__ == '__main__': + main() +--- 11,17 ---- + def main(): + y = 4 + v = myfunction(y) +! assert v>10 + + if __name__ == '__main__': + main() + + +RUNNING ./foo.mutant.56.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v>10 + ^^^^ +AssertionError +./foo.mutant.56.py KILLED IN 0.17843103408813477 (RETURN CODE 1) + RUNNING SCORE: 0.8846153846153846 +================================================================================ +#27: [18.39s 36.62% DONE] +MUTANT: ./foo.mutant.61.py +*** Original +--- Mutant +*************** +*** 11,17 **** + def main(): + y = 4 + v = myfunction(y) +! assert v==10 + + if __name__ == '__main__': + main() +--- 11,17 ---- + def main(): + y = 4 + v = myfunction(y) +! assert v==(10+1) + + if __name__ == '__main__': + main() + + +RUNNING ./foo.mutant.61.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==(10+1) + ^^^^^^^^^ +AssertionError +./foo.mutant.61.py KILLED IN 0.06344199180603027 (RETURN CODE 1) + RUNNING SCORE: 0.8888888888888888 +================================================================================ +#28: [18.48s 38.03% DONE] +MUTANT: ./foo.mutant.41.py +*** Original +--- Mutant +*************** +*** 5,11 **** + print(x) + x = 20 + while (x > (10-1)): +! x -= 1 + return x + + def main(): +--- 5,11 ---- + print(x) + x = 20 + while (x > (10-1)): +! x -= (1+1) + return x + + def main(): + + +RUNNING ./foo.mutant.41.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.41.py KILLED IN 0.16735219955444336 (RETURN CODE 1) + RUNNING SCORE: 0.8928571428571429 +================================================================================ +#29: [18.66s 39.44% DONE] +MUTANT: ./foo.mutant.57.py +*** Original +--- Mutant +*************** +*** 11,17 **** + def main(): + y = 4 + v = myfunction(y) +! assert v==10 + + if __name__ == '__main__': + main() +--- 11,17 ---- + def main(): + y = 4 + v = myfunction(y) +! assert v<10 + + if __name__ == '__main__': + main() + + +RUNNING ./foo.mutant.57.py... +4 +./foo.mutant.57.py NOT KILLED + RUNNING SCORE: 0.8620689655172413 +================================================================================ +#30: [18.72s 40.85% DONE] +MUTANT: ./foo.mutant.21.py +*** Original +--- Mutant +*************** +*** 4,10 **** + if (x < 6): + print(x) + x = 20 +! while (x > (10-1)): + x -= 1 + return x + +--- 4,10 ---- + if (x < 6): + print(x) + x = 20 +! while (x > (10%1)): + x -= 1 + return x + + + +RUNNING ./foo.mutant.21.py... +4 +Traceback (most recent call last): + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 17, in + main() + File "/mnt/data/universalmutator-master/universalmutator-master/examples/foo.py", line 14, in main + assert v==10 + ^^^^^ +AssertionError +./foo.mutant.21.py KILLED IN 0.11383891105651855 (RETURN CODE 1) + RUNNING SCORE: 0.8666666666666667 +================================================================================ +#31: [18.84s 42.25% DONE] +MUTANT: ./foo.mutant.69.py +*** Original +--- Mutant +*************** +*** 13,17 **** + v = myfunction(y) + assert v==10 + +! if __name__ == '__main__': + main() +--- 13,17 ---- + v = myfunction(y) + assert v==10 + +! if not (__name__ == '__main__'): + main() + + +RUNNING ./foo.mutant.69.py... +./foo.mutant.69.py NOT KILLED + RUNNING SCORE: 0.8387096774193549 +================================================================================ +#32: [18.94s 43.66% DONE] +MUTANT: ./foo.mutant.63.py +*** Original +--- Mutant +*************** +*** 11,17 **** + def main(): + y = 4 + v = myfunction(y) +! assert v==10 + + if __name__ == '__main__': + main() +--- 11,17 ---- + def main(): + y = 4 + v = myfunction(y) +! pass + + if __name__ == '__main__': + main() + + +RUNNING ./foo.mutant.63.py... +4 +./foo.mutant.63.py NOT KILLED + RUNNING SCORE: 0.8125 +================================================================================ +#33: [19.08s 45.07% DONE] +MUTANT: ./foo.mutant.33.py +*** Original +--- Mutant +*************** +*** 5,11 **** + print(x) + x = 20 + while (x > (10-1)): +! x -= 1 + return x + + def main(): +--- 5,11 ---- + print(x) + x = 20 + while (x > (10-1)): +! x += 1 + return x + + def main(): + + +RUNNING ./foo.mutant.33.py... diff --git a/examples/another-file.tolk b/examples/another-file.tolk new file mode 100644 index 0000000..9fdc6ee --- /dev/null +++ b/examples/another-file.tolk @@ -0,0 +1 @@ +// placeholder module for import tests diff --git a/examples/foo.fc b/examples/foo.fc new file mode 100644 index 0000000..6c65fb7 --- /dev/null +++ b/examples/foo.fc @@ -0,0 +1,16 @@ +;; Example FunC file for UniversalMutator smoke-tests. +;; Intentionally small; not guaranteed to compile in every environment. + +(int) sum(int a, int b) { + ;; basic ops + throw_unless(33, a != 0); + throw_if(34, a == 0); + if (a < b) { + return a + b; + } + return a - b; +} + +() main() impure { + ;; no-op entrypoint for compilation +} diff --git a/examples/foo.py.um.backup b/examples/foo.py.um.backup new file mode 100644 index 0000000..f7027a7 --- /dev/null +++ b/examples/foo.py.um.backup @@ -0,0 +1,17 @@ +from __future__ import print_function + +def myfunction(x): + if (x < 6): + print(x) + x = 20 + while (x > (10-1)): + x -= 1 + return x + +def main(): + y = 4 + v = myfunction(y) + assert v==10 + +if __name__ == '__main__': + main() diff --git a/examples/foo.tact b/examples/foo.tact new file mode 100644 index 0000000..5614ee0 --- /dev/null +++ b/examples/foo.tact @@ -0,0 +1,13 @@ +// Example Tact file for UniversalMutator smoke-tests. +// Intentionally small; not guaranteed to compile in every environment. + +import "@stdlib/deploy"; + +fun sum(a: Int, b: Int): Int { + // basic ops + boolean literals + if (a < b && true) { + return a + b; + } + throwUnless(1024, a != 0); + return a - b; +} diff --git a/examples/foo.tolk b/examples/foo.tolk new file mode 100644 index 0000000..01ff9e8 --- /dev/null +++ b/examples/foo.tolk @@ -0,0 +1,17 @@ +// Example Tolk file for UniversalMutator smoke-tests. +// Intentionally small; not guaranteed to compile in every environment. + +import "another-file"; + +fun sum(a: int, b: int): int { + val coeff = 5; + if (a < b && true) { + return a + b; + } + return a - b; +} + +// Minimal entrypoint to satisfy the compiler. +fun onInternalMessage() { + // no-op +} diff --git a/setup.py b/setup.py index b2e62f8..4243023 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,10 @@ 'static/vyper.rules', 'static/none.rules', 'static/fe.rules', + # TON languages + 'static/tact.rules', + 'static/func.rules', + 'static/tolk.rules', 'comby/universal.rules', 'comby/c_like.rules', 'comby/c.rules', @@ -47,6 +51,10 @@ 'comby/solidity.rules', 'comby/vyper.rules', 'comby/none.rules', + # TON languages + 'comby/tact.rules', + 'comby/func.rules', + 'comby/tolk.rules', ] }, license='MIT', diff --git a/tests/test_ton_examples.py b/tests/test_ton_examples.py new file mode 100644 index 0000000..d29f842 --- /dev/null +++ b/tests/test_ton_examples.py @@ -0,0 +1,72 @@ +from __future__ import print_function + +import os +import subprocess +from unittest import TestCase + + +class TestTONExamples(TestCase): + def setUp(self): + os.chdir("examples") + + def tearDown(self): + # Clean up generated mutants + for fn in os.listdir("."): + if ".mutant." in fn: + try: + os.remove(fn) + except Exception: + pass + if os.path.exists(".tmp_mutant." + str(os.getpid()) + ".tact"): + try: + os.remove(".tmp_mutant." + str(os.getpid()) + ".tact") + except Exception: + pass + if os.path.exists(".tmp_mutant." + str(os.getpid()) + ".fc"): + try: + os.remove(".tmp_mutant." + str(os.getpid()) + ".fc") + except Exception: + pass + if os.path.exists(".tmp_mutant." + str(os.getpid()) + ".tolk"): + try: + os.remove(".tmp_mutant." + str(os.getpid()) + ".tolk") + except Exception: + pass + + os.chdir("..") + + def _run_mutate(self, filename, language, rulefile): + with open("mutate.out", "w") as f: + r = subprocess.call( + [ + "mutate", + filename, + language, + "--only", + rulefile, + "--noCheck", + ], + stdout=f, + stderr=f, + ) + self.assertEqual(r, 0) + with open("mutate.out", "r") as f: + out = f.read() + self.assertIn("MUTANTS GENERATED", out) + # Ensure at least one mutant was produced + gen = None + for line in out.splitlines(): + if "MUTANTS GENERATED" in line: + gen = int(line.split()[0]) + break + self.assertIsNotNone(gen) + self.assertTrue(gen > 0) + + def test_tact(self): + self._run_mutate("foo.tact", "tact", "tact.rules") + + def test_func(self): + self._run_mutate("foo.fc", "func", "func.rules") + + def test_tolk(self): + self._run_mutate("foo.tolk", "tolk", "tolk.rules") diff --git a/universalmutator.egg-info/PKG-INFO b/universalmutator.egg-info/PKG-INFO new file mode 100644 index 0000000..49acffe --- /dev/null +++ b/universalmutator.egg-info/PKG-INFO @@ -0,0 +1,152 @@ +Metadata-Version: 2.4 +Name: universalmutator +Version: 1.1.12 +Summary: Universal regexp-based mutation tool +Home-page: https://github.com/agroce/universalmutator +License: MIT +Keywords: testing mutation mutation-testing +Classifier: Intended Audience :: Developers +Classifier: Development Status :: 4 - Beta +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: comby +Requires-Dist: python-levenshtein +Requires-Dist: tabulate +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: home-page +Dynamic: keywords +Dynamic: license +Dynamic: license-file +Dynamic: requires-dist +Dynamic: summary + +This is a tool based on source-based rewrite of code lines for mutation generation, including +multi-language rules aided by special rules for languages or even projects. Originally, the approach used only regular expressions, +treating code as text. However, there is also a mode that can use the [Comby](https://github.com/comby-tools/comby) tool +for more sophisticated mutation that produces fewer invalid mutants. Regular-expression based mutation works well, in our experience; +comby-aided mutation works even better. The key advantage of either approach is that the tool can probably mutate approximately *any* interesting source code you have, and language changes don't force +rewriting of the mutation tool. To use the comby mode, just make sure comby is installed and add `--comby` when you run `mutate`. + +More information on this project can be found in a [2024 FSE paper](https://agroce.github.io/fse24.pdf), and in the original [2018 ICSE Tool Paper](https://agroce.github.io/icse18t.pdf). + +A [guest blog post](https://blog.trailofbits.com/2019/01/23/fuzzing-an-api-with-deepstate-part-2/) for Trail of Bits shows how to use the universalmutator to help improve a C/C++ API fuzzing effort using [DeepState](https://github.com/trailofbits/deepstate) and libFuzzer. + +The universalmutator has support for extracting coverage information to guide mutation from the [TSTL](https://github.com/agroce/tstl) testing tool for Python. + +HOW TO USE IT +============= + +To use this, you should really just do: + +`pip install universalmutator` + +then + +`mutate --help` + +SIMPLE EXAMPLE USAGE +==================== + +`mutate foo.py` + +or + +`mutate foo.swift` + +should, if you have the appropriate compilers installed, generate a bunch of valid, non-trivially redundant, mutants. + + +A MORE COMPLEX EXAMPLE +====================== + +Sometimes the mutated code needs to be built with a more complicated command than a simple compiler call, and of course you want help discovering which mutants are killed and not killed. For example, to mutate and test mutants for the mandelbrot plotting example included in the PROGRAMMING RUST book (http://shop.oreilly.com/product/0636920040385.do), just do this: + + + git clone https://github.com/ProgrammingRust/mandelbrot + cd mandelbrot + cargo build + target/debug/mandelbrot origmandel.png 1000x750 -1.20,0.35 -1,0.20 + mkdir mutants + mutate src/main.rs --mutantDir mutants --noCheck + analyze_mutants src/main.rs "cargo clean; cargo build; rm mandel.png; target/debug/mandelbrot mandel.png 1000x750 -1.20,0.35 -1,0.20; diff mandel.png origmandel.png" --mutantDir mutants + +(It will go faster if you edit `main.rs` to lower the maximum number of threads used to something like 8, not 90.) At the moment, this won't use any Trivial Compiler Equivalence, but still kills about 60% of the 1000+ mutants. The killed mutant filenames will be in `killed.txt` and the non-killed ones in `not-killed.txt`. + +Working with something like maven is very similar, except you can probably edit the complicated build/clean stuff to just a 'mvn test' or similar. + +CURRENTLY SUPPORTED LANGUAGES +============================= + +The tool will likely mutate other things, if you tell it they are "c" or something, but there is auto-detection based on file ending and specific rule support for: + +``` +C +C++ +Java +JavaScript +Python +Swift +R +Rust +Go +Lisp +Fortran +Solidity +Vyper +Fe +``` + +(the last three are smart contract languages for the Ethereum blockchain). + +All but C, C++, JavaScript, and Go will try, by default, to compile the mutated +file and use TCE to detect redundancy. Of course, build dependencies +may frustrate this process, in which case --noCheck will turn off TCE +and just dump all the mutants in the directory, for pruning using a +real build process. In the long run, we plan to integrate with +standard build systems to avoid this problem, and with automated test +generation systems such as TSTL (https://github.com/agroce/tstl) for +Python or Echidna for Solidity +(https://github.com/trailofbits/echidna). Even now, however, with +`analyze_mutants` it is fairly easy to set up automatic evaluation of +your automated test generator. + +MUTATING SOLIDITY CODE +====================== + +The universalmutator has been most frequently applied to smart +contracts written in the Solidity language. It supports a few special +features that are particularly useful in this context. + +First, +Solidity libraries are often written with only `internal` functions +--- and the compiler will not emit code for such functions if you +compile a library by itself, resulting in no non-redundant mutants. +In order to handle this case, `mutate` can take a `--compile` option +that specifies another file (a contract using the library, or the +tests in question) that is used to check whether mutants are +redundant. + +Second, swapping adjacent lines of code is a seldom-used mutation +operator that is unusually attractive in a Solidity context because +swapping a state-changing operation and a requirement may reveal that +testing is incapable of detecting some +[re-entrancy](https://github.com/crytic/not-so-smart-contracts/tree/master/reentrancy) +vulnerabilities. The testing may notice the absence of the check, but +not a mis-ordering, and these mutants may reveal that. To add code +swaps to your mutations, just add `--swap` to the `mutate` call. Note +that swaps work in any language; they are just particularly appealing +for smart contracts. + +MORE INFORMATON +=============== + +For much more information, again see https://agroce.github.io/icse18t.pdf -- demo/tool paper at ICSE 18 and especially our full FSE 2024 paper -- https://agroce.github.io/fse24.pdf -- the latter discusses the latest version of the tool/approach, and includes a comparison with many other mutation testing tools. + +The aim of this project is partly to see how quickly mutation can be applied to new languages, partly how much the work of a tool can be +offloaded to the compiler / test analysis tools. + +Authors: Alex Groce, Josie Holmes, Darko Marinov, August Shi, Lingming Zhang, Kush Jain, Rijnard van Tonder, Sourav Deb diff --git a/universalmutator.egg-info/SOURCES.txt b/universalmutator.egg-info/SOURCES.txt new file mode 100644 index 0000000..1be8976 --- /dev/null +++ b/universalmutator.egg-info/SOURCES.txt @@ -0,0 +1,78 @@ +LICENSE +README.md +setup.py +tests/test_foo_example.py +tests/test_ton_examples.py +universalmutator/__init__.py +universalmutator/analyze.py +universalmutator/c_handler.py +universalmutator/checkcov.py +universalmutator/cpp_handler.py +universalmutator/fe_handler.py +universalmutator/findmissing.py +universalmutator/fortran_handler.py +universalmutator/func_handler.py +universalmutator/genmutants.py +universalmutator/go_handler.py +universalmutator/intersect.py +universalmutator/java_handler.py +universalmutator/javascript_handler.py +universalmutator/lisp_handler.py +universalmutator/mutator.py +universalmutator/prioritize.py +universalmutator/prune.py +universalmutator/python_handler.py +universalmutator/r_handler.py +universalmutator/rust_handler.py +universalmutator/server.py +universalmutator/show.py +universalmutator/solidity_handler.py +universalmutator/swift_handler.py +universalmutator/tact_handler.py +universalmutator/tolk_handler.py +universalmutator/utils.py +universalmutator/vyper_handler.py +universalmutator.egg-info/PKG-INFO +universalmutator.egg-info/SOURCES.txt +universalmutator.egg-info/dependency_links.txt +universalmutator.egg-info/entry_points.txt +universalmutator.egg-info/requires.txt +universalmutator.egg-info/top_level.txt +universalmutator/comby/c.rules +universalmutator/comby/c_like.rules +universalmutator/comby/cpp.rules +universalmutator/comby/fortran.rules +universalmutator/comby/func.rules +universalmutator/comby/go.rules +universalmutator/comby/java.rules +universalmutator/comby/lisp.rules +universalmutator/comby/none.rules +universalmutator/comby/python.rules +universalmutator/comby/r.rules +universalmutator/comby/rust.rules +universalmutator/comby/solidity.rules +universalmutator/comby/swift.rules +universalmutator/comby/tact.rules +universalmutator/comby/tolk.rules +universalmutator/comby/universal.rules +universalmutator/comby/vyper.rules +universalmutator/static/c.rules +universalmutator/static/c_like.rules +universalmutator/static/cpp.rules +universalmutator/static/fe.rules +universalmutator/static/fortran.rules +universalmutator/static/func.rules +universalmutator/static/go.rules +universalmutator/static/java.rules +universalmutator/static/javascript.rules +universalmutator/static/lisp.rules +universalmutator/static/none.rules +universalmutator/static/python.rules +universalmutator/static/r.rules +universalmutator/static/rust.rules +universalmutator/static/solidity.rules +universalmutator/static/swift.rules +universalmutator/static/tact.rules +universalmutator/static/tolk.rules +universalmutator/static/universal.rules +universalmutator/static/vyper.rules \ No newline at end of file diff --git a/universalmutator.egg-info/dependency_links.txt b/universalmutator.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/universalmutator.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/universalmutator.egg-info/entry_points.txt b/universalmutator.egg-info/entry_points.txt new file mode 100644 index 0000000..4ebc43d --- /dev/null +++ b/universalmutator.egg-info/entry_points.txt @@ -0,0 +1,9 @@ +[console_scripts] +analyze_mutants = universalmutator.analyze:main +check_covered = universalmutator.checkcov:main +intersect_mutants = universalmutator.intersect:main +mutant_server = universalmutator.server:main +mutate = universalmutator.genmutants:main +prioritize_mutants = universalmutator.prioritize:main +prune_mutants = universalmutator.prune:main +show_mutants = universalmutator.show:main diff --git a/universalmutator.egg-info/requires.txt b/universalmutator.egg-info/requires.txt new file mode 100644 index 0000000..f68a6dc --- /dev/null +++ b/universalmutator.egg-info/requires.txt @@ -0,0 +1,3 @@ +comby +python-levenshtein +tabulate diff --git a/universalmutator.egg-info/top_level.txt b/universalmutator.egg-info/top_level.txt new file mode 100644 index 0000000..132847f --- /dev/null +++ b/universalmutator.egg-info/top_level.txt @@ -0,0 +1 @@ +universalmutator diff --git a/universalmutator/comby/func.rules b/universalmutator/comby/func.rules new file mode 100644 index 0000000..8b5acdb --- /dev/null +++ b/universalmutator/comby/func.rules @@ -0,0 +1,8 @@ +# FunC (TON) comby rules. + +;;:[comment] ==> DO_NOT_MUTATE + +#include :[rest] ==> DO_NOT_MUTATE + +throw_if(:[x]) ==> throw_unless(:[x]) +throw_unless(:[x]) ==> throw_if(:[x]) \ No newline at end of file diff --git a/universalmutator/comby/tact.rules b/universalmutator/comby/tact.rules new file mode 100644 index 0000000..e96c92f --- /dev/null +++ b/universalmutator/comby/tact.rules @@ -0,0 +1,12 @@ +# Tact (TON) comby rules. + +//:[comment] ==> DO_NOT_MUTATE +///:[comment] ==> DO_NOT_MUTATE + +import :[rest] ==> DO_NOT_MUTATE + +true ==> false +false ==> true + +throwIf(:[x]) ==> throwUnless(:[x]) +throwUnless(:[x]) ==> throwIf(:[x]) \ No newline at end of file diff --git a/universalmutator/comby/tolk.rules b/universalmutator/comby/tolk.rules new file mode 100644 index 0000000..4c9a25b --- /dev/null +++ b/universalmutator/comby/tolk.rules @@ -0,0 +1,10 @@ +# Tolk (TON) comby rules. + +//:[comment] ==> DO_NOT_MUTATE + +import :[rest] ==> DO_NOT_MUTATE + +true ==> false +false ==> true + +val :[rest] ==> var :[rest] \ No newline at end of file diff --git a/universalmutator/func_handler.py b/universalmutator/func_handler.py new file mode 100644 index 0000000..2e3d155 --- /dev/null +++ b/universalmutator/func_handler.py @@ -0,0 +1,50 @@ +"""FunC handler. + +Default behavior is "dumb" (treat every mutant as VALID), like C/C++. + +To enable compilation-based validity filtering during mutant generation, set: + + UM_FUNC_CMD='func -o /dev/null MUTANT' + +The command must return 0 for a valid mutant. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess + + +_CMD = os.environ.get("UM_FUNC_CMD") + +dumb = _CMD is None + + +def handler(tmpMutantName, mutant, sourceFile, uniqueMutants, compileFile=None): + cmd = _CMD + if cmd is None: + return "VALID" + + target = compileFile if compileFile is not None else sourceFile + pid = os.getpid() + backupName = None + + if "MUTANT" not in cmd: + backupName = target + ".um.backup." + str(pid) + shutil.copy(target, backupName) + shutil.copy(tmpMutantName, target) + + try: + outFile = f".um.func_output.{pid}" + with open(outFile, "w") as f: + r = subprocess.call( + [cmd.replace("MUTANT", tmpMutantName)], + shell=True, + stderr=f, + stdout=f, + ) + return "VALID" if r == 0 else "INVALID" + finally: + if backupName is not None: + shutil.copy(backupName, target) diff --git a/universalmutator/genmutants.py b/universalmutator/genmutants.py index 84c800e..ef1a9b5 100644 --- a/universalmutator/genmutants.py +++ b/universalmutator/genmutants.py @@ -3,7 +3,7 @@ import os import random -from re import T +import re import sys import shutil import subprocess @@ -25,6 +25,11 @@ import universalmutator.r_handler as r_handler import universalmutator.fortran_handler as fortran_handler +# TON languages +import universalmutator.tact_handler as tact_handler +import universalmutator.func_handler as func_handler +import universalmutator.tolk_handler as tolk_handler + def nullHandler(tmpMutantName, mutant, sourceFile, uniqueMutants): return "VALID" @@ -112,7 +117,13 @@ def main(): ".R": "r", ".sol": "solidity", ".vy": "vyper", - ".fe": "fe"} + ".fe": "fe", + + # TON languages + ".tact": "tact", + ".fc": "func", + ".func": "func", + ".tolk": "tolk"} print("*** UNIVERSALMUTATOR ***") @@ -262,6 +273,9 @@ def main(): if mdir[-1] != "/": mdir += "/" + # NEW: ensure mutant output directory exists + os.makedirs(mdir, exist_ok=True) + ignoreFile = None try: ignorepos = args.index("--ignore") @@ -305,7 +319,12 @@ def main(): "lisp": lisp_handler, "solidity": solidity_handler, "vyper": vyper_handler, - "fe": fe_handler} + "fe": fe_handler, + + # TON languages + "tact": tact_handler, + "func": func_handler, + "tolk": tolk_handler} cLikeLanguages = [ "c", @@ -316,7 +335,11 @@ def main(): "c++", "rust", "solidity", - "go"] + "go", + + # TON languages have C-like syntax (especially Tolk) + "tolk" + ] try: handlers["custom"] == custom_handler @@ -327,7 +350,7 @@ def main(): base = (".".join((sourceFile.split(".")[:-1]))).split("/")[-1] ending = "." + sourceFile.split(".")[-1] - if "--only" not in args: + if "--only" not in args: if len(args) < 3: try: language = languages[ending] @@ -387,11 +410,26 @@ def main(): mutants = [] if comby: - mutants = mutator.mutants_comby(source, ruleFiles=rules, mutateTestCode=mutateTestCode, mutateBoth=mutateBoth, - ignorePatterns=ignorePatterns, ignoreStringOnly=not mutateInStrings, fuzzing=fuzz, language=ending) + mutants = mutator.mutants_comby( + source, + ruleFiles=rules, + mutateTestCode=mutateTestCode, + mutateBoth=mutateBoth, + ignorePatterns=ignorePatterns, + ignoreStringOnly=not mutateInStrings, + fuzzing=fuzz, + language=ending + ) else: - mutants = mutator.mutants(source, ruleFiles=rules, mutateTestCode=mutateTestCode, mutateBoth=mutateBoth, - ignorePatterns=ignorePatterns, ignoreStringOnly=not mutateInStrings, fuzzing=fuzz) + mutants = mutator.mutants( + source, + ruleFiles=rules, + mutateTestCode=mutateTestCode, + mutateBoth=mutateBoth, + ignorePatterns=ignorePatterns, + ignoreStringOnly=not mutateInStrings, + fuzzing=fuzz + ) if fuzz: if len(mutants) == 0: sys.exit(255) @@ -456,17 +494,17 @@ def main(): fastCheckLine(mutant, source, sourceFile, uniqueMutants, compileFile, handler, deadCodeLines, interestingLines, tmpMutantName, mutant[0]) if mutant[0] in deadCodeLines: continue - + if comby: sourceJoined = ''.join(source) print("PROCESSING MUTANT:", - "range" + str(mutant[0]) + ":", sourceJoined[mutant[0][0]:mutant[0][1]].replace("\n", "\\n"), " ==> ", mutant[1], end="...") + "range" + str(mutant[0]) + ":", sourceJoined[mutant[0][0]:mutant[0][1]].replace("\n", "\\n"), " ==> ", mutant[1], end="...") else: print("PROCESSING MUTANT:", - str(mutant[0]) + ":", source[mutant[0] - 1][:-1], " ==> ", mutant[1][:-1], end="...") + str(mutant[0]) + ":", source[mutant[0] - 1][:-1], " ==> ", mutant[1][:-1], end="...") if (not comby) and showRules: print("(FROM:", mutant[2][1], end=")...") - + if comby: mCreated = mutator.makeMutantComby(sourceJoined, mutant, tmpMutantName) else: @@ -475,7 +513,7 @@ def main(): print("REDUNDANT (SOURCE COPY!)") redundantMutants.append(mutant) continue - + if compileFile is None: mutantResult = handler(tmpMutantName, mutant, sourceFile, uniqueMutants) else: @@ -536,11 +574,11 @@ def main(): print(len(validMutants), "VALID MUTANTS") print(len(invalidMutants), "INVALID MUTANTS") print(len(redundantMutants), "REDUNDANT MUTANTS") - + totalMutants = len(validMutants) + len(invalidMutants) + len(redundantMutants) valid_rate = 0 if totalMutants == 0 else (len(validMutants) * 100.0)/totalMutants print(f"Valid Percentage: {valid_rate}%") - + (rules, ignoreRules, skipRules) = mutator.parseRules(rules, comby= comby) if printStat: @@ -589,14 +627,14 @@ def printRulesStat(rules, validMutants, invalidMutants): lhs, rhs = mutant[-1] if (lhs,rhs) not in valid_cnt: valid_cnt[(lhs,rhs)] = 0 - valid_cnt[(lhs,rhs)] += 1 + valid_cnt[(lhs,rhs)] += 1 for mutant in invalidMutants: lhs, rhs = mutant[-1] if (lhs,rhs) not in invalid_cnt: invalid_cnt[(lhs,rhs)] = 0 - invalid_cnt[(lhs,rhs)] += 1 - + invalid_cnt[(lhs,rhs)] += 1 + fis = open("rules_count.txt", "w") i = 0 table = [] @@ -610,7 +648,7 @@ def printRulesStat(rules, validMutants, invalidMutants): i += 1 table.append([f'{i}',f'{lhs} ==> {rhs}', f'{valid_cnt[(lhs,rhs)]}', f'{invalid_cnt[(lhs,rhs)]}']) - + fis.write(tabulate(table,tablefmt="grid")) sys.stdout.flush() fis.close() diff --git a/universalmutator/mutator.py b/universalmutator/mutator.py index 8222f23..85fb726 100644 --- a/universalmutator/mutator.py +++ b/universalmutator/mutator.py @@ -1,6 +1,6 @@ from __future__ import print_function import re -import pkg_resources +import pkgutil import random from comby import Comby import os @@ -17,10 +17,11 @@ def parseRules(ruleFiles, comby=False): rulePath = os.path.join('comby', ruleFile) else: rulePath = os.path.join('static', ruleFile) - with pkg_resources.resource_stream('universalmutator', rulePath) as builtInRule: - for line in builtInRule: - line = line.decode() - rulesText.append((line, "builtin:" + ruleFile)) + data = pkgutil.get_data('universalmutator', rulePath) + if data is None: + raise FileNotFoundError(rulePath) + for line in data.decode('utf-8', errors='replace').splitlines(True): + rulesText.append((line, "builtin:" + ruleFile)) except BaseException: print("FAILED TO FIND RULE", ruleFile, "AS BUILT-IN...") try: diff --git a/universalmutator/static/func.rules b/universalmutator/static/func.rules new file mode 100644 index 0000000..89b0ad9 --- /dev/null +++ b/universalmutator/static/func.rules @@ -0,0 +1,12 @@ +# FunC (TON) rules. + +^\s*#include\s ==> DO_NOT_MUTATE + +;; ==> SKIP_MUTATING_REST + +throw_if ==> throw_unless +throw_unless ==> throw_if + +# Some codebases use upper-cased macros. +THROW_IF ==> THROW_UNLESS +THROW_UNLESS ==> THROW_IF \ No newline at end of file diff --git a/universalmutator/static/tact.rules b/universalmutator/static/tact.rules new file mode 100644 index 0000000..ecdf8dc --- /dev/null +++ b/universalmutator/static/tact.rules @@ -0,0 +1,17 @@ +# Tact (TON) rules. +# +# Notes: +# - universal.rules already provides most operator/number mutations. +# - Here we mainly add comment skipping and a few Tact-specific flips. + +^\s*import\s ==> DO_NOT_MUTATE + +// ==> SKIP_MUTATING_REST +/// ==> SKIP_MUTATING_REST +/\* ==> SKIP_MUTATING_REST + +true ==> false +false ==> true + +throwIf ==> throwUnless +throwUnless ==> throwIf diff --git a/universalmutator/static/tolk.rules b/universalmutator/static/tolk.rules new file mode 100644 index 0000000..1c87bdb --- /dev/null +++ b/universalmutator/static/tolk.rules @@ -0,0 +1,12 @@ +# Tolk (TON) rules. + +^\s*import\s ==> DO_NOT_MUTATE + +// ==> SKIP_MUTATING_REST +/\* ==> SKIP_MUTATING_REST + +true ==> false +false ==> true + +# Make immutables mutable (likely still compiles). +\bval\b ==> var \ No newline at end of file diff --git a/universalmutator/tact_handler.py b/universalmutator/tact_handler.py new file mode 100644 index 0000000..0913d86 --- /dev/null +++ b/universalmutator/tact_handler.py @@ -0,0 +1,54 @@ +"""Tact handler. + +By default this is a "dumb" handler (treats every mutant as VALID), like the +handlers for C/C++/JS. + +If you want validity filtering during generation, set an env var: + + UM_TACT_CMD='tact compile MUTANT' + +The command must return exit code 0 for a valid mutant. If the string "MUTANT" +is not present, the handler will temporarily swap the mutant into the source +file before running the command (same behavior as genmutants --cmd). +""" + +from __future__ import annotations + +import os +import shutil +import subprocess + + +_CMD = os.environ.get("UM_TACT_CMD") + +# If no compiler command is configured, behave like a dumb handler. +dumb = _CMD is None + + +def handler(tmpMutantName, mutant, sourceFile, uniqueMutants, compileFile=None): + cmd = _CMD + if cmd is None: + return "VALID" + + target = compileFile if compileFile is not None else sourceFile + pid = os.getpid() + backupName = None + + if "MUTANT" not in cmd: + backupName = target + ".um.backup." + str(pid) + shutil.copy(target, backupName) + shutil.copy(tmpMutantName, target) + + try: + outFile = f".um.tact_output.{pid}" + with open(outFile, "w") as f: + r = subprocess.call( + [cmd.replace("MUTANT", tmpMutantName)], + shell=True, + stderr=f, + stdout=f, + ) + return "VALID" if r == 0 else "INVALID" + finally: + if backupName is not None: + shutil.copy(backupName, target) diff --git a/universalmutator/tolk_handler.py b/universalmutator/tolk_handler.py new file mode 100644 index 0000000..08a7f69 --- /dev/null +++ b/universalmutator/tolk_handler.py @@ -0,0 +1,50 @@ +"""Tolk handler. + +Default behavior is "dumb" (treat every mutant as VALID). + +To enable compilation-based validity filtering during mutant generation, set: + + UM_TOLK_CMD='tolk compile MUTANT' + +The command must return 0 for a valid mutant. +""" + +from __future__ import annotations + +import os +import shutil +import subprocess + + +_CMD = os.environ.get("UM_TOLK_CMD") + +dumb = _CMD is None + + +def handler(tmpMutantName, mutant, sourceFile, uniqueMutants, compileFile=None): + cmd = _CMD + if cmd is None: + return "VALID" + + target = compileFile if compileFile is not None else sourceFile + pid = os.getpid() + backupName = None + + if "MUTANT" not in cmd: + backupName = target + ".um.backup." + str(pid) + shutil.copy(target, backupName) + shutil.copy(tmpMutantName, target) + + try: + outFile = f".um.tolk_output.{pid}" + with open(outFile, "w") as f: + r = subprocess.call( + [cmd.replace("MUTANT", tmpMutantName)], + shell=True, + stderr=f, + stdout=f, + ) + return "VALID" if r == 0 else "INVALID" + finally: + if backupName is not None: + shutil.copy(backupName, target)