Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
40ff12d
Expand test coverage with comprehensive test suite
claude Nov 3, 2025
728dc54
Add comprehensive segfault safety and concurrency tests
claude Nov 3, 2025
2e8fbee
Update test documentation with segfault and concurrency test details
claude Nov 3, 2025
ec010ed
Add comprehensive test validation report
claude Nov 3, 2025
6afed2e
Fix test bugs and document critical library segfaults discovered duri…
claude Nov 3, 2025
ee7ac05
Fix critical segfault bugs in library and correct test assertions
claude Nov 3, 2025
fb71e18
Add comprehensive memory safety tests and fix library limitations
claude Nov 3, 2025
5e6d125
Improve README clarity and add comprehensive user scenario tests
claude Nov 3, 2025
ec5bc55
Fix test assertions to match actual API behavior
claude Nov 3, 2025
c5bc8a4
Fix concurrency test bugs
claude Nov 3, 2025
f04ffbb
Enable all previously skipped tests - all segfaults are now fixed
claude Nov 3, 2025
0190809
Fix P1 bug: Validate index when erasing last element
claude Nov 3, 2025
6c5db7e
Convert all Japanese text to English
claude Nov 3, 2025
b2e404c
Fix Windows multiprocessing pickling error and malformed docstrings
claude Nov 3, 2025
d86b372
Fix Python 3.8 compatibility: Use typing.Tuple instead of tuple[]
claude Nov 3, 2025
9279e29
Reduce stress test intensity to prevent CI timeouts
claude Nov 3, 2025
a6a2834
Fix CI hanging on emulated platforms (aarch64/musllinux)
atksh Nov 4, 2025
63be86b
Fix P1 bug: test_query_intersections_deterministic should compare uno…
atksh Nov 4, 2025
68b7d7e
Optimize CI for PRs: Add pairwise coverage unit tests and skip wheel …
atksh Nov 4, 2025
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
332 changes: 171 additions & 161 deletions README.md

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions src/python_prtree/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ def __init__(self, *args, **kwargs):

def __getattr__(self, name):
def handler_function(*args, **kwargs):
# Handle empty tree cases for methods that cause segfaults
if self.n == 0 and name in ('rebuild', 'save'):
# These operations are not meaningful/safe on empty trees
if name == 'rebuild':
return # No-op for empty tree
elif name == 'save':
raise ValueError("Cannot save empty tree")

ret = getattr(self._tree, name)(*args, **kwargs)
return ret

Expand All @@ -47,6 +55,13 @@ def __len__(self):
def erase(self, idx):
if self.n == 0:
raise ValueError("Nothing to erase")

# Handle erasing the last element (library limitation workaround)
if self.n == 1:
# Recreate an empty tree (workaround for C++ limitation)
self._tree = self.Klass()
return

Choose a reason for hiding this comment

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

P1 Badge Validate index when erasing last element

When the tree holds a single item, erase now recreates an empty tree and returns without forwarding the call. This bypasses the underlying index validation, so erase(999) on a one-item tree silently drops the only element instead of raising the expected error. The method should still verify that the requested index exists before clearing the tree.

Useful? React with 👍 / 👎.


self._tree.erase(idx)

def set_obj(self, idx, obj):
Expand All @@ -73,6 +88,10 @@ def insert(self, idx=None, bb=None, obj=None):
self._tree.insert(idx, bb, objdumps)

def query(self, *args, return_obj=False):
# Handle empty tree case to prevent segfault
if self.n == 0:
return []

if len(args) == 1:
out = self._tree.query(*args)
else:
Expand All @@ -83,6 +102,17 @@ def query(self, *args, return_obj=False):
else:
return out

def batch_query(self, queries, *args, **kwargs):
# Handle empty tree case to prevent segfault
if self.n == 0:
# Return empty list for each query
import numpy as np
if hasattr(queries, 'shape'):
return [[] for _ in range(len(queries))]
return []

return self._tree.batch_query(queries, *args, **kwargs)


class PRTree3D(PRTree2D):
Klass = _PRTree3D
Expand Down
193 changes: 193 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# Test Suite for python_prtree

This directory contains a comprehensive test suite for python_prtree, organized by test type and functionality.

## Directory Structure

```
tests/
├── unit/ # Unit tests (individual features)
│ ├── test_construction.py
│ ├── test_query.py
│ ├── test_batch_query.py
│ ├── test_insert.py
│ ├── test_erase.py
│ ├── test_persistence.py
│ ├── test_rebuild.py
│ ├── test_intersections.py
│ ├── test_object_handling.py
│ ├── test_properties.py
│ └── test_precision.py
├── integration/ # Integration tests (feature combinations)
│ ├── test_insert_query_workflow.py
│ ├── test_erase_query_workflow.py
│ ├── test_persistence_query_workflow.py
│ ├── test_rebuild_query_workflow.py
│ └── test_mixed_operations.py
├── e2e/ # End-to-end tests (user scenarios)
│ ├── test_readme_examples.py
│ ├── test_regression.py
│ └── test_user_workflows.py
├── legacy/ # Original test file (kept for reference)
│ └── test_PRTree.py
├── conftest.py # Shared fixtures and configuration
└── README.md # This file

## Running Tests

### Run all tests
```bash
pytest tests/
```

### Run specific test category
```bash
# Unit tests only
pytest tests/unit/

# Integration tests only
pytest tests/integration/

# E2E tests only
pytest tests/e2e/
```

### Run specific test file
```bash
pytest tests/unit/test_construction.py
```

### Run tests for specific dimension
```bash
# Run all PRTree2D tests
pytest tests/ -k "PRTree2D"

# Run all PRTree3D tests
pytest tests/ -k "PRTree3D"

# Run all PRTree4D tests
pytest tests/ -k "PRTree4D"
```

### Run with coverage
```bash
pytest --cov=python_prtree --cov-report=html tests/
```

### Run with verbose output
```bash
pytest -v tests/
```

### Run specific test by name
```bash
pytest tests/unit/test_construction.py::TestNormalConstruction::test_construction_with_valid_inputs
```

## Test Organization

### Unit Tests (`tests/unit/`)
Test individual functions and methods in isolation:
- **test_construction.py**: Tree initialization and construction
- **test_query.py**: Single query operations
- **test_batch_query.py**: Batch query operations
- **test_insert.py**: Insert operations
- **test_erase.py**: Erase operations
- **test_persistence.py**: Save/load operations
- **test_rebuild.py**: Rebuild operations
- **test_intersections.py**: Query intersections operations
- **test_object_handling.py**: Object storage and retrieval
- **test_properties.py**: Properties (size, len, n)
- **test_precision.py**: Float32/64 precision handling
- **test_segfault_safety.py**: Segmentation fault safety tests
- **test_crash_isolation.py**: Crash isolation tests (subprocess)
- **test_memory_safety.py**: Memory safety and bounds checking
- **test_concurrency.py**: Python threading/multiprocessing/async tests
- **test_parallel_configuration.py**: Parallel execution configuration tests

### Integration Tests (`tests/integration/`)
Test interactions between multiple components:
- **test_insert_query_workflow.py**: Insert → Query workflows
- **test_erase_query_workflow.py**: Erase → Query workflows
- **test_persistence_query_workflow.py**: Save → Load → Query workflows
- **test_rebuild_query_workflow.py**: Rebuild → Query workflows
- **test_mixed_operations.py**: Complex operation sequences

### End-to-End Tests (`tests/e2e/`)
Test complete user workflows and scenarios:
- **test_readme_examples.py**: All examples from README
- **test_regression.py**: Known bug fixes and edge cases
- **test_user_workflows.py**: Common user scenarios

## Test Coverage

The test suite covers:
- ✅ All public APIs (PRTree2D, PRTree3D, PRTree4D)
- ✅ Normal cases (happy path)
- ✅ Error cases (invalid inputs)
- ✅ Boundary values (empty, single, large datasets)
- ✅ Precision cases (float32 vs float64)
- ✅ Edge cases (degenerate boxes, touching boxes, etc.)
- ✅ Consistency (query vs batch_query, save/load, etc.)
- ✅ Known regressions (bugs from issues)
- ✅ Memory safety (segfault prevention, bounds checking)
- ✅ Concurrency (threading, multiprocessing, async)
- ✅ Parallel execution (batch_query parallelization)

## Test Matrix

See [docs/TEST_STRATEGY.md](../docs/TEST_STRATEGY.md) for the complete feature-perspective test matrix.

## Adding New Tests

When adding new tests:

1. **Choose the right category**:
- Unit tests: Testing a single feature in isolation
- Integration tests: Testing multiple features together
- E2E tests: Testing complete user workflows

2. **Follow naming conventions**:
```python
def test_<feature>_<scenario>_<expected>():
"""Test description in Japanese and English."""
pass
```

3. **Use parametrization** for dimension testing:
```python
@pytest.mark.parametrize("PRTree, dim", [(PRTree2D, 2), (PRTree3D, 3), (PRTree4D, 4)])
def test_my_feature(PRTree, dim):
pass
```

4. **Use shared fixtures** from `conftest.py` when appropriate

5. **Update TEST_STRATEGY.md** if adding new test perspectives

## Continuous Integration

These tests are run automatically on:
- Every pull request
- Every push to main branch
- Scheduled daily builds

See `.github/workflows/` for CI configuration.

## Known Issues

- Some tests may take longer on slower systems due to large dataset sizes
- Float precision tests are sensitive to numpy/system math libraries
- File I/O tests require write permissions in tmp_path

## Contributing

When contributing tests:
1. Ensure all tests pass locally before submitting PR
2. Add tests for any new features or bug fixes
3. Update this README if adding new test categories
4. Aim for >90% line coverage and >85% branch coverage
58 changes: 58 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Shared pytest fixtures and configuration for all tests."""
import numpy as np
import pytest


@pytest.fixture(params=[(2, "PRTree2D"), (3, "PRTree3D"), (4, "PRTree4D")])
def dimension_and_class(request):
"""Parametrize tests across all dimensions and tree classes."""
from python_prtree import PRTree2D, PRTree3D, PRTree4D

dim, class_name = request.param
tree_classes = {
"PRTree2D": PRTree2D,
"PRTree3D": PRTree3D,
"PRTree4D": PRTree4D,
}
return dim, tree_classes[class_name]


@pytest.fixture
def sample_boxes_2d():
"""Generate sample 2D bounding boxes for testing."""
np.random.seed(42)
n = 100
idx = np.arange(n)
boxes = np.random.rand(n, 4) * 100
boxes[:, 2] += boxes[:, 0] + 1 # xmax > xmin
boxes[:, 3] += boxes[:, 1] + 1 # ymax > ymin
return idx, boxes


@pytest.fixture
def sample_boxes_3d():
"""Generate sample 3D bounding boxes for testing."""
np.random.seed(42)
n = 100
idx = np.arange(n)
boxes = np.random.rand(n, 6) * 100
for i in range(3):
boxes[:, i + 3] += boxes[:, i] + 1
return idx, boxes


@pytest.fixture
def sample_boxes_4d():
"""Generate sample 4D bounding boxes for testing."""
np.random.seed(42)
n = 100
idx = np.arange(n)
boxes = np.random.rand(n, 8) * 100
for i in range(4):
boxes[:, i + 4] += boxes[:, i] + 1
return idx, boxes


def has_intersect(x, y, dim):
"""Helper function to check if two boxes intersect."""
return all([max(x[i], y[i]) <= min(x[i + dim], y[i + dim]) for i in range(dim)])
1 change: 1 addition & 0 deletions tests/e2e/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""End-to-end tests for python_prtree."""
Loading
Loading