Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve slither-mutate testing #2482

Merged
merged 6 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/scripts/tool_test_runner.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

# used to pass --cov=$path and --cov-append to pytest
if [ "$1" != "" ]; then
pytest "$1" tests/tools/read-storage/test_read_storage.py
pytest "$1" tests/tools
status_code=$?
python -m coverage report
else
pytest tests/tools/read-storage/test_read_storage.py
pytest tests/tools
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

The search results indicate multiple references to 'tests' in the CI configuration files, but none specifically mention 'tests/tools'. This suggests that the new test directory structure might not be fully reflected in the CI pipeline.

  • .github/workflows/test.yml: Contains several references to 'tests', but none to 'tests/tools'.
  • .github/workflows/pylint.yml, .github/workflows/linter.yml, .github/workflows/ci.yml, .github/workflows/black.yml: Also contain references to 'tests', but not 'tests/tools'.

Please ensure that the new test directory structure 'tests/tools' is correctly referenced in the CI configuration files.

  • Update CI configuration files to reflect the new test directory structure.
Analysis chain

Ensure the new test directory structure is correctly reflected in the CI pipeline.

Scripts executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Description: Verify the new test directory structure is correctly reflected in the CI pipeline.

# Test: Search for the test directory usage in CI configuration files. Expect: Only occurrences of the new structure.
rg --type yaml $'tests/tools' .github/workflows/

Length of output: 47



Script:

#!/bin/bash
# Description: Verify the new test directory structure is correctly reflected in the CI pipeline.

# Test: Search for any references to 'tests' in CI configuration files to ensure the new structure is being used.
rg --type yaml 'tests' .github/workflows/

Length of output: 898

status_code=$?
fi

Expand Down
4 changes: 2 additions & 2 deletions slither/tools/mutator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys
import time
from pathlib import Path
from typing import Type, List, Any, Optional
from typing import Type, List, Any, Optional, Union
from crytic_compile import cryticparser
from slither import Slither
from slither.tools.mutator.utils.testing_generated_mutant import run_test_cmd
Expand Down Expand Up @@ -116,7 +116,7 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args()


def _get_mutators(mutators_list: List[str] | None) -> List[Type[AbstractMutator]]:
def _get_mutators(mutators_list: Union[List[str], None]) -> List[Type[AbstractMutator]]:
detectors_ = [getattr(all_mutators, name) for name in dir(all_mutators)]
if mutators_list is not None:
detectors = [
Expand Down
4 changes: 2 additions & 2 deletions slither/tools/mutator/mutators/LIR.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches
literal_replacements.append(variable.type.max) # append data type max value
if str(variable.type).startswith("uint"):
literal_replacements.append("1")
elif str(variable.type).startswith("uint"):
elif str(variable.type).startswith("int"):
literal_replacements.append("-1")
# Get the string
start = variable.source_mapping.start
Expand Down Expand Up @@ -63,7 +63,7 @@ def _mutate(self) -> Dict: # pylint: disable=too-many-branches
literal_replacements.append(variable.type.max)
if str(variable.type).startswith("uint"):
literal_replacements.append("1")
elif str(variable.type).startswith("uint"):
elif str(variable.type).startswith("int"):
literal_replacements.append("-1")
start = variable.source_mapping.start
stop = start + variable.source_mapping.length
Expand Down
8 changes: 4 additions & 4 deletions slither/tools/mutator/mutators/abstract_mutator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import abc
import logging
from pathlib import Path
from typing import Optional, Dict, Tuple, List
from typing import Optional, Dict, Tuple, List, Union
from slither.core.compilation_unit import SlitherCompilationUnit
from slither.formatters.utils.patches import apply_patch, create_diff
from slither.tools.mutator.utils.testing_generated_mutant import test_patch
Expand All @@ -27,7 +27,7 @@ def __init__( # pylint: disable=too-many-arguments
testing_command: str,
testing_directory: str,
contract_instance: Contract,
solc_remappings: str | None,
solc_remappings: Union[str, None],
verbose: bool,
very_verbose: bool,
output_folder: Path,
Expand Down Expand Up @@ -81,7 +81,7 @@ def mutate(self) -> Tuple[List[int], List[int], List[int]]:
(all_patches) = self._mutate()
if "patches" not in all_patches:
logger.debug("No patches found by %s", self.NAME)
return ([0, 0, 0], [0, 0, 0], self.dont_mutate_line)
return [0, 0, 0], [0, 0, 0], self.dont_mutate_line

for file in all_patches["patches"]: # Note: This should only loop over a single file
original_txt = self.slither.source_code[file].encode("utf8")
Expand Down Expand Up @@ -146,4 +146,4 @@ def mutate(self) -> Tuple[List[int], List[int], List[int]]:
f"Found {self.uncaught_mutant_counts[2]} uncaught tweak mutants so far (out of {self.total_mutant_counts[2]} that compile)"
)

return (self.total_mutant_counts, self.uncaught_mutant_counts, self.dont_mutate_line)
return self.total_mutant_counts, self.uncaught_mutant_counts, self.dont_mutate_line
2 changes: 1 addition & 1 deletion slither/tools/mutator/utils/file_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def get_sol_file_list(codebase: Path, ignore_paths: Union[List[str], None]) -> L
# if input is folder
if codebase.is_dir():
for file_name in codebase.rglob("*.sol"):
if not any(part in ignore_paths for part in file_name.parts):
if file_name.is_file() and not any(part in ignore_paths for part in file_name.parts):
sol_file_list.append(file_name.as_posix())

return sol_file_list
7 changes: 6 additions & 1 deletion slither/tools/mutator/utils/testing_generated_mutant.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ def compile_generated_mutant(file_path: str, mappings: str) -> bool:
return False


def run_test_cmd(cmd: str, timeout: int | None, target_file: str | None, verbose: bool) -> bool:
def run_test_cmd(
cmd: str,
timeout: Union[int, None] = None,
target_file: Union[str, None] = None,
verbose: bool = False,
) -> bool:
"""
function to run codebase tests
returns: boolean whether the tests passed or not
Expand Down
Empty file.
7 changes: 7 additions & 0 deletions tests/tools/mutator/test_data/test_source_unit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Counter

Init using :

```shell
forge install --no-commit --no-git .
```
7 changes: 7 additions & 0 deletions tests/tools/mutator/test_data/test_source_unit/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[profile.default]
src = 'src'
out = 'out'
libs = ['lib']
solc = "0.8.15"

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Script, console} from "forge-std/Script.sol";

contract CounterScript is Script {
function setUp() public {}

function run() public {
vm.broadcast();
}
}
14 changes: 14 additions & 0 deletions tests/tools/mutator/test_data/test_source_unit/src/Counter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

contract Counter {
uint256 public number;

function setNumber(uint256 newNumber) public {
number = newNumber;
}

function increment() public {
number++;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

import {Test, console} from "forge-std/Test.sol";
import {Counter} from "../src/Counter.sol";

contract CounterTest is Test {
Counter public counter;

function setUp() public {
counter = new Counter();
counter.setNumber(0);
}

function test_Increment() public {
counter.increment();
assertEq(counter.number(), 1);
}

function testFuzz_SetNumber(uint256 x) public {
counter.setNumber(x);
assertEq(counter.number(), x);
}
}
133 changes: 133 additions & 0 deletions tests/tools/mutator/test_mutator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
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]