mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2024-12-14 12:10:30 +00:00
Add a system for client-side tests (#1219)
- Add a new ClientJavaExec Gradle task, which is used for client-side tests. This: - Copies the exec spec from another JavaExec task. - Sets some additional system properties to configure on gametest framework. - Runs Java inside an X framebuffer (when available), meaning we don't need to spin up a new window. We also configure this task so that only one instance can run at once, meaning we don't spawn multiple MC windows at once! - Port our 1.16 client test framework to 1.19. This is mostly the same as before, but screenshots no longer do a golden test: they /just/ write to a folder. Screenshots are compared manually afterwards. This is still pretty brittle, and there's a lot of sleeps scattered around in the code. It's not clear how well this will play on CI. - Roll our own game test loader, rather than relying on the mod loader to do it for us. This ensures that loading is consistent between platforms (we already had to do some hacks for Forge) and makes it easier to provide custom logic for loading client-only tests. - Run several client tests (namely those involving monitor rendering) against Sodium and Iris too. There's some nastiness here to set up new Loom run configurations and automatically configure Iris to use Complementary Shaders, but it's not too bad. These tests /don't/ run on CI, so it doesn't need to be as reliable.
This commit is contained in:
parent
b58b9b7df3
commit
8f92417a2f
3
.github/workflows/main-ci.yml
vendored
3
.github/workflows/main-ci.yml
vendored
@ -33,6 +33,9 @@ jobs:
|
||||
./gradlew downloadAssets || ./gradlew downloadAssets
|
||||
./gradlew build
|
||||
|
||||
- name: Run client tests
|
||||
run: ./gradlew runGametestClient # Not checkClient, as no point running rendering tests.
|
||||
|
||||
- name: Upload Jar
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
@ -4,22 +4,21 @@ import net.ltgt.gradle.errorprone.CheckSeverity
|
||||
import net.ltgt.gradle.errorprone.errorprone
|
||||
import org.gradle.api.NamedDomainObjectProvider
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.Task
|
||||
import org.gradle.api.attributes.TestSuiteType
|
||||
import org.gradle.api.file.FileSystemOperations
|
||||
import org.gradle.api.plugins.JavaPluginExtension
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.api.provider.SetProperty
|
||||
import org.gradle.api.reporting.ReportingExtension
|
||||
import org.gradle.api.tasks.JavaExec
|
||||
import org.gradle.api.tasks.SourceSet
|
||||
import org.gradle.api.tasks.SourceSetContainer
|
||||
import org.gradle.api.tasks.bundling.Jar
|
||||
import org.gradle.api.tasks.compile.JavaCompile
|
||||
import org.gradle.api.tasks.javadoc.Javadoc
|
||||
import org.gradle.configurationcache.extensions.capitalized
|
||||
import org.gradle.kotlin.dsl.get
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin
|
||||
import org.gradle.language.jvm.tasks.ProcessResources
|
||||
import org.gradle.process.JavaForkOptions
|
||||
import org.gradle.testing.jacoco.plugins.JacocoCoverageReport
|
||||
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
|
||||
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
|
||||
@ -27,8 +26,11 @@ import org.gradle.testing.jacoco.tasks.JacocoReport
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.OutputStreamWriter
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
import java.util.regex.Pattern
|
||||
|
||||
abstract class CCTweakedExtension(
|
||||
@ -79,8 +81,7 @@ abstract class CCTweakedExtension(
|
||||
/**
|
||||
* References to other sources
|
||||
*/
|
||||
val sourceDirectories: SetProperty<SourceSetReference> =
|
||||
project.objects.setProperty(SourceSetReference::class.java)
|
||||
val sourceDirectories: SetProperty<SourceSetReference> = project.objects.setProperty(SourceSetReference::class.java)
|
||||
|
||||
/** All source sets referenced by this project. */
|
||||
val sourceSets = sourceDirectories.map { x -> x.map { it.sourceSet } }
|
||||
@ -181,7 +182,7 @@ abstract class CCTweakedExtension(
|
||||
}
|
||||
}
|
||||
|
||||
fun jacoco(task: NamedDomainObjectProvider<JavaExec>) {
|
||||
fun <T> jacoco(task: NamedDomainObjectProvider<T>) where T : Task, T : JavaForkOptions {
|
||||
val classDump = project.buildDir.resolve("jacocoClassDump/${task.name}")
|
||||
val reportTaskName = "jacoco${task.name.capitalized()}Report"
|
||||
|
||||
@ -210,8 +211,7 @@ abstract class CCTweakedExtension(
|
||||
classDirectories.from(classDump)
|
||||
|
||||
// Don't want to use sourceSets(...) here as we have a custom class directory.
|
||||
val sourceSets = project.extensions.getByType(SourceSetContainer::class.java)
|
||||
sourceDirectories.from(sourceSets["main"].allSource.sourceDirectories)
|
||||
for (ref in sourceSets.get()) sourceDirectories.from(ref.allSource.sourceDirectories)
|
||||
}
|
||||
|
||||
project.extensions.configure(ReportingExtension::class.java) {
|
||||
@ -221,6 +221,41 @@ abstract class CCTweakedExtension(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file by creating a dummy Ivy repository.
|
||||
*
|
||||
* This should only be used for one-off downloads. Using a more conventional Ivy or Maven repository is preferred
|
||||
* where possible.
|
||||
*/
|
||||
fun downloadFile(label: String, url: String): File {
|
||||
val url = URL(url)
|
||||
val path = File(url.path)
|
||||
|
||||
project.repositories.ivy {
|
||||
name = label
|
||||
setUrl(URI(url.protocol, url.userInfo, url.host, url.port, path.parent, null, null))
|
||||
patternLayout {
|
||||
artifact("[artifact].[ext]")
|
||||
}
|
||||
metadataSources {
|
||||
artifact()
|
||||
}
|
||||
content {
|
||||
includeModule("cc.tweaked.internal", path.nameWithoutExtension)
|
||||
}
|
||||
}
|
||||
|
||||
return project.configurations.detachedConfiguration(
|
||||
project.dependencies.create(
|
||||
mapOf(
|
||||
"group" to "cc.tweaked.internal",
|
||||
"name" to path.nameWithoutExtension,
|
||||
"ext" to path.extension,
|
||||
),
|
||||
),
|
||||
).resolve().single()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val EMAIL = Pattern.compile("^([^<]+) <.+>$")
|
||||
private val IGNORED_USERS = setOf(
|
||||
@ -238,7 +273,7 @@ abstract class CCTweakedExtension(
|
||||
}
|
||||
}
|
||||
|
||||
internal val isIdeSync: Boolean
|
||||
private val isIdeSync: Boolean
|
||||
get() = java.lang.Boolean.parseBoolean(System.getProperty("idea.sync.active", "false"))
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import org.gradle.api.file.RegularFileProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.*
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Checks the `changelog.md` and `whatsnew.md` files are well-formed.
|
||||
|
@ -5,7 +5,6 @@ import com.diffplug.spotless.FormatterStep
|
||||
import com.diffplug.spotless.generic.LicenseHeaderStep
|
||||
import java.io.File
|
||||
import java.io.Serializable
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Similar to [LicenseHeaderStep], but supports multiple licenses.
|
||||
|
@ -1,11 +1,10 @@
|
||||
package cc.tweaked.gradle
|
||||
|
||||
import org.gradle.api.artifacts.ResolvedDependency
|
||||
import org.gradle.api.artifacts.dsl.DependencyHandler
|
||||
import org.gradle.api.publish.maven.MavenPublication
|
||||
import org.gradle.api.specs.Spec
|
||||
import org.gradle.api.tasks.JavaExec
|
||||
import org.gradle.process.BaseExecSpec
|
||||
import org.gradle.process.JavaExecSpec
|
||||
import org.gradle.process.ProcessForkOptions
|
||||
|
||||
/**
|
||||
* Add an annotation processor to all source sets.
|
||||
@ -26,10 +25,28 @@ fun DependencyHandler.annotationProcessorEverywhere(dep: Any) {
|
||||
*/
|
||||
fun JavaExec.copyToFull(spec: JavaExec) {
|
||||
copyTo(spec)
|
||||
spec.classpath = classpath
|
||||
spec.mainClass.set(mainClass)
|
||||
spec.javaLauncher.set(javaLauncher)
|
||||
|
||||
// Additional Java options
|
||||
spec.jvmArgs = jvmArgs // Fabric overrides getJvmArgs so copyTo doesn't do the right thing.
|
||||
spec.args = args
|
||||
spec.argumentProviders.addAll(argumentProviders)
|
||||
spec.mainClass.set(mainClass)
|
||||
spec.classpath = classpath
|
||||
spec.javaLauncher.set(javaLauncher)
|
||||
if (executable != null) spec.setExecutable(executable!!)
|
||||
|
||||
// Additional ExecSpec options
|
||||
copyToExec(spec)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy additional [BaseExecSpec] options which aren't handled by [ProcessForkOptions.copyTo].
|
||||
*/
|
||||
fun BaseExecSpec.copyToExec(spec: BaseExecSpec) {
|
||||
spec.isIgnoreExitValue = isIgnoreExitValue
|
||||
if (standardInput != null) spec.standardInput = standardInput
|
||||
if (standardOutput != null) spec.standardOutput = standardOutput
|
||||
if (errorOutput != null) spec.errorOutput = errorOutput
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,3 +59,59 @@ fun JavaExec.copyToFull(spec: JavaExec) {
|
||||
* ```
|
||||
*/
|
||||
class UseNamedArgs private constructor()
|
||||
|
||||
/**
|
||||
* An [AutoCloseable] implementation which can be used to combine other [AutoCloseable] instances.
|
||||
*
|
||||
* Values which implement [AutoCloseable] can be dynamically registered with [CloseScope.add]. When the scope is closed,
|
||||
* each value is closed in the opposite order.
|
||||
*
|
||||
* This is largely intended for cases where it's not appropriate to nest [AutoCloseable.use], for instance when nested
|
||||
* would be too deep.
|
||||
*/
|
||||
class CloseScope : AutoCloseable {
|
||||
private val toClose = ArrayDeque<AutoCloseable>()
|
||||
|
||||
/**
|
||||
* Add a value to be closed when this scope is closed.
|
||||
*/
|
||||
public fun add(value: AutoCloseable) {
|
||||
toClose.addLast(value)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
close(null)
|
||||
}
|
||||
|
||||
@PublishedApi
|
||||
internal fun close(baseException: Throwable?) {
|
||||
var exception = baseException
|
||||
|
||||
while (true) {
|
||||
var toClose = toClose.removeLastOrNull() ?: break
|
||||
try {
|
||||
toClose.close()
|
||||
} catch (e: Throwable) {
|
||||
if (exception == null) {
|
||||
exception = e
|
||||
} else {
|
||||
exception.addSuppressed(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exception != null) throw exception
|
||||
}
|
||||
|
||||
inline fun <R> use(block: (CloseScope) -> R): R {
|
||||
var exception: Throwable? = null
|
||||
try {
|
||||
return block(this)
|
||||
} catch (e: Throwable) {
|
||||
exception = e
|
||||
throw e
|
||||
} finally {
|
||||
close(exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import org.w3c.dom.Attr
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Node
|
||||
import org.xml.sax.InputSource
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
@ -36,7 +35,7 @@ internal class IdeaRunConfigurations(project: Project) {
|
||||
val ideaMisc = ideaDir.resolve("misc.xml")
|
||||
|
||||
try {
|
||||
val doc = Files.newBufferedReader(ideaMisc.toPath(), StandardCharsets.UTF_8).use {
|
||||
val doc = Files.newBufferedReader(ideaMisc.toPath()).use {
|
||||
documentBuilder.parse(InputSource(it))
|
||||
}
|
||||
val node =
|
||||
|
202
buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt
Normal file
202
buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt
Normal file
@ -0,0 +1,202 @@
|
||||
package cc.tweaked.gradle
|
||||
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.file.FileSystemOperations
|
||||
import org.gradle.api.invocation.Gradle
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.api.services.BuildService
|
||||
import org.gradle.api.services.BuildServiceParameters
|
||||
import org.gradle.api.tasks.*
|
||||
import org.gradle.kotlin.dsl.getByName
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.function.Supplier
|
||||
import javax.inject.Inject
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* A [JavaExec] task for client-tests. This sets some common setup, and uses [MinecraftRunnerService] to ensure only one
|
||||
* test runs at once.
|
||||
*/
|
||||
abstract class ClientJavaExec : JavaExec() {
|
||||
private val clientRunner: Provider<MinecraftRunnerService> = MinecraftRunnerService.get(project.gradle)
|
||||
|
||||
init {
|
||||
group = LifecycleBasePlugin.VERIFICATION_GROUP
|
||||
usesService(clientRunner)
|
||||
}
|
||||
|
||||
/**
|
||||
* When [false], tests will not be run automatically, allowing the user to debug rendering.
|
||||
*/
|
||||
@get:Input
|
||||
val clientDebug get() = project.hasProperty("clientDebug")
|
||||
|
||||
/**
|
||||
* When [false], tests will not run under a framebuffer.
|
||||
*/
|
||||
@get:Input
|
||||
val useFramebuffer get() = !clientDebug && !project.hasProperty("clientNoFramebuffer")
|
||||
|
||||
/**
|
||||
* The folder screenshots are written to.
|
||||
*/
|
||||
@get:OutputDirectory
|
||||
val screenshots = project.layout.buildDirectory.dir("testScreenshots")
|
||||
|
||||
/**
|
||||
* The path test results are written to.
|
||||
*/
|
||||
@get:OutputFile
|
||||
val testResults = project.layout.buildDirectory.file("test-results/$name.xml")
|
||||
|
||||
/**
|
||||
* Copy configuration from a task with the given name.
|
||||
*/
|
||||
fun copyFrom(path: String) = copyFrom(project.tasks.getByName(path, JavaExec::class))
|
||||
|
||||
/**
|
||||
* Copy configuration from an existing [JavaExec] task.
|
||||
*/
|
||||
fun copyFrom(task: JavaExec) {
|
||||
for (dep in task.dependsOn) dependsOn(dep)
|
||||
task.copyToFull(this)
|
||||
|
||||
if (!clientDebug) systemProperty("cctest.client", "")
|
||||
systemProperty("cctest.gametest-report", testResults.get().asFile.absoluteFile)
|
||||
workingDir(project.buildDir.resolve("gametest").resolve(name))
|
||||
}
|
||||
|
||||
/**
|
||||
* Only run tests with the given tags.
|
||||
*/
|
||||
fun tags(vararg tags: String) {
|
||||
systemProperty("cctest.tags", tags.joinToString(","))
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a file with the given contents before starting Minecraft. This may be useful for writing config files.
|
||||
*/
|
||||
fun withFileContents(path: Any, contents: Supplier<String>) {
|
||||
val file = project.file(path).toPath()
|
||||
doFirst {
|
||||
Files.createDirectories(file.parent)
|
||||
Files.writeString(file, contents.get())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy a file to the provided path before starting Minecraft. This copy only occurs if the file does not already
|
||||
* exist.
|
||||
*/
|
||||
fun withFileFrom(path: Any, source: Supplier<File>) {
|
||||
val file = project.file(path).toPath()
|
||||
doFirst {
|
||||
Files.createDirectories(file.parent)
|
||||
if (!Files.exists(file)) Files.copy(source.get().toPath(), file)
|
||||
}
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
override fun exec() {
|
||||
Files.createDirectories(workingDir.toPath())
|
||||
fsOperations.delete { delete(workingDir.resolve("screenshots")) }
|
||||
|
||||
if (useFramebuffer) {
|
||||
clientRunner.get().wrapClient(this) { super.exec() }
|
||||
} else {
|
||||
super.exec()
|
||||
}
|
||||
|
||||
fsOperations.copy {
|
||||
from(workingDir.resolve("screenshots"))
|
||||
into(screenshots)
|
||||
}
|
||||
}
|
||||
|
||||
@get:Inject
|
||||
protected abstract val fsOperations: FileSystemOperations
|
||||
}
|
||||
|
||||
/**
|
||||
* A service for [JavaExec] tasks which start Minecraft.
|
||||
*
|
||||
* Tasks may run `usesService(MinecraftRunnerService.get(gradle))` to ensure that only one Minecraft-related task runs
|
||||
* at once.
|
||||
*/
|
||||
abstract class MinecraftRunnerService : BuildService<BuildServiceParameters.None> {
|
||||
private val hasXvfb = lazy {
|
||||
System.getProperty("os.name", "").equals("linux", ignoreCase = true) && ProcessHelpers.onPath("xvfb-run")
|
||||
}
|
||||
|
||||
internal fun wrapClient(exec: JavaExec, run: () -> Unit) = when {
|
||||
hasXvfb.value -> runXvfb(exec, run)
|
||||
else -> run()
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a program under Xvfb, preventing it spawning a window.
|
||||
*/
|
||||
private fun runXvfb(exec: JavaExec, run: () -> Unit) {
|
||||
fun ProcessBuilder.startVerbose(): Process {
|
||||
exec.logger.info("Running ${this.command()}")
|
||||
return start()
|
||||
}
|
||||
|
||||
CloseScope().use { scope ->
|
||||
val dir = Files.createTempDirectory("cctweaked").toAbsolutePath()
|
||||
scope.add { fsOperations.delete { delete(dir) } }
|
||||
|
||||
val authFile = Files.createTempFile(dir, "Xauthority", "").toAbsolutePath()
|
||||
|
||||
val cookie = StringBuilder().also {
|
||||
for (i in 0..31) it.append("0123456789abcdef"[Random.nextInt(16)])
|
||||
}.toString()
|
||||
|
||||
val xvfb =
|
||||
ProcessBuilder("Xvfb", "-displayfd", "1", "-screen", "0", "640x480x24", "-nolisten", "tcp").also {
|
||||
it.inheritIO()
|
||||
it.environment()["XAUTHORITY"] = authFile.toString()
|
||||
it.redirectOutput(ProcessBuilder.Redirect.PIPE)
|
||||
}.startVerbose()
|
||||
scope.add { xvfb.destroyForcibly().waitFor() }
|
||||
|
||||
val server = xvfb.inputReader().use { it.readLine().trim() }
|
||||
exec.logger.info("Running at :$server (XAUTHORITY=$authFile.toA")
|
||||
|
||||
ProcessBuilder("xauth", "add", ":$server", ".", cookie).also {
|
||||
it.inheritIO()
|
||||
it.environment()["XAUTHORITY"] = authFile.toString()
|
||||
}.startVerbose().waitForOrThrow("Failed to setup XAuthority file")
|
||||
|
||||
scope.add {
|
||||
ProcessBuilder("xauth", "remove", ":$server").also {
|
||||
it.inheritIO()
|
||||
it.environment()["XAUTHORITY"] = authFile.toString()
|
||||
}.startVerbose().waitFor()
|
||||
}
|
||||
|
||||
// Wait a few seconds for Xvfb to start. Ugly, but identical to xvfb-run.
|
||||
if (xvfb.waitFor(3, TimeUnit.SECONDS)) {
|
||||
throw GradleException("Xvfb unexpectedly exited (with status code ${xvfb.exitValue()})")
|
||||
}
|
||||
|
||||
exec.environment("XAUTHORITY", authFile.toString())
|
||||
exec.environment("DISPLAY", ":$server")
|
||||
|
||||
run()
|
||||
}
|
||||
}
|
||||
|
||||
@get:Inject
|
||||
protected abstract val fsOperations: FileSystemOperations
|
||||
|
||||
companion object {
|
||||
fun get(gradle: Gradle): Provider<MinecraftRunnerService> =
|
||||
gradle.sharedServices.registerIfAbsent("cc.tweaked.gradle.ClientJavaExec", MinecraftRunnerService::class.java) {
|
||||
maxParallelUsages.set(1)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
package cc.tweaked.gradle
|
||||
|
||||
import org.codehaus.groovy.runtime.ProcessGroovyMethods
|
||||
import org.gradle.api.GradleException
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStreamReader
|
||||
|
||||
@ -31,4 +33,14 @@ internal object ProcessHelpers {
|
||||
if (process.waitFor() != 0) throw IOException("Command exited with a non-0 status")
|
||||
return out
|
||||
}
|
||||
|
||||
fun onPath(name: String): Boolean {
|
||||
val path = System.getenv("PATH") ?: return false
|
||||
return path.splitToSequence(File.pathSeparator).any { File(it, name).exists() }
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Process.waitForOrThrow(message: String) {
|
||||
val ret = waitFor()
|
||||
if (ret != 0) throw GradleException("$message (exited with $ret)")
|
||||
}
|
||||
|
@ -28,10 +28,12 @@ slf4j = "1.7.36"
|
||||
|
||||
# Minecraft mods
|
||||
forgeConfig = "4.2.6"
|
||||
iris = "1.19.x-v1.2.6"
|
||||
iris = "1.19.x-v1.4.0"
|
||||
jei = "11.3.0.262"
|
||||
oculus = "1.2.5"
|
||||
rei = "9.1.550"
|
||||
rubidium = "0.6.1"
|
||||
sodium = "mc1.19.2-0.4.4"
|
||||
|
||||
# Testing
|
||||
byteBuddy = "1.12.18"
|
||||
@ -91,6 +93,8 @@ oculus = { module = "maven.modrinth:oculus", version.ref = "oculus" }
|
||||
rei-api = { module = "me.shedaniel:RoughlyEnoughItems-api", version.ref = "rei" }
|
||||
rei-builtin = { module = "me.shedaniel:RoughlyEnoughItems-default-plugin", version.ref = "rei" }
|
||||
rei-fabric = { module = "me.shedaniel:RoughlyEnoughItems-fabric", version.ref = "rei" }
|
||||
rubidium = { module = "maven.modrinth:rubidium", version.ref = "rubidium" }
|
||||
sodium = { module = "maven.modrinth:sodium", version.ref = "sodium" }
|
||||
|
||||
# Testing
|
||||
byteBuddyAgent = { module ="net.bytebuddy:byte-buddy-agent", version.ref = "byteBuddy" }
|
||||
|
@ -10,6 +10,7 @@ import net.minecraft.core.Registry;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.item.enchantment.Enchantment;
|
||||
@ -31,6 +32,7 @@ public final class Registries {
|
||||
public static final RegistryWrapper<ArgumentTypeInfo<?, ?>> COMMAND_ARGUMENT_TYPES = PlatformHelper.get().wrap(Registry.COMMAND_ARGUMENT_TYPE_REGISTRY);
|
||||
public static final RegistryWrapper<SoundEvent> SOUND_EVENTS = PlatformHelper.get().wrap(Registry.SOUND_EVENT_REGISTRY);
|
||||
public static final RegistryWrapper<RecipeSerializer<?>> RECIPE_SERIALIZERS = PlatformHelper.get().wrap(Registry.RECIPE_SERIALIZER_REGISTRY);
|
||||
public static final RegistryWrapper<MenuType<?>> MENU = PlatformHelper.get().wrap(Registry.MENU_REGISTRY);
|
||||
|
||||
public interface RegistryWrapper<T> extends Iterable<T> {
|
||||
int getId(T object);
|
||||
|
@ -1,23 +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.gametest.api;
|
||||
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Marks classes containing {@linkplain GameTest game tests}.
|
||||
* <p>
|
||||
* This is used by Forge to automatically load and test classes.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface GameTestHolder {
|
||||
}
|
@ -105,7 +105,7 @@ class CCTestCommand {
|
||||
}
|
||||
|
||||
private static Path getSourceComputerPath() {
|
||||
return TestHooks.sourceDir.resolve("computer");
|
||||
return TestHooks.getSourceDir().resolve("computer");
|
||||
}
|
||||
|
||||
private static int error(CommandSourceStack source, String message) {
|
||||
|
@ -12,6 +12,8 @@ import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.gametest.api.ComputerState;
|
||||
import dan200.computercraft.gametest.api.TestExtensionsKt;
|
||||
import net.minecraft.gametest.framework.GameTestSequence;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Optional;
|
||||
@ -24,6 +26,8 @@ import java.util.Optional;
|
||||
* @see TestExtensionsKt#thenComputerOk(GameTestSequence, String, String) To check tests on the computer have passed.
|
||||
*/
|
||||
public class TestAPI extends ComputerState implements ILuaAPI {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TestAPI.class);
|
||||
|
||||
private final IComputerSystem system;
|
||||
private @Nullable String label;
|
||||
|
||||
@ -36,10 +40,10 @@ public class TestAPI extends ComputerState implements ILuaAPI {
|
||||
if (label == null) label = system.getLabel();
|
||||
if (label == null) {
|
||||
label = "#" + system.getID();
|
||||
TestHooks.LOG.warn("Computer {} has no label", label);
|
||||
LOG.warn("Computer {} has no label", label);
|
||||
}
|
||||
|
||||
TestHooks.LOG.info("Computer '{}' has turned on.", label);
|
||||
LOG.info("Computer '{}' has turned on.", label);
|
||||
markers.clear();
|
||||
error = null;
|
||||
lookup.put(label, this);
|
||||
@ -47,7 +51,7 @@ public class TestAPI extends ComputerState implements ILuaAPI {
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
TestHooks.LOG.info("Computer '{}' has shut down.", label);
|
||||
LOG.info("Computer '{}' has shut down.", label);
|
||||
if (lookup.get(label) == this) lookup.remove(label);
|
||||
}
|
||||
|
||||
@ -58,7 +62,7 @@ public class TestAPI extends ComputerState implements ILuaAPI {
|
||||
|
||||
@LuaFunction
|
||||
public final void fail(String message) throws LuaException {
|
||||
TestHooks.LOG.error("Computer '{}' failed with {}", label, message);
|
||||
LOG.error("Computer '{}' failed with {}", label, message);
|
||||
if (markers.contains(ComputerState.DONE)) throw new LuaException("Cannot call fail/ok multiple times.");
|
||||
markers.add(ComputerState.DONE);
|
||||
error = message;
|
||||
@ -77,6 +81,6 @@ public class TestAPI extends ComputerState implements ILuaAPI {
|
||||
|
||||
@LuaFunction
|
||||
public final void log(String message) {
|
||||
TestHooks.LOG.info("[Computer '{}'] {}", label, message);
|
||||
LOG.info("[Computer '{}'] {}", label, message);
|
||||
}
|
||||
}
|
||||
|
@ -1,55 +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.gametest.core;
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI;
|
||||
import dan200.computercraft.gametest.api.Times;
|
||||
import dan200.computercraft.shared.computer.core.ServerContext;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTestRunner;
|
||||
import net.minecraft.gametest.framework.GameTestTicker;
|
||||
import net.minecraft.gametest.framework.StructureUtils;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.GameRules;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class TestHooks {
|
||||
public static final Logger LOG = LoggerFactory.getLogger(TestHooks.class);
|
||||
|
||||
public static final Path sourceDir = Paths.get(System.getProperty("cctest.sources")).normalize().toAbsolutePath();
|
||||
|
||||
public static void init() {
|
||||
ServerContext.luaMachine = ManagedComputers.INSTANCE;
|
||||
ComputerCraftAPI.registerAPIFactory(TestAPI::new);
|
||||
StructureUtils.testStructuresDir = sourceDir.resolve("structures").toString();
|
||||
}
|
||||
|
||||
public static void onServerStarted(MinecraftServer server) {
|
||||
var rules = server.getGameRules();
|
||||
rules.getRule(GameRules.RULE_DAYLIGHT).set(false, server);
|
||||
|
||||
var world = server.getLevel(Level.OVERWORLD);
|
||||
if (world != null) world.setDayTime(Times.NOON);
|
||||
|
||||
LOG.info("Cleaning up after last run");
|
||||
GameTestRunner.clearAllTests(server.overworld(), new BlockPos(0, -60, 0), GameTestTicker.SINGLETON, 200);
|
||||
|
||||
// Delete server context and add one with a mutable machine factory. This allows us to set the factory for
|
||||
// specific test batches without having to reset all computers.
|
||||
for (var computer : ServerContext.get(server).registry().getComputers()) {
|
||||
var label = computer.getLabel() == null ? "#" + computer.getID() : computer.getLabel();
|
||||
LOG.warn("Unexpected computer {}", label);
|
||||
}
|
||||
|
||||
LOG.info("Importing files");
|
||||
CCTestCommand.importFiles(server);
|
||||
}
|
||||
}
|
@ -14,4 +14,7 @@ import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
public interface GameTestSequenceAccessor {
|
||||
@Accessor
|
||||
GameTestInfo getParent();
|
||||
|
||||
@Accessor
|
||||
long getLastTick();
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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.mixin.gametest;
|
||||
|
||||
import net.minecraft.SharedConstants;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Overwrite;
|
||||
|
||||
@Mixin(SharedConstants.class)
|
||||
class SharedConstantsMixin {
|
||||
/**
|
||||
* Disable DFU initialisation.
|
||||
*
|
||||
* @author SquidDev
|
||||
* @reason This doesn't have any impact on gameplay, and slightly speeds up tests.
|
||||
*/
|
||||
@Overwrite
|
||||
public static void enableDataFixerOptimizations() {
|
||||
}
|
||||
}
|
@ -5,18 +5,25 @@
|
||||
*/
|
||||
package dan200.computercraft.gametest
|
||||
|
||||
import dan200.computercraft.api.lua.ObjectArguments
|
||||
import dan200.computercraft.client.gui.AbstractComputerScreen
|
||||
import dan200.computercraft.core.apis.RedstoneAPI
|
||||
import dan200.computercraft.core.apis.TermAPI
|
||||
import dan200.computercraft.core.computer.ComputerSide
|
||||
import dan200.computercraft.gametest.api.*
|
||||
import dan200.computercraft.shared.ModRegistry
|
||||
import dan200.computercraft.test.core.computer.getApi
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.gametest.framework.GameTest
|
||||
import net.minecraft.gametest.framework.GameTestHelper
|
||||
import net.minecraft.world.InteractionHand
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.LeverBlock
|
||||
import net.minecraft.world.level.block.RedstoneLampBlock
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.lwjgl.glfw.GLFW
|
||||
|
||||
@GameTestHolder
|
||||
class Computer_Test {
|
||||
/**
|
||||
* Ensures redstone signals do not travel through computers.
|
||||
@ -50,6 +57,9 @@ class Computer_Test {
|
||||
thenExecute { context.assertBlockHas(lamp, RedstoneLampBlock.LIT, false, "Lamp should still not be lit") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check computers propagate redstone to surrounding blocks.
|
||||
*/
|
||||
@GameTest
|
||||
fun Set_and_destroy(context: GameTestHelper) = context.sequence {
|
||||
val lamp = BlockPos(2, 2, 3)
|
||||
@ -62,6 +72,9 @@ class Computer_Test {
|
||||
thenExecute { context.assertBlockHas(lamp, RedstoneLampBlock.LIT, false, "Lamp should not be lit") }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check computers and turtles expose peripherals.
|
||||
*/
|
||||
@GameTest
|
||||
fun Computer_peripheral(context: GameTestHelper) = context.sequence {
|
||||
thenExecute {
|
||||
@ -69,4 +82,62 @@ class Computer_Test {
|
||||
context.assertPeripheral(BlockPos(1, 2, 2), type = "turtle")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the client can open the computer UI and interact with it.
|
||||
*/
|
||||
@ClientGameTest
|
||||
fun Open_on_client(context: GameTestHelper) = context.sequence {
|
||||
// Write "Hello, world!" and then print each event to the terminal.
|
||||
thenOnComputer { getApi<TermAPI>().write(ObjectArguments("Hello, world!")) }
|
||||
thenStartComputer {
|
||||
val term = getApi<TermAPI>().terminal
|
||||
while (true) {
|
||||
val event = pullEvent()
|
||||
if (term.cursorY >= term.height) {
|
||||
term.scroll(1)
|
||||
term.setCursorPos(0, term.height)
|
||||
} else {
|
||||
term.setCursorPos(0, term.cursorY + 1)
|
||||
}
|
||||
|
||||
term.write(event.contentToString())
|
||||
}
|
||||
}
|
||||
// Teleport the player to the computer and then open it.
|
||||
thenExecute {
|
||||
context.positionAt(BlockPos(2, 2, 1))
|
||||
val computer = context.getBlockEntity(BlockPos(2, 2, 2), ModRegistry.BlockEntities.COMPUTER_ADVANCED.get())
|
||||
computer.use(context.level.randomPlayer!!, InteractionHand.MAIN_HAND)
|
||||
}
|
||||
// Assert the terminal is synced to the client.
|
||||
thenIdle(2)
|
||||
thenOnClient {
|
||||
val menu = getOpenMenu(ModRegistry.Menus.COMPUTER.get())
|
||||
val term = menu.terminal
|
||||
assertEquals("Hello, world!", term.getLine(0).toString().trim(), "Terminal contents is synced")
|
||||
assertTrue(menu.isOn, "Computer is on")
|
||||
}
|
||||
// Press a key on the client
|
||||
thenOnClient {
|
||||
val screen = minecraft.screen as AbstractComputerScreen<*>
|
||||
screen.keyPressed(GLFW.GLFW_KEY_A, 0, 0)
|
||||
screen.keyReleased(GLFW.GLFW_KEY_A, 0, 0)
|
||||
}
|
||||
// And assert it is handled and sent back to the client
|
||||
thenIdle(2)
|
||||
thenOnClient {
|
||||
val term = getOpenMenu(ModRegistry.Menus.COMPUTER.get()).terminal
|
||||
assertEquals(
|
||||
"[key, ${GLFW.GLFW_KEY_A}, false]",
|
||||
term.getLine(1).toString().trim(),
|
||||
"Terminal contents is synced",
|
||||
)
|
||||
assertEquals(
|
||||
"[key_up, ${GLFW.GLFW_KEY_A}]",
|
||||
term.getLine(2).toString().trim(),
|
||||
"Terminal contents is synced",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,14 +5,12 @@
|
||||
*/
|
||||
package dan200.computercraft.gametest
|
||||
|
||||
import dan200.computercraft.gametest.api.GameTestHolder
|
||||
import dan200.computercraft.gametest.api.Timeouts
|
||||
import dan200.computercraft.gametest.api.sequence
|
||||
import dan200.computercraft.gametest.api.thenComputerOk
|
||||
import net.minecraft.gametest.framework.GameTest
|
||||
import net.minecraft.gametest.framework.GameTestHelper
|
||||
|
||||
@GameTestHolder
|
||||
class CraftOs_Test {
|
||||
/**
|
||||
* Sends a rednet message to another a computer and back again.
|
||||
|
@ -21,7 +21,6 @@ import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.level.block.RedStoneWireBlock
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
|
||||
@GameTestHolder
|
||||
class Disk_Drive_Test {
|
||||
/**
|
||||
* Ensure audio disks exist and we can play them.
|
||||
|
@ -5,7 +5,6 @@
|
||||
*/
|
||||
package dan200.computercraft.gametest
|
||||
|
||||
import dan200.computercraft.gametest.api.GameTestHolder
|
||||
import dan200.computercraft.gametest.api.Structures
|
||||
import dan200.computercraft.gametest.api.sequence
|
||||
import dan200.computercraft.shared.ModRegistry
|
||||
@ -16,7 +15,6 @@ import net.minecraft.world.level.block.Blocks
|
||||
import net.minecraft.world.level.block.entity.ChestBlockEntity
|
||||
import net.minecraft.world.level.storage.loot.BuiltInLootTables
|
||||
|
||||
@GameTestHolder
|
||||
class Loot_Test {
|
||||
/**
|
||||
* Test that the loot tables will spawn in treasure disks.
|
||||
|
@ -20,7 +20,6 @@ import net.minecraft.gametest.framework.GameTestHelper
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@GameTestHolder
|
||||
class Modem_Test {
|
||||
@GameTest
|
||||
fun Have_peripherals(helper: GameTestHelper) = helper.sequence {
|
||||
|
@ -7,17 +7,20 @@ package dan200.computercraft.gametest
|
||||
|
||||
import dan200.computercraft.gametest.api.*
|
||||
import dan200.computercraft.shared.ModRegistry
|
||||
import dan200.computercraft.shared.config.Config
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorBlock
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorEdgeState
|
||||
import dan200.computercraft.shared.peripheral.monitor.MonitorRenderer
|
||||
import net.minecraft.commands.arguments.blocks.BlockInput
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.gametest.framework.GameTest
|
||||
import net.minecraft.gametest.framework.GameTestGenerator
|
||||
import net.minecraft.gametest.framework.GameTestHelper
|
||||
import net.minecraft.gametest.framework.TestFunction
|
||||
import net.minecraft.nbt.CompoundTag
|
||||
import net.minecraft.world.level.block.Blocks
|
||||
import java.util.*
|
||||
|
||||
@GameTestHolder
|
||||
class Monitor_Test {
|
||||
@GameTest
|
||||
fun Ensures_valid_on_place(context: GameTestHelper) = context.sequence {
|
||||
@ -58,4 +61,64 @@ class Monitor_Test {
|
||||
helper.assertBlockHas(BlockPos(3, 2, 2), MonitorBlock.STATE, MonitorEdgeState.NONE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test
|
||||
*/
|
||||
@GameTestGenerator
|
||||
fun Render_monitor_tests(): List<TestFunction> {
|
||||
val tests = mutableListOf<TestFunction>()
|
||||
|
||||
fun addTest(label: String, renderer: MonitorRenderer, time: Long = Times.NOON, tag: String = TestTags.CLIENT) {
|
||||
if (!TestTags.isEnabled(tag)) return
|
||||
|
||||
val className = Monitor_Test::class.java.simpleName.lowercase()
|
||||
val testName = "$className.render_monitor"
|
||||
|
||||
tests.add(
|
||||
TestFunction(
|
||||
"$testName.$label",
|
||||
"$testName.$label",
|
||||
testName,
|
||||
Timeouts.DEFAULT,
|
||||
0,
|
||||
true,
|
||||
) { renderMonitor(it, renderer, time) },
|
||||
)
|
||||
}
|
||||
|
||||
addTest("tbo_noon", MonitorRenderer.TBO, Times.NOON)
|
||||
addTest("tbo_midnight", MonitorRenderer.TBO, Times.MIDNIGHT)
|
||||
addTest("vbo_noon", MonitorRenderer.VBO, Times.NOON)
|
||||
addTest("vbo_midnight", MonitorRenderer.VBO, Times.MIDNIGHT)
|
||||
|
||||
addTest("sodium_tbo", MonitorRenderer.TBO, tag = "sodium")
|
||||
addTest("sodium_vbo", MonitorRenderer.VBO, tag = "sodium")
|
||||
|
||||
addTest("iris_noon", MonitorRenderer.BEST, Times.NOON, tag = "iris")
|
||||
addTest("iris_midnight", MonitorRenderer.BEST, Times.MIDNIGHT, tag = "iris")
|
||||
|
||||
return tests
|
||||
}
|
||||
|
||||
private fun renderMonitor(helper: GameTestHelper, renderer: MonitorRenderer, time: Long) = helper.sequence {
|
||||
thenExecute {
|
||||
Config.monitorRenderer = renderer
|
||||
helper.level.dayTime = time
|
||||
helper.positionAtArmorStand()
|
||||
|
||||
// Get the monitor and peripheral. This forces us to create a server monitor at this location.
|
||||
val monitor = helper.getBlockEntity(BlockPos(2, 2, 3), ModRegistry.BlockEntities.MONITOR_ADVANCED.get())
|
||||
monitor.peripheral()
|
||||
|
||||
val terminal = monitor.cachedServerMonitor!!.terminal!!
|
||||
terminal.write("Hello, world!")
|
||||
terminal.setCursorPos(1, 2)
|
||||
terminal.textColour = 2
|
||||
terminal.backgroundColour = 3
|
||||
terminal.write("Some coloured text")
|
||||
}
|
||||
|
||||
thenScreenshot()
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
package dan200.computercraft.gametest
|
||||
|
||||
import dan200.computercraft.gametest.api.*
|
||||
import dan200.computercraft.gametest.api.assertBlockHas
|
||||
import dan200.computercraft.gametest.api.assertExactlyItems
|
||||
import dan200.computercraft.gametest.api.getBlockEntity
|
||||
import dan200.computercraft.gametest.api.sequence
|
||||
import dan200.computercraft.shared.ModRegistry
|
||||
import dan200.computercraft.shared.peripheral.printer.PrinterBlock
|
||||
import net.minecraft.core.BlockPos
|
||||
@ -11,7 +14,6 @@ import net.minecraft.world.item.ItemStack
|
||||
import net.minecraft.world.item.Items
|
||||
import net.minecraft.world.level.block.RedStoneWireBlock
|
||||
|
||||
@GameTestHolder
|
||||
class Printer_Test {
|
||||
/**
|
||||
* Check comparators can read the contents of the disk drive
|
||||
|
@ -5,7 +5,6 @@
|
||||
*/
|
||||
package dan200.computercraft.gametest
|
||||
|
||||
import dan200.computercraft.gametest.api.GameTestHolder
|
||||
import dan200.computercraft.gametest.api.Structures
|
||||
import dan200.computercraft.gametest.api.sequence
|
||||
import dan200.computercraft.shared.ModRegistry
|
||||
@ -24,7 +23,6 @@ import net.minecraft.world.item.crafting.RecipeType
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import java.util.*
|
||||
|
||||
@GameTestHolder
|
||||
class Recipe_Test {
|
||||
/**
|
||||
* Test that crafting results contain NBT data.
|
||||
|
@ -38,7 +38,6 @@ import org.junit.jupiter.api.Assertions.assertNotEquals
|
||||
import java.util.*
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@GameTestHolder
|
||||
class Turtle_Test {
|
||||
@GameTest
|
||||
fun Unequip_refreshes_peripheral(helper: GameTestHelper) = helper.sequence {
|
||||
@ -421,7 +420,7 @@ class Turtle_Test {
|
||||
turtle.back().await().assertArrayEquals(true, message = "Moved turtle forward")
|
||||
TestHooks.LOG.info("[{}] Finished turtle at {}", testInfo, testInfo.`computercraft$getTick`())
|
||||
}
|
||||
thenIdle(2) // Should happen immediately, but computers might be slow.
|
||||
thenIdle(4) // Should happen immediately, but computers might be slow.
|
||||
thenExecute {
|
||||
assertEquals(
|
||||
listOf(
|
||||
|
@ -0,0 +1,28 @@
|
||||
package dan200.computercraft.gametest.api
|
||||
|
||||
import net.minecraft.gametest.framework.GameTest
|
||||
|
||||
/**
|
||||
* Similar to [GameTest], this annotation defines a method which runs under Minecraft's gametest sequence.
|
||||
*
|
||||
* Unlike standard game tests, client game tests are only registered when running under the Minecraft client, and run
|
||||
* sequentially rather than in parallel.
|
||||
*/
|
||||
@Target(AnnotationTarget.FUNCTION)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class ClientGameTest(
|
||||
/**
|
||||
* The template to use for this test, identical to [GameTest.template]
|
||||
*/
|
||||
val template: String = "",
|
||||
|
||||
/**
|
||||
* The timeout for this test, identical to [GameTest.timeoutTicks].
|
||||
*/
|
||||
val timeoutTicks: Int = Timeouts.DEFAULT,
|
||||
|
||||
/**
|
||||
* The tag associated with this test, denoting when it should run.
|
||||
*/
|
||||
val tag: String = TestTags.CLIENT,
|
||||
)
|
@ -0,0 +1,132 @@
|
||||
package dan200.computercraft.gametest.api
|
||||
|
||||
import dan200.computercraft.mixin.gametest.GameTestSequenceAccessor
|
||||
import dan200.computercraft.shared.platform.Registries
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.Screenshot
|
||||
import net.minecraft.client.gui.screens.inventory.MenuAccess
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.gametest.framework.GameTestAssertException
|
||||
import net.minecraft.gametest.framework.GameTestHelper
|
||||
import net.minecraft.gametest.framework.GameTestSequence
|
||||
import net.minecraft.server.level.ServerPlayer
|
||||
import net.minecraft.world.entity.EntityType
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu
|
||||
import net.minecraft.world.inventory.MenuType
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.ExecutionException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
/**
|
||||
* Attempt to guess whether all chunks have been rendered.
|
||||
*/
|
||||
fun Minecraft.isRenderingStable(): Boolean = level != null && player != null &&
|
||||
levelRenderer.isChunkCompiled(player!!.blockPosition()) && levelRenderer.countRenderedChunks() > 10 &&
|
||||
levelRenderer.hasRenderedAllChunks()
|
||||
|
||||
/**
|
||||
* Run a task on the client.
|
||||
*/
|
||||
fun GameTestSequence.thenOnClient(task: ClientTestHelper.() -> Unit): GameTestSequence {
|
||||
var future: CompletableFuture<Void>? = null
|
||||
thenExecute { future = Minecraft.getInstance().submit { task(ClientTestHelper()) } }
|
||||
thenWaitUntil { if (!future!!.isDone) throw GameTestAssertException("Not done task yet") }
|
||||
thenExecute {
|
||||
try {
|
||||
future!!.get()
|
||||
} catch (e: ExecutionException) {
|
||||
throw e.cause ?: e
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a screenshot of the current game state.
|
||||
*/
|
||||
fun GameTestSequence.thenScreenshot(name: String? = null): GameTestSequence {
|
||||
val suffix = if (name == null) "" else "-$name"
|
||||
val test = (this as GameTestSequenceAccessor).parent
|
||||
val fullName = "${test.testName}$suffix"
|
||||
|
||||
// Wait until all chunks have been rendered and we're idle for an extended period.
|
||||
var counter = 0
|
||||
thenWaitUntil {
|
||||
if (Minecraft.getInstance().isRenderingStable()) {
|
||||
val idleFor = ++counter
|
||||
if (idleFor <= 20) throw GameTestAssertException("Only idle for $idleFor ticks")
|
||||
} else {
|
||||
counter = 0
|
||||
throw GameTestAssertException("Waiting for client to finish rendering")
|
||||
}
|
||||
}
|
||||
|
||||
// Now disable the GUI, take a screenshot and reenable it. Sleep a little afterwards to ensure the render thread
|
||||
// has caught up.
|
||||
thenOnClient { minecraft.options.hideGui = true }
|
||||
thenIdle(2)
|
||||
|
||||
// Take a screenshot and wait for it to have finished.
|
||||
val hasScreenshot = AtomicBoolean()
|
||||
thenOnClient { screenshot("$fullName.png") { hasScreenshot.set(true) } }
|
||||
thenWaitUntil { if (!hasScreenshot.get()) throw GameTestAssertException("Screenshot does not exist") }
|
||||
thenOnClient { minecraft.options.hideGui = false }
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* "Reset" the current player, ensuring.
|
||||
*/
|
||||
fun ServerPlayer.setupForTest() {
|
||||
if (containerMenu != inventoryMenu) closeContainer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the player at an armor stand.
|
||||
*/
|
||||
fun GameTestHelper.positionAtArmorStand() {
|
||||
val stand = getEntity(EntityType.ARMOR_STAND)
|
||||
val player = level.randomPlayer ?: throw GameTestAssertException("Player does not exist")
|
||||
|
||||
player.setupForTest()
|
||||
player.connection.teleport(stand.x, stand.y, stand.z, stand.yRot, stand.xRot)
|
||||
}
|
||||
|
||||
/**
|
||||
* Position the player at a given coordinate.
|
||||
*/
|
||||
fun GameTestHelper.positionAt(pos: BlockPos) {
|
||||
val absolutePos = absolutePos(pos)
|
||||
val player = level.randomPlayer ?: throw GameTestAssertException("Player does not exist")
|
||||
|
||||
player.setupForTest()
|
||||
player.connection.teleport(absolutePos.x + 0.5, absolutePos.y + 0.5, absolutePos.z + 0.5, 0.0f, 0.0f)
|
||||
}
|
||||
|
||||
/**
|
||||
* The equivalent of a [GameTestHelper] on the client.
|
||||
*/
|
||||
class ClientTestHelper {
|
||||
val minecraft: Minecraft = Minecraft.getInstance()
|
||||
|
||||
fun screenshot(name: String, callback: () -> Unit = {}) {
|
||||
Screenshot.grab(minecraft.gameDirectory, name, minecraft.mainRenderTarget) { callback() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently open [AbstractContainerMenu], ensuring it is of a specific type.
|
||||
*/
|
||||
fun <T : AbstractContainerMenu> getOpenMenu(type: MenuType<T>): T {
|
||||
fun getName(type: MenuType<*>) = Registries.MENU.getKey(type)
|
||||
|
||||
val screen = minecraft.screen
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
when {
|
||||
screen == null -> throw GameTestAssertException("Expected a ${getName(type)} menu, but no screen is open")
|
||||
screen !is MenuAccess<*> -> throw GameTestAssertException("Expected a ${getName(type)} menu, but a $screen is open")
|
||||
screen.menu.type != type -> throw GameTestAssertException("Expected a ${getName(type)} menu, but a ${getName(screen.menu.type)} is open")
|
||||
else -> return screen.menu as T
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ package dan200.computercraft.gametest.api
|
||||
|
||||
import dan200.computercraft.gametest.core.ManagedComputers
|
||||
import dan200.computercraft.mixin.gametest.GameTestHelperAccessor
|
||||
import dan200.computercraft.mixin.gametest.GameTestInfoAccessor
|
||||
import dan200.computercraft.mixin.gametest.GameTestSequenceAccessor
|
||||
import dan200.computercraft.shared.platform.PlatformHelper
|
||||
import dan200.computercraft.shared.platform.Registries
|
||||
@ -41,6 +42,8 @@ object Structures {
|
||||
/** Pre-set in-game times */
|
||||
object Times {
|
||||
const val NOON: Long = 6000
|
||||
|
||||
const val MIDNIGHT: Long = 18000
|
||||
}
|
||||
|
||||
/**
|
||||
@ -49,7 +52,9 @@ object Times {
|
||||
* @see GameTest.timeoutTicks
|
||||
*/
|
||||
object Timeouts {
|
||||
private const val SECOND: Int = 20
|
||||
const val SECOND: Int = 20
|
||||
|
||||
const val DEFAULT: Int = SECOND * 5
|
||||
|
||||
const val COMPUTER_TIMEOUT: Int = SECOND * 15
|
||||
}
|
||||
@ -90,11 +95,18 @@ fun GameTestSequence.thenStartComputer(name: String? = null, action: suspend Lua
|
||||
* Run a task on a computer and wait for it to finish.
|
||||
*/
|
||||
fun GameTestSequence.thenOnComputer(name: String? = null, action: suspend LuaTaskContext.() -> Unit): GameTestSequence {
|
||||
val test = (this as GameTestSequenceAccessor).parent
|
||||
val self = (this as GameTestSequenceAccessor)
|
||||
val test = self.parent
|
||||
|
||||
val label = test.testName + (if (name == null) "" else ".$name")
|
||||
var monitor: ManagedComputers.Monitor? = null
|
||||
thenExecuteFailFast { monitor = ManagedComputers.enqueue(test, label, action) }
|
||||
thenWaitUntil { if (!monitor!!.isFinished) throw GameTestAssertException("Computer '$label' has not finished yet.") }
|
||||
thenWaitUntil {
|
||||
if (!monitor!!.isFinished) {
|
||||
val runningFor = (test as GameTestInfoAccessor).`computercraft$getTick`() - self.lastTick
|
||||
throw GameTestAssertException("Computer '$label' has not finished yet (running for $runningFor ticks).")
|
||||
}
|
||||
}
|
||||
thenExecuteFailFast { monitor!!.check() }
|
||||
return this
|
||||
}
|
||||
|
@ -0,0 +1,16 @@
|
||||
package dan200.computercraft.gametest.api
|
||||
|
||||
/**
|
||||
* "Tags" associated with each test, denoting whether a specific test should be registered for the current Minecraft
|
||||
* session.
|
||||
*
|
||||
* This is used to only run some tests on the client, or when a specific mod is loaded.
|
||||
*/
|
||||
object TestTags {
|
||||
const val COMMON = "common"
|
||||
const val CLIENT = "client"
|
||||
|
||||
private val tags: Set<String> = System.getProperty("cctest.tags", COMMON).split(',').toSet()
|
||||
|
||||
fun isEnabled(tag: String) = tags.contains(tag)
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
package dan200.computercraft.gametest.core
|
||||
|
||||
import dan200.computercraft.gametest.api.Timeouts
|
||||
import dan200.computercraft.gametest.api.isRenderingStable
|
||||
import dan200.computercraft.gametest.api.setupForTest
|
||||
import net.minecraft.client.CloudStatus
|
||||
import net.minecraft.client.Minecraft
|
||||
import net.minecraft.client.ParticleStatus
|
||||
import net.minecraft.client.gui.screens.Screen
|
||||
import net.minecraft.client.gui.screens.TitleScreen
|
||||
import net.minecraft.client.tutorial.TutorialSteps
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.core.Registry
|
||||
import net.minecraft.core.RegistryAccess
|
||||
import net.minecraft.gametest.framework.*
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.sounds.SoundSource
|
||||
import net.minecraft.util.RandomSource
|
||||
import net.minecraft.world.Difficulty
|
||||
import net.minecraft.world.level.DataPackConfig
|
||||
import net.minecraft.world.level.GameRules
|
||||
import net.minecraft.world.level.GameType
|
||||
import net.minecraft.world.level.LevelSettings
|
||||
import net.minecraft.world.level.block.Rotation
|
||||
import net.minecraft.world.level.levelgen.presets.WorldPresets
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/**
|
||||
* Client-side hooks for game tests.
|
||||
*
|
||||
* This mirrors Minecraft's
|
||||
*/
|
||||
object ClientTestHooks {
|
||||
private val LOG: Logger = LoggerFactory.getLogger(ClientTestHooks::class.java)
|
||||
|
||||
private const val LEVEL_NAME = "test"
|
||||
|
||||
/**
|
||||
* Time (in ticks) that we wait after the client joins the world
|
||||
*/
|
||||
private const val STARTUP_DELAY = 5 * Timeouts.SECOND
|
||||
|
||||
/**
|
||||
* Whether our client-side game test driver is enabled.
|
||||
*/
|
||||
private val enabled: Boolean = System.getProperty("cctest.client") != null
|
||||
|
||||
private var loadedWorld: Boolean = false
|
||||
|
||||
@JvmStatic
|
||||
fun onOpenScreen(screen: Screen): Boolean = when {
|
||||
enabled && !loadedWorld && screen is TitleScreen -> {
|
||||
loadedWorld = true
|
||||
openWorld()
|
||||
true
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
|
||||
/**
|
||||
* Open or create our test world immediately on game launch.
|
||||
*/
|
||||
private fun openWorld() {
|
||||
val minecraft = Minecraft.getInstance()
|
||||
|
||||
// Clear some options before we get any further.
|
||||
minecraft.options.autoJump().set(false)
|
||||
minecraft.options.cloudStatus().set(CloudStatus.OFF)
|
||||
minecraft.options.particles().set(ParticleStatus.MINIMAL)
|
||||
minecraft.options.tutorialStep = TutorialSteps.NONE
|
||||
minecraft.options.renderDistance().set(6)
|
||||
minecraft.options.gamma().set(1.0)
|
||||
minecraft.options.setSoundCategoryVolume(SoundSource.MUSIC, 0.0f)
|
||||
minecraft.options.setSoundCategoryVolume(SoundSource.AMBIENT, 0.0f)
|
||||
|
||||
if (minecraft.levelSource.levelExists(LEVEL_NAME)) {
|
||||
LOG.info("World already exists, opening.")
|
||||
minecraft.createWorldOpenFlows().loadLevel(minecraft.screen, LEVEL_NAME)
|
||||
} else {
|
||||
LOG.info("World does not exist, creating it.")
|
||||
val rules = GameRules()
|
||||
rules.getRule(GameRules.RULE_DOMOBSPAWNING).set(false, null)
|
||||
rules.getRule(GameRules.RULE_DAYLIGHT).set(false, null)
|
||||
rules.getRule(GameRules.RULE_WEATHER_CYCLE).set(false, null)
|
||||
|
||||
val registries = RegistryAccess.builtinCopy().freeze()
|
||||
minecraft.createWorldOpenFlows().createFreshLevel(
|
||||
LEVEL_NAME,
|
||||
LevelSettings("Test Level", GameType.CREATIVE, false, Difficulty.EASY, true, rules, DataPackConfig.DEFAULT),
|
||||
registries,
|
||||
registries
|
||||
.registryOrThrow(Registry.WORLD_PRESET_REGISTRY)
|
||||
.getHolderOrThrow(WorldPresets.FLAT).value()
|
||||
.createWorldGenSettings(RandomSource.create().nextLong(), false, false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private var testTracker: MultipleTestTracker? = null
|
||||
private var hasFinished: Boolean = false
|
||||
private var startupDelay: Int = STARTUP_DELAY
|
||||
|
||||
@JvmStatic
|
||||
fun onServerTick(server: MinecraftServer) {
|
||||
if (!enabled || hasFinished) return
|
||||
|
||||
val testTracker = when (val tracker = this.testTracker) {
|
||||
null -> {
|
||||
if (server.overworld().players().isEmpty()) return
|
||||
if (!Minecraft.getInstance().isRenderingStable()) return
|
||||
if (startupDelay >= 0) {
|
||||
// TODO: Is there a better way? Maybe set a flag when the client starts rendering?
|
||||
startupDelay--
|
||||
return
|
||||
}
|
||||
|
||||
LOG.info("Server ready, starting.")
|
||||
|
||||
val tests = GameTestRunner.runTestBatches(
|
||||
GameTestRunner.groupTestsIntoBatches(GameTestRegistry.getAllTestFunctions()),
|
||||
BlockPos(0, -60, 0),
|
||||
Rotation.NONE,
|
||||
server.overworld(),
|
||||
GameTestTicker.SINGLETON,
|
||||
8,
|
||||
)
|
||||
val testTracker = MultipleTestTracker(tests)
|
||||
testTracker.addListener(
|
||||
object : GameTestListener {
|
||||
fun testFinished() {
|
||||
for (it in server.playerList.players) it.setupForTest()
|
||||
}
|
||||
|
||||
override fun testPassed(test: GameTestInfo) = testFinished()
|
||||
override fun testFailed(test: GameTestInfo) = testFinished()
|
||||
override fun testStructureLoaded(test: GameTestInfo) = Unit
|
||||
},
|
||||
)
|
||||
|
||||
LOG.info("{} tests are now running!", testTracker.totalCount)
|
||||
this.testTracker = testTracker
|
||||
testTracker
|
||||
}
|
||||
|
||||
else -> tracker
|
||||
}
|
||||
|
||||
if (server.overworld().gameTime % 20L == 0L) LOG.info(testTracker.progressBar)
|
||||
|
||||
if (testTracker.isDone) {
|
||||
hasFinished = true
|
||||
LOG.info(testTracker.progressBar)
|
||||
|
||||
GlobalTestReporter.finish()
|
||||
LOG.info("========= {} GAME TESTS COMPLETE ======================", testTracker.totalCount)
|
||||
if (testTracker.hasFailedRequired()) {
|
||||
LOG.info("{} required tests failed :(", testTracker.failedRequiredCount)
|
||||
for (test in testTracker.failedRequired) LOG.info(" - {}", test.testName)
|
||||
} else {
|
||||
LOG.info("All {} required tests passed :)", testTracker.totalCount)
|
||||
}
|
||||
if (testTracker.hasFailedOptional()) {
|
||||
LOG.info("{} optional tests failed", testTracker.failedOptionalCount)
|
||||
for (test in testTracker.failedOptional) LOG.info(" - {}", test.testName)
|
||||
}
|
||||
LOG.info("====================================================")
|
||||
|
||||
// Stop Minecraft *from the client thread*. We need to do this to avoid deadlocks in stopping the server.
|
||||
val minecraft = Minecraft.getInstance()
|
||||
minecraft.execute {
|
||||
LOG.info("Stopping client.")
|
||||
minecraft.level!!.disconnect()
|
||||
minecraft.clearLevel()
|
||||
minecraft.stop()
|
||||
|
||||
exitProcess(
|
||||
when {
|
||||
testTracker.totalCount == 0 -> 1
|
||||
testTracker.hasFailedRequired() -> 2
|
||||
else -> 0
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
package dan200.computercraft.gametest.core
|
||||
|
||||
import dan200.computercraft.api.ComputerCraftAPI
|
||||
import dan200.computercraft.gametest.*
|
||||
import dan200.computercraft.gametest.api.ClientGameTest
|
||||
import dan200.computercraft.gametest.api.TestTags
|
||||
import dan200.computercraft.gametest.api.Times
|
||||
import dan200.computercraft.shared.computer.core.ServerContext
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.gametest.framework.*
|
||||
import net.minecraft.server.MinecraftServer
|
||||
import net.minecraft.world.level.GameRules
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Method
|
||||
import java.lang.reflect.Modifier
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.util.function.Consumer
|
||||
import javax.xml.parsers.ParserConfigurationException
|
||||
|
||||
object TestHooks {
|
||||
@JvmField
|
||||
val LOG: Logger = LoggerFactory.getLogger(TestHooks::class.java)
|
||||
|
||||
@JvmStatic
|
||||
val sourceDir: Path = Paths.get(System.getProperty("cctest.sources")).normalize().toAbsolutePath()
|
||||
|
||||
@JvmStatic
|
||||
fun init() {
|
||||
ServerContext.luaMachine = ManagedComputers
|
||||
ComputerCraftAPI.registerAPIFactory(::TestAPI)
|
||||
StructureUtils.testStructuresDir = sourceDir.resolve("structures").toString()
|
||||
|
||||
// Set up our test reporter if configured.
|
||||
val outputPath = System.getProperty("cctest.gametest-report")
|
||||
if (outputPath != null) {
|
||||
try {
|
||||
GlobalTestReporter.replaceWith(
|
||||
MultiTestReporter(
|
||||
JunitTestReporter(File(outputPath)),
|
||||
LogTestReporter(),
|
||||
),
|
||||
)
|
||||
} catch (e: ParserConfigurationException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun onServerStarted(server: MinecraftServer) {
|
||||
val rules = server.gameRules
|
||||
rules.getRule(GameRules.RULE_DAYLIGHT).set(false, server)
|
||||
server.overworld().dayTime = Times.NOON
|
||||
|
||||
LOG.info("Cleaning up after last run")
|
||||
GameTestRunner.clearAllTests(server.overworld(), BlockPos(0, -60, 0), GameTestTicker.SINGLETON, 200)
|
||||
|
||||
// Delete server context and add one with a mutable machine factory. This allows us to set the factory for
|
||||
// specific test batches without having to reset all computers.
|
||||
for (computer in ServerContext.get(server).registry().computers) {
|
||||
val label = if (computer.label == null) "#" + computer.id else computer.label!!
|
||||
LOG.warn("Unexpected computer {}", label)
|
||||
}
|
||||
|
||||
LOG.info("Importing files")
|
||||
CCTestCommand.importFiles(server)
|
||||
}
|
||||
|
||||
private val testClasses = listOf(
|
||||
Computer_Test::class.java,
|
||||
CraftOs_Test::class.java,
|
||||
Disk_Drive_Test::class.java,
|
||||
Loot_Test::class.java,
|
||||
Modem_Test::class.java,
|
||||
Monitor_Test::class.java,
|
||||
Printer_Test::class.java,
|
||||
Recipe_Test::class.java,
|
||||
Turtle_Test::class.java,
|
||||
)
|
||||
|
||||
/**
|
||||
* Register all of our gametests.
|
||||
*
|
||||
* This is super nasty, as it bypasses any loader-specific hooks for registering tests. However, it makes it much
|
||||
* easier to ensure consistent behaviour between loaders (namely making [GameTest.template] point to a
|
||||
* structure rather than a per-test-class one), as well as supporting our custom client tests.
|
||||
*
|
||||
* @param fallbackRegister A fallback function which registers non-test methods (such as [BeforeBatch]). This
|
||||
* should be [GameTestRegistry.register] or equivalent.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun loadTests(fallbackRegister: Consumer<Method>) {
|
||||
for (testClass in testClasses) {
|
||||
for (method in testClass.declaredMethods) {
|
||||
registerTest(testClass, method, fallbackRegister)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val isCi = System.getenv("CI") != null
|
||||
|
||||
/**
|
||||
* Adjust the timeout of a test. This makes it 1.5 times longer when run under CI, as CI servers are less powerful
|
||||
* than our own.
|
||||
*/
|
||||
private fun adjustTimeout(timeout: Int): Int = if (isCi) timeout + (timeout / 2) else timeout
|
||||
|
||||
private fun registerTest(testClass: Class<*>, method: Method, fallbackRegister: Consumer<Method>) {
|
||||
val className = testClass.simpleName.lowercase()
|
||||
val testName = className + "." + method.name.lowercase()
|
||||
|
||||
method.getAnnotation(GameTest::class.java)?.let { testInfo ->
|
||||
if (!TestTags.isEnabled(TestTags.COMMON)) return
|
||||
|
||||
GameTestRegistry.getAllTestFunctions().add(
|
||||
TestFunction(
|
||||
testInfo.batch, testName, testInfo.template.ifEmpty { testName },
|
||||
StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps),
|
||||
adjustTimeout(testInfo.timeoutTicks),
|
||||
testInfo.setupTicks,
|
||||
testInfo.required, testInfo.requiredSuccesses, testInfo.attempts,
|
||||
) { value -> safeInvoke(method, value) },
|
||||
)
|
||||
GameTestRegistry.getAllTestClassNames().add(testClass.simpleName)
|
||||
return
|
||||
}
|
||||
|
||||
method.getAnnotation(ClientGameTest::class.java)?.let { testInfo ->
|
||||
if (!TestTags.isEnabled(testInfo.tag)) return
|
||||
|
||||
GameTestRegistry.getAllTestFunctions().add(
|
||||
TestFunction(
|
||||
testName,
|
||||
testName,
|
||||
testInfo.template.ifEmpty { testName },
|
||||
adjustTimeout(testInfo.timeoutTicks),
|
||||
0,
|
||||
true,
|
||||
) { value -> safeInvoke(method, value) },
|
||||
)
|
||||
GameTestRegistry.getAllTestClassNames().add(testClass.simpleName)
|
||||
return
|
||||
}
|
||||
|
||||
fallbackRegister.accept(method)
|
||||
}
|
||||
|
||||
private fun safeInvoke(method: Method, value: Any) {
|
||||
try {
|
||||
var instance: Any? = null
|
||||
if (!Modifier.isStatic(method.modifiers)) {
|
||||
instance = method.declaringClass.getConstructor().newInstance()
|
||||
}
|
||||
method.invoke(instance, value)
|
||||
} catch (e: InvocationTargetException) {
|
||||
when (val cause = e.cause) {
|
||||
is RuntimeException -> throw cause
|
||||
else -> throw RuntimeException(cause)
|
||||
}
|
||||
} catch (e: ReflectiveOperationException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package dan200.computercraft.gametest.core
|
||||
|
||||
import net.minecraft.gametest.framework.GameTestInfo
|
||||
import net.minecraft.gametest.framework.JUnitLikeTestReporter
|
||||
import net.minecraft.gametest.framework.TestReporter
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.nio.file.Files
|
||||
import javax.xml.transform.TransformerException
|
||||
|
||||
/**
|
||||
* A test reporter which delegates to a list of other reporters.
|
||||
*/
|
||||
class MultiTestReporter(private val reporters: List<TestReporter>) : TestReporter {
|
||||
constructor(vararg reporters: TestReporter) : this(listOf(*reporters))
|
||||
|
||||
override fun onTestFailed(test: GameTestInfo) {
|
||||
for (reporter in reporters) reporter.onTestFailed(test)
|
||||
}
|
||||
|
||||
override fun onTestSuccess(test: GameTestInfo) {
|
||||
for (reporter in reporters) reporter.onTestSuccess(test)
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
for (reporter in reporters) reporter.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports tests to a JUnit XML file. This is equivalent to [JUnitLikeTestReporter], except it ensures the destination
|
||||
* directory exists.
|
||||
*/
|
||||
class JunitTestReporter constructor(destination: File) : JUnitLikeTestReporter(destination) {
|
||||
override fun save(file: File) {
|
||||
try {
|
||||
Files.createDirectories(file.toPath().parent)
|
||||
} catch (e: IOException) {
|
||||
throw TransformerException("Failed to create parent directory", e)
|
||||
}
|
||||
super.save(file)
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@
|
||||
"GameTestInfoAccessor",
|
||||
"GameTestSequenceAccessor",
|
||||
"GameTestSequenceMixin",
|
||||
"SharedConstantsMixin",
|
||||
"TestCommandAccessor"
|
||||
]
|
||||
}
|
||||
|
137
projects/common/src/testMod/resources/data/cctest/structures/computer_test.open_on_client.snbt
generated
Normal file
137
projects/common/src/testMod/resources/data/cctest/structures/computer_test.open_on_client.snbt
generated
Normal file
@ -0,0 +1,137 @@
|
||||
{
|
||||
DataVersion: 3120,
|
||||
size: [5, 5, 5],
|
||||
data: [
|
||||
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 1, 0], state: "minecraft:air"},
|
||||
{pos: [0, 1, 1], state: "minecraft:air"},
|
||||
{pos: [0, 1, 2], state: "minecraft:air"},
|
||||
{pos: [0, 1, 3], state: "minecraft:air"},
|
||||
{pos: [0, 1, 4], state: "minecraft:air"},
|
||||
{pos: [1, 1, 0], state: "minecraft:air"},
|
||||
{pos: [1, 1, 1], state: "minecraft:air"},
|
||||
{pos: [1, 1, 2], state: "minecraft:air"},
|
||||
{pos: [1, 1, 3], state: "minecraft:air"},
|
||||
{pos: [1, 1, 4], state: "minecraft:air"},
|
||||
{pos: [2, 1, 0], state: "minecraft:air"},
|
||||
{pos: [2, 1, 1], state: "minecraft:air"},
|
||||
{pos: [2, 1, 2], state: "computercraft:computer_advanced{facing:north,state:on}", nbt: {ComputerId: 1, Label: "computer_test.open_on_client", On: 1b, id: "computercraft:computer_advanced"}},
|
||||
{pos: [2, 1, 3], state: "minecraft:air"},
|
||||
{pos: [2, 1, 4], state: "minecraft:air"},
|
||||
{pos: [3, 1, 0], state: "minecraft:air"},
|
||||
{pos: [3, 1, 1], state: "minecraft:air"},
|
||||
{pos: [3, 1, 2], state: "minecraft:air"},
|
||||
{pos: [3, 1, 3], state: "minecraft:air"},
|
||||
{pos: [3, 1, 4], state: "minecraft:air"},
|
||||
{pos: [4, 1, 0], state: "minecraft:air"},
|
||||
{pos: [4, 1, 1], state: "minecraft:air"},
|
||||
{pos: [4, 1, 2], state: "minecraft:air"},
|
||||
{pos: [4, 1, 3], state: "minecraft:air"},
|
||||
{pos: [4, 1, 4], state: "minecraft:air"},
|
||||
{pos: [0, 2, 0], state: "minecraft:air"},
|
||||
{pos: [0, 2, 1], state: "minecraft:air"},
|
||||
{pos: [0, 2, 2], state: "minecraft:air"},
|
||||
{pos: [0, 2, 3], state: "minecraft:air"},
|
||||
{pos: [0, 2, 4], state: "minecraft:air"},
|
||||
{pos: [1, 2, 0], state: "minecraft:air"},
|
||||
{pos: [1, 2, 1], state: "minecraft:air"},
|
||||
{pos: [1, 2, 2], state: "minecraft:air"},
|
||||
{pos: [1, 2, 3], state: "minecraft:air"},
|
||||
{pos: [1, 2, 4], state: "minecraft:air"},
|
||||
{pos: [2, 2, 0], state: "minecraft:air"},
|
||||
{pos: [2, 2, 1], state: "minecraft:air"},
|
||||
{pos: [2, 2, 2], state: "minecraft:air"},
|
||||
{pos: [2, 2, 3], state: "minecraft:air"},
|
||||
{pos: [2, 2, 4], state: "minecraft:air"},
|
||||
{pos: [3, 2, 0], state: "minecraft:air"},
|
||||
{pos: [3, 2, 1], state: "minecraft:air"},
|
||||
{pos: [3, 2, 2], state: "minecraft:air"},
|
||||
{pos: [3, 2, 3], state: "minecraft:air"},
|
||||
{pos: [3, 2, 4], state: "minecraft:air"},
|
||||
{pos: [4, 2, 0], state: "minecraft:air"},
|
||||
{pos: [4, 2, 1], state: "minecraft:air"},
|
||||
{pos: [4, 2, 2], state: "minecraft:air"},
|
||||
{pos: [4, 2, 3], state: "minecraft:air"},
|
||||
{pos: [4, 2, 4], state: "minecraft:air"},
|
||||
{pos: [0, 3, 0], state: "minecraft:air"},
|
||||
{pos: [0, 3, 1], state: "minecraft:air"},
|
||||
{pos: [0, 3, 2], state: "minecraft:air"},
|
||||
{pos: [0, 3, 3], state: "minecraft:air"},
|
||||
{pos: [0, 3, 4], state: "minecraft:air"},
|
||||
{pos: [1, 3, 0], state: "minecraft:air"},
|
||||
{pos: [1, 3, 1], state: "minecraft:air"},
|
||||
{pos: [1, 3, 2], state: "minecraft:air"},
|
||||
{pos: [1, 3, 3], state: "minecraft:air"},
|
||||
{pos: [1, 3, 4], state: "minecraft:air"},
|
||||
{pos: [2, 3, 0], state: "minecraft:air"},
|
||||
{pos: [2, 3, 1], state: "minecraft:air"},
|
||||
{pos: [2, 3, 2], state: "minecraft:air"},
|
||||
{pos: [2, 3, 3], state: "minecraft:air"},
|
||||
{pos: [2, 3, 4], state: "minecraft:air"},
|
||||
{pos: [3, 3, 0], state: "minecraft:air"},
|
||||
{pos: [3, 3, 1], state: "minecraft:air"},
|
||||
{pos: [3, 3, 2], state: "minecraft:air"},
|
||||
{pos: [3, 3, 3], state: "minecraft:air"},
|
||||
{pos: [3, 3, 4], state: "minecraft:air"},
|
||||
{pos: [4, 3, 0], state: "minecraft:air"},
|
||||
{pos: [4, 3, 1], state: "minecraft:air"},
|
||||
{pos: [4, 3, 2], state: "minecraft:air"},
|
||||
{pos: [4, 3, 3], state: "minecraft:air"},
|
||||
{pos: [4, 3, 4], state: "minecraft:air"},
|
||||
{pos: [0, 4, 0], state: "minecraft:air"},
|
||||
{pos: [0, 4, 1], state: "minecraft:air"},
|
||||
{pos: [0, 4, 2], state: "minecraft:air"},
|
||||
{pos: [0, 4, 3], state: "minecraft:air"},
|
||||
{pos: [0, 4, 4], state: "minecraft:air"},
|
||||
{pos: [1, 4, 0], state: "minecraft:air"},
|
||||
{pos: [1, 4, 1], state: "minecraft:air"},
|
||||
{pos: [1, 4, 2], state: "minecraft:air"},
|
||||
{pos: [1, 4, 3], state: "minecraft:air"},
|
||||
{pos: [1, 4, 4], state: "minecraft:air"},
|
||||
{pos: [2, 4, 0], state: "minecraft:air"},
|
||||
{pos: [2, 4, 1], state: "minecraft:air"},
|
||||
{pos: [2, 4, 2], state: "minecraft:air"},
|
||||
{pos: [2, 4, 3], state: "minecraft:air"},
|
||||
{pos: [2, 4, 4], state: "minecraft:air"},
|
||||
{pos: [3, 4, 0], state: "minecraft:air"},
|
||||
{pos: [3, 4, 1], state: "minecraft:air"},
|
||||
{pos: [3, 4, 2], state: "minecraft:air"},
|
||||
{pos: [3, 4, 3], state: "minecraft:air"},
|
||||
{pos: [3, 4, 4], state: "minecraft:air"},
|
||||
{pos: [4, 4, 0], state: "minecraft:air"},
|
||||
{pos: [4, 4, 1], state: "minecraft:air"},
|
||||
{pos: [4, 4, 2], state: "minecraft:air"},
|
||||
{pos: [4, 4, 3], state: "minecraft:air"},
|
||||
{pos: [4, 4, 4], state: "minecraft:air"}
|
||||
],
|
||||
entities: [],
|
||||
palette: [
|
||||
"minecraft:polished_andesite",
|
||||
"minecraft:air",
|
||||
"computercraft:computer_advanced{facing:north,state:on}"
|
||||
]
|
||||
}
|
144
projects/common/src/testMod/resources/data/cctest/structures/monitor_test.render_monitor.snbt
generated
Normal file
144
projects/common/src/testMod/resources/data/cctest/structures/monitor_test.render_monitor.snbt
generated
Normal file
@ -0,0 +1,144 @@
|
||||
{
|
||||
DataVersion: 3120,
|
||||
size: [5, 5, 5],
|
||||
data: [
|
||||
{pos: [0, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [1, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [2, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [3, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 0], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 1], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 2], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 3], state: "minecraft:polished_andesite"},
|
||||
{pos: [4, 0, 4], state: "minecraft:polished_andesite"},
|
||||
{pos: [0, 1, 0], state: "minecraft:air"},
|
||||
{pos: [0, 1, 1], state: "minecraft:air"},
|
||||
{pos: [0, 1, 2], state: "minecraft:air"},
|
||||
{pos: [0, 1, 3], state: "minecraft:air"},
|
||||
{pos: [0, 1, 4], state: "minecraft:air"},
|
||||
{pos: [1, 1, 0], state: "minecraft:air"},
|
||||
{pos: [1, 1, 1], state: "minecraft:air"},
|
||||
{pos: [1, 1, 2], state: "minecraft:air"},
|
||||
{pos: [1, 1, 3], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:lu}", nbt: {Height: 2, Width: 3, XIndex: 2, YIndex: 0, id: "computercraft:monitor_advanced"}},
|
||||
{pos: [1, 1, 4], state: "minecraft:air"},
|
||||
{pos: [2, 1, 0], state: "minecraft:air"},
|
||||
{pos: [2, 1, 1], state: "minecraft:air"},
|
||||
{pos: [2, 1, 2], state: "minecraft:air"},
|
||||
{pos: [2, 1, 3], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:lru}", nbt: {Height: 2, Width: 3, XIndex: 1, YIndex: 0, id: "computercraft:monitor_advanced"}},
|
||||
{pos: [2, 1, 4], state: "minecraft:air"},
|
||||
{pos: [3, 1, 0], state: "minecraft:air"},
|
||||
{pos: [3, 1, 1], state: "minecraft:air"},
|
||||
{pos: [3, 1, 2], state: "minecraft:air"},
|
||||
{pos: [3, 1, 3], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:ru}", nbt: {Height: 2, Width: 3, XIndex: 0, YIndex: 0, id: "computercraft:monitor_advanced"}},
|
||||
{pos: [3, 1, 4], state: "minecraft:air"},
|
||||
{pos: [4, 1, 0], state: "minecraft:air"},
|
||||
{pos: [4, 1, 1], state: "minecraft:air"},
|
||||
{pos: [4, 1, 2], state: "minecraft:air"},
|
||||
{pos: [4, 1, 3], state: "minecraft:air"},
|
||||
{pos: [4, 1, 4], state: "minecraft:air"},
|
||||
{pos: [0, 2, 0], state: "minecraft:air"},
|
||||
{pos: [0, 2, 1], state: "minecraft:air"},
|
||||
{pos: [0, 2, 2], state: "minecraft:air"},
|
||||
{pos: [0, 2, 3], state: "minecraft:air"},
|
||||
{pos: [0, 2, 4], state: "minecraft:air"},
|
||||
{pos: [1, 2, 0], state: "minecraft:air"},
|
||||
{pos: [1, 2, 1], state: "minecraft:air"},
|
||||
{pos: [1, 2, 2], state: "minecraft:air"},
|
||||
{pos: [1, 2, 3], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:ld}", nbt: {Height: 2, Width: 3, XIndex: 2, YIndex: 1, id: "computercraft:monitor_advanced"}},
|
||||
{pos: [1, 2, 4], state: "minecraft:air"},
|
||||
{pos: [2, 2, 0], state: "minecraft:air"},
|
||||
{pos: [2, 2, 1], state: "minecraft:air"},
|
||||
{pos: [2, 2, 2], state: "minecraft:air"},
|
||||
{pos: [2, 2, 3], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:lrd}", nbt: {Height: 2, Width: 3, XIndex: 1, YIndex: 1, id: "computercraft:monitor_advanced"}},
|
||||
{pos: [2, 2, 4], state: "minecraft:air"},
|
||||
{pos: [3, 2, 0], state: "minecraft:air"},
|
||||
{pos: [3, 2, 1], state: "minecraft:air"},
|
||||
{pos: [3, 2, 2], state: "minecraft:air"},
|
||||
{pos: [3, 2, 3], state: "computercraft:monitor_advanced{facing:north,orientation:north,state:rd}", nbt: {Height: 2, Width: 3, XIndex: 0, YIndex: 1, id: "computercraft:monitor_advanced"}},
|
||||
{pos: [3, 2, 4], state: "minecraft:air"},
|
||||
{pos: [4, 2, 0], state: "minecraft:air"},
|
||||
{pos: [4, 2, 1], state: "minecraft:air"},
|
||||
{pos: [4, 2, 2], state: "minecraft:air"},
|
||||
{pos: [4, 2, 3], state: "minecraft:air"},
|
||||
{pos: [4, 2, 4], state: "minecraft:air"},
|
||||
{pos: [0, 3, 0], state: "minecraft:air"},
|
||||
{pos: [0, 3, 1], state: "minecraft:air"},
|
||||
{pos: [0, 3, 2], state: "minecraft:air"},
|
||||
{pos: [0, 3, 3], state: "minecraft:air"},
|
||||
{pos: [0, 3, 4], state: "minecraft:air"},
|
||||
{pos: [1, 3, 0], state: "minecraft:air"},
|
||||
{pos: [1, 3, 1], state: "minecraft:air"},
|
||||
{pos: [1, 3, 2], state: "minecraft:air"},
|
||||
{pos: [1, 3, 3], state: "minecraft:air"},
|
||||
{pos: [1, 3, 4], state: "minecraft:air"},
|
||||
{pos: [2, 3, 0], state: "minecraft:air"},
|
||||
{pos: [2, 3, 1], state: "minecraft:air"},
|
||||
{pos: [2, 3, 2], state: "minecraft:air"},
|
||||
{pos: [2, 3, 3], state: "minecraft:air"},
|
||||
{pos: [2, 3, 4], state: "minecraft:air"},
|
||||
{pos: [3, 3, 0], state: "minecraft:air"},
|
||||
{pos: [3, 3, 1], state: "minecraft:air"},
|
||||
{pos: [3, 3, 2], state: "minecraft:air"},
|
||||
{pos: [3, 3, 3], state: "minecraft:air"},
|
||||
{pos: [3, 3, 4], state: "minecraft:air"},
|
||||
{pos: [4, 3, 0], state: "minecraft:air"},
|
||||
{pos: [4, 3, 1], state: "minecraft:air"},
|
||||
{pos: [4, 3, 2], state: "minecraft:air"},
|
||||
{pos: [4, 3, 3], state: "minecraft:air"},
|
||||
{pos: [4, 3, 4], state: "minecraft:air"},
|
||||
{pos: [0, 4, 0], state: "minecraft:air"},
|
||||
{pos: [0, 4, 1], state: "minecraft:air"},
|
||||
{pos: [0, 4, 2], state: "minecraft:air"},
|
||||
{pos: [0, 4, 3], state: "minecraft:air"},
|
||||
{pos: [0, 4, 4], state: "minecraft:air"},
|
||||
{pos: [1, 4, 0], state: "minecraft:air"},
|
||||
{pos: [1, 4, 1], state: "minecraft:air"},
|
||||
{pos: [1, 4, 2], state: "minecraft:air"},
|
||||
{pos: [1, 4, 3], state: "minecraft:air"},
|
||||
{pos: [1, 4, 4], state: "minecraft:air"},
|
||||
{pos: [2, 4, 0], state: "minecraft:air"},
|
||||
{pos: [2, 4, 1], state: "minecraft:air"},
|
||||
{pos: [2, 4, 2], state: "minecraft:air"},
|
||||
{pos: [2, 4, 3], state: "minecraft:air"},
|
||||
{pos: [2, 4, 4], state: "minecraft:air"},
|
||||
{pos: [3, 4, 0], state: "minecraft:air"},
|
||||
{pos: [3, 4, 1], state: "minecraft:air"},
|
||||
{pos: [3, 4, 2], state: "minecraft:air"},
|
||||
{pos: [3, 4, 3], state: "minecraft:air"},
|
||||
{pos: [3, 4, 4], state: "minecraft:air"},
|
||||
{pos: [4, 4, 0], state: "minecraft:air"},
|
||||
{pos: [4, 4, 1], state: "minecraft:air"},
|
||||
{pos: [4, 4, 2], state: "minecraft:air"},
|
||||
{pos: [4, 4, 3], state: "minecraft:air"},
|
||||
{pos: [4, 4, 4], state: "minecraft:air"}
|
||||
],
|
||||
entities: [
|
||||
{blockPos: [2, 1, 0], pos: [2.5773471891671482d, 1.0d, 0.31606672131648317d], nbt: {AbsorptionAmount: 0.0f, Air: 300s, ArmorItems: [{}, {}, {}, {}], Attributes: [{Base: 0.699999988079071d, Name: "minecraft:generic.movement_speed"}], Brain: {memories: {}}, DeathTime: 0s, DisabledSlots: 0, FallDistance: 0.0f, FallFlying: 0b, Fire: 0s, HandItems: [{}, {}], Health: 20.0f, HurtByTimestamp: 0, HurtTime: 0s, Invisible: 1b, Invulnerable: 0b, Marker: 1b, Motion: [0.0d, 0.0d, 0.0d], NoBasePlate: 0b, OnGround: 0b, PortalCooldown: 0, Pos: [-5.422652810832852d, -58.0d, 14.316066721316483d], Pose: {}, Rotation: [0.0f, 0.0f], ShowArms: 0b, Small: 0b, UUID: [I; -1438461995, 798246633, -1208936981, 1180880781], id: "minecraft:armor_stand"}}
|
||||
],
|
||||
palette: [
|
||||
"minecraft:polished_andesite",
|
||||
"minecraft:air",
|
||||
"computercraft:monitor_advanced{facing:north,orientation:north,state:lu}",
|
||||
"computercraft:monitor_advanced{facing:north,orientation:north,state:lru}",
|
||||
"computercraft:monitor_advanced{facing:north,orientation:north,state:ru}",
|
||||
"computercraft:monitor_advanced{facing:north,orientation:north,state:ld}",
|
||||
"computercraft:monitor_advanced{facing:north,orientation:north,state:lrd}",
|
||||
"computercraft:monitor_advanced{facing:north,orientation:north,state:rd}"
|
||||
]
|
||||
}
|
@ -1,8 +1,6 @@
|
||||
import cc.tweaked.gradle.annotationProcessorEverywhere
|
||||
import cc.tweaked.gradle.clientClasses
|
||||
import cc.tweaked.gradle.commonClasses
|
||||
import cc.tweaked.gradle.mavenDependencies
|
||||
import cc.tweaked.gradle.*
|
||||
import net.fabricmc.loom.configuration.ide.RunConfigSettings
|
||||
import java.util.*
|
||||
|
||||
plugins {
|
||||
id("cc-tweaked.fabric")
|
||||
@ -19,6 +17,23 @@ cct {
|
||||
allProjects.forEach { externalSources(it) }
|
||||
}
|
||||
|
||||
fun addRemappedConfiguration(name: String) {
|
||||
val original = configurations.create(name) {
|
||||
isCanBeConsumed = false
|
||||
isCanBeResolved = true
|
||||
}
|
||||
val capitalName = name.capitalize(Locale.ROOT)
|
||||
loom.addRemapConfiguration("mod$capitalName") {
|
||||
onCompileClasspath.set(false)
|
||||
onRuntimeClasspath.set(false)
|
||||
targetConfigurationName.set(name)
|
||||
}
|
||||
original.extendsFrom(configurations["mod${capitalName}Mapped"])
|
||||
}
|
||||
|
||||
addRemappedConfiguration("testWithSodium")
|
||||
addRemappedConfiguration("testWithIris")
|
||||
|
||||
dependencies {
|
||||
modImplementation(libs.bundles.externalMods.fabric)
|
||||
modCompileOnly(libs.bundles.externalMods.fabric.compile) {
|
||||
@ -30,6 +45,10 @@ dependencies {
|
||||
exclude("net.fabricmc.fabric-api")
|
||||
}
|
||||
|
||||
"modTestWithSodium"(libs.sodium)
|
||||
"modTestWithIris"(libs.iris)
|
||||
"modTestWithIris"(libs.sodium)
|
||||
|
||||
include(libs.cobalt)
|
||||
include(libs.netty.http) // It might be better to shadowJar this, as we don't use half of it.
|
||||
include(libs.forgeConfig)
|
||||
@ -120,6 +139,7 @@ loom {
|
||||
configureForGameTest(this)
|
||||
|
||||
runDir("run/testClient")
|
||||
property("cctest.tags", "client,common")
|
||||
}
|
||||
|
||||
register("gametest") {
|
||||
@ -128,7 +148,7 @@ loom {
|
||||
configureForGameTest(this)
|
||||
|
||||
property("fabric-api.gametest")
|
||||
property("fabric-api.gametest.report-file", project.buildDir.resolve("test-results/gametest/gametest.xml").absolutePath)
|
||||
property("fabric-api.gametest.report-file", project.buildDir.resolve("test-results/runGametest.xml").absolutePath)
|
||||
runDir("run/gametest")
|
||||
}
|
||||
}
|
||||
@ -158,14 +178,57 @@ val validateMixinNames by tasks.registering(net.fabricmc.loom.task.ValidateMixin
|
||||
source(sourceSets.client.get().output)
|
||||
source(sourceSets.testMod.get().output)
|
||||
}
|
||||
tasks.check { dependsOn(validateMixinNames) }
|
||||
|
||||
tasks.test { dependsOn(tasks.generateDLIConfig) }
|
||||
|
||||
val runGametest = tasks.named<JavaExec>("runGametest")
|
||||
|
||||
val runGametest = tasks.named<JavaExec>("runGametest") {
|
||||
usesService(MinecraftRunnerService.get(gradle))
|
||||
}
|
||||
cct.jacoco(runGametest)
|
||||
tasks.check { dependsOn(runGametest) }
|
||||
|
||||
tasks.check { dependsOn(validateMixinNames, runGametest) }
|
||||
val runGametestClient by tasks.registering(ClientJavaExec::class) {
|
||||
description = "Runs client-side gametests with no mods"
|
||||
copyFrom("runTestClient")
|
||||
|
||||
tags("client")
|
||||
}
|
||||
cct.jacoco(runGametestClient)
|
||||
|
||||
val runGametestClientWithSodium by tasks.registering(ClientJavaExec::class) {
|
||||
description = "Runs client-side gametests with Sodium"
|
||||
copyFrom("runTestClient")
|
||||
|
||||
tags("sodium")
|
||||
classpath += configurations["testWithSodium"]
|
||||
}
|
||||
cct.jacoco(runGametestClientWithSodium)
|
||||
|
||||
val runGametestClientWithIris by tasks.registering(ClientJavaExec::class) {
|
||||
description = "Runs client-side gametests with Iris"
|
||||
copyFrom("runTestClient")
|
||||
|
||||
tags("iris")
|
||||
classpath += configurations["testWithIris"]
|
||||
|
||||
withFileFrom(workingDir.resolve("shaderpacks/ComplementaryShaders_v4.6.zip")) {
|
||||
cct.downloadFile("Complementary Shaders", "https://edge.forgecdn.net/files/3951/170/ComplementaryShaders_v4.6.zip")
|
||||
}
|
||||
withFileContents(workingDir.resolve("config/iris.properties")) {
|
||||
"""
|
||||
enableShaders=true
|
||||
shaderPack=ComplementaryShaders_v4.6.zip
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
cct.jacoco(runGametestClientWithIris)
|
||||
|
||||
tasks.register("checkClient") {
|
||||
group = LifecycleBasePlugin.VERIFICATION_GROUP
|
||||
description = "Runs all client-only checks."
|
||||
dependsOn(runGametestClient, runGametestClientWithSodium, runGametestClientWithIris)
|
||||
}
|
||||
|
||||
tasks.withType(GenerateModuleMetadata::class).configureEach { isEnabled = false }
|
||||
publishing {
|
||||
|
@ -10,9 +10,12 @@ import dan200.computercraft.export.Exporter;
|
||||
import net.fabricmc.api.ClientModInitializer;
|
||||
import net.fabricmc.api.ModInitializer;
|
||||
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
|
||||
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||
import net.fabricmc.fabric.api.event.Event;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
|
||||
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents;
|
||||
import net.minecraft.gametest.framework.GameTestRegistry;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
public class TestMod implements ModInitializer, ClientModInitializer {
|
||||
@ -23,12 +26,15 @@ public class TestMod implements ModInitializer, ClientModInitializer {
|
||||
var phase = new ResourceLocation(ComputerCraftAPI.MOD_ID, "test_mod");
|
||||
ServerLifecycleEvents.SERVER_STARTED.addPhaseOrdering(Event.DEFAULT_PHASE, phase);
|
||||
ServerLifecycleEvents.SERVER_STARTED.register(phase, TestHooks::onServerStarted);
|
||||
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> CCTestCommand.register(dispatcher));
|
||||
|
||||
TestHooks.loadTests(GameTestRegistry::register);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
ServerTickEvents.START_SERVER_TICK.register(ClientTestHooks::onServerTick);
|
||||
ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> ClientTestHooks.onOpenScreen(screen));
|
||||
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> Exporter.register(dispatcher));
|
||||
}
|
||||
}
|
||||
|
@ -8,17 +8,6 @@
|
||||
],
|
||||
"client": [
|
||||
"dan200.computercraft.gametest.core.TestMod"
|
||||
],
|
||||
"fabric-gametest": [
|
||||
"dan200.computercraft.gametest.Computer_Test",
|
||||
"dan200.computercraft.gametest.CraftOs_Test",
|
||||
"dan200.computercraft.gametest.Disk_Drive_Test",
|
||||
"dan200.computercraft.gametest.Loot_Test",
|
||||
"dan200.computercraft.gametest.Modem_Test",
|
||||
"dan200.computercraft.gametest.Monitor_Test",
|
||||
"dan200.computercraft.gametest.Printer_Test",
|
||||
"dan200.computercraft.gametest.Recipe_Test",
|
||||
"dan200.computercraft.gametest.Turtle_Test"
|
||||
]
|
||||
},
|
||||
"mixins": [
|
||||
|
@ -101,6 +101,8 @@ minecraft {
|
||||
workingDirectory(file("run/testClient"))
|
||||
parent(client.get())
|
||||
configureForGameTest()
|
||||
|
||||
property("cctest.tags", "client,common")
|
||||
}
|
||||
|
||||
val gameTestServer by registering {
|
||||
@ -254,18 +256,32 @@ val runGametest by tasks.registering(JavaExec::class) {
|
||||
group = LifecycleBasePlugin.VERIFICATION_GROUP
|
||||
description = "Runs tests on a temporary Minecraft instance."
|
||||
dependsOn("cleanRunGametest")
|
||||
usesService(MinecraftRunnerService.get(gradle))
|
||||
|
||||
// Copy from runGameTestServer. We do it in this slightly odd way as runGameTestServer
|
||||
// isn't created until the task is configured (which is no good for us).
|
||||
val exec = tasks.getByName<JavaExec>("runGameTestServer")
|
||||
dependsOn(exec.dependsOn)
|
||||
exec.copyToFull(this)
|
||||
|
||||
systemProperty("cctest.gametest-report", project.buildDir.resolve("test-results/$name.xml").absolutePath)
|
||||
}
|
||||
|
||||
cct.jacoco(runGametest)
|
||||
|
||||
tasks.check { dependsOn(runGametest) }
|
||||
|
||||
val runGametestClient by tasks.registering(ClientJavaExec::class) {
|
||||
description = "Runs client-side gametests with no mods"
|
||||
copyFrom("runTestClient")
|
||||
tags("client")
|
||||
}
|
||||
cct.jacoco(runGametestClient)
|
||||
|
||||
tasks.register("checkClient") {
|
||||
group = LifecycleBasePlugin.VERIFICATION_GROUP
|
||||
description = "Runs all client-only checks."
|
||||
dependsOn(runGametestClient)
|
||||
}
|
||||
|
||||
// Upload tasks
|
||||
|
||||
val publishCurseForge by tasks.registering(TaskPublishCurseForge::class) {
|
||||
|
@ -1,85 +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.gametest.core;
|
||||
|
||||
import net.minecraft.client.CloudStatus;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.ParticleStatus;
|
||||
import net.minecraft.client.gui.screens.TitleScreen;
|
||||
import net.minecraft.client.tutorial.TutorialSteps;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.client.event.ScreenEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = "cctest", value = Dist.CLIENT)
|
||||
public final class ClientHooks {
|
||||
private static final Logger LOG = LogManager.getLogger(TestHooks.class);
|
||||
|
||||
private static boolean triggered = false;
|
||||
|
||||
private ClientHooks() {
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onGuiInit(ScreenEvent.Init event) {
|
||||
if (triggered || !(event.getScreen() instanceof TitleScreen)) return;
|
||||
triggered = true;
|
||||
|
||||
ClientHooks.openWorld();
|
||||
}
|
||||
|
||||
private static void openWorld() {
|
||||
var minecraft = Minecraft.getInstance();
|
||||
|
||||
// Clear some options before we get any further.
|
||||
minecraft.options.autoJump().set(false);
|
||||
minecraft.options.cloudStatus().set(CloudStatus.OFF);
|
||||
minecraft.options.particles().set(ParticleStatus.MINIMAL);
|
||||
minecraft.options.tutorialStep = TutorialSteps.NONE;
|
||||
minecraft.options.renderDistance().set(6);
|
||||
minecraft.options.gamma().set(1.0);
|
||||
|
||||
/*
|
||||
if( minecraft.getLevelSource().levelExists( "test" ) )
|
||||
{
|
||||
LOG.info( "World exists, loading it" );
|
||||
Minecraft.getInstance().loadLevel( "test" );
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info( "World does not exist, creating it for the first time" );
|
||||
|
||||
RegistryAccess registries = RegistryAccess.builtinCopy();
|
||||
|
||||
Registry<DimensionType> dimensions = registries.registryOrThrow( Registry.DIMENSION_TYPE_REGISTRY );
|
||||
var biomes = registries.registryOrThrow( Registry.BIOME_REGISTRY );
|
||||
var structures = registries.registryOrThrow( Registry.STRUCTURE_SET_REGISTRY );
|
||||
|
||||
FlatLevelGeneratorSettings flatSettings = FlatLevelGeneratorSettings.getDefault( biomes, structures )
|
||||
.withLayers(
|
||||
Collections.singletonList( new FlatLayerInfo( 4, Blocks.WHITE_CONCRETE ) ),
|
||||
Optional.empty()
|
||||
);
|
||||
flatSettings.setBiome( biomes.getHolderOrThrow( Biomes.DESERT ) );
|
||||
|
||||
WorldGenSettings generator = new WorldGenSettings( 0, false, false, withOverworld(
|
||||
dimensions,
|
||||
DimensionType.defaultDimensions( registries, 0 ),
|
||||
new FlatLevelSource( structures, flatSettings )
|
||||
) );
|
||||
|
||||
LevelSettings settings = new LevelSettings(
|
||||
"test", GameType.CREATIVE, false, Difficulty.PEACEFUL, true,
|
||||
new GameRules(), DataPackConfig.DEFAULT
|
||||
);
|
||||
Minecraft.getInstance().createLevel( "test", settings, registries, generator );
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
@ -6,31 +6,18 @@
|
||||
package dan200.computercraft.gametest.core;
|
||||
|
||||
import dan200.computercraft.export.Exporter;
|
||||
import dan200.computercraft.gametest.api.GameTestHolder;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestRegistry;
|
||||
import net.minecraft.gametest.framework.StructureUtils;
|
||||
import net.minecraft.gametest.framework.TestFunction;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.client.event.RegisterClientCommandsEvent;
|
||||
import net.minecraftforge.client.event.ScreenEvent;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.event.RegisterCommandsEvent;
|
||||
import net.minecraftforge.event.RegisterGameTestsEvent;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
import net.minecraftforge.event.server.ServerStartedEvent;
|
||||
import net.minecraftforge.eventbus.api.EventPriority;
|
||||
import net.minecraftforge.fml.ModList;
|
||||
import net.minecraftforge.fml.DistExecutor;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||
import net.minecraftforge.forgespi.language.ModFileScanData;
|
||||
import net.minecraftforge.gametest.PrefixGameTestTemplate;
|
||||
import org.objectweb.asm.Type;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Collection;
|
||||
import java.util.Locale;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Mod("cctest")
|
||||
public class TestMod {
|
||||
@ -40,85 +27,21 @@ public class TestMod {
|
||||
var bus = MinecraftForge.EVENT_BUS;
|
||||
bus.addListener(EventPriority.LOW, (ServerStartedEvent e) -> TestHooks.onServerStarted(e.getServer()));
|
||||
bus.addListener((RegisterCommandsEvent e) -> CCTestCommand.register(e.getDispatcher()));
|
||||
bus.addListener((RegisterClientCommandsEvent e) -> Exporter.register(e.getDispatcher()));
|
||||
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> TestMod::onInitializeClient);
|
||||
|
||||
var modBus = FMLJavaModLoadingContext.get().getModEventBus();
|
||||
modBus.addListener((RegisterGameTestsEvent event) -> {
|
||||
var holder = Type.getType(GameTestHolder.class);
|
||||
ModList.get().getAllScanData().stream()
|
||||
.map(ModFileScanData::getAnnotations)
|
||||
.flatMap(Collection::stream)
|
||||
.filter(a -> holder.equals(a.annotationType()))
|
||||
.forEach(x -> registerClass(x.clazz().getClassName(), event::register));
|
||||
modBus.addListener((RegisterGameTestsEvent event) -> TestHooks.loadTests(event::register));
|
||||
}
|
||||
|
||||
private static void onInitializeClient() {
|
||||
var bus = MinecraftForge.EVENT_BUS;
|
||||
|
||||
bus.addListener((TickEvent.ServerTickEvent e) -> {
|
||||
if (e.phase == TickEvent.Phase.START) ClientTestHooks.onServerTick(e.getServer());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private static Class<?> loadClass(String name) {
|
||||
try {
|
||||
return Class.forName(name, true, TestMod.class.getClassLoader());
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerClass(String className, Consumer<Method> fallback) {
|
||||
var klass = loadClass(className);
|
||||
for (var method : klass.getDeclaredMethods()) {
|
||||
var testInfo = method.getAnnotation(GameTest.class);
|
||||
if (testInfo == null) {
|
||||
fallback.accept(method);
|
||||
continue;
|
||||
}
|
||||
|
||||
GameTestRegistry.getAllTestFunctions().add(turnMethodIntoTestFunction(method, testInfo));
|
||||
GameTestRegistry.getAllTestClassNames().add(className);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom implementation of {@link GameTestRegistry#turnMethodIntoTestFunction(Method)} which makes
|
||||
* {@link GameTest#template()} behave the same as Fabric, namely in that it points to a {@link ResourceLocation},
|
||||
* rather than a test-class-specific structure.
|
||||
* <p>
|
||||
* This effectively acts as a global version of {@link PrefixGameTestTemplate}, just one which doesn't require Forge
|
||||
* to be present.
|
||||
*
|
||||
* @param method The method to register.
|
||||
* @param testInfo The test info.
|
||||
* @return The constructed test function.
|
||||
*/
|
||||
private static TestFunction turnMethodIntoTestFunction(Method method, GameTest testInfo) {
|
||||
var className = method.getDeclaringClass().getSimpleName().toLowerCase(Locale.ROOT);
|
||||
var testName = className + "." + method.getName().toLowerCase(Locale.ROOT);
|
||||
return new TestFunction(
|
||||
testInfo.batch(),
|
||||
testName,
|
||||
testInfo.template().isEmpty() ? testName : testInfo.template(),
|
||||
StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps()), testInfo.timeoutTicks(), testInfo.setupTicks(),
|
||||
testInfo.required(), testInfo.requiredSuccesses(), testInfo.attempts(),
|
||||
turnMethodIntoConsumer(method)
|
||||
);
|
||||
}
|
||||
|
||||
private static <T> Consumer<T> turnMethodIntoConsumer(Method method) {
|
||||
return value -> {
|
||||
try {
|
||||
Object instance = null;
|
||||
if (!Modifier.isStatic(method.getModifiers())) {
|
||||
instance = method.getDeclaringClass().getConstructor().newInstance();
|
||||
}
|
||||
|
||||
method.invoke(instance, value);
|
||||
} catch (InvocationTargetException e) {
|
||||
if (e.getCause() instanceof RuntimeException) {
|
||||
throw (RuntimeException) e.getCause();
|
||||
} else {
|
||||
throw new RuntimeException(e.getCause());
|
||||
}
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
bus.addListener((ScreenEvent.Opening e) -> {
|
||||
if (ClientTestHooks.onOpenScreen(e.getScreen())) e.setCanceled(true);
|
||||
});
|
||||
bus.addListener((RegisterClientCommandsEvent e) -> Exporter.register(e.getDispatcher()));
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,8 @@ PROJECT_LOCATIONS = [
|
||||
"projects/core",
|
||||
"projects/common-api",
|
||||
"projects/common",
|
||||
"projects/fabric-api",
|
||||
"projects/fabric",
|
||||
"projects/forge-api",
|
||||
"projects/forge",
|
||||
]
|
||||
@ -73,18 +75,16 @@ def find_location(message: str) -> Optional[Tuple[str, str]]:
|
||||
|
||||
|
||||
def _parse_junit_file(path: pathlib.Path):
|
||||
for testcase in ET.parse(path).getroot():
|
||||
if testcase.tag != "testcase":
|
||||
continue
|
||||
|
||||
for testcase in ET.parse(path).findall(".//testcase"):
|
||||
for result in testcase:
|
||||
if result.tag != "failure":
|
||||
if result.tag == "skipped":
|
||||
continue
|
||||
|
||||
name = f'{testcase.attrib["classname"]}.{testcase.attrib["name"]}'
|
||||
message = result.attrib.get("message")
|
||||
full_message = result.text or message
|
||||
|
||||
location = find_location(result.text)
|
||||
location = find_location(full_message)
|
||||
error = ERROR_MESSAGE.match(message)
|
||||
if error:
|
||||
error = error[1]
|
||||
@ -97,7 +97,7 @@ def _parse_junit_file(path: pathlib.Path):
|
||||
print(f"::error::{name} failed")
|
||||
|
||||
print("::group::Full error message")
|
||||
print(result.text)
|
||||
print(full_message)
|
||||
print("::endgroup")
|
||||
|
||||
|
||||
@ -112,6 +112,9 @@ def parse_junit() -> None:
|
||||
for path in pathlib.Path(os.path.join(project, "build/test-results/test")).glob("TEST-*.xml"):
|
||||
_parse_junit_file(path)
|
||||
|
||||
for path in pathlib.Path(os.path.join(project, "build/test-results")).glob("run*.xml"):
|
||||
_parse_junit_file(path)
|
||||
|
||||
print("::remove-matcher owner=junit::")
|
||||
|
||||
|
||||
|
113
tools/screenshots.py
Executable file
113
tools/screenshots.py
Executable file
@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Combines screenshots from the Forge and Fabric tests into a single HTML page.
|
||||
"""
|
||||
import os
|
||||
import os.path
|
||||
from dataclasses import dataclass
|
||||
from typing import TextIO
|
||||
from textwrap import dedent
|
||||
import webbrowser
|
||||
import argparse
|
||||
|
||||
PROJECT_LOCATIONS = [
|
||||
"projects/fabric",
|
||||
"projects/forge",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Image:
|
||||
name: list[str]
|
||||
path: str
|
||||
|
||||
|
||||
def write_images(io: TextIO, images: list[Image]):
|
||||
io.write(
|
||||
dedent(
|
||||
"""\
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>CC:T test screenshots</title>
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 1em;
|
||||
gap: 1em;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit,minmax(25em,1fr));
|
||||
}
|
||||
|
||||
.image {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.desc { text-align: center; }
|
||||
.desc-prefix { color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
for image in images:
|
||||
io.write(
|
||||
dedent(
|
||||
f"""\
|
||||
<div class="image">
|
||||
<img src="../{image.path}" />
|
||||
<span class="desc">
|
||||
<span class="desc-prefix">{" » ".join(image.name[:-2])} »</span>
|
||||
<span class="desc-main">{image.name[-1]}</span>
|
||||
</span>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
io.write("</div></body></html>")
|
||||
|
||||
|
||||
def main():
|
||||
spec = argparse.ArgumentParser(
|
||||
description="Combines screenshots from the Forge and Fabric tests into a single HTML page."
|
||||
)
|
||||
spec.add_argument(
|
||||
"--open",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Open the output file in a web browser.",
|
||||
)
|
||||
args = spec.parse_args()
|
||||
|
||||
images: list[Image] = []
|
||||
for project, dir in {
|
||||
"Forge": "projects/forge",
|
||||
"Fabric": "projects/fabric",
|
||||
}.items():
|
||||
dir = os.path.join(dir, "build", "testScreenshots")
|
||||
for file in sorted(os.listdir(dir)):
|
||||
name = [project, *os.path.splitext(file)[0].split(".")]
|
||||
images.append(Image(name, os.path.join(dir, file)))
|
||||
|
||||
out_file = "build/screenshots.html"
|
||||
with open(out_file, encoding="utf-8", mode="w") as out:
|
||||
write_images(out, images)
|
||||
|
||||
if args.open:
|
||||
webbrowser.open(out_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in New Issue
Block a user