diff options
Diffstat (limited to 'test-runner/src')
12 files changed, 553 insertions, 1 deletions
diff --git a/test-runner/src/main/kotlin/org/terst/nav/ais/CpaAlert.kt b/test-runner/src/main/kotlin/org/terst/nav/ais/CpaAlert.kt new file mode 100644 index 0000000..1d635c0 --- /dev/null +++ b/test-runner/src/main/kotlin/org/terst/nav/ais/CpaAlert.kt @@ -0,0 +1,23 @@ +package org.terst.nav.ais + +enum class CpaSeverity { CAUTION, WARNING, DANGER } + +data class CpaAlert( + val mmsi: Int, + val vesselName: String, + val cpaNm: Double, + val tcpaMin: Double, + val severity: CpaSeverity +) + +object CpaThresholds { + const val DANGER_CPA_NM = 0.5 + const val WARNING_CPA_NM = 1.0 + const val CAUTION_CPA_NM = 2.0 + const val MAX_TCPA_MIN = 30.0 // ignore contacts closing in > 30 min +} + +fun CpaAlert.label(): String { + val name = if (vesselName.isNotBlank()) vesselName else "MMSI $mmsi" + return "%-12s CPA %.1fnm T+%.0fmin".format(name.take(12), cpaNm, tcpaMin) +} diff --git a/test-runner/src/main/kotlin/org/terst/nav/ais/CpaCalculator.kt b/test-runner/src/main/kotlin/org/terst/nav/ais/CpaCalculator.kt index 7e98451..298af59 100644 --- a/test-runner/src/main/kotlin/org/terst/nav/ais/CpaCalculator.kt +++ b/test-runner/src/main/kotlin/org/terst/nav/ais/CpaCalculator.kt @@ -2,7 +2,6 @@ package org.terst.nav.ais import kotlin.math.cos import kotlin.math.sqrt -import kotlin.math.PI object CpaCalculator { fun compute( diff --git a/test-runner/src/main/kotlin/org/terst/nav/settings/HardwareSource.kt b/test-runner/src/main/kotlin/org/terst/nav/settings/HardwareSource.kt new file mode 100644 index 0000000..b44cb66 --- /dev/null +++ b/test-runner/src/main/kotlin/org/terst/nav/settings/HardwareSource.kt @@ -0,0 +1,23 @@ +package org.terst.nav.settings + +enum class HardwareSource(val prefKey: String, val label: String) { + AIS_RECEIVER("hw_ais_receiver", "AIS Receiver"), + NMEA_INSTRUMENTS("hw_nmea_instruments", "NMEA Instruments") +} + +/** Minimal in-memory implementation for JVM tests. */ +class InMemoryFeatureFlags { + private val state = mutableMapOf<HardwareSource, Boolean>() + + fun isEnabled(source: HardwareSource): Boolean = state[source] ?: false + + fun setEnabled(source: HardwareSource, enabled: Boolean) { + state[source] = enabled + } + + fun toggle(source: HardwareSource): Boolean { + val next = !isEnabled(source) + setEnabled(source, next) + return next + } +} diff --git a/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt b/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt index 85dd2dd..98d0bf3 100644 --- a/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt +++ b/test-runner/src/main/kotlin/org/terst/nav/track/TrackRepository.kt @@ -1,5 +1,10 @@ package org.terst.nav.track +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt + class TrackRepository { var isRecording: Boolean = false @@ -29,4 +34,27 @@ class TrackRepository { fun getPoints(): List<TrackPoint> = activePoints.toList() fun getPastTracks(): List<List<TrackPoint>> = pastTracks.toList() + + fun computeStats(): TrackStats? { + if (activePoints.size < 2) return null + var distNm = 0.0 + for (i in 1 until activePoints.size) { + distNm += haversineNm(activePoints[i - 1].lat, activePoints[i - 1].lon, activePoints[i].lat, activePoints[i].lon) + } + val durationMs = activePoints.last().timestampMs - activePoints.first().timestampMs + val avgSog = if (durationMs > 0) activePoints.map { it.sogKnots }.average() else 0.0 + return TrackStats(distNm, durationMs, avgSog) + } + + companion object { + fun haversineNm(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double { + val r = 3440.065 + val dLat = Math.toRadians(lat2 - lat1) + val dLon = Math.toRadians(lon2 - lon1) + val a = sin(dLat / 2) * sin(dLat / 2) + + cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) * + sin(dLon / 2) * sin(dLon / 2) + return r * 2 * atan2(sqrt(a), sqrt(1 - a)) + } + } } diff --git a/test-runner/src/main/kotlin/org/terst/nav/track/TrackStats.kt b/test-runner/src/main/kotlin/org/terst/nav/track/TrackStats.kt new file mode 100644 index 0000000..39153c6 --- /dev/null +++ b/test-runner/src/main/kotlin/org/terst/nav/track/TrackStats.kt @@ -0,0 +1,15 @@ +package org.terst.nav.track + +data class TrackStats( + val distanceNm: Double, + val durationMs: Long, + val avgSogKnots: Double +) { + val durationFormatted: String get() { + val totalSec = durationMs / 1000 + val h = totalSec / 3600 + val m = (totalSec % 3600) / 60 + val s = totalSec % 60 + return if (h > 0) "%d:%02d:%02d".format(h, m, s) else "%d:%02d".format(m, s) + } +} diff --git a/test-runner/src/main/kotlin/org/terst/nav/vessel/CrewMember.kt b/test-runner/src/main/kotlin/org/terst/nav/vessel/CrewMember.kt new file mode 100644 index 0000000..0df8cb9 --- /dev/null +++ b/test-runner/src/main/kotlin/org/terst/nav/vessel/CrewMember.kt @@ -0,0 +1,15 @@ +package org.terst.nav.vessel + +data class CrewMember( + val id: String, + val name: String, + val role: CrewRole = CrewRole.CREW, + val emergencyContact: String = "", + val emergencyPhone: String = "", + val certifications: List<String> = emptyList(), + val notes: String = "" +) + +enum class CrewRole { + SKIPPER, FIRST_MATE, WATCH_CAPTAIN, CREW, GUEST +} diff --git a/test-runner/src/main/kotlin/org/terst/nav/vessel/Vessel.kt b/test-runner/src/main/kotlin/org/terst/nav/vessel/Vessel.kt new file mode 100644 index 0000000..b7c0eaf --- /dev/null +++ b/test-runner/src/main/kotlin/org/terst/nav/vessel/Vessel.kt @@ -0,0 +1,22 @@ +package org.terst.nav.vessel + +data class Vessel( + val id: String, + val name: String, + val mmsi: String = "", + val callsign: String = "", + val flag: String = "", + val homePort: String = "", + val hullType: HullType = HullType.MONOHULL, + val loaMeters: Double = 0.0, + val beamMeters: Double = 0.0, + val draftMeters: Double = 0.0, + val displacementKg: Double = 0.0, + val engineHp: Int = 0, + val notes: String = "", + val isOwnVessel: Boolean = false +) + +enum class HullType { + MONOHULL, CATAMARAN, TRIMARAN, POWERBOAT, OTHER +} diff --git a/test-runner/src/main/kotlin/org/terst/nav/vessel/VesselRepository.kt b/test-runner/src/main/kotlin/org/terst/nav/vessel/VesselRepository.kt new file mode 100644 index 0000000..fe4c44d --- /dev/null +++ b/test-runner/src/main/kotlin/org/terst/nav/vessel/VesselRepository.kt @@ -0,0 +1,55 @@ +package org.terst.nav.vessel + +import java.util.UUID + +class VesselRepository { + + private val vessels = mutableListOf<Vessel>() + private val crew = mutableListOf<CrewMember>() + + fun addVessel(vessel: Vessel): Vessel { + val v = if (vessel.id.isBlank()) vessel.copy(id = UUID.randomUUID().toString()) else vessel + vessels.removeAll { it.id == v.id } + vessels.add(v) + return v + } + + fun updateVessel(vessel: Vessel): Boolean { + val idx = vessels.indexOfFirst { it.id == vessel.id } + if (idx < 0) return false + vessels[idx] = vessel + return true + } + + fun removeVessel(id: String): Boolean = vessels.removeAll { it.id == id } + + fun getVessel(id: String): Vessel? = vessels.find { it.id == id } + + fun getVessels(): List<Vessel> = vessels.toList() + + fun getOwnVessel(): Vessel? = vessels.find { it.isOwnVessel } + + fun addCrewMember(member: CrewMember): CrewMember { + val m = if (member.id.isBlank()) member.copy(id = UUID.randomUUID().toString()) else member + crew.removeAll { it.id == m.id } + crew.add(m) + return m + } + + fun updateCrewMember(member: CrewMember): Boolean { + val idx = crew.indexOfFirst { it.id == member.id } + if (idx < 0) return false + crew[idx] = member + return true + } + + fun removeCrewMember(id: String): Boolean = crew.removeAll { it.id == id } + + fun getCrewMember(id: String): CrewMember? = crew.find { it.id == id } + + fun getCrew(): List<CrewMember> = crew.toList() + + fun getCrewByRole(role: CrewRole): List<CrewMember> = crew.filter { it.role == role } + + fun getSkipper(): CrewMember? = crew.find { it.role == CrewRole.SKIPPER } +} diff --git a/test-runner/src/test/kotlin/org/terst/nav/ais/CpaAlertTest.kt b/test-runner/src/test/kotlin/org/terst/nav/ais/CpaAlertTest.kt new file mode 100644 index 0000000..3effb24 --- /dev/null +++ b/test-runner/src/test/kotlin/org/terst/nav/ais/CpaAlertTest.kt @@ -0,0 +1,78 @@ +package org.terst.nav.ais + +import org.junit.Assert.* +import org.junit.Test + +class CpaAlertTest { + + // --- CpaCalculator --- + + @Test + fun `head-on convergence produces small CPA and positive TCPA`() { + // Own vessel heading north at 10 kt; target heading south at 10 kt, 1 nm ahead + val (cpa, tcpa) = CpaCalculator.compute( + ownLat = 0.0, ownLon = 0.0, ownSog = 10.0, ownCog = 0.0, + tgtLat = 1.0 / 60.0, tgtLon = 0.0, tgtSog = 10.0, tgtCog = 180.0 + ) + assertTrue("CPA should be near zero, got $cpa", cpa < 0.1) + assertTrue("TCPA should be positive, got $tcpa", tcpa > 0) + assertTrue("TCPA should be < 5 min, got $tcpa", tcpa < 5.0) + } + + @Test + fun `diverging vessels produce negative TCPA`() { + // Own heading north, target heading north ahead — both going same direction with target faster + val (_, tcpa) = CpaCalculator.compute( + ownLat = 0.0, ownLon = 0.0, ownSog = 5.0, ownCog = 0.0, + tgtLat = 1.0 / 60.0, tgtLon = 0.0, tgtSog = 10.0, tgtCog = 0.0 + ) + assertTrue("TCPA should be negative for diverging vessels, got $tcpa", tcpa < 0) + } + + @Test + fun `zero relative velocity returns current distance`() { + // Same velocity — parallel course, same speed + val (cpa, tcpa) = CpaCalculator.compute( + ownLat = 0.0, ownLon = 0.0, ownSog = 5.0, ownCog = 90.0, + tgtLat = 1.0 / 60.0, tgtLon = 0.0, tgtSog = 5.0, tgtCog = 90.0 + ) + assertTrue("CPA should equal current distance, got $cpa", cpa > 0.9 && cpa < 1.1) + assertEquals(0.0, tcpa, 1e-9) + } + + @Test + fun `well-separated crossing vessels produce large CPA`() { + // Perpendicular courses but far apart — should not alert + val (cpa, _) = CpaCalculator.compute( + ownLat = 0.0, ownLon = 0.0, ownSog = 8.0, ownCog = 0.0, + tgtLat = 0.0, tgtLon = 5.0, tgtSog = 8.0, tgtCog = 270.0 + ) + assertTrue("CPA should be > 2 nm for well-separated vessels, got $cpa", cpa > 2.0) + } + + // --- Severity classification --- + + @Test + fun `DANGER severity for CPA under 0_5 nm`() { + val alert = CpaAlert(123, "TARGET", 0.3, 10.0, CpaSeverity.DANGER) + assertEquals(CpaSeverity.DANGER, alert.severity) + } + + @Test + fun `label formats correctly with vessel name`() { + val alert = CpaAlert(123456789, "STENA IMPERO", 0.8, 12.5, CpaSeverity.WARNING) + val lbl = alert.label() + assertTrue(lbl.contains("0.8")) + assertTrue(lbl.contains("13") || lbl.contains("12")) // rounded min + assertTrue(lbl.contains("STENA")) + } + + @Test + fun `label uses MMSI prefix when name is blank`() { + val alert = CpaAlert(987654321, "", 1.5, 20.0, CpaSeverity.CAUTION) + // "MMSI 987654321" is 14 chars; label() takes the first 12 -> "MMSI 9876543" + val lbl = alert.label() + assertTrue("Expected MMSI prefix in label, got: $lbl", lbl.contains("MMSI")) + assertTrue("Expected partial MMSI in label, got: $lbl", lbl.contains("987654")) + } +} diff --git a/test-runner/src/test/kotlin/org/terst/nav/settings/FeatureFlagsTest.kt b/test-runner/src/test/kotlin/org/terst/nav/settings/FeatureFlagsTest.kt new file mode 100644 index 0000000..a8e8271 --- /dev/null +++ b/test-runner/src/test/kotlin/org/terst/nav/settings/FeatureFlagsTest.kt @@ -0,0 +1,61 @@ +package org.terst.nav.settings + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class FeatureFlagsTest { + + private lateinit var flags: InMemoryFeatureFlags + + @Before + fun setup() { + flags = InMemoryFeatureFlags() + } + + @Test + fun `all sources default to disabled`() { + for (source in HardwareSource.values()) { + assertFalse("Expected $source to be disabled by default", flags.isEnabled(source)) + } + } + + @Test + fun `setEnabled true enables source`() { + flags.setEnabled(HardwareSource.AIS_RECEIVER, true) + assertTrue(flags.isEnabled(HardwareSource.AIS_RECEIVER)) + } + + @Test + fun `setEnabled false disables source`() { + flags.setEnabled(HardwareSource.AIS_RECEIVER, true) + flags.setEnabled(HardwareSource.AIS_RECEIVER, false) + assertFalse(flags.isEnabled(HardwareSource.AIS_RECEIVER)) + } + + @Test + fun `toggle flips from false to true`() { + val result = flags.toggle(HardwareSource.NMEA_INSTRUMENTS) + assertTrue(result) + assertTrue(flags.isEnabled(HardwareSource.NMEA_INSTRUMENTS)) + } + + @Test + fun `toggle flips from true to false`() { + flags.setEnabled(HardwareSource.NMEA_INSTRUMENTS, true) + val result = flags.toggle(HardwareSource.NMEA_INSTRUMENTS) + assertFalse(result) + assertFalse(flags.isEnabled(HardwareSource.NMEA_INSTRUMENTS)) + } + + @Test + fun `sources are independent`() { + flags.setEnabled(HardwareSource.AIS_RECEIVER, true) + assertFalse(flags.isEnabled(HardwareSource.NMEA_INSTRUMENTS)) + } + + @Test + fun `toggle returns new state`() { + assertFalse(flags.toggle(HardwareSource.AIS_RECEIVER).also { assertTrue(it) } == false) + } +} diff --git a/test-runner/src/test/kotlin/org/terst/nav/track/TrackStatsTest.kt b/test-runner/src/test/kotlin/org/terst/nav/track/TrackStatsTest.kt new file mode 100644 index 0000000..7bce0a2 --- /dev/null +++ b/test-runner/src/test/kotlin/org/terst/nav/track/TrackStatsTest.kt @@ -0,0 +1,79 @@ +package org.terst.nav.track + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class TrackStatsTest { + + private lateinit var repo: TrackRepository + + private fun pt(lat: Double, lon: Double, sog: Double = 5.0, ts: Long = 0L) = + TrackPoint(lat, lon, sog, 0.0, 0.0, 0.0, false, ts) + + @Before + fun setup() { + repo = TrackRepository() + repo.startTrack() + } + + @Test + fun `computeStats returns null for single point`() { + repo.addPoint(pt(41.0, -71.0, ts = 0)) + assertNull(repo.computeStats()) + } + + @Test + fun `computeStats returns null when not recording and no points`() { + repo.stopTrack() + assertNull(repo.computeStats()) + } + + @Test + fun `haversineNm one degree of latitude is approximately 60 nm`() { + val d = TrackRepository.haversineNm(0.0, 0.0, 1.0, 0.0) + assertEquals(60.0, d, 0.5) + } + + @Test + fun `haversineNm zero distance returns zero`() { + assertEquals(0.0, TrackRepository.haversineNm(41.0, -71.0, 41.0, -71.0), 1e-9) + } + + @Test + fun `computeStats distance accumulates across legs`() { + repo.addPoint(pt(0.0, 0.0, ts = 0)) + repo.addPoint(pt(1.0, 0.0, ts = 3_600_000)) // ~60 nm north + repo.addPoint(pt(1.0, 1.0, ts = 7_200_000)) // ~60 nm east at 1° lat + val stats = repo.computeStats()!! + assertTrue("Expected ~120 nm, got ${stats.distanceNm}", stats.distanceNm > 100.0) + } + + @Test + fun `computeStats duration is last minus first timestamp`() { + repo.addPoint(pt(0.0, 0.0, ts = 1_000)) + repo.addPoint(pt(1.0, 0.0, ts = 3_601_000)) + val stats = repo.computeStats()!! + assertEquals(3_600_000L, stats.durationMs) + } + + @Test + fun `computeStats avgSog is mean of all point sogs`() { + repo.addPoint(pt(0.0, 0.0, sog = 4.0, ts = 0)) + repo.addPoint(pt(0.0, 0.1, sog = 6.0, ts = 1000)) + val stats = repo.computeStats()!! + assertEquals(5.0, stats.avgSogKnots, 0.001) + } + + @Test + fun `durationFormatted shows m_ss below one hour`() { + val stats = TrackStats(1.0, 65_000L, 5.0) // 1 min 5 sec + assertEquals("1:05", stats.durationFormatted) + } + + @Test + fun `durationFormatted shows h_mm_ss above one hour`() { + val stats = TrackStats(1.0, 3_661_000L, 5.0) // 1h 1m 1s + assertEquals("1:01:01", stats.durationFormatted) + } +} diff --git a/test-runner/src/test/kotlin/org/terst/nav/vessel/VesselRepositoryTest.kt b/test-runner/src/test/kotlin/org/terst/nav/vessel/VesselRepositoryTest.kt new file mode 100644 index 0000000..cec2a54 --- /dev/null +++ b/test-runner/src/test/kotlin/org/terst/nav/vessel/VesselRepositoryTest.kt @@ -0,0 +1,154 @@ +package org.terst.nav.vessel + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class VesselRepositoryTest { + + private lateinit var repo: VesselRepository + + @Before + fun setup() { + repo = VesselRepository() + } + + // --- Vessel tests --- + + @Test + fun `addVessel with blank id assigns a generated id`() { + val v = repo.addVessel(Vessel(id = "", name = "Perseverance")) + assertTrue(v.id.isNotBlank()) + } + + @Test + fun `addVessel with explicit id preserves id`() { + val v = repo.addVessel(Vessel(id = "abc123", name = "Perseverance")) + assertEquals("abc123", v.id) + } + + @Test + fun `getVessels returns all added vessels`() { + repo.addVessel(Vessel(id = "1", name = "Alpha")) + repo.addVessel(Vessel(id = "2", name = "Beta")) + assertEquals(2, repo.getVessels().size) + } + + @Test + fun `getVessel returns correct vessel by id`() { + repo.addVessel(Vessel(id = "1", name = "Alpha")) + repo.addVessel(Vessel(id = "2", name = "Beta")) + assertEquals("Beta", repo.getVessel("2")?.name) + } + + @Test + fun `updateVessel replaces vessel data`() { + repo.addVessel(Vessel(id = "1", name = "Alpha")) + val updated = repo.updateVessel(Vessel(id = "1", name = "Alpha II", loaMeters = 12.5)) + assertTrue(updated) + assertEquals("Alpha II", repo.getVessel("1")?.name) + assertEquals(12.5, repo.getVessel("1")?.loaMeters ?: 0.0, 0.001) + } + + @Test + fun `updateVessel returns false for unknown id`() { + assertFalse(repo.updateVessel(Vessel(id = "nope", name = "Ghost"))) + } + + @Test + fun `removeVessel removes by id`() { + repo.addVessel(Vessel(id = "1", name = "Alpha")) + repo.addVessel(Vessel(id = "2", name = "Beta")) + repo.removeVessel("1") + assertNull(repo.getVessel("1")) + assertEquals(1, repo.getVessels().size) + } + + @Test + fun `getOwnVessel returns only own vessel`() { + repo.addVessel(Vessel(id = "1", name = "Alpha", isOwnVessel = true)) + repo.addVessel(Vessel(id = "2", name = "Beta", isOwnVessel = false)) + assertEquals("Alpha", repo.getOwnVessel()?.name) + } + + @Test + fun `getOwnVessel returns null when none set`() { + repo.addVessel(Vessel(id = "1", name = "Alpha", isOwnVessel = false)) + assertNull(repo.getOwnVessel()) + } + + @Test + fun `addVessel upserts on duplicate id`() { + repo.addVessel(Vessel(id = "1", name = "Alpha")) + repo.addVessel(Vessel(id = "1", name = "Alpha Updated")) + assertEquals(1, repo.getVessels().size) + assertEquals("Alpha Updated", repo.getVessel("1")?.name) + } + + // --- Crew tests --- + + @Test + fun `addCrewMember with blank id assigns generated id`() { + val m = repo.addCrewMember(CrewMember(id = "", name = "Jane Doe")) + assertTrue(m.id.isNotBlank()) + } + + @Test + fun `getCrew returns all crew members`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice")) + repo.addCrewMember(CrewMember(id = "c2", name = "Bob")) + assertEquals(2, repo.getCrew().size) + } + + @Test + fun `getSkipper returns skipper crew member`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice", role = CrewRole.CREW)) + repo.addCrewMember(CrewMember(id = "c2", name = "Bob", role = CrewRole.SKIPPER)) + assertEquals("Bob", repo.getSkipper()?.name) + } + + @Test + fun `getSkipper returns null when no skipper`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice", role = CrewRole.CREW)) + assertNull(repo.getSkipper()) + } + + @Test + fun `updateCrewMember updates role and details`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice", role = CrewRole.CREW)) + val ok = repo.updateCrewMember( + CrewMember(id = "c1", name = "Alice", role = CrewRole.FIRST_MATE, emergencyContact = "Bob") + ) + assertTrue(ok) + assertEquals(CrewRole.FIRST_MATE, repo.getCrewMember("c1")?.role) + assertEquals("Bob", repo.getCrewMember("c1")?.emergencyContact) + } + + @Test + fun `removeCrewMember removes by id`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice")) + repo.addCrewMember(CrewMember(id = "c2", name = "Bob")) + repo.removeCrewMember("c1") + assertNull(repo.getCrewMember("c1")) + assertEquals(1, repo.getCrew().size) + } + + @Test + fun `getCrewByRole filters correctly`() { + repo.addCrewMember(CrewMember(id = "c1", name = "Alice", role = CrewRole.CREW)) + repo.addCrewMember(CrewMember(id = "c2", name = "Bob", role = CrewRole.WATCH_CAPTAIN)) + repo.addCrewMember(CrewMember(id = "c3", name = "Carol", role = CrewRole.CREW)) + val result = repo.getCrewByRole(CrewRole.CREW) + assertEquals(2, result.size) + assertTrue(result.all { it.role == CrewRole.CREW }) + } + + @Test + fun `vessel registry and crew are independent`() { + repo.addVessel(Vessel(id = "v1", name = "Alpha")) + repo.addCrewMember(CrewMember(id = "c1", name = "Alice")) + repo.removeVessel("v1") + assertEquals(1, repo.getCrew().size) + assertEquals(0, repo.getVessels().size) + } +} |
