Skip to content

Integration tests - #9

Merged
aaronbrethorst merged 9 commits into
developfrom
integration-tests
Apr 24, 2026
Merged

Integration tests#9
aaronbrethorst merged 9 commits into
developfrom
integration-tests

Conversation

@aaronbrethorst

@aaronbrethorst aaronbrethorst commented Apr 24, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added a data-capture tool to produce transit GTFS/AVL fixtures.
    • New Maven profile to run unit, pipeline, and integration tests together.
  • Documentation

    • Added capture tool README and example env; updated test-running docs.
  • Chores

    • CI updated to run integration tests as a separate step.
    • Test runtime deps and build plugins upgraded; integration test cache/config adjusted.
    • Some long-running integration tests temporarily disabled.

`mvn install -P include-integration-tests` was failing before any test
could run due to a cascade of bitrot. This commit gets the fork VM
healthy and the suite executing; two tests still fail on stale 2016
assertions (RecoverFromDetourTest, PredictionAccuracyIntegrationTest)
but those are real test-expectation issues, not infrastructure.

- PlaybackModule: match current GtfsData ctor (18 args, not 16) and
  stage GTFS to a temp dir with rewritten calendar.txt end_dates, so
  GtfsData.isCalendarActiveInTheFuture stops calling System.exit(-1)
  and killing the surefire fork. Mirrors transitclockPipelineTests'
  CoreHarness.
- pom.xml: add test-scoped commons-lang3 (transitclockCore shades it,
  so consumers don't see it transitively). Bump surefire/failsafe to
  3.5.4 for readable failure output.
- hsql_integration_test_hibernate.cfg.xml: hbm2ddl.auto create -> update.
  Core's constructor calls clearSessionFactory() after setupGtfs loads
  the GTFS; with "create" the factory rebuild drops every table,
  leaving BlockAssigner with serviceIds=[].
- ehcache.xml: shadow the core copy which hardcodes persistence at
  /usr/local/transitclock/cache/ (not writable under CI/dev). Uses
  target/ehcache-integration-store, same pattern as pipeline tests.
- AVL/pred CSVs: convert yyyy-MM-dd -> MM-dd-yyyy. Time.parse expects
  MM-dd-yyyy; ISO strings were being parsed as year 0195 and no
  vehicle could match any service.
Both rely on 2016 WMATA AVL/GTFS data plus (for the prediction test) a
2016 baseline that predates ~10 years of predictor changes, so their
failing assertions no longer represent regressions against the current
code. Re-enable once tools/wmata_capture lands and we can rebuild
fixtures/baselines against the current engine.

- RecoverFromDetourTest.test: #7
- PredictionAccuracyIntegrationTest.testPredictions: #8
Single-file Python (PEP 723) capture tool that polls WMATA's GTFS-RT
VehiclePositions feed and writes per-vehicle CSVs in the exact shape
BatchCsvAvlFeedModule ingests (header: vehicleId,time,assignmentId,
assignmentType,heading,latitude,longitude; time in MM-dd-yyyy HH:mm:ss
Eastern). Also fetches and unpacks the static GTFS once so the capture
output is a self-contained fixture bundle.

Requires uv; dependencies (requests, gtfs-realtime-bindings) declared
inline via PEP 723 so `uv run capture.py ...` is the only setup step.
WMATA_API_KEY is read from the environment only; the script refuses to
accept it on the command line and prints a pointer to .env.example.

Output/ and .env are gitignored so a long background capture can't
accidentally leak AVL data or the API key into VCS.

Intended use: regenerate the 2016 fixtures used by the now-@ignore'd
RecoverFromDetourTest (#7) and PredictionAccuracyIntegrationTest (#8).
The README walks through the smoke test, a realistic 4-hour AM-peak
capture, and the promote-to-fixtures steps.
Running unit + pipeline + integration tiers used to require two separate
invocations. The new root-pom profile layers both transitclockIntegration
and transitclockPipelineTests into the reactor so a single
`mvn install -P run-all-tests` exercises everything. README.md and
CLAUDE.md call out the new command alongside the existing per-tier
profiles.
Both classes extended junit.framework.TestCase, so surefire routed them
through JUnit38ClassRunner — which discovers tests by "test*" naming and
ignores JUnit 4 annotations including @ignore. The tests kept running
and kept failing, defeating the disable.

Convert both to pure JUnit 4 by dropping `extends TestCase`, switching
to `org.junit.Assert.*` static imports, and (for the prediction test)
marking the former JUnit 3 `setUp()` with `@Before`. After this the two
tests report as Skipped instead of Failure.

The other two integration tests (EffectiveScheduleDifferenceDuringLayover,
GenerateEffectiveScheduleDifference) still extend TestCase and continue
to run under the JUnit 3 runner — not touching them.
Mirrors the existing pipeline-tests step: separate invocation of
`mvn -pl transitclockIntegration -am -P include-integration-tests test`
so an integration failure is attributed distinctly from the unit and
pipeline tiers. All three tiers now run on every push/PR to the main
branches.
@coderabbitai

coderabbitai Bot commented Apr 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a WMATA capture CLI to record GTFS/GTFS‑RT fixtures, CI and Maven support to run integration tests (new profile and CI step), integration test migration to JUnit4 with some tests ignored, and integration-test-specific cache/DB staging and config updates; docs and .gitignore entries included.

Changes

Cohort / File(s) Summary
CI & Build
.github/workflows/ci.yml, pom.xml, transitclockIntegration/pom.xml
Added CI step to run integration module with -P include-integration-tests and -am; new run-all-tests Maven profile to include pipeline+integration modules; upgraded surefire/failsafe and added commons-lang3 test dependency.
WMATA Capture Tool
tools/wmata_capture/capture.py, tools/wmata_capture/.env.example
New CLI script to download GTFS, build trip→block map, poll GTFS‑RT VehiclePositions, dedupe and write per-vehicle AVL CSVs, handle signals/retries/duration, and produce a capture.log; example .env added.
WMATA Capture Docs & Guidance
tools/wmata_capture/README.md, README.md, CLAUDE.md
New, detailed usage and promotion workflow for capture.py; documents mvn install -P run-all-tests and how to regenerate prediction baselines; run recipes and caveats included.
Integration Test Code
transitclockIntegration/src/test/java/.../RecoverFromDetourTest.java, .../PredictionAccuracyIntegrationTest.java
Migrated tests from JUnit3 TestCase to JUnit4 style, switched to JUnit4 assertions, and annotated long-running/fixture-dependent tests with @Ignore.
Integration Test Runtime Config
transitclockIntegration/src/main/java/.../PlaybackModule.java, .../hsql_integration_test_hibernate.cfg.xml, .../ehcache.xml
GTFS staging added (calendar end_dates rewritten) and new GtfsData params; Hibernate schema strategy changed from createupdate; added integration-specific Ehcache config with persistence under target/.
Git & Env Ignoring
.gitignore
Ignore WMATA capture outputs and .env plus __pycache__/.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as capture.py (CLI)
    participant WMATA as WMATA API
    participant FS as FileSystem
    participant State as Capture State
    participant Tests as Integration Tests

    CLI->>WMATA: download static GTFS (api_key)
    WMATA-->>FS: GTFS tar.gz
    CLI->>FS: extract GTFS -> output_dir/gtfs
    CLI->>CLI: build trip_id -> block_id map

    loop polling loop (with exponential backoff)
        CLI->>WMATA: fetch GTFS-RT VehiclePositions
        WMATA-->>CLI: FeedMessage
        CLI->>State: filter by route/vehicle, map assignment
        CLI->>State: deduplicate by (vehicle, timestamp)
        CLI->>FS: append CSV row to avl/<route>_<vehicle>.csv
        CLI->>FS: flush writers (periodic)
    end

    CLI->>FS: append capture.log + summary
    CLI->>Tests: user promotes captured GTFS/AVL into integration test resources
    Tests->>FS: consume GTFS + AVL CSVs during integration test run
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related issues

Possibly related PRs

  • Tests #1: CI/profile changes here extend the same CI and Maven profile area (integration/pipeline test coordination) introduced in that PR.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Integration tests' is vague and generic. While the PR does add integration test infrastructure, the title doesn't convey the scope of changes which include WMATA data capture tooling, Maven profile configuration, CI workflow updates, and comprehensive documentation. Consider a more descriptive title such as 'Add integration test infrastructure with WMATA capture tooling' or 'Set up integration test suite with data capture and CI workflows' to better reflect the breadth of changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch integration-tests

Comment @coderabbitai help to get the list of available commands and usage tips.

`source .env` doesn't export plain KEY=VALUE lines, so users who
followed the README literally hit "WMATA_API_KEY is not set" even with
a populated .env. Fix by reading tools/wmata_capture/.env at script
startup with a minimal built-in parser — no new dependencies, no shell
gymnastics required. An already-exported WMATA_API_KEY still wins over
the file, so explicit exports keep working.

README and .env.example updated to match the new ergonomics.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
pom.xml (1)

71-77: Profile may need -am flag or core modules.

The run-all-tests profile only includes transitclockIntegration and transitclockPipelineTests, but both depend on transitclockCore. Unlike include-integration-tests (which lists all core modules), this profile requires the -am (also-make) flag to build dependencies, or it will fail when used standalone.

Consider either:

  1. Documenting that mvn install -P run-all-tests -am is required, or
  2. Adding core modules to match include-integration-tests
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pom.xml` around lines 71 - 77, The run-all-tests profile (profile id
"run-all-tests") only lists modules transitclockIntegration and
transitclockPipelineTests but those depend on transitclockCore, so update the
build to ensure dependencies are built: either document that callers must invoke
Maven with -am (e.g., "mvn install -P run-all-tests -am") or modify the
run-all-tests profile to include the core modules (mirror what
include-integration-tests does by adding transitclockCore and other core
modules) so the profile can be executed standalone without -am.
transitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java (1)

250-251: Temp directory cleanup may not work as expected.

staged.toFile().deleteOnExit() only deletes the directory itself on JVM exit, and only if it's empty. The files copied/created inside won't be deleted, so the directory delete will fail silently. Over many test runs, this could accumulate orphaned temp directories.

Consider registering each file for deletion or using a shutdown hook that recursively deletes the staged directory.

♻️ Suggested fix to ensure cleanup
 		Path staged = Files.createTempDirectory("playback-gtfs-");
-		staged.toFile().deleteOnExit();
+		// Register shutdown hook for recursive cleanup
+		Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+			try {
+				Files.walk(staged)
+					.sorted(java.util.Comparator.reverseOrder())
+					.forEach(p -> { try { Files.deleteIfExists(p); } catch (IOException ignored) {} });
+			} catch (IOException ignored) {}
+		}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@transitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java`
around lines 250 - 251, The temp-dir created via
Files.createTempDirectory("playback-gtfs-") and assigned to staged is only
scheduled for deletion with staged.toFile().deleteOnExit(), which fails if the
directory is non-empty; change PlaybackModule to instead register a JVM shutdown
hook that recursively deletes the staged directory (walk the file tree and
delete files then directories) or explicitly register each created temp file for
deletionOnExit; locate the staged variable and replace the single deleteOnExit()
call with a shutdown hook that performs Files.walkFileTree (or equivalent
recursive delete) to remove all contents and the directory on JVM exit.
README.md (1)

60-64: Add language specifier to fenced code block.

For consistency with the other code blocks in this file (which use language specifiers), add bash or sh to the code fence.

✏️ Suggested fix
 To run **everything** (unit + pipeline + integration) in one go:
-```
+```bash
 mvn install -P run-all-tests
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @README.md around lines 60 - 64, Update the fenced code block that contains
"mvn install -P run-all-tests" to include a language specifier (use "bash" or
"sh") so the opening backticks become ```bash; this keeps the README's code
block style consistent with the other examples and enables proper syntax
highlighting for the mvn command.


</details>

</blockquote></details>
<details>
<summary>tools/wmata_capture/README.md (1)</summary><blockquote>

`107-119`: **Clarify the baseline regeneration workflow wording.**

Line 117 is a bit confusing: "Restore `@Ignore` removal into a real code change, not a test run." 

I believe the intent is to permanently remove the `@Ignore` annotation once the new baseline is generated, but the current wording could be misread. Consider rephrasing.



<details>
<summary>✏️ Suggested clarification</summary>

```diff
-   - Restore `@Ignore` removal into a real code change, not a test run.
+   - Permanently remove the `@Ignore` annotation (commit the change rather than just un-ignoring for a local test run).
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@tools/wmata_capture/README.md` around lines 107 - 119, Reword the confusing
sentence about `@Ignore` in the README step for regenerating prediction baselines:
update the sentence referencing PredictionAccuracyIntegrationTest and `@Ignore` to
clearly state the intended workflow (temporarily remove `@Ignore` to run the test
and generate the CSV baseline, then re-apply `@Ignore` in the test and make any
permanent code changes in a separate commit/PR), so readers understand that
`@Ignore` removal is only for the temporary test run and permanent changes should
be made in a proper code change referencing PredictionAccuracyIntegrationTest
and the generated baseline.
```

</details>

</blockquote></details>
<details>
<summary>tools/wmata_capture/capture.py (1)</summary><blockquote>

`267-267`: **Remove redundant `int()` cast.**

In Python 3, `round()` returns an `int` when called with a single argument, making the outer `int()` unnecessary.



<details>
<summary>✏️ Suggested fix</summary>

```diff
-    heading = int(round(getattr(pos, "bearing", 0.0) or 0.0))
+    heading = round(getattr(pos, "bearing", 0.0) or 0.0)
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@tools/wmata_capture/capture.py` at line 267, The assignment to heading wraps
round(...) with an unnecessary int() cast; replace heading =
int(round(getattr(pos, "bearing", 0.0) or 0.0)) with heading =
round(getattr(pos, "bearing", 0.0) or 0.0) to remove the redundant int() while
keeping the same behavior for the heading variable (use the existing
getattr(pos, "bearing", 0.0) expression to locate the code).
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against the current code and only fix it if needed.

Inline comments:
In @transitclockIntegration/src/main/resources/ehcache.xml:

  • Line 16: The ehcache configuration references a non-existent thread pool via
    the attribute thread-pool="pool-disk" on the ehcache:disk-store element; either
    add a section that declares a thread-pool with alias="pool-disk"
    (e.g., thread-pool alias="pool-disk" with appropriate min-size/max-size) or
    remove the thread-pool="pool-disk" attribute from the ehcache:disk-store element
    to rely on Ehcache defaults so the configuration is consistent.

Nitpick comments:
In @pom.xml:

  • Around line 71-77: The run-all-tests profile (profile id "run-all-tests") only
    lists modules transitclockIntegration and transitclockPipelineTests but those
    depend on transitclockCore, so update the build to ensure dependencies are
    built: either document that callers must invoke Maven with -am (e.g., "mvn
    install -P run-all-tests -am") or modify the run-all-tests profile to include
    the core modules (mirror what include-integration-tests does by adding
    transitclockCore and other core modules) so the profile can be executed
    standalone without -am.

In @README.md:

  • Around line 60-64: Update the fenced code block that contains "mvn install -P
    run-all-tests" to include a language specifier (use "bash" or "sh") so the
    opening backticks become ```bash; this keeps the README's code block style
    consistent with the other examples and enables proper syntax highlighting for
    the mvn command.

In @tools/wmata_capture/capture.py:

  • Line 267: The assignment to heading wraps round(...) with an unnecessary int()
    cast; replace heading = int(round(getattr(pos, "bearing", 0.0) or 0.0)) with
    heading = round(getattr(pos, "bearing", 0.0) or 0.0) to remove the redundant
    int() while keeping the same behavior for the heading variable (use the existing
    getattr(pos, "bearing", 0.0) expression to locate the code).

In @tools/wmata_capture/README.md:

  • Around line 107-119: Reword the confusing sentence about @Ignore in the README
    step for regenerating prediction baselines: update the sentence referencing
    PredictionAccuracyIntegrationTest and @Ignore to clearly state the intended
    workflow (temporarily remove @Ignore to run the test and generate the CSV
    baseline, then re-apply @Ignore in the test and make any permanent code changes
    in a separate commit/PR), so readers understand that @Ignore removal is only for
    the temporary test run and permanent changes should be made in a proper code
    change referencing PredictionAccuracyIntegrationTest and the generated baseline.

In
@transitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java:

  • Around line 250-251: The temp-dir created via
    Files.createTempDirectory("playback-gtfs-") and assigned to staged is only
    scheduled for deletion with staged.toFile().deleteOnExit(), which fails if the
    directory is non-empty; change PlaybackModule to instead register a JVM shutdown
    hook that recursively deletes the staged directory (walk the file tree and
    delete files then directories) or explicitly register each created temp file for
    deletionOnExit; locate the staged variable and replace the single deleteOnExit()
    call with a shutdown hook that performs Files.walkFileTree (or equivalent
    recursive delete) to remove all contents and the directory on JVM exit.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro Plus

**Run ID**: `d0a48a80-e740-4061-aa65-8c0e3c693d4c`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 08f65709d159f46494fbe1ad5fc4005387f8850f and 18ba5e5af7e7c8b37faf36e5c03b7acd85d01fce.

</details>

<details>
<summary>⛔ Files ignored due to path filters (4)</summary>

* `transitclockIntegration/src/test/resources/avl/3T_3757.csv` is excluded by `!**/*.csv`
* `transitclockIntegration/src/test/resources/avl/5A_8062.csv` is excluded by `!**/*.csv`
* `transitclockIntegration/src/test/resources/avl/S2_2113.csv` is excluded by `!**/*.csv`
* `transitclockIntegration/src/test/resources/pred/S2_2113.csv` is excluded by `!**/*.csv`

</details>

<details>
<summary>📒 Files selected for processing (14)</summary>

* `.github/workflows/ci.yml`
* `.gitignore`
* `CLAUDE.md`
* `README.md`
* `pom.xml`
* `tools/wmata_capture/.env.example`
* `tools/wmata_capture/README.md`
* `tools/wmata_capture/capture.py`
* `transitclockIntegration/pom.xml`
* `transitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java`
* `transitclockIntegration/src/main/resources/ehcache.xml`
* `transitclockIntegration/src/main/resources/hsql_integration_test_hibernate.cfg.xml`
* `transitclockIntegration/src/test/java/org/transitclock/integration_tests/RecoverFromDetourTest.java`
* `transitclockIntegration/src/test/java/org/transitclock/integration_tests/prediction/PredictionAccuracyIntegrationTest.java`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

xmlns:ehcache='http://www.ehcache.org/v3'
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.1.xsd">
<ehcache:persistence directory="target/ehcache-integration-store" />
<ehcache:disk-store thread-pool="pool-disk" />

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

🧩 Analysis chain

🌐 Web query:

Ehcache 3 disk-store thread-pool configuration

💡 Result:

In Ehcache 3, the disk store (OffHeapDiskStore) performs asynchronous disk writes using configurable thread pools. Configuration is done via PooledExecutionService for thread pools, with specific settings for disk stores at CacheManager or cache levels. Key configuration points: 1. Define thread pools using PooledExecutionServiceConfigurationBuilder: - pool(alias, minSize, maxSize) 2. At CacheManager level: - .withDefaultDiskStoreThreadPool("poolAlias") - Or via OffHeapDiskStoreProviderConfiguration(threadPoolAlias) 3. At cache level: - CacheConfigurationBuilder.withDiskStoreThreadPool("poolAlias", writerConcurrency) Example (Java API): CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder .using(PooledExecutionServiceConfigurationBuilder.newPooledExecutionServiceConfigurationBuilder .defaultPool("dflt", 0, 10) .pool("defaultDiskPool", 1, 3) .build) .withDefaultDiskStoreThreadPool("defaultDiskPool") .withCache("myCache", CacheConfigurationBuilder.newCacheConfigurationBuilder(Long.class, String.class, ResourcePoolsBuilder.newResourcePoolsBuilder .heap(10, EntryUnit.ENTRIES) .disk(10L, MemoryUnit.MB)) .withDiskStoreThreadPool("customPool", 2)) // optional per-cache override .build(true); XML example: 10 10 By default, disk stores use a dynamic thread pool shrinking/growing up to the OS CPU count. Disk writes are asynchronous, handled by threads like 'Ehcache [default]-0'. Writer concurrency controls parallel writer threads. This applies to Ehcache 3.x, including recent versions like 3.8 as referenced in discussions.

Citations:


🏁 Script executed:

find . -name "ehcache.xml" -path "*/transitclockIntegration/*"

Repository: OneBusAway/thetransitclock

Length of output: 126


🏁 Script executed:

cat -n ./transitclockIntegration/src/main/resources/ehcache.xml

Repository: OneBusAway/thetransitclock

Length of output: 5181


🏁 Script executed:

find . -name "ehcache.xml" -path "*/transitclockCore/*"

Repository: OneBusAway/thetransitclock

Length of output: 52


🏁 Script executed:

find . -name "ehcache.xml" | head -20

Repository: OneBusAway/thetransitclock

Length of output: 231


🏁 Script executed:

cat -n ./transitclock/src/main/resources/ehcache.xml

Repository: OneBusAway/thetransitclock

Length of output: 4775


🏁 Script executed:

rg -i "thread-pool\|PooledExecutionService" --type java --type xml | head -30

Repository: OneBusAway/thetransitclock

Length of output: 52


Define the referenced thread pool or remove the explicit reference.

The <ehcache:disk-store thread-pool="pool-disk" /> references a thread pool named pool-disk that is never declared in this configuration file. According to Ehcache 3 specifications, thread pools must be defined using a <thread-pools> element (e.g., <thread-pool alias="pool-disk" min-size="1" max-size="3"/>). While Ehcache may fall back to its default dynamic thread pool if none is explicitly configured, the explicit reference creates an inconsistency. Either define the pool or remove the thread-pool attribute to rely on Ehcache defaults.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@transitclockIntegration/src/main/resources/ehcache.xml` at line 16, The
ehcache configuration references a non-existent thread pool via the attribute
thread-pool="pool-disk" on the ehcache:disk-store element; either add a
<thread-pools> section that declares a thread-pool with alias="pool-disk" (e.g.,
thread-pool alias="pool-disk" with appropriate min-size/max-size) or remove the
thread-pool="pool-disk" attribute from the ehcache:disk-store element to rely on
Ehcache defaults so the configuration is consistent.

- PlaybackModule: File.deleteOnExit() only removes *empty* directories,
  so the staged GTFS copy leaked across runs. Replaced with a shutdown
  hook that recursively deletes the staging root on JVM exit.
- tools/wmata_capture/capture.py: dropped a redundant int() around
  round() — round(float) already returns int in Python 3.
- tools/wmata_capture/README.md: reworded the confusing "restore @ignore
  removal into a real code change" bullet; the intent is simply that
  un-ignoring the test is part of the refresh, not a local throwaway.

Skipped the rest: the ehcache pool-disk reference mirrors a pre-existing
pattern in transitclock/ and transitclockPipelineTests/, so changing it
only in the integration file would diverge from the "shadow of core"
design. The run-all-tests `-am` comment is incorrect — the root pom's
top-level <modules> list is always in the reactor, so core builds
alongside the profile's extras (verified earlier). And the README code
fence already matches the file's style (no fences in this README use
language specifiers).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
tools/wmata_capture/README.md (1)

53-56: Clarify the required working directory for the run commands.

uv run capture.py only works when invoked from tools/wmata_capture/. If run from repo root, it should be uv run tools/wmata_capture/capture.py.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/wmata_capture/README.md` around lines 53 - 56, The README's run example
is ambiguous about the required working directory: update the instructions to
state that `uv run capture.py` must be executed from within the
tools/wmata_capture/ directory (because .env is auto-loaded from
tools/wmata_capture/.env), and show the alternative invocation `uv run
tools/wmata_capture/capture.py` to use from the repo root; reference the script
name capture.py and the tools/wmata_capture/ directory so readers know which
path to use in each case.
tools/wmata_capture/capture.py (1)

148-150: Consider validating ZIP member paths for defense-in-depth.

Line 149 uses extractall() on remote content. While ZIP-slip is a valid vulnerability class, the risk is mitigated here by downloading from a hardcoded, official WMATA API endpoint rather than untrusted archives. If you wish to follow defense-in-depth practices, the proposed path-validation helper would harden this further—but it is not critical given the trusted source.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/wmata_capture/capture.py` around lines 148 - 150, The code currently
calls zf.extractall(gtfs_dir) on remote ZIP content; add defense-in-depth by
validating ZIP members before extraction to prevent ZIP-slip. Implement a helper
(e.g., safe_extract or is_within_directory) that iterates
ZipFile.namelist()/infolist(), rejects or normalizes entries that contain
absolute paths or path traversal components (".."), and only writes members
whose resolved destination path is inside gtfs_dir; then replace
zf.extractall(gtfs_dir) with this validated extraction routine in the same scope
where ZipFile is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/wmata_capture/capture.py`:
- Around line 468-479: The CLI allows invalid numeric values; update the
argument validation so --poll-interval must be > 0 and --duration-hours must be
>= 0: add or use a small validator function (referenced where ap.add_argument is
called for "--poll-interval" and "--duration-hours") or validate after parsing
(e.g., in main/arg parsing block that defines ap) and exit with an error if
poll-interval <= 0 or duration-hours < 0, giving a clear message indicating
which flag is invalid.
- Around line 169-174: The log call reports block_id availability using "yes" if
mapping else "no", which is wrong when the block_id column exists but has no
values; change the code to detect the presence of the block_id column from the
CSV header instead of checking mapping length. When parsing trips.csv, record a
boolean like trips_has_block_id = ('block_id' in reader.fieldnames) (or set it
where the header is examined), then use that flag in the log.info call (replace
"yes" if mapping else "no" with "yes" if trips_has_block_id else "no") while
keeping mapping and trips_csv as-is.
- Around line 260-275: The code currently uses getattr() so unset protobuf
fields are indistinguishable from default values; instead check presence with
protobuf HasField to avoid writing bogus 0,0 coords or treating missing
timestamps as real zeros: call vehicle_msg.HasField("position") before accessing
position/latitude/longitude/bearing (and only then read pos =
vehicle_msg.position), and call vehicle_msg.HasField("timestamp") (or HasField
on the timestamp field name used in the proto) before using
vehicle_msg.timestamp so you can correctly fall back to header TS when absent;
update the logic around vehicle_msg, position, latitude/longitude and timestamp
to use these presence checks (referencing vehicle_msg, position, latitude,
longitude, bearing, timestamp and HasField).

In `@tools/wmata_capture/README.md`:
- 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.

---

Nitpick comments:
In `@tools/wmata_capture/capture.py`:
- Around line 148-150: The code currently calls zf.extractall(gtfs_dir) on
remote ZIP content; add defense-in-depth by validating ZIP members before
extraction to prevent ZIP-slip. Implement a helper (e.g., safe_extract or
is_within_directory) that iterates ZipFile.namelist()/infolist(), rejects or
normalizes entries that contain absolute paths or path traversal components
(".."), and only writes members whose resolved destination path is inside
gtfs_dir; then replace zf.extractall(gtfs_dir) with this validated extraction
routine in the same scope where ZipFile is used.

In `@tools/wmata_capture/README.md`:
- Around line 53-56: The README's run example is ambiguous about the required
working directory: update the instructions to state that `uv run capture.py`
must be executed from within the tools/wmata_capture/ directory (because .env is
auto-loaded from tools/wmata_capture/.env), and show the alternative invocation
`uv run tools/wmata_capture/capture.py` to use from the repo root; reference the
script name capture.py and the tools/wmata_capture/ directory so readers know
which path to use in each case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a575848-eb95-40f6-8317-674c628476a6

📥 Commits

Reviewing files that changed from the base of the PR and between 18ba5e5 and 6ff1424.

📒 Files selected for processing (5)
  • .gitignore
  • tools/wmata_capture/.env.example
  • tools/wmata_capture/README.md
  • tools/wmata_capture/capture.py
  • transitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java
✅ Files skipped from review due to trivial changes (2)
  • .gitignore
  • tools/wmata_capture/.env.example
🚧 Files skipped from review as they are similar to previous changes (1)
  • transitclockIntegration/src/main/java/org/transitclock/playback/PlaybackModule.java

Comment on lines +169 to +174
log.info(
"Loaded %d trip→block mappings from %s (trips.txt had block_id: %s)",
len(mapping),
trips_csv,
"yes" if mapping else "no",
)

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

Fix the block_id availability log signal.

Line 173 uses mapping size to report whether trips.txt had a block_id column, which is misleading when the column exists but values are empty.

📝 Proposed fix
     log.info(
         "Loaded %d trip→block mappings from %s (trips.txt had block_id: %s)",
         len(mapping),
         trips_csv,
-        "yes" if mapping else "no",
+        "yes" if has_block else "no",
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/wmata_capture/capture.py` around lines 169 - 174, The log call reports
block_id availability using "yes" if mapping else "no", which is wrong when the
block_id column exists but has no values; change the code to detect the presence
of the block_id column from the CSV header instead of checking mapping length.
When parsing trips.csv, record a boolean like trips_has_block_id = ('block_id'
in reader.fieldnames) (or set it where the header is examined), then use that
flag in the log.info call (replace "yes" if mapping else "no" with "yes" if
trips_has_block_id else "no") while keeping mapping and trips_csv as-is.

Comment on lines +260 to +275
pos = getattr(vehicle_msg, "position", None)
if pos is None:
return
lat = getattr(pos, "latitude", None)
lon = getattr(pos, "longitude", None)
if lat is None or lon is None:
return
heading = round(getattr(pos, "bearing", 0.0) or 0.0)

ts = int(getattr(vehicle_msg, "timestamp", 0) or 0)
if ts <= 0:
# No vehicle-reported timestamp — fall back to the feed header's ts.
# Caller doesn't have access here, so skip; header ts is used only
# as a last resort in the main loop.
return

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure
find . -name "capture.py" -path "*/wmata_capture/*" | head -5

Repository: OneBusAway/thetransitclock

Length of output: 102


🏁 Script executed:

# Read the actual file content around the specified lines
cat -n tools/wmata_capture/capture.py | sed -n '250,285p'

Repository: OneBusAway/thetransitclock

Length of output: 1507


🏁 Script executed:

# Check the imports and message type definitions
cat -n tools/wmata_capture/capture.py | head -50

Repository: OneBusAway/thetransitclock

Length of output: 1794


🏁 Script executed:

# Look for vehicle_msg declaration and type information
cat -n tools/wmata_capture/capture.py | grep -B5 -A5 "vehicle_msg"

Repository: OneBusAway/thetransitclock

Length of output: 1813


🏁 Script executed:

# Look for protobuf imports and message type definitions
cat -n tools/wmata_capture/capture.py | grep -E "(import|from|Message|vehicle_msg)" | head -30

Repository: OneBusAway/thetransitclock

Length of output: 1317


🏁 Script executed:

# Find the function that contains the reviewed code
cat -n tools/wmata_capture/capture.py | sed -n '235,260p'

Repository: OneBusAway/thetransitclock

Length of output: 1071


🌐 Web query:

gtfs_realtime_pb2 VehiclePosition Position latitude longitude HasField proto3

💡 Result:

In GTFS Realtime, gtfs_realtime_pb2.VehiclePosition does not directly contain latitude/longitude fields. Latitude and longitude are fields of the nested message VehiclePosition.position (type Position). Proto fields (from gtfs-realtime.proto): - message VehiclePosition { optional Position position = 2; } - message Position { - required float latitude = 1; - required float longitude = 2; } Therefore: - VehiclePosition HasField('position') can tell you whether the nested Position message is present (in Python protobuf APIs, HasField works for sub-messages). - Once position is present, you can access position.latitude and position.longitude. - There is no HasField('latitude') on VehiclePosition; latitude is not a direct field of VehiclePosition. Grounding example from the official schema and language-binding docs: - GTFS Realtime VehiclePosition has optional Position position=2. - Position defines required latitude and required longitude. - Google’s Python binding docs show using entity.HasField('trip_update') / sub-message presence checks.

Citations:


🌐 Web query:

google.transit.gtfs_realtime_pb2 Position message optional fields

💡 Result:

The Position message in google.transit.gtfs_realtime_pb2 (from GTFS Realtime protobuf definition) has two required fields and four optional fields: Required fields: - latitude (float): Degrees North, in the WGS-84 coordinate system. - longitude (float): Degrees East, in the WGS-84 coordinate system. Optional fields: - bearing (float): Bearing, in degrees, clockwise from True North (0 is North, 90 is East). Can be compass bearing or direction to next stop. - odometer (double): Odometer value, in meters. - speed (float): Momentary speed measured by the vehicle, in meters per second. - Extensions 1000-1999 and 9000-9999: For 3rd-party extensions and private use. This structure is consistent across official sources including the GTFS Realtime .proto file.

Citations:


🌐 Web query:

proto3 required fields HasField() Python protobuf

💡 Result:

In proto3, there are no "required" fields like in proto2. All fields are optional by default, and scalar fields (int, bool, string, etc.) without the optional keyword do not track field presence—HasField returns ValueError for them, and they always appear to have their default value (e.g., 0 for int, false for bool, empty string). To enable field presence tracking (and use HasField) for scalar fields in proto3, mark them as optional (supported since protobuf 3.15+): syntax = "proto3"; message Foo { optional int32 bar = 1; // Now HasField('bar') works bool baz = 2; // No presence tracking; HasField('baz') raises ValueError } In Python generated code: foo = Foo print(foo.HasField('bar')) # False (unset) foo.bar = 0 print(foo.HasField('bar')) # True (explicitly set, even to default value) print(foo.HasField('baz')) # ValueError: Can't test field "Foo.baz" for presence in proto3 # For message fields, HasField always works (implicit presence) # For oneof fields, use WhichOneof HasField(field_name) checks if the field was explicitly set or parsed from wire (returns True) vs. unset (False, returns default). Use ClearField('bar') to unset. Singular message fields always have presence (HasField works). Repeated fields and maps do not. Deprecated fields ([deprecated=true]) are still present in generated code; use ListFields or descriptor to check schema presence, not HasField (which checks value presence).

Citations:


🌐 Web query:

python protobuf HasField required fields behavior

💡 Result:

In Python protobuf, the HasField(field_name) method checks if a field has been explicitly set (presence tracking). Behavior differs by proto syntax and field type: Proto2: - Works for all singular fields (scalars, messages). - Returns True if field was set to any value, including default (e.g., set int32 to 0 returns True). - Required fields must be set explicitly; defaults don't count as set for serialization. Proto3 (standard singular scalar fields): - Only works reliably for singular message fields (non-message scalars lack presence tracking). - For scalars (int, string, etc.), HasField may raise ValueError or not indicate presence; use value != default (e.g., int != 0) instead, but can't distinguish explicit default from unset. Proto3 optional fields (syntax="proto3"; optional field = 1; since ~protoc 3.12+): - Enables explicit presence tracking for scalars like proto2. - HasField('field') returns True if set (even to default), False if unset. - Use ClearField('field') to unset. Examples: # Proto3 optional int32 foo = 1; m = Msg m.HasField('foo') # False (unset) m.foo = 0 # Set to default m.HasField('foo') # True m.ClearField('foo') m.HasField('foo') # False ListFields lists only present fields (HasField True). No "required" fields in proto3; use business logic validation. As of 2026, no major changes to this behavior per recent protobuf releases.

Citations:


🌐 Web query:

gtfs-realtime.proto syntax proto2 or proto3 version

💡 Result:

The GTFS Realtime protocol buffer definition (gtfs-realtime.proto) uses proto2 syntax (it explicitly declares syntax = "proto2"). So you should compile it as proto2; there isn’t a proto3 version in the official schema itself. (Some community projects provide proto3-converted copies, but the official gtfs-realtime.proto is proto2.)

Citations:


🏁 Script executed:

# Check the actual protobuf message structure from gtfs-realtime-bindings
python3 << 'EOF'
try:
    from google.transit import gtfs_realtime_pb2
    
    # Create instances to check message structure
    vehicle = gtfs_realtime_pb2.VehiclePosition()
    position = gtfs_realtime_pb2.Position()
    
    # Check descriptor for Position message
    print("Position fields:")
    for field in position.DESCRIPTOR.fields:
        print(f"  {field.name}: label={field.label} (1=optional, 2=required, 3=repeated), type={field.type}")
    
    print("\nVehiclePosition fields:")
    for field in vehicle.DESCRIPTOR.fields:
        print(f"  {field.name}: label={field.label} (1=optional, 2=required, 3=repeated), type={field.type}")
    
    # Test HasField behavior on unset Position
    print("\nTesting getattr on unset position:")
    print(f"  getattr(vehicle, 'position', None) = {getattr(vehicle, 'position', None)}")
    print(f"  vehicle.position = {vehicle.position}")
    
    # Test HasField on position
    try:
        print(f"  vehicle.HasField('position') = {vehicle.HasField('position')}")
    except ValueError as e:
        print(f"  vehicle.HasField('position') raises: {e}")
    
    # Test on actual position object
    print("\nTesting on Position object:")
    try:
        print(f"  position.HasField('latitude') = {position.HasField('latitude')}")
    except ValueError as e:
        print(f"  position.HasField('latitude') raises: {e}")
    
    print(f"  getattr(position, 'latitude', None) = {getattr(position, 'latitude', None)}")
    print(f"  position.latitude = {position.latitude}")
    
except Exception as e:
    print(f"Error: {e}")
    import traceback
    traceback.print_exc()
EOF

Repository: OneBusAway/thetransitclock

Length of output: 253


🏁 Script executed:

# Install gtfs-realtime-bindings to inspect the actual message structure
pip install gtfs-realtime-bindings -q && python3 << 'EOF'
try:
    from google.transit import gtfs_realtime_pb2
    
    # Create instances to check message structure
    vehicle = gtfs_realtime_pb2.VehiclePosition()
    position = gtfs_realtime_pb2.Position()
    
    # Check descriptor for Position message
    print("Position fields:")
    for field in position.DESCRIPTOR.fields:
        label_names = {1: "optional", 2: "required", 3: "repeated"}
        print(f"  {field.name}: label={label_names.get(field.label, field.label)}")
    
    print("\nVehiclePosition fields:")
    for field in vehicle.DESCRIPTOR.fields:
        label_names = {1: "optional", 2: "required", 3: "repeated"}
        print(f"  {field.name}: label={label_names.get(field.label, field.label)}")
    
    # Test HasField behavior on unset Position
    print("\nTesting getattr on unset position:")
    print(f"  getattr(vehicle, 'position', None) = {getattr(vehicle, 'position', None)}")
    print(f"  vehicle.position = {vehicle.position}")
    print(f"  type(vehicle.position) = {type(vehicle.position)}")
    
    # Test HasField on position
    try:
        result = vehicle.HasField('position')
        print(f"  vehicle.HasField('position') = {result}")
    except ValueError as e:
        print(f"  vehicle.HasField('position') raises: {e}")
    
    # Test on actual position object
    print("\nTesting on Position object (unset fields):")
    try:
        result = position.HasField('latitude')
        print(f"  position.HasField('latitude') = {result}")
    except ValueError as e:
        print(f"  position.HasField('latitude') raises: {e}")
    
    print(f"  getattr(position, 'latitude', None) = {getattr(position, 'latitude', None)}")
    print(f"  position.latitude = {position.latitude}")
    print(f"  position.latitude == 0.0: {position.latitude == 0.0}")
    
    # Test bearing (optional field)
    print("\nTesting bearing (optional field):")
    try:
        result = position.HasField('bearing')
        print(f"  position.HasField('bearing') = {result}")
    except ValueError as e:
        print(f"  position.HasField('bearing') raises: {e}")
    print(f"  position.bearing = {position.bearing}")
    
except Exception as e:
    print(f"Error: {e}")
    import traceback
    traceback.print_exc()
EOF

Repository: OneBusAway/thetransitclock

Length of output: 1083


🏁 Script executed:

# Let's verify the exact behavior of the current code logic
python3 << 'EOF'
from google.transit import gtfs_realtime_pb2

# Simulate the current code's logic
vehicle_msg = gtfs_realtime_pb2.VehiclePosition()

print("=== Current code logic (using getattr) ===")
pos = getattr(vehicle_msg, "position", None)
print(f"pos = getattr(vehicle_msg, 'position', None)")
print(f"  pos = {repr(pos)}")
print(f"  pos is None: {pos is None}")
print(f"  type(pos): {type(pos)}")

if pos is None:
    print("  Would return early (check fails - pos is not None!)")
else:
    print("  Continues past first check")
    lat = getattr(pos, "latitude", None)
    lon = getattr(pos, "longitude", None)
    print(f"  lat = getattr(pos, 'latitude', None) = {lat}")
    print(f"  lon = getattr(pos, 'longitude', None) = {lon}")
    print(f"  lat is None or lon is None: {lat is None or lon is None}")
    if lat is None or lon is None:
        print("  Would return early (check fails - lat and lon are 0.0, not None!)")
    else:
        print("  Would continue with bogus 0,0 coordinates!")

print("\n=== Proposed fix logic (using HasField) ===")
if not vehicle_msg.HasField("position"):
    print("  Would return early (HasField correctly detects unset position)")
else:
    print("  Has position, continue...")
    pos = vehicle_msg.position
    if not pos.HasField("latitude") or not pos.HasField("longitude"):
        print("  Would return early (HasField correctly detects unset lat/lon)")
    else:
        print("  Both lat and lon are set, safe to use")

# Now test with an actual set position
print("\n=== Testing with actual position set ===")
vehicle_msg2 = gtfs_realtime_pb2.VehiclePosition()
vehicle_msg2.position.latitude = 38.95778
vehicle_msg2.position.longitude = -77.03645

print(f"vehicle_msg2.HasField('position'): {vehicle_msg2.HasField('position')}")
pos2 = vehicle_msg2.position
print(f"pos2.HasField('latitude'): {pos2.HasField('latitude')}")
print(f"pos2.HasField('longitude'): {pos2.HasField('longitude')}")
print(f"pos2.latitude = {pos2.latitude}, pos2.longitude = {pos2.longitude}")
EOF

Repository: OneBusAway/thetransitclock

Length of output: 766


Use protobuf presence checks to distinguish unset fields from defaults.

The current code using getattr() cannot distinguish between an unset position (or timestamp) and one with default values. For an unset position, getattr(vehicle_msg, "position", None) returns an empty Position object (not None), and accessing its fields like latitude returns 0.0 (not None). This causes the code to write bogus 0,0 coordinates. Similarly, an unset timestamp field returns 0, which is indistinguishable from an explicitly set timestamp of 0. Use HasField() instead to check if these fields were actually present in the feed.

Proposed fix
-    pos = getattr(vehicle_msg, "position", None)
-    if pos is None:
+    if not vehicle_msg.HasField("position"):
         return
+    pos = vehicle_msg.position
-    lat = getattr(pos, "latitude", None)
-    lon = getattr(pos, "longitude", None)
-    if lat is None or lon is None:
+    if not pos.HasField("latitude") or not pos.HasField("longitude"):
         return
+    lat = pos.latitude
+    lon = pos.longitude
     heading = round(getattr(pos, "bearing", 0.0) or 0.0)
 
-    ts = int(getattr(vehicle_msg, "timestamp", 0) or 0)
-    if ts <= 0:
+    if not vehicle_msg.HasField("timestamp"):
+        return
+    ts = int(vehicle_msg.timestamp)
+    if ts <= 0:
         # No vehicle-reported timestamp — fall back to the feed header's ts.
         # Caller doesn't have access here, so skip; header ts is used only
         # as a last resort in the main loop.
         return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/wmata_capture/capture.py` around lines 260 - 275, The code currently
uses getattr() so unset protobuf fields are indistinguishable from default
values; instead check presence with protobuf HasField to avoid writing bogus 0,0
coords or treating missing timestamps as real zeros: call
vehicle_msg.HasField("position") before accessing
position/latitude/longitude/bearing (and only then read pos =
vehicle_msg.position), and call vehicle_msg.HasField("timestamp") (or HasField
on the timestamp field name used in the proto) before using
vehicle_msg.timestamp so you can correctly fall back to header TS when absent;
update the logic around vehicle_msg, position, latitude/longitude and timestamp
to use these presence checks (referencing vehicle_msg, position, latitude,
longitude, bearing, timestamp and HasField).

Comment on lines +468 to +479
ap.add_argument(
"--duration-hours",
type=float,
default=0.0,
help="Stop after this many hours. 0 or omitted = run until SIGINT.",
)
ap.add_argument(
"--poll-interval",
type=float,
default=30.0,
help="Seconds between GTFS-RT polls (default 30).",
)

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 | 🟠 Major

Validate numeric CLI bounds for polling behavior.

--poll-interval currently accepts <= 0 and --duration-hours accepts negative values. That can cause tight retry loops or surprising runtime behavior.

🛠️ Proposed fix
+def _positive_float(value: str) -> float:
+    v = float(value)
+    if v <= 0:
+        raise argparse.ArgumentTypeError("must be > 0")
+    return v
+
+
+def _non_negative_float(value: str) -> float:
+    v = float(value)
+    if v < 0:
+        raise argparse.ArgumentTypeError("must be >= 0")
+    return v
+
+
 def parse_args(argv: list[str]) -> argparse.Namespace:
@@
     ap.add_argument(
         "--duration-hours",
-        type=float,
+        type=_non_negative_float,
         default=0.0,
         help="Stop after this many hours. 0 or omitted = run until SIGINT.",
     )
@@
     ap.add_argument(
         "--poll-interval",
-        type=float,
+        type=_positive_float,
         default=30.0,
         help="Seconds between GTFS-RT polls (default 30).",
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/wmata_capture/capture.py` around lines 468 - 479, The CLI allows
invalid numeric values; update the argument validation so --poll-interval must
be > 0 and --duration-hours must be >= 0: add or use a small validator function
(referenced where ap.add_argument is called for "--poll-interval" and
"--duration-hours") or validate after parsing (e.g., in main/arg parsing block
that defines ap) and exit with an error if poll-interval <= 0 or duration-hours
< 0, giving a clear message indicating which flag is invalid.

`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.

@aaronbrethorst
aaronbrethorst merged commit b579ed7 into develop Apr 24, 2026
3 checks passed
@aaronbrethorst
aaronbrethorst deleted the integration-tests branch April 24, 2026 01:38
@coderabbitai coderabbitai Bot mentioned this pull request Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant