mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 06:12:18 +00:00
Test that transit heading, data age and selection stay continuous through a stop, a turn and a feed outage
This commit is contained in:
1 parent
736ee8d694
commit
b29e20632b
1 file changed
+188
@@ -0,0 +1,188 @@
|
|||||||
|
// Edge cases from docs/migration/planet-separation/layers.md (transit): a stop,
|
||||||
|
// a turn, a feed loss and its return keep the heading and the data age
|
||||||
|
// continuous, and the selection stays bound to the vehicle/trip identity.
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createState } from './state.js';
|
||||||
|
import { createIngestion } from './ingestion.js';
|
||||||
|
import { applyDisplayCourse, updatePlayback } from './movement.js';
|
||||||
|
import { TRANSIT_POLL_MS, transitVehicleKey } from './policy.js';
|
||||||
|
import { getRegisteredTransitFeed } from '../../data/transitFeeds.js';
|
||||||
|
|
||||||
|
const START_MS = 1_789_000_000_000;
|
||||||
|
const LAT = 42.3601;
|
||||||
|
const LON = -71.0589;
|
||||||
|
const M_PER_DEG = 111_320;
|
||||||
|
const LON_M_PER_DEG = M_PER_DEG * Math.cos((LAT * Math.PI) / 180);
|
||||||
|
|
||||||
|
/** Real state and ingestion; the scene parts are inert doubles. */
|
||||||
|
function harness(t) {
|
||||||
|
t.mock.timers.enable({ apis: ['Date'], now: START_MS });
|
||||||
|
t.mock.method(performance, 'now', () => Date.now() - START_MS);
|
||||||
|
t.mock.method(console, 'warn', () => {});
|
||||||
|
const services = {
|
||||||
|
overlays: {
|
||||||
|
setOverlayEntries() {},
|
||||||
|
setOverlaySourceVisible() {},
|
||||||
|
clearOverlaySource() {},
|
||||||
|
},
|
||||||
|
render: { governorRequestRender() {} },
|
||||||
|
sprites: {},
|
||||||
|
};
|
||||||
|
const state = createState({ services });
|
||||||
|
const parts = {};
|
||||||
|
let answer = null;
|
||||||
|
const source = {
|
||||||
|
requestSnapshot: async () => answer(),
|
||||||
|
getHistory: async () => ({ vehicles: [] }),
|
||||||
|
};
|
||||||
|
parts.rendering = {
|
||||||
|
syncRenderHold() {},
|
||||||
|
schedulePlayback() {},
|
||||||
|
refreshEndpoints() {},
|
||||||
|
addMarker: () => ({ show: true }),
|
||||||
|
paintMode() {},
|
||||||
|
paintSelected() {},
|
||||||
|
cancelWake() {},
|
||||||
|
requestVisibility() {},
|
||||||
|
};
|
||||||
|
parts.height = {
|
||||||
|
requestHeight: () => ({ height: 10, cell: 'cell', prior: null }),
|
||||||
|
releaseVehicle() {},
|
||||||
|
anchorFloors() {},
|
||||||
|
};
|
||||||
|
parts.selection = {
|
||||||
|
clearSelection() {
|
||||||
|
state._selectedKey = null;
|
||||||
|
},
|
||||||
|
refreshSelectedCard() {},
|
||||||
|
};
|
||||||
|
parts.ingestion = createIngestion({ state, services, parts, source });
|
||||||
|
const feed = getRegisteredTransitFeed('mbta');
|
||||||
|
state._enabled = true;
|
||||||
|
state._activeFeeds.set(feed.id, feed);
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
feed,
|
||||||
|
/** One poll: `report` is the vehicle the operator sends, or null for an outage. */
|
||||||
|
async poll(report) {
|
||||||
|
answer = () =>
|
||||||
|
report
|
||||||
|
? {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
headers: { get: () => null },
|
||||||
|
json: async () => ({
|
||||||
|
feedId: feed.id,
|
||||||
|
fetchedAt: Date.now(),
|
||||||
|
vehicles: [report],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
ok: false,
|
||||||
|
status: 504,
|
||||||
|
headers: { get: () => null },
|
||||||
|
json: async () => ({ error: 'Transit feed unavailable' }),
|
||||||
|
};
|
||||||
|
await parts.ingestion.pollFeed(feed, state._generation);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A vehicle `east`/`north` metres from the start, reported now. */
|
||||||
|
const report = (east, north, trip = 'trip-1') => ({
|
||||||
|
id: 'bus-7',
|
||||||
|
lat: LAT + north / M_PER_DEG,
|
||||||
|
lon: LON + east / LON_M_PER_DEG,
|
||||||
|
bearing: null,
|
||||||
|
speedMps: null,
|
||||||
|
timestamp: Date.now() / 1000,
|
||||||
|
timestampSource: 'vehicle',
|
||||||
|
routeId: '1',
|
||||||
|
tripId: trip,
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edge:transit: a stop, a turn, a feed loss and its return keep heading, data age and selection continuous', async (t) => {
|
||||||
|
const h = harness(t);
|
||||||
|
const key = transitVehicleKey(h.feed.id, 'bus-7');
|
||||||
|
// East at 10 m/s, a stop, a turn north, an outage, then the feed returns.
|
||||||
|
const script = [
|
||||||
|
[0, 0],
|
||||||
|
[150, 0],
|
||||||
|
[300, 0],
|
||||||
|
[450, 0],
|
||||||
|
[450, 0],
|
||||||
|
[450, 0],
|
||||||
|
[450, 150],
|
||||||
|
[450, 300],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
[450, 600],
|
||||||
|
[450, 750],
|
||||||
|
];
|
||||||
|
const courses = [];
|
||||||
|
const ages = [];
|
||||||
|
let entry = null;
|
||||||
|
for (const position of script) {
|
||||||
|
await h.poll(position && report(...position));
|
||||||
|
entry ??= h.state._vehicles.get(key);
|
||||||
|
if (h.state._selectedKey === null) h.state._selectedKey = key;
|
||||||
|
assert.equal(h.state._vehicles.get(key), entry, 'one entry per vehicle');
|
||||||
|
for (let s = 0; s < TRANSIT_POLL_MS / 1000; s += 1) {
|
||||||
|
t.mock.timers.tick(1000);
|
||||||
|
updatePlayback(entry, Date.now(), performance.now());
|
||||||
|
applyDisplayCourse(entry, performance.now());
|
||||||
|
courses.push(entry.courseDeg);
|
||||||
|
ages.push(entry.sample.latestReportAgeMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The selection survives the outage on the same vehicle and trip.
|
||||||
|
assert.equal(h.state._selectedKey, key);
|
||||||
|
assert.equal(entry.record.tripId, 'trip-1');
|
||||||
|
|
||||||
|
// Heading: once known it never drops out and never jumps (120 deg/s, 0.25 s
|
||||||
|
// per evaluation), it holds through the stop and the outage, and it ends
|
||||||
|
// pointing north after the turn.
|
||||||
|
const first = courses.findIndex(Number.isFinite);
|
||||||
|
assert.ok(first >= 0, 'the vehicle earns a heading');
|
||||||
|
for (let i = first + 1; i < courses.length; i += 1) {
|
||||||
|
assert.ok(Number.isFinite(courses[i]), `heading lost at second ${i}`);
|
||||||
|
const step = Math.abs(((courses[i] - courses[i - 1] + 540) % 360) - 180);
|
||||||
|
assert.ok(step <= 30 + 1e-9, `heading jumped ${step} deg at second ${i}`);
|
||||||
|
}
|
||||||
|
const atSecond = (poll, s = 14) => courses[poll * 15 + s];
|
||||||
|
assert.equal(Math.round(atSecond(5)), 90, 'parked: still facing east');
|
||||||
|
assert.equal(Math.round(atSecond(11)), 0, 'outage: still facing north');
|
||||||
|
assert.equal(Math.round(atSecond(13)), 0, 'after the return: north');
|
||||||
|
|
||||||
|
// Data age: grows second by second while nothing arrives and only falls
|
||||||
|
// when a fresh report lands.
|
||||||
|
for (let i = 1; i < ages.length; i += 1) {
|
||||||
|
assert.ok(Number.isFinite(ages[i]), `age missing at second ${i}`);
|
||||||
|
const freshPoll = i % 15 === 0 && script[i / 15] !== null;
|
||||||
|
if (!freshPoll)
|
||||||
|
assert.equal(ages[i] - ages[i - 1], 1000, `age jumped at second ${i}`);
|
||||||
|
}
|
||||||
|
assert.ok(ages[12 * 15 - 1] >= 75_000, 'the outage is visible in the age');
|
||||||
|
assert.ok(ages[12 * 15] < ages[12 * 15 - 1], 'the return resets the age');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edge:transit: a new trip on the same vehicle keeps the selection on that vehicle', async (t) => {
|
||||||
|
const h = harness(t);
|
||||||
|
const key = transitVehicleKey(h.feed.id, 'bus-7');
|
||||||
|
await h.poll(report(0, 0, 'trip-1'));
|
||||||
|
h.state._selectedKey = key;
|
||||||
|
const entry = h.state._vehicles.get(key);
|
||||||
|
const firstEpoch = entry.currentEpoch;
|
||||||
|
t.mock.timers.tick(TRANSIT_POLL_MS);
|
||||||
|
await h.poll(report(100, 0, 'trip-2'));
|
||||||
|
assert.equal(h.state._vehicles.get(key), entry);
|
||||||
|
assert.equal(h.state._selectedKey, key);
|
||||||
|
assert.equal(entry.record.tripId, 'trip-2');
|
||||||
|
// The new trip opens its own history epoch rather than joining the old one.
|
||||||
|
assert.ok(entry.currentEpoch > firstEpoch);
|
||||||
|
assert.equal(entry.track.epochs.get(entry.currentEpoch).trip, 'trip-2');
|
||||||
|
});
|
||||||
Reference in new issue
Block a user