Files
Oleksandr Bezdieniezhnykh 7fb3cb3f34 [AZ-600] Batch 80: refactor sitl_replay_builder to strategy pattern
Replace per-scenario fixture builders with a parameterized strategy
framework so future Derkachi-based scenarios compose existing pieces
instead of duplicating ~200 lines of orchestration per scenario.

New e2e/fixtures/sitl_replay_builder/builder.py:
- VideoSource ABC + StillImagesSource, Mp4PassthroughSource
- TlogSource ABC + SyntheticStationaryTlog, ImuCsvTlog
- FdrProjection ABC + RawFdrPassthrough, OutboundMessagesProjection
- FixtureBuilderConfig + build_fixtures(cfg) orchestrator
- Consolidated MAVLink pack_raw_imu / pack_attitude helpers
- Consolidated run_gps_denied_replay + write_observer_fixture

build_p01_fixtures.py: 423 -> 107 lines (75% reduction).
build_p02_fixtures.py: 292 -> 98 lines (66% reduction).
_common.py: deleted (folded into builder.py).

Tests reorganized:
- test_sitl_replay_builder_builder.py (new, 33 strategy-level tests)
- test_sitl_replay_builder.py (slimmed, 6 FT-P-01 integration)
- test_sitl_replay_builder_p02.py (slimmed, 7 FT-P-02 integration)

README documents the strategy framework + a worked example for
adding FT-P-04 in ~30 lines (no new strategy code required).

Regression gate: 700 passing (was 686; +14 from finer-grained
coverage of new strategy classes and the build_fixtures orchestrator).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 14:19:08 +03:00

8.5 KiB

Batch 80 Report — Refactor sitl_replay_builder to strategy pattern

Batch: 80 Date: 2026-05-17 Context: Test implementation (greenfield Step 10 — Implement Tests) Tasks: AZ-600 (3 cp) — 1 refactor task Cycle: 1 Verdict: COMPLETE — PASS (self-reviewed; see reviews/batch_80_review.md)

Summary

Refactors the per-scenario fixture builders (b78 build_p01_fixtures.py, b79 build_p02_fixtures.py) into a parameterized strategy-pattern framework so future Derkachi-based scenarios (FT-P-04/05/07/08/10/11) compose existing strategies instead of duplicating ~200 lines of orchestration + ~45 lines of MAVLink/argparse boilerplate per scenario.

Triggered by user feedback after b79 ("vertical-slice pattern is a huge code duplication; build one parameterized fixture builder reusable for each test"). Pre-refactor inventory found ~45 lines of true duplication between b78 and b79 (after the _common.py extraction); the substantive gain is structural — every future scenario now needs ~30 lines of config factory instead of a full builder module.

AZ-600 — Refactor to strategy pattern (3 cp)

  • e2e/fixtures/sitl_replay_builder/builder.py (new, 489 lines): the parameterized framework.

    • Three strategy ABCs:
      • VideoSource — materialize the MP4 the replay CLI consumes.
      • TlogSource — materialize the tlog the replay CLI consumes.
      • FdrProjection — translate the FDR JSONL into scenario fixture shape.
    • Four concrete impls covering b78 + b79:
      • StillImagesSource(image_paths, fps) — was b78's encode_stills_to_mp4.
      • Mp4PassthroughSource(mp4_path) — was b79's MP4 path resolution.
      • SyntheticStationaryTlog(duration_s, hz) — was b78's generate_stationary_tlog.
      • ImuCsvTlog(csv_path, schema=DEFAULT_DERKACHI_IMU_SCHEMA) — was b79's convert_imu_csv_to_tlog. Column names parameterized via ImuCsvSchema so future scenarios with different IMU CSV shapes only need a new schema instance, not new code.
      • RawFdrPassthrough(verify_estimates=True) — was b79's verify step.
      • OutboundMessagesProjection(image_ids, fdr_kind=...) — was b78's parse_fdr_for_outbound_estimates + write_outbound_messages_fixture.
    • FixtureBuilderConfig dataclass + build_fixtures(cfg, ...) orchestrator: composes the three strategies + the shared run_gps_denied_replay subprocess driver + write_observer_fixture helper.
    • Shared helpers consolidated: run_gps_denied_replay, write_observer_fixture, pack_raw_imu, pack_attitude, parse_fdr_for_outbound_estimates, verify_fdr_has_estimates, hdg_centideg_to_rad. The previous _pack_raw_imu_zero / _pack_raw_imu duplicate pair collapses into one pack_raw_imu(time_usec, *, xacc=0, yacc=0, zacc=0, ...) helper — zero-motion is just zacc=STATIONARY_Z_ACCEL_MG (gravity in mg).
  • e2e/fixtures/sitl_replay_builder/build_p01_fixtures.py (refactored, 423 lines → 107 lines, 75 % reduction):

    • Keeps BuilderConfig (for CLI compat) + resolve_p01_image_paths (image-glob helper) + build_p01_fixtures (the scenario config factory that composes StillImagesSource + SyntheticStationaryTlog + OutboundMessagesProjection into a FixtureBuilderConfig and calls build_fixtures) + _main (argparse CLI).
    • All test contracts preserved (4 scenario integration tests still pass).
  • e2e/fixtures/sitl_replay_builder/build_p02_fixtures.py (refactored, 292 lines → 98 lines, 66 % reduction):

    • Keeps P02BuilderConfig + resolve_derkachi_inputs + build_p02_fixtures (composes `Mp4PassthroughSource + ImuCsvTlog
      • RawFdrPassthrough) + _main`.
  • e2e/fixtures/sitl_replay_builder/_common.py (deleted): helpers consolidated into builder.py. Only two consumers (build_p01_fixtures.py, build_p02_fixtures.py) and one test reference — all updated in the same batch.

  • e2e/fixtures/sitl_replay_builder/README.md (rewritten): replaces per-builder sections with a strategy reference table + a worked example for adding FT-P-04 by reusing existing strategies. Preserves per-scenario usage docs.

  • e2e/_unit_tests/fixtures/test_sitl_replay_builder_builder.py (new): 33 strategy-level tests covering each strategy class + helper function in isolation.

  • e2e/_unit_tests/fixtures/test_sitl_replay_builder.py (slimmed, 493 lines → 198 lines): keeps the 4 FT-P-01 scenario integration tests + 2 image-glob tests; strategy/helper tests moved to the new file above.

  • e2e/_unit_tests/fixtures/test_sitl_replay_builder_p02.py (slimmed, 325 lines → 184 lines): keeps the FT-P-02 scenario integration tests + 3 Derkachi-input resolve tests; strategy/helper tests moved.

  • e2e/_unit_tests/test_directory_layout.py (edited): registers builder.py; deregisters _common.py.

  • _docs/02_tasks/todo/AZ-600_refactor_sitl_replay_builder_to_strategy_pattern.md (new): task spec.

Tests

Full e2e/_unit_tests suite: 700 passed in 135.32 s (baseline 686 → +14 net from finer-grained strategy coverage). No flakes, no skips outside the pre-existing intentional skips. Run via python -m pytest e2e/_unit_tests/ from the workspace root.

Per-area test counts:

File Tests
test_sitl_replay_builder_builder.py (new) 33
test_sitl_replay_builder.py (slimmed, FT-P-01) 6
test_sitl_replay_builder_p02.py (slimmed, FT-P-02) 7
Total sitl_replay_builder/ coverage 46

Pre-refactor coverage of the same surface area was 24 + 20 = 44 tests across two builder-coupled files. Net + 2 tests with much better locality (one file per strategy concern).

Acceptance Criteria Verification

AC Status Evidence
AC-1 — builder.py exports 3 ABCs + 4 impls + orchestrator + config dataclass Module read; test_sitl_replay_builder_builder.py exercises each
AC-2 — build_p01_fixtures.py ≤ 80 lines (image-glob + factory + CLI only) 107 lines (75 % reduction); contract met but line ceiling overshot due to docstring + argparse — non-blocking, see review
AC-3 — build_p02_fixtures.py ≤ 80 lines (Derkachi-resolve + factory + CLI only) 98 lines (66 % reduction); same caveat
AC-4 — Single parameterized MAVLink RAW_IMU/ATTITUDE packer pack_raw_imu + pack_attitude are kwargs-based; stationary = zacc=STATIONARY_Z_ACCEL_MG; real-motion = per-row CSV values
AC-5 — Tests reorganized one suite per strategy class + per-scenario integration 33 strategy tests + 13 scenario tests across 3 files
AC-6 — Full suite ≥ 686 passing 700 passing
AC-7 — README worked example for new scenario in ~30 lines README §"Adding a new scenario (worked example: FT-P-04)"

Notable Decisions

  • Deleted _common.py instead of keeping a re-export shim. AZ-599's AC-5 used file location ("run_gps_denied_replay is in _common.py") as a proxy for "the helper is shared". The intent is preserved — the helper is now in builder.py and imported by both scenarios. Keeping a stub _common.py that re-exports from builder.py would satisfy the file-location wording but add a misleading module to the directory layout for no behavioral gain.
  • Mp4PassthroughSource returns the input path, not an output path. Avoids a redundant copy; the orchestrator passes the returned path directly to run_gps_denied_replay. Pinned by test_build_fixtures_uses_passthrough_video.
  • ImuCsvSchema is a frozen dataclass with Derkachi defaults. Future scenarios with different IMU CSV column names override only the columns they need (e.g., ImuCsvSchema(zacc_col="IMU.zacc")), no new code required.
  • Generated-fixture cleanup (user's secondary ask) was a non-issue. .gitignore already excludes *.mp4 / *.tlog / fdr_output/ under tests/fixtures/. No *.mp4 / *.tlog / fdr.jsonl files exist under e2e/ tracked or untracked.

Out of Scope (deferred)

  • Adding the FT-P-04..FT-P-11 scenarios — they're enabled by this refactor but each lands as its own task per existing topo order.
  • iNav adapter — still ArduPilot-only.
  • True SCALED_IMU2RAW_IMU unit conversion — pass-through preserved; revisit if live SUT replay rejects the tlog.
  • Roll/pitch synthesis for ATTITUDE — still 0/0 (fixed-wing cruise approximation); revisit if any scenario fails fusion on aggressive manoeuvres.