mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 10:46:12 -04:00
Merge main and common module to simplify.
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package eu.darken.capod.common
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
|
||||
class ByteArrayExtensionsTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
fun `hex - ByteArray conversion`() = runTest {
|
||||
val addr = "78-73-AF-B4-85-22"
|
||||
val raw = addr.fromHex()
|
||||
raw.toHex() shouldBe addr
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
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.shouldBeInstanceOf
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
import testhelpers.coroutine.runTest2
|
||||
import testhelpers.flow.test
|
||||
import java.io.IOException
|
||||
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`() = runTest2(expectedError = IOException::class) {
|
||||
val testScope = this
|
||||
|
||||
val hotData = DynamicStateFlow<String>(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = Dispatchers.Unconfined,
|
||||
startValueProvider = { throw IOException() }
|
||||
)
|
||||
|
||||
// This blocking scope gets the init exception as the first caller
|
||||
hotData.flow.firstOrNull()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `subscription doesn't end when no subscriber is collecting, mode Lazily`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> String>()
|
||||
coEvery { valueProvider.invoke(any()) } returns "Test"
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = Dispatchers.Unconfined,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `value updates`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
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(scope = testScope)
|
||||
testCollector.silent = true
|
||||
|
||||
(1..16).forEach { _ ->
|
||||
thread {
|
||||
(1..200).forEach { _ ->
|
||||
hotData.updateAsync(
|
||||
onUpdate = { this + 1L },
|
||||
onError = { throw it }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector.await { list, _ -> list.size == 3201 }
|
||||
testCollector.latestValues shouldBe (1..3201).toList()
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
data class TestData(
|
||||
val number: Long = 1
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `check multi threading value updates with more complex data`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
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(scope = testScope)
|
||||
testCollector.silent = true
|
||||
|
||||
(1..10).forEach { _ ->
|
||||
thread {
|
||||
(1..400).forEach { _ ->
|
||||
hotData.updateAsync {
|
||||
mutate {
|
||||
this["data"] = getValue("data").copy(
|
||||
number = getValue("data").number + 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector.await { list, _ -> list.size == 4001 }
|
||||
testCollector.latestValues.map { it.values.single().number } shouldBe (1L..4001L).toList()
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only emit new values if they actually changed updates`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = { "1" },
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(scope = testScope)
|
||||
testCollector.silent = true
|
||||
|
||||
hotData.updateAsync { "1" }
|
||||
hotData.updateAsync { "2" }
|
||||
hotData.updateAsync { "2" }
|
||||
hotData.updateAsync { "1" }
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector.await { list, _ -> list.size == 3 }
|
||||
testCollector.latestValues shouldBe listOf("1", "2", "1")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `multiple subscribers share the flow`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> String>()
|
||||
coEvery { valueProvider.invoke(any()) } returns "Test"
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
val sub1 = hotData.flow.test(tag = "sub1", scope = testScope)
|
||||
val sub2 = hotData.flow.test(tag = "sub2", scope = testScope)
|
||||
val sub3 = hotData.flow.test(tag = "sub3", scope = testScope)
|
||||
|
||||
hotData.updateAsync { "A" }
|
||||
hotData.updateAsync { "B" }
|
||||
hotData.updateAsync { "C" }
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
listOf(sub1, sub2, sub3).forEach {
|
||||
it.await { list, _ -> list.size == 4 }
|
||||
it.latestValues shouldBe listOf("Test", "A", "B", "C")
|
||||
it.cancelAndJoin()
|
||||
}
|
||||
|
||||
hotData.flow.first() shouldBe "C"
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `value is persisted between unsubscribes`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val valueProvider = mockk<suspend CoroutineScope.() -> Long>()
|
||||
coEvery { valueProvider.invoke(any()) } returns 1
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = this.coroutineContext,
|
||||
startValueProvider = valueProvider,
|
||||
)
|
||||
|
||||
val testCollector1 = hotData.flow.test(tag = "collector1", scope = testScope)
|
||||
testCollector1.silent = false
|
||||
|
||||
(1..10).forEach { _ ->
|
||||
hotData.updateAsync {
|
||||
this + 1L
|
||||
}
|
||||
}
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector1.await { list, _ -> list.size == 11 }
|
||||
testCollector1.latestValues shouldBe (1L..11L).toList()
|
||||
|
||||
testCollector1.cancelAndJoin()
|
||||
|
||||
val testCollector2 = hotData.flow.test(tag = "collector2", scope = testScope)
|
||||
testCollector2.silent = false
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector2.cancelAndJoin()
|
||||
|
||||
testCollector2.latestValues shouldBe listOf(11L)
|
||||
|
||||
coVerify(exactly = 1) { valueProvider.invoke(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocking update is actually blocking`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
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(scope = testScope)
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
hotData.updateBlocking { this - 3 } shouldBe 0
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector.await { _, i -> i == 3 }
|
||||
testCollector.latestValues shouldBe listOf(2, 3, 0)
|
||||
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `blocking update rethrows error`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = testScope.coroutineContext,
|
||||
startValueProvider = {
|
||||
delay(2000)
|
||||
2
|
||||
},
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(scope = testScope)
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
shouldThrow<IOException> {
|
||||
hotData.updateBlocking { throw IOException("Surprise") } shouldBe 0
|
||||
}
|
||||
hotData.flow.first() shouldBe 2
|
||||
|
||||
hotData.updateBlocking { 3 } shouldBe 3
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
hotData.flow.first() shouldBe 3
|
||||
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `async updates error handler`() = runTest2(expectedError = IOException::class) {
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = this,
|
||||
startValueProvider = { 1 },
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(scope = this)
|
||||
advanceUntilIdle()
|
||||
|
||||
hotData.updateAsync { throw IOException("Surprise") }
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `async updates rethrow errors on HotDataFlow scope if no error handler is set`() = runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
startValueProvider = { 1 },
|
||||
)
|
||||
|
||||
val testCollector = hotData.flow.test(scope = testScope)
|
||||
testScope.advanceUntilIdle()
|
||||
|
||||
var thrownError: Exception? = null
|
||||
|
||||
hotData.updateAsync(
|
||||
onUpdate = { throw IOException("Surprise") },
|
||||
onError = { thrownError = it }
|
||||
)
|
||||
|
||||
testScope.advanceUntilIdle()
|
||||
thrownError!!.shouldBeInstanceOf<IOException>()
|
||||
|
||||
testCollector.cancelAndJoin()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clean up function is called when parent scope is cancelled`() {
|
||||
var onReleaseValue: String? = null
|
||||
|
||||
runTest2(autoCancel = true) {
|
||||
val testScope = this
|
||||
|
||||
val hotData = DynamicStateFlow(
|
||||
loggingTag = "tag",
|
||||
parentScope = testScope,
|
||||
coroutineContext = Dispatchers.Unconfined,
|
||||
startValueProvider = { "Test" },
|
||||
onRelease = {
|
||||
onReleaseValue = it
|
||||
}
|
||||
)
|
||||
|
||||
hotData.flow.first() shouldBe "Test"
|
||||
}
|
||||
|
||||
onReleaseValue shouldBe "Test"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
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.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
import testhelpers.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`() = runTest {
|
||||
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`() = runTest {
|
||||
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`() = runTest {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package eu.darken.capod.common.preferences
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
import testhelpers.preferences.MockSharedPreferences
|
||||
|
||||
class FlowPreferenceTest : BaseTest() {
|
||||
|
||||
private val mockPreferences = MockSharedPreferences()
|
||||
|
||||
@Test
|
||||
fun `reading and writing strings`() = runTest {
|
||||
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`() = runTest {
|
||||
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`() = runTest {
|
||||
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`() = runTest {
|
||||
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`() = runTest {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.darken.capod.monitor.core
|
||||
|
||||
import eu.darken.capod.common.fromHex
|
||||
import eu.darken.capod.pods.core.apple.protocol.RPAChecker
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
|
||||
class RPACheckerTest : BaseTest() {
|
||||
|
||||
@Test
|
||||
fun `test check`() {
|
||||
val checker = RPAChecker()
|
||||
checker.verify(
|
||||
address = "5A:16:2B:91:D1:CD",
|
||||
irk = "79-04-65-1E-E2-CC-D9-26-F2-6E-20-EE-3E-CC-DE-79".fromHex(),
|
||||
) shouldBe true
|
||||
checker.verify(
|
||||
address = "5A:16:2B:91:D1:CD",
|
||||
irk = "79-04-65-1E-E2-CC-D9-26-F2-6E-20-EE-3E-CC-DE-AA".fromHex(),
|
||||
) shouldBe false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bad input check`() {
|
||||
val checker = RPAChecker()
|
||||
checker.verify(
|
||||
address = "5A:16:2B:91:D1:CD",
|
||||
irk = "".fromHex(),
|
||||
) shouldBe false
|
||||
checker.verify(
|
||||
address = "",
|
||||
irk = "79-04-65-1E-E2-CC-D9-26-F2-6E-20-EE-3E-CC-DE-AA".fromHex(),
|
||||
) shouldBe false
|
||||
checker.verify(
|
||||
address = "",
|
||||
irk = "".fromHex(),
|
||||
) shouldBe false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
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 kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsFactoryTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `create AirPodsGen1`() = runTest {
|
||||
create<DualApplePods>("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`() = runTest {
|
||||
create<DualApplePods>("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`() = runTest {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import eu.darken.capod.common.SystemClockWrap
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.fromHex
|
||||
import eu.darken.capod.common.serialization.SerializationModule
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
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.mockk
|
||||
import io.mockk.mockkObject
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import testhelpers.BaseTest
|
||||
import testhelpers.preferences.mockFlowPreference
|
||||
import java.time.Instant
|
||||
import javax.inject.Singleton
|
||||
|
||||
abstract class BaseAirPodsTest : BaseTest() {
|
||||
@Singleton
|
||||
@Component(modules = [AppleFactoryModule::class, SerializationModule::class])
|
||||
interface AppleFactoryTestComponent {
|
||||
|
||||
val appleFactory: AppleFactory
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(
|
||||
@BindsInstance generalSettings: GeneralSettings,
|
||||
): AppleFactoryTestComponent
|
||||
}
|
||||
}
|
||||
|
||||
val generalSettings = mockk<GeneralSettings>().apply {
|
||||
every { mainDeviceIdentityKey } returns mockFlowPreference(null)
|
||||
every { mainDeviceEncryptionKey } returns mockFlowPreference(null)
|
||||
}
|
||||
|
||||
private fun hexToByteArray(hex: String): ByteArray = hex
|
||||
.replace(">", "")
|
||||
.replace("<", "")
|
||||
.fromHex()
|
||||
|
||||
private fun cleanKey(key: String): ByteArray = hexToByteArray(key)
|
||||
.also { require(it.size == 16) { "Not a valid key: ${it.size} byte" } }
|
||||
|
||||
fun setKeyIRK(key: String?) {
|
||||
generalSettings.apply {
|
||||
every { mainDeviceIdentityKey } returns mockFlowPreference(key?.let { cleanKey(it) })
|
||||
}
|
||||
}
|
||||
|
||||
fun setKeyEnc(key: String?) {
|
||||
generalSettings.apply {
|
||||
every { mainDeviceEncryptionKey } returns mockFlowPreference(key?.let { cleanKey(it) })
|
||||
}
|
||||
}
|
||||
|
||||
val factory: AppleFactory = DaggerBaseAirPodsTest_AppleFactoryTestComponent.factory().create(
|
||||
generalSettings = generalSettings
|
||||
).appleFactory
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
mockkObject(SystemClockWrap)
|
||||
every { SystemClockWrap.elapsedRealtimeNanos } returns 1000L
|
||||
}
|
||||
|
||||
internal suspend inline fun <reified T : PodDevice?> create(
|
||||
hex: String,
|
||||
address: String = "77:49:4C:D8:25:0C",
|
||||
block: T.() -> Unit
|
||||
) {
|
||||
val result = BleScanResult(
|
||||
receivedAt = Instant.now(),
|
||||
address = address,
|
||||
rssi = -66,
|
||||
generatedAtNanos = 136136027721826,
|
||||
manufacturerSpecificData = mutableMapOf<Int, ByteArray>().apply {
|
||||
this[ContinuityProtocol.APPLE_COMPANY_IDENTIFIER] = hexToByteArray(hex)
|
||||
}
|
||||
)
|
||||
|
||||
block.invoke(factory.create(result) as T)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BasicSingleApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `test mapping`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0520.toUShort()
|
||||
pubStatus shouldBe 0x00.toUByte()
|
||||
pubPodsBattery shouldBe 0xF5.toUByte()
|
||||
pubFlags shouldBe 0x0.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x01.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test battery headset percent`() = runTest {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class DualApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `test bit mapping`() = runTest {
|
||||
create<DualApplePods>("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") {
|
||||
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0e20.toUShort()
|
||||
pubStatus shouldBe 0x54.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0xB.toUShort()
|
||||
pubCaseBattery shouldBe 0x5.toUShort()
|
||||
pubCaseLidState shouldBe 0x31.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - active microphone`() = runTest {
|
||||
create<DualApplePods>("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<DualApplePods>("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`() = runTest {
|
||||
// Left Pod primary
|
||||
create<DualApplePods>("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<DualApplePods>("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<DualApplePods>("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<DualApplePods>("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`() = runTest {
|
||||
// Left Pod primary
|
||||
create<DualApplePods>("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<DualApplePods>("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<DualApplePods>("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<DualApplePods>("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`() = runTest {
|
||||
// Right Pod is primary
|
||||
create<DualApplePods>("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<DualApplePods>("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`() = runTest {
|
||||
/**
|
||||
* Right pod is charging
|
||||
*/
|
||||
// This is the left
|
||||
create<DualApplePods>("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<DualApplePods>("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<DualApplePods>("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<DualApplePods>("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<DualApplePods>("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<DualApplePods>("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`() = runTest {
|
||||
create<DualApplePods>("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<DualApplePods>("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`() = runTest {
|
||||
// Lid open
|
||||
create<DualApplePods>("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 DualApplePods.LidState.OPEN
|
||||
}
|
||||
// Lid open, left pod in case
|
||||
create<DualApplePods>("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 DualApplePods.LidState.OPEN
|
||||
}
|
||||
// Lid open, left pod in case
|
||||
create<DualApplePods>("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 DualApplePods.LidState.OPEN
|
||||
}
|
||||
|
||||
|
||||
// Lid just closed
|
||||
create<DualApplePods>("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 DualApplePods.LidState.CLOSED
|
||||
}
|
||||
// Lid closed
|
||||
create<DualApplePods>("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 DualApplePods.LidState.CLOSED
|
||||
}
|
||||
// Lid closed, right pod in case
|
||||
create<DualApplePods>("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 DualApplePods.LidState.CLOSED
|
||||
}
|
||||
// Lid closed, left pod in case
|
||||
create<DualApplePods>("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 DualApplePods.LidState.CLOSED
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPodDevice - connection state`() = runTest {
|
||||
// Disconnected
|
||||
create<HasStateDetectionAirPods>("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 HasStateDetectionAirPods.ConnectionState.DISCONNECTED
|
||||
}
|
||||
// Connected idle
|
||||
create<HasStateDetectionAirPods>("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 HasStateDetectionAirPods.ConnectionState.IDLE
|
||||
}
|
||||
// Connected and playing music
|
||||
create<HasStateDetectionAirPods>("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 HasStateDetectionAirPods.ConnectionState.MUSIC
|
||||
}
|
||||
// Connected and call active
|
||||
create<HasStateDetectionAirPods>("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 HasStateDetectionAirPods.ConnectionState.CALL
|
||||
}
|
||||
// Connected and call active
|
||||
create<HasStateDetectionAirPods>("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 HasStateDetectionAirPods.ConnectionState.RINGING
|
||||
}
|
||||
// Switching?
|
||||
create<HasStateDetectionAirPods>("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 HasStateDetectionAirPods.ConnectionState.HANGING_UP
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SingleApplePodsTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `default bit mapping Max`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0A20.toUShort()
|
||||
pubStatus shouldBe 0x62.toUByte()
|
||||
pubPodsBattery shouldBe 0x04.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x40.toUByte()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
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`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0220.toUShort()
|
||||
pubStatus shouldBe 0x55.toUByte()
|
||||
pubPodsBattery shouldBe 0xAF.toUByte()
|
||||
pubFlags shouldBe 0x5.toUShort()
|
||||
pubCaseBattery shouldBe 0x6.toUShort()
|
||||
pubCaseLidState shouldBe 0x31.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix 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 DualApplePods.LidState.OPEN
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_GEN1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsGen2Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `random Neighbor AirPodsGen2`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0F20.toUShort()
|
||||
pubStatus shouldBe 0x02.toUByte()
|
||||
pubPodsBattery shouldBe 0xF9.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix 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 DualApplePods.LidState.NOT_IN_CASE
|
||||
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_GEN2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsGen3Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `AirPods Gen3`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1320.toUShort()
|
||||
pubStatus shouldBe 0x75.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0xB.toUShort()
|
||||
pubCaseBattery shouldBe 0x9.toUShort()
|
||||
pubCaseLidState shouldBe 0x31.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix 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 DualApplePods.LidState.OPEN
|
||||
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_GEN3
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `random guy at bus stop`() = runTest {
|
||||
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 DualApplePods.LidState.NOT_IN_CASE
|
||||
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.UNKNOWN
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsGen4AncTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `AirPods Gen4 with ANC via log from #226`() = runTest {
|
||||
create<AirPodsGen4Anc>("07 19 01 1B 20 0B 9A 8F 10 00 04 43 DF EC 1D D3 F1 C3 F4 A1 9B 29 26 B9 E7 3A A0") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1B20.toUShort()
|
||||
pubStatus shouldBe 0x0b.toUByte()
|
||||
pubPodsBattery shouldBe 0x9A.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x10.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 0.9f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
|
||||
isLeftPodInEar shouldBe true
|
||||
isRightPodInEar shouldBe true
|
||||
batteryCasePercent shouldBe null
|
||||
|
||||
caseLidState shouldBe DualApplePods.LidState.UNKNOWN
|
||||
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_GEN4_ANC
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsGen4Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `AirPods Gen4 via log from #225`() = runTest {
|
||||
create<AirPodsGen4>("07 19 01 19 20 2B 33 8F 11 00 04 59 D4 57 20 0F 1C 13 38 B2 00 74 E9 DD 70 D7 A5") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1920.toUShort()
|
||||
pubStatus shouldBe 0x2B.toUByte()
|
||||
pubPodsBattery shouldBe 0x33.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x11.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 0.3f
|
||||
batteryRightPodPercent shouldBe 0.3f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
|
||||
isLeftPodInEar shouldBe true
|
||||
isRightPodInEar shouldBe true
|
||||
batteryCasePercent shouldBe null
|
||||
|
||||
caseLidState shouldBe DualApplePods.LidState.UNKNOWN
|
||||
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_GEN4
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
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.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsMaxTest : BaseAirPodsTest() {
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/124
|
||||
@Test
|
||||
fun `default AirPods Max`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0A20.toUShort()
|
||||
pubStatus shouldBe 0x62.toUByte()
|
||||
pubPodsBattery shouldBe 0x04.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_MAX
|
||||
}
|
||||
}
|
||||
|
||||
// Test data from https://github.com/adolfintel/OpenPods/issues/124
|
||||
@Test
|
||||
fun `default AirPods Max flipped values`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0A20.toUShort()
|
||||
pubStatus shouldBe 0x02.toUByte()
|
||||
pubPodsBattery shouldBe 0x05.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x04.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x44.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `some dude at the gym`() = runTest {
|
||||
create<AirPodsMax>("07 19 01 0A 20 23 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0A20.toUShort()
|
||||
pubStatus shouldBe 0x23.toUByte()
|
||||
pubPodsBattery shouldBe 0x07.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x03.toUByte()
|
||||
pubDeviceColor shouldBe 0x03.toUByte()
|
||||
pubSuffix shouldBe 0x65.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.7f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
pubStatus.isBitSet(5) shouldBe true
|
||||
|
||||
podStyle shouldBe HasAppleColor.DeviceColor.BLUE
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wear status`() = runTest {
|
||||
create<AirPodsMax>("07 19 01 0A 20 03 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
|
||||
pubStatus shouldBe 0x03.toUByte()
|
||||
|
||||
pubStatus.isBitSet(5) shouldBe false
|
||||
isBeingWorn shouldBe false
|
||||
}
|
||||
create<AirPodsMax>("07 19 01 0A 20 23 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
|
||||
pubStatus shouldBe 0x23.toUByte()
|
||||
|
||||
pubStatus.isBitSet(5) shouldBe true
|
||||
isBeingWorn shouldBe true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsMaxUsbcTest : BaseAirPodsTest() {
|
||||
|
||||
// Test data from https://github.com/d4rken-org/capod/issues/236
|
||||
@Test
|
||||
fun `default AirPods Max`() = runTest {
|
||||
create<AirPodsMaxUsbc>("07 19 01 1F 20 2B 05 80 03 12 C5 2E 8B F9 9A 7E 19 7B 63 0F 30 6E D7 3B E2 EC 32") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1F20.toUShort()
|
||||
pubStatus shouldBe 0x2B.toUByte()
|
||||
pubPodsBattery shouldBe 0x05.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x03.toUByte()
|
||||
pubDeviceColor shouldBe 0x12.toUByte()
|
||||
pubSuffix shouldBe 0xC5.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
isHeadsetBeingCharged shouldBe false
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_MAX_USBC
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
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.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsPro2Test : BaseAirPodsTest() {
|
||||
|
||||
/**
|
||||
* https://github.com/d4rken-org/capod/issues/31#issuecomment-1256791084
|
||||
*/
|
||||
@Test
|
||||
fun `test AirPods Pro 2 - unknown setup from #31`() = runTest {
|
||||
create<AirPodsPro2>("07 19 01 14 20 55 88 F9 51 00 04 20 50 03 CA D5 C9 AC 0F FA 84 78 94 5A 4D DF F5") {
|
||||
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1420.toUShort()
|
||||
pubStatus shouldBe 0x55.toUByte()
|
||||
pubPodsBattery shouldBe 0x88.toUByte()
|
||||
pubFlags shouldBe 0xF.toUShort()
|
||||
pubCaseBattery shouldBe 0x9.toUShort()
|
||||
pubCaseLidState shouldBe 0x51.toUByte()
|
||||
pubDeviceColor shouldBe 0x0.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
batteryLeftPodPercent shouldBe 0.8f
|
||||
batteryRightPodPercent shouldBe 0.8f
|
||||
|
||||
isCaseCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe true
|
||||
batteryCasePercent shouldBe 0.9f
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_PRO2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* https://old.reddit.com/message/messages/1hst12h
|
||||
*/
|
||||
@Test
|
||||
fun `test AirPods Pro 2 - unknown setup from reddit user`() = runTest {
|
||||
create<AirPodsPro2>("07 19 01 14 20 2B 9A 8F 01 00 04 0F 26 1A C4 2B FA 2F B9 B6 08 CD 60 CB DF 75 AB") {
|
||||
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1420.toUShort()
|
||||
pubStatus shouldBe 0x2B.toUByte()
|
||||
pubPodsBattery shouldBe 0x9A.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
isLeftPodInEar shouldBe true
|
||||
isRightPodInEar shouldBe true
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 0.9f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
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.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsPro2UsbcTest : BaseAirPodsTest() {
|
||||
|
||||
/**
|
||||
* https://github.com/d4rken-org/capod/issues/164
|
||||
*/
|
||||
@Test
|
||||
fun `AirPods Pro 2 with USB-C - via #164`() = runTest {
|
||||
create<AirPodsPro2Usbc>("07 19 01 24 20 0B 99 8F 11 00 04 BD A7 3B FF 2D 8A 3C AF 9B 1A 7C 74 B7 A9 D1 C3") {
|
||||
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x2420.toUShort()
|
||||
pubStatus shouldBe 0x0B.toUByte()
|
||||
pubPodsBattery shouldBe 0x99.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x11.toUByte()
|
||||
pubDeviceColor shouldBe 0x0.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe false
|
||||
isRightPodMicrophone shouldBe true
|
||||
|
||||
isLeftPodInEar shouldBe true
|
||||
isRightPodInEar shouldBe true
|
||||
|
||||
batteryLeftPodPercent shouldBe 0.9f
|
||||
batteryRightPodPercent shouldBe 0.9f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_PRO2_USBC
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AirPods Pro 2 with USB-C - via #164 - in case`() = runTest {
|
||||
create<AirPodsPro2Usbc>("07 19 01 24 20 53 AA 98 32 00 05 49 0A B8 BF 8E 29 D8 70 12 0D A7 0C CE 77 56 00") {
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
isLeftPodInEar shouldBe true
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
batteryLeftPodPercent shouldBe 1f
|
||||
batteryRightPodPercent shouldBe 1f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe 0.8f
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_PRO2_USBC
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
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.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsPro3Test : BaseAirPodsTest() {
|
||||
|
||||
/**
|
||||
* Test case for AirPods Pro 3 - placeholder with correct device code
|
||||
* Following the pattern of other AirPods models (0x2720)
|
||||
*/
|
||||
@Test
|
||||
fun `AirPods Pro 3 - placeholder test`() = runTest {
|
||||
// This test uses a placeholder hex string with the correct device code 0x2720
|
||||
// The actual proximity pairing data will need to be captured from real AirPods Pro 3
|
||||
create<AirPodsPro3>("07 19 01 27 20 0B 99 8F 11 00 04 BD A7 3B FF 2D 8A 3C AF 9B 1A 7C 74 B7 A9 D1 C3") {
|
||||
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x2720.toUShort()
|
||||
pubStatus shouldBe 0x0B.toUByte()
|
||||
pubPodsBattery shouldBe 0x99.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x11.toUByte()
|
||||
pubDeviceColor shouldBe 0x0.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe false
|
||||
isRightPodMicrophone shouldBe true
|
||||
|
||||
isLeftPodInEar shouldBe true
|
||||
isRightPodInEar shouldBe true
|
||||
|
||||
batteryLeftPodPercent shouldBe 0.9f
|
||||
batteryRightPodPercent shouldBe 0.9f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_PRO3
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AirPods Pro 3 - placeholder test - in case`() = runTest {
|
||||
// This test uses a placeholder hex string with the correct device code 0x2720
|
||||
// The actual proximity pairing data will need to be captured from real AirPods Pro 3
|
||||
create<AirPodsPro3>("07 19 01 27 20 53 AA 98 32 00 05 49 0A B8 BF 8E 29 D8 70 12 0D A7 0C CE 77 56 00") {
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
isLeftPodInEar shouldBe true
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
batteryLeftPodPercent shouldBe 1f
|
||||
batteryRightPodPercent shouldBe 1f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe 0.8f
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_PRO3
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AirPods Pro 3`() = runTest {
|
||||
create<AirPodsPro3>("07 19 01 27 20 0B 99 8F 11 00 05 43 A8 85 17 07 FF 41 62 2C FE 5E 20 08 1A 52 40") {
|
||||
isLeftPodInEar shouldBe true
|
||||
isRightPodInEar shouldBe true
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_PRO3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.common.toHex
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
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.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class AirPodsProTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `test AirPods Pro - default changed and in case`() = runTest {
|
||||
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") {
|
||||
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0e20.toUShort()
|
||||
pubStatus shouldBe 0x54.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0xB.toUShort()
|
||||
pubCaseBattery shouldBe 0x5.toUShort()
|
||||
pubCaseLidState shouldBe 0x31.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix 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
|
||||
|
||||
model shouldBe PodDevice.Model.AIRPODS_PRO
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test AirPods from my downstairs neighbour`() = runTest {
|
||||
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`() = runTest {
|
||||
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`() = runTest {
|
||||
create<AirPodsGen1>("071901022055AA563100006FE4DF10AF106081033B76D9C7112288") {
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe 0.6f
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `left pod has no data`() = runTest {
|
||||
create<AirPodsPro>("07 19 01 0E 20 0B F9 8F 03 00 05 5B 59 67 4C F7 F3 EF 01 BA F4 92 1B 26 E4 90 40") {
|
||||
pubPodsBattery shouldBe 0xF9.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe null
|
||||
batteryRightPodPercent shouldBe 0.9f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `decrypt data`() = runTest {
|
||||
val data = "07 19 01 0E 20 51 9A 98 33 00 04 0C 14 E0 EB 43 3F 4B 22 C0 A9 ED CB 33 E7 09 71"
|
||||
val address = "5A:16:2B:91:D1:CD"
|
||||
create<AirPodsPro>(data, address) {
|
||||
batteryLeftPodPercent shouldBe 0.9f
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
batteryCasePercent shouldBe 0.8f
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
payload.public.data.toByteArray().toHex(" ") shouldBe "01 0E 20 51 9A 98 33 00 04"
|
||||
payload.private shouldBe null
|
||||
}
|
||||
setKeyIRK("79-04-65-1E-E2-CC-D9-26-F2-6E-20-EE-3E-CC-DE-79")
|
||||
setKeyEnc("3B-9C-80-57-E6-45-7F-F2-1B-8E-07-63-6C-99-E0-29")
|
||||
create<AirPodsPro>(data, address) {
|
||||
batteryLeftPodPercent shouldBe 0.98f
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
batteryCasePercent shouldBe 0.86f
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
payload.public.data.toByteArray().toHex(" ") shouldBe "01 0E 20 51 9A 98 33 00 04"
|
||||
payload.private!!.data.toByteArray().toHex(" ") shouldBe "44 E4 62 56 17 FA 06 31 E4 0A 01 13 31 13 4C 40"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
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.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsFitProTest : BaseAirPodsTest() {
|
||||
|
||||
/**
|
||||
* From https://github.com/d4rken-org/capod/issues/33#issuecomment-1256235651
|
||||
*/
|
||||
@Test
|
||||
fun `test basics`() = runTest {
|
||||
create<BeatsFitPro>("07 19 01 12 20 20 FA 8F 01 11 24 9B 9B 23 52 60 5A 8C 32 1C A5 C2 81 51 82 AF C8") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1220.toUShort()
|
||||
pubStatus shouldBe 0x20.toUByte()
|
||||
pubPodsBattery shouldBe 0xFA.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x11.toUByte()
|
||||
pubSuffix shouldBe 0x24.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe null
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.UNKNOWN.name
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_FIT_PRO
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extra rl test case`() = runTest {
|
||||
create<BeatsFitPro>("07 19 01 12 20 04 FA 92 54 11 24 CE B1 DF 9D 8D F5 E3 37 60 B1 23 8B 90 3B 63 3F") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1220.toUShort()
|
||||
pubStatus shouldBe 0x04.toUByte()
|
||||
pubPodsBattery shouldBe 0xFA.toUByte()
|
||||
pubFlags shouldBe 0x9.toUShort()
|
||||
pubCaseBattery shouldBe 0x2.toUShort()
|
||||
pubCaseLidState shouldBe 0x54.toUByte()
|
||||
pubDeviceColor shouldBe 0x11.toUByte()
|
||||
pubSuffix shouldBe 0x24.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe false
|
||||
isRightPodMicrophone shouldBe true
|
||||
|
||||
batteryLeftPodPercent shouldBe null
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe true
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe 0.2f
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.UNKNOWN.name
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_FIT_PRO
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsFlexText : BaseAirPodsTest() {
|
||||
|
||||
// Raw data from https://github.com/adolfintel/OpenPods/issues/105
|
||||
@Test
|
||||
fun `default BeatsFlex`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1020.toUShort()
|
||||
pubStatus shouldBe 0x0A.toUByte()
|
||||
pubPodsBattery shouldBe 0xF4.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x00.toUByte()
|
||||
pubDeviceColor shouldBe 0x01.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_FLEX
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `random neighbour`() = runTest {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsSolo3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@Test
|
||||
fun `default BeatsSolo3`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0620.toUShort()
|
||||
pubStatus shouldBe 0x62.toUByte()
|
||||
pubPodsBattery shouldBe 0x04.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_SOLO_3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsStudio3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@Test
|
||||
fun `default BeatsStudio3`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0920.toUShort()
|
||||
pubStatus shouldBe 0x62.toUByte()
|
||||
pubPodsBattery shouldBe 0x04.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_STUDIO_3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeatsXTest : BaseAirPodsTest() {
|
||||
|
||||
// Raw data from https://github.com/adolfintel/OpenPods/issues/105
|
||||
@Test
|
||||
fun `default BeatsX`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0520.toUShort()
|
||||
pubStatus shouldBe 0x00.toUByte()
|
||||
pubPodsBattery shouldBe 0xF5.toUByte()
|
||||
pubFlags shouldBe 0x0.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x01.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.5f
|
||||
|
||||
model shouldBe PodDevice.Model.BEATS_X
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PowerBeats3Test : BaseAirPodsTest() {
|
||||
|
||||
// TODO This is handcrafted data, get actual data for tests
|
||||
@Test
|
||||
fun `default PowerBeats3`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0320.toUShort()
|
||||
pubStatus shouldBe 0x62.toUByte()
|
||||
pubPodsBattery shouldBe 0x04.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x0F.toUByte()
|
||||
pubSuffix shouldBe 0x40.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.4f
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PowerBeats4Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `playing music`() = runTest {
|
||||
create<PowerBeats4>("07 19 01 0D 20 00 02 80 01 00 05 07 C5 E1 9C BD 9A 05 0E AE 9E 56 53 2F F9 75 4A") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0D20.toUShort()
|
||||
pubStatus shouldBe 0x0.toUByte()
|
||||
pubPodsBattery shouldBe 0x02.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x05.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.2f
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_4
|
||||
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `extra test case`() = runTest {
|
||||
create<PowerBeats4>("07 19 01 0D 20 00 02 80 02 00 04 80 90 60 77 32 C2 0C 75 7A 3D 7E D7 1C 0E B8 43") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0D20.toUShort()
|
||||
pubStatus shouldBe 0x0.toUByte()
|
||||
pubPodsBattery shouldBe 0x02.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x02.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.2f
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_4
|
||||
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `disconnected state`() = runTest {
|
||||
create<PowerBeats4>("07 19 01 0D 20 00 02 80 01 00 00 25 DF 66 40 AF 44 7B 77 95 8F D1 92 50 26 11 74") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0D20.toUShort()
|
||||
pubStatus shouldBe 0x0.toUByte()
|
||||
pubPodsBattery shouldBe 0x02.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0x0.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
batteryHeadsetPercent shouldBe 0.2f
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_4
|
||||
|
||||
state shouldBe HasStateDetectionAirPods.ConnectionState.DISCONNECTED
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
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.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PowerBeatsPro2Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `test PowerBeatsPro2`() = runTest {
|
||||
create<PowerBeatsPro2>("07 19 01 1D 20 2B AA 8F 01 02 04 30 42 6F CE 74 EE ED 56 AF 4E 31 16 9D 00 D7 DE") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1D20.toUShort()
|
||||
pubStatus shouldBe 0x2B.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x01.toUByte()
|
||||
pubDeviceColor shouldBe 0x02.toUByte()
|
||||
pubSuffix shouldBe 0x04.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.RED.name
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_PRO2
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test PowerBeatsPro2 - variant 2`() = runTest {
|
||||
create<PowerBeatsPro2>("07 19 01 1D 20 73 AA 98 32 02 04 7D 47 B0 15 DE 5F B6 6B CB 46 F9 16 5C 88 4F 4E") {
|
||||
isLeftPodMicrophone shouldBe false
|
||||
isRightPodMicrophone shouldBe true
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe true
|
||||
batteryCasePercent shouldBe 0.8f
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.RED.name
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_PRO2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
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.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class PowerBeatsProTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `test PowerBeatsPro`() = runTest {
|
||||
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") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0B20.toUShort()
|
||||
pubStatus shouldBe 0x54.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0xB.toUShort()
|
||||
pubCaseBattery shouldBe 0x5.toUShort()
|
||||
pubCaseLidState shouldBe 0x31.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix 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
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_PRO
|
||||
}
|
||||
}
|
||||
|
||||
// Via https://github.com/d4rken-org/capod/pull/303#issuecomment-2991876052
|
||||
@Test
|
||||
fun `test PowerBeatsPro - variant 2`() = runTest {
|
||||
create<PowerBeatsPro>("07 19 01 0B 20 21 AA 8F 02 44 24 6D 3E CD 38 A6 F9 6C 6A EC 95 65 AF 97 08 95 49") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0B20.toUShort()
|
||||
pubStatus shouldBe 0x21.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x8.toUShort()
|
||||
pubCaseBattery shouldBe 0xF.toUShort()
|
||||
pubCaseLidState shouldBe 0x02.toUByte()
|
||||
pubDeviceColor shouldBe 0x44.toUByte()
|
||||
pubSuffix shouldBe 0x24.toUByte()
|
||||
|
||||
isLeftPodMicrophone shouldBe true
|
||||
isRightPodMicrophone shouldBe false
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isRightPodCharging shouldBe false
|
||||
isLeftPodCharging shouldBe false
|
||||
batteryCasePercent shouldBe null
|
||||
|
||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.UNKNOWN.name
|
||||
|
||||
model shouldBe PodDevice.Model.POWERBEATS_PRO
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class FakeAirPodsGen1Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `charging in box`() = runTest {
|
||||
create<FakeAirPodsGen2>("07 13 01 0F 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0F20.toUShort()
|
||||
pubStatus shouldBe 0x71.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x3.toUShort()
|
||||
pubCaseBattery shouldBe 0x7.toUShort()
|
||||
pubCaseLidState shouldBe 0x32.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
batteryCasePercent shouldBe 0.7f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class FakeAirPodsGen2Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `charging in box`() = runTest {
|
||||
create<FakeAirPodsGen1>("07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0220.toUShort()
|
||||
pubStatus shouldBe 0x71.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x3.toUShort()
|
||||
pubCaseBattery shouldBe 0x7.toUShort()
|
||||
pubCaseLidState shouldBe 0x32.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
batteryCasePercent shouldBe 0.7f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class FakeAirPodsGen3Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `charging in case`() = runTest {
|
||||
create<FakeAirPodsGen3>("07 13 01 13 20 75 AA 37 34 00 10 00 E4 E4 64 00 00 00 00 00 00") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1320.toUShort()
|
||||
pubStatus shouldBe 0x75.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x3.toUShort()
|
||||
pubCaseBattery shouldBe 0x7.toUShort()
|
||||
pubCaseLidState shouldBe 0x34.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1f
|
||||
batteryRightPodPercent shouldBe 1f
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
batteryCasePercent shouldBe 0.7f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class FakeAirPodsPro2Test : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `guessed data`() = runTest {
|
||||
create<FakeAirPodsPro2>("07 13 01 14 20 75 AA 58 35 00 10 00 E4 E4 26 00 00 00 00 00 00") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1420.toUShort()
|
||||
pubStatus shouldBe 0x75.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x5.toUShort()
|
||||
pubCaseBattery shouldBe 0x8.toUShort()
|
||||
pubCaseLidState shouldBe 0x35.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe true
|
||||
|
||||
batteryCasePercent shouldBe 0.8f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* https://discord.com/channels/548521543039189022/927235844127993866/1063027552765087754
|
||||
*/
|
||||
@Test
|
||||
fun `user supplied`() = runTest {
|
||||
create<FakeAirPodsPro2>("07 13 01 14 20 75 AA 72 39 00 00 6F E4 E4 93 30 00 30 30 30 30") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x1420.toUShort()
|
||||
pubStatus shouldBe 0x75.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x7.toUShort()
|
||||
pubCaseBattery shouldBe 0x2.toUShort()
|
||||
pubCaseLidState shouldBe 0x39.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x0.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isCaseCharging shouldBe true
|
||||
|
||||
batteryCasePercent shouldBe 0.2f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package eu.darken.capod.pods.core.apple.misc
|
||||
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class FakeAirPodsProTest : BaseAirPodsTest() {
|
||||
|
||||
@Test
|
||||
fun `guessed data`() = runTest {
|
||||
create<FakeAirPodsPro>("07 13 01 0E 20 71 AA 37 36 00 10 00 FF 64 FF 00 00 00 00 00 00") {
|
||||
pubPrefix shouldBe 0x01.toUByte()
|
||||
pubDeviceModel shouldBe 0x0E20.toUShort()
|
||||
pubStatus shouldBe 0x71.toUByte()
|
||||
pubPodsBattery shouldBe 0xAA.toUByte()
|
||||
pubFlags shouldBe 0x3.toUShort()
|
||||
pubCaseBattery shouldBe 0x7.toUShort()
|
||||
pubCaseLidState shouldBe 0x36.toUByte()
|
||||
pubDeviceColor shouldBe 0x00.toUByte()
|
||||
pubSuffix shouldBe 0x10.toUByte()
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
isLeftPodCharging shouldBe true
|
||||
isRightPodCharging shouldBe true
|
||||
|
||||
isLeftPodInEar shouldBe false
|
||||
isRightPodInEar shouldBe false
|
||||
|
||||
isCaseCharging shouldBe false
|
||||
|
||||
batteryCasePercent shouldBe 0.7f
|
||||
|
||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_PRO
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package testhelpers
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package testhelpers
|
||||
|
||||
class IsAUnitTest
|
||||
@@ -0,0 +1,23 @@
|
||||
package testhelpers.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)
|
||||
@@ -0,0 +1,38 @@
|
||||
package testhelpers.coroutine
|
||||
|
||||
import eu.darken.capod.common.debug.logging.asLog
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
fun runTest2(
|
||||
autoCancel: Boolean = false,
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
expectedError: KClass<out Throwable>? = null,
|
||||
testBody: suspend TestScope.() -> Unit
|
||||
) {
|
||||
try {
|
||||
val scope = TestScope(context = context)
|
||||
try {
|
||||
scope.runTest {
|
||||
testBody()
|
||||
if (autoCancel) scope.cancel("autoCancel")
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
val isExpected = expectedError?.isInstance(e) ?: false
|
||||
if (!isExpected) throw e
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
if (e.message == "autoCancel" && autoCancel) {
|
||||
io.kotest.mpp.log { "Test was auto-cancelled ${e.asLog()}" }
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package testhelpers.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.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
||||
fun <T> Flow<T>.test(
|
||||
tag: String? = null,
|
||||
scope: CoroutineScope
|
||||
): TestCollector<T> = createTest(tag ?: "FlowTest").start(scope = scope)
|
||||
|
||||
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) job.cancel()
|
||||
try {
|
||||
job.join()
|
||||
} catch (e: Exception) {
|
||||
error = e
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun assertNoErrors() = apply {
|
||||
awaitFinal()
|
||||
require(error == null) { "Error was not null: $error" }
|
||||
}
|
||||
|
||||
suspend fun cancelAndJoin() {
|
||||
if (job.isCompleted) throw IllegalStateException("Flow is already canceled.")
|
||||
|
||||
job.cancelAndJoin()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package testhelpers.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) }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package testhelpers.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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package testhelpers.logging
|
||||
|
||||
import eu.darken.capod.common.debug.logging.Logging
|
||||
|
||||
class JUnitLogger(private val minLogLevel: Logging.Priority = Logging.Priority.VERBOSE) : Logging.Logger {
|
||||
|
||||
override fun isLoggable(priority: Logging.Priority): Boolean = priority.intValue >= minLogLevel.intValue
|
||||
|
||||
override fun log(priority: Logging.Priority, tag: String, message: String, metaData: Map<String, Any>?) {
|
||||
println("${System.currentTimeMillis()} ${priority.shortLabel}/$tag: $message")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package testhelpers.preferences
|
||||
|
||||
import eu.darken.capod.common.preferences.FlowPreference
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
fun <T> mockFlowPreference(
|
||||
defaultValue: T
|
||||
): FlowPreference<T> {
|
||||
val instance = mockk<FlowPreference<T>>()
|
||||
val flow = MutableStateFlow(defaultValue)
|
||||
every { instance.flow } answers { flow }
|
||||
every { instance.value } answers { flow.value }
|
||||
every { instance.update(any()) } answers {
|
||||
val updateCall = arg<(T) -> T>(0)
|
||||
flow.value = updateCall(flow.value)
|
||||
}
|
||||
|
||||
return instance
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package testhelpers.preferences
|
||||
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class MockSharedPreferences : SharedPreferences {
|
||||
private val listeners = mutableListOf<SharedPreferences.OnSharedPreferenceChangeListener>()
|
||||
private val dataMap = mutableMapOf<String, Any>()
|
||||
val dataMapPeek: Map<String, Any>
|
||||
get() = dataMap.toMap()
|
||||
|
||||
override fun getAll(): MutableMap<String, *> = dataMap
|
||||
|
||||
override fun getString(key: String, defValue: String?): String? =
|
||||
dataMap[key] as? String ?: defValue
|
||||
|
||||
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String> {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getInt(key: String, defValue: Int): Int =
|
||||
dataMap[key] as? Int ?: defValue
|
||||
|
||||
override fun getLong(key: String, defValue: Long): Long =
|
||||
dataMap[key] as? Long ?: defValue
|
||||
|
||||
override fun getFloat(key: String, defValue: Float): Float {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String, defValue: Boolean): Boolean =
|
||||
dataMap[key] as? Boolean ?: defValue
|
||||
|
||||
override fun contains(key: String): Boolean = dataMap.contains(key)
|
||||
|
||||
override fun edit(): SharedPreferences.Editor = createEditor(dataMap.toMap()) { newData ->
|
||||
dataMap.clear()
|
||||
dataMap.putAll(newData)
|
||||
}
|
||||
|
||||
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
private fun createEditor(
|
||||
toEdit: Map<String, Any>,
|
||||
onSave: (Map<String, Any>) -> Unit
|
||||
): SharedPreferences.Editor {
|
||||
return object : SharedPreferences.Editor {
|
||||
private val editorData = toEdit.toMutableMap()
|
||||
override fun putString(key: String, value: String?): SharedPreferences.Editor = apply {
|
||||
value?.let { editorData[key] = it } ?: editorData.remove(key)
|
||||
}
|
||||
|
||||
override fun putStringSet(
|
||||
key: String?,
|
||||
values: MutableSet<String>?
|
||||
): SharedPreferences.Editor {
|
||||
throw NotImplementedError()
|
||||
}
|
||||
|
||||
override fun putInt(key: String, value: Int): SharedPreferences.Editor = apply {
|
||||
editorData[key] = value
|
||||
}
|
||||
|
||||
override fun putLong(key: String, value: Long): SharedPreferences.Editor = apply {
|
||||
editorData[key] = value
|
||||
}
|
||||
|
||||
override fun putFloat(key: String, value: Float): SharedPreferences.Editor = apply {
|
||||
editorData[key] = value
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor = apply {
|
||||
editorData[key] = value
|
||||
}
|
||||
|
||||
override fun remove(key: String): SharedPreferences.Editor = apply {
|
||||
editorData.remove(key)
|
||||
}
|
||||
|
||||
override fun clear(): SharedPreferences.Editor = apply {
|
||||
editorData.clear()
|
||||
}
|
||||
|
||||
override fun commit(): Boolean {
|
||||
onSave(editorData)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun apply() {
|
||||
onSave(editorData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package testhelpers.preferences
|
||||
|
||||
import androidx.core.content.edit
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.junit.jupiter.api.Test
|
||||
import testhelpers.BaseTest
|
||||
|
||||
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