Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ jobs:
- name: Build and run unit tests
run: mvn -B -ntp verify

# Pipeline tests live in the transitclockPipelineTests module and are
# 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. Separate
# step so failures are attributed distinctly from unit-test failures.
- name: Run pipeline tests
run: mvn -B -ntp -pl transitclockPipelineTests -am -P include-pipeline-tests test

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

settings.local.json
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Built from the repository root as a Maven multi-module project.
- Run a single module's tests: `mvn -pl transitclock test`
- 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`.

`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 All @@ -33,14 +34,15 @@ JaCoCo 0.8.12 is wired at the root `pom.xml`. It inherits into any module that d

## Module layout

Seven Maven modules under the root aggregator `pom.xml`:
Eight Maven modules under the root aggregator `pom.xml`:
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

- **transitclock** — core engine. Artifact id `transitclockCore`. Contains domain model, AVL ingestion, matching, prediction generation, Hibernate entities, config, modules, IPC servers, and all executable `main` classes under `org.transitclock.applications`.
- **transitclockApi** — JAX-RS REST API WAR. Calls into a running Core process via RMI (see `org.transitclock.ipc`); does **not** talk to the DB directly for live vehicle/prediction data.
- **transitclockWebapp** — user-facing web UI WAR. Consumes the REST API; deployed to the same Tomcat instance as `transitclockApi`. Connects to the DB via `hibernate.cfg.xml` in `src/main/resources`.
- **transitclockQuickStart** — standalone `java -jar` launcher bundling Core+API+Webapp for local experimentation.
- **transitclockTraccarClient**, **transitclockBarefootClient** — thin clients for Traccar GPS devices and the Barefoot map-matching server. Depended on by `transitclock`.
- **transitclockIntegration** — end-to-end / prediction-accuracy tests; only built under the `include-integration-tests` profile.
- **transitclockPipelineTests** — real-Core behavior tests for the prediction pipeline (`AvlProcessor`, and eventually the other matcher/generator classes). Boots a real Core against in-memory HSQL with a small WMATA GTFS fixture via `CoreHarness` (a JUnit `@ClassRule`). Only built under the `include-pipeline-tests` profile.
- **coverage-report** — `packaging=pom` aggregator whose sole purpose is producing a JaCoCo aggregate report. No sources; inherits the root jacoco plugin and runs `report-aggregate` at `verify`.

## Runtime architecture
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,24 @@ The transitimeWebapp in turn is a web application which uses the transitTimeAPI

The transitimeQuickStart can be built with mvn install and ran using java -jar transitimeQuickStart it is currently a work in progress but the gui elements can be seen.

<b>Running tests</b>

- Default unit tests across all modules: `mvn verify` (not `mvn test` — see CLAUDE.md for why).
- Single module: `mvn -pl transitclock test`
- Single class: `mvn -pl transitclock test -Dtest=TestAPIKeyManager`

Two additional test suites are opt-in via Maven profiles and excluded from the default build:

- **Pipeline tests** (`transitclockPipelineTests`) — boot a real Core against an in-memory HSQL database populated with a small WMATA GTFS fixture, then exercise matcher / generator behavior end-to-end. Run with:
```
mvn -pl transitclockPipelineTests -am -P include-pipeline-tests test
```
First run takes ~30s while `transitclockCore` compiles; subsequent runs are ~3s. CI runs this on every PR as a separate step after the unit-test build.
- **Integration tests** (`transitclockIntegration`) — full AVL-CSV replay runs, heavier than pipeline tests. Run with:
```
mvn install -P include-integration-tests
```

<b>Code coverage</b>

JaCoCo generates coverage reports as part of the Maven `verify` phase.
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,11 @@
<module>coverage-report</module>
</modules>
</profile>
<profile>
<id>include-pipeline-tests</id>
<modules>
<module>transitclockPipelineTests</module>
</modules>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.transitclock.core;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

/**
* VehicleAtStopInfo is a thin Indices subclass whose entire purpose is to
* (a) pin the segmentIndex to 0 and (b) carry "at a stop" semantics for the
* vehicle-state machine. Both constructor forms and the getClass()-based
* equality with Indices are the behaviors worth locking down here.
*
* The methods that dereference the Block (toString, getStopId, atEndOfBlock)
* require a fully-wired Block+Trip+StopPath graph and are exercised via the
* integration tests. These unit tests stick to the null-block branch, mirroring
* IndicesTest.
*/
public class VehicleAtStopInfoTest {

@Test
public void blockConstructorForcesSegmentIndexToZero() {
VehicleAtStopInfo info = new VehicleAtStopInfo(null, 2, 5);
assertEquals(2, info.getTripIndex());
assertEquals(5, info.getStopPathIndex());
assertEquals(0, info.getSegmentIndex());
assertNull(info.getBlock());
}

@Test
public void indicesConstructorCopiesFieldsAndForcesSegmentIndexToZero() {
// Source Indices has a non-zero segment index. VehicleAtStopInfo
// deliberately drops that — once we know a vehicle is at a stop the
// segment offset within the stop path is no longer meaningful.
Indices source = new Indices(null, 2, 5, 7);
VehicleAtStopInfo info = new VehicleAtStopInfo(source);
assertEquals(2, info.getTripIndex());
assertEquals(5, info.getStopPathIndex());
assertEquals(0, info.getSegmentIndex());
assertNull(info.getBlock());
}

@Test
public void bothConstructorFormsProduceEquivalentInstances() {
VehicleAtStopInfo fromBlock = new VehicleAtStopInfo(null, 3, 4);
VehicleAtStopInfo fromIndices =
new VehicleAtStopInfo(new Indices(null, 3, 4, 0));
// Indices defines equals() but not hashCode(), so two equal instances
// won't have matching hash codes. Only assert logical equality here.
assertEquals(fromBlock, fromIndices);
}

@Test
public void vehicleAtStopInfoNotEqualToPlainIndicesWithSameFields() {
// Indices.equals uses getClass()-based comparison, so subclass
// instances never compare equal to base-class instances even when
// the fields all match. This keeps different kinds of indices from
// colliding in sets/maps.
VehicleAtStopInfo sub = new VehicleAtStopInfo(null, 1, 2);
Indices base = new Indices(null, 1, 2, 0);
assertFalse(sub.equals(base));
assertFalse(base.equals(sub));
}

@Test
public void twoVehicleAtStopInfosWithSameFieldsAreEqual() {
VehicleAtStopInfo a = new VehicleAtStopInfo(null, 1, 2);
VehicleAtStopInfo b = new VehicleAtStopInfo(null, 1, 2);
assertTrue(a.equals(b));
}

@Test
public void differentStopPathIndexMakesInstancesUnequal() {
VehicleAtStopInfo a = new VehicleAtStopInfo(null, 1, 2);
VehicleAtStopInfo b = new VehicleAtStopInfo(null, 1, 3);
assertFalse(a.equals(b));
}

@Test
public void differentTripIndexMakesInstancesUnequal() {
VehicleAtStopInfo a = new VehicleAtStopInfo(null, 1, 2);
VehicleAtStopInfo b = new VehicleAtStopInfo(null, 5, 2);
assertFalse(a.equals(b));
}

@Test
public void segmentIndexIgnoredInSource() {
// Even if the source Indices has a huge segment index, the derived
// VehicleAtStopInfo normalizes it to 0.
Indices source = new Indices(null, 0, 0, 999);
VehicleAtStopInfo info = new VehicleAtStopInfo(source);
assertEquals(0, info.getSegmentIndex());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package org.transitclock.db.structs;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;

import java.util.Date;

import org.junit.Test;

/**
* Arrival and Departure are tiny wrappers over ArrivalDeparture that exist
* solely to pin the isArrival flag and (for Arrival) to expose a
* withUpdatedTime copy. Their value is precisely that distinction, so these
* tests verify the isArrival/isDeparture discrimination and getter delegation.
*
* The configRev-taking constructors are used throughout so the tests don't
* depend on a running Core (the other constructor form fetches configRev via
* Core.getInstance()). Passing a null Block keeps the tests free of the
* Hibernate/Trip/StopPath graph — the base constructor short-circuits most
* derived-field computation when block is null.
*/
public class ArrivalDepartureSubclassesTest {

private static final int CONFIG_REV = 7;
private static final String VEHICLE_ID = "v1";

private static Date time(long millis) {
return new Date(millis);
}

@Test
public void arrivalHasIsArrivalTrue() {
Arrival a = new Arrival(CONFIG_REV, VEHICLE_ID, time(1_000_000),
time(999_000), null, 0, 1, null);
assertTrue(a.isArrival());
assertFalse(a.isDeparture());
}

@Test
public void departureHasIsArrivalFalse() {
Departure d = new Departure(CONFIG_REV, VEHICLE_ID, time(1_000_000),
time(999_000), null, 0, 1, null);
assertFalse(d.isArrival());
assertTrue(d.isDeparture());
}

@Test
public void arrivalExposesConstructorFields() {
Date t = time(2_000_000);
Date avl = time(1_998_000);
Arrival a = new Arrival(CONFIG_REV, VEHICLE_ID, t, avl, null, 3, 5, null);
assertEquals(VEHICLE_ID, a.getVehicleId());
assertEquals(CONFIG_REV, a.getConfigRev());
assertEquals(t, a.getDate());
assertEquals(t.getTime(), a.getTime());
assertSame(avl, a.getAvlTime());
assertEquals(3, a.getTripIndex());
assertEquals(5, a.getStopPathIndex());
assertNull(a.getFreqStartTime());
}

@Test
public void departureExposesConstructorFields() {
Date t = time(2_000_000);
Date avl = time(1_998_000);
Departure d = new Departure(CONFIG_REV, VEHICLE_ID, t, avl, null, 4, 2, null);
assertEquals(VEHICLE_ID, d.getVehicleId());
assertEquals(CONFIG_REV, d.getConfigRev());
assertEquals(t, d.getDate());
assertEquals(t.getTime(), d.getTime());
assertSame(avl, d.getAvlTime());
assertEquals(4, d.getTripIndex());
assertEquals(2, d.getStopPathIndex());
}

@Test
public void freqStartTimeRoundTrips() {
Date freqStart = time(500_000);
Arrival a = new Arrival(CONFIG_REV, VEHICLE_ID, time(1_000_000),
time(999_000), null, 0, 1, freqStart);
assertSame(freqStart, a.getFreqStartTime());

Departure d = new Departure(CONFIG_REV, VEHICLE_ID, time(1_000_000),
time(999_000), null, 0, 1, freqStart);
assertSame(freqStart, d.getFreqStartTime());
}

@Test
public void nullBlockResultsInNullBlockAndDefaultFields() {
// When block is null the base constructor takes the short-circuit
// branch: no derived fields (stopId/routeId/etc.) are populated.
// This test pins that behavior so we notice if the null-block branch
// ever starts resolving those fields differently.
Arrival a = new Arrival(CONFIG_REV, VEHICLE_ID, time(1_000_000),
time(999_000), null, 0, 1, null);
assertNull(a.getBlock());
assertEquals("", a.getStopId());
assertEquals("", a.getTripId());
assertEquals("", a.getServiceId());
assertNull(a.getScheduledDate());
assertEquals(0f, a.getStopPathLength(), 0.0f);
assertEquals(Integer.valueOf(0), a.getStopOrder());
}

@Test
public void arrivalAndDepartureAreNotEqualEvenWithSameFields() {
// ArrivalDeparture.equals() includes the isArrival flag, so an
// Arrival and a Departure built from the same fields must differ.
Date t = time(1_000_000);
Date avl = time(999_000);
Arrival a = new Arrival(CONFIG_REV, VEHICLE_ID, t, avl, null, 0, 1, null);
Departure d = new Departure(CONFIG_REV, VEHICLE_ID, t, avl, null, 0, 1, null);
assertFalse(a.equals(d));
assertFalse(d.equals(a));
}

@Test
public void twoArrivalsWithSameFieldsAreEqual() {
Date t = time(1_000_000);
Date avl = time(999_000);
Arrival a = new Arrival(CONFIG_REV, VEHICLE_ID, t, avl, null, 0, 1, null);
Arrival b = new Arrival(CONFIG_REV, VEHICLE_ID, t, avl, null, 0, 1, null);
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
}

@Test
public void toStringDistinguishesArrivalAndDeparture() {
Arrival a = new Arrival(CONFIG_REV, VEHICLE_ID, time(1_000_000),
time(999_000), null, 0, 1, null);
Departure d = new Departure(CONFIG_REV, VEHICLE_ID, time(1_000_000),
time(999_000), null, 0, 1, null);
// Base class toString starts with "Arrival " or "Departure".
assertTrue(a.toString().startsWith("Arrival"));
assertTrue(d.toString().startsWith("Departure"));
}
}
Loading
Loading