Skip to content

Commit 8c2c53d

Browse files
committed
Speed up behavior-focused test fixtures
1 parent aa00fe8 commit 8c2c53d

4 files changed

Lines changed: 97 additions & 21 deletions

File tree

tests/test_catalog_curation.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,17 @@ def recorded_data_root(tmp_path_factory: pytest.TempPathFactory) -> Path:
172172
episode_specs = {
173173
# ~12.5% blackout: passes the gate below.
174174
"fold_napkin": SyntheticEpisodeSpec(
175-
duration_s=4.0, task="fold_napkin", black_segment=(1.0, 1.5)
175+
duration_s=1.0,
176+
cameras=("wrist_cam",),
177+
task="fold_napkin",
178+
black_segment=(0.2, 0.35),
176179
),
177-
# 75% blackout: quarantined by the gate below.
180+
# ~80% blackout: quarantined by the gate below.
178181
"pour_water": SyntheticEpisodeSpec(
179-
duration_s=4.0, task="pour_water", black_segment=(0.5, 3.5)
182+
duration_s=1.0,
183+
cameras=("wrist_cam",),
184+
task="pour_water",
185+
black_segment=(0.1, 0.9),
180186
),
181187
}
182188

tests/test_checks.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,31 @@ def test_action_rate_matches_the_synthesized_rate(jittery_episode: hflow.Episode
131131

132132

133133
def test_content_digest_identifies_duplicate_content(tmp_path: Path) -> None:
134-
spec = SyntheticEpisodeSpec(duration_s=2.0)
134+
# Digest behavior is independent of camera encoding. A tiny state stream
135+
# keeps this contract focused on message content instead of fixture cost.
136+
spec = SyntheticEpisodeSpec(
137+
duration_s=0.2,
138+
cameras=(),
139+
joint_hz=10.0,
140+
joint_count=1,
141+
black_segment=None,
142+
joint_jump_at_s=None,
143+
timestamp_offset_segment=None,
144+
)
135145
first = synthesize_episode(tmp_path / "a.mcap", spec)
136146
duplicate = synthesize_episode(tmp_path / "b.mcap", spec)
137147
different = synthesize_episode(
138-
tmp_path / "c.mcap", SyntheticEpisodeSpec(duration_s=2.0, joint_jump_at_s=1.0)
148+
tmp_path / "c.mcap",
149+
SyntheticEpisodeSpec(
150+
duration_s=0.2,
151+
cameras=(),
152+
joint_hz=10.0,
153+
joint_count=1,
154+
black_segment=None,
155+
joint_jump_at_s=None,
156+
timestamp_offset_segment=None,
157+
seed=1,
158+
),
139159
)
140160
with (
141161
hflow.Episode(first) as ep_a,

tests/test_end_to_end.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99

1010
import hflow
11-
from hflow.testing import synthesize_episode
11+
from hflow.testing import SyntheticEpisodeSpec, synthesize_episode
1212

1313

1414
def check_joint_smoothness(joints: np.ndarray, rate_hz: float) -> dict[str, float]:
@@ -21,7 +21,23 @@ def check_joint_smoothness(joints: np.ndarray, rate_hz: float) -> dict[str, floa
2121

2222
@pytest.fixture(scope="module")
2323
def source_episode(tmp_path_factory: pytest.TempPathFactory) -> Path:
24-
return synthesize_episode(tmp_path_factory.mktemp("e2e-source") / "episode_0001.mcap")
24+
return synthesize_episode(
25+
tmp_path_factory.mktemp("e2e-source") / "episode_0001.mcap",
26+
SyntheticEpisodeSpec(
27+
duration_s=2.0,
28+
black_segment=(0.5, 0.75),
29+
joint_jump_at_s=1.0,
30+
timestamp_offset_segment=(1.4, 1.7),
31+
),
32+
)
33+
34+
35+
@pytest.fixture(scope="module")
36+
def state_only_source_episode(tmp_path_factory: pytest.TempPathFactory) -> Path:
37+
return synthesize_episode(
38+
tmp_path_factory.mktemp("e2e-state-only-source") / "episode_0001.mcap",
39+
SyntheticEpisodeSpec(duration_s=2.0, cameras=(), joint_jump_at_s=1.0),
40+
)
2541

2642

2743
@pytest.fixture(scope="module")
@@ -109,7 +125,7 @@ def test_builtin_checks_found_the_injected_defects(
109125
assert blackout is not None
110126
black_pct = blackout.measurements["black_pct"]
111127
assert isinstance(black_pct, float)
112-
# The fixture blacks out 1s of the 8s wrist stream: ~12.5% of frames.
128+
# The fixture blacks out 0.25s of the 2s wrist stream: ~12.5% of frames.
113129
assert 5.0 < black_pct < 25.0
114130

115131

@@ -129,7 +145,7 @@ def test_canonical_episode_accessors(
129145
assert mp4.is_file() and mp4.stat().st_size > 0
130146

131147
frames = episode.frames("overhead_cam", fps=2.0)
132-
assert 12 <= len(frames) <= 17 # ~8s at 2 fps
148+
assert 3 <= len(frames) <= 5 # ~2s at 2 fps
133149
assert frames[0].path.read_bytes()[:2] == b"\xff\xd8"
134150
# Frame log times map to SOURCE messages: at t=0.5s the fps filter
135151
# emits the frame visible then -- source frame floor(0.5 * 15) = 7,
@@ -141,7 +157,7 @@ def test_arrow_export(report_and_app: tuple[hflow.TestReport, hflow.App]) -> Non
141157
report, _app = report_and_app
142158
with hflow.Episode(report.canonical_path) as episode:
143159
table: Any = episode.channel("/joint_states").to_arrow()
144-
assert table.num_rows == 800
160+
assert table.num_rows == 200
145161
assert "log_time_ns" in table.column_names
146162
assert "position" in table.column_names
147163

@@ -171,7 +187,9 @@ def never_reached(ep: hflow.Episode) -> hflow.CheckResult:
171187
assert by_name["camera_blackout"].status == "failed"
172188

173189

174-
def test_crashing_check_is_infrastructure_not_data(source_episode: Path, tmp_path: Path) -> None:
190+
def test_crashing_check_is_infrastructure_not_data(
191+
state_only_source_episode: Path, tmp_path: Path
192+
) -> None:
175193
app = hflow.App("crashy-pipeline", data_root=tmp_path)
176194

177195
@app.check()
@@ -182,7 +200,7 @@ def exploding(ep: hflow.Episode) -> hflow.CheckResult:
182200
def still_runs(ep: hflow.Episode) -> hflow.CheckResult:
183201
return hflow.CheckResult(measurements={"ran": True})
184202

185-
report = app.test(source_episode, verbose=False)
203+
report = app.test(state_only_source_episode, verbose=False)
186204
assert not report.quarantined
187205
by_name = {run.check.name: run for run in report.checks}
188206
assert by_name["exploding"].status == "error"
@@ -191,7 +209,7 @@ def still_runs(ep: hflow.Episode) -> hflow.CheckResult:
191209

192210

193211
def test_resource_declaring_checks_run_after_plain_ones(
194-
source_episode: Path, tmp_path: Path
212+
state_only_source_episode: Path, tmp_path: Path
195213
) -> None:
196214
app = hflow.App(
197215
"ordered-pipeline", data_root=tmp_path, endpoints={"judge": "http://localhost:9"}
@@ -208,16 +226,18 @@ def cheap(ep: hflow.Episode) -> hflow.CheckResult:
208226
execution_order.append("cheap")
209227
return hflow.CheckResult()
210228

211-
app.test(source_episode, verbose=False)
229+
app.test(state_only_source_episode, verbose=False)
212230
assert execution_order == ["cheap", "expensive"]
213231

214232

215-
def test_missing_provider_alias_fails_preflight(source_episode: Path, tmp_path: Path) -> None:
233+
def test_missing_provider_alias_fails_preflight(
234+
state_only_source_episode: Path, tmp_path: Path
235+
) -> None:
216236
app = hflow.App("misconfigured", data_root=tmp_path)
217237

218238
@app.check(uses="judge")
219239
def needs_endpoint(ep: hflow.Episode) -> hflow.CheckResult:
220240
return hflow.CheckResult()
221241

222242
with pytest.raises(ValueError, match="judge"):
223-
app.test(source_episode, verbose=False)
243+
app.test(state_only_source_episode, verbose=False)

tests/test_runtime_client.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,34 @@ def log_message(self, format: str, *args: Any) -> None:
9999
pass
100100

101101

102-
@pytest.fixture()
103-
def stub_server() -> Iterator[str]:
102+
def _reset_stub_airflow_state() -> None:
104103
_StubAirflowHandler.issued_tokens = []
105104
_StubAirflowHandler.requests_seen = []
106105
_StubAirflowHandler.healthy = True
107106
_StubAirflowHandler.expire_first_token = False
107+
108+
109+
@pytest.fixture(scope="module")
110+
def stub_server_base_url() -> Iterator[str]:
108111
server = ThreadingHTTPServer(("127.0.0.1", 0), _StubAirflowHandler)
109-
thread = threading.Thread(target=server.serve_forever, daemon=True)
110-
thread.start()
112+
server_thread = threading.Thread(
113+
target=server.serve_forever,
114+
kwargs={"poll_interval": 0.01},
115+
daemon=True,
116+
)
117+
server_thread.start()
111118
try:
112119
yield f"http://127.0.0.1:{server.server_port}"
113120
finally:
114121
server.shutdown()
122+
server.server_close()
123+
server_thread.join()
124+
125+
126+
@pytest.fixture()
127+
def stub_server(stub_server_base_url: str) -> str:
128+
_reset_stub_airflow_state()
129+
return stub_server_base_url
115130

116131

117132
def test_trigger_fetches_token_once_and_sends_bearer(stub_server: str) -> None:
@@ -176,7 +191,22 @@ def test_health_parses_body_not_status(stub_server: str) -> None:
176191

177192

178193
def test_wait_until_healthy_times_out_with_last_status(stub_server: str) -> None:
194+
class InstantlyAdvancingClock:
195+
def __init__(self) -> None:
196+
self.current_time_s = 0.0
197+
198+
def monotonic(self) -> float:
199+
return self.current_time_s
200+
201+
def sleep(self, duration_s: float) -> None:
202+
self.current_time_s += duration_s
203+
179204
_StubAirflowHandler.healthy = False
180205
client = AirflowClient(stub_server, "airflow", "right-password")
181-
with pytest.raises(TimeoutError, match="scheduler=unhealthy"):
206+
instantly_advancing_clock = InstantlyAdvancingClock()
207+
with (
208+
pytest.MonkeyPatch.context() as monkeypatch,
209+
pytest.raises(TimeoutError, match="scheduler=unhealthy"),
210+
):
211+
monkeypatch.setattr("hflow.runtime._client.time", instantly_advancing_clock)
182212
client.wait_until_healthy(timeout_s=0.3, poll_interval_s=0.1)

0 commit comments

Comments
 (0)