A Python package for purifying test cases to improve fault localization effectiveness through test case atomization and dynamic program slicing.
- Test Case Atomization: Automatically splits tests with multiple assertions into single-assertion tests
- Dynamic Slicing: Removes irrelevant code from tests using execution tracing and dependency analysis
- Command-Line Interface: Easy-to-use CLI for quick purification
- Python API: Programmatic access for integration into fault localization pipelines
# Install from PyPI (when published)
pip install pyurify
# Or install from source
git clone https://github.com/smythi93/pyurify.git
cd test-purification
pip install -e .
# With test dependencies
pip install -e ".[test]"# Basic purification with slicing (default)
pyurify --src-dir tests/ --dst-dir purified/ \
--failing-tests "test_math.py::test_add"
# Disable dynamic slicing (atomization only)
pyurify --src-dir tests/ --dst-dir purified/ \
--failing-tests "test_math.py::test_add" \
--disable-slicing
# Multiple tests
pyurify --src-dir tests/ --dst-dir purified/ \
--failing-tests "test_math.py::test_add" "test_math.py::test_subtract"from pathlib import Path
from pyurify import purify_tests
# Purify tests
result = purify_tests(
src_dir=Path("tests"),
dst_dir=Path("purified"),
failing_tests=["test_math.py::test_add"],
enable_slicing=True,
)
# Check results
for test_id, file_param_tuples in result.items():
print(f"{test_id}:")
for purified_file, param_suffix in file_param_tuples:
if param_suffix:
print(f" - {purified_file} [params: {param_suffix}]")
else:
print(f" - {purified_file}")Splits tests with multiple assertions into separate tests. Each atomized test keeps one assertion active and wraps the others in try-except blocks to suppress them:
Before:
def test_math():
x = 1
y = 2
z = x + y
assert z == 3
assert x == 1After (2 files):
# test_math_assertion_5.py - First assertion active
def test_math():
x = 1
y = 2
z = x + y
assert z == 3
try:
assert x == 1
except AssertionError:
pass
# test_math_assertion_6.py - Second assertion active
def test_math():
x = 1
y = 2
z = x + y
try:
assert z == 3
except AssertionError:
pass
assert x == 1Removes code not relevant to each assertion. After atomization with try-except blocks, slicing further removes irrelevant statements:
After Atomization:
# test_math_assertion_6.py
def test_math():
x = 1
y = 2
z = x + y
try:
assert z == 3
except AssertionError:
pass
assert x == 1After Slicing:
# test_math_assertion_6.py
def test_math():
x = 1
# y, z, and first assertion removed - not needed for second assertion
assert x == 1-s, --src-dir PATH Source directory containing tests (required)
-d, --dst-dir PATH Destination for purified tests (required)
-f, --failing-tests TEST... Space-separated test identifiers (required)
--disable-slicing Disable dynamic slicing (slicing enabled by default)
--test-base PATH Base directory for tests (default: src-dir)
--python PATH Python executable (default: python)
-v, --verbose Enable verbose output
def purify_tests(
src_dir: Path,
dst_dir: Path,
failing_tests: List[str],
enable_slicing: bool = False,
test_base: Optional[Path] = None,
venv_python: str = None,
venv: Optional[dict] = None,
) -> Dict[str, List[tuple[Path, Optional[str]]]]Parameters:
src_dir: Source directory containing test filesdst_dir: Destination directory for purified testsfailing_tests: List of test identifiers (e.g.,["test.py::test_func"])enable_slicing: Whether to apply dynamic slicing (default: False)test_base: Base directory for tests (default: src_dir)venv_python: Python executable path (default: None, uses sys.executable)venv: Environment variables dict (default: None, uses os.environ)
Returns:
- Dict mapping test IDs to lists of (purified_file, param_suffix) tuples
- For parameterized tests,
param_suffixcontains parameter values - For non-parameterized tests,
param_suffixis None
from pathlib import Path
from pyurify import PytestSlicer
# Initialize slicer
slicer = PytestSlicer(
test_file=Path("test.py"),
python_executable="python", # Optional
env=None, # Optional: environment variables
base_dir=None, # Optional: base directory
)
# Slice a test
results = slicer.slice_test(
test_pattern="test_func", # Optional: pytest pattern
target_line=10 # Optional: specific line to slice
)
# Access results
print(f"Test file: {results['test_file']}")
print(f"Slices: {results['slices']}")# Run all tests
pytest tests/
# Run with coverage
pytest tests/ --cov=pyurify --cov-report=html
# Run specific test
pytest tests/test_purification.py -v# Format code
black src/pyurify tests/
# Check formatting
black --check src/pyurify tests/- Fault Localization: Improve FL accuracy by focusing on relevant code
- Test Debugging: Isolate failing assertions for easier debugging
- Test Optimization: Reduce test code size and execution time
- Research: Study test behavior and dependencies
- Python 3.10+
- pytest 9.0+ (for testing)
Apache License 2.0 - see LICENSE file for details.