mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 05:33:00 +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:
		| @@ -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) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Jonathan Coates
					Jonathan Coates