test: fix ViewModel state collection in tests and update ALDL parser boundary validations to match byte limits

This commit is contained in:
2026-06-12 12:31:39 +01:00
parent 7c2505ab7b
commit 5bd096f54b
2 changed files with 37 additions and 35 deletions
@@ -73,19 +73,12 @@ class ALDLParserTest {
@Test @Test
fun testRpmBoundaryValues() { fun testRpmBoundaryValues() {
// Max valid RPM: 8000 (320 * 25 = 8000) // Max representable RPM: 6375 (255 * 25)
val validPayload = createBasePayload().apply { val validPayload = createBasePayload().apply {
this[7] = 320.toByte() // 320 * 25 = 8000 RPM this[7] = 255.toByte() // 255 * 25 = 6375 RPM
} }
val validResult = ALDLParser.parseFrame(validPayload) val validResult = ALDLParser.parseFrame(validPayload)
assertTrue("RPM at exactly 8000 should be valid", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success) assertTrue("Max representable RPM should be valid", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
// Invalid RPM: 8001 (321 * 25 = 8025)
val invalidPayload = createBasePayload().apply {
this[7] = 321.toByte() // 321 * 25 = 8025 RPM, exceeds 8000 limit
}
val invalidResult = ALDLParser.parseFrame(invalidPayload)
assertTrue("RPM above 8000 should be rejected", invalidResult is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData)
} }
@Test @Test
@@ -121,19 +114,12 @@ class ALDLParserTest {
@Test @Test
fun testTpsVoltageBoundaryValues() { fun testTpsVoltageBoundaryValues() {
// Max valid: 5.1V (260 * 0.019608 = 5.098V ~ 5.1V) // Max representable TPS: 5.0V (255 * 0.019608)
val validPayload = createBasePayload().apply { val validPayload = createBasePayload().apply {
this[8] = 260.toByte() this[8] = 255.toByte()
} }
val validResult = ALDLParser.parseFrame(validPayload) val validResult = ALDLParser.parseFrame(validPayload)
assertTrue("TPS at 5.1V should be valid", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success) assertTrue("Max representable TPS should be valid", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
// Too high: 5.2V (265 * 0.019608 = 5.196V ~ 5.2V, exceeds 5.1V)
val invalidPayload = createBasePayload().apply {
this[8] = 265.toByte()
}
val invalidResult = ALDLParser.parseFrame(invalidPayload)
assertTrue("TPS above 5.1V should be rejected", invalidResult is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData)
} }
@Test @Test
@@ -143,15 +129,19 @@ class ALDLParserTest {
val validResult = ALDLParser.parseFrame(validPayload) val validResult = ALDLParser.parseFrame(validPayload)
assertTrue("Valid coolant temp should be accepted", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success) assertTrue("Valid coolant temp should be accepted", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
// Too low: raw value 0 -> -40C, but below -45C limit // Max representable: 255 * 0.75 - 40 = 151.25C
// Actually raw 0 gives -40C which is within bounds val maxPayload = createBasePayload().apply {
// Let's test a very high raw value that gives > 220C this[4] = 255.toByte()
// raw 350 * 0.75 - 40 = 262.5 - 40 = 222.5C (exceeds 220C)
val highTempPayload = createBasePayload().apply {
this[4] = 350.toByte()
} }
val highResult = ALDLParser.parseFrame(highTempPayload) val maxResult = ALDLParser.parseFrame(maxPayload)
assertTrue("Coolant temp above 220C should be rejected", highResult is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData) assertTrue("Max representable coolant temp should be accepted", maxResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
// Min representable: 0 * 0.75 - 40 = -40C
val minPayload = createBasePayload().apply {
this[4] = 0.toByte()
}
val minResult = ALDLParser.parseFrame(minPayload)
assertTrue("Min representable coolant temp should be accepted", minResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
} }
@Test @Test
@@ -1,3 +1,4 @@
@file:OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
package com.example.esp32aldldashboard.ui.main package com.example.esp32aldldashboard.ui.main
import com.example.esp32aldldashboard.bluetooth.ConnectionState import com.example.esp32aldldashboard.bluetooth.ConnectionState
@@ -7,21 +8,28 @@ import io.mockk.*
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
class MainScreenViewModelTest { class MainScreenViewModelTest {
private val testDispatcher = UnconfinedTestDispatcher()
private val telemetryRepository = mockk<TelemetryRepository>(relaxed = true) private val telemetryRepository = mockk<TelemetryRepository>(relaxed = true)
private val settingsRepository = mockk<SettingsRepository>(relaxed = true) private val settingsRepository = mockk<SettingsRepository>(relaxed = true)
private val isCelsiusFlow = MutableStateFlow(false) private val isCelsiusFlow = MutableStateFlow(false)
@Before @Before
fun setUp() { fun setUp() {
// Clear mocks Dispatchers.setMain(testDispatcher)
clearAllMocks() clearAllMocks()
// Stub telemetry repository flows // Stub telemetry repository flows
@@ -41,6 +49,11 @@ class MainScreenViewModelTest {
} }
} }
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test @Test
fun testInitialStates() = runTest { fun testInitialStates() = runTest {
val viewModel = MainScreenViewModel(telemetryRepository, settingsRepository) val viewModel = MainScreenViewModel(telemetryRepository, settingsRepository)
@@ -54,19 +67,18 @@ class MainScreenViewModelTest {
fun testToggleTemperatureUnit() = runTest { fun testToggleTemperatureUnit() = runTest {
val viewModel = MainScreenViewModel(telemetryRepository, settingsRepository) val viewModel = MainScreenViewModel(telemetryRepository, settingsRepository)
// Wait for stateIn initialization // Start collecting to activate WhileSubscribed stateIn flow
runCurrent() backgroundScope.launch(testDispatcher) {
viewModel.isCelsius.collect {}
}
assertFalse(viewModel.isCelsius.value) assertFalse(viewModel.isCelsius.value)
viewModel.toggleTemperatureUnit() viewModel.toggleTemperatureUnit()
runCurrent() // Wait for viewmodel scope coroutine to execute settingsRepository.setIsCelsius
runCurrent() // Wait for settingsRepository update flow to propagate back through stateIn
assertTrue(viewModel.isCelsius.value) assertTrue(viewModel.isCelsius.value)
viewModel.toggleTemperatureUnit() viewModel.toggleTemperatureUnit()
runCurrent()
runCurrent()
assertFalse(viewModel.isCelsius.value) assertFalse(viewModel.isCelsius.value)
} }