Add a fancy test system for async methods

Written in order to ~avoid working on~ test #695. Sadly, no luck there.
This commit is contained in:
Jonathan Coates 2021-07-25 16:25:59 +01:00
parent 3a80b51a9f
commit d50db8a6f3
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
3 changed files with 173 additions and 0 deletions

View File

@ -62,6 +62,7 @@ interface IPeripheralChangeListener
@Nullable
IPeripheral getPeripheral( ComputerSide side );
@Nullable
String getLabel();
void setLabel( @Nullable String label );

View File

@ -0,0 +1,121 @@
package dan200.computercraft.core.apis
import dan200.computercraft.ComputerCraft
import dan200.computercraft.api.lua.ILuaAPI
import dan200.computercraft.api.lua.MethodResult
import dan200.computercraft.api.peripheral.IPeripheral
import dan200.computercraft.api.peripheral.IWorkMonitor
import dan200.computercraft.core.computer.BasicEnvironment
import dan200.computercraft.core.computer.ComputerSide
import dan200.computercraft.core.computer.IComputerEnvironment
import dan200.computercraft.core.filesystem.FileSystem
import dan200.computercraft.core.terminal.Terminal
import dan200.computercraft.core.tracking.TrackingField
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.seconds
abstract class NullApiEnvironment : IAPIEnvironment {
private val computerEnv = BasicEnvironment()
override fun getComputerID(): Int = 0
override fun getComputerEnvironment(): IComputerEnvironment = computerEnv
override fun getMainThreadMonitor(): IWorkMonitor = throw IllegalStateException("Work monitor not available")
override fun getTerminal(): Terminal = throw IllegalStateException("Terminal not available")
override fun getFileSystem(): FileSystem = throw IllegalStateException("Terminal not available")
override fun shutdown() {}
override fun reboot() {}
override fun setOutput(side: ComputerSide?, output: Int) {}
override fun getOutput(side: ComputerSide?): Int = 0
override fun getInput(side: ComputerSide?): Int = 0
override fun setBundledOutput(side: ComputerSide?, output: Int) {}
override fun getBundledOutput(side: ComputerSide?): Int = 0
override fun getBundledInput(side: ComputerSide?): Int = 0
override fun setPeripheralChangeListener(listener: IAPIEnvironment.IPeripheralChangeListener?) {}
override fun getPeripheral(side: ComputerSide?): IPeripheral? = null
override fun getLabel(): String? = null
override fun setLabel(label: String?) {}
override fun startTimer(ticks: Long): Int = 0
override fun cancelTimer(id: Int) {}
override fun addTrackingChange(field: TrackingField, change: Long) {}
}
class EventResult(val name: String, val args: Array<Any?>)
class AsyncRunner : NullApiEnvironment() {
private val eventStream: Channel<Array<Any?>> = Channel(Int.MAX_VALUE)
private val apis: MutableList<ILuaAPI> = mutableListOf()
override fun queueEvent(event: String?, vararg args: Any?) {
ComputerCraft.log.debug("Queue event $event ${args.contentToString()}")
if (!eventStream.offer(arrayOf(event, *args))) {
throw IllegalStateException("Queue is full")
}
}
override fun shutdown() {
super.shutdown()
eventStream.close()
apis.forEach { it.shutdown() }
}
fun <T : ILuaAPI> addApi(api: T): T {
apis.add(api)
api.startup()
return api
}
suspend fun resultOf(toRun: MethodResult): Array<Any?> {
var running = toRun
while (running.callback != null) running = runOnce(running)
return running.result ?: empty
}
private suspend fun runOnce(obj: MethodResult): MethodResult {
val callback = obj.callback ?: throw NullPointerException("Callback cannot be null")
val result = obj.result
val filter: String? = if (result.isNullOrEmpty() || result[0] !is String) {
null
} else {
result[0] as String
}
return callback.resume(pullEventImpl(filter))
}
private suspend fun pullEventImpl(filter: String?): Array<Any?> {
for (event in eventStream) {
ComputerCraft.log.debug("Pulled event ${event.contentToString()}")
val eventName = event[0] as String
if (filter == null || eventName == filter || eventName == "terminate") return event
}
throw IllegalStateException("No more events")
}
suspend fun pullEvent(filter: String? = null): EventResult {
val result = pullEventImpl(filter)
return EventResult(result[0] as String, result.copyOfRange(1, result.size))
}
companion object {
private val empty: Array<Any?> = arrayOf()
@OptIn(ExperimentalTime::class)
fun runTest(timeout: Duration = 5.seconds, fn: suspend AsyncRunner.() -> Unit) {
runBlocking {
val runner = AsyncRunner()
try {
withTimeout(timeout) { fn(runner) }
} finally {
runner.shutdown()
}
}
}
}
}

View File

@ -0,0 +1,51 @@
package dan200.computercraft.core.apis.http.options
import dan200.computercraft.ComputerCraft
import dan200.computercraft.core.apis.AsyncRunner
import dan200.computercraft.core.apis.HTTPAPI
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertArrayEquals
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import java.util.*
@Disabled("Requires some setup locally.")
class TestHttpApi {
companion object {
private const val WS_ADDRESS = "ws://127.0.0.1:8080"
@JvmStatic
@BeforeAll
fun before() {
ComputerCraft.httpRules = listOf(AddressRule.parse("*", null, Action.ALLOW.toPartial()))
}
@JvmStatic
@AfterAll
fun after() {
ComputerCraft.httpRules = Collections.unmodifiableList(
listOf(
AddressRule.parse("\$private", null, Action.DENY.toPartial()),
AddressRule.parse("*", null, Action.ALLOW.toPartial())
)
)
}
}
@Test
fun `Connects to websocket`() {
AsyncRunner.runTest {
val httpApi = addApi(HTTPAPI(this))
val result = httpApi.websocket(WS_ADDRESS, Optional.empty())
assertArrayEquals(arrayOf(true), result, "Should have created websocket")
val event = pullEvent()
assertEquals("websocket_success", event.name) {
"Websocket failed to connect: ${event.args.contentToString()}"
}
}
}
}