Network/test/dorkboxTest/network/BaseTest.kt

363 lines
13 KiB
Kotlin
Raw Normal View History

2020-08-19 15:29:35 +02:00
/*
2023-03-01 00:22:11 +01:00
* Copyright 2023 dorkbox, llc
2020-08-19 15:29:35 +02:00
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkboxTest.network
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.classic.joran.JoranConfigurator
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.ConsoleAppender
2023-08-21 02:20:41 +02:00
import dorkbox.hex.toHexString
import dorkbox.network.*
2023-05-28 18:41:46 +02:00
import dorkbox.network.aeron.AeronDriver
import dorkbox.network.connection.Connection
import dorkbox.network.connection.EndPoint
import dorkbox.os.OS
2021-08-23 08:39:55 +02:00
import dorkbox.storage.Storage
import dorkbox.util.entropy.Entropy
import dorkbox.util.entropy.SimpleEntropy
import dorkbox.util.exceptions.InitializationException
import kotlinx.coroutines.DelicateCoroutinesApi
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.slf4j.LoggerFactory
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.util.concurrent.*
2023-06-25 17:25:29 +02:00
@OptIn(DelicateCoroutinesApi::class)
abstract class BaseTest {
companion object {
const val LOCALHOST = "localhost"
2023-10-26 21:19:50 +02:00
const val DEBUG = false
2022-08-02 21:15:02 +02:00
// wait minimum of 3 minutes before we automatically fail the unit test.
var AUTO_FAIL_TIMEOUT: Long = if (DEBUG) 9999999999L else 180L
init {
if (OS.javaVersion >= 9) {
// disableAccessWarnings
try {
val unsafeClass = Class.forName("sun.misc.Unsafe")
val field: Field = unsafeClass.getDeclaredField("theUnsafe")
field.isAccessible = true
val unsafe: Any = field.get(null)
val putObjectVolatile: Method = unsafeClass.getDeclaredMethod("putObjectVolatile", Any::class.java, Long::class.javaPrimitiveType, Any::class.java)
val staticFieldOffset: Method = unsafeClass.getDeclaredMethod("staticFieldOffset", Field::class.java)
val loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger")
val loggerField: Field = loggerClass.getDeclaredField("logger")
val offset = staticFieldOffset.invoke(unsafe, loggerField) as Long
putObjectVolatile.invoke(unsafe, loggerClass, offset, null)
} catch (ignored: Exception) {
}
}
// if (System.getProperty("logback.configurationFile") == null) {
// val file = File("logback.xml")
// if (file.canRead()) {
// System.setProperty("logback.configurationFile", file.toPath().toRealPath().toFile().toString())
// } else {
// System.setProperty("logback.configurationFile", "logback.xml")
// }
// }
2023-03-01 00:22:11 +01:00
// setLogLevel(Level.TRACE)
// setLogLevel(Level.ERROR)
// setLogLevel(Level.DEBUG)
// we want our entropy generation to be simple (ie, no user interaction to generate)
try {
Entropy.init(SimpleEntropy::class.java)
} catch (e: InitializationException) {
e.printStackTrace()
}
}
2023-09-04 00:47:46 +02:00
fun pause(timeToSleep: Long) {
Thread.sleep(timeToSleep)
}
fun clientConfig(block: Configuration.() -> Unit = {}): ClientConfiguration {
2021-04-30 18:22:38 +02:00
val configuration = ClientConfiguration()
2023-07-11 15:51:28 +02:00
configuration.appId = "network_test"
configuration.tag = "**Client**"
2021-08-23 08:39:55 +02:00
configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk!
2021-04-30 18:22:38 +02:00
configuration.enableIpc = false
2023-06-25 17:25:29 +02:00
configuration.enableIPv6 = false
2021-04-30 18:22:38 +02:00
2021-04-29 11:26:34 +02:00
block(configuration)
return configuration
}
fun serverConfig(block: ServerConfiguration.() -> Unit = {}): ServerConfiguration {
val configuration = ServerConfiguration()
2023-07-11 15:51:28 +02:00
configuration.appId = "network_test"
2021-08-23 08:39:55 +02:00
configuration.settingsStore = Storage.Memory() // don't want to persist anything on disk!
2021-04-30 18:22:38 +02:00
configuration.enableIpc = false
2023-06-25 17:25:29 +02:00
configuration.enableIPv6 = false
configuration.maxClientCount = 50
configuration.maxConnectionsPerIpAddress = 50
2021-04-29 11:26:34 +02:00
block(configuration)
return configuration
}
2021-04-30 18:22:38 +02:00
fun setLogLevel(level: Level) {
2021-07-26 20:16:10 +02:00
println("Log level: $level")
2021-04-30 18:22:38 +02:00
// assume SLF4J is bound to logback in the current environment
val rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger
rootLogger.detachAndStopAllAppenders()
2021-04-30 18:22:38 +02:00
val context = rootLogger.loggerContext
val jc = JoranConfigurator()
2021-04-30 18:22:38 +02:00
jc.context = context
// jc.doConfigure(File("logback.xml").absoluteFile)
context.reset() // override default configuration
val encoder = PatternLayoutEncoder()
encoder.context = context
2023-03-17 15:05:59 +01:00
encoder.pattern = "%date{HH:mm:ss.SSS} %-5level [%logger{35}] [%t] %msg%n"
encoder.start()
val consoleAppender = ConsoleAppender<ILoggingEvent>()
consoleAppender.context = context
consoleAppender.encoder = encoder
consoleAppender.start()
rootLogger.addAppender(consoleAppender)
// modify the level AFTER we setup the context!
rootLogger.level = level
// we only want error messages
val nettyLogger = LoggerFactory.getLogger("io.netty") as Logger
nettyLogger.level = Level.ERROR
// we only want error messages
val kryoLogger = LoggerFactory.getLogger("com.esotericsoftware") as Logger
kryoLogger.level = Level.ERROR
2021-04-30 18:22:38 +02:00
}
}
2021-04-30 18:22:38 +02:00
@Volatile
private var autoFailThread: Thread? = null
private val endPointConnections: MutableList<EndPoint<*>> = CopyOnWriteArrayList()
@Volatile
private var isStopping = false
2023-05-28 18:41:46 +02:00
private val logger: org.slf4j.Logger = LoggerFactory.getLogger(this.javaClass.simpleName)!!
2020-09-03 15:01:24 +02:00
init {
2023-05-28 18:41:46 +02:00
setLogLevel(Level.TRACE)
2023-10-24 13:46:28 +02:00
if (DEBUG) {
logger.error("---- " + this.javaClass.simpleName + " :: DEBUG UNIT TESTS ENABLED")
} else {
logger.error("---- " + this.javaClass.simpleName)
}
2023-06-25 17:25:29 +02:00
AeronDriver.checkForMemoryLeaks()
2020-09-03 15:01:24 +02:00
}
2023-05-28 18:41:46 +02:00
fun addEndPoint(endPoint: EndPoint<*>, runCheck: Boolean = true) {
if (runCheck && !endPoint.ensureStopped()) {
throw IllegalStateException("Unable to continue, AERON was unable to stop.")
}
endPoint.onInit { logger.error("UNIT TEST: init $id (${uuid.toHexString()})") }
endPoint.onConnect { logger.error("UNIT TEST: connect $id (${uuid.toHexString()})") }
endPoint.onDisconnect { logger.error("UNIT TEST: disconnect $id (${uuid.toHexString()})") }
2023-05-28 18:41:46 +02:00
endPoint.onError { logger.error("UNIT TEST: ERROR! $id (${uuid.toHexString()})", it) }
2023-05-28 18:41:46 +02:00
endPoint.onError {
logger.error("UNIT TEST: ERROR! $id (${uuid.toHexString()})", it)
Assert.fail("Exception caught, and it shouldn't have happened!")
}
2023-05-28 18:41:46 +02:00
endPointConnections.add(endPoint)
}
/**
* Immediately stop the endpoints. DOES NOT WAIT FOR THEM TO CLOSE!
2023-06-16 14:15:27 +02:00
*
* Can stop from inside different callbacks
* - message (network event poller)
* - connect (eventdispatch.connect)
* - disconnect (eventdispatch.connect)
2023-05-28 18:41:46 +02:00
*/
2023-09-04 00:47:46 +02:00
fun stopEndPoints(stopAfterMillis: Long = 0L) {
if (isStopping) {
return
}
2023-06-25 17:25:29 +02:00
// if (EventDispatcher.isCurrentEvent()) {
// val mutex = Mutex(true)
//
// // we want to redispatch, in the event we are already running inside the event dispatch
// // this gives us the chance to properly exit/close WITHOUT blocking currentEventDispatch
// // during the `waitForClose()` call
// GlobalScope.launch {
// stopEndPoints(stopAfterMillis)
// mutex.unlock()
// }
//
// runBlocking {
// mutex.withLock { }
// }
//
// return
// }
2023-06-25 17:25:29 +02:00
isStopping = true
2021-07-26 20:16:10 +02:00
if (stopAfterMillis > 0L) {
2023-09-04 00:47:46 +02:00
Thread.sleep(stopAfterMillis)
2021-07-26 20:16:10 +02:00
}
2023-05-28 18:41:46 +02:00
val clients = endPointConnections.filterIsInstance<Client<Connection>>()
val servers = endPointConnections.filterIsInstance<Server<Connection>>()
logger.error("Unit test shutting down ${clients.size} clients...")
logger.error("Unit test shutting down ${servers.size} server...")
// shutdown clients first
logger.error("Closing clients...")
2023-05-28 18:41:46 +02:00
clients.forEach { endPoint ->
2023-06-25 17:25:29 +02:00
endPoint.close()
}
logger.error("NOT WAITING FOR CLIENT CLOSE.")
2023-05-28 18:41:46 +02:00
2021-07-26 20:16:10 +02:00
// shutdown everything else (should only be servers) last
logger.error("Closing servers...")
2023-05-28 18:41:46 +02:00
servers.forEach {
2023-06-25 17:25:29 +02:00
it.close()
}
logger.error("NOT WAITING FOR SERVER CLOSE.")
logger.error("Closed endpoints...")
}
/**
2023-05-28 18:41:46 +02:00
* Wait for network client/server threads to shut down for the specified time. 0 will wait forever
*
* it should close as close to naturally as possible, otherwise there are problems
*
* @param stopAfterSeconds how many seconds to wait, the default is 2 minutes.
*/
fun waitForThreads(stopAfterSeconds: Long = AUTO_FAIL_TIMEOUT) {
2023-06-25 17:25:29 +02:00
val clients = endPointConnections.filterIsInstance<Client<Connection>>()
val servers = endPointConnections.filterIsInstance<Server<Connection>>()
val timeoutMS = TimeUnit.SECONDS.toMillis(stopAfterSeconds)
var successClients = true
var successServers = true
2023-06-25 17:25:29 +02:00
clients.forEach { endPoint ->
successClients = successClients && endPoint.waitForClose(timeoutMS)
2023-06-25 17:25:29 +02:00
}
servers.forEach { endPoint ->
successServers = successServers && endPoint.waitForClose(timeoutMS)
}
clients.forEach { endPoint ->
2023-06-25 17:25:29 +02:00
endPoint.stopDriver()
}
servers.forEach { endPoint ->
endPoint.stopDriver()
}
// run actions before we actually shutdown, but after we wait
if (!successClients || !successServers) {
2023-10-24 13:46:28 +02:00
Assert.fail("Shutdown latch not triggered ($successClients|$successServers)!")
2023-05-28 18:41:46 +02:00
}
// we must always make sure that aeron is shut-down before starting again.
clients.forEach { endPoint ->
endPoint.ensureStopped()
if (!Client.ensureStopped(endPoint.config.copy())) {
throw IllegalStateException("Unable to continue, AERON client was unable to stop.")
}
}
servers.forEach { endPoint ->
endPoint.ensureStopped()
if (!Client.ensureStopped(endPoint.config.copy())) {
throw IllegalStateException("Unable to continue, AERON server was unable to stop.")
}
}
if (!AeronDriver.areAllInstancesClosed(logger)) {
throw RuntimeException("Unable to shutdown! There are still Aeron drivers loaded!")
2023-05-28 18:41:46 +02:00
}
logger.error("UNIT TEST, checking driver and memory leaks")
// have to make sure that the aeron driver is CLOSED.
Assert.assertTrue("The aeron drivers are not fully closed!", AeronDriver.areAllInstancesClosed())
2023-06-25 17:25:29 +02:00
AeronDriver.checkForMemoryLeaks()
2023-05-28 18:41:46 +02:00
endPointConnections.clear()
logger.error("Finished shutting down all endpoints... ($successClients, $successServers)")
}
@Before
fun setupFailureCheck() {
2023-05-28 18:41:46 +02:00
autoFailThread = Thread({
// not the best, but this works for our purposes. This is a TAD hacky, because we ALSO have to make sure that we
// ARE NOT in the same thread group as netty!
try {
Thread.sleep(AUTO_FAIL_TIMEOUT * 1000L)
// if the thread is interrupted, then it means we finished the test.
2023-05-28 18:41:46 +02:00
LoggerFactory.getLogger(this.javaClass.simpleName).error("Test did not complete in a timely manner...")
stopEndPoints()
waitForThreads()
Assert.fail("Test did not complete in a timely manner.")
} catch (ignored: InterruptedException) {
}
}, "UnitTest timeout fail condition")
autoFailThread!!.isDaemon = true
// autoFailThread.start();
}
@After
fun cancelFailureCheck() {
if (autoFailThread != null) {
autoFailThread!!.interrupt()
autoFailThread = null
}
}
}