1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-04-15 07:13:13 +00:00

Wait for computers to run each tick in gametests

This commit is contained in:
Jonathan Coates 2025-01-17 18:43:19 +00:00
parent d6749f8461
commit 6739c4c6c0
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
6 changed files with 102 additions and 17 deletions

View File

@ -10,9 +10,7 @@ import org.gradle.api.GradleException
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.SourceSet
@ -168,7 +166,7 @@ abstract class CCTweakedExtension(private val project: Project) {
jacoco.applyTo(this)
extensions.configure(JacocoTaskExtension::class.java) {
includes = listOf("dan200.computercraft.*")
includes = listOf("dan200.computercraft.*")
excludes = listOf(
"dan200.computercraft.mixin.*", // Exclude mixins, as they're not executed at runtime.
"dan200.computercraft.shared.Capabilities$*", // Exclude capability tokens, as Forge rewrites them.

View File

@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2025 The CC: Tweaked Developers
//
// SPDX-License-Identifier: MPL-2.0
package dan200.computercraft.mixin.gametest;
import com.mojang.datafixers.DataFixer;
import dan200.computercraft.gametest.core.TestHooks;
import net.minecraft.gametest.framework.GameTestServer;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.Services;
import net.minecraft.server.WorldStem;
import net.minecraft.server.level.progress.ChunkProgressListenerFactory;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.net.Proxy;
import java.util.concurrent.locks.LockSupport;
@Mixin(GameTestServer.class)
abstract class GameTestServerMixin extends MinecraftServer {
GameTestServerMixin(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer fixerUpper, Services services, ChunkProgressListenerFactory progressListenerFactory) {
super(serverThread, storageSource, packRepository, worldStem, proxy, fixerUpper, services, progressListenerFactory);
}
/**
* Overwrite {@link GameTestServer#waitUntilNextTick()} to wait for all computers to finish executing.
* <p>
* This is a little dangerous (breaks async behaviour of computers), but it forces tests to be deterministic.
*
* @reason See above. This is only in the test mod, so no risk of collision.
* @author SquidDev.
*/
@Overwrite
@Override
public void waitUntilNextTick() {
while (true) {
runAllTasks();
if (!haveTestsStarted() || TestHooks.areComputersIdle(this)) break;
LockSupport.parkNanos(100_000);
}
}
@Shadow
private boolean haveTestsStarted() {
throw new AssertionError("Stub.");
}
}

View File

@ -5,6 +5,8 @@
package dan200.computercraft.gametest.core
import dan200.computercraft.api.ComputerCraftAPI
import dan200.computercraft.core.ComputerContext
import dan200.computercraft.core.computer.computerthread.ComputerThread
import dan200.computercraft.gametest.*
import dan200.computercraft.gametest.api.ClientGameTest
import dan200.computercraft.gametest.api.TestTags
@ -23,6 +25,8 @@ import net.minecraft.world.phys.Vec3
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Modifier
@ -80,6 +84,9 @@ object TestHooks {
CCTestCommand.importFiles(server)
}
@JvmStatic
fun areComputersIdle(server: MinecraftServer) = ComputerThreadReflection.isFullyIdle(ServerContext.get(server))
private val testClasses = listOf(
Computer_Test::class.java,
CraftOs_Test::class.java,
@ -116,14 +123,6 @@ object TestHooks {
}
}
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()
@ -135,7 +134,7 @@ object TestHooks {
TestFunction(
testInfo.batch, testName, testInfo.template.ifEmpty { testName },
StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps),
adjustTimeout(testInfo.timeoutTicks),
testInfo.timeoutTicks,
testInfo.setupTicks,
testInfo.required, testInfo.requiredSuccesses, testInfo.attempts,
) { value -> safeInvoke(method, value) },
@ -149,10 +148,8 @@ object TestHooks {
GameTestRegistry.getAllTestFunctions().add(
TestFunction(
testName,
testName,
testInfo.template.ifEmpty { testName },
adjustTimeout(testInfo.timeoutTicks),
testName, testName, testInfo.template.ifEmpty { testName },
testInfo.timeoutTicks,
0,
true,
) { value -> safeInvoke(method, value) },
@ -200,3 +197,31 @@ object TestHooks {
return false
}
}
/**
* Nasty reflection to determine if computers are fully idle.
*
* This is horribly nasty, and should not be used as a model for any production code!
*
* @see [ComputerThread.isFullyIdle]
* @see [dan200.computercraft.mixin.gametest.GameTestServerMixin]
*/
private object ComputerThreadReflection {
private val lookup = MethodHandles.lookup()
@JvmField
val computerContext: MethodHandle = lookup.unreflectGetter(
ServerContext::class.java.getDeclaredField("context").also { it.isAccessible = true },
)
@JvmField
val isFullyIdle: MethodHandle = lookup.unreflect(
ComputerThread::class.java.getDeclaredMethod("isFullyIdle").also { it.isAccessible = true },
)
fun isFullyIdle(context: ServerContext): Boolean {
val computerContext = computerContext.invokeExact(context) as ComputerContext
val computerThread = computerContext.computerScheduler() as ComputerThread
return isFullyIdle.invokeExact(computerThread) as Boolean
}
}

View File

@ -35,7 +35,7 @@ class MultiTestReporter(private val reporters: List<TestReporter>) : TestReporte
* 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) {
class JunitTestReporter(destination: File) : JUnitLikeTestReporter(destination) {
override fun save(file: File) {
try {
Files.createDirectories(file.toPath().parent)

View File

@ -11,6 +11,7 @@
"GameTestInfoAccessor",
"GameTestSequenceAccessor",
"GameTestSequenceMixin",
"GameTestServerMixin",
"TestCommandAccessor"
],
"client": [

View File

@ -433,6 +433,16 @@ public final class ComputerThread implements ComputerScheduler {
return computerQueueSize() > idleWorkers.get();
}
/**
* Determine if no work is queued, and all workers are idle.
*
* @return If the threads are fully idle.
*/
@VisibleForTesting
boolean isFullyIdle() {
return computerQueueSize() == 0 && idleWorkers.get() >= workerCount();
}
private void workerFinished(WorkerThread worker) {
// We should only shut down a worker once! This should only happen if we fail to abort a worker and then the
// worker finishes normally.