summaryrefslogtreecommitdiff
path: root/test-runner/src/test/kotlin/org/terst
diff options
context:
space:
mode:
authorClaude <noreply@anthropic.com>2026-05-26 08:07:50 +0000
committerClaude <noreply@anthropic.com>2026-05-26 08:07:50 +0000
commitf564e06338f846930300c366774efb97c042a5fb (patch)
tree369ec57c831fb3b3de7220f4ba06fb3cb6d32440 /test-runner/src/test/kotlin/org/terst
parentbbfcb667d9510e604676ac92cecc69f9bc39e440 (diff)
parent3673ed0ef79d776c6da3bfa0db9de8b08326fd32 (diff)
Merge claude/whats-next-0XCO3: fix ANR, tack detection, empty-state flash
Diffstat (limited to 'test-runner/src/test/kotlin/org/terst')
-rw-r--r--test-runner/src/test/kotlin/org/terst/nav/track/TackDetectorTest.kt126
1 files changed, 79 insertions, 47 deletions
diff --git a/test-runner/src/test/kotlin/org/terst/nav/track/TackDetectorTest.kt b/test-runner/src/test/kotlin/org/terst/nav/track/TackDetectorTest.kt
index 44bd667..265ba76 100644
--- a/test-runner/src/test/kotlin/org/terst/nav/track/TackDetectorTest.kt
+++ b/test-runner/src/test/kotlin/org/terst/nav/track/TackDetectorTest.kt
@@ -5,24 +5,27 @@ import org.junit.Test
class TackDetectorTest {
- private fun pt(lat: Double, lon: Double, cog: Double, ts: Long) =
- TrackPoint(lat, lon, 5.0, cog, 12.0, 0.0, true, ts)
-
- private fun straightTrack(cog: Double, count: Int = 10): List<TrackPoint> =
- (0 until count).map { i -> pt(37.0 + i * 0.01, -122.0, cog, i * 10_000L) }
-
- private fun tackTrack(cogBefore: Double, cogAfter: Double, each: Int = 6): List<TrackPoint> {
- val before = (0 until each).map { i -> pt(37.0 + i * 0.005, -122.0, cogBefore, i * 10_000L) }
- val after = (0 until each).map { i -> pt(37.0 + each * 0.005 + i * 0.005, -122.0, cogAfter, (each + i) * 10_000L) }
- return before + after
- }
+ // 1 Hz GPS (FULL mode). Time-based windows:
+ // T_SETTLE=30s, T_MANEUVER=30s, START_SKIP=60s.
+ // Minimum for a candidate to fire: START_SKIP(60) + T_SETTLE(30) + T_MANEUVER/2(15) = 105 s from t0.
+ // To also have a 30s after-window: apex at ~105s, after-window ends at 105+15+30=150s.
+ // So a safe warmup leg is 120 pts (t=0..119s), tack apex around t=120s, after-leg ≥ 60 pts.
+ private val STEP_MS = 1_000L // 1 s between points
+
+ private fun leg(cog: Double, count: Int, startMs: Long, sogKnots: Double = 5.0): List<TrackPoint> =
+ (0 until count).map { i ->
+ TrackPoint(37.0 + (startMs / 1000 + i) * 0.00001, -122.0, sogKnots, cog, 12.0, 0.0, true,
+ startMs + i * STEP_MS)
+ }
+
+ // Standard two-leg track: 120 s warmup on cogBefore, 120 s on cogAfter.
+ // Tack apex candidates fire around t≈120 s, well past START_SKIP=60s.
+ private fun tackTrack(cogBefore: Double, cogAfter: Double): List<TrackPoint> =
+ leg(cogBefore, 120, 0L) + leg(cogAfter, 120, 120_000L)
@Test fun `straight course yields no tacks`() {
- assertTrue(TackDetector.detectTacks(straightTrack(90.0)).isEmpty())
- }
-
- @Test fun `straight course on port tack yields no tacks`() {
- assertTrue(TackDetector.detectTacks(straightTrack(330.0)).isEmpty())
+ val pts = leg(90.0, 300, 0L)
+ assertTrue(TackDetector.detectTacks(pts).isEmpty())
}
@Test fun `detects port to starboard tack`() {
@@ -33,63 +36,92 @@ class TackDetectorTest {
}
@Test fun `detects starboard to port tack`() {
- val tacks = TackDetector.detectTacks(tackTrack(70.0, 330.0))
- assertEquals(1, tacks.size)
+ assertEquals(1, TackDetector.detectTacks(tackTrack(70.0, 330.0)).size)
}
@Test fun `detects jibe on broad reach`() {
- // broad reach port → starboard: 150° → 210°, delta = 60°
- val tacks = TackDetector.detectTacks(tackTrack(150.0, 250.0))
- assertEquals(1, tacks.size)
+ // 150° → 290°: delta = 140°, within [60°, 160°]
+ assertEquals(1, TackDetector.detectTacks(tackTrack(150.0, 290.0)).size)
}
@Test fun `handles 0-360 wrap`() {
- // 350° → 80°: delta = 90° through north
- val tacks = TackDetector.detectTacks(tackTrack(350.0, 80.0))
- assertEquals(1, tacks.size)
- }
-
- @Test fun `gradual course change is not a tack`() {
- // COG increases by 5° per point: window delta ≈ 15°, below threshold
- val points = (0 until 20).map { i -> pt(37.0 + i * 0.01, -122.0, i * 5.0, i * 10_000L) }
- assertTrue(TackDetector.detectTacks(points).isEmpty())
+ assertEquals(1, TackDetector.detectTacks(tackTrack(350.0, 80.0)).size)
}
- @Test fun `small heading change below threshold is ignored`() {
- // 45° change — not a tack
+ @Test fun `heading change below MIN_DELTA is ignored`() {
+ // 45° delta — below MIN_DELTA=75°
assertTrue(TackDetector.detectTacks(tackTrack(90.0, 135.0)).isEmpty())
}
- @Test fun `very large heading change above threshold is ignored`() {
- // 160° delta — U-turn, not a normal tack or jibe
- assertTrue(TackDetector.detectTacks(tackTrack(90.0, 250.0)).isEmpty())
+ @Test fun `heading change of 170 degrees is detected`() {
+ // 170° delta — valid crash-tack; no upper cap
+ assertEquals(1, TackDetector.detectTacks(tackTrack(90.0, 260.0)).size)
}
- @Test fun `detects two tacks in sequence`() {
- val leg1 = (0 until 6).map { i -> pt(37.0 + i * 0.01, -122.0, 330.0, i * 10_000L) }
- val leg2 = (0 until 6).map { i -> pt(37.06 + i * 0.01, -122.0, 70.0, (6 + i) * 10_000L) }
- val leg3 = (0 until 6).map { i -> pt(37.12 + i * 0.01, -122.0, 330.0, (12 + i) * 10_000L) }
+ @Test fun `detects two tacks in sequence with sufficient gap`() {
+ // leg1: 120s, leg2: 180s (> MIN_GAP_MS=60s), leg3: 120s
+ val leg1 = leg(330.0, 120, 0L)
+ val leg2 = leg(70.0, 180, 120_000L)
+ val leg3 = leg(330.0, 120, 300_000L)
val tacks = TackDetector.detectTacks(leg1 + leg2 + leg3)
assertEquals(2, tacks.size)
}
- @Test fun `deduplicates tacks at same transition`() {
- val before = (0 until 8).map { i -> pt(37.0, -122.0 + i * 0.0001, 330.0, i * 5_000L) }
- val after = (0 until 8).map { i -> pt(37.0, -122.0 + 0.0008 + i * 0.0001, 70.0, (8 + i) * 5_000L) }
- val tacks = TackDetector.detectTacks(before + after)
+ @Test fun `deduplicates adjacent candidates into single tack`() {
+ // Single abrupt tack fires many consecutive raw candidates; must collapse to 1
+ val tacks = TackDetector.detectTacks(tackTrack(330.0, 70.0))
assertEquals(1, tacks.size)
}
- @Test fun `tack event contains correct position`() {
+ @Test fun `tack event contains plausible position`() {
val tacks = TackDetector.detectTacks(tackTrack(330.0, 70.0))
assertEquals(1, tacks.size)
- // Position should be somewhere in the track bounds
assertTrue(tacks[0].lat in 37.0..37.1)
assertTrue(tacks[0].lon in -122.1..-122.0)
}
@Test fun `too few points returns empty`() {
- val points = (0 until 3).map { i -> pt(37.0, -122.0, 90.0, i * 1000L) }
- assertTrue(TackDetector.detectTacks(points).isEmpty())
+ val pts = leg(90.0, 2, 0L)
+ assertTrue(TackDetector.detectTacks(pts).isEmpty())
+ }
+
+ @Test fun `cold start suppresses tack in first 60 seconds`() {
+ // Tack apex at t=30s — inside START_SKIP_MS=60s → filtered
+ val pts = leg(330.0, 40, 0L) + leg(70.0, 60, 40_000L)
+ assertEquals(0, TackDetector.detectTacks(pts).size)
+ }
+
+ @Test fun `noisy anchor COG does not produce false tack`() {
+ // Simulate boat at anchor: COG rotates 0..360 uniformly (MAD ≈ 90°, fails STAB_MAX=20°)
+ val pts = (0 until 300).map { i ->
+ TrackPoint(37.0, -122.0, 0.1, (i * 37.0) % 360.0, 12.0, 0.0, true, i * STEP_MS)
+ }
+ assertTrue(TackDetector.detectTacks(pts).isEmpty())
+ }
+
+ @Test fun `unstable before-window prevents false detection`() {
+ // Before-window has scattered headings (std dev >> STAB_MAX), after-window is stable
+ val noisyBefore = (0 until 120).map { i ->
+ TrackPoint(37.0, -122.0, 3.0, (i * 47.0) % 360.0, 12.0, 0.0, true, i * STEP_MS)
+ }
+ val stableAfter = leg(70.0, 120, 120_000L)
+ assertTrue(TackDetector.detectTacks(noisyBefore + stableAfter).isEmpty())
+ }
+
+ @Test fun `gradual 5-degree-per-second course change is not a tack`() {
+ // Course changes 5°/s for 60 s = 300° total change but never a sudden tack
+ val pts = (0 until 300).map { i ->
+ TrackPoint(37.0 + i * 0.0001, -122.0, 5.0, (i * 5.0) % 360.0, 12.0, 0.0, true, i * STEP_MS)
+ }
+ assertTrue(TackDetector.detectTacks(pts).isEmpty())
+ }
+
+ @Test fun `two tacks closer than MIN_GAP_MS produces only one event`() {
+ // leg1=120s, leg2=45s (< MIN_GAP_MS=60s gap to leg3 candidates), leg3=120s
+ val leg1 = leg(330.0, 120, 0L)
+ val leg2 = leg(70.0, 45, 120_000L)
+ val leg3 = leg(330.0, 120, 165_000L)
+ val tacks = TackDetector.detectTacks(leg1 + leg2 + leg3)
+ assertEquals(1, tacks.size)
}
}