import argparse from contextlib import contextmanager import os from pathlib import Path import shutil import subprocess import tempfile from unittest import mock import pytest from slither import Slither from slither.tools.mutator.__main__ import _get_mutators, main from slither.tools.mutator.utils.testing_generated_mutant import run_test_cmd from slither.tools.mutator.utils.file_handling import get_sol_file_list, backup_source_file TEST_DATA_DIR = Path(__file__).resolve().parent / "test_data" foundry_available = shutil.which("forge") is not None project_ready = Path(TEST_DATA_DIR, "test_source_unit/lib/forge-std").exists() @contextmanager def change_directory(new_dir): original_dir = os.getcwd() os.chdir(new_dir) try: yield finally: os.chdir(original_dir) def test_get_mutators(): mutators = _get_mutators(None) assert mutators mutators = _get_mutators(["ASOR"]) assert len(mutators) == 1 assert mutators[0].NAME == "ASOR" mutators = _get_mutators(["ASOR", "NotExisiting"]) assert len(mutators) == 1 @mock.patch( "argparse.ArgumentParser.parse_args", return_value=argparse.Namespace( test_cmd="forge test", test_dir=None, ignore_dirs="lib,mutation_campaign", output_dir=None, timeout=None, solc_remaps="forge-std=./lib/forge-std", verbose=None, very_verbose=None, mutators_to_run=None, comprehensive=None, codebase=(TEST_DATA_DIR / "test_source_unit" / "src" / "Counter.sol").as_posix(), contract_names="Counter", ), ) @pytest.mark.skip(reason="Slow test") def test_mutator(mock_args, solc_binary_path): # pylint: disable=unused-argument with change_directory(TEST_DATA_DIR / "test_source_unit"): main() def test_backup_source_file(solc_binary_path): solc_path = solc_binary_path("0.8.15") file_path = (TEST_DATA_DIR / "test_source_unit" / "src" / "Counter.sol").as_posix() sl = Slither(file_path, solc=solc_path) with tempfile.TemporaryDirectory() as directory: files_dict = backup_source_file(sl.source_code, Path(directory)) assert len(files_dict) == 1 assert Path(files_dict[file_path]).exists() @pytest.mark.skipif( not foundry_available or not project_ready, reason="requires Foundry and project setup" ) def test_get_sol_file_list(): project_directory = TEST_DATA_DIR / "test_source_unit" files = get_sol_file_list(project_directory, None) assert len(files) == 46 files = get_sol_file_list(project_directory, ["lib"]) assert len(files) == 3 files = get_sol_file_list(project_directory, ["lib", "script"]) assert len(files) == 2 files = get_sol_file_list(project_directory / "src" / "Counter.sol", None) assert len(files) == 1 (project_directory / "test.sol").mkdir() files = get_sol_file_list(project_directory, None) assert all("test.sol" not in file for file in files) (project_directory / "test.sol").rmdir() @pytest.mark.skipif( not foundry_available or not project_ready, reason="requires Foundry and project setup" ) def test_run_test(caplog): with change_directory(TEST_DATA_DIR / "test_source_unit"): result = run_test_cmd("forge test", timeout=None, target_file=None, verbose=True) assert result assert not caplog.records # Failed command result = run_test_cmd("forge non-test", timeout=None, target_file=None, verbose=True) assert not result assert caplog.records def test_run_tests_timeout(caplog, monkeypatch): def mock_run(*args, **kwargs): raise subprocess.TimeoutExpired(cmd=args[0], timeout=kwargs.get("timeout")) monkeypatch.setattr(subprocess, "run", mock_run) with change_directory(TEST_DATA_DIR / "test_source_unit"): result = run_test_cmd("forge test", timeout=1) assert not result assert "Tests took too long" in caplog.messages[0]