Skip to content

Commit cd7a206

Browse files
Testing Setup (#44)
* Add SingleRunData-level trim/split helpers * More tests * Cleanup * Clean up tests
1 parent cfa49ff commit cd7a206

30 files changed

Lines changed: 1432 additions & 475 deletions

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ perda/models/stsb-cross-encoder/
1717
docs/build/
1818
docs/build_md/
1919
docs/source/_autosummary/
20+
21+
# Ignore test artifacts
22+
.coverage
23+
.pytest_cache/
24+
htmlcov/

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,15 @@ Docs are autogenerated using Sphinx. To generate the docs locally, run the follo
1313
cd docs
1414
sphinx-build -b html source build # HTML
1515
sphinx-build -M markdown source build_md # Markdown
16-
```
16+
```
17+
18+
## Testing
19+
20+
We use `pytest` for testing. In the root directory, run the following
21+
22+
```bash
23+
pytest
24+
25+
# Generates coverage report in htmlcov/index.html
26+
pytest --cov=perda --cov-report=html
27+
```

perda/analyzer/analyzer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def __init__(
8282
Maximum number of parsing errors before stopping. Default is 100.
8383
preprocessing : list[PreprocessingStep] | None, optional
8484
Ordered list of post-parse preprocessing steps to apply. Each step
85-
is a ``SingleRunData SingleRunData`` callable. Steps are skipped
85+
is a ``SingleRunData -> SingleRunData`` callable. Steps are skipped
8686
with a warning if required variables are absent. Default is None.
8787
8888
Examples
@@ -213,7 +213,7 @@ def plot(
213213
if self.data.concat_boundaries:
214214
vlines = [_to_seconds(b, unit) for b in self.data.concat_boundaries]
215215

216-
# Apply time range filter if specified (convert seconds raw units for trim)
216+
# Apply time range filter if specified (convert seconds -> raw units for trim)
217217
if ts_start is not None or ts_end is not None:
218218
start_raw = _from_seconds(ts_start, unit) if ts_start is not None else None
219219
end_raw = _from_seconds(ts_end, unit) if ts_end is not None else None

perda/core_data_structures/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
from .joins import *
44
from .resampling import *
55
from .single_run_data import *
6+
from .split_helpers import *
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from .single_run_data import DataInstance, SingleRunData
2+
3+
4+
def trim_single_run_data(
5+
data: SingleRunData,
6+
ts_start: float,
7+
ts_end: float,
8+
) -> SingleRunData:
9+
"""Return a new SingleRunData with every variable trimmed to [ts_start, ts_end].
10+
11+
Parameters
12+
----------
13+
data : SingleRunData
14+
ts_start, ts_end : float
15+
Timestamps in the same unit as data.timestamp_unit.
16+
17+
Returns
18+
-------
19+
SingleRunData
20+
Fresh object; the original is not mutated.
21+
"""
22+
trimmed: dict[int, DataInstance] = {
23+
var_id: di.trim(ts_start, ts_end) for var_id, di in data.id_to_instance.items()
24+
}
25+
26+
return SingleRunData(
27+
id_to_instance=trimmed,
28+
cpp_name_to_id=dict(data.cpp_name_to_id),
29+
id_to_cpp_name=dict(data.id_to_cpp_name),
30+
id_to_descript=dict(data.id_to_descript),
31+
total_data_points=sum(len(di.value_np) for di in trimmed.values()),
32+
data_start_time=int(ts_start),
33+
data_end_time=int(ts_end),
34+
timestamp_unit=data.timestamp_unit,
35+
concat_boundaries=[],
36+
)
37+
38+
39+
def split_single_run_data(
40+
data: SingleRunData,
41+
split_timestamps: list[float],
42+
) -> list[SingleRunData]:
43+
"""Split a SingleRunData into segments defined by a list of boundary timestamps.
44+
45+
Each consecutive pair of timestamps in ``split_timestamps`` defines one
46+
segment. The result is keyed by 1-based segment number.
47+
48+
Parameters
49+
----------
50+
data : SingleRunData
51+
split_timestamps : list[float]
52+
Ordered boundary timestamps in the same unit as data.timestamp_unit.
53+
Must contain at least 2 values.
54+
55+
Returns
56+
-------
57+
List[SingleRunData]
58+
"""
59+
if len(split_timestamps) < 2:
60+
raise ValueError(f"Need at least 2 boundary timestamps")
61+
62+
segments = []
63+
for t_start, t_end in zip(split_timestamps[:-1], split_timestamps[1:]):
64+
segments.append(trim_single_run_data(data, t_start, t_end))
65+
66+
return segments

perda/utils/preprocessing.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
STEERING_RAW = "ludwig.steeringWheel.raw"
2828
STEERING_ANGLE = "ludwig.steeringWheel.angle"
2929

30-
# Default 3-point voltageangle calibration for the steering pot.
30+
# Default 3-point voltage->angle calibration for the steering pot.
3131
SteeringCalibration = tuple[tuple[float, float], ...]
3232
DEFAULT_STEERING_CALIBRATION: SteeringCalibration = (
3333
(1.86, -97.0), # max left
@@ -148,7 +148,7 @@ def convert_wheelspeeds_to_m_per_s(data: SingleRunData) -> SingleRunData:
148148
)
149149

150150
print(
151-
f"convert_wheelspeeds_to_m_per_s: converted {len(cols)} channels mph m/s, backups in *_mph"
151+
f"convert_wheelspeeds_to_m_per_s: converted {len(cols)} channels mph -> m/s, backups in *_mph"
152152
)
153153
return data
154154

@@ -308,7 +308,7 @@ def apply_preprocessing(
308308
data : SingleRunData
309309
Parsed run data to preprocess.
310310
steps : list of PreprocessingStep
311-
Ordered list of ``SingleRunData SingleRunData`` callables to apply.
311+
Ordered list of ``SingleRunData -> SingleRunData`` callables to apply.
312312
313313
Returns
314314
-------

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,4 @@ build-backend = "setuptools.build_meta"
5252

5353
[tool.pytest.ini_options]
5454
testpaths = ["tests"]
55+
addopts = "--import-mode=importlib"

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ black[jupyter]
88
isort
99
mypy
1010
pytest
11+
pytest-cov

tests/CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
- Organize tests by files. Each test_* file should correspond to a single file in the codebase.
2+
3+
- When possible, parameterize tests. Increase the number of test cases without increasing the number of functions.
4+
5+
- Use fixtures to set up common test data or state. Use fixture.py file per test folder to keep files smaller and more maintainable. Only put widely used fixtures in the top-level conftest.py.
6+
7+
- Do not make section header comments in test files

tests/analyzer/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)