1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-05-07 01:44:14 +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.NamedDomainObjectProvider
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.Task import org.gradle.api.Task
import org.gradle.api.artifacts.Dependency
import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Provider import org.gradle.api.provider.Provider
import org.gradle.api.provider.SetProperty import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.SourceSet

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 package dan200.computercraft.gametest.core
import dan200.computercraft.api.ComputerCraftAPI 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.*
import dan200.computercraft.gametest.api.ClientGameTest import dan200.computercraft.gametest.api.ClientGameTest
import dan200.computercraft.gametest.api.TestTags import dan200.computercraft.gametest.api.TestTags
@ -23,6 +25,8 @@ import net.minecraft.world.phys.Vec3
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.lang.invoke.MethodHandle
import java.lang.invoke.MethodHandles
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method import java.lang.reflect.Method
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
@ -80,6 +84,9 @@ object TestHooks {
CCTestCommand.importFiles(server) CCTestCommand.importFiles(server)
} }
@JvmStatic
fun areComputersIdle(server: MinecraftServer) = ComputerThreadReflection.isFullyIdle(ServerContext.get(server))
private val testClasses = listOf( private val testClasses = listOf(
Computer_Test::class.java, Computer_Test::class.java,
CraftOs_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>) { private fun registerTest(testClass: Class<*>, method: Method, fallbackRegister: Consumer<Method>) {
val className = testClass.simpleName.lowercase() val className = testClass.simpleName.lowercase()
val testName = className + "." + method.name.lowercase() val testName = className + "." + method.name.lowercase()
@ -135,7 +134,7 @@ object TestHooks {
TestFunction( TestFunction(
testInfo.batch, testName, testInfo.template.ifEmpty { testName }, testInfo.batch, testName, testInfo.template.ifEmpty { testName },
StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps), StructureUtils.getRotationForRotationSteps(testInfo.rotationSteps),
adjustTimeout(testInfo.timeoutTicks), testInfo.timeoutTicks,
testInfo.setupTicks, testInfo.setupTicks,
testInfo.required, testInfo.requiredSuccesses, testInfo.attempts, testInfo.required, testInfo.requiredSuccesses, testInfo.attempts,
) { value -> safeInvoke(method, value) }, ) { value -> safeInvoke(method, value) },
@ -149,10 +148,8 @@ object TestHooks {
GameTestRegistry.getAllTestFunctions().add( GameTestRegistry.getAllTestFunctions().add(
TestFunction( TestFunction(
testName, testName, testName, testInfo.template.ifEmpty { testName },
testName, testInfo.timeoutTicks,
testInfo.template.ifEmpty { testName },
adjustTimeout(testInfo.timeoutTicks),
0, 0,
true, true,
) { value -> safeInvoke(method, value) }, ) { value -> safeInvoke(method, value) },
@ -200,3 +197,31 @@ object TestHooks {
return false 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 * Reports tests to a JUnit XML file. This is equivalent to [JUnitLikeTestReporter], except it ensures the destination
* directory exists. * directory exists.
*/ */
class JunitTestReporter constructor(destination: File) : JUnitLikeTestReporter(destination) { class JunitTestReporter(destination: File) : JUnitLikeTestReporter(destination) {
override fun save(file: File) { override fun save(file: File) {
try { try {
Files.createDirectories(file.toPath().parent) Files.createDirectories(file.toPath().parent)

View File

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

View File

@ -433,6 +433,16 @@ public final class ComputerThread implements ComputerScheduler {
return computerQueueSize() > idleWorkers.get(); 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) { 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 // 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. // worker finishes normally.