mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-01-07 07:50:27 +00:00
Move some test support code into testFixtues
This offers very few advantages now, but helps support the following in the future: - Reuse test support code across multiple projects (useful for multi-loader). - Allow using test fixture code in testMod. We've got a version of our gametest which use Kotlin instead of Lua for asserting computer behaviour. We can't use java-test-fixtures here for Forge reasons, so have to roll our own version. Alas. - Add an ILuaMachine implementation which runs Kotlin coroutines instead. We can use this for testing asynchronous APIs. This also replaces the FakeComputerManager. - Move most things in the .support module to .test.core. We need to use a separate package in order to cope with Java 9 modules (again, thanks Forge).
This commit is contained in:
parent
1e88d37004
commit
71f81e1201
@ -1,9 +1,9 @@
|
|||||||
import cc.tweaked.gradle.*
|
import cc.tweaked.gradle.*
|
||||||
import net.darkhax.curseforgegradle.TaskPublishCurseForge
|
import net.darkhax.curseforgegradle.TaskPublishCurseForge
|
||||||
|
import net.minecraftforge.gradle.common.util.RunConfig
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
// Build
|
// Build
|
||||||
alias(libs.plugins.kotlin)
|
|
||||||
alias(libs.plugins.forgeGradle)
|
alias(libs.plugins.forgeGradle)
|
||||||
alias(libs.plugins.mixinGradle)
|
alias(libs.plugins.mixinGradle)
|
||||||
alias(libs.plugins.librarian)
|
alias(libs.plugins.librarian)
|
||||||
@ -18,7 +18,7 @@ plugins {
|
|||||||
|
|
||||||
id("cc-tweaked.illuaminate")
|
id("cc-tweaked.illuaminate")
|
||||||
id("cc-tweaked.node")
|
id("cc-tweaked.node")
|
||||||
id("cc-tweaked.java-convention")
|
id("cc-tweaked.gametest")
|
||||||
id("cc-tweaked")
|
id("cc-tweaked")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,8 +36,6 @@ sourceSets {
|
|||||||
main {
|
main {
|
||||||
resources.srcDir("src/generated/resources")
|
resources.srcDir("src/generated/resources")
|
||||||
}
|
}
|
||||||
|
|
||||||
register("testMod")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
minecraft {
|
minecraft {
|
||||||
@ -76,21 +74,26 @@ minecraft {
|
|||||||
property("cct.pretty-json", "true")
|
property("cct.pretty-json", "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun RunConfig.configureForGameTest() {
|
||||||
|
mods.register("cctest") {
|
||||||
|
source(sourceSets["testMod"])
|
||||||
|
source(sourceSets["testFixtures"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val testClient by registering {
|
val testClient by registering {
|
||||||
workingDirectory(file("run/testClient"))
|
workingDirectory(file("run/testClient"))
|
||||||
parent(client.get())
|
parent(client.get())
|
||||||
|
configureForGameTest()
|
||||||
mods.register("cctest") { source(sourceSets["testMod"]) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val testServer by registering {
|
val testServer by registering {
|
||||||
workingDirectory(file("run/testServer"))
|
workingDirectory(file("run/testServer"))
|
||||||
parent(server.get())
|
parent(server.get())
|
||||||
|
configureForGameTest()
|
||||||
|
|
||||||
property("cctest.run", "true")
|
property("cctest.run", "true")
|
||||||
property("forge.logging.console.level", "info")
|
property("forge.logging.console.level", "info")
|
||||||
|
|
||||||
mods.register("cctest") { source(sourceSets["testMod"]) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,8 +116,6 @@ configurations {
|
|||||||
val shade by registering { isTransitive = false }
|
val shade by registering { isTransitive = false }
|
||||||
implementation { extendsFrom(shade.get()) }
|
implementation { extendsFrom(shade.get()) }
|
||||||
register("cctJavadoc")
|
register("cctJavadoc")
|
||||||
|
|
||||||
named("testModImplementation") { extendsFrom(implementation.get(), testImplementation.get()) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -132,12 +133,13 @@ dependencies {
|
|||||||
|
|
||||||
"shade"(libs.cobalt)
|
"shade"(libs.cobalt)
|
||||||
|
|
||||||
|
testFixturesApi(libs.bundles.test)
|
||||||
|
testFixturesApi(libs.bundles.kotlin)
|
||||||
|
|
||||||
testImplementation(libs.bundles.test)
|
testImplementation(libs.bundles.test)
|
||||||
testImplementation(libs.bundles.kotlin)
|
testImplementation(libs.bundles.kotlin)
|
||||||
testRuntimeOnly(libs.bundles.testRuntime)
|
testRuntimeOnly(libs.bundles.testRuntime)
|
||||||
|
|
||||||
"testModImplementation"(sourceSets.main.get().output)
|
|
||||||
|
|
||||||
"cctJavadoc"(libs.cctJavadoc)
|
"cctJavadoc"(libs.cctJavadoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,7 +207,7 @@ tasks.jar {
|
|||||||
"Specification-Title" to "computercraft",
|
"Specification-Title" to "computercraft",
|
||||||
"Specification-Vendor" to "SquidDev",
|
"Specification-Vendor" to "SquidDev",
|
||||||
"Specification-Version" to "1",
|
"Specification-Version" to "1",
|
||||||
"specificationVersion" to "cctweaked",
|
"Implementation-Title" to "cctweaked",
|
||||||
"Implementation-Version" to modVersion,
|
"Implementation-Version" to modVersion,
|
||||||
"Implementation-Vendor" to "SquidDev",
|
"Implementation-Vendor" to "SquidDev",
|
||||||
)
|
)
|
||||||
|
@ -9,6 +9,7 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.kotlin.plugin)
|
||||||
implementation(libs.spotless)
|
implementation(libs.spotless)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
53
buildSrc/src/main/kotlin/cc-tweaked.gametest.gradle.kts
Normal file
53
buildSrc/src/main/kotlin/cc-tweaked.gametest.gradle.kts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the configurations for writing game tests.
|
||||||
|
*
|
||||||
|
* See notes in [cc.tweaked.gradle.MinecraftConfigurations] for the general design behind these cursed ideas.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("cc-tweaked.kotlin-convention")
|
||||||
|
id("cc-tweaked.java-convention")
|
||||||
|
}
|
||||||
|
|
||||||
|
val main = sourceSets.main.get()
|
||||||
|
|
||||||
|
// Both testMod and testFixtures inherit from the main classpath, just so we have access to Minecraft classes.
|
||||||
|
val testMod by sourceSets.creating {
|
||||||
|
compileClasspath += main.compileClasspath
|
||||||
|
runtimeClasspath += main.runtimeClasspath
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
named(testMod.compileClasspathConfigurationName) {
|
||||||
|
shouldResolveConsistentlyWith(compileClasspath.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
named(testMod.runtimeClasspathConfigurationName) {
|
||||||
|
shouldResolveConsistentlyWith(runtimeClasspath.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like the main test configurations, we're safe to depend on source set outputs.
|
||||||
|
dependencies {
|
||||||
|
add(testMod.implementationConfigurationName, main.output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Similar to java-test-fixtures, but tries to avoid putting the obfuscated jar on the classpath.
|
||||||
|
|
||||||
|
val testFixtures by sourceSets.creating {
|
||||||
|
compileClasspath += main.compileClasspath
|
||||||
|
}
|
||||||
|
|
||||||
|
java.registerFeature("testFixtures") {
|
||||||
|
usingSourceSet(testFixtures)
|
||||||
|
disablePublication()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
add(testFixtures.implementationConfigurationName, main.output)
|
||||||
|
|
||||||
|
testImplementation(testFixtures(project))
|
||||||
|
add(testMod.implementationConfigurationName, testFixtures(project))
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import cc.tweaked.gradle.CCTweakedPlugin
|
||||||
import cc.tweaked.gradle.LicenseHeader
|
import cc.tweaked.gradle.LicenseHeader
|
||||||
import com.diffplug.gradle.spotless.FormatExtension
|
import com.diffplug.gradle.spotless.FormatExtension
|
||||||
import com.diffplug.spotless.LineEnding
|
import com.diffplug.spotless.LineEnding
|
||||||
@ -12,7 +13,7 @@ plugins {
|
|||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion.set(JavaLanguageVersion.of(8))
|
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
withSourcesJar()
|
withSourcesJar()
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import cc.tweaked.gradle.CCTweakedPlugin
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain {
|
||||||
|
languageVersion.set(CCTweakedPlugin.JAVA_VERSION)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType(KotlinCompile::class.java).configureEach {
|
||||||
|
// So technically we shouldn't need to do this as the toolchain sets it above. However, the option only appears
|
||||||
|
// to be set when the task executes, so doesn't get picked up by IDEs.
|
||||||
|
kotlinOptions.jvmTarget = when {
|
||||||
|
CCTweakedPlugin.JAVA_VERSION.asInt() > 8 -> CCTweakedPlugin.JAVA_VERSION.toString()
|
||||||
|
else -> "1.${CCTweakedPlugin.JAVA_VERSION.asInt()}"
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ package cc.tweaked.gradle
|
|||||||
|
|
||||||
import org.gradle.api.Plugin
|
import org.gradle.api.Plugin
|
||||||
import org.gradle.api.Project
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.jvm.toolchain.JavaLanguageVersion
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configures projects to match a shared configuration.
|
* Configures projects to match a shared configuration.
|
||||||
@ -10,4 +11,8 @@ class CCTweakedPlugin : Plugin<Project> {
|
|||||||
override fun apply(project: Project) {
|
override fun apply(project: Project) {
|
||||||
project.extensions.create("cct", CCTweakedExtension::class.java)
|
project.extensions.create("cct", CCTweakedExtension::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val JAVA_VERSION = JavaLanguageVersion.of(8)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,6 +49,7 @@ junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", vers
|
|||||||
# Build tools
|
# Build tools
|
||||||
cctJavadoc = { module = "cc.tweaked:cct-javadoc", version.ref = "cctJavadoc" }
|
cctJavadoc = { module = "cc.tweaked:cct-javadoc", version.ref = "cctJavadoc" }
|
||||||
checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" }
|
checkstyle = { module = "com.puppycrawl.tools:checkstyle", version.ref = "checkstyle" }
|
||||||
|
kotlin-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||||
spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
|
spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.computer;
|
package dan200.computercraft.core.computer;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.core.ComputerContext;
|
import dan200.computercraft.core.ComputerContext;
|
||||||
@ -443,7 +444,8 @@ public final class ComputerThread
|
|||||||
*
|
*
|
||||||
* @return If we have work queued up.
|
* @return If we have work queued up.
|
||||||
*/
|
*/
|
||||||
boolean hasPendingWork()
|
@VisibleForTesting
|
||||||
|
public boolean hasPendingWork()
|
||||||
{
|
{
|
||||||
// FIXME: See comment in scaledPeriod. Again, we access this in multiple threads but not clear if it matters!
|
// FIXME: See comment in scaledPeriod. Again, we access this in multiple threads but not clear if it matters!
|
||||||
return !computerQueue.isEmpty();
|
return !computerQueue.isEmpty();
|
||||||
|
@ -11,16 +11,16 @@ import dan200.computercraft.api.lua.ILuaAPI;
|
|||||||
import dan200.computercraft.api.lua.LuaException;
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
import dan200.computercraft.api.lua.LuaFunction;
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||||
import dan200.computercraft.core.computer.BasicEnvironment;
|
|
||||||
import dan200.computercraft.core.computer.Computer;
|
import dan200.computercraft.core.computer.Computer;
|
||||||
import dan200.computercraft.core.computer.ComputerSide;
|
import dan200.computercraft.core.computer.ComputerSide;
|
||||||
import dan200.computercraft.core.computer.FakeMainThreadScheduler;
|
|
||||||
import dan200.computercraft.core.filesystem.FileMount;
|
import dan200.computercraft.core.filesystem.FileMount;
|
||||||
import dan200.computercraft.core.filesystem.FileSystemException;
|
import dan200.computercraft.core.filesystem.FileSystemException;
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
import dan200.computercraft.core.terminal.Terminal;
|
||||||
import dan200.computercraft.shared.peripheral.modem.ModemState;
|
import dan200.computercraft.shared.peripheral.modem.ModemState;
|
||||||
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral;
|
import dan200.computercraft.shared.peripheral.modem.wireless.WirelessModemPeripheral;
|
||||||
import dan200.computercraft.support.TestFiles;
|
import dan200.computercraft.support.TestFiles;
|
||||||
|
import dan200.computercraft.test.core.computer.BasicEnvironment;
|
||||||
|
import dan200.computercraft.test.core.computer.FakeMainThreadScheduler;
|
||||||
import net.minecraft.util.math.vector.Vector3d;
|
import net.minecraft.util.math.vector.Vector3d;
|
||||||
import net.minecraft.world.World;
|
import net.minecraft.world.World;
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
@ -1,123 +0,0 @@
|
|||||||
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.ComputerEnvironment
|
|
||||||
import dan200.computercraft.core.computer.ComputerSide
|
|
||||||
import dan200.computercraft.core.computer.GlobalEnvironment
|
|
||||||
import dan200.computercraft.core.filesystem.FileSystem
|
|
||||||
import dan200.computercraft.core.metrics.Metric
|
|
||||||
import dan200.computercraft.core.terminal.Terminal
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withTimeout
|
|
||||||
import kotlin.time.Duration
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
|
|
||||||
abstract class NullApiEnvironment : IAPIEnvironment {
|
|
||||||
private val computerEnv = BasicEnvironment()
|
|
||||||
|
|
||||||
override fun getComputerID(): Int = 0
|
|
||||||
override fun getComputerEnvironment(): ComputerEnvironment = computerEnv
|
|
||||||
override fun getGlobalEnvironment(): GlobalEnvironment = 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 observe(field: Metric.Counter) {}
|
|
||||||
override fun observe(field: Metric.Event, 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.trySend(arrayOf(event, *args)).isSuccess) {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -18,7 +18,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import static dan200.computercraft.support.ContramapMatcher.contramap;
|
import static dan200.computercraft.test.core.ContramapMatcher.contramap;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
@ -13,8 +13,9 @@ import dan200.computercraft.api.lua.LuaException;
|
|||||||
import dan200.computercraft.api.lua.LuaFunction;
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
import dan200.computercraft.core.ComputerContext;
|
import dan200.computercraft.core.ComputerContext;
|
||||||
import dan200.computercraft.core.computer.mainthread.MainThread;
|
import dan200.computercraft.core.computer.mainthread.MainThread;
|
||||||
import dan200.computercraft.core.filesystem.MemoryMount;
|
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
import dan200.computercraft.core.terminal.Terminal;
|
||||||
|
import dan200.computercraft.test.core.computer.BasicEnvironment;
|
||||||
|
import dan200.computercraft.test.core.filesystem.MemoryMount;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -8,6 +8,7 @@ package dan200.computercraft.core.computer;
|
|||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.core.lua.MachineResult;
|
import dan200.computercraft.core.lua.MachineResult;
|
||||||
import dan200.computercraft.support.ConcurrentHelpers;
|
import dan200.computercraft.support.ConcurrentHelpers;
|
||||||
|
import dan200.computercraft.test.core.computer.KotlinComputerManager;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@ -25,12 +26,12 @@ import static org.junit.jupiter.api.Assertions.*;
|
|||||||
@Execution( ExecutionMode.CONCURRENT )
|
@Execution( ExecutionMode.CONCURRENT )
|
||||||
public class ComputerThreadTest
|
public class ComputerThreadTest
|
||||||
{
|
{
|
||||||
private FakeComputerManager manager;
|
private KotlinComputerManager manager;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void before()
|
public void before()
|
||||||
{
|
{
|
||||||
manager = new FakeComputerManager();
|
manager = new KotlinComputerManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
|
@ -1,259 +0,0 @@
|
|||||||
/*
|
|
||||||
* This file is part of ComputerCraft - http://www.computercraft.info
|
|
||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
|
||||||
*/
|
|
||||||
package dan200.computercraft.core.computer;
|
|
||||||
|
|
||||||
import dan200.computercraft.api.lua.ILuaAPI;
|
|
||||||
import dan200.computercraft.core.ComputerContext;
|
|
||||||
import dan200.computercraft.core.lua.ILuaMachine;
|
|
||||||
import dan200.computercraft.core.lua.MachineResult;
|
|
||||||
import dan200.computercraft.core.terminal.Terminal;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Queue;
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.locks.Condition;
|
|
||||||
import java.util.concurrent.locks.Lock;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates "fake" computers, which just run user-defined tasks rather than Lua code.
|
|
||||||
*/
|
|
||||||
public class FakeComputerManager implements AutoCloseable
|
|
||||||
{
|
|
||||||
interface Task
|
|
||||||
{
|
|
||||||
MachineResult run( TimeoutState state ) throws Exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Map<Computer, Queue<Task>> machines = new HashMap<>();
|
|
||||||
private final ComputerContext context = new ComputerContext(
|
|
||||||
new BasicEnvironment(),
|
|
||||||
new ComputerThread( 1 ),
|
|
||||||
new FakeMainThreadScheduler(),
|
|
||||||
args -> new DummyLuaMachine( args.timeout )
|
|
||||||
);
|
|
||||||
|
|
||||||
private final Lock errorLock = new ReentrantLock();
|
|
||||||
private final Condition hasError = errorLock.newCondition();
|
|
||||||
private volatile @Nullable Throwable error;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context.ensureClosed( 1, TimeUnit.SECONDS );
|
|
||||||
}
|
|
||||||
catch( InterruptedException e )
|
|
||||||
{
|
|
||||||
throw new IllegalStateException( "Runtime thread was interrupted", e );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComputerContext context()
|
|
||||||
{
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new computer which pulls from our task queue.
|
|
||||||
*
|
|
||||||
* @return The computer. This will not be started yet, you must call {@link Computer#turnOn()} and
|
|
||||||
* {@link Computer#tick()} to do so.
|
|
||||||
*/
|
|
||||||
public Computer create()
|
|
||||||
{
|
|
||||||
Queue<Task> queue = new ConcurrentLinkedQueue<>();
|
|
||||||
Computer computer = new Computer( context, new BasicEnvironment(), new Terminal( 51, 19, true ), 0 );
|
|
||||||
computer.addApi( new QueuePassingAPI( queue ) ); // Inject an extra API to pass the queue to the machine.
|
|
||||||
machines.put( computer, queue );
|
|
||||||
return computer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and start a new computer which loops forever.
|
|
||||||
*/
|
|
||||||
public void createLoopingComputer()
|
|
||||||
{
|
|
||||||
Computer computer = create();
|
|
||||||
enqueueForever( computer, t -> {
|
|
||||||
Thread.sleep( 100 );
|
|
||||||
return MachineResult.OK;
|
|
||||||
} );
|
|
||||||
computer.turnOn();
|
|
||||||
computer.tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enqueue a task on a computer.
|
|
||||||
*
|
|
||||||
* @param computer The computer to enqueue the work on.
|
|
||||||
* @param task The task to run.
|
|
||||||
*/
|
|
||||||
public void enqueue( Computer computer, Task task )
|
|
||||||
{
|
|
||||||
machines.get( computer ).offer( task );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enqueue a repeated task on a computer. This is automatically requeued when the task finishes, meaning the task
|
|
||||||
* queue is never empty.
|
|
||||||
*
|
|
||||||
* @param computer The computer to enqueue the work on.
|
|
||||||
* @param task The task to run.
|
|
||||||
*/
|
|
||||||
private void enqueueForever( Computer computer, Task task )
|
|
||||||
{
|
|
||||||
machines.get( computer ).offer( t -> {
|
|
||||||
MachineResult result = task.run( t );
|
|
||||||
|
|
||||||
enqueueForever( computer, task );
|
|
||||||
computer.queueEvent( "some_event", null );
|
|
||||||
return result;
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sleep for a given period, immediately propagating any exceptions thrown by a computer.
|
|
||||||
*
|
|
||||||
* @param delay The duration to sleep for.
|
|
||||||
* @param unit The time unit the duration is measured in.
|
|
||||||
* @throws Exception An exception thrown by a running computer.
|
|
||||||
*/
|
|
||||||
public void sleep( long delay, TimeUnit unit ) throws Exception
|
|
||||||
{
|
|
||||||
errorLock.lock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
rethrowIfNeeded();
|
|
||||||
if( hasError.await( delay, unit ) ) rethrowIfNeeded();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
errorLock.unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start a computer and wait for it to finish.
|
|
||||||
*
|
|
||||||
* @param computer The computer to wait for.
|
|
||||||
* @throws Exception An exception thrown by a running computer.
|
|
||||||
*/
|
|
||||||
public void startAndWait( Computer computer ) throws Exception
|
|
||||||
{
|
|
||||||
computer.turnOn();
|
|
||||||
computer.tick();
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
sleep( 100, TimeUnit.MILLISECONDS );
|
|
||||||
} while( context.computerScheduler().hasPendingWork() || computer.isOn() );
|
|
||||||
|
|
||||||
rethrowIfNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void rethrowIfNeeded() throws Exception
|
|
||||||
{
|
|
||||||
Throwable error = this.error;
|
|
||||||
if( error == null ) return;
|
|
||||||
if( error instanceof Exception ) throw (Exception) error;
|
|
||||||
rethrow( error );
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings( "unchecked" )
|
|
||||||
private static <T extends Throwable> void rethrow( Throwable e ) throws T
|
|
||||||
{
|
|
||||||
throw (T) e;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class QueuePassingAPI implements ILuaAPI
|
|
||||||
{
|
|
||||||
final Queue<Task> tasks;
|
|
||||||
|
|
||||||
private QueuePassingAPI( Queue<Task> tasks )
|
|
||||||
{
|
|
||||||
this.tasks = tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String[] getNames()
|
|
||||||
{
|
|
||||||
return new String[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class DummyLuaMachine implements ILuaMachine
|
|
||||||
{
|
|
||||||
private final TimeoutState state;
|
|
||||||
private @Nullable Queue<Task> tasks;
|
|
||||||
|
|
||||||
DummyLuaMachine( TimeoutState state )
|
|
||||||
{
|
|
||||||
this.state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addAPI( @Nonnull ILuaAPI api )
|
|
||||||
{
|
|
||||||
if( api instanceof QueuePassingAPI ) tasks = ((QueuePassingAPI) api).tasks;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MachineResult loadBios( @Nonnull InputStream bios )
|
|
||||||
{
|
|
||||||
return MachineResult.OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MachineResult handleEvent( @Nullable String eventName, @Nullable Object[] arguments )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if( tasks == null ) throw new IllegalStateException( "Not received tasks yet" );
|
|
||||||
return tasks.remove().run( state );
|
|
||||||
}
|
|
||||||
catch( Throwable e )
|
|
||||||
{
|
|
||||||
errorLock.lock();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if( error == null )
|
|
||||||
{
|
|
||||||
error = e;
|
|
||||||
hasError.signal();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
error.addSuppressed( e );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
errorLock.unlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
if( !(e instanceof Exception) && !(e instanceof AssertionError) ) rethrow( e );
|
|
||||||
return MachineResult.error( e.getMessage() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void printExecutionState( StringBuilder out )
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,7 +7,8 @@ package dan200.computercraft.core.terminal;
|
|||||||
|
|
||||||
import dan200.computercraft.api.lua.LuaValues;
|
import dan200.computercraft.api.lua.LuaValues;
|
||||||
import dan200.computercraft.shared.util.Colour;
|
import dan200.computercraft.shared.util.Colour;
|
||||||
import dan200.computercraft.support.CallCounter;
|
import dan200.computercraft.test.core.CallCounter;
|
||||||
|
import dan200.computercraft.test.core.terminal.TerminalMatchers;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import net.minecraft.nbt.CompoundNBT;
|
import net.minecraft.nbt.CompoundNBT;
|
||||||
import net.minecraft.network.PacketBuffer;
|
import net.minecraft.network.PacketBuffer;
|
||||||
@ -16,7 +17,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
import static dan200.computercraft.core.terminal.TerminalMatchers.*;
|
import static dan200.computercraft.test.core.terminal.TerminalMatchers.*;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.allOf;
|
import static org.hamcrest.Matchers.allOf;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
@ -7,7 +7,7 @@ package dan200.computercraft.shared.network.server;
|
|||||||
|
|
||||||
import dan200.computercraft.shared.computer.upload.FileSlice;
|
import dan200.computercraft.shared.computer.upload.FileSlice;
|
||||||
import dan200.computercraft.shared.computer.upload.FileUpload;
|
import dan200.computercraft.shared.computer.upload.FileUpload;
|
||||||
import dan200.computercraft.support.ArbitraryByteBuffer;
|
import dan200.computercraft.test.core.ArbitraryByteBuffer;
|
||||||
import dan200.computercraft.support.FakeContainer;
|
import dan200.computercraft.support.FakeContainer;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import net.jqwik.api.*;
|
import net.jqwik.api.*;
|
||||||
@ -21,9 +21,9 @@ import java.util.List;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static dan200.computercraft.shared.network.server.UploadFileMessage.*;
|
import static dan200.computercraft.shared.network.server.UploadFileMessage.*;
|
||||||
import static dan200.computercraft.support.ByteBufferMatcher.bufferEqual;
|
import static dan200.computercraft.test.core.ByteBufferMatcher.bufferEqual;
|
||||||
import static dan200.computercraft.support.ContramapMatcher.contramap;
|
import static dan200.computercraft.test.core.ContramapMatcher.contramap;
|
||||||
import static dan200.computercraft.support.CustomMatchers.containsWith;
|
import static dan200.computercraft.test.core.CustomMatchers.containsWith;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package dan200.computercraft.core.apis.http.options
|
package dan200.computercraft.core.http
|
||||||
|
|
||||||
import dan200.computercraft.ComputerCraft
|
import dan200.computercraft.ComputerCraft
|
||||||
import dan200.computercraft.core.apis.AsyncRunner
|
|
||||||
import dan200.computercraft.core.apis.HTTPAPI
|
import dan200.computercraft.core.apis.HTTPAPI
|
||||||
|
import dan200.computercraft.core.apis.http.options.Action
|
||||||
|
import dan200.computercraft.core.apis.http.options.AddressRule
|
||||||
|
import dan200.computercraft.test.core.computer.LuaTaskRunner
|
||||||
import org.junit.jupiter.api.AfterAll
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
@ -36,15 +38,15 @@ class TestHttpApi {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Connects to websocket`() {
|
fun `Connects to websocket`() {
|
||||||
AsyncRunner.runTest {
|
LuaTaskRunner.runTest {
|
||||||
val httpApi = addApi(HTTPAPI(this))
|
val httpApi = addApi(HTTPAPI(environment))
|
||||||
|
|
||||||
val result = httpApi.websocket(WS_ADDRESS, Optional.empty())
|
val result = httpApi.websocket(WS_ADDRESS, Optional.empty())
|
||||||
assertArrayEquals(arrayOf(true), result, "Should have created websocket")
|
assertArrayEquals(arrayOf(true), result, "Should have created websocket")
|
||||||
|
|
||||||
val event = pullEvent()
|
val event = pullEvent()
|
||||||
assertEquals("websocket_success", event.name) {
|
assertEquals("websocket_success", event[0]) {
|
||||||
"Websocket failed to connect: ${event.args.contentToString()}"
|
"Websocket failed to connect: ${event.contentToString()}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.support;
|
package dan200.computercraft.test.core;
|
||||||
|
|
||||||
import net.jqwik.api.*;
|
import net.jqwik.api.*;
|
||||||
import net.jqwik.api.arbitraries.SizableArbitrary;
|
import net.jqwik.api.arbitraries.SizableArbitrary;
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.support;
|
package dan200.computercraft.test.core;
|
||||||
|
|
||||||
import org.hamcrest.Description;
|
import org.hamcrest.Description;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.support;
|
package dan200.computercraft.test.core;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.support;
|
package dan200.computercraft.test.core;
|
||||||
|
|
||||||
import org.hamcrest.FeatureMatcher;
|
import org.hamcrest.FeatureMatcher;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.support;
|
package dan200.computercraft.test.core;
|
||||||
|
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
@ -0,0 +1,161 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||||
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
|
*/
|
||||||
|
package dan200.computercraft.test.core.apis;
|
||||||
|
|
||||||
|
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||||
|
import dan200.computercraft.api.peripheral.IWorkMonitor;
|
||||||
|
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||||
|
import dan200.computercraft.core.computer.ComputerEnvironment;
|
||||||
|
import dan200.computercraft.core.computer.ComputerSide;
|
||||||
|
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||||
|
import dan200.computercraft.core.filesystem.FileSystem;
|
||||||
|
import dan200.computercraft.core.metrics.Metric;
|
||||||
|
import dan200.computercraft.core.terminal.Terminal;
|
||||||
|
import dan200.computercraft.test.core.computer.BasicEnvironment;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public abstract class BasicApiEnvironment implements IAPIEnvironment
|
||||||
|
{
|
||||||
|
private final BasicEnvironment environment;
|
||||||
|
private @Nullable String label;
|
||||||
|
|
||||||
|
public BasicApiEnvironment( BasicEnvironment environment )
|
||||||
|
{
|
||||||
|
this.environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getComputerID()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public ComputerEnvironment getComputerEnvironment()
|
||||||
|
{
|
||||||
|
return environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public GlobalEnvironment getGlobalEnvironment()
|
||||||
|
{
|
||||||
|
return environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public IWorkMonitor getMainThreadMonitor()
|
||||||
|
{
|
||||||
|
throw new IllegalStateException( "Main thread monitor not available" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
public Terminal getTerminal()
|
||||||
|
{
|
||||||
|
throw new IllegalStateException( "Terminal not available" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileSystem getFileSystem()
|
||||||
|
{
|
||||||
|
throw new IllegalStateException( "Filesystem not available" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reboot()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setOutput( ComputerSide side, int output )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOutput( ComputerSide side )
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getInput( ComputerSide side )
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBundledOutput( ComputerSide side, int output )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBundledOutput( ComputerSide side )
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBundledInput( ComputerSide side )
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPeripheralChangeListener( @Nullable IPeripheralChangeListener listener )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public IPeripheral getPeripheral( ComputerSide side )
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public String getLabel()
|
||||||
|
{
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setLabel( @Nullable String label )
|
||||||
|
{
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int startTimer( long ticks )
|
||||||
|
{
|
||||||
|
throw new IllegalStateException( "Cannot start timers" );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cancelTimer( int id )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void observe( @Nonnull Metric.Event summary, long value )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void observe( @Nonnull Metric.Counter counter )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -3,16 +3,18 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.computer;
|
package dan200.computercraft.test.core.computer;
|
||||||
|
|
||||||
import dan200.computercraft.ComputerCraft;
|
import dan200.computercraft.ComputerCraft;
|
||||||
import dan200.computercraft.api.filesystem.IMount;
|
import dan200.computercraft.api.filesystem.IMount;
|
||||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||||
|
import dan200.computercraft.core.computer.ComputerEnvironment;
|
||||||
|
import dan200.computercraft.core.computer.GlobalEnvironment;
|
||||||
import dan200.computercraft.core.filesystem.FileMount;
|
import dan200.computercraft.core.filesystem.FileMount;
|
||||||
import dan200.computercraft.core.filesystem.JarMount;
|
import dan200.computercraft.core.filesystem.JarMount;
|
||||||
import dan200.computercraft.core.filesystem.MemoryMount;
|
|
||||||
import dan200.computercraft.core.metrics.Metric;
|
import dan200.computercraft.core.metrics.Metric;
|
||||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
||||||
|
import dan200.computercraft.test.core.filesystem.MemoryMount;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -24,7 +26,8 @@ import java.net.URISyntaxException;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A very basic environment.
|
* A basic implementation of {@link ComputerEnvironment} and {@link GlobalEnvironment}, suitable for a context which
|
||||||
|
* will only run a single computer.
|
||||||
*/
|
*/
|
||||||
public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment, MetricsObserver
|
public class BasicEnvironment implements ComputerEnvironment, GlobalEnvironment, MetricsObserver
|
||||||
{
|
{
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.computer;
|
package dan200.computercraft.test.core.computer;
|
||||||
|
|
||||||
import dan200.computercraft.core.computer.mainthread.MainThreadScheduler;
|
import dan200.computercraft.core.computer.mainthread.MainThreadScheduler;
|
||||||
import dan200.computercraft.core.metrics.MetricsObserver;
|
import dan200.computercraft.core.metrics.MetricsObserver;
|
@ -3,7 +3,7 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.filesystem;
|
package dan200.computercraft.test.core.filesystem;
|
||||||
|
|
||||||
import dan200.computercraft.api.filesystem.IWritableMount;
|
import dan200.computercraft.api.filesystem.IWritableMount;
|
||||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
@ -3,9 +3,11 @@
|
|||||||
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
* Send enquiries to dratcliffe@gmail.com
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
*/
|
*/
|
||||||
package dan200.computercraft.core.terminal;
|
package dan200.computercraft.test.core.terminal;
|
||||||
|
|
||||||
import dan200.computercraft.support.ContramapMatcher;
|
import dan200.computercraft.core.terminal.Terminal;
|
||||||
|
import dan200.computercraft.core.terminal.TextBuffer;
|
||||||
|
import dan200.computercraft.test.core.ContramapMatcher;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
|
|
@ -0,0 +1,63 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||||
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
|
*/
|
||||||
|
package dan200.computercraft.test.core
|
||||||
|
|
||||||
|
import org.hamcrest.BaseMatcher
|
||||||
|
import org.hamcrest.Description
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.collection.IsArray
|
||||||
|
import org.junit.jupiter.api.Assertions
|
||||||
|
|
||||||
|
/** Postfix version of [Assertions.assertArrayEquals] */
|
||||||
|
fun Array<out Any?>?.assertArrayEquals(vararg expected: Any?, message: String? = null) {
|
||||||
|
assertThat(
|
||||||
|
message ?: "",
|
||||||
|
this,
|
||||||
|
IsArrayVerbose(expected.map { FuzzyEqualTo(it) }.toTypedArray()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension of [IsArray] which always prints the array, not just when the items are mismatched.
|
||||||
|
*/
|
||||||
|
internal class IsArrayVerbose<T>(private val elementMatchers: Array<Matcher<in T>>) : IsArray<T>(elementMatchers) {
|
||||||
|
override fun describeMismatchSafely(actual: Array<out T>, description: Description) {
|
||||||
|
description.appendText("array was ").appendValue(actual)
|
||||||
|
if (actual.size != elementMatchers.size) {
|
||||||
|
description.appendText(" with length ").appendValue(actual.size)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in actual.indices) {
|
||||||
|
if (!elementMatchers[i].matches(actual[i])) {
|
||||||
|
description.appendText("with element ").appendValue(i).appendText(" ")
|
||||||
|
elementMatchers[i].describeMismatch(actual[i], description)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An equality matcher which is slightly more relaxed on comparing some values.
|
||||||
|
*/
|
||||||
|
internal class FuzzyEqualTo(private val expected: Any?) : BaseMatcher<Any?>() {
|
||||||
|
override fun describeTo(description: Description) {
|
||||||
|
description.appendValue(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun matches(actual: Any?): Boolean {
|
||||||
|
if (actual == null) return false
|
||||||
|
|
||||||
|
if (actual is Number && expected is Number && actual.javaClass != expected.javaClass) {
|
||||||
|
// Allow equating integers and floats.
|
||||||
|
return actual.toDouble() == expected.toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
return actual == expected
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of ComputerCraft - http://www.computercraft.info
|
||||||
|
* Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
|
||||||
|
* Send enquiries to dratcliffe@gmail.com
|
||||||
|
*/
|
||||||
|
package dan200.computercraft.test.core.computer
|
||||||
|
|
||||||
|
import dan200.computercraft.api.lua.ILuaAPI
|
||||||
|
import dan200.computercraft.core.ComputerContext
|
||||||
|
import dan200.computercraft.core.computer.Computer
|
||||||
|
import dan200.computercraft.core.computer.ComputerThread
|
||||||
|
import dan200.computercraft.core.computer.TimeoutState
|
||||||
|
import dan200.computercraft.core.lua.MachineEnvironment
|
||||||
|
import dan200.computercraft.core.lua.MachineResult
|
||||||
|
import dan200.computercraft.core.terminal.Terminal
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.locks.Lock
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
|
|
||||||
|
typealias FakeComputerTask = (state: TimeoutState) -> MachineResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates "fake" computers, which just run user-defined tasks rather than Lua code.
|
||||||
|
*/
|
||||||
|
class KotlinComputerManager : AutoCloseable {
|
||||||
|
|
||||||
|
private val machines: MutableMap<Computer, Queue<FakeComputerTask>> = HashMap()
|
||||||
|
private val context = ComputerContext(BasicEnvironment(), ComputerThread(1), FakeMainThreadScheduler()) { DummyLuaMachine(it) }
|
||||||
|
private val errorLock: Lock = ReentrantLock()
|
||||||
|
private val hasError = errorLock.newCondition()
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var error: Throwable? = null
|
||||||
|
override fun close() {
|
||||||
|
try {
|
||||||
|
context.ensureClosed(1, TimeUnit.SECONDS)
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
throw IllegalStateException("Runtime thread was interrupted", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun context(): ComputerContext {
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new computer which pulls from our task queue.
|
||||||
|
*
|
||||||
|
* @return The computer. This will not be started yet, you must call [Computer.turnOn] and
|
||||||
|
* [Computer.tick] to do so.
|
||||||
|
*/
|
||||||
|
fun create(): Computer {
|
||||||
|
val queue: Queue<FakeComputerTask> = ConcurrentLinkedQueue()
|
||||||
|
val computer = Computer(context, BasicEnvironment(), Terminal(51, 19, true), 0)
|
||||||
|
computer.addApi(QueuePassingAPI(queue)) // Inject an extra API to pass the queue to the machine.
|
||||||
|
machines[computer] = queue
|
||||||
|
return computer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and start a new computer which loops forever.
|
||||||
|
*/
|
||||||
|
fun createLoopingComputer() {
|
||||||
|
val computer = create()
|
||||||
|
enqueueForever(computer) {
|
||||||
|
Thread.sleep(100)
|
||||||
|
MachineResult.OK
|
||||||
|
}
|
||||||
|
computer.turnOn()
|
||||||
|
computer.tick()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue a task on a computer.
|
||||||
|
*
|
||||||
|
* @param computer The computer to enqueue the work on.
|
||||||
|
* @param task The task to run.
|
||||||
|
*/
|
||||||
|
fun enqueue(computer: Computer, task: FakeComputerTask) {
|
||||||
|
machines[computer]!!.offer(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue a repeated task on a computer. This is automatically requeued when the task finishes, meaning the task
|
||||||
|
* queue is never empty.
|
||||||
|
*
|
||||||
|
* @param computer The computer to enqueue the work on.
|
||||||
|
* @param task The task to run.
|
||||||
|
*/
|
||||||
|
private fun enqueueForever(computer: Computer, task: FakeComputerTask) {
|
||||||
|
machines[computer]!!.offer {
|
||||||
|
val result = task(it)
|
||||||
|
enqueueForever(computer, task)
|
||||||
|
computer.queueEvent("some_event", null)
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleep for a given period, immediately propagating any exceptions thrown by a computer.
|
||||||
|
*
|
||||||
|
* @param delay The duration to sleep for.
|
||||||
|
* @param unit The time unit the duration is measured in.
|
||||||
|
* @throws Exception An exception thrown by a running computer.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun sleep(delay: Long, unit: TimeUnit?) {
|
||||||
|
errorLock.lock()
|
||||||
|
try {
|
||||||
|
rethrowIfNeeded()
|
||||||
|
if (hasError.await(delay, unit)) rethrowIfNeeded()
|
||||||
|
} finally {
|
||||||
|
errorLock.unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a computer and wait for it to finish.
|
||||||
|
*
|
||||||
|
* @param computer The computer to wait for.
|
||||||
|
* @throws Exception An exception thrown by a running computer.
|
||||||
|
*/
|
||||||
|
@Throws(Exception::class)
|
||||||
|
fun startAndWait(computer: Computer) {
|
||||||
|
computer.turnOn()
|
||||||
|
computer.tick()
|
||||||
|
do {
|
||||||
|
sleep(100, TimeUnit.MILLISECONDS)
|
||||||
|
} while (context.computerScheduler().hasPendingWork() || computer.isOn)
|
||||||
|
|
||||||
|
rethrowIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
private fun rethrowIfNeeded() {
|
||||||
|
val error = error ?: return
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
private class QueuePassingAPI constructor(val tasks: Queue<FakeComputerTask>) : ILuaAPI {
|
||||||
|
override fun getNames(): Array<String> = arrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class DummyLuaMachine(private val environment: MachineEnvironment) : KotlinLuaMachine(environment) {
|
||||||
|
private var tasks: Queue<FakeComputerTask>? = null
|
||||||
|
override fun addAPI(api: ILuaAPI) {
|
||||||
|
super.addAPI(api)
|
||||||
|
if (api is QueuePassingAPI) tasks = api.tasks
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTask(): (suspend KotlinLuaMachine.() -> Unit)? {
|
||||||
|
try {
|
||||||
|
val tasks = this.tasks ?: throw NullPointerException("Not received tasks yet")
|
||||||
|
val task = tasks.remove()
|
||||||
|
return {
|
||||||
|
try {
|
||||||
|
task(environment.timeout)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
reportError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
reportError(e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {}
|
||||||
|
|
||||||
|
private fun reportError(e: Throwable) {
|
||||||
|
errorLock.lock()
|
||||||
|
try {
|
||||||
|
if (error == null) {
|
||||||
|
error = e
|
||||||
|
hasError.signal()
|
||||||
|
} else {
|
||||||
|
error!!.addSuppressed(e)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
errorLock.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e is Exception || e is AssertionError) return else throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package dan200.computercraft.test.core.computer
|
||||||
|
|
||||||
|
import dan200.computercraft.api.lua.ILuaAPI
|
||||||
|
import dan200.computercraft.api.lua.ILuaContext
|
||||||
|
import dan200.computercraft.core.lua.ILuaMachine
|
||||||
|
import dan200.computercraft.core.lua.MachineEnvironment
|
||||||
|
import dan200.computercraft.core.lua.MachineResult
|
||||||
|
import kotlinx.coroutines.CoroutineName
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [ILuaMachine] which runs Kotlin functions instead.
|
||||||
|
*/
|
||||||
|
abstract class KotlinLuaMachine(environment: MachineEnvironment) : ILuaMachine, AbstractLuaTaskContext() {
|
||||||
|
override val context: ILuaContext = environment.context
|
||||||
|
|
||||||
|
override fun addAPI(api: ILuaAPI) = addApi(api)
|
||||||
|
|
||||||
|
override fun loadBios(bios: InputStream): MachineResult = MachineResult.OK
|
||||||
|
|
||||||
|
override fun handleEvent(eventName: String?, arguments: Array<out Any>?): MachineResult {
|
||||||
|
if (hasEventListeners) {
|
||||||
|
queueEvent(eventName, arguments)
|
||||||
|
} else {
|
||||||
|
val task = getTask()
|
||||||
|
if (task != null) CoroutineScope(Dispatchers.Unconfined + CoroutineName("Computer")).launch { task() }
|
||||||
|
}
|
||||||
|
|
||||||
|
return MachineResult.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun printExecutionState(out: StringBuilder) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next task to execute on this computer.
|
||||||
|
*/
|
||||||
|
protected abstract fun getTask(): (suspend KotlinLuaMachine.() -> Unit)?
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
package dan200.computercraft.test.core.computer
|
||||||
|
|
||||||
|
import dan200.computercraft.api.lua.ILuaAPI
|
||||||
|
import dan200.computercraft.api.lua.ILuaContext
|
||||||
|
import dan200.computercraft.api.lua.MethodResult
|
||||||
|
import dan200.computercraft.api.lua.ObjectArguments
|
||||||
|
import dan200.computercraft.core.apis.PeripheralAPI
|
||||||
|
import kotlinx.coroutines.CancellableContinuation
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The context for tasks which consume Lua objects.
|
||||||
|
*
|
||||||
|
* This provides helpers for converting CC's callback-based code into a more direct style based on Kotlin coroutines.
|
||||||
|
*/
|
||||||
|
interface LuaTaskContext {
|
||||||
|
/** The current Lua context, to be passed to method calls. */
|
||||||
|
val context: ILuaContext
|
||||||
|
|
||||||
|
/** Get a registered API. */
|
||||||
|
fun <T : ILuaAPI> getApi(api: Class<T>): T
|
||||||
|
|
||||||
|
/** Pull a Lua event */
|
||||||
|
suspend fun pullEvent(event: String? = null): Array<out Any?>
|
||||||
|
|
||||||
|
/** Resolve a [MethodResult] until completion, returning the resulting values. */
|
||||||
|
suspend fun MethodResult.await(): Array<out Any?>? {
|
||||||
|
var result = this
|
||||||
|
while (true) {
|
||||||
|
val callback = result.callback
|
||||||
|
val values = result.result
|
||||||
|
|
||||||
|
if (callback == null) return values
|
||||||
|
|
||||||
|
val filter = if (values == null) null else values[0] as String?
|
||||||
|
result = callback.resume(pullEvent(filter))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call a peripheral method. */
|
||||||
|
suspend fun LuaTaskContext.callPeripheral(name: String, method: String, vararg args: Any?): Array<out Any?>? =
|
||||||
|
getApi<PeripheralAPI>().call(context, ObjectArguments(name, method, *args)).await()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get a registered API. */
|
||||||
|
inline fun <reified T : ILuaAPI> LuaTaskContext.getApi(): T = getApi(T::class.java)
|
||||||
|
|
||||||
|
abstract class AbstractLuaTaskContext : LuaTaskContext, AutoCloseable {
|
||||||
|
private val pullEvents = mutableListOf<PullEvent>()
|
||||||
|
private val apis = mutableMapOf<Class<out ILuaAPI>, ILuaAPI>()
|
||||||
|
|
||||||
|
protected fun addApi(api: ILuaAPI) {
|
||||||
|
apis[api.javaClass] = api
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val hasEventListeners
|
||||||
|
get() = pullEvents.isNotEmpty()
|
||||||
|
|
||||||
|
protected fun queueEvent(eventName: String?, arguments: Array<out Any?>?) {
|
||||||
|
val fullEvent: Array<out Any?> = when {
|
||||||
|
eventName == null && arguments == null -> arrayOf()
|
||||||
|
eventName != null && arguments == null -> arrayOf(eventName)
|
||||||
|
eventName == null && arguments != null -> arguments
|
||||||
|
else -> arrayOf(eventName, *arguments!!)
|
||||||
|
}
|
||||||
|
for (i in pullEvents.size - 1 downTo 0) {
|
||||||
|
val puller = pullEvents[i]
|
||||||
|
if (puller.name == null || puller.name == eventName || eventName == "terminate") {
|
||||||
|
pullEvents.removeAt(i)
|
||||||
|
puller.cont.resumeWith(Result.success(fullEvent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
for (pullEvent in pullEvents) pullEvent.cont.cancel()
|
||||||
|
pullEvents.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun <T : ILuaAPI> getApi(api: Class<T>): T =
|
||||||
|
api.cast(apis[api] ?: throw IllegalStateException("No API of type ${api.name}"))
|
||||||
|
|
||||||
|
final override suspend fun pullEvent(event: String?): Array<out Any?> =
|
||||||
|
suspendCancellableCoroutine { cont -> pullEvents.add(PullEvent(event, cont)) }
|
||||||
|
|
||||||
|
private class PullEvent(val name: String?, val cont: CancellableContinuation<Array<out Any?>>)
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package dan200.computercraft.test.core.computer
|
||||||
|
|
||||||
|
import dan200.computercraft.api.lua.ILuaAPI
|
||||||
|
import dan200.computercraft.api.lua.ILuaContext
|
||||||
|
import dan200.computercraft.api.lua.LuaException
|
||||||
|
import dan200.computercraft.core.apis.IAPIEnvironment
|
||||||
|
import dan200.computercraft.test.core.apis.BasicApiEnvironment
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
class LuaTaskRunner : AbstractLuaTaskContext() {
|
||||||
|
private val eventStream: Channel<Event> = Channel(Channel.UNLIMITED)
|
||||||
|
private val apis = mutableListOf<ILuaAPI>()
|
||||||
|
|
||||||
|
val environment: IAPIEnvironment = object : BasicApiEnvironment(BasicEnvironment()) {
|
||||||
|
override fun queueEvent(event: String?, vararg args: Any?) {
|
||||||
|
if (eventStream.trySend(Event(event, args)).isFailure) {
|
||||||
|
throw IllegalStateException("Queue is full")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shutdown() {
|
||||||
|
super.shutdown()
|
||||||
|
eventStream.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override val context =
|
||||||
|
ILuaContext { throw LuaException("Cannot queue main thread task") }
|
||||||
|
|
||||||
|
fun <T : ILuaAPI> addApi(api: T): T {
|
||||||
|
super.addApi(api)
|
||||||
|
apis.add(api)
|
||||||
|
api.startup()
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
environment.shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun run() {
|
||||||
|
for (event in eventStream) {
|
||||||
|
queueEvent(event.name, event.args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Event(val name: String?, val args: Array<out Any?>)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun runTest(timeout: Duration = 5.seconds, fn: suspend LuaTaskRunner.() -> Unit) {
|
||||||
|
runBlocking {
|
||||||
|
withTimeout(timeout) {
|
||||||
|
val runner = LuaTaskRunner()
|
||||||
|
launch { runner.run() }
|
||||||
|
runner.use { fn(runner) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user