feat: сетевая часть для app-phone (WS-сервер) и app-glasses (WS-клиент), PhoneState/PhoneActions → lib-core
lib-core: - core/phone/: PhoneState, PhoneActions, GlassesSender (интерфейс) — перенесены из app-host - core/net/: GlassesWsClient — KMP WS-клиент очков (реконнект, Hello, send GlassesToHost) - GlassesWsClientTest + PhoneActionsTest (перенесён) app-phone (Android): - GlassesServer: Ktor WS-сервер (CIO) 0.0.0.0:8080/ws/glasses, принимает очки, Welcome из стейта - PhoneApp (Application): клиенты, стейт, действия, сервер - PhoneConfig: креды Jellyfin/Mirror - MainActivity: статус сервера (порт, счётчик подключений) - GlassesServerTest: hello→welcome, gesture app-glasses (Android): - GlassesApp (Application), HostConnection: WS-клиент к телефону, Hello, реконнект, обработка HostToGlasses (PlayVideo/SetMode и т.д.), StateFlow для UI - MainActivity: статус подключения app-host: PhoneActions/PhoneState удалены (теперь lib-core), адаптирован под GlassesSender 171 тест, 0 провалов; app-host жив (catalog/watch/status работают через lib-core)
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package pw.binom.viewmate.phone
|
||||
|
||||
import io.ktor.client.plugins.websocket.WebSockets
|
||||
import io.ktor.client.plugins.websocket.webSocket
|
||||
import io.ktor.server.testing.testApplication
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.readText
|
||||
import io.ktor.websocket.send
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.cancelAndJoin
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import pw.binom.viewmate.core.GlassesMode
|
||||
import pw.binom.viewmate.core.phone.PhoneState
|
||||
import pw.binom.viewmate.core.protocol.Gesture
|
||||
import pw.binom.viewmate.core.protocol.GlassesStatus
|
||||
import pw.binom.viewmate.core.protocol.GlassesToHost
|
||||
import pw.binom.viewmate.core.protocol.Hello
|
||||
import pw.binom.viewmate.core.protocol.HostToGlasses
|
||||
import pw.binom.viewmate.core.protocol.SetMode
|
||||
import pw.binom.viewmate.core.protocol.Welcome
|
||||
import pw.binom.viewmate.core.protocol.protocolJson
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.fail
|
||||
|
||||
class GlassesServerTest {
|
||||
|
||||
private val json = protocolJson
|
||||
|
||||
@Test
|
||||
fun helloGetsWelcome() = testApplication {
|
||||
val hub = GlassesHub(PhoneState())
|
||||
application { glassesServerModule(hub) }
|
||||
val client = createClient { install(WebSockets) }
|
||||
|
||||
client.webSocket(GLASSES_WS_PATH) {
|
||||
send(json.encodeToString(GlassesToHost.serializer(), Hello(appVersion = "0.1.0")))
|
||||
val frame = incoming.receive() as Frame.Text
|
||||
val welcome = json.decodeFromString(HostToGlasses.serializer(), frame.readText())
|
||||
assertIs<Welcome>(welcome)
|
||||
assertEquals(GlassesMode.MOVIE, welcome.mode)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun gestureIsProcessed() = testApplication {
|
||||
val hub = GlassesHub(PhoneState())
|
||||
application { glassesServerModule(hub) }
|
||||
val client = createClient { install(WebSockets) }
|
||||
|
||||
client.webSocket(GLASSES_WS_PATH) {
|
||||
send(json.encodeToString(GlassesToHost.serializer(), Gesture(gesture = "DoubleClick")))
|
||||
send(json.encodeToString(GlassesToHost.serializer(), GlassesStatus(70, 10.5, 32.0)))
|
||||
}
|
||||
awaitTrue { hub.gesturesReceived.value == 1 }
|
||||
awaitTrue { hub.connected.value == 0 }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun connectedCounterTracksSessions() = testApplication {
|
||||
val hub = GlassesHub(PhoneState())
|
||||
application { glassesServerModule(hub) }
|
||||
val client = createClient { install(WebSockets) }
|
||||
|
||||
client.webSocket(GLASSES_WS_PATH) { }
|
||||
awaitTrue { hub.connected.value == 0 }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun broadcastReachesAllGlasses() = testApplication {
|
||||
val hub = GlassesHub(PhoneState())
|
||||
application { glassesServerModule(hub) }
|
||||
val client = createClient { install(WebSockets) }
|
||||
|
||||
coroutineScope {
|
||||
val opened = Channel<Unit>(2)
|
||||
val received = Channel<HostToGlasses>(2)
|
||||
|
||||
val jobs = (1..2).map {
|
||||
launch {
|
||||
client.webSocket(GLASSES_WS_PATH) {
|
||||
opened.send(Unit)
|
||||
while (true) {
|
||||
val frame = incoming.receive()
|
||||
if (frame is Frame.Text) {
|
||||
received.send(json.decodeFromString(HostToGlasses.serializer(), frame.readText()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repeat(2) { opened.receive() }
|
||||
awaitTrue { hub.connected.value == 2 }
|
||||
|
||||
hub.broadcast(SetMode(GlassesMode.CHAT))
|
||||
|
||||
val first = received.receive()
|
||||
val second = received.receive()
|
||||
listOf(first, second).forEach {
|
||||
assertIs<SetMode>(it)
|
||||
assertEquals(GlassesMode.CHAT, it.mode)
|
||||
}
|
||||
|
||||
jobs.forEach { it.cancelAndJoin() }
|
||||
awaitTrue { hub.connected.value == 0 }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun awaitTrue(timeoutMs: Long = 10_000, condition: () -> Boolean) {
|
||||
val deadline = System.currentTimeMillis() + timeoutMs
|
||||
while (!condition()) {
|
||||
if (System.currentTimeMillis() > deadline) fail("условие не выполнено за ${timeoutMs}мс")
|
||||
delay(20)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user