1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-01-15 03:35:42 +00:00

Refactor the test code to be closer to 1.17

Basically mimic the actual API that Minecraft would expose if the
methods hadn't been stripped. Lots of ATs and unsafe hacks to get this
working, but thankfully the API we can expose to the user is pretty
nice. Yay for Kotlin?

Anyway, will cause some immediate pain (yay merge conflicts) but should
make keeping the two in sync much easier.
This commit is contained in:
Jonathan Coates 2021-08-20 12:28:43 +01:00
parent 4dea3dff36
commit 4b33306940
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
19 changed files with 344 additions and 380 deletions

View File

@ -4,24 +4,28 @@ import dan200.computercraft.ingame.api.*
import net.minecraft.block.LeverBlock
import net.minecraft.block.RedstoneLampBlock
import net.minecraft.util.math.BlockPos
import org.junit.jupiter.api.Assertions.assertFalse
class ComputerTest {
class Computer_Test {
/**
* Ensures redstone signals do not travel through computers.
*
* @see [#548](https://github.com/SquidDev-CC/CC-Tweaked/issues/548)
*/
@GameTest
suspend fun `No through signal`(context: TestContext) {
val lamp = BlockPos(2, 0, 4)
val lever = BlockPos(2, 0, 0)
assertFalse(context.getBlock(lamp).getValue(RedstoneLampBlock.LIT), "Lamp should not be lit")
fun No_through_signal(context: GameTestHelper) = context.sequence {
val lamp = BlockPos(2, 2, 4)
val lever = BlockPos(2, 2, 0)
this
.thenExecute {
context.assertBlockState(lamp, { !it.getValue(RedstoneLampBlock.LIT) }, { "Lamp should not be lit" })
context.modifyBlock(lever) { x -> x.setValue(LeverBlock.POWERED, true) }
context.sleep(3)
assertFalse(context.getBlock(lamp).getValue(RedstoneLampBlock.LIT), "Lamp should still not be lit")
}
.thenIdle(3)
.thenExecute {
context.assertBlockState(
lamp,
{ !it.getValue(RedstoneLampBlock.LIT) },
{ "Lamp should still not be lit" })
}
}
}

View File

@ -1,13 +1,14 @@
package dan200.computercraft.ingame
import dan200.computercraft.ingame.api.GameTest
import dan200.computercraft.ingame.api.TestContext
import dan200.computercraft.ingame.api.checkComputerOk
import dan200.computercraft.ingame.api.GameTestHelper
import dan200.computercraft.ingame.api.sequence
import dan200.computercraft.ingame.api.thenComputerOk
class CraftOsTest {
class CraftOs_Test {
/**
* Sends a rednet message to another a computer and back again.
*/
@GameTest
suspend fun `Sends basic rednet messages`(context: TestContext) = context.checkComputerOk(13)
fun Sends_basic_rednet_messages(context: GameTestHelper) = context.sequence { thenComputerOk(13) }
}

View File

@ -1,27 +1,23 @@
package dan200.computercraft.ingame
import dan200.computercraft.ingame.api.*
import net.minecraft.entity.item.ItemEntity
import net.minecraft.item.Items
import net.minecraft.util.math.BlockPos
import org.junit.jupiter.api.Assertions.assertEquals
class DiskDriveTest {
class Disk_Drive_Test {
/**
* Ensure audio disks exist and we can play them.
*
* @see [#688](https://github.com/SquidDev-CC/CC-Tweaked/issues/688)
*/
@GameTest
suspend fun `Audio disk`(context: TestContext) = context.checkComputerOk(3)
fun Audio_disk(helper: GameTestHelper) = helper.sequence { thenComputerOk(3) }
@GameTest
suspend fun `Ejects disk`(context: TestContext) {
val stackAt = BlockPos(2, 0, 2)
context.checkComputerOk(4)
context.waitUntil { context.getEntity(stackAt) != null }
val stack = context.getEntityOfType<ItemEntity>(stackAt)!!
assertEquals(Items.MUSIC_DISC_13, stack.item.item, "Correct item stack")
fun Ejects_disk(helper: GameTestHelper) = helper.sequence {
val stackAt = BlockPos(2, 2, 2)
this
.thenComputerOk(4)
.thenWaitUntil { helper.assertItemEntityPresent(Items.MUSIC_DISC_13, stackAt, 0.0) }
}
}

View File

@ -5,20 +5,21 @@ import dan200.computercraft.shared.Registry
import dan200.computercraft.shared.peripheral.modem.wired.BlockCable
import net.minecraft.util.math.BlockPos
class ModemTest {
class Modem_Test {
@GameTest
suspend fun `Have peripherals`(context: TestContext) = context.checkComputerOk(15)
fun Have_peripherals(helper: GameTestHelper) = helper.sequence { thenComputerOk(15) }
@GameTest
suspend fun `Gains peripherals`(context: TestContext) {
val position = BlockPos(2, 0, 2)
context.checkComputerOk(16, "initial")
context.setBlock(position, BlockCable.correctConnections(
context.level, context.offset(position),
fun Gains_peripherals(helper: GameTestHelper) = helper.sequence {
val position = BlockPos(2, 2, 2)
this
.thenComputerOk(16, "initial")
.thenExecute {
helper.setBlock(position, BlockCable.correctConnections(
helper.level, helper.absolutePos(position),
Registry.ModBlocks.CABLE.get().defaultBlockState().setValue(BlockCable.CABLE, true)
))
context.checkComputerOk(16)
}
.thenComputerOk(16)
}
}

View File

@ -7,14 +7,12 @@ import net.minecraft.block.Blocks
import net.minecraft.command.arguments.BlockStateInput
import net.minecraft.nbt.CompoundNBT
import net.minecraft.util.math.BlockPos
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.fail
import java.util.*
class MonitorTest {
class Monitor_Test {
@GameTest
suspend fun `Ensures valid on place`(context: TestContext) {
val pos = BlockPos(2, 0, 2)
fun Ensures_valid_on_place(context: GameTestHelper) = context.sequence {
val pos = BlockPos(2, 2, 2)
val tag = CompoundNBT()
tag.putInt("Width", 2)
tag.putInt("Height", 2)
@ -28,12 +26,18 @@ class MonitorTest {
context.setBlock(pos, Blocks.AIR.defaultBlockState())
context.setBlock(pos, toSet)
context.sleep(2)
this
.thenIdle(2)
.thenExecute {
val tile = context.getBlockEntity(pos)
if (tile !is TileMonitor) {
context.fail("Expected tile to be monitor, is $tile", pos)
return@thenExecute
}
val tile = context.getTile(pos)
if (tile !is TileMonitor) fail("Expected tile to be monitor, is $tile")
assertEquals(1, tile.width, "Width should be 1")
assertEquals(1, tile.height, "Width should be 1")
if (tile.width != 1 || tile.height != 1) {
context.fail("Tile has width and height of ${tile.width}x${tile.height}, but should be 1x1", pos)
}
}
}
}

View File

@ -1,71 +1,78 @@
package dan200.computercraft.ingame
import dan200.computercraft.ingame.api.GameTest
import dan200.computercraft.ingame.api.TestContext
import dan200.computercraft.ingame.api.checkComputerOk
import dan200.computercraft.ingame.api.GameTestHelper
import dan200.computercraft.ingame.api.sequence
import dan200.computercraft.ingame.api.thenComputerOk
class TurtleTest {
@GameTest
suspend fun `Unequip refreshes peripheral`(context: TestContext) = context.checkComputerOk(1)
class Turtle_Test {
@GameTest(timeoutTicks = TIMEOUT)
fun Unequip_refreshes_peripheral(helper: GameTestHelper) = helper.sequence { thenComputerOk(1) }
/**
* Checks turtles can sheer sheep (and drop items)
*
* @see [#537](https://github.com/SquidDev-CC/CC-Tweaked/issues/537)
*/
@GameTest
suspend fun `Shears sheep`(context: TestContext) = context.checkComputerOk(5)
@GameTest(timeoutTicks = TIMEOUT)
fun Shears_sheep(helper: GameTestHelper) = helper.sequence { thenComputerOk(5) }
/**
* Checks turtles can place lava.
*
* @see [#518](https://github.com/SquidDev-CC/CC-Tweaked/issues/518)
*/
@GameTest
suspend fun `Place lava`(context: TestContext) = context.checkComputerOk(5)
@GameTest(timeoutTicks = TIMEOUT)
fun Place_lava(helper: GameTestHelper) = helper.sequence { thenComputerOk(5) }
/**
* Checks turtles can place when waterlogged.
*
* @see [#385](https://github.com/SquidDev-CC/CC-Tweaked/issues/385)
*/
@GameTest
suspend fun `Place waterlogged`(context: TestContext) = context.checkComputerOk(7)
@GameTest(timeoutTicks = TIMEOUT)
fun Place_waterlogged(helper: GameTestHelper) = helper.sequence { thenComputerOk(7) }
/**
* Checks turtles can pick up lava
*
* @see [#297](https://github.com/SquidDev-CC/CC-Tweaked/issues/297)
*/
@GameTest
suspend fun `Gather lava`(context: TestContext) = context.checkComputerOk(8)
@GameTest(timeoutTicks = TIMEOUT)
fun Gather_lava(helper: GameTestHelper) = helper.sequence { thenComputerOk(8) }
/**
* Checks turtles can hoe dirt.
*
* @see [#258](https://github.com/SquidDev-CC/CC-Tweaked/issues/258)
*/
@GameTest
suspend fun `Hoe dirt`(context: TestContext) = context.checkComputerOk(9)
@GameTest(timeoutTicks = TIMEOUT)
fun Hoe_dirt(helper: GameTestHelper) = helper.sequence { thenComputerOk(9) }
/**
* Checks turtles can place monitors
*
* @see [#691](https://github.com/SquidDev-CC/CC-Tweaked/issues/691)
*/
@GameTest
suspend fun `Place monitor`(context: TestContext) = context.checkComputerOk(10)
@GameTest(timeoutTicks = TIMEOUT)
fun Place_monitor(helper: GameTestHelper) = helper.sequence { thenComputerOk(10) }
/**
* Checks turtles can place into compostors. These are non-typical inventories, so
* worth testing.
*/
@GameTest
suspend fun `Use compostors`(context: TestContext) = context.checkComputerOk(11)
@GameTest(timeoutTicks = TIMEOUT)
fun Use_compostors(helper: GameTestHelper) = helper.sequence { thenComputerOk(11) }
/**
* Checks turtles can be cleaned in cauldrons.
*
* Currently not required as turtles can no longer right-click cauldrons.
*/
@GameTest
suspend fun `Cleaned with cauldrons`(context: TestContext) = context.checkComputerOk(12)
fun Cleaned_with_cauldrons(helper: GameTestHelper) = helper.sequence { thenComputerOk(12) }
companion object {
const val TIMEOUT = 200
}
}

View File

@ -6,7 +6,7 @@
package dan200.computercraft.ingame.api;
import dan200.computercraft.ingame.mod.TestAPI;
import kotlin.coroutines.Continuation;
import net.minecraft.test.TestList;
import javax.annotation.Nonnull;
import java.util.HashSet;
@ -18,7 +18,7 @@ import java.util.concurrent.ConcurrentHashMap;
* Assertion state of a computer.
*
* @see TestAPI For the Lua interface for this.
* @see TestExtensionsKt#checkComputerOk(TestContext, int, String, Continuation)
* @see TestExtensionsKt#thenComputerOk(TestList, int, String)
*/
public class ComputerState
{

View File

@ -12,9 +12,7 @@ import java.lang.annotation.Target;
/**
* A test which manipulates the game. This should applied on an instance function, and should accept a single
* {@link TestContext} argument.
*
* Tests may/should be written as Kotlin coroutines.
* {@link GameTestHelper} argument.
*/
@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.METHOD )
@ -25,7 +23,7 @@ public @interface GameTest
*
* @return The time the test can run in ticks.
*/
int timeout() default 200;
int timeoutTicks() default 200;
/**
* Number of ticks to delay between building the structure and running the test code.

View File

@ -0,0 +1,148 @@
package dan200.computercraft.ingame.api
import dan200.computercraft.ingame.mod.TestMod
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.entity.EntityType
import net.minecraft.item.Item
import net.minecraft.test.*
import net.minecraft.tileentity.StructureBlockTileEntity
import net.minecraft.tileentity.TileEntity
import net.minecraft.util.math.AxisAlignedBB
import net.minecraft.util.math.BlockPos
import net.minecraft.world.server.ServerWorld
import net.minecraftforge.fml.unsafe.UnsafeHacks
import java.lang.reflect.Field
/**
* Backport of 1.17's GameTestHelper class.
*
* Much of the underlying functionality is present in MC, it's just the methods to use it have been stripped out!
*/
class GameTestHelper(holder: TestTrackerHolder) {
val tracker: TestTracker = holder.testInfo
val level: ServerWorld = tracker.level
fun startSequence(): GameTestSequence {
val sequence = UnsafeHacks.newInstance(GameTestSequence::class.java) as GameTestSequence
sequence.parent = tracker
sequence.lastTick = tracker.tickCount
sequence.events = mutableListOf()
tracker.sequences.add(sequence)
return sequence
}
fun absolutePos(pos: BlockPos): BlockPos = tracker.structureBlockPos.offset(pos)
fun getBlockState(pos: BlockPos): BlockState = tracker.level.getBlockState(absolutePos(pos))
fun setBlock(pos: BlockPos, block: BlockState) = tracker.level.setBlock(absolutePos(pos), block, 3)
fun getBlockEntity(pos: BlockPos): TileEntity? = tracker.level.getBlockEntity(absolutePos(pos))
fun assertBlockState(pos: BlockPos, predicate: (BlockState) -> Boolean, message: () -> String) {
val block = getBlockState(pos)
if (!predicate(block)) throw GameTestAssertException("Invalid block $block at $pos: ${message()}")
}
fun assertItemEntityPresent(item: Item, pos: BlockPos, range: Double) {
val absPos = absolutePos(pos)
for (entity in level.getEntities(EntityType.ITEM, AxisAlignedBB(absPos).inflate(range)) { it.isAlive }) {
if (entity.item.item == item) return
}
throw GameTestAssertException("Expected $item at $pos")
}
fun fail(e: Throwable) {
tracker.fail(e)
}
fun fail(message: String) {
tracker.fail(GameTestAssertException(message))
}
fun fail(message: String, pos: BlockPos) {
tracker.fail(GameTestAssertException("$message at $pos"))
}
val bounds: AxisAlignedBB
get() {
val structure = tracker.level.getBlockEntity(tracker.structureBlockPos)
if (structure !is StructureBlockTileEntity) throw IllegalStateException("Cannot find structure block")
return StructureHelper.getStructureBounds(structure)
}
}
typealias GameTestSequence = TestList
typealias GameTestAssertException = RuntimeException
private fun GameTestSequence.addResult(task: () -> Unit, delay: Long? = null): GameTestSequence {
val result = UnsafeHacks.newInstance(TestTickResult::class.java) as TestTickResult
result.assertion = Runnable(task)
result.expectedDelay = delay
events.add(result)
return this
}
fun GameTestSequence.thenSucceed() {
addResult({
if (parent.error != null) return@addResult
parent.finish()
parent.markAsComplete()
})
}
fun GameTestSequence.thenWaitUntil(task: () -> Unit) = addResult(task)
fun GameTestSequence.thenExecute(task: () -> Unit) = addResult({
try {
task()
} catch (e: Exception) {
parent.fail(e)
}
})
fun GameTestSequence.thenIdle(delay: Long) = addResult({
if (parent.tickCount < lastTick + delay) {
throw GameTestAssertException("Waiting")
}
})
/**
* Proguard strips out all the "on success" code as it's never called anywhere. This is workable most of the time, but
* means we can't support multiple test batches as the [TestExecutor] never thinks the first batch has finished.
*
* This function does two nasty things:
* - Update the beacon when the test passes.
* - Find the current test executor by searching through our listener list and call its [TestExecutor.testCompleted]
* method.
*/
private fun TestTracker.markAsComplete() {
try {
TestUtils.spawnBeacon(this, Blocks.LIME_STAINED_GLASS)
val listeners: Collection<ITestCallback> = TestTracker::class.java.getDeclaredField("listeners").unsafeGet(this)
for (listener in listeners) {
if (listener.javaClass.name != "net.minecraft.test.TestExecutor$1") continue
for (field in listener.javaClass.declaredFields) {
if (field.type == TestExecutor::class.java) {
field.unsafeGet<TestExecutor>(listener).testCompleted(this)
}
}
break
}
} catch (e: Exception) {
TestMod.log.error("Failed to mark as complete", e)
}
}
@Suppress("UNCHECKED_CAST")
private fun <T> Field.unsafeGet(owner: Any): T {
isAccessible = true
return get(owner) as T
}

View File

@ -1,63 +0,0 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.ingame.api;
import net.minecraft.block.Block;
import net.minecraft.block.Blocks;
import net.minecraft.test.TestTracker;
import net.minecraft.test.TestTrackerHolder;
import net.minecraft.test.TestUtils;
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
import java.lang.reflect.Method;
/**
* The context a test is run within.
*
* @see TestExtensionsKt For additional test helper methods.
*/
public final class TestContext
{
private final TestTracker tracker;
public TestContext( TestTrackerHolder holder )
{
this.tracker = ObfuscationReflectionHelper.getPrivateValue( TestTrackerHolder.class, holder, "field_229487_a_" );
}
public TestTracker getTracker()
{
return tracker;
}
public void ok()
{
try
{
Method finish = TestTracker.class.getDeclaredMethod( "finish" );
finish.setAccessible( true );
finish.invoke( tracker );
Method spawn = TestUtils.class.getDeclaredMethod( "spawnBeacon", TestTracker.class, Block.class );
spawn.setAccessible( true );
spawn.invoke( null, tracker, Blocks.LIME_STAINED_GLASS );
}
catch( ReflectiveOperationException e )
{
throw new RuntimeException( e );
}
}
public void fail( Throwable e )
{
if( !tracker.isDone() ) tracker.fail( e );
}
public boolean isDone()
{
return tracker.isDone();
}
}

View File

@ -1,95 +1,45 @@
package dan200.computercraft.ingame.api
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import dan200.computercraft.ComputerCraft
import net.minecraft.block.BlockState
import net.minecraft.command.arguments.BlockStateInput
import net.minecraft.entity.Entity
import net.minecraft.tileentity.TileEntity
import net.minecraft.util.math.AxisAlignedBB
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
/**
* Wait until a predicate matches (or the test times out).
*/
suspend inline fun TestContext.waitUntil(fn: () -> Boolean) {
while (true) {
if (isDone) throw CancellationException()
if (fn()) return
delay(50)
}
}
/**
* Wait until a computer has finished running and check it is OK.
*/
suspend fun TestContext.checkComputerOk(id: Int, marker: String = ComputerState.DONE) {
waitUntil {
fun GameTestSequence.thenComputerOk(id: Int, marker: String = ComputerState.DONE): GameTestSequence =
thenWaitUntil {
val computer = ComputerState.get(id)
computer != null && computer.isDone(marker)
}
if (computer == null || !computer.isDone(marker)) throw GameTestAssertException("Computer #${id} has not finished yet.")
}.thenExecute {
ComputerState.get(id).check(marker)
}
/**
* Sleep for a given number of ticks.
*/
suspend fun TestContext.sleep(ticks: Int = 1) {
val target = tracker.level.gameTime + ticks
waitUntil { tracker.level.gameTime >= target }
}
val TestContext.level: World
get() = tracker.level
fun TestContext.offset(pos: BlockPos): BlockPos = tracker.structureBlockPos.offset(pos.x, pos.y + 2, pos.z)
/**
* Get a block within the test structure.
*/
fun TestContext.getBlock(pos: BlockPos): BlockState = tracker.level.getBlockState(offset(pos))
/**
* Set a block within the test structure.
*/
fun TestContext.setBlock(pos: BlockPos, state: BlockState) {
tracker.level.setBlockAndUpdate(offset(pos), state)
}
/**
* Set a block within the test structure.
*/
fun TestContext.setBlock(pos: BlockPos, state: BlockStateInput) = state.place(tracker.level, offset(pos), 3)
}
/**
* Modify a block state within the test.
*/
fun TestContext.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
val level = tracker.level
val offset = offset(pos)
level.setBlockAndUpdate(offset, modify(level.getBlockState(offset)))
fun GameTestHelper.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
setBlock(pos, modify(getBlockState(pos)))
}
fun GameTestHelper.sequence(run: GameTestSequence.() -> GameTestSequence) {
run(startSequence()).thenSucceed()
}
/**
* Get a tile within the test structure.
* Set a block within the test structure.
*/
fun TestContext.getTile(pos: BlockPos): TileEntity? = tracker.level.getBlockEntity(offset(pos))
fun GameTestHelper.setBlock(pos: BlockPos, state: BlockStateInput) = state.place(level, absolutePos(pos), 3)
/**
* Get an entity within the test structure.
*/
fun TestContext.getEntity(pos: BlockPos): Entity? {
val entities = tracker.level.getEntitiesOfClass(Entity::class.java, AxisAlignedBB(offset(pos)))
return if (entities.isEmpty()) null else entities.get(0)
}
/**
* Get an entity within the test structure.
*/
inline fun <reified T : Entity> TestContext.getEntityOfType(pos: BlockPos): T? {
val entities = tracker.level.getEntitiesOfClass(T::class.java, AxisAlignedBB(offset(pos)))
return if (entities.isEmpty()) null else entities.get(0)
fun GameTestSequence.thenExecuteSafe(runnable: Runnable) {
try {
runnable.run()
} catch (e: GameTestAssertException) {
throw e
} catch (e: RuntimeException) {
ComputerCraft.log.error("Error in test", e)
throw GameTestAssertException(e.toString())
}
}

View File

@ -1,76 +0,0 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.ingame.mixin;
import net.minecraft.test.TestFunctionInfo;
import net.minecraft.test.TestTrackerHolder;
import net.minecraft.util.Rotation;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.util.function.Consumer;
/**
* Mixin to replace final fields and some getters with non-final versions.
*
* Due to (I assume) the magic of proguard, some getters are replaced with constant
* implementations. Thus we need to replace them with a sensible version.
*/
@Mixin( TestFunctionInfo.class )
public class MixinTestFunctionInfo
{
@Shadow
@Mutable
private String batchName;
@Shadow
@Mutable
private String testName;
@Shadow
@Mutable
private String structureName;
@Shadow
@Mutable
private boolean required;
@Shadow
@Mutable
private Consumer<TestTrackerHolder> function;
@Shadow
@Mutable
private int maxTicks;
@Shadow
@Mutable
private long setupTicks;
@Shadow
@Mutable
private Rotation rotation;
@Overwrite
public int getMaxTicks()
{
return this.maxTicks;
}
@Overwrite
public long getSetupTicks()
{
return setupTicks;
}
@Overwrite
public boolean isRequired()
{
return required;
}
}

View File

@ -6,6 +6,7 @@
package dan200.computercraft.ingame.mod;
import com.mojang.brigadier.CommandDispatcher;
import dan200.computercraft.ComputerCraft;
import net.minecraft.command.CommandSource;
import net.minecraft.data.NBTToSNBTConverter;
import net.minecraft.server.MinecraftServer;
@ -13,6 +14,7 @@ import net.minecraft.test.*;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.text.StringTextComponent;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.world.storage.FolderName;
import javax.annotation.Nonnull;
import java.io.IOException;
@ -63,7 +65,7 @@ class CCTestCommand
{
try
{
Copier.replicate( TestMod.sourceDir.resolve( "computers" ), server.getServerDirectory().toPath().resolve( "world/computercraft" ) );
Copier.replicate( TestMod.sourceDir.resolve( "computers" ), server.getWorldPath( new FolderName( ComputerCraft.MOD_ID ) ) );
}
catch( IOException e )
{
@ -75,7 +77,7 @@ class CCTestCommand
{
try
{
Copier.replicate( server.getServerDirectory().toPath().resolve( "world/computercraft" ), TestMod.sourceDir.resolve( "computers" ) );
Copier.replicate( server.getWorldPath( new FolderName( ComputerCraft.MOD_ID ) ), TestMod.sourceDir.resolve( "computers" ) );
}
catch( IOException e )
{

View File

@ -11,9 +11,8 @@ import dan200.computercraft.api.lua.ILuaAPI;
import dan200.computercraft.api.lua.LuaException;
import dan200.computercraft.api.lua.LuaFunction;
import dan200.computercraft.ingame.api.ComputerState;
import dan200.computercraft.ingame.api.TestContext;
import dan200.computercraft.ingame.api.TestExtensionsKt;
import kotlin.coroutines.Continuation;
import net.minecraft.test.TestList;
import java.util.Optional;
@ -22,7 +21,7 @@ import java.util.Optional;
*
* Note, we extend this API within startup file of computers (see {@code cctest.lua}).
*
* @see TestExtensionsKt#checkComputerOk(TestContext, int, String, Continuation) To check tests on the computer have passed.
* @see TestExtensionsKt#thenComputerOk(TestList, int, String) To check tests on the computer have passed.
*/
public class TestAPI extends ComputerState implements ILuaAPI
{

View File

@ -1,3 +1,8 @@
/*
* This file is part of ComputerCraft - http://www.computercraft.info
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.ingame.mod;
import net.minecraft.client.Minecraft;
@ -26,15 +31,16 @@ import java.util.stream.Collectors;
@Mod.EventBusSubscriber( modid = TestMod.MOD_ID )
public class TestHooks
{
private static final Logger log = LogManager.getLogger( TestHooks.class );
private static final Logger LOG = LogManager.getLogger( TestHooks.class );
private static TestResultList runningTests = null;
private static boolean shutdown = false;
private static int countdown = 20;
@SubscribeEvent
public static void onRegisterCommands( RegisterCommandsEvent event )
{
log.info( "Starting server, registering command helpers." );
LOG.info( "Starting server, registering command helpers." );
TestCommand.register( event.getDispatcher() );
CCTestCommand.register( event.getDispatcher() );
}
@ -51,11 +57,11 @@ public class TestHooks
ServerWorld world = event.getServer().getLevel( World.OVERWORLD );
if( world != null ) world.setDayTime( 6000 );
log.info( "Cleaning up after last run" );
LOG.info( "Cleaning up after last run" );
CommandSource source = server.createCommandSourceStack();
TestUtils.clearAllTests( source.getLevel(), getStart( source ), TestCollection.singleton, 200 );
log.info( "Importing files" );
LOG.info( "Importing files" );
CCTestCommand.importFiles( server );
}
@ -69,7 +75,6 @@ public class TestHooks
if( countdown == 0 && System.getProperty( "cctest.run", "false" ).equals( "true" ) ) startTests();
TestCollection.singleton.tick();
MainThread.INSTANCE.tick();
if( runningTests != null && runningTests.isDone() ) finishTests();
}
@ -83,7 +88,7 @@ public class TestHooks
.filter( x -> FMLLoader.getDist().isClient() | !x.batchName.startsWith( "client" ) )
.collect( Collectors.toList() );
log.info( "Running {} tests...", tests.size() );
LOG.info( "Running {} tests...", tests.size() );
Collection<TestBatch> batches = TestUtils.groupTestsIntoBatches( tests );
return new TestResultList( TestUtils.runTestBatches(
@ -98,25 +103,22 @@ public class TestHooks
private static void finishTests()
{
log.info( "Finished tests - {} were run", runningTests.getTotalCount() );
if( shutdown ) return;
shutdown = true;
LOG.info( "Finished tests - {} were run", runningTests.getTotalCount() );
if( runningTests.hasFailedRequired() )
{
log.error( "{} required tests failed", runningTests.getFailedRequiredCount() );
LOG.error( "{} required tests failed", runningTests.getFailedRequiredCount() );
}
if( runningTests.hasFailedOptional() )
{
log.warn( "{} optional tests failed", runningTests.getFailedOptionalCount() );
LOG.warn( "{} optional tests failed", runningTests.getFailedOptionalCount() );
}
if( ServerLifecycleHooks.getCurrentServer().isDedicatedServer() )
if( FMLLoader.getDist().isDedicatedServer() )
{
log.info( "Stopping server." );
// We can't exit in the main thread, as Minecraft registers a shutdown hook which results
// in a deadlock. So we do this weird janky thing!
Thread thread = new Thread( () -> System.exit( runningTests.hasFailedRequired() ? 1 : 0 ) );
thread.setDaemon( true );
thread.start();
shutdownServer();
}
else
{
@ -130,10 +132,31 @@ public class TestHooks
return new BlockPos( pos.getX(), source.getLevel().getHeightmapPos( Heightmap.Type.WORLD_SURFACE, pos ).getY(), pos.getZ() + 3 );
}
public static void shutdownCommon()
{
System.exit( runningTests.hasFailedRequired() ? 1 : 0 );
}
private static void shutdownServer()
{
// We can't exit normally as Minecraft registers a shutdown hook which results in a deadlock.
LOG.info( "Stopping server." );
MinecraftServer server = ServerLifecycleHooks.getCurrentServer();
new Thread( () -> {
server.halt( true );
shutdownCommon();
}, "Background shutdown" ).start();
}
private static void shutdownClient()
{
Minecraft.getInstance().clearLevel();
Minecraft.getInstance().stop();
if( runningTests.hasFailedOptional() ) System.exit( 1 );
Minecraft minecraft = Minecraft.getInstance();
minecraft.execute( () -> {
LOG.info( "Stopping client." );
minecraft.level.disconnect();
minecraft.clearLevel();
minecraft.stop();
shutdownCommon();
} );
}
}

View File

@ -5,8 +5,8 @@
*/
package dan200.computercraft.ingame.mod;
import com.google.common.base.CaseFormat;
import dan200.computercraft.ingame.api.GameTest;
import dan200.computercraft.ingame.api.GameTestHelper;
import net.minecraft.test.TestFunctionInfo;
import net.minecraft.test.TestRegistry;
import net.minecraft.test.TestTrackerHolder;
@ -20,6 +20,7 @@ import org.objectweb.asm.Type;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Locale;
import java.util.Set;
import java.util.function.Consumer;
@ -60,8 +61,8 @@ class TestLoader
throw new RuntimeException( e );
}
String className = CaseFormat.UPPER_CAMEL.to( CaseFormat.LOWER_UNDERSCORE, klass.getSimpleName() );
String name = className + "." + method.getName().toLowerCase().replace( ' ', '_' );
String className = klass.getSimpleName().toLowerCase( Locale.ROOT );
String name = className + "." + method.getName().toLowerCase( Locale.ROOT );
GameTest test = method.getAnnotation( GameTest.class );
@ -70,8 +71,8 @@ class TestLoader
testFunctions.add( createTestFunction(
test.batch(), name, name,
test.required(),
new TestRunner( name, method ),
test.timeout(),
holder -> runTest( holder, method ),
test.timeoutTicks(),
test.setup()
) );
}
@ -97,4 +98,17 @@ class TestLoader
func.rotation = Rotation.NONE;
return func;
}
private static void runTest( TestTrackerHolder holder, Method method )
{
GameTestHelper helper = new GameTestHelper( holder );
try
{
method.invoke( method.getDeclaringClass().getConstructor().newInstance(), helper );
}
catch( Exception e )
{
helper.fail( e );
}
}
}

View File

@ -1,62 +0,0 @@
package dan200.computercraft.ingame.mod
import dan200.computercraft.ingame.api.TestContext
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.minecraft.test.TestTrackerHolder
import java.lang.reflect.Method
import java.util.*
import java.util.concurrent.ConcurrentLinkedDeque
import java.util.function.Consumer
import kotlin.coroutines.AbstractCoroutineContextElement
import kotlin.coroutines.Continuation
import kotlin.coroutines.ContinuationInterceptor
import kotlin.coroutines.CoroutineContext
import kotlin.reflect.full.callSuspend
import kotlin.reflect.jvm.kotlinFunction
internal class TestRunner(private val name: String, private val method: Method) : Consumer<TestTrackerHolder> {
override fun accept(t: TestTrackerHolder) {
GlobalScope.launch(MainThread + CoroutineName(name)) {
val testContext = TestContext(t)
try {
val instance = method.declaringClass.newInstance()
val function = method.kotlinFunction;
if (function == null) {
method.invoke(instance, testContext)
} else {
function.callSuspend(instance, testContext)
}
testContext.ok()
} catch (e: Exception) {
testContext.fail(e)
}
}
}
}
/**
* A coroutine scope which runs everything on the main thread.
*/
internal object MainThread : AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
private val queue: Queue<() -> Unit> = ConcurrentLinkedDeque()
fun tick() {
while (true) {
val q = queue.poll() ?: break;
q.invoke()
}
}
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
MainThreadInterception(continuation)
private class MainThreadInterception<T>(val cont: Continuation<T>) : Continuation<T> {
override val context: CoroutineContext get() = cont.context
override fun resumeWith(result: Result<T>) {
queue.add { cont.resumeWith(result) }
}
}
}

View File

@ -6,3 +6,21 @@ public-f net.minecraft.test.TestFunctionInfo field_229654_e_
public-f net.minecraft.test.TestFunctionInfo field_229655_f_
public-f net.minecraft.test.TestFunctionInfo field_229656_g_
public-f net.minecraft.test.TestFunctionInfo field_240589_h_
public net.minecraft.test.TestTickResult
public-f net.minecraft.test.TestTickResult field_229485_a_
public-f net.minecraft.test.TestTickResult field_229486_b_
public-f net.minecraft.test.TestList field_229564_a_
public-f net.minecraft.test.TestList field_229565_b_
public net.minecraft.test.TestList field_229566_c_
public net.minecraft.test.TestTracker field_229496_i_
public net.minecraft.test.TestTracker field_229493_f_
public net.minecraft.test.TestTracker func_229525_v_()V # finish
public net.minecraft.test.TestTrackerHolder field_229487_a_ # testInfo
public net.minecraft.test.TestUtils func_229559_b_(Lnet/minecraft/test/TestTracker;Lnet/minecraft/block/Block;)V # spawnBeacon
public net.minecraft.test.TestExecutor func_229479_a_(Lnet/minecraft/test/TestTracker;)V # testCompleted