Pyurify: Purifying Python Tests for Precise Fault Localization
A Python package for purifying test cases to improve fault localization effectiveness through test case atomization and dynamic program slicing.
Features
- 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
Installation
# 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]"
Quick Start
Command Line
# 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"
Python API
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}")
How It Works
1. Test Case Atomization
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 == 1
After (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 == 1
2. Dynamic Slicing (Optional)
Removes 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 == 1
After 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
CLI Options
-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
API Reference
purify_tests()
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
PytestSlicer
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']}")
Development
Running Tests
# Run all tests
pytest tests/
# Run with coverage
pytest tests/ --cov=pyurify --cov-report=html
# Run specific test
pytest tests/test_purification.py -v
Code Formatting
# Format code
black src/pyurify tests/
# Check formatting
black --check src/pyurify tests/
Use Cases
- 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
Requirements
- Python 3.10+
- pytest 9.0+ (for testing)
License
Apache License 2.0 - see LICENSE file for details.
Metadata
Release files for pyurify 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| pyurify-0.0.1.tar.gz | 53.1 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyurify-0.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 87.8 kB
Release files / pyurify-0.0.1.tar.gz
| Download URL | pyurify-0.0.1.tar.gz |
|---|---|
| Size | 53.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a0575b4da97c7094286e41271cce49fe33951f47e95b612ab6db2064e41ee844
|
|
BLAKE2b-256 checksum How to use checksums |
f8a21975fde1077559c9e4bd92cddbbe73b6420f0b82b53b3d9574c817e12ea5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.2
|
Release files / pyurify-0.0.1-py3-none-any.whl
| Download URL | pyurify-0.0.1-py3-none-any.whl |
|---|---|
| Size | 34.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2008c177796c6071015e326da2b8b87e6114c61179780d3da62ebda264207119
|
|
BLAKE2b-256 checksum How to use checksums |
bb5853e00e864034f4b0259b8db22ba04b00f3dfa34938548506233e5c4c02bf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.2
|
Security for pyurify 0.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.