From 97a899c0c37a6833372840ea23031e1d2d3e5992 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 27 Oct 2026 14:55:03 +0100 Subject: [PATCH] Model delayed network callbacks and config workers in host tests --- .../main/kotlin/android/net/HostNetwork.kt | 22 ++++++++++++++++ .../src/main/kotlin/android/os/HostOs.kt | 21 +++++++++++++--- .../utun/chat/data/RouterHostEnvironment.kt | 25 +++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) 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 index cd01fafb..cf1a85e6 100644 --- 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 @@ -1,10 +1,20 @@ package android.net +class Network +class LinkProperties + /** Только host-тест: listener принадлежит конкретному manager; старые callbacks тест может доставить после удаления. */ class ConnectivityManager { + open class NetworkCallback { + open fun onAvailable(network: Network) {} + open fun onLost(network: Network) {} + open fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {} + } fun interface OnNetworkActiveListener { fun onNetworkActive() } val listeners = mutableListOf() val history = mutableListOf() + val callbacks = mutableListOf() + val callbackHistory = mutableListOf() var addAttempts = 0 var removeAttempts = 0 var failAdd = false @@ -32,4 +42,16 @@ class ConnectivityManager { } fun deliver(index: Int) { history[index].onNetworkActive() } + + fun registerDefaultNetworkCallback(callback: NetworkCallback) { + if (failAdd) throw SecurityException("network callback registration denied") + check(callback !in callbacks) + callbacks.add(callback) + callbackHistory.add(callback) + } + + fun unregisterNetworkCallback(callback: NetworkCallback) { + if (failRemove) throw IllegalStateException("network callback removal failed") + check(callbacks.remove(callback)) { "callback belongs to another manager or was already removed" } + } } 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 2bf6004f..627f0fb6 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 @@ -5,11 +5,20 @@ 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 } + private data class Task(val handler: Handler, val runnable: Runnable, val deadline: Long) + private val tasks = ConcurrentLinkedQueue() + @Volatile private var time = 0L + internal fun post(handler: Handler, task: Runnable, delay: Long): Boolean { + tasks.add(Task(handler, task, time + delay)); return true + } + internal fun remove(handler: Handler, task: Runnable) { tasks.removeIf { it.handler === handler && it.runnable === task } } + fun advanceBy(milliseconds: Long) { check(milliseconds >= 0); time += milliseconds; dispatchAll() } fun dispatchAll() { check(myLooper() === this) - while (true) (tasks.poll() ?: return).run() + while (true) { + val next = tasks.filter { it.deadline <= time }.minByOrNull { it.deadline } ?: return + if (tasks.remove(next)) next.runnable.run() + } } companion object { private val main = Looper() @@ -18,5 +27,9 @@ class Looper private constructor() { fun myLooper(): Looper? = if (Thread.currentThread() === owner) main else null } } -class Handler(val looper: Looper) { fun post(task: Runnable): Boolean = looper.post(task) } +class Handler(val looper: Looper) { + fun post(task: Runnable): Boolean = looper.post(this, task, 0) + fun postDelayed(task: Runnable, delay: Long): Boolean = looper.post(this, task, delay) + fun removeCallbacks(task: Runnable) = looper.remove(this, task) +} object SystemClock { fun elapsedRealtime(): Long = System.nanoTime() / 1_000_000 } diff --git a/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt b/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt index c24f6552..2d0c8973 100644 --- a/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt +++ b/tools/chatgui-android/router-tests/src/main/kotlin/com/utun/chat/data/RouterHostEnvironment.kt @@ -26,5 +26,30 @@ object LogManager { /** Только host-тест: считает уведомления standby, не загружает JNI и не запускает ядро. */ object NativeLib { val networkNotifications = AtomicInteger() + var ready = true + var acceptSocketUpdate = true + val socketUpdates = mutableListOf() fun notifyNetworkActive() { networkNotifications.incrementAndGet() } + fun isReady(): Boolean = ready + fun updateSockets(config: String): Boolean { socketUpdates.add(config); return acceptSocketUpdate } +} + +/** Управляемая подготовка конфига: тест удерживает реальный worker, считает перекрытия и затем дожидается его завершения. */ +class HostConfigProvider { + val builds = AtomicInteger() + val inBuild = AtomicInteger() + val maximumConcurrentBuilds = AtomicInteger() + private val workers: MutableSet = Collections.synchronizedSet(mutableSetOf()) + @Volatile var interfaces = listOf("wlan0" to "192.0.2.1") + @Volatile var onBuild: (Int) -> String = { "config-$it" } + + fun getAvailableInterfaces(): List> { workers.add(Thread.currentThread()); return interfaces } + fun buildConfigText(): String { + maximumConcurrentBuilds.accumulateAndGet(inBuild.incrementAndGet(), ::maxOf) + try { return onBuild(builds.incrementAndGet()) } finally { inBuild.decrementAndGet() } + } + fun joinWorkers() { + val snapshot = synchronized(workers) { workers.toList() } + snapshot.forEach { it.join(2_000); check(!it.isAlive) { "config worker did not finish" } } + } }