mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 10:46:12 -04:00
Go multi-module: app-common, app-phone, app-wear
This commit is contained in:
@@ -1,351 +0,0 @@
|
||||
package eu.darken.capod.common.flow
|
||||
|
||||
import eu.darken.capod.common.collections.mutate
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.types.instanceOf
|
||||
import io.kotest.matchers.types.shouldBeInstanceOf
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelper.BaseTest
|
||||
import testhelper.coroutine.runBlockingTest2
|
||||
import testhelper.flow.test
|
||||
import java.io.IOException
|
||||
import java.lang.Thread.sleep
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class DynamicStateFlowTest : BaseTest() {
|
||||
|
||||
// Without an init value, there isn't a way to keep using the flow
|
||||
@Test
|
||||
fun `exceptions on initialization are rethrown`() {
|
||||
val testScope = TestCoroutineScope()
|
||||
val hotData = DynamicStateFlow<String>(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = Dispatchers.Unconfined,
|
||||
startValueProvider = { throw IOException() }
|
||||
)
|
||||
runBlocking {
|
||||
withTimeoutOrNull(500) {
|
||||
// This blocking scope gets the init exception as the first caller
|
||||
hotData.flow.firstOrNull()
|
||||
} shouldBe null
|
||||
}
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
testScope.uncaughtExceptions.single() shouldBe instanceOf(IOException::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `subscription doesn't end when no subscriber is collecting, mode Lazily`() {
|
||||
val testScope = TestCoroutineScope()
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> String>()
|
||||
coEvery { valueProvider.invoke(any()) } returns "Test"
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = Dispatchers.Unconfined,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
testScope.apply {
|
||||
runBlockingTest2(allowUncompleted = true) {
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
}
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `value updates`() {
|
||||
val testScope = TestCoroutineScope()
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> Long>()
|
||||
coEvery { valueProvider.invoke(any()) } returns 1
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
testCollector.silent = true
|
||||
|
||||
(1..16).forEach { _ ->
|
||||
thread {
|
||||
(1..200).forEach { _ ->
|
||||
sleep(10)
|
||||
hotData.updateAsync(
|
||||
onUpdate = { this + 1L },
|
||||
onError = { throw it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
testCollector.await { list, _ -> list.size == 3201 }
|
||||
testCollector.latestValues shouldBe (1..3201).toList()
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
}
|
||||
|
||||
data class TestData(
|
||||
val number: Long = 1
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `check multi threading value updates with more complex data`() {
|
||||
val testScope = TestCoroutineScope()
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> Map<String, TestData>>()
|
||||
coEvery { valueProvider.invoke(any()) } returns mapOf("data" to TestData())
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
testCollector.silent = true
|
||||
|
||||
(1..10).forEach { _ ->
|
||||
thread {
|
||||
(1..400).forEach { _ ->
|
||||
hotData.updateAsync {
|
||||
mutate {
|
||||
this["data"] = getValue("data").copy(
|
||||
number = getValue("data").number + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runBlocking {
|
||||
testCollector.await { list, _ -> list.size == 4001 }
|
||||
testCollector.latestValues.map { it.values.single().number } shouldBe (1L..4001L).toList()
|
||||
}
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only emit new values if they actually changed updates`() {
|
||||
val testScope = TestCoroutineScope()
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = { "1" },
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
testCollector.silent = true
|
||||
|
||||
hotData.updateAsync { "1" }
|
||||
hotData.updateAsync { "2" }
|
||||
hotData.updateAsync { "2" }
|
||||
hotData.updateAsync { "1" }
|
||||
|
||||
runBlocking {
|
||||
testCollector.await { list, _ -> list.size == 3 }
|
||||
testCollector.latestValues shouldBe listOf("1", "2", "1")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple subscribers share the flow`() {
|
||||
val testScope = TestCoroutineScope()
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> String>()
|
||||
coEvery { valueProvider.invoke(any()) } returns "Test"
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
testScope.runBlockingTest2(allowUncompleted = true) {
|
||||
val sub1 = hotData.flow.test(tag = "sub1", startOnScope = this)
|
||||
val sub2 = hotData.flow.test(tag = "sub2", startOnScope = this)
|
||||
val sub3 = hotData.flow.test(tag = "sub3", startOnScope = this)
|
||||
|
||||
hotData.updateAsync { "A" }
|
||||
hotData.updateAsync { "B" }
|
||||
hotData.updateAsync { "C" }
|
||||
|
||||
listOf(sub1, sub2, sub3).forEach {
|
||||
it.await { list, _ -> list.size == 4 }
|
||||
it.latestValues shouldBe listOf("Test", "A", "B", "C")
|
||||
it.cancel()
|
||||
}
|
||||
|
||||
hotData.flow.first() shouldBe "C"
|
||||
}
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `value is persisted between unsubscribes`() = runBlockingTest2(allowUncompleted = true) {
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> Long>()
|
||||
coEvery { valueProvider.invoke(any()) } returns 1
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = this,
|
||||
coroutineContext = this.coroutineContext,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
val testCollector1 = hotData.flow.test(tag = "collector1", startOnScope = this)
|
||||
testCollector1.silent = false
|
||||
|
||||
(1..10).forEach { _ ->
|
||||
hotData.updateAsync {
|
||||
this + 1L
|
||||
}
|
||||
}
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector1.await { list, _ -> list.size == 11 }
|
||||
testCollector1.latestValues shouldBe (1L..11L).toList()
|
||||
|
||||
testCollector1.cancel()
|
||||
testCollector1.awaitFinal()
|
||||
|
||||
val testCollector2 = hotData.flow.test(tag = "collector2", startOnScope = this)
|
||||
testCollector2.silent = false
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector2.cancel()
|
||||
testCollector2.awaitFinal()
|
||||
|
||||
testCollector2.latestValues shouldBe listOf(11L)
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocking update is actually blocking`() = runBlocking {
|
||||
val testScope = TestCoroutineScope()
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = testScope.coroutineContext,
|
||||
startValueProvider = {
|
||||
delay(2000)
|
||||
2
|
||||
},
|
||||
)
|
||||
|
||||
hotData.updateAsync {
|
||||
delay(2000)
|
||||
this + 1
|
||||
}
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
hotData.updateBlocking { this - 3 } shouldBe 0
|
||||
|
||||
testCollector.await { _, i -> i == 3 }
|
||||
testCollector.latestValues shouldBe listOf(2, 3, 0)
|
||||
|
||||
testCollector.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocking update rethrows error`() = runBlocking {
|
||||
val testScope = TestCoroutineScope()
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = testScope.coroutineContext,
|
||||
startValueProvider = {
|
||||
delay(2000)
|
||||
2
|
||||
},
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
shouldThrow<IOException> {
|
||||
hotData.updateBlocking { throw IOException("Surprise") } shouldBe 0
|
||||
}
|
||||
hotData.flow.first() shouldBe 2
|
||||
|
||||
hotData.updateBlocking { 3 } shouldBe 3
|
||||
hotData.flow.first() shouldBe 3
|
||||
|
||||
testScope.uncaughtExceptions.singleOrNull() shouldBe null
|
||||
|
||||
testCollector.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `async updates error handler`() {
|
||||
val testScope = TestCoroutineScope()
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = { 1 },
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
hotData.updateAsync { throw IOException("Surprise") }
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
testScope.uncaughtExceptions.single() shouldBe instanceOf(IOException::class)
|
||||
|
||||
testCollector.cancel()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `async updates rethrow errors on HotDataFlow scope if no error handler is set`() = runBlocking {
|
||||
val testScope = TestCoroutineScope()
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = { 1 },
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(startOnScope = testScope)
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
var thrownError: Exception? = null
|
||||
|
||||
hotData.updateAsync(
|
||||
onUpdate = { throw IOException("Surprise") },
|
||||
onError = { thrownError = it }
|
||||
)
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
thrownError!!.shouldBeInstanceOf<IOException>()
|
||||
testScope.uncaughtExceptions.singleOrNull() shouldBe null
|
||||
|
||||
testCollector.cancel()
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package eu.darken.capod.common.preferences
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.squareup.moshi.Moshi
|
||||
import eu.darken.capod.main.core.MonitorMode
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelper.BaseTest
|
||||
import testhelper.json.toComparableJson
|
||||
import testhelpers.preferences.MockSharedPreferences
|
||||
|
||||
class FlowPreferenceMoshiTest : BaseTest() {
|
||||
|
||||
private val mockPreferences = MockSharedPreferences()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TestGson(
|
||||
val string: String = "",
|
||||
val boolean: Boolean = true,
|
||||
val float: Float = 1.0f,
|
||||
val int: Int = 1,
|
||||
val long: Long = 1L
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `reading and writing using manual reader and writer`() = runBlockingTest {
|
||||
val testData1 = TestGson(string = "teststring")
|
||||
val testData2 = TestGson(string = "update")
|
||||
val moshi = Moshi.Builder().build()
|
||||
FlowPreference<TestGson?>(
|
||||
preferences = mockPreferences,
|
||||
key = "testKey",
|
||||
rawReader = moshiReader(moshi, testData1),
|
||||
rawWriter = moshiWriter(moshi)
|
||||
).apply {
|
||||
value shouldBe testData1
|
||||
flow.first() shouldBe testData1
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
|
||||
update {
|
||||
it shouldBe testData1
|
||||
it!!.copy(string = "update")
|
||||
}
|
||||
|
||||
value shouldBe testData2
|
||||
flow.first() shouldBe testData2
|
||||
(mockPreferences.dataMapPeek.values.first() as String).toComparableJson() shouldBe """
|
||||
{
|
||||
"string":"update",
|
||||
"boolean":true,
|
||||
"float":1.0,
|
||||
"int":1,
|
||||
"long":1
|
||||
}
|
||||
""".toComparableJson()
|
||||
|
||||
update {
|
||||
it shouldBe testData2
|
||||
null
|
||||
}
|
||||
value shouldBe testData1
|
||||
flow.first() shouldBe testData1
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reading and writing using autocreated reader and writer`() = runBlockingTest {
|
||||
val testData1 = TestGson(string = "teststring")
|
||||
val testData2 = TestGson(string = "update")
|
||||
val moshi = Moshi.Builder().build()
|
||||
|
||||
mockPreferences.createFlowPreference<TestGson?>(
|
||||
key = "testKey",
|
||||
defaultValue = testData1,
|
||||
moshi = moshi
|
||||
).apply {
|
||||
value shouldBe testData1
|
||||
flow.first() shouldBe testData1
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
|
||||
update {
|
||||
it shouldBe testData1
|
||||
it!!.copy(string = "update")
|
||||
}
|
||||
|
||||
value shouldBe testData2
|
||||
flow.first() shouldBe testData2
|
||||
(mockPreferences.dataMapPeek.values.first() as String).toComparableJson() shouldBe """
|
||||
{
|
||||
"string":"update",
|
||||
"boolean":true,
|
||||
"float":1.0,
|
||||
"int":1,
|
||||
"long":1
|
||||
}
|
||||
""".toComparableJson()
|
||||
|
||||
update {
|
||||
it shouldBe testData2
|
||||
null
|
||||
}
|
||||
value shouldBe testData1
|
||||
flow.first() shouldBe testData1
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `enum serialization`() = runBlockingTest {
|
||||
val moshi = Moshi.Builder().build()
|
||||
val monitorMode = mockPreferences.createFlowPreference(
|
||||
"core.monitor.mode",
|
||||
MonitorMode.AUTOMATIC,
|
||||
moshi
|
||||
)
|
||||
|
||||
monitorMode.value shouldBe MonitorMode.AUTOMATIC
|
||||
monitorMode.update { MonitorMode.MANUAL }
|
||||
monitorMode.value shouldBe MonitorMode.MANUAL
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package eu.darken.capod.common.preferences
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelper.BaseTest
|
||||
import testhelpers.preferences.MockSharedPreferences
|
||||
|
||||
class FlowPreferenceTest : BaseTest() {
|
||||
|
||||
private val mockPreferences = MockSharedPreferences()
|
||||
|
||||
@Test
|
||||
fun `reading and writing strings`() = runBlockingTest {
|
||||
mockPreferences.createFlowPreference<String?>(
|
||||
key = "testKey",
|
||||
defaultValue = "default"
|
||||
).apply {
|
||||
value shouldBe "default"
|
||||
flow.first() shouldBe "default"
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
|
||||
update {
|
||||
it shouldBe "default"
|
||||
"newvalue"
|
||||
}
|
||||
|
||||
value shouldBe "newvalue"
|
||||
flow.first() shouldBe "newvalue"
|
||||
mockPreferences.dataMapPeek.values.first() shouldBe "newvalue"
|
||||
|
||||
update {
|
||||
it shouldBe "newvalue"
|
||||
null
|
||||
}
|
||||
value shouldBe "default"
|
||||
flow.first() shouldBe "default"
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reading and writing boolean`() = runBlockingTest {
|
||||
mockPreferences.createFlowPreference<Boolean?>(
|
||||
key = "testKey",
|
||||
defaultValue = true
|
||||
).apply {
|
||||
value shouldBe true
|
||||
flow.first() shouldBe true
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
|
||||
update {
|
||||
it shouldBe true
|
||||
false
|
||||
}
|
||||
|
||||
value shouldBe false
|
||||
flow.first() shouldBe false
|
||||
mockPreferences.dataMapPeek.values.first() shouldBe false
|
||||
|
||||
update {
|
||||
it shouldBe false
|
||||
null
|
||||
}
|
||||
value shouldBe true
|
||||
flow.first() shouldBe true
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reading and writing long`() = runBlockingTest {
|
||||
mockPreferences.createFlowPreference<Long?>(
|
||||
key = "testKey",
|
||||
defaultValue = 9000L
|
||||
).apply {
|
||||
value shouldBe 9000L
|
||||
flow.first() shouldBe 9000L
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
|
||||
update {
|
||||
it shouldBe 9000L
|
||||
9001L
|
||||
}
|
||||
|
||||
value shouldBe 9001L
|
||||
flow.first() shouldBe 9001L
|
||||
mockPreferences.dataMapPeek.values.first() shouldBe 9001L
|
||||
|
||||
update {
|
||||
it shouldBe 9001L
|
||||
null
|
||||
}
|
||||
value shouldBe 9000L
|
||||
flow.first() shouldBe 9000L
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reading and writing integer`() = runBlockingTest {
|
||||
mockPreferences.createFlowPreference<Long?>(
|
||||
key = "testKey",
|
||||
defaultValue = 123
|
||||
).apply {
|
||||
value shouldBe 123
|
||||
flow.first() shouldBe 123
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
|
||||
update {
|
||||
it shouldBe 123
|
||||
44
|
||||
}
|
||||
|
||||
value shouldBe 44
|
||||
flow.first() shouldBe 44
|
||||
mockPreferences.dataMapPeek.values.first() shouldBe 44
|
||||
|
||||
update {
|
||||
it shouldBe 44
|
||||
null
|
||||
}
|
||||
value shouldBe 123
|
||||
flow.first() shouldBe 123
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reading and writing float`() = runBlockingTest {
|
||||
mockPreferences.createFlowPreference<Float?>(
|
||||
key = "testKey",
|
||||
defaultValue = 3.6f
|
||||
).apply {
|
||||
value shouldBe 3.6f
|
||||
flow.first() shouldBe 3.6f
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
|
||||
update {
|
||||
it shouldBe 3.6f
|
||||
15000f
|
||||
}
|
||||
|
||||
value shouldBe 15000f
|
||||
flow.first() shouldBe 15000f
|
||||
mockPreferences.dataMapPeek.values.first() shouldBe 15000f
|
||||
|
||||
update {
|
||||
it shouldBe 15000f
|
||||
null
|
||||
}
|
||||
value shouldBe 3.6f
|
||||
flow.first() shouldBe 3.6f
|
||||
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||
import eu.darken.capod.pods.core.apple.misc.UnknownAppleDevice
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.types.instanceOf
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsFactoryTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `create AirPodsGen1`() = runBlockingTest {
|
||||
create<DualAirPods>("07 19 01 02 20 55 AA 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") {
|
||||
this shouldBe instanceOf<AirPodsGen1>()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create AirPodsPro`() = runBlockingTest {
|
||||
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
||||
this shouldBe instanceOf<AirPodsPro>()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unknown AppleDevice`() = runBlockingTest {
|
||||
create<ApplePods>("07 19 01 FF FF 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
||||
this shouldBe instanceOf<UnknownAppleDevice>()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid data`() = runBlockingTest {
|
||||
create<PodDevice?>("abcd") {
|
||||
this shouldBe null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import dagger.Component
|
||||
import eu.darken.capod.common.SystemClockWrap
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkObject
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import testhelper.BaseTest
|
||||
import javax.inject.Singleton
|
||||
|
||||
abstract class BaseAirPodsTest : BaseTest() {
|
||||
@Singleton
|
||||
@Component(modules = [AppleFactoryModule::class])
|
||||
interface AppleFactoryTestComponent {
|
||||
|
||||
val appleFactory: AppleFactory
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(): AppleFactoryTestComponent
|
||||
}
|
||||
}
|
||||
|
||||
private val baseBleScanResult = BleScanResult(
|
||||
address = "77:49:4C:D8:25:0C",
|
||||
rssi = -66,
|
||||
generatedAtNanos = 136136027721826,
|
||||
manufacturerSpecificData = emptyMap()
|
||||
)
|
||||
|
||||
val factory: AppleFactory = DaggerBaseAirPodsTest_AppleFactoryTestComponent.factory().create().appleFactory
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
mockkObject(SystemClockWrap)
|
||||
every { SystemClockWrap.elapsedRealtimeNanos } returns 1000L
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : PodDevice?> create(hex: String, block: T.() -> Unit) {
|
||||
val trimmed = hex
|
||||
.replace(" ", "")
|
||||
.replace(">", "")
|
||||
.replace("<", "")
|
||||
require(trimmed.length % 2 == 0) { "Not a HEX string" }
|
||||
val bytes = trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
val result = mockData(bytes)
|
||||
block.invoke(factory.create(result) as T)
|
||||
}
|
||||
|
||||
fun mockData(hex: String): BleScanResult {
|
||||
val trimmed = hex
|
||||
.replace(" ", "")
|
||||
.replace(">", "")
|
||||
.replace("<", "")
|
||||
require(trimmed.length % 2 == 0) { "Not a HEX string" }
|
||||
val bytes = trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||
return mockData(bytes)
|
||||
}
|
||||
|
||||
fun mockData(data: ByteArray): BleScanResult = baseBleScanResult.copy(
|
||||
manufacturerSpecificData = mutableMapOf<Int, ByteArray>().apply {
|
||||
this[ContinuityProtocol.APPLE_COMPANY_IDENTIFIER] = data
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BasicSingleApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `test mapping`() = runBlockingTest {
|
||||
create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0520.toUShort()
|
||||
rawStatus shouldBe 0x00.toUByte()
|
||||
rawPodsBattery shouldBe 0xF5.toUByte()
|
||||
rawFlags shouldBe 0x0.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x01.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test battery headset percent`() = runBlockingTest {
|
||||
create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DualApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `test bit mapping`() = runBlockingTest {
|
||||
create<DualAirPods>("07 19 01 0E 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0e20.toUShort()
|
||||
rawStatus shouldBe 0x54.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0xB.toUShort()
|
||||
rawCaseBattery shouldBe 0x5.toUShort()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - active microphone`() = runBlockingTest {
|
||||
create<DualAirPods>("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00101011
|
||||
// --^-----
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
}
|
||||
create<DualAirPods>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00001011
|
||||
// --^-----
|
||||
isLeftPodMicrophone shouldBe false
|
||||
isRightPodMicrophone shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - left pod ear status`() = runBlockingTest {
|
||||
// Left Pod primary
|
||||
create<DualAirPods>("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00100010
|
||||
// 765432¹0
|
||||
isLeftPodInEar shouldBe true
|
||||
}
|
||||
create<DualAirPods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00100000
|
||||
// 765432¹0
|
||||
isLeftPodInEar shouldBe false
|
||||
}
|
||||
|
||||
// Right Pod is primary
|
||||
create<DualAirPods>("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00001001
|
||||
// 7654³210
|
||||
isLeftPodInEar shouldBe true
|
||||
}
|
||||
create<DualAirPods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00000001
|
||||
// 7654³210
|
||||
isLeftPodInEar shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - right pod ear status`() = runBlockingTest {
|
||||
// Left Pod primary
|
||||
create<DualAirPods>("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00101001
|
||||
// 7654³210
|
||||
isRightPodInEar shouldBe true
|
||||
}
|
||||
create<DualAirPods>("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00100001
|
||||
// 7654³210
|
||||
isRightPodInEar shouldBe false
|
||||
}
|
||||
|
||||
// Right Pod is primary
|
||||
create<DualAirPods>("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00000011
|
||||
// 765432¹0
|
||||
isRightPodInEar shouldBe true
|
||||
}
|
||||
create<DualAirPods>("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00000001
|
||||
// 765432¹0
|
||||
isRightPodInEar shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - battery status`() = runBlockingTest {
|
||||
// Right Pod is primary
|
||||
create<DualAirPods>("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
|
||||
// 88 10001000
|
||||
batteryLeftPodPercent shouldBe 0.9f
|
||||
batteryRightPodPercent shouldBe 0.8f
|
||||
}
|
||||
// Left Pod primary
|
||||
create<DualAirPods>("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
|
||||
// F8 11111000
|
||||
batteryLeftPodPercent shouldBe 0.9f
|
||||
batteryRightPodPercent shouldBe 0.8f
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - pod charging`() = runBlockingTest {
|
||||
/**
|
||||
* Right pod is charging
|
||||
*/
|
||||
// This is the left
|
||||
create<DualAirPods>("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") {
|
||||
// 1001 0100
|
||||
isLeftPodCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
}
|
||||
// This is the right
|
||||
create<DualAirPods>("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") {
|
||||
// 1010 0100
|
||||
isLeftPodCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
}
|
||||
|
||||
/**
|
||||
* Left pod is charging
|
||||
*/
|
||||
// This is the left
|
||||
create<DualAirPods>("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") {
|
||||
// 1001 0100
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe false
|
||||
}
|
||||
// This is the right
|
||||
create<DualAirPods>("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") {
|
||||
// 1010 0100
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe false
|
||||
}
|
||||
|
||||
// Both charging
|
||||
create<DualAirPods>("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") {
|
||||
// 1011 0100
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
}
|
||||
// Both not charging
|
||||
create<DualAirPods>("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") {
|
||||
// 1000 1111
|
||||
isLeftPodCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - case charging`() = runBlockingTest {
|
||||
create<DualAirPods>("07 19 01 0E 20 75 99 >B4< 31 00 05 77 C8 BA 0C 4E 1F BE AD 70 C5 40 71 D2 E9 17 A2") {
|
||||
// 0011 0011
|
||||
isCaseCharging shouldBe false
|
||||
}
|
||||
create<DualAirPods>("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") {
|
||||
// 0101 0011
|
||||
isCaseCharging shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - case lid test`() = runBlockingTest {
|
||||
// Lid open
|
||||
create<DualAirPods>("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") {
|
||||
// 31 0011 0001
|
||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
||||
}
|
||||
// Lid open, left pod in case
|
||||
create<DualAirPods>("07 19 01 0E 20 51 9A 93 >31< 00 00 95 D0 A5 D7 E3 F4 F1 38 38 99 61 3B 57 95 37 B7") {
|
||||
// 31 0011 0001
|
||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
||||
}
|
||||
// Lid open, left pod in case
|
||||
create<DualAirPods>("07 19 01 0E 20 71 A9 92 31 00 00 DB 48 7F 32 8C CE 80 6F D9 27 98 D6 76 45 9D 62") {
|
||||
// 31 0011 0001
|
||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
||||
}
|
||||
|
||||
|
||||
// Lid just closed
|
||||
create<DualAirPods>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") {
|
||||
// 39 0011 1001
|
||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
||||
}
|
||||
// Lid closed
|
||||
create<DualAirPods>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") {
|
||||
// 38 0011 1000
|
||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
||||
}
|
||||
// Lid closed, right pod in case
|
||||
create<DualAirPods>("07 19 01 0E 20 51 9A 93 >38< 00 00 3A D8 85 76 B0 91 48 31 DA FF 6C 4A 2B C2 67 F4") {
|
||||
// 38 0011 1000
|
||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
||||
}
|
||||
// Lid closed, left pod in case
|
||||
create<DualAirPods>("07 19 01 0E 20 71 A9 92 38 00 00 44 91 C4 8B 85 98 DD 55 4E 6A CA BC B5 CA 8D 37") {
|
||||
// 38 0011 1000
|
||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - connection state`() = runBlockingTest {
|
||||
// Disconnected
|
||||
create<DualAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >00< 62 D4 BB F1 A7 F8 64 98 D2 C8 BD 7B 3A EF 2E 15") {
|
||||
// 31 0011 0001
|
||||
state shouldBe DualAirPods.ConnectionState.DISCONNECTED
|
||||
}
|
||||
// Connected idle
|
||||
create<DualAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >04< 1D 69 69 9C C2 51 F3 1F BF 6E 45 DA 90 4A A3 E3") {
|
||||
// 39 0011 1001
|
||||
state shouldBe DualAirPods.ConnectionState.IDLE
|
||||
}
|
||||
// Connected and playing music
|
||||
create<DualAirPods>("07 19 01 0E 20 2B A9 8F 01 00 >05< 14 F7 CB 49 9F D3 B3 22 77 D2 22 F1 74 8C AC A6") {
|
||||
// 38 0011 1000
|
||||
state shouldBe DualAirPods.ConnectionState.MUSIC
|
||||
}
|
||||
// Connected and call active
|
||||
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >06< 0F 4B 43 25 E0 4A 73 63 14 22 C2 3C 89 13 BD 97") {
|
||||
// 38 0011 1000
|
||||
state shouldBe DualAirPods.ConnectionState.CALL
|
||||
}
|
||||
// Connected and call active
|
||||
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >07< E7 DF 76 44 85 B5 30 F4 95 14 02 DC A1 A4 8A 09") {
|
||||
// 38 0011 1000
|
||||
state shouldBe DualAirPods.ConnectionState.RINGING
|
||||
}
|
||||
// Switching?
|
||||
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
||||
// 38 0011 1000
|
||||
state shouldBe DualAirPods.ConnectionState.HANGING_UP
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsMax
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SingleApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `default bit mapping Max`() = runBlockingTest {
|
||||
create<SingleApplePods>("07 19 01 0A 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||
rawStatus shouldBe 0x62.toUByte()
|
||||
rawPodsBattery shouldBe 0x04.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test values based on AirPodMax`() = runBlockingTest {
|
||||
create<AirPodsMax>("07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E") {
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isHeadphonesBeingWorn shouldBe true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsGen1Test : BaseAirPodsTest() {
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/39#issuecomment-557664269
|
||||
@Test
|
||||
fun `fake airpods`() = runBlockingTest {
|
||||
create<AirPodsGen1>("07 19 01 02 20 55 AF 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0220.toUShort()
|
||||
rawStatus shouldBe 0x55.toUByte()
|
||||
rawPodsBattery shouldBe 0xAF.toUByte()
|
||||
rawFlags shouldBe 0x5.toUShort()
|
||||
rawCaseBattery shouldBe 0x6.toUShort()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe null
|
||||
|
||||
isCaseCharging shouldBe true
|
||||
isLeftPodCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe false
|
||||
batteryCasePercent shouldBe 0.6f
|
||||
|
||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
||||
|
||||
state shouldBe DualAirPods.ConnectionState.DISCONNECTED
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsGen2Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `random Neighbor AirPodsGen2`() = runBlockingTest {
|
||||
create<AirPodsGen2>("07 19 01 0F 20 02 F9 8F 01 00 05 F2 7E 14 E0 54 0A 53 69 5B 7D F2 15 1F D7 B1 12") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0F20.toUShort()
|
||||
rawStatus shouldBe 0x02.toUByte()
|
||||
rawPodsBattery shouldBe 0xF9.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x05.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe null
|
||||
batteryRightPodPercent shouldBe 0.9f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe true
|
||||
batteryCasePercent shouldBe null
|
||||
|
||||
caseLidState shouldBe DualAirPods.LidState.NOT_IN_CASE
|
||||
|
||||
state shouldBe DualAirPods.ConnectionState.MUSIC
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsGen3Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `AirPods Gen3`() = runBlockingTest {
|
||||
create<AirPodsGen3>("07 19 01 13 20 75 AA B9 31 00 04 67 9A 57 DF BE F6 90 52 B0 04 1F 8D 89 DA F4 9E") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1320.toUShort()
|
||||
rawStatus shouldBe 0x75.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0xB.toUShort()
|
||||
rawCaseBattery shouldBe 0x9.toUShort()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe false
|
||||
batteryCasePercent shouldBe 0.9f
|
||||
|
||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
||||
|
||||
state shouldBe DualAirPods.ConnectionState.IDLE
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `random guy at bus stop`() = runBlockingTest {
|
||||
create<AirPodsGen3>("07 19 01 13 20 2B 88 8F 01 00 08 E2 0E 84 37 C5 98 16 D4 B7 37 ED 23 8B 08 EA A1") {
|
||||
batteryLeftPodPercent shouldBe 0.8f
|
||||
batteryRightPodPercent shouldBe 0.8f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
|
||||
isLeftPodInEar shouldBe true
|
||||
isRightPodInEar shouldBe true
|
||||
batteryCasePercent shouldBe null
|
||||
|
||||
caseLidState shouldBe DualAirPods.LidState.NOT_IN_CASE
|
||||
|
||||
state shouldBe DualAirPods.ConnectionState.UNKNOWN
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/124
|
||||
@Test
|
||||
fun `default AirPods Max`() = runBlockingTest {
|
||||
create<AirPodsMax>("07 19 01 0A 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||
rawStatus shouldBe 0x62.toUByte()
|
||||
rawPodsBattery shouldBe 0x04.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isHeadphonesBeingWorn shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/124
|
||||
@Test
|
||||
fun `default AirPods Max flipped values`() = runBlockingTest {
|
||||
create<AirPodsMax>("07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||
rawStatus shouldBe 0x02.toUByte()
|
||||
rawPodsBattery shouldBe 0x05.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x04.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x44.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
isHeadphonesBeingWorn shouldBe true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsProTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `test AirPods Pro - default changed and in case`() = runBlockingTest {
|
||||
create<AirPodsPro>("07 19 01 0E 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0e20.toUShort()
|
||||
rawStatus shouldBe 0x54.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0xB.toUShort()
|
||||
rawCaseBattery shouldBe 0x5.toUShort()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe true
|
||||
batteryCasePercent shouldBe 0.5f
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPods from my downstairs neighbour`() = runBlockingTest {
|
||||
create<AirPodsPro>("07 19 01 0E 20 00 F3 8F 02 00 04 79 C6 3F F9 C3 15 D9 11 A1 3C B1 58 66 B9 8B 67") {
|
||||
isLeftPodMicrophone shouldBe false
|
||||
isRightPodMicrophone shouldBe true
|
||||
|
||||
batteryLeftPodPercent shouldBe null
|
||||
batteryRightPodPercent shouldBe 0.3f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
}
|
||||
}
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/34#issuecomment-565894487
|
||||
@Test
|
||||
fun `various AirPods Pro messages`() = runBlockingTest {
|
||||
create<AirPodsPro>("0719010e202b668f01000500000000000000000000000000000000") {
|
||||
batteryLeftPodPercent shouldBe 0.6f
|
||||
batteryRightPodPercent shouldBe 0.6f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
}
|
||||
|
||||
create<AirPodsPro>("0719010e202b668f01000500000000000000000000000000000000") {
|
||||
batteryLeftPodPercent shouldBe 0.6f
|
||||
batteryRightPodPercent shouldBe 0.6f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
}
|
||||
|
||||
create<AirPodsPro>("0719010e202b668f01000400000000000000000000000000000000") {
|
||||
batteryLeftPodPercent shouldBe 0.6f
|
||||
batteryRightPodPercent shouldBe 0.6f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
}
|
||||
|
||||
create<AirPodsPro>("0719010e200b668f01000500000000000000000000000000000000") {
|
||||
batteryLeftPodPercent shouldBe 0.6f
|
||||
batteryRightPodPercent shouldBe 0.6f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
}
|
||||
|
||||
create<AirPodsPro>("0719010e2003668f01000500000000000000000000000000000000") {
|
||||
batteryLeftPodPercent shouldBe 0.6f
|
||||
batteryRightPodPercent shouldBe 0.6f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
}
|
||||
|
||||
create<AirPodsPro>("0719010e2001668f01000500000000000000000000000000000000") {
|
||||
batteryLeftPodPercent shouldBe 0.6f
|
||||
batteryRightPodPercent shouldBe 0.6f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
}
|
||||
|
||||
create<AirPodsPro>("0719010e2009668f01000500000000000000000000000000000000") {
|
||||
batteryLeftPodPercent shouldBe 0.6f
|
||||
batteryRightPodPercent shouldBe 0.6f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
}
|
||||
|
||||
create<AirPodsPro>("0719010e2053669653000500000000000000000000000000000000") {
|
||||
batteryLeftPodPercent shouldBe 0.6f
|
||||
batteryRightPodPercent shouldBe 0.6f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe 0.6f
|
||||
}
|
||||
|
||||
create<AirPodsPro>("0719010e203366a602000500000000000000000000000000000000") {
|
||||
batteryLeftPodPercent shouldBe 0.6f
|
||||
batteryRightPodPercent shouldBe 0.6f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe 0.6f
|
||||
}
|
||||
|
||||
create<AirPodsPro>("0719010e202b768f02000500000000000000000000000000000000") {
|
||||
batteryLeftPodPercent shouldBe 0.6f
|
||||
batteryRightPodPercent shouldBe 0.7f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe 0.6f
|
||||
}
|
||||
}
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/39#issuecomment-557664269
|
||||
@Test
|
||||
fun `fake airpods`() = runBlockingTest {
|
||||
create<AirPodsGen1>("071901022055AA563100006FE4DF10AF106081033B76D9C7112288") {
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe 0.6f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsFlexText : BaseAirPodsTest() {
|
||||
|
||||
// Raw data from https://github.com/adolfintel/OpenPods/issues/105
|
||||
@Test
|
||||
fun `default BeatsFlex`() = runBlockingTest {
|
||||
create<BeatsFlex>("07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x1020.toUShort()
|
||||
rawStatus shouldBe 0x0A.toUByte()
|
||||
rawPodsBattery shouldBe 0xF4.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x00.toUByte()
|
||||
rawDeviceColor shouldBe 0x01.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `random neighbour`() = runBlockingTest {
|
||||
create<BeatsFlex>("07 19 01 10 20 0A F6 8F 02 4F 00 95 68 94 9E 99 D6 90 F4 5E 68 3C 58 21 68 9F 0D") {
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.6f
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsSolo3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@Test
|
||||
fun `default BeatsSolo3`() = runBlockingTest {
|
||||
create<BeatsSolo3>("07 19 01 06 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0620.toUShort()
|
||||
rawStatus shouldBe 0x62.toUByte()
|
||||
rawPodsBattery shouldBe 0x04.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsStudio3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@Test
|
||||
fun `default BeatsStudio3`() = runBlockingTest {
|
||||
create<BeatsStudio3>("07 19 01 09 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0920.toUShort()
|
||||
rawStatus shouldBe 0x62.toUByte()
|
||||
rawPodsBattery shouldBe 0x04.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsXTest : BaseAirPodsTest() {
|
||||
|
||||
// Raw data from https://github.com/adolfintel/OpenPods/issues/105
|
||||
@Test
|
||||
fun `default BeatsX`() = runBlockingTest {
|
||||
create<BeatsX>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0520.toUShort()
|
||||
rawStatus shouldBe 0x00.toUByte()
|
||||
rawPodsBattery shouldBe 0xF5.toUByte()
|
||||
rawFlags shouldBe 0x0.toUShort()
|
||||
rawCaseBattery shouldBe 0xF.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x01.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PowerBeats3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@Test
|
||||
fun `default PowerBeats3`() = runBlockingTest {
|
||||
create<PowerBeats3>("07 19 01 03 20 62 04 80 01 0F 40 0D 70 50 16 F2 40 83 16 BF 10 16 34 9B 74 84 E8") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0320.toUShort()
|
||||
rawStatus shouldBe 0x62.toUByte()
|
||||
rawPodsBattery shouldBe 0x04.toUByte()
|
||||
rawFlags shouldBe 0x8.toUShort()
|
||||
rawCaseBattery shouldBe 0x0.toUShort()
|
||||
rawCaseLidState shouldBe 0x01.toUByte()
|
||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||
rawSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PowerBeatsProTest : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@Test
|
||||
fun `test PowerBeatsPro`() = runBlockingTest {
|
||||
create<PowerBeatsPro>("07 19 01 0B 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0B20.toUShort()
|
||||
rawStatus shouldBe 0x54.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0xB.toUShort()
|
||||
rawCaseBattery shouldBe 0x5.toUShort()
|
||||
rawCaseLidState shouldBe 0x31.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe true
|
||||
batteryCasePercent shouldBe 0.5f
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class Twsi99999Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `charging in box`() = runBlockingTest {
|
||||
create<Twsi99999>("07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0220.toUShort()
|
||||
rawStatus shouldBe 0x71.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x3.toUShort()
|
||||
rawCaseBattery shouldBe 0x7.toUShort()
|
||||
rawCaseLidState shouldBe 0x32.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
batteryCasePercent shouldBe 0.7f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class VarunrAirPodsProTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `guessed data`() = runBlockingTest {
|
||||
create<VarunrAirPodsPro>("07 13 01 0E 20 71 AA 37 36 00 10 00 FF 64 FF 00 00 00 00 00 00") {
|
||||
rawPrefix shouldBe 0x01.toUByte()
|
||||
rawDeviceModel shouldBe 0x0E20.toUShort()
|
||||
rawStatus shouldBe 0x71.toUByte()
|
||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||
rawFlags shouldBe 0x3.toUShort()
|
||||
rawCaseBattery shouldBe 0x7.toUShort()
|
||||
rawCaseLidState shouldBe 0x36.toUByte()
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
batteryCasePercent shouldBe 0.7f
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package testhelper
|
||||
|
||||
import eu.darken.capod.common.debug.logging.Logging
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import io.mockk.unmockkAll
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import testhelpers.logging.JUnitLogger
|
||||
|
||||
|
||||
open class BaseTest {
|
||||
init {
|
||||
Logging.clearAll()
|
||||
Logging.install(JUnitLogger())
|
||||
testClassName = this.javaClass.simpleName
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var testClassName: String? = null
|
||||
|
||||
@JvmStatic
|
||||
@AfterAll
|
||||
fun onTestClassFinished() {
|
||||
unmockkAll()
|
||||
log(testClassName!!, VERBOSE) { "onTestClassFinished()" }
|
||||
Logging.clearAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package testhelper.coroutine
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class CoroutinesTestExtension(
|
||||
private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
|
||||
) : BeforeEachCallback, AfterEachCallback, TestCoroutineScope by TestCoroutineScope(dispatcher) {
|
||||
|
||||
override fun beforeEach(context: ExtensionContext?) {
|
||||
Dispatchers.setMain(dispatcher)
|
||||
}
|
||||
|
||||
override fun afterEach(context: ExtensionContext?) {
|
||||
cleanupTestCoroutines()
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package testhelper.coroutine
|
||||
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class TestDispatcherProvider(private val context: CoroutineContext? = null) : DispatcherProvider {
|
||||
override val Default: CoroutineContext
|
||||
get() = context ?: Dispatchers.Unconfined
|
||||
override val Main: CoroutineContext
|
||||
get() = context ?: Dispatchers.Unconfined
|
||||
override val MainImmediate: CoroutineContext
|
||||
get() = context ?: Dispatchers.Unconfined
|
||||
override val Unconfined: CoroutineContext
|
||||
get() = context ?: Dispatchers.Unconfined
|
||||
override val IO: CoroutineContext
|
||||
get() = context ?: Dispatchers.Unconfined
|
||||
}
|
||||
|
||||
fun CoroutineScope.asDispatcherProvider() = this.coroutineContext.asDispatcherProvider()
|
||||
|
||||
fun CoroutineContext.asDispatcherProvider() = TestDispatcherProvider(context = this)
|
||||
@@ -1,49 +0,0 @@
|
||||
package testhelper.coroutine
|
||||
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
import kotlinx.coroutines.test.UncompletedCoroutinesError
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
/**
|
||||
* If you have a test that uses a coroutine that never stops, you may use this.
|
||||
*/
|
||||
|
||||
@ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
|
||||
fun TestCoroutineScope.runBlockingTest2(
|
||||
allowUncompleted: Boolean = false,
|
||||
block: suspend TestCoroutineScope.() -> Unit
|
||||
): Unit = runBlockingTest2(
|
||||
allowUncompleted = allowUncompleted,
|
||||
context = coroutineContext,
|
||||
testBody = block
|
||||
)
|
||||
|
||||
fun runBlockingTest2(
|
||||
allowUncompleted: Boolean = false,
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
testBody: suspend TestCoroutineScope.() -> Unit
|
||||
) {
|
||||
try {
|
||||
runBlocking {
|
||||
try {
|
||||
runBlockingTest(
|
||||
context = context,
|
||||
testBody = testBody
|
||||
)
|
||||
} catch (e: UncompletedCoroutinesError) {
|
||||
if (!allowUncompleted) throw e
|
||||
else log { "Ignoring active job." }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (!allowUncompleted || (e.message != "This job has not completed yet")) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package testhelper.flow
|
||||
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.test.TestCoroutineScope
|
||||
|
||||
fun <T> Flow<T>.test(
|
||||
tag: String? = null,
|
||||
startOnScope: CoroutineScope = TestCoroutineScope()
|
||||
): TestCollector<T> = createTest(tag ?: "FlowTest").start(scope = startOnScope)
|
||||
|
||||
fun <T> Flow<T>.createTest(
|
||||
tag: String? = null
|
||||
): TestCollector<T> = TestCollector(this, tag ?: "FlowTest")
|
||||
|
||||
class TestCollector<T>(
|
||||
private val flow: Flow<T>,
|
||||
private val tag: String
|
||||
|
||||
) {
|
||||
private var error: Throwable? = null
|
||||
private lateinit var job: Job
|
||||
private val cache = MutableSharedFlow<T>(
|
||||
replay = Int.MAX_VALUE,
|
||||
extraBufferCapacity = Int.MAX_VALUE,
|
||||
onBufferOverflow = BufferOverflow.SUSPEND
|
||||
)
|
||||
private var latestInternal: T? = null
|
||||
private val collectedValuesMutex = Mutex()
|
||||
private val collectedValues = mutableListOf<T>()
|
||||
|
||||
var silent = false
|
||||
|
||||
fun start(scope: CoroutineScope) = apply {
|
||||
flow
|
||||
.buffer(capacity = Int.MAX_VALUE)
|
||||
.onStart { log(tag) { "Setting up." } }
|
||||
.onCompletion { log(tag) { "Final." } }
|
||||
.onEach {
|
||||
collectedValuesMutex.withLock {
|
||||
if (!silent) log(tag) { "Collecting: $it" }
|
||||
latestInternal = it
|
||||
collectedValues.add(it)
|
||||
cache.emit(it)
|
||||
}
|
||||
}
|
||||
.catch { e ->
|
||||
log(tag, WARN) { "Caught error: ${e.asLog()}" }
|
||||
error = e
|
||||
}
|
||||
.launchIn(scope)
|
||||
.also { job = it }
|
||||
}
|
||||
|
||||
fun emissions(): Flow<T> = cache
|
||||
|
||||
val latestValue: T?
|
||||
get() = collectedValues.last()
|
||||
|
||||
val latestValues: List<T>
|
||||
get() = collectedValues
|
||||
|
||||
fun await(
|
||||
timeout: Long = 10_000,
|
||||
condition: (List<T>, T) -> Boolean
|
||||
): T = runBlocking {
|
||||
withTimeout(timeMillis = timeout) {
|
||||
emissions().first {
|
||||
condition(collectedValues, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun awaitFinal(cancel: Boolean = false) = apply {
|
||||
if (cancel) cancel()
|
||||
try {
|
||||
job.join()
|
||||
} catch (e: Exception) {
|
||||
error = e
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun assertNoErrors() = apply {
|
||||
awaitFinal()
|
||||
require(error == null) { "Error was not null: $error" }
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
if (job.isCompleted) throw IllegalStateException("Flow is already canceled.")
|
||||
|
||||
runBlocking {
|
||||
job.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package testhelper.json
|
||||
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.Moshi
|
||||
import okio.Buffer
|
||||
import okio.ByteString.Companion.encode
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import java.io.File
|
||||
|
||||
|
||||
fun String.toComparableJson(): String {
|
||||
val value = Buffer().use {
|
||||
it.writeUtf8(this)
|
||||
val reader = JsonReader.of(it)
|
||||
reader.readJsonValue()
|
||||
}
|
||||
|
||||
val adapter = Moshi.Builder().build().adapter(Any::class.java).indent(" ")
|
||||
|
||||
return adapter.toJson(value)
|
||||
}
|
||||
|
||||
fun String.writeToFile(file: File) = encode().let { text ->
|
||||
require(!file.exists())
|
||||
file.parentFile?.mkdirs()
|
||||
file.createNewFile()
|
||||
file.sink().buffer().use { it.write(text) }
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package testhelper.livedata
|
||||
|
||||
import androidx.arch.core.executor.ArchTaskExecutor
|
||||
import androidx.arch.core.executor.TaskExecutor
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback
|
||||
import org.junit.jupiter.api.extension.ExtensionContext
|
||||
|
||||
class InstantExecutorExtension : BeforeEachCallback, AfterEachCallback {
|
||||
|
||||
override fun beforeEach(context: ExtensionContext?) {
|
||||
ArchTaskExecutor.getInstance().setDelegate(
|
||||
object : TaskExecutor() {
|
||||
override fun executeOnDiskIO(runnable: Runnable) = runnable.run()
|
||||
|
||||
override fun postToMainThread(runnable: Runnable) = runnable.run()
|
||||
|
||||
override fun isMainThread(): Boolean = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun afterEach(context: ExtensionContext?) {
|
||||
ArchTaskExecutor.getInstance().setDelegate(null)
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package testhelper.preferences
|
||||
|
||||
import androidx.core.content.edit
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelper.BaseTest
|
||||
import testhelpers.preferences.MockSharedPreferences
|
||||
|
||||
class MockSharedPreferencesTest : BaseTest() {
|
||||
|
||||
private fun createInstance() = MockSharedPreferences()
|
||||
|
||||
@Test
|
||||
fun `test boolean insertion`() {
|
||||
val prefs = createInstance()
|
||||
prefs.dataMapPeek shouldBe emptyMap()
|
||||
prefs.getBoolean("key", true) shouldBe true
|
||||
prefs.edit { putBoolean("key", false) }
|
||||
prefs.getBoolean("key", true) shouldBe false
|
||||
prefs.dataMapPeek["key"] shouldBe false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user