mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-01-15 11:45: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:
parent
4dea3dff36
commit
4b33306940
@ -4,24 +4,28 @@ import dan200.computercraft.ingame.api.*
|
|||||||
import net.minecraft.block.LeverBlock
|
import net.minecraft.block.LeverBlock
|
||||||
import net.minecraft.block.RedstoneLampBlock
|
import net.minecraft.block.RedstoneLampBlock
|
||||||
import net.minecraft.util.math.BlockPos
|
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.
|
* Ensures redstone signals do not travel through computers.
|
||||||
*
|
*
|
||||||
* @see [#548](https://github.com/SquidDev-CC/CC-Tweaked/issues/548)
|
* @see [#548](https://github.com/SquidDev-CC/CC-Tweaked/issues/548)
|
||||||
*/
|
*/
|
||||||
@GameTest
|
@GameTest
|
||||||
suspend fun `No through signal`(context: TestContext) {
|
fun No_through_signal(context: GameTestHelper) = context.sequence {
|
||||||
val lamp = BlockPos(2, 0, 4)
|
val lamp = BlockPos(2, 2, 4)
|
||||||
val lever = BlockPos(2, 0, 0)
|
val lever = BlockPos(2, 2, 0)
|
||||||
|
this
|
||||||
assertFalse(context.getBlock(lamp).getValue(RedstoneLampBlock.LIT), "Lamp should not be lit")
|
.thenExecute {
|
||||||
|
context.assertBlockState(lamp, { !it.getValue(RedstoneLampBlock.LIT) }, { "Lamp should not be lit" })
|
||||||
context.modifyBlock(lever) { x -> x.setValue(LeverBlock.POWERED, true) }
|
context.modifyBlock(lever) { x -> x.setValue(LeverBlock.POWERED, true) }
|
||||||
context.sleep(3)
|
}
|
||||||
|
.thenIdle(3)
|
||||||
assertFalse(context.getBlock(lamp).getValue(RedstoneLampBlock.LIT), "Lamp should still not be lit")
|
.thenExecute {
|
||||||
|
context.assertBlockState(
|
||||||
|
lamp,
|
||||||
|
{ !it.getValue(RedstoneLampBlock.LIT) },
|
||||||
|
{ "Lamp should still not be lit" })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
package dan200.computercraft.ingame
|
package dan200.computercraft.ingame
|
||||||
|
|
||||||
import dan200.computercraft.ingame.api.GameTest
|
import dan200.computercraft.ingame.api.GameTest
|
||||||
import dan200.computercraft.ingame.api.TestContext
|
import dan200.computercraft.ingame.api.GameTestHelper
|
||||||
import dan200.computercraft.ingame.api.checkComputerOk
|
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.
|
* Sends a rednet message to another a computer and back again.
|
||||||
*/
|
*/
|
||||||
@GameTest
|
@GameTest
|
||||||
suspend fun `Sends basic rednet messages`(context: TestContext) = context.checkComputerOk(13)
|
fun Sends_basic_rednet_messages(context: GameTestHelper) = context.sequence { thenComputerOk(13) }
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,23 @@
|
|||||||
package dan200.computercraft.ingame
|
package dan200.computercraft.ingame
|
||||||
|
|
||||||
import dan200.computercraft.ingame.api.*
|
import dan200.computercraft.ingame.api.*
|
||||||
import net.minecraft.entity.item.ItemEntity
|
|
||||||
import net.minecraft.item.Items
|
import net.minecraft.item.Items
|
||||||
import net.minecraft.util.math.BlockPos
|
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.
|
* Ensure audio disks exist and we can play them.
|
||||||
*
|
*
|
||||||
* @see [#688](https://github.com/SquidDev-CC/CC-Tweaked/issues/688)
|
* @see [#688](https://github.com/SquidDev-CC/CC-Tweaked/issues/688)
|
||||||
*/
|
*/
|
||||||
@GameTest
|
@GameTest
|
||||||
suspend fun `Audio disk`(context: TestContext) = context.checkComputerOk(3)
|
fun Audio_disk(helper: GameTestHelper) = helper.sequence { thenComputerOk(3) }
|
||||||
|
|
||||||
@GameTest
|
@GameTest
|
||||||
suspend fun `Ejects disk`(context: TestContext) {
|
fun Ejects_disk(helper: GameTestHelper) = helper.sequence {
|
||||||
val stackAt = BlockPos(2, 0, 2)
|
val stackAt = BlockPos(2, 2, 2)
|
||||||
context.checkComputerOk(4)
|
this
|
||||||
context.waitUntil { context.getEntity(stackAt) != null }
|
.thenComputerOk(4)
|
||||||
|
.thenWaitUntil { helper.assertItemEntityPresent(Items.MUSIC_DISC_13, stackAt, 0.0) }
|
||||||
val stack = context.getEntityOfType<ItemEntity>(stackAt)!!
|
|
||||||
assertEquals(Items.MUSIC_DISC_13, stack.item.item, "Correct item stack")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,20 +5,21 @@ import dan200.computercraft.shared.Registry
|
|||||||
import dan200.computercraft.shared.peripheral.modem.wired.BlockCable
|
import dan200.computercraft.shared.peripheral.modem.wired.BlockCable
|
||||||
import net.minecraft.util.math.BlockPos
|
import net.minecraft.util.math.BlockPos
|
||||||
|
|
||||||
class ModemTest {
|
class Modem_Test {
|
||||||
@GameTest
|
@GameTest
|
||||||
suspend fun `Have peripherals`(context: TestContext) = context.checkComputerOk(15)
|
fun Have_peripherals(helper: GameTestHelper) = helper.sequence { thenComputerOk(15) }
|
||||||
|
|
||||||
@GameTest
|
@GameTest
|
||||||
suspend fun `Gains peripherals`(context: TestContext) {
|
fun Gains_peripherals(helper: GameTestHelper) = helper.sequence {
|
||||||
val position = BlockPos(2, 0, 2)
|
val position = BlockPos(2, 2, 2)
|
||||||
context.checkComputerOk(16, "initial")
|
this
|
||||||
|
.thenComputerOk(16, "initial")
|
||||||
context.setBlock(position, BlockCable.correctConnections(
|
.thenExecute {
|
||||||
context.level, context.offset(position),
|
helper.setBlock(position, BlockCable.correctConnections(
|
||||||
|
helper.level, helper.absolutePos(position),
|
||||||
Registry.ModBlocks.CABLE.get().defaultBlockState().setValue(BlockCable.CABLE, true)
|
Registry.ModBlocks.CABLE.get().defaultBlockState().setValue(BlockCable.CABLE, true)
|
||||||
))
|
))
|
||||||
|
}
|
||||||
context.checkComputerOk(16)
|
.thenComputerOk(16)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,12 @@ import net.minecraft.block.Blocks
|
|||||||
import net.minecraft.command.arguments.BlockStateInput
|
import net.minecraft.command.arguments.BlockStateInput
|
||||||
import net.minecraft.nbt.CompoundNBT
|
import net.minecraft.nbt.CompoundNBT
|
||||||
import net.minecraft.util.math.BlockPos
|
import net.minecraft.util.math.BlockPos
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
|
||||||
import org.junit.jupiter.api.fail
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class MonitorTest {
|
class Monitor_Test {
|
||||||
@GameTest
|
@GameTest
|
||||||
suspend fun `Ensures valid on place`(context: TestContext) {
|
fun Ensures_valid_on_place(context: GameTestHelper) = context.sequence {
|
||||||
val pos = BlockPos(2, 0, 2)
|
val pos = BlockPos(2, 2, 2)
|
||||||
val tag = CompoundNBT()
|
val tag = CompoundNBT()
|
||||||
tag.putInt("Width", 2)
|
tag.putInt("Width", 2)
|
||||||
tag.putInt("Height", 2)
|
tag.putInt("Height", 2)
|
||||||
@ -28,12 +26,18 @@ class MonitorTest {
|
|||||||
context.setBlock(pos, Blocks.AIR.defaultBlockState())
|
context.setBlock(pos, Blocks.AIR.defaultBlockState())
|
||||||
context.setBlock(pos, toSet)
|
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.width != 1 || tile.height != 1) {
|
||||||
if (tile !is TileMonitor) fail("Expected tile to be monitor, is $tile")
|
context.fail("Tile has width and height of ${tile.width}x${tile.height}, but should be 1x1", pos)
|
||||||
|
}
|
||||||
assertEquals(1, tile.width, "Width should be 1")
|
}
|
||||||
assertEquals(1, tile.height, "Width should be 1")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,71 +1,78 @@
|
|||||||
package dan200.computercraft.ingame
|
package dan200.computercraft.ingame
|
||||||
|
|
||||||
import dan200.computercraft.ingame.api.GameTest
|
import dan200.computercraft.ingame.api.GameTest
|
||||||
import dan200.computercraft.ingame.api.TestContext
|
import dan200.computercraft.ingame.api.GameTestHelper
|
||||||
import dan200.computercraft.ingame.api.checkComputerOk
|
import dan200.computercraft.ingame.api.sequence
|
||||||
|
import dan200.computercraft.ingame.api.thenComputerOk
|
||||||
|
|
||||||
class TurtleTest {
|
class Turtle_Test {
|
||||||
@GameTest
|
@GameTest(timeoutTicks = TIMEOUT)
|
||||||
suspend fun `Unequip refreshes peripheral`(context: TestContext) = context.checkComputerOk(1)
|
fun Unequip_refreshes_peripheral(helper: GameTestHelper) = helper.sequence { thenComputerOk(1) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks turtles can sheer sheep (and drop items)
|
* Checks turtles can sheer sheep (and drop items)
|
||||||
*
|
*
|
||||||
* @see [#537](https://github.com/SquidDev-CC/CC-Tweaked/issues/537)
|
* @see [#537](https://github.com/SquidDev-CC/CC-Tweaked/issues/537)
|
||||||
*/
|
*/
|
||||||
@GameTest
|
@GameTest(timeoutTicks = TIMEOUT)
|
||||||
suspend fun `Shears sheep`(context: TestContext) = context.checkComputerOk(5)
|
fun Shears_sheep(helper: GameTestHelper) = helper.sequence { thenComputerOk(5) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks turtles can place lava.
|
* Checks turtles can place lava.
|
||||||
*
|
*
|
||||||
* @see [#518](https://github.com/SquidDev-CC/CC-Tweaked/issues/518)
|
* @see [#518](https://github.com/SquidDev-CC/CC-Tweaked/issues/518)
|
||||||
*/
|
*/
|
||||||
@GameTest
|
@GameTest(timeoutTicks = TIMEOUT)
|
||||||
suspend fun `Place lava`(context: TestContext) = context.checkComputerOk(5)
|
fun Place_lava(helper: GameTestHelper) = helper.sequence { thenComputerOk(5) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks turtles can place when waterlogged.
|
* Checks turtles can place when waterlogged.
|
||||||
*
|
*
|
||||||
* @see [#385](https://github.com/SquidDev-CC/CC-Tweaked/issues/385)
|
* @see [#385](https://github.com/SquidDev-CC/CC-Tweaked/issues/385)
|
||||||
*/
|
*/
|
||||||
@GameTest
|
@GameTest(timeoutTicks = TIMEOUT)
|
||||||
suspend fun `Place waterlogged`(context: TestContext) = context.checkComputerOk(7)
|
fun Place_waterlogged(helper: GameTestHelper) = helper.sequence { thenComputerOk(7) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks turtles can pick up lava
|
* Checks turtles can pick up lava
|
||||||
*
|
*
|
||||||
* @see [#297](https://github.com/SquidDev-CC/CC-Tweaked/issues/297)
|
* @see [#297](https://github.com/SquidDev-CC/CC-Tweaked/issues/297)
|
||||||
*/
|
*/
|
||||||
@GameTest
|
@GameTest(timeoutTicks = TIMEOUT)
|
||||||
suspend fun `Gather lava`(context: TestContext) = context.checkComputerOk(8)
|
fun Gather_lava(helper: GameTestHelper) = helper.sequence { thenComputerOk(8) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks turtles can hoe dirt.
|
* Checks turtles can hoe dirt.
|
||||||
*
|
*
|
||||||
* @see [#258](https://github.com/SquidDev-CC/CC-Tweaked/issues/258)
|
* @see [#258](https://github.com/SquidDev-CC/CC-Tweaked/issues/258)
|
||||||
*/
|
*/
|
||||||
@GameTest
|
@GameTest(timeoutTicks = TIMEOUT)
|
||||||
suspend fun `Hoe dirt`(context: TestContext) = context.checkComputerOk(9)
|
fun Hoe_dirt(helper: GameTestHelper) = helper.sequence { thenComputerOk(9) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks turtles can place monitors
|
* Checks turtles can place monitors
|
||||||
*
|
*
|
||||||
* @see [#691](https://github.com/SquidDev-CC/CC-Tweaked/issues/691)
|
* @see [#691](https://github.com/SquidDev-CC/CC-Tweaked/issues/691)
|
||||||
*/
|
*/
|
||||||
@GameTest
|
@GameTest(timeoutTicks = TIMEOUT)
|
||||||
suspend fun `Place monitor`(context: TestContext) = context.checkComputerOk(10)
|
fun Place_monitor(helper: GameTestHelper) = helper.sequence { thenComputerOk(10) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks turtles can place into compostors. These are non-typical inventories, so
|
* Checks turtles can place into compostors. These are non-typical inventories, so
|
||||||
* worth testing.
|
* worth testing.
|
||||||
*/
|
*/
|
||||||
@GameTest
|
@GameTest(timeoutTicks = TIMEOUT)
|
||||||
suspend fun `Use compostors`(context: TestContext) = context.checkComputerOk(11)
|
fun Use_compostors(helper: GameTestHelper) = helper.sequence { thenComputerOk(11) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks turtles can be cleaned in cauldrons.
|
* Checks turtles can be cleaned in cauldrons.
|
||||||
|
*
|
||||||
|
* Currently not required as turtles can no longer right-click cauldrons.
|
||||||
*/
|
*/
|
||||||
@GameTest
|
@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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
package dan200.computercraft.ingame.api;
|
package dan200.computercraft.ingame.api;
|
||||||
|
|
||||||
import dan200.computercraft.ingame.mod.TestAPI;
|
import dan200.computercraft.ingame.mod.TestAPI;
|
||||||
import kotlin.coroutines.Continuation;
|
import net.minecraft.test.TestList;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@ -18,7 +18,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
* Assertion state of a computer.
|
* Assertion state of a computer.
|
||||||
*
|
*
|
||||||
* @see TestAPI For the Lua interface for this.
|
* @see TestAPI For the Lua interface for this.
|
||||||
* @see TestExtensionsKt#checkComputerOk(TestContext, int, String, Continuation)
|
* @see TestExtensionsKt#thenComputerOk(TestList, int, String)
|
||||||
*/
|
*/
|
||||||
public class ComputerState
|
public class ComputerState
|
||||||
{
|
{
|
||||||
|
@ -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
|
* A test which manipulates the game. This should applied on an instance function, and should accept a single
|
||||||
* {@link TestContext} argument.
|
* {@link GameTestHelper} argument.
|
||||||
*
|
|
||||||
* Tests may/should be written as Kotlin coroutines.
|
|
||||||
*/
|
*/
|
||||||
@Retention( RetentionPolicy.RUNTIME )
|
@Retention( RetentionPolicy.RUNTIME )
|
||||||
@Target( ElementType.METHOD )
|
@Target( ElementType.METHOD )
|
||||||
@ -25,7 +23,7 @@ public @interface GameTest
|
|||||||
*
|
*
|
||||||
* @return The time the test can run in ticks.
|
* @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.
|
* Number of ticks to delay between building the structure and running the test code.
|
||||||
|
@ -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
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +1,45 @@
|
|||||||
package dan200.computercraft.ingame.api
|
package dan200.computercraft.ingame.api
|
||||||
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import dan200.computercraft.ComputerCraft
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import net.minecraft.block.BlockState
|
import net.minecraft.block.BlockState
|
||||||
import net.minecraft.command.arguments.BlockStateInput
|
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.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.
|
* Wait until a computer has finished running and check it is OK.
|
||||||
*/
|
*/
|
||||||
suspend fun TestContext.checkComputerOk(id: Int, marker: String = ComputerState.DONE) {
|
fun GameTestSequence.thenComputerOk(id: Int, marker: String = ComputerState.DONE): GameTestSequence =
|
||||||
waitUntil {
|
thenWaitUntil {
|
||||||
val computer = ComputerState.get(id)
|
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)
|
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.
|
* Modify a block state within the test.
|
||||||
*/
|
*/
|
||||||
fun TestContext.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
|
fun GameTestHelper.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
|
||||||
val level = tracker.level
|
setBlock(pos, modify(getBlockState(pos)))
|
||||||
val offset = offset(pos)
|
}
|
||||||
level.setBlockAndUpdate(offset, modify(level.getBlockState(offset)))
|
|
||||||
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
fun GameTestSequence.thenExecuteSafe(runnable: Runnable) {
|
||||||
* Get an entity within the test structure.
|
try {
|
||||||
*/
|
runnable.run()
|
||||||
inline fun <reified T : Entity> TestContext.getEntityOfType(pos: BlockPos): T? {
|
} catch (e: GameTestAssertException) {
|
||||||
val entities = tracker.level.getEntitiesOfClass(T::class.java, AxisAlignedBB(offset(pos)))
|
throw e
|
||||||
return if (entities.isEmpty()) null else entities.get(0)
|
} catch (e: RuntimeException) {
|
||||||
|
ComputerCraft.log.error("Error in test", e)
|
||||||
|
throw GameTestAssertException(e.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,6 +6,7 @@
|
|||||||
package dan200.computercraft.ingame.mod;
|
package dan200.computercraft.ingame.mod;
|
||||||
|
|
||||||
import com.mojang.brigadier.CommandDispatcher;
|
import com.mojang.brigadier.CommandDispatcher;
|
||||||
|
import dan200.computercraft.ComputerCraft;
|
||||||
import net.minecraft.command.CommandSource;
|
import net.minecraft.command.CommandSource;
|
||||||
import net.minecraft.data.NBTToSNBTConverter;
|
import net.minecraft.data.NBTToSNBTConverter;
|
||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
@ -13,6 +14,7 @@ import net.minecraft.test.*;
|
|||||||
import net.minecraft.util.ResourceLocation;
|
import net.minecraft.util.ResourceLocation;
|
||||||
import net.minecraft.util.text.StringTextComponent;
|
import net.minecraft.util.text.StringTextComponent;
|
||||||
import net.minecraft.util.text.TextFormatting;
|
import net.minecraft.util.text.TextFormatting;
|
||||||
|
import net.minecraft.world.storage.FolderName;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -63,7 +65,7 @@ class CCTestCommand
|
|||||||
{
|
{
|
||||||
try
|
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 )
|
catch( IOException e )
|
||||||
{
|
{
|
||||||
@ -75,7 +77,7 @@ class CCTestCommand
|
|||||||
{
|
{
|
||||||
try
|
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 )
|
catch( IOException e )
|
||||||
{
|
{
|
||||||
|
@ -11,9 +11,8 @@ import dan200.computercraft.api.lua.ILuaAPI;
|
|||||||
import dan200.computercraft.api.lua.LuaException;
|
import dan200.computercraft.api.lua.LuaException;
|
||||||
import dan200.computercraft.api.lua.LuaFunction;
|
import dan200.computercraft.api.lua.LuaFunction;
|
||||||
import dan200.computercraft.ingame.api.ComputerState;
|
import dan200.computercraft.ingame.api.ComputerState;
|
||||||
import dan200.computercraft.ingame.api.TestContext;
|
|
||||||
import dan200.computercraft.ingame.api.TestExtensionsKt;
|
import dan200.computercraft.ingame.api.TestExtensionsKt;
|
||||||
import kotlin.coroutines.Continuation;
|
import net.minecraft.test.TestList;
|
||||||
|
|
||||||
import java.util.Optional;
|
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}).
|
* 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
|
public class TestAPI extends ComputerState implements ILuaAPI
|
||||||
{
|
{
|
||||||
|
@ -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;
|
package dan200.computercraft.ingame.mod;
|
||||||
|
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
@ -26,15 +31,16 @@ import java.util.stream.Collectors;
|
|||||||
@Mod.EventBusSubscriber( modid = TestMod.MOD_ID )
|
@Mod.EventBusSubscriber( modid = TestMod.MOD_ID )
|
||||||
public class TestHooks
|
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 TestResultList runningTests = null;
|
||||||
|
private static boolean shutdown = false;
|
||||||
private static int countdown = 20;
|
private static int countdown = 20;
|
||||||
|
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public static void onRegisterCommands( RegisterCommandsEvent event )
|
public static void onRegisterCommands( RegisterCommandsEvent event )
|
||||||
{
|
{
|
||||||
log.info( "Starting server, registering command helpers." );
|
LOG.info( "Starting server, registering command helpers." );
|
||||||
TestCommand.register( event.getDispatcher() );
|
TestCommand.register( event.getDispatcher() );
|
||||||
CCTestCommand.register( event.getDispatcher() );
|
CCTestCommand.register( event.getDispatcher() );
|
||||||
}
|
}
|
||||||
@ -51,11 +57,11 @@ public class TestHooks
|
|||||||
ServerWorld world = event.getServer().getLevel( World.OVERWORLD );
|
ServerWorld world = event.getServer().getLevel( World.OVERWORLD );
|
||||||
if( world != null ) world.setDayTime( 6000 );
|
if( world != null ) world.setDayTime( 6000 );
|
||||||
|
|
||||||
log.info( "Cleaning up after last run" );
|
LOG.info( "Cleaning up after last run" );
|
||||||
CommandSource source = server.createCommandSourceStack();
|
CommandSource source = server.createCommandSourceStack();
|
||||||
TestUtils.clearAllTests( source.getLevel(), getStart( source ), TestCollection.singleton, 200 );
|
TestUtils.clearAllTests( source.getLevel(), getStart( source ), TestCollection.singleton, 200 );
|
||||||
|
|
||||||
log.info( "Importing files" );
|
LOG.info( "Importing files" );
|
||||||
CCTestCommand.importFiles( server );
|
CCTestCommand.importFiles( server );
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +75,6 @@ public class TestHooks
|
|||||||
if( countdown == 0 && System.getProperty( "cctest.run", "false" ).equals( "true" ) ) startTests();
|
if( countdown == 0 && System.getProperty( "cctest.run", "false" ).equals( "true" ) ) startTests();
|
||||||
|
|
||||||
TestCollection.singleton.tick();
|
TestCollection.singleton.tick();
|
||||||
MainThread.INSTANCE.tick();
|
|
||||||
|
|
||||||
if( runningTests != null && runningTests.isDone() ) finishTests();
|
if( runningTests != null && runningTests.isDone() ) finishTests();
|
||||||
}
|
}
|
||||||
@ -83,7 +88,7 @@ public class TestHooks
|
|||||||
.filter( x -> FMLLoader.getDist().isClient() | !x.batchName.startsWith( "client" ) )
|
.filter( x -> FMLLoader.getDist().isClient() | !x.batchName.startsWith( "client" ) )
|
||||||
.collect( Collectors.toList() );
|
.collect( Collectors.toList() );
|
||||||
|
|
||||||
log.info( "Running {} tests...", tests.size() );
|
LOG.info( "Running {} tests...", tests.size() );
|
||||||
|
|
||||||
Collection<TestBatch> batches = TestUtils.groupTestsIntoBatches( tests );
|
Collection<TestBatch> batches = TestUtils.groupTestsIntoBatches( tests );
|
||||||
return new TestResultList( TestUtils.runTestBatches(
|
return new TestResultList( TestUtils.runTestBatches(
|
||||||
@ -98,25 +103,22 @@ public class TestHooks
|
|||||||
|
|
||||||
private static void finishTests()
|
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() )
|
if( runningTests.hasFailedRequired() )
|
||||||
{
|
{
|
||||||
log.error( "{} required tests failed", runningTests.getFailedRequiredCount() );
|
LOG.error( "{} required tests failed", runningTests.getFailedRequiredCount() );
|
||||||
}
|
}
|
||||||
if( runningTests.hasFailedOptional() )
|
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." );
|
shutdownServer();
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
else
|
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 );
|
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()
|
private static void shutdownClient()
|
||||||
{
|
{
|
||||||
Minecraft.getInstance().clearLevel();
|
Minecraft minecraft = Minecraft.getInstance();
|
||||||
Minecraft.getInstance().stop();
|
minecraft.execute( () -> {
|
||||||
if( runningTests.hasFailedOptional() ) System.exit( 1 );
|
LOG.info( "Stopping client." );
|
||||||
|
minecraft.level.disconnect();
|
||||||
|
minecraft.clearLevel();
|
||||||
|
minecraft.stop();
|
||||||
|
shutdownCommon();
|
||||||
|
} );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
package dan200.computercraft.ingame.mod;
|
package dan200.computercraft.ingame.mod;
|
||||||
|
|
||||||
import com.google.common.base.CaseFormat;
|
|
||||||
import dan200.computercraft.ingame.api.GameTest;
|
import dan200.computercraft.ingame.api.GameTest;
|
||||||
|
import dan200.computercraft.ingame.api.GameTestHelper;
|
||||||
import net.minecraft.test.TestFunctionInfo;
|
import net.minecraft.test.TestFunctionInfo;
|
||||||
import net.minecraft.test.TestRegistry;
|
import net.minecraft.test.TestRegistry;
|
||||||
import net.minecraft.test.TestTrackerHolder;
|
import net.minecraft.test.TestTrackerHolder;
|
||||||
@ -20,6 +20,7 @@ import org.objectweb.asm.Type;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@ -60,8 +61,8 @@ class TestLoader
|
|||||||
throw new RuntimeException( e );
|
throw new RuntimeException( e );
|
||||||
}
|
}
|
||||||
|
|
||||||
String className = CaseFormat.UPPER_CAMEL.to( CaseFormat.LOWER_UNDERSCORE, klass.getSimpleName() );
|
String className = klass.getSimpleName().toLowerCase( Locale.ROOT );
|
||||||
String name = className + "." + method.getName().toLowerCase().replace( ' ', '_' );
|
String name = className + "." + method.getName().toLowerCase( Locale.ROOT );
|
||||||
|
|
||||||
GameTest test = method.getAnnotation( GameTest.class );
|
GameTest test = method.getAnnotation( GameTest.class );
|
||||||
|
|
||||||
@ -70,8 +71,8 @@ class TestLoader
|
|||||||
testFunctions.add( createTestFunction(
|
testFunctions.add( createTestFunction(
|
||||||
test.batch(), name, name,
|
test.batch(), name, name,
|
||||||
test.required(),
|
test.required(),
|
||||||
new TestRunner( name, method ),
|
holder -> runTest( holder, method ),
|
||||||
test.timeout(),
|
test.timeoutTicks(),
|
||||||
test.setup()
|
test.setup()
|
||||||
) );
|
) );
|
||||||
}
|
}
|
||||||
@ -97,4 +98,17 @@ class TestLoader
|
|||||||
func.rotation = Rotation.NONE;
|
func.rotation = Rotation.NONE;
|
||||||
return func;
|
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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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_229655_f_
|
||||||
public-f net.minecraft.test.TestFunctionInfo field_229656_g_
|
public-f net.minecraft.test.TestFunctionInfo field_229656_g_
|
||||||
public-f net.minecraft.test.TestFunctionInfo field_240589_h_
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user