1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-05-04 16:34:14 +00:00
Jonathan Coates 0ff6b0ca70
Client-side tests
This spins up a Minecraft instance (much like we do for the server) and
instructs the client to take screenshots at particular times. We then
compare those screenshots and assert they match (modulo some small
delta).
2021-08-20 17:05:13 +01:00

158 lines
6.0 KiB
Kotlin

package dan200.computercraft.ingame.api
import dan200.computercraft.ingame.mod.ImageUtils
import dan200.computercraft.ingame.mod.TestMod
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.client.Minecraft
import net.minecraft.command.arguments.BlockStateInput
import net.minecraft.entity.item.ArmorStandEntity
import net.minecraft.util.ScreenShotHelper
import net.minecraft.util.math.BlockPos
import net.minecraft.world.gen.Heightmap
import java.nio.file.Files
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException
import java.util.concurrent.atomic.AtomicInteger
import java.util.function.Supplier
import javax.imageio.ImageIO
/**
* Wait until a computer has finished running and check it is OK.
*/
fun GameTestSequence.thenComputerOk(id: Int, marker: String = ComputerState.DONE): GameTestSequence =
thenWaitUntil {
val computer = ComputerState.get(id)
if (computer == null || !computer.isDone(marker)) throw GameTestAssertException("Computer #${id} has not finished yet.")
}.thenExecute {
ComputerState.get(id).check(marker)
}
/**
* Run a task on the client
*/
fun GameTestSequence.thenOnClient(task: ClientTestHelper.() -> Unit): GameTestSequence {
var future: CompletableFuture<Unit>? = null
return this
.thenExecute { future = Minecraft.getInstance().submit(Supplier { task(ClientTestHelper()) }) }
.thenWaitUntil { if (!future!!.isDone) throw GameTestAssertException("Not done task yet") }
.thenExecute {
try {
future!!.get()
} catch (e: ExecutionException) {
throw e.cause ?: e
}
}
}
/**
* Idle for one tick to allow the client to catch up, then take a screenshot.
*/
fun GameTestSequence.thenScreenshot(name: String? = null): GameTestSequence {
val suffix = if (name == null) "" else "-$name"
val fullName = "${parent.testName}$suffix"
val counter = AtomicInteger()
return this
// Wait until all chunks have been rendered and we're idle for an extended period.
.thenExecute { counter.set(0) }
.thenWaitUntil {
if (Minecraft.getInstance().levelRenderer.hasRenderedAllChunks()) {
val idleFor = counter.getAndIncrement()
if (idleFor <= 20) throw GameTestAssertException("Only idle for $idleFor ticks")
} else {
counter.set(0)
throw GameTestAssertException("Waiting for client to finish rendering")
}
}
// Now disable the GUI, take a screenshot and reenable it. We sleep either side to give the client time to do
// its thing.
.thenExecute { Minecraft.getInstance().options.hideGui = true }
.thenIdle(5) // Some delay before/after to ensure the render thread has caught up.
.thenOnClient { screenshot("$fullName.png") }
.thenIdle(2)
.thenExecute {
Minecraft.getInstance().options.hideGui = false
val screenshotsPath = Minecraft.getInstance().gameDirectory.toPath().resolve("screenshots")
val screenshotPath = screenshotsPath.resolve("$fullName.png")
val originalPath = TestMod.sourceDir.resolve("screenshots").resolve("$fullName.png")
if (!Files.exists(originalPath)) throw GameTestAssertException("$fullName does not exist. Use `/cctest promote' to create it.");
val screenshot = ImageIO.read(screenshotPath.toFile())
val original = ImageIO.read(originalPath.toFile())
if (screenshot.width != original.width || screenshot.height != original.height) {
throw GameTestAssertException("$fullName screenshot is ${screenshot.width}x${screenshot.height} but original is ${original.width}x${original.height}")
}
if (ImageUtils.areSame(screenshot, original)) return@thenExecute
ImageUtils.writeDifference(screenshotsPath.resolve("$fullName.diff.png"), screenshot, original)
throw GameTestAssertException("Images are different.")
}
}
val GameTestHelper.testName: String get() = tracker.testName
/**
* Modify a block state within the test.
*/
fun GameTestHelper.modifyBlock(pos: BlockPos, modify: (BlockState) -> BlockState) {
setBlock(pos, modify(getBlockState(pos)))
}
fun GameTestHelper.sequence(run: GameTestSequence.() -> GameTestSequence) {
run(startSequence()).thenSucceed()
}
/**
* Set a block within the test structure.
*/
fun GameTestHelper.setBlock(pos: BlockPos, state: BlockStateInput) = state.place(level, absolutePos(pos), 3)
/**
* "Normalise" the current world in preparation for screenshots.
*
* Basically removes any dirt and replaces it with concrete.
*/
fun GameTestHelper.normaliseScene() {
val y = level.getHeightmapPos(Heightmap.Type.WORLD_SURFACE, absolutePos(BlockPos.ZERO))
for (x in -100..100) {
for (z in -100..100) {
val pos = y.offset(x, -3, z)
val block = level.getBlockState(pos).block
if (block == Blocks.DIRT || block == Blocks.GRASS_BLOCK) {
level.setBlock(pos, Blocks.WHITE_CONCRETE.defaultBlockState(), 3)
}
}
}
}
/**
* Position the player at an armor stand.
*/
fun GameTestHelper.positionAtArmorStand() {
val entities = level.getEntities(null, bounds) { it.name.string == testName }
if (entities.size <= 0 || entities[0] !is ArmorStandEntity) throw IllegalStateException("Cannot find armor stand")
val stand = entities[0] as ArmorStandEntity
val player = level.randomPlayer ?: throw NullPointerException("Player does not exist")
player.connection.teleport(stand.x, stand.y, stand.z, stand.yRot, stand.xRot)
}
class ClientTestHelper {
val minecraft: Minecraft = Minecraft.getInstance()
fun screenshot(name: String) {
ScreenShotHelper.grab(
minecraft.gameDirectory, name,
minecraft.window.width, minecraft.window.height, minecraft.mainRenderTarget
) { TestMod.log.info(it.string) }
}
}