Skip to content

Commit 08f6570

Browse files
Merge pull request #6 from OneBusAway/di-seam
more test coverage
2 parents 11df7bd + 0ce17ad commit 08f6570

5 files changed

Lines changed: 421 additions & 41 deletions

File tree

README.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
core [![Build Status](https://travis-ci.org/TheTransitClock/transitime.svg?branch=develop)](https://travis-ci.org/TheTransitClock/transitime)
1+
TheTransitClock
22
====
33

4-
The complete core Java software for the Transitime real-time transit information project. The goal is to use any type of real-time GPS data to generate useful public transportation information. The system is for both letting passengers know the status of their vehicles and helping agencies more effectively manage their systems. By providing a complete open-source system, agencies can have a cost effective system and have full ownership of it.
4+
This is a fork of TheTransitClock, a [GTFS-RT Trip Updates](https://gtfs.org/documentation/realtime/feed-entities/trip-updates/) generation engine used by public transit agencies around the world, including in Minneapolis, MN where [the software was found to outperform proprietary alternatives](http://berrebi.net/wp-content/uploads/2020/01/trbws07_FileUploads_2020-AM-Presentations_3470_pdf_13557_P20-20421_2020-01-14-09-24-54.pdf).
55

6-
<b>Build</b>
6+
[TheTransitClock](https://thetransitclock.github.io) was developed by Sean Óg Crudden and Simon J. Berrebi, Ph.D., itself a fork of [Swiftly Transitime](https://transitime.github.io/core/).
77

8-
The software is made up of three modules which can each be built with maven. See BUILD.md
8+
## About this Repo
9+
10+
The complete core Java software for the Transitime real-time transit information project. The purpose of the software is to use any type of real-time GPS data to generate useful public transportation information, namely a GTFS-RT Trip Updates feed.
11+
12+
The system is for both letting passengers know the status of their vehicles and helping agencies more effectively manage their systems. By providing a complete open-source system, agencies can have a cost effective system and have full ownership of it.
13+
14+
## Build
15+
16+
The software is made up of three modules which can each be built with maven. See [BUILD.md](./BUILD.md).
917

1018
The core functionality is in the transitime project. The REST api is in transitimeApi and the user Web applicaton is in transitimeWebapp.
1119

12-
<b>Setup</b>
20+
## Setup
1321

1422
The main module is transitTime. This has several standalone programs in the org.transitime.applications package.
1523

@@ -29,12 +37,14 @@ The transitimeWebapp in turn is a web application which uses the transitTimeAPI
2937

3038
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.
3139

32-
<b>Running tests</b>
40+
## Running tests
3341

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

46+
`mvn test` on the full reactor fails because `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 a single module with `-pl`, or skip QuickStart with `mvn test -pl '!transitclockQuickStart'`.
47+
3848
Two additional test suites are opt-in via Maven profiles and excluded from the default build:
3949

4050
- **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:
@@ -47,13 +57,10 @@ Two additional test suites are opt-in via Maven profiles and excluded from the d
4757
mvn install -P include-integration-tests
4858
```
4959

50-
<b>Code coverage</b>
60+
### Code coverage
5161

5262
JaCoCo generates coverage reports as part of the Maven `verify` phase.
5363

5464
- Per-module HTML reports: `<module>/target/site/jacoco/index.html`
5565
- Aggregate report across Core + thin clients: `coverage-report/target/site/jacoco-aggregate/index.html`
5666
- Regenerate just the aggregate (fastest): `mvn verify -pl coverage-report -am`
57-
58-
[![Build Status](https://zenodo.org/badge/DOI/10.5281/zenodo.3550975.svg)](https://zenodo.org/record/3550975#.XdgmVedKjOQ)
59-

transitclock/src/main/java/org/transitclock/core/holdingmethod/HoldingTimeGeneratorDefaultImpl.java

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,19 @@ public class HoldingTimeGeneratorDefaultImpl implements HoldingTimeGenerator {
6767
protected static IntegerConfigValue plannedHeadwayMsec = new IntegerConfigValue("transitclock.holding.plannedHeadwayMsec", 60*1000*9, "Planned Headway");
6868
protected static StringListConfigValue controlStopList = new StringListConfigValue("transitclock.holding.controlStops", null, "This is a list of stops to generate holding times for.");
6969

70+
public HoldingTimeGeneratorDefaultImpl() {
71+
// Operator opted into this generator via transitclock.core.holdingTimeGeneratorClass
72+
// but left controlStops unset; the feature will produce no holding times until
73+
// transitclock.holding.controlStops is configured. Surfaced once here because the
74+
// per-event callers (isControlStop, handleDeparture) would flood the logs.
75+
if (controlStopList.getValue() == null) {
76+
logger.warn(
77+
"HoldingTimeGeneratorDefaultImpl is active but transitclock.holding.controlStops is unset — "
78+
+ "no holding times will be produced. Configure a stop-id list, or clear "
79+
+ "transitclock.core.holdingTimeGeneratorClass to silence this warning.");
80+
}
81+
}
82+
7083
public HoldingTime generateHoldingTime(VehicleState vehicleState, IpcArrivalDeparture event) {
7184

7285
PredictionDataCache predictionCache = PredictionDataCache.getInstance();
@@ -659,7 +672,16 @@ public List<ControlStop> getControlPointStops() {
659672

660673
ArrayList<ControlStop> controlStops=new ArrayList<ControlStop>();
661674

662-
for(String stopEntry: controlStopList.getValue())
675+
// Treat an unset controlStops list as "no control stops configured" — the
676+
// feature is a no-op in that case. Without this guard, every caller of
677+
// getControlPointStops (isControlStop, handleDeparture, generateHoldingTime)
678+
// would NPE iterating a null list.
679+
List<String> configured = controlStopList.getValue();
680+
if (configured == null) {
681+
return controlStops;
682+
}
683+
684+
for(String stopEntry: configured)
663685
{
664686
controlStops.add(new ControlStop(stopEntry));
665687
}
@@ -669,16 +691,16 @@ public List<ControlStop> getControlPointStops() {
669691

670692
private Long[] predictionsToLongArray(List<IpcPrediction> predictions)
671693
{
694+
if(predictions==null)
695+
{
696+
return new Long[0];
697+
}
672698
Long[] list=new Long[predictions.size()];
673-
674-
if(predictions!=null)
699+
int i=0;
700+
for(IpcPrediction prediction:predictions)
675701
{
676-
int i=0;
677-
for(IpcPrediction prediction:predictions)
678-
{
679-
list[i]=new Long(prediction.getPredictionTime());
680-
i++;
681-
}
702+
list[i]=new Long(prediction.getPredictionTime());
703+
i++;
682704
}
683705
return list;
684706
}
@@ -698,14 +720,11 @@ ArrayList<Long> predictionsToLongArrayList(List<IpcPrediction> predictions, Arra
698720
private boolean isControlStop(String stopId)
699721
{
700722
ControlStop controlStop=new ControlStop( null, stopId);
701-
if(getControlPointStops()!=null)
723+
for(ControlStop controlStopInList:getControlPointStops())
702724
{
703-
for(ControlStop controlStopInList:getControlPointStops())
725+
if(controlStopInList.getStopId().equals(controlStop.getStopId()))
704726
{
705-
if(controlStopInList.getStopId().equals(controlStop.getStopId()))
706-
{
707-
return true;
708-
}
727+
return true;
709728
}
710729
}
711730
return false;

transitclockPipelineTests/src/test/java/org/transitclock/pipelinetests/AvlProcessorBehaviorTest.java

Lines changed: 128 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,6 @@ private long advanceClockBy(long deltaMs) {
115115
return next;
116116
}
117117

118-
/**
119-
* Jumps the harness clock to a specific epoch (for tests that need a
120-
* real-world date, e.g. a known active service day). Updates
121-
* {@link #nextTime} to the larger of its current value and the jump
122-
* target, so subsequent tests' {@code @Before} advances never rewind
123-
* the clock.
124-
*/
125-
private void jumpClockTo(long epochMs) {
126-
CORE.setNow(epochMs);
127-
nextTime.updateAndGet(cur -> Math.max(cur, epochMs));
128-
}
129-
130118
private static AvlReport avlReport(String vehicleId, double lat, double lon) {
131119
// Use CORE.clock() so the report's time advances with setNow(). Tests
132120
// that want a time offset from "now" can construct their own Date.
@@ -140,6 +128,30 @@ private static AvlReport avlReport(String vehicleId, double lat, double lon) {
140128
return report;
141129
}
142130

131+
/**
132+
* Pins Core's clock to an epoch within the known-good SE-08 service window
133+
* that is strictly greater than any timestamp prior tests already used.
134+
* Multiple happy-path tests in the same JVM would otherwise all pin the
135+
* same {@code HAPPY_PATH_EPOCH_MS}; the second test's AVL report would fail
136+
* {@code setLastAvlReport}'s "only store newer" guard and be silently
137+
* dropped.
138+
*/
139+
private long pinClockToHappyPathAnchor() {
140+
long anchor = nextTime.updateAndGet(
141+
cur -> Math.max(cur, HAPPY_PATH_EPOCH_MS) + 100L);
142+
CORE.setNow(anchor);
143+
return anchor;
144+
}
145+
146+
private static AvlReport reportAtFirstStop(String vehicleId, long epochMs,
147+
String assignmentId, AssignmentType assignmentType) {
148+
AvlReport report = new AvlReport(vehicleId, epochMs,
149+
HAPPY_PATH_LAT, HAPPY_PATH_LON,
150+
Float.NaN, Float.NaN, "test");
151+
report.setAssignment(assignmentId, assignmentType);
152+
return report;
153+
}
154+
143155
// ---------- Tests ----------
144156

145157
@Test
@@ -233,9 +245,9 @@ public void reportAtFirstStopOfActiveBlockProducesPredictableVehicle() {
233245
// it starts failing, the matching pipeline has regressed and the
234246
// rest of the suite — all of which asserts !isPredictable — would
235247
// silently approve broken code.
236-
jumpClockTo(HAPPY_PATH_EPOCH_MS);
237-
AvlReport report = avlReport("v-happy", HAPPY_PATH_LAT, HAPPY_PATH_LON);
238-
report.setAssignment(HAPPY_PATH_BLOCK_ID, AssignmentType.BLOCK_ID);
248+
long when = pinClockToHappyPathAnchor();
249+
AvlReport report = reportAtFirstStop("v-happy", when,
250+
HAPPY_PATH_BLOCK_ID, AssignmentType.BLOCK_ID);
239251

240252
AvlProcessor.getInstance().processAvlReport(report);
241253

@@ -249,4 +261,105 @@ public void reportAtFirstStopOfActiveBlockProducesPredictableVehicle() {
249261
.isNotNull();
250262
assertThat(state.getAssignmentId()).isEqualTo(HAPPY_PATH_BLOCK_ID);
251263
}
264+
265+
@Test
266+
public void reportWithTripIdAssignmentProducesPredictableVehicle() {
267+
long when = pinClockToHappyPathAnchor();
268+
AvlReport report = reportAtFirstStop("v-happy-tid", when,
269+
"868588900", AssignmentType.TRIP_ID);
270+
271+
AvlProcessor.getInstance().processAvlReport(report);
272+
273+
VehicleState state = VehicleStateManager.getInstance().getVehicleState("v-happy-tid");
274+
assertThat(state).isNotNull();
275+
assertThat(state.isPredictable())
276+
.as("vehicle with TRIP_ID assignment on an active block should be predictable")
277+
.isTrue();
278+
assertThat(state.getMatch())
279+
.as("TRIP_ID-predictable vehicle must have a concrete TemporalMatch")
280+
.isNotNull();
281+
// The assignment-id on the state carries through as the block id
282+
// once BlockAssigner resolves the trip, not the raw TRIP_ID.
283+
assertThat(state.getAssignmentId()).isEqualTo(HAPPY_PATH_BLOCK_ID);
284+
}
285+
286+
@Test
287+
public void cacheAvlReportWithoutProcessingUpdatesStateButDoesNotMatch() {
288+
long when = advanceClockBy(1_000L);
289+
AvlReport report = avlReport("v-cache-only", NEAR_ROUTE_LAT, NEAR_ROUTE_LON);
290+
291+
AvlProcessor.getInstance().cacheAvlReportWithoutProcessing(report);
292+
293+
VehicleState state = VehicleStateManager.getInstance().getVehicleState("v-cache-only");
294+
assertThat(state).isNotNull();
295+
assertThat(state.getAvlReport())
296+
.as("cached report should be attached to the vehicle state")
297+
.isNotNull();
298+
assertThat(state.getAvlReport().getVehicleId()).isEqualTo("v-cache-only");
299+
assertThat(state.getAvlReport().getTime()).isEqualTo(when);
300+
assertThat(state.isPredictable())
301+
.as("cacheAvlReportWithoutProcessing must never attempt matching")
302+
.isFalse();
303+
assertThat(state.getMatch())
304+
.as("no match should be produced when skipping processing")
305+
.isNull();
306+
}
307+
308+
@Test
309+
public void makeVehicleUnpredictableClearsMatchOnPredictableVehicle() {
310+
long when = pinClockToHappyPathAnchor();
311+
AvlReport report = reportAtFirstStop("v-unwind", when,
312+
HAPPY_PATH_BLOCK_ID, AssignmentType.BLOCK_ID);
313+
AvlProcessor.getInstance().processAvlReport(report);
314+
VehicleState beforeUnwind =
315+
VehicleStateManager.getInstance().getVehicleState("v-unwind");
316+
assertThat(beforeUnwind.isPredictable())
317+
.as("setup assumption: happy path must produce a predictable vehicle")
318+
.isTrue();
319+
320+
AvlProcessor.getInstance().makeVehicleUnpredictable(
321+
"v-unwind",
322+
"behavior-test-triggered unwind",
323+
org.transitclock.db.structs.VehicleEvent.ASSIGNMENT_CHANGED);
324+
325+
VehicleState afterUnwind =
326+
VehicleStateManager.getInstance().getVehicleState("v-unwind");
327+
assertThat(afterUnwind)
328+
.as("VehicleStateManager should still yield the same state object")
329+
.isSameAs(beforeUnwind);
330+
assertThat(afterUnwind.isPredictable())
331+
.as("vehicle must be unpredictable after makeVehicleUnpredictable")
332+
.isFalse();
333+
assertThat(afterUnwind.getMatch())
334+
.as("match must be cleared after makeVehicleUnpredictable")
335+
.isNull();
336+
}
337+
338+
// Schedule-based-predictions AVL reports are synthetic — they exist
339+
// to produce predictions for runs with no real vehicle assigned. They
340+
// must NOT update lastRegularReportProcessed, because that timestamp
341+
// drives the "AVL feed is up" monitoring check.
342+
@Test
343+
public void schedBasedPredsReportDoesNotUpdateLastRegularReport() {
344+
long baselineEpoch = advanceClockBy(1_000L);
345+
AvlReport baseline = avlReport("v-regular", NEAR_ROUTE_LAT, NEAR_ROUTE_LON);
346+
AvlProcessor.getInstance().processAvlReport(baseline);
347+
long baselineLast = AvlProcessor.getInstance().lastAvlReportTime();
348+
assertThat(baselineLast)
349+
.as("regular report should update lastAvlReportTime")
350+
.isEqualTo(baselineEpoch);
351+
352+
long schedEpoch = advanceClockBy(30_000L);
353+
AvlReport schedBased = new AvlReport("v-schedbased",
354+
schedEpoch,
355+
NEAR_ROUTE_LAT, NEAR_ROUTE_LON,
356+
Float.NaN, Float.NaN, "test");
357+
schedBased.setAssignment(HAPPY_PATH_BLOCK_ID,
358+
AssignmentType.BLOCK_FOR_SCHED_BASED_PREDS);
359+
AvlProcessor.getInstance().processAvlReport(schedBased);
360+
361+
assertThat(AvlProcessor.getInstance().lastAvlReportTime())
362+
.as("schedule-based-preds report must not advance the regular-AVL clock")
363+
.isEqualTo(baselineLast);
364+
}
252365
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package org.transitclock.pipelinetests;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.Date;
6+
7+
import org.junit.ClassRule;
8+
import org.junit.Test;
9+
import org.transitclock.core.holdingmethod.HoldingTimeGeneratorDefaultImpl;
10+
import org.transitclock.db.structs.HoldingTime;
11+
import org.transitclock.ipc.data.IpcArrivalDeparture;
12+
13+
/**
14+
* Behavior tests for {@link HoldingTimeGeneratorDefaultImpl}. The existing
15+
* unit test in {@code transitclock/src/test} only covers the private
16+
* {@code calculateHoldingTime} math helper via reflection; the public
17+
* {@code generateHoldingTime} contract had no coverage.
18+
*/
19+
public class HoldingTimeGeneratorBehaviorTest {
20+
21+
@ClassRule
22+
public static final CoreHarness CORE = CoreHarness.withWmata5A();
23+
24+
private static final String NON_CONTROL_STOP_ID = "14253";
25+
private static final String ROUTE_ID = "5A";
26+
private static final String TRIP_ID = "868588900";
27+
28+
private static IpcArrivalDeparture event(String vehicleId, String stopId,
29+
boolean isArrival, long epochMs) {
30+
IpcArrivalDeparture ad = new IpcArrivalDeparture();
31+
ad.setVehicleId(vehicleId);
32+
ad.setStopId(stopId);
33+
ad.setRouteId(ROUTE_ID);
34+
ad.setTripId(TRIP_ID);
35+
ad.setArrival(isArrival);
36+
ad.setTime(new Date(epochMs));
37+
ad.setAvlTime(new Date(epochMs));
38+
return ad;
39+
}
40+
41+
// ---------- Tests ----------
42+
43+
@Test
44+
public void generateHoldingTime_nonArrivalEventReturnsNull() {
45+
IpcArrivalDeparture departure = event(
46+
"v-ht-depart", NON_CONTROL_STOP_ID, /*isArrival*/ false,
47+
1466437800000L);
48+
49+
HoldingTime result = new HoldingTimeGeneratorDefaultImpl()
50+
.generateHoldingTime(/*vehicleState*/ null, departure);
51+
52+
assertThat(result)
53+
.as("departure events must not produce a holding time")
54+
.isNull();
55+
}
56+
57+
@Test
58+
public void generateHoldingTime_arrivalAtNonControlStopReturnsNull() {
59+
IpcArrivalDeparture arrival = event(
60+
"v-ht-arr-nonctrl", NON_CONTROL_STOP_ID, /*isArrival*/ true,
61+
1466437800000L);
62+
63+
HoldingTime result = new HoldingTimeGeneratorDefaultImpl()
64+
.generateHoldingTime(/*vehicleState*/ null, arrival);
65+
66+
assertThat(result)
67+
.as("arrivals at non-control stops must not produce a holding time")
68+
.isNull();
69+
}
70+
71+
@Test
72+
public void getControlPointStops_defaultsToEmptyList() {
73+
// Callers of getControlPointStops iterate without null-guarding, so an
74+
// unset controlStops config must produce an empty list, not null.
75+
assertThat(new HoldingTimeGeneratorDefaultImpl().getControlPointStops())
76+
.as("with no transitclock.holding.controlStops configured, "
77+
+ "getControlPointStops() must return an empty (non-null) list")
78+
.isNotNull()
79+
.isEmpty();
80+
}
81+
}

0 commit comments

Comments
 (0)