mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-05-07 18:04:15 +00:00
Wait for computers to run each tick in gametests
This commit is contained in:
parent
d6749f8461
commit
6739c4c6c0
@ -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
|
||||||
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"GameTestInfoAccessor",
|
"GameTestInfoAccessor",
|
||||||
"GameTestSequenceAccessor",
|
"GameTestSequenceAccessor",
|
||||||
"GameTestSequenceMixin",
|
"GameTestSequenceMixin",
|
||||||
|
"GameTestServerMixin",
|
||||||
"TestCommandAccessor"
|
"TestCommandAccessor"
|
||||||
],
|
],
|
||||||
"client": [
|
"client": [
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user