From 8a22afc42de361a828db2f95bea1ecfb54b5994b Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 14:42:47 +0100 Subject: [PATCH] Add controlled host fixtures for Android network listeners --- .../main/kotlin/android/content/Context.kt | 9 ++++- .../main/kotlin/android/net/HostNetwork.kt | 35 +++++++++++++++++++ .../src/main/kotlin/android/os/HostOs.kt | 23 +++++++++--- 3 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 tools/chatgui-android/router-tests/src/main/kotlin/android/net/HostNetwork.kt diff --git a/tools/chatgui-android/router-tests/src/main/kotlin/android/content/Context.kt b/tools/chatgui-android/router-tests/src/main/kotlin/android/content/Context.kt index 2587c39d..284690db 100644 --- a/tools/chatgui-android/router-tests/src/main/kotlin/android/content/Context.kt +++ b/tools/chatgui-android/router-tests/src/main/kotlin/android/content/Context.kt @@ -1,14 +1,21 @@ package android.content import android.media.AudioManager +import android.net.ConnectivityManager -/** Только host-тест: один управляемый AudioManager, без устройств/разрешений ОС. */ +/** Только host-тест: управляемые AudioManager/ConnectivityManager, без устройств/разрешений ОС. */ class Context { companion object { const val AUDIO_SERVICE = "audio" } var permissionGranted = true + var connectivityManager: ConnectivityManager? = ConnectivityManager() + val applicationContext: Context get() = this fun getSystemService(name: String): Any { check(name == AUDIO_SERVICE) return AudioManager.shared } + fun getSystemService(type: Class): T? { + check(type == ConnectivityManager::class.java) + return type.cast(connectivityManager) + } fun checkSelfPermission(name: String): Int = if (permissionGranted) 0 else -1 } diff --git a/tools/chatgui-android/router-tests/src/main/kotlin/android/net/HostNetwork.kt b/tools/chatgui-android/router-tests/src/main/kotlin/android/net/HostNetwork.kt new file mode 100644 index 00000000..cd01fafb --- /dev/null +++ b/tools/chatgui-android/router-tests/src/main/kotlin/android/net/HostNetwork.kt @@ -0,0 +1,35 @@ +package android.net + +/** Только host-тест: listener принадлежит конкретному manager; старые callbacks тест может доставить после удаления. */ +class ConnectivityManager { + fun interface OnNetworkActiveListener { fun onNetworkActive() } + val listeners = mutableListOf() + val history = mutableListOf() + var addAttempts = 0 + var removeAttempts = 0 + var failAdd = false + var failRemove = false + var failQuery = false + var active = false + + fun addDefaultNetworkActiveListener(listener: OnNetworkActiveListener) { + addAttempts++ + if (failAdd) throw SecurityException("network listener registration denied") + check(listener !in listeners) + listeners.add(listener) + history.add(listener) + } + + fun removeDefaultNetworkActiveListener(listener: OnNetworkActiveListener) { + removeAttempts++ + if (failRemove) throw IllegalStateException("network listener removal failed") + check(listeners.remove(listener)) { "listener belongs to another manager or was already removed" } + } + + val isDefaultNetworkActive: Boolean get() { + if (failQuery) throw IllegalStateException("network activity query failed") + return active + } + + fun deliver(index: Int) { history[index].onNetworkActive() } +} diff --git a/tools/chatgui-android/router-tests/src/main/kotlin/android/os/HostOs.kt b/tools/chatgui-android/router-tests/src/main/kotlin/android/os/HostOs.kt index 02ee3b38..2bf6004f 100644 --- a/tools/chatgui-android/router-tests/src/main/kotlin/android/os/HostOs.kt +++ b/tools/chatgui-android/router-tests/src/main/kotlin/android/os/HostOs.kt @@ -1,7 +1,22 @@ package android.os -/** Host-модель API36; callbacks доставляет тест явно, все времена монотонные. */ -object Build { object VERSION { const val SDK_INT = 36 }; object VERSION_CODES { const val S = 31 } } -class Looper { companion object { fun getMainLooper() = Looper() } } -class Handler(val looper: Looper) +import java.util.concurrent.ConcurrentLinkedQueue + +/** Host-модель SDK/главной очереди: callbacks и их доставку тест контролирует явно, без настоящего Android Looper. */ +object Build { object VERSION { var SDK_INT = 36 }; object VERSION_CODES { const val P = 28; const val S = 31 } } +class Looper private constructor() { + private val tasks = ConcurrentLinkedQueue() + internal fun post(task: Runnable): Boolean { tasks.add(task); return true } + fun dispatchAll() { + check(myLooper() === this) + while (true) (tasks.poll() ?: return).run() + } + companion object { + private val main = Looper() + private val owner = Thread.currentThread() + fun getMainLooper() = main + fun myLooper(): Looper? = if (Thread.currentThread() === owner) main else null + } +} +class Handler(val looper: Looper) { fun post(task: Runnable): Boolean = looper.post(task) } object SystemClock { fun elapsedRealtime(): Long = System.nanoTime() / 1_000_000 }