diff --git a/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java index 25960c580..1f6ffe08a 100644 --- a/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java +++ b/src/main/java/dan200/computercraft/core/apis/IAPIEnvironment.java @@ -62,6 +62,7 @@ interface IPeripheralChangeListener @Nullable IPeripheral getPeripheral( ComputerSide side ); + @Nullable String getLabel(); void setLabel( @Nullable String label ); diff --git a/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt b/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt new file mode 100644 index 000000000..936fb6c62 --- /dev/null +++ b/src/test/java/dan200/computercraft/core/apis/AsyncRunner.kt @@ -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) + +class AsyncRunner : NullApiEnvironment() { + private val eventStream: Channel> = Channel(Int.MAX_VALUE) + private val apis: MutableList = 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 addApi(api: T): T { + apis.add(api) + api.startup() + return api + } + + suspend fun resultOf(toRun: MethodResult): Array { + 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 { + 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 = 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() + } + } + } + } +} diff --git a/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt b/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt new file mode 100644 index 000000000..206924cc0 --- /dev/null +++ b/src/test/java/dan200/computercraft/core/apis/http/options/TestHttpApi.kt @@ -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()}" + } + } + } +}