Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ jobs:
- name: Run pipeline tests
run: mvn -B -ntp -pl transitclockPipelineTests -am -P include-pipeline-tests test

# Integration tests live in transitclockIntegration and are the
# heaviest tier — full AVL-CSV replays through a booted Core. Opt-in
# via include-integration-tests. Kept as its own step so a failure
# here is attributed distinctly from the unit and pipeline tiers.
- name: Run integration tests
run: mvn -B -ntp -pl transitclockIntegration -am -P include-integration-tests test

- name: Upload JaCoCo aggregate report
if: always()
uses: actions/upload-artifact@v4
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@
.DS_Store

settings.local.json

# WMATA capture output — live API data, not fixture-ready, not for VCS
/tools/wmata_capture/output/
/tools/wmata_capture/.env
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Built from the repository root as a Maven multi-module project.
- Run a single test class: `mvn -pl transitclock test -Dtest=TestAPIKeyManager`
- Integration tests live in the `transitclockIntegration` module and are **excluded by default** via the `skip-integration-tests` profile. Enable with: `mvn install -P include-integration-tests`
- Pipeline tests live in the `transitclockPipelineTests` module and are **excluded by default** — opt-in via the `include-pipeline-tests` profile. They boot a real Core against an in-memory HSQL database populated with a small WMATA GTFS fixture and exercise matcher/generator behavior end-to-end (lighter than the full AVL-trace runs in `transitclockIntegration`). Enable with: `mvn -pl transitclockPipelineTests -am -P include-pipeline-tests test`. CI runs this as a separate step after `mvn verify`.
- Run **everything** (unit + pipeline + integration) in one invocation: `mvn install -P run-all-tests`. The `run-all-tests` profile activates both extra modules so the reactor picks them up alongside the default build.

`mvn test` on the full reactor fails: `transitclockQuickStart` binds `maven-dependency-plugin:copy` to `generate-resources` to pull the `transitclockApi` WAR into its resources, but the `test` phase never packages that WAR (MDEP-187: "Artifact has not been packaged yet"). Use `mvn verify` / `mvn package` / `mvn install` to exercise all tests, or scope to one module with `-pl`, or skip QuickStart: `mvn test -pl '!transitclockQuickStart'`.
- Shaded executable JARs are emitted into `transitclock/target/` (e.g. `Core.jar`, `GtfsFileProcessor.jar`, `SchemaGenerator.jar`, `CreateWebAgency.jar`, `CreateAPIKey.jar`, `RmiQuery.jar`, `UpdateTravelTimes.jar`, `ScheduleGenerator.jar`) — each is a maven-shade execution in `transitclock/pom.xml`.
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ Two additional test suites are opt-in via Maven profiles and excluded from the d
mvn install -P include-integration-tests
```

To run **everything** (unit + pipeline + integration) in one go:
```
mvn install -P run-all-tests
```

### Code coverage

JaCoCo generates coverage reports as part of the Maven `verify` phase.
Expand Down
7 changes: 7 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,12 @@
<module>transitclockPipelineTests</module>
</modules>
</profile>
<profile>
<id>run-all-tests</id>
<modules>
<module>transitclockIntegration</module>
<module>transitclockPipelineTests</module>
</modules>
</profile>
</profiles>
</project>
8 changes: 8 additions & 0 deletions tools/wmata_capture/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copy this file to `.env` and fill in your real key. `.env` is gitignored.
#
# Request a key at https://developer.wmata.com — the free tier covers a
# handful of consecutive hours of GTFS-RT polling, which is plenty to
# capture fixture-sized traces (~4h).
#
# DO NOT commit your real key. DO NOT pass it as a CLI flag.
WMATA_API_KEY=your-wmata-api-key-here
149 changes: 149 additions & 0 deletions tools/wmata_capture/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# tools/wmata_capture

Capture live WMATA bus data into the exact fixture format the
`transitclockIntegration` suite already ingests, so we can refresh the
aging 2016 fixtures with fresh, realistic traces.

The capture is a single-file Python script (`capture.py`) with
[PEP 723](https://peps.python.org/pep-0723/) inline dependency metadata.
It's designed to run for hours in the background and produce:

1. `<output-dir>/gtfs/` — an unpacked static GTFS feed (the feed WMATA
publishes at the instant you start the capture).
2. `<output-dir>/avl/<route_id>_<vehicle_id>.csv` — one CSV per vehicle,
formatted for `BatchCsvAvlFeedModule` with header
`vehicleId,time,assignmentId,assignmentType,heading,latitude,longitude`.
3. `<output-dir>/capture.log` — run log (appended across repeated runs
against the same output dir).

## Why this exists

Two integration tests in `transitclockIntegration` are currently
`@Ignore`'d because their 2016 WMATA fixtures have aged past the point
where the pinned assertions are meaningful — see
[OneBusAway/thetransitclock#7](https://github.com/OneBusAway/thetransitclock/issues/7)
and
[OneBusAway/thetransitclock#8](https://github.com/OneBusAway/thetransitclock/issues/8).
To re-enable them properly we need fresh AVL traces and a fresh baseline
prediction CSV. This tool produces the AVL side; the baseline CSV is then
regenerated by re-running the prediction test against the captured data.

## Prerequisites

- `uv` installed (`brew install uv` or see
[astral.sh/uv](https://docs.astral.sh/uv/)).
- A WMATA developer API key from
[developer.wmata.com](https://developer.wmata.com). The free tier is
sufficient.
- `WMATA_API_KEY` available in your environment. Copy `.env.example` to
`.env` and `source .env` before running — **never** commit the key, and
never pass it as a CLI flag (it would show up in shell history and in
the output of `ps`).

The script declares its own Python dependencies (`requests`,
`gtfs-realtime-bindings`) via PEP 723, so `uv run capture.py ...` will
resolve them into an ephemeral venv the first time.

## Run recipes

### Quick smoke test (5 minutes, one route)

```sh
export WMATA_API_KEY=...
uv run capture.py \
--output-dir ./output/smoke-$(date +%Y%m%d-%H%M%S) \
--duration-hours 0.083 \
--routes S2
```

Expected output:

- `./output/smoke-.../gtfs/trips.txt` etc. present
- `./output/smoke-.../avl/S2_<vehicleId>.csv` for every S2 vehicle the
feed reported
- `./output/smoke-.../capture.log` with `polls ok: ~10` and a row count

### Realistic fixture capture (AM rush, small route set)

```sh
export WMATA_API_KEY=...
uv run capture.py \
--output-dir ./output/fixtures-$(date +%Y%m%d) \
--duration-hours 4 \
--poll-interval 30 \
--routes S2,3T,5A
```

Run against Eastern weekday AM peak (~06:00–10:00 ET) for the most data
per vehicle. Background it with `nohup ... &` or run under `tmux`.

### Single-vehicle targeted capture

```sh
export WMATA_API_KEY=...
uv run capture.py \
--output-dir ./output/target-$(date +%Y%m%d) \
--duration-hours 2 \
--vehicles 2113
```

## Promoting captures to fixtures

Once a capture looks good:

1. Replace the module's GTFS:
```sh
rm -rf transitclockIntegration/src/test/resources/gtfs/S2
cp -R tools/wmata_capture/output/<run>/gtfs transitclockIntegration/src/test/resources/gtfs/S2
```
Adjust the target subdir name to the route the test uses.
2. Replace the AVL CSV:
```sh
cp tools/wmata_capture/output/<run>/avl/S2_<vehicleId>.csv \
transitclockIntegration/src/test/resources/avl/S2_<vehicleId>.csv
```
Rename to match the test's hard-coded constant
(e.g. `S2_2113.csv`).
3. **Regenerate the prediction baseline** (`pred/*.csv`). This is not
produced by this capture tool — it's the *output* of running the
current predictor against the new AVL. To rebuild:
- Temporarily remove the `@Ignore` from
`PredictionAccuracyIntegrationTest`.
- Run the test; dump the `new`-side predictions (see the test's
`setUp` — `session.createCriteria(Prediction.class).list()`) to a
CSV with the same columns as the existing
`pred/S2_2113.csv`.
- Save that CSV as the new baseline.
- Restore `@Ignore` removal into a real code change, not a test run.
4. Commit the new fixtures + baseline in one PR; reference issues
OneBusAway/thetransitclock#7 and #8 in the description.

## Known caveats

- **`block_id` in WMATA GTFS is not always populated.** The script falls
back to `assignmentType=TRIP_ID` when the GTFS-RT feed reports a
`trip_id` that has no corresponding `block_id` in `trips.txt`.
`BatchCsvAvlFeedModule` + `BlockAssigner` handle both types, but some
transitclock behavior paths depend on block assignment specifically —
prefer capturing a route/period where `trips.txt` does carry block_ids
(check via `awk -F, '{print $<block-col>}' trips.txt | sort -u | head`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Replace the non-runnable awk placeholder with an executable command.

$<block-col> is a placeholder and will fail if copied verbatim.

📘 Proposed doc fix
-  (check via `awk -F, '{print $<block-col>}' trips.txt | sort -u | head`).
+  (check via `awk -F, 'NR==1{for(i=1;i<=NF;i++) if($i=="block_id") c=i; next} c && $c!="" {print $c}' trips.txt | sort -u | head`).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(check via `awk -F, '{print $<block-col>}' trips.txt | sort -u | head`).
(check via `awk -F, 'NR==1{for(i=1;i<=NF;i++) if($i=="block_id") c=i; next} c && $c!="" {print $c}' trips.txt | sort -u | head`).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/wmata_capture/README.md` at line 131, The README contains a
non-runnable awk placeholder "$<block-col>" — replace that placeholder with the
actual CSV column index number used for the block column, or modify the awk
invocation to accept a variable (e.g., pass -v col=N and use print $col) so the
command is executable; update the example line that currently shows "awk -F,
'{print $<block-col>}'" to use either a concrete column number or the -v
variable form so readers can copy-and-run it.

- **The GTFS-RT feed repeats observations.** The script de-duplicates on
`(vehicle_id, feed-reported timestamp)` within a single run, so a
30-second poll interval against a feed that only updates every 60s
won't double up. De-dup state is not persisted across runs — don't
resume into the same output dir and expect continuity; start a new
output dir.
- **Clock.** Timestamps are emitted in `America/New_York` local time
formatted `MM-dd-yyyy HH:mm:ss` (the format `BatchCsvAvlFeedModule`
parses using the JVM default timezone). The capture host's TZ does
not matter.
- **Outages happen.** WMATA's API can return 5xx and can stall for
minutes. The script retries with a bounded exponential backoff (up to
~2 minutes between attempts) rather than dying.

## Files

- `capture.py` — the script (PEP 723, single file).
- `.env.example` — template for `WMATA_API_KEY`. Real `.env` is
gitignored.
- `output/` — default capture destination. Gitignored.
Loading
Loading