diff options
Diffstat (limited to 'android-app/app/src/test/kotlin')
3 files changed, 191 insertions, 0 deletions
diff --git a/android-app/app/src/test/kotlin/com/example/androidapp/tide/HarmonicTideCalculatorTest.kt b/android-app/app/src/test/kotlin/com/example/androidapp/tide/HarmonicTideCalculatorTest.kt new file mode 100644 index 0000000..612ae34 --- /dev/null +++ b/android-app/app/src/test/kotlin/com/example/androidapp/tide/HarmonicTideCalculatorTest.kt @@ -0,0 +1,135 @@ +package com.example.androidapp.tide + +import com.example.androidapp.data.model.TideConstituent +import com.example.androidapp.data.model.TideStation +import org.junit.Assert.* +import org.junit.Test + +class HarmonicTideCalculatorTest { + + // Reference epoch: 2000-01-01 00:00:00 UTC = 946_684_800_000 ms + private val epochMs = HarmonicTideCalculator.EPOCH_MS + private val oneHourMs = 3_600_000L + + private fun stationWith( + speed: Double = 30.0, + amplitude: Double = 1.0, + phase: Double = 0.0, + datum: Double = 0.0 + ) = TideStation( + id = "test", name = "Test", lat = 0.0, lon = 0.0, + datumOffsetMeters = datum, + constituents = listOf(TideConstituent("S2", speed, amplitude, phase)) + ) + + @Test + fun `predictHeight at epoch gives datum plus amplitude for zero-phase constituent`() { + val station = stationWith(speed = 30.0, amplitude = 1.5, phase = 0.0, datum = 0.5) + val height = HarmonicTideCalculator.predictHeight(station, epochMs) + assertEquals(0.5 + 1.5, height, 1e-9) // cos(0°) = 1.0 + } + + @Test + fun `predictHeight at half period gives datum minus amplitude`() { + // speed = 30 deg/hr → half period = 6 hours → cos(180°) = -1.0 + val station = stationWith(speed = 30.0, amplitude = 1.0, phase = 0.0, datum = 0.0) + val height = HarmonicTideCalculator.predictHeight(station, epochMs + 6 * oneHourMs) + assertEquals(-1.0, height, 1e-9) + } + + @Test + fun `predictHeight at quarter period is near zero`() { + // speed = 30 deg/hr → quarter period = 3 hours → cos(90°) ≈ 0.0 + val station = stationWith(speed = 30.0, amplitude = 1.0, phase = 0.0, datum = 0.0) + val height = HarmonicTideCalculator.predictHeight(station, epochMs + 3 * oneHourMs) + assertEquals(0.0, height, 1e-9) + } + + @Test + fun `predictHeight applies phase offset correctly`() { + // phase = 90 → cos(0 - 90°) = cos(-90°) ≈ 0.0 at epoch + val station = stationWith(speed = 30.0, amplitude = 1.0, phase = 90.0, datum = 0.0) + val height = HarmonicTideCalculator.predictHeight(station, epochMs) + assertEquals(0.0, height, 1e-9) + } + + @Test + fun `predictHeight sums multiple constituents at epoch`() { + val station = TideStation( + id = "test", name = "Test", lat = 0.0, lon = 0.0, + datumOffsetMeters = 2.0, + constituents = listOf( + TideConstituent("S2", 30.0, 1.0, 0.0), // +1.0 at epoch + TideConstituent("K1", 30.0, 0.5, 0.0) // +0.5 at epoch + ) + ) + val height = HarmonicTideCalculator.predictHeight(station, epochMs) + assertEquals(3.5, height, 1e-9) // 2.0 + 1.0 + 0.5 + } + + @Test + fun `predictHeight with empty constituents returns datum offset only`() { + val station = TideStation("t", "T", 0.0, 0.0, 3.14, emptyList()) + assertEquals(3.14, HarmonicTideCalculator.predictHeight(station, epochMs), 1e-9) + } + + @Test + fun `predictRange returns correct number of predictions`() { + val station = stationWith() + val predictions = HarmonicTideCalculator.predictRange( + station, epochMs, epochMs + 3 * oneHourMs, oneHourMs + ) + assertEquals(4, predictions.size) // t=0h, 1h, 2h, 3h + } + + @Test + fun `predictRange timestamps are evenly spaced`() { + val station = stationWith() + val predictions = HarmonicTideCalculator.predictRange( + station, epochMs, epochMs + 2 * oneHourMs, oneHourMs + ) + assertEquals(epochMs, predictions[0].timestampMs) + assertEquals(epochMs + oneHourMs, predictions[1].timestampMs) + assertEquals(epochMs + 2 * oneHourMs, predictions[2].timestampMs) + } + + @Test + fun `predictRange with equal from and to returns single prediction`() { + val station = stationWith() + val predictions = HarmonicTideCalculator.predictRange(station, epochMs, epochMs, oneHourMs) + assertEquals(1, predictions.size) + assertEquals(epochMs, predictions[0].timestampMs) + } + + @Test + fun `findHighLow returns empty list for fewer than 3 predictions`() { + val station = stationWith() + val predictions = HarmonicTideCalculator.predictRange( + station, epochMs, epochMs + oneHourMs, oneHourMs + ) + assertEquals(2, predictions.size) + assertTrue(HarmonicTideCalculator.findHighLow(predictions).isEmpty()) + } + + @Test + fun `findHighLow detects high and low water events`() { + // speed = 30 deg/hr, 3-hour samples over 24 hours + // Heights: 1.0, 0.0, -1.0, 0.0, 1.0, 0.0, -1.0, 0.0, 1.0 + // Turning points at t=6h(low), t=12h(high), t=18h(low) + val station = stationWith(speed = 30.0, amplitude = 1.0, phase = 0.0, datum = 0.0) + val predictions = HarmonicTideCalculator.predictRange( + station, + epochMs, + epochMs + 24 * oneHourMs, + 3 * oneHourMs + ) + val highLow = HarmonicTideCalculator.findHighLow(predictions) + assertEquals(3, highLow.size) + assertEquals(epochMs + 6 * oneHourMs, highLow[0].timestampMs) + assertEquals(-1.0, highLow[0].heightMeters, 1e-9) + assertEquals(epochMs + 12 * oneHourMs, highLow[1].timestampMs) + assertEquals(1.0, highLow[1].heightMeters, 1e-9) + assertEquals(epochMs + 18 * oneHourMs, highLow[2].timestampMs) + assertEquals(-1.0, highLow[2].heightMeters, 1e-9) + } +} diff --git a/android-app/app/src/test/kotlin/org/terst/nav/data/model/TideModelTest.kt b/android-app/app/src/test/kotlin/org/terst/nav/data/model/TideModelTest.kt new file mode 100644 index 0000000..0a6f4bb --- /dev/null +++ b/android-app/app/src/test/kotlin/org/terst/nav/data/model/TideModelTest.kt @@ -0,0 +1,56 @@ +package com.example.androidapp.data.model + +import org.junit.Assert.* +import org.junit.Test + +class TideModelTest { + + @Test + fun `TideConstituent holds all fields`() { + val c = TideConstituent("M2", 28.9841042, 0.85, 120.0) + assertEquals("M2", c.name) + assertEquals(28.9841042, c.speedDegPerHour, 1e-7) + assertEquals(0.85, c.amplitudeMeters, 1e-9) + assertEquals(120.0, c.phaseDeg, 1e-9) + } + + @Test + fun `TidePrediction holds timestamp and height`() { + val p = TidePrediction(1_700_000_000_000L, 2.34) + assertEquals(1_700_000_000_000L, p.timestampMs) + assertEquals(2.34, p.heightMeters, 1e-9) + } + + @Test + fun `TidePrediction data class equality`() { + val p1 = TidePrediction(1_000L, 1.5) + val p2 = TidePrediction(1_000L, 1.5) + assertEquals(p1, p2) + } + + @Test + fun `TideStation holds all fields and constituents`() { + val c = TideConstituent("K1", 15.0410686, 0.3, 45.0) + val station = TideStation( + id = "9447130", + name = "Seattle, WA", + lat = 47.602, + lon = -122.339, + datumOffsetMeters = 1.8, + constituents = listOf(c) + ) + assertEquals("9447130", station.id) + assertEquals("Seattle, WA", station.name) + assertEquals(47.602, station.lat, 1e-9) + assertEquals(-122.339, station.lon, 1e-9) + assertEquals(1.8, station.datumOffsetMeters, 1e-9) + assertEquals(1, station.constituents.size) + assertEquals("K1", station.constituents[0].name) + } + + @Test + fun `TideStation with empty constituents is valid`() { + val station = TideStation("test", "Test", 0.0, 0.0, 0.0, emptyList()) + assertTrue(station.constituents.isEmpty()) + } +} diff --git a/android-app/app/src/test/kotlin/org\/terst\/nav/data/model/TideModelTest.kt b/android-app/app/src/test/kotlin/org\/terst\/nav/data/model/TideModelTest.kt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/android-app/app/src/test/kotlin/org\/terst\/nav/data/model/TideModelTest.kt |
