Repository navigation
fix: radar false crashes, wrong sides, and a pole side per track #831
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
estem8
wants to merge
33
commits into
tariknz:main
Choose a base branch
from
estem8:fix/radar-fixes
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 18 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
937667d
fix: radar shows false crashes, overlapping cars and wrong pace sides
estem8 bfee575
fix: radar keeps pit lane cars alongside and splits the draw module
estem8 b7dcf28
chore: filter radar pit wall side once and trim draw module exports
estem8 89c8642
test: cover radar pit wall separation with the focus car in the pits
estem8 f57913b
fix: radar hazards ignore corner pace and replay scrubs, no spacing w…
estem8 8535a46
feat: radar pole side per track from a data file
estem8 872aff7
data: Fuji GP pole side is verified (right)
estem8 6d59daf
refactor: pole side table reads unknown sources as geometry and keeps…
estem8 084dfb9
feat: radar keybinding to flip the pole side per track, remembered lo…
estem8 1dc9e4f
data: pole side table carries track names and iRacing's start/restart…
estem8 79f5112
feat: report the player's radar pole side corrections, per start and …
estem8 ff1b6af
test: mock the pole side storage in the paused tape bridge spec
estem8 2616a5a
fix: the road shifts to the middle of a two-wide formation, and follo…
estem8 55b9224
feat: report radar pole side corrections a minute after the last flip
estem8 6b21944
feat: the pole side key and a note on it in the Radar settings
estem8 b37afd8
refactor: keep the pole side table out of the renderer bundles, simpl…
estem8 8f3a662
feat: the pole side flip also works on a standing grid
estem8 13ac8d5
fix: address the code review of the radar branch
estem8 3cdffd1
fix: address the second code review of the radar branch
estem8 3ef9cfe
fix: keep a pole side correction when its write fails, test the host …
estem8 e6ea5c3
fix: resolve the pole side file path lazily so importing the module n…
estem8 11ea837
fix: read the pole side file ahead of the radar without sync I/O, flu…
estem8 13aa5a8
fix: load the pole side file once before any reader, never replace a …
estem8 180e2e0
fix: recheck the analytics opt-out right before sending the pole side…
estem8 1aeb24c
fix: forget a sudden drop once the car keeps moving above a crawl for…
estem8 2af30a7
fix: keep pole side types out of the processors, log no file paths, s…
estem8 7311a13
fix: address the code review of the pole side and hazard changes
estem8 1cdc197
fix: ease the radar road to its lane instead of jumping when a format…
estem8 1b7fb49
test: cover the pole side report hash storage
estem8 c1723d0
test: use promise-based fs in the pole side hash spec hooks
estem8 cb71bb1
test: use promise-based fs in the pole side overrides storage spec
estem8 03c06cb
Merge remote-tracking branch 'upstream/main' into fix/radar-fixes
estem8 1541a9c
fix: radar pace car on the shifted road, drop the pole notice on a re…
estem8 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
| import type { EventMessage, IdentifyMessage } from 'posthog-node'; | ||
| import type { DashboardLayout } from '@irdashies/types'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { mkdtempSync } from 'node:fs'; | ||
|
|
||
| const { userDataPath, store } = vi.hoisted(() => ({ | ||
| userDataPath: { value: '' }, | ||
| store: { data: new Map<string, unknown>() }, | ||
| })); | ||
|
|
||
| vi.mock('electron', () => ({ | ||
| app: { getPath: vi.fn(() => userDataPath.value) }, | ||
| screen: { | ||
| getAllDisplays: vi.fn().mockReturnValue([]), | ||
| getPrimaryDisplay: vi | ||
| .fn() | ||
| .mockReturnValue({ size: { width: 0, height: 0 } }), | ||
| }, | ||
| })); | ||
|
|
||
| vi.mock('electron-log/main', () => ({ | ||
| default: { | ||
| transports: { file: {}, console: {} }, | ||
| initialize: vi.fn(), | ||
| info: vi.fn(), | ||
| warn: vi.fn(), | ||
| error: vi.fn(), | ||
| debug: vi.fn(), | ||
| }, | ||
| })); | ||
|
|
||
| vi.mock('./storage/storage', () => ({ | ||
| readData: vi.fn((key: string) => store.data.get(key)), | ||
| writeData: vi.fn((key: string, value: unknown) => store.data.set(key, value)), | ||
| })); | ||
|
|
||
| vi.mock('./storage/analytics', () => ({ | ||
| getAnalyticsOptOut: vi.fn().mockReturnValue(false), | ||
| })); | ||
|
|
||
| const { corrections, flipped } = vi.hoisted(() => ({ | ||
| corrections: { value: {} as Record<string, unknown> }, | ||
| flipped: { listeners: new Set<() => void>() }, | ||
| })); | ||
| vi.mock('./storage/radarPoleSideOverrides', () => ({ | ||
| radarPoleSideOverrides: { | ||
| snapshot: () => corrections.value, | ||
| onChange: (listener: () => void) => { | ||
| flipped.listeners.add(listener); | ||
| return () => flipped.listeners.delete(listener); | ||
| }, | ||
| }, | ||
| })); | ||
|
|
||
| import { Analytics } from './analytics'; | ||
| import { getAnalyticsOptOut } from './storage/analytics'; | ||
|
|
||
| const HASH_KEY = 'radarPoleSideReportHash'; | ||
|
|
||
| const makeProvider = () => ({ | ||
| capture: vi.fn<(event: EventMessage) => void>(), | ||
| captureConfirmed: vi | ||
| .fn<(event: EventMessage) => Promise<void>>() | ||
| .mockResolvedValue(undefined), | ||
| identify: vi.fn<(message: IdentifyMessage) => void>(), | ||
| shutdown: vi.fn<(shutdownTimeoutMs?: number) => void>(), | ||
| }); | ||
|
|
||
| describe('Analytics radar pole side report', () => { | ||
| let provider: ReturnType<typeof makeProvider>; | ||
|
|
||
| const start = async (version = '2.0.0') => { | ||
| const analytics = new Analytics(provider); | ||
| await analytics.init(version, {} as DashboardLayout); | ||
| await analytics.waitForRadarPoleSideUpload(); | ||
| }; | ||
| const reports = () => | ||
| provider.captureConfirmed.mock.calls | ||
| .map(([event]) => event) | ||
| .filter((event) => event.event === 'radar_pole_side'); | ||
|
|
||
| beforeEach(() => { | ||
| userDataPath.value = mkdtempSync(path.join(os.tmpdir(), 'irdashies-')); | ||
| store.data.clear(); | ||
| corrections.value = {}; | ||
| flipped.listeners.clear(); | ||
| provider = makeProvider(); | ||
| vi.mocked(getAnalyticsOptOut).mockReturnValue(false); | ||
| }); | ||
|
|
||
| it('sends nothing for an installation that never corrected a side', async () => { | ||
| await start(); | ||
| expect(reports()).toEqual([]); | ||
| }); | ||
|
|
||
| it('reports the corrections once, then remembers what was acknowledged', async () => { | ||
| corrections.value = { '95': { start: false, table: true } }; | ||
| await start(); | ||
|
|
||
| expect(reports()).toHaveLength(1); | ||
| expect(reports()[0]).toMatchObject({ | ||
| event: 'radar_pole_side', | ||
| properties: { | ||
| version: '2.0.0', | ||
| track_count: 1, | ||
| tracks: { '95': { start_left: false, table_left: true } }, | ||
| }, | ||
| }); | ||
| expect(store.data.get(HASH_KEY)).toBe(reports()[0].properties?.data_hash); | ||
|
|
||
| await start('3.0.0'); | ||
| expect(reports()).toHaveLength(1); | ||
| }); | ||
|
|
||
| it('reports a change, and a revert to nothing', async () => { | ||
| corrections.value = { '95': { start: false, table: true } }; | ||
| await start(); | ||
| corrections.value = { | ||
| '95': { start: false, restart: false, table: true }, | ||
| }; | ||
| await start(); | ||
| corrections.value = {}; | ||
| await start(); | ||
|
|
||
| expect(reports()).toHaveLength(3); | ||
| expect(reports()[2].properties?.track_count).toBe(0); | ||
| }); | ||
|
|
||
| it('does not record a hash when delivery fails, so it is retried', async () => { | ||
| corrections.value = { '95': { start: false, table: true } }; | ||
| provider.captureConfirmed.mockRejectedValueOnce(new Error('down')); | ||
| await expect(start()).resolves.toBeUndefined(); | ||
| expect(store.data.get(HASH_KEY)).toBeUndefined(); | ||
|
|
||
| await start(); | ||
| expect(store.data.get(HASH_KEY)).toBeDefined(); | ||
| }); | ||
|
|
||
| it('respects the analytics opt-out', async () => { | ||
| corrections.value = { '95': { start: false, table: true } }; | ||
| vi.mocked(getAnalyticsOptOut).mockReturnValue(true); | ||
| await start(); | ||
| expect(reports()).toEqual([]); | ||
| }); | ||
|
|
||
| it('ignores a stored value that is not a hash', async () => { | ||
| store.data.set(HASH_KEY, 'not-a-hash'); | ||
| await start(); | ||
| expect(reports()).toEqual([]); | ||
| }); | ||
|
|
||
| describe('after a flip', () => { | ||
| beforeEach(() => { | ||
| vi.useFakeTimers(); | ||
| }); | ||
| afterEach(() => { | ||
| vi.useRealTimers(); | ||
| }); | ||
|
|
||
| const flip = () => flipped.listeners.forEach((listener) => listener()); | ||
|
|
||
| it('reports a minute after the last flip, not at once and not once per press', async () => { | ||
| await start(); | ||
| expect(reports()).toEqual([]); | ||
|
|
||
| corrections.value = { '95': { start: false, table: true } }; | ||
| flip(); | ||
| await vi.advanceTimersByTimeAsync(30_000); | ||
| flip(); | ||
| await vi.advanceTimersByTimeAsync(59_000); | ||
| expect(reports()).toEqual([]); | ||
|
|
||
| await vi.advanceTimersByTimeAsync(2_000); | ||
| expect(reports()).toHaveLength(1); | ||
| expect(reports()[0].properties?.track_count).toBe(1); | ||
| expect(store.data.get(HASH_KEY)).toBeDefined(); | ||
| }); | ||
|
|
||
| it('sends nothing when the flips cancelled out and nothing was ever sent', async () => { | ||
| await start(); | ||
| flip(); | ||
| await vi.advanceTimersByTimeAsync(61_000); | ||
| expect(reports()).toEqual([]); | ||
| }); | ||
|
|
||
| it('stops its timer when the app shuts down', async () => { | ||
| const analytics = new Analytics(provider); | ||
| await analytics.init('2.0.0', {} as DashboardLayout); | ||
| await analytics.waitForRadarPoleSideUpload(); | ||
| corrections.value = { '95': { start: false, table: true } }; | ||
| flip(); | ||
| analytics.shutdown(); | ||
| await vi.advanceTimersByTimeAsync(120_000); | ||
| expect(reports()).toEqual([]); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the one-minute post-flip timer reaches
captureRadarPoleSides, this call and line 317 go throughstorage/storage.ts'sreadFileSync/writeFileSync; that callback executes on Electron's main thread, so a slow or locked user-data volume stalls telemetry/window processing and the checkpoint write bypasses the required debouncing and quit flush. Use dedicated async checkpoint persistence, like the pit-lane report does.AGENTS.md reference: AGENTS.md:L14-L14
Useful? React with 👍 / 👎.