Add tests to slither-mutate and fix some Python 3.8 compatibility issues.

pull/2482/head
Alexis 5 months ago
parent 02df0dcf37
commit c3a5fb219b
No known key found for this signature in database
  1. 8
      slither/tools/mutator/mutators/abstract_mutator.py
  2. 2
      slither/tools/mutator/utils/file_handling.py
  3. 7
      slither/tools/mutator/utils/testing_generated_mutant.py
  4. 0
      tests/tools/mutator/__init__.py
  5. 66
      tests/tools/mutator/test_data/test_source_unit/README.md
  6. 7
      tests/tools/mutator/test_data/test_source_unit/foundry.toml
  7. 12
      tests/tools/mutator/test_data/test_source_unit/script/Counter.s.sol
  8. 14
      tests/tools/mutator/test_data/test_source_unit/src/Counter.sol
  9. 24
      tests/tools/mutator/test_data/test_source_unit/test/Counter.t.sol
  10. 122
      tests/tools/mutator/test_mutator.py

@ -1,7 +1,7 @@
import abc import abc
import logging import logging
from pathlib import Path 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.core.compilation_unit import SlitherCompilationUnit
from slither.formatters.utils.patches import apply_patch, create_diff from slither.formatters.utils.patches import apply_patch, create_diff
from slither.tools.mutator.utils.testing_generated_mutant import test_patch from slither.tools.mutator.utils.testing_generated_mutant import test_patch
@ -27,7 +27,7 @@ class AbstractMutator(
testing_command: str, testing_command: str,
testing_directory: str, testing_directory: str,
contract_instance: Contract, contract_instance: Contract,
solc_remappings: str | None, solc_remappings: Union[str, None],
verbose: bool, verbose: bool,
very_verbose: bool, very_verbose: bool,
output_folder: Path, output_folder: Path,
@ -81,7 +81,7 @@ class AbstractMutator(
(all_patches) = self._mutate() (all_patches) = self._mutate()
if "patches" not in all_patches: if "patches" not in all_patches:
logger.debug("No patches found by %s", self.NAME) 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 for file in all_patches["patches"]: # Note: This should only loop over a single file
original_txt = self.slither.source_code[file].encode("utf8") original_txt = self.slither.source_code[file].encode("utf8")
@ -146,4 +146,4 @@ class AbstractMutator(
f"Found {self.uncaught_mutant_counts[2]} uncaught tweak mutants so far (out of {self.total_mutant_counts[2]} that compile)" 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

@ -111,7 +111,7 @@ def get_sol_file_list(codebase: Path, ignore_paths: Union[List[str], None]) -> L
# if input is folder # if input is folder
if codebase.is_dir(): if codebase.is_dir():
for file_name in codebase.rglob("*.sol"): 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()) sol_file_list.append(file_name.as_posix())
return sol_file_list return sol_file_list

@ -22,7 +22,12 @@ def compile_generated_mutant(file_path: str, mappings: str) -> bool:
return False 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 function to run codebase tests
returns: boolean whether the tests passed or not returns: boolean whether the tests passed or not

@ -0,0 +1,66 @@
## Foundry
**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
Foundry consists of:
- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
## Documentation
https://book.getfoundry.sh/
## Usage
### Build
```shell
$ forge build
```
### Test
```shell
$ forge test
```
### Format
```shell
$ forge fmt
```
### Gas Snapshots
```shell
$ forge snapshot
```
### Anvil
```shell
$ anvil
```
### Deploy
```shell
$ forge script script/Counter.s.sol:CounterScript --rpc-url <your_rpc_url> --private-key <your_private_key>
```
### Cast
```shell
$ cast <subcommand>
```
### Help
```shell
$ forge --help
$ anvil --help
$ cast --help
```

@ -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

@ -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();
}
}

@ -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++;
}
}

@ -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);
}
}

@ -0,0 +1,122 @@
import os
import subprocess
import tempfile
from pathlib import Path
from unittest import mock
import argparse
from contextlib import contextmanager
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"
@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): # pylint: disable=unused-argument
with change_directory(TEST_DATA_DIR / "test_source_unit"):
main()
def test_backup_source_file():
file_path = (TEST_DATA_DIR / "test_source_unit" / "src" / "Counter.sol").as_posix()
sl = Slither(file_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()
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()
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]
Loading…
Cancel
Save