From 3a96aea894755e43c0b53e5c1d24fee1260e9a37 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Mon, 21 Nov 2022 21:34:35 +0000 Subject: [PATCH] Add a couple of tests for pocket computers - Ensure they're correctly synced to the client. This definitely isn't comprehensive, but doing anything further probably involves multiple players, which is tricky. - Quick rendering test for in-hand computers. --- .../kotlin/cc/tweaked/gradle/MinecraftExec.kt | 11 -- .../pocket/items/PocketComputerItem.java | 4 +- .../gametest/core/MinecraftExtensions.java | 15 +++ .../mixin/gametest/client/MinecraftMixin.java | 54 ++++++++++ .../computercraft/gametest/Inventory_Test.kt | 2 +- .../gametest/Pocket_Computer_Test.kt | 100 ++++++++++++++++++ .../gametest/api/ClientTestExtensions.kt | 13 ++- .../computercraft/gametest/core/TestHooks.kt | 1 + .../computercraft-gametest.mixins.json | 3 + tools/screenshots.py | 45 ++++++-- 10 files changed, 216 insertions(+), 32 deletions(-) create mode 100644 projects/common/src/testMod/java/dan200/computercraft/gametest/core/MinecraftExtensions.java create mode 100644 projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/client/MinecraftMixin.java create mode 100644 projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt diff --git a/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt b/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt index c7a7185ab..85da76164 100644 --- a/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt +++ b/buildSrc/src/main/kotlin/cc/tweaked/gradle/MinecraftExec.kt @@ -40,12 +40,6 @@ val clientDebug get() = project.hasProperty("clientDebug") @get:Input val useFramebuffer get() = !clientDebug && !project.hasProperty("clientNoFramebuffer") - /** - * The folder screenshots are written to. - */ - @get:OutputDirectory - val screenshots = project.layout.buildDirectory.dir("testScreenshots") - /** * The path test results are written to. */ @@ -109,11 +103,6 @@ override fun exec() { } else { super.exec() } - - fsOperations.copy { - from(workingDir.resolve("screenshots")) - into(screenshots) - } } @get:Inject diff --git a/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java b/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java index 5c52dbb01..f8fc88455 100644 --- a/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java +++ b/projects/common/src/main/java/dan200/computercraft/shared/pocket/items/PocketComputerItem.java @@ -49,9 +49,9 @@ public class PocketComputerItem extends Item implements IComputerItem, IMedia, I private static final String NBT_UPGRADE = "Upgrade"; private static final String NBT_UPGRADE_INFO = "UpgradeInfo"; public static final String NBT_LIGHT = "Light"; - private static final String NBT_ON = "On"; + public static final String NBT_ON = "On"; - private static final String NBT_INSTANCE = "Instanceid"; + private static final String NBT_INSTANCE = "InstanceId"; private static final String NBT_SESSION = "SessionId"; private final ComputerFamily family; diff --git a/projects/common/src/testMod/java/dan200/computercraft/gametest/core/MinecraftExtensions.java b/projects/common/src/testMod/java/dan200/computercraft/gametest/core/MinecraftExtensions.java new file mode 100644 index 000000000..1a21f8d9a --- /dev/null +++ b/projects/common/src/testMod/java/dan200/computercraft/gametest/core/MinecraftExtensions.java @@ -0,0 +1,15 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.gametest.core; + +import net.minecraft.client.Minecraft; + +/** + * Extensions to {@link Minecraft}, injected via mixin. + */ +public interface MinecraftExtensions { + boolean computercraft$isRenderingStable(); +} diff --git a/projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/client/MinecraftMixin.java b/projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/client/MinecraftMixin.java new file mode 100644 index 000000000..6a60799a2 --- /dev/null +++ b/projects/common/src/testMod/java/dan200/computercraft/mixin/gametest/client/MinecraftMixin.java @@ -0,0 +1,54 @@ +/* + * This file is part of ComputerCraft - http://www.computercraft.info + * Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission. + * Send enquiries to dratcliffe@gmail.com + */ +package dan200.computercraft.mixin.gametest.client; + +import dan200.computercraft.gametest.core.MinecraftExtensions; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.client.renderer.LevelRenderer; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.annotation.Nullable; +import java.util.concurrent.atomic.AtomicBoolean; + +@Mixin(Minecraft.class) +class MinecraftMixin implements MinecraftExtensions { + @Final + @Shadow + public LevelRenderer levelRenderer; + + @Shadow + @Nullable + public ClientLevel level; + + @Shadow + @Nullable + public LocalPlayer player; + + @Unique + private final AtomicBoolean isStable = new AtomicBoolean(false); + + @Inject(method = "runTick", at = @At("TAIL")) + private void updateStable(boolean render, CallbackInfo ci) { + isStable.set( + level != null && player != null && + levelRenderer.isChunkCompiled(player.blockPosition()) && levelRenderer.countRenderedChunks() > 10 && + levelRenderer.hasRenderedAllChunks() + ); + } + + @Override + public boolean computercraft$isRenderingStable() { + return isStable.get(); + } +} diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Inventory_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Inventory_Test.kt index e33050fdc..db0883bd5 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Inventory_Test.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Inventory_Test.kt @@ -44,7 +44,7 @@ fun Checks_valid_item(helper: GameTestHelper) = helper.sequence { /** * Ensures inventory methods check an item is valid before moving it. * - * @see + * @see */ @GameTest fun Fails_on_full(helper: GameTestHelper) = helper.sequence { diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt new file mode 100644 index 000000000..8b1c56d29 --- /dev/null +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/Pocket_Computer_Test.kt @@ -0,0 +1,100 @@ +package dan200.computercraft.gametest + +import dan200.computercraft.api.lua.ObjectArguments +import dan200.computercraft.client.pocket.ClientPocketComputers +import dan200.computercraft.core.apis.TermAPI +import dan200.computercraft.gametest.api.* +import dan200.computercraft.mixin.gametest.GameTestHelperAccessor +import dan200.computercraft.shared.ModRegistry +import dan200.computercraft.shared.computer.core.ComputerState +import dan200.computercraft.shared.pocket.items.PocketComputerItem +import dan200.computercraft.test.core.computer.getApi +import net.minecraft.core.BlockPos +import net.minecraft.gametest.framework.GameTestHelper +import net.minecraft.gametest.framework.GameTestSequence +import org.junit.jupiter.api.Assertions.assertEquals +import kotlin.random.Random + +class Pocket_Computer_Test { + /** + * Checks pocket computer state is synced to the holding player. + */ + @ClientGameTest(template = Structures.DEFAULT) + fun Sync_state(context: GameTestHelper) = context.sequence { + // We use a unique label for each test run as computers from previous runs may not have been disposed yet. + val unique = java.lang.Long.toHexString(Random.nextLong()) + + // Give the player a pocket computer. + thenExecute { + context.positionAt(BlockPos(2, 2, 2)) + context.givePocketComputer(unique) + } + // Write some text to the computer. + thenOnComputer(unique) { getApi().write(ObjectArguments("Hello, world!")) } + // And ensure its synced to the client. + thenIdle(4) + thenOnClient { + val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem) + assertEquals(ComputerState.ON, pocketComputer.state) + + val term = pocketComputer.terminal + assertEquals("Hello, world!", term.getLine(0).toString().trim(), "Terminal contents is synced") + } + // Update the terminal contents again. + thenOnComputer(unique) { + val term = getApi() + term.setCursorPos(1, 1) + term.setCursorBlink(true) + term.write(ObjectArguments("Updated text :)")) + } + // And ensure the new computer state and terminal are sent. + thenIdle(4) + thenOnClient { + val pocketComputer = ClientPocketComputers.get(minecraft.player!!.mainHandItem) + assertEquals(ComputerState.BLINKING, pocketComputer.state) + + val term = pocketComputer.terminal + assertEquals("Updated text :)", term.getLine(0).toString().trim(), "Terminal contents is synced") + } + } + + /** + * Checks pocket computers are rendered when being held like a map. + */ + @ClientGameTest(template = Structures.DEFAULT) + fun Renders_map_view(context: GameTestHelper) = context.sequence { + // We use a unique label for each test run as computers from previous runs may not have been disposed yet. + val unique = java.lang.Long.toHexString(Random.nextLong()) + + // Give the player a pocket computer. + thenExecute { + context.positionAt(BlockPos(2, 2, 2), xRot = 90.0f) + context.givePocketComputer(unique) + } + thenOnComputer(unique) { + val terminal = getApi().terminal + terminal.write("Hello, world!") + terminal.setCursorPos(1, 2) + terminal.textColour = 2 + terminal.backgroundColour = 3 + terminal.write("Some coloured text") + } + thenIdle(4) + thenScreenshot(showGui = true) + } + + /** + * Give the current player a pocket computer, suitable to be controlled by [GameTestSequence.thenOnComputer]. + */ + private fun GameTestHelper.givePocketComputer(name: String? = null) { + val player = level.randomPlayer!! + player.inventory.clearContent() + + val testName = (this as GameTestHelperAccessor).testInfo.testName + val label = testName + (if (name == null) "" else ".$name") + + val item = ModRegistry.Items.POCKET_COMPUTER_ADVANCED.get().create(1, label, -1, null) + item.getOrCreateTag().putBoolean(PocketComputerItem.NBT_ON, true) + player.inventory.setItem(0, item) + } +} diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/ClientTestExtensions.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/ClientTestExtensions.kt index 998102ac1..39cd17452 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/ClientTestExtensions.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/api/ClientTestExtensions.kt @@ -1,5 +1,6 @@ package dan200.computercraft.gametest.api +import dan200.computercraft.gametest.core.MinecraftExtensions import dan200.computercraft.mixin.gametest.GameTestSequenceAccessor import dan200.computercraft.shared.platform.Registries import net.minecraft.client.Minecraft @@ -20,9 +21,7 @@ /** * Attempt to guess whether all chunks have been rendered. */ -fun Minecraft.isRenderingStable(): Boolean = level != null && player != null && - levelRenderer.isChunkCompiled(player!!.blockPosition()) && levelRenderer.countRenderedChunks() > 10 && - levelRenderer.hasRenderedAllChunks() +fun Minecraft.isRenderingStable(): Boolean = (this as MinecraftExtensions).`computercraft$isRenderingStable`() /** * Run a task on the client. @@ -44,7 +43,7 @@ /** * Take a screenshot of the current game state. */ -fun GameTestSequence.thenScreenshot(name: String? = null): GameTestSequence { +fun GameTestSequence.thenScreenshot(name: String? = null, showGui: Boolean = false): GameTestSequence { val suffix = if (name == null) "" else "-$name" val test = (this as GameTestSequenceAccessor).parent val fullName = "${test.testName}$suffix" @@ -63,7 +62,7 @@ // Now disable the GUI, take a screenshot and reenable it. Sleep a little afterwards to ensure the render thread // has caught up. - thenOnClient { minecraft.options.hideGui = true } + thenOnClient { minecraft.options.hideGui = !showGui } thenIdle(2) // Take a screenshot and wait for it to have finished. @@ -96,12 +95,12 @@ /** * Position the player at a given coordinate. */ -fun GameTestHelper.positionAt(pos: BlockPos) { +fun GameTestHelper.positionAt(pos: BlockPos, yRot: Float = 0.0f, xRot: Float = 0.0f) { val absolutePos = absolutePos(pos) val player = level.randomPlayer ?: throw GameTestAssertException("Player does not exist") player.setupForTest() - player.connection.teleport(absolutePos.x + 0.5, absolutePos.y + 0.5, absolutePos.z + 0.5, 0.0f, 0.0f) + player.connection.teleport(absolutePos.x + 0.5, absolutePos.y + 0.5, absolutePos.z + 0.5, yRot, xRot) } /** diff --git a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt index 348a294d6..25f351bb9 100644 --- a/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt +++ b/projects/common/src/testMod/kotlin/dan200/computercraft/gametest/core/TestHooks.kt @@ -78,6 +78,7 @@ fun onServerStarted(server: MinecraftServer) { Loot_Test::class.java, Modem_Test::class.java, Monitor_Test::class.java, + Pocket_Computer_Test::class.java, Printer_Test::class.java, Recipe_Test::class.java, Turtle_Test::class.java, diff --git a/projects/common/src/testMod/resources/computercraft-gametest.mixins.json b/projects/common/src/testMod/resources/computercraft-gametest.mixins.json index a67f1f07e..f10093924 100644 --- a/projects/common/src/testMod/resources/computercraft-gametest.mixins.json +++ b/projects/common/src/testMod/resources/computercraft-gametest.mixins.json @@ -13,5 +13,8 @@ "GameTestSequenceMixin", "SharedConstantsMixin", "TestCommandAccessor" + ], + "client": [ + "client.MinecraftMixin" ] } diff --git a/tools/screenshots.py b/tools/screenshots.py index 3a70acd37..948f6d705 100755 --- a/tools/screenshots.py +++ b/tools/screenshots.py @@ -2,13 +2,13 @@ """ Combines screenshots from the Forge and Fabric tests into a single HTML page. """ -import os -import os.path -from dataclasses import dataclass -from typing import TextIO -from textwrap import dedent -import webbrowser import argparse +import pathlib +import webbrowser +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from textwrap import dedent +from typing import TextIO PROJECT_LOCATIONS = [ "projects/fabric", @@ -68,7 +68,7 @@ def write_images(io: TextIO, images: list[Image]):
- {" » ".join(image.name[:-2])} » + {" » ".join(image.name[:-1])} » {image.name[-1]}
@@ -79,6 +79,22 @@ def write_images(io: TextIO, images: list[Image]): io.write("") +def _normalise_id(name: str) -> str: + """Normalise a test ID so it's more readable.""" + return name[0].upper() + name[1:].replace("_", " ") + + +def _format_timedelta(delta: timedelta) -> str: + if delta.days > 0: + return f"{delta.days} days ago" + elif delta.seconds >= 60 * 60 * 2: + return f"{delta.seconds // (60 * 60)} hours ago" + elif delta.seconds >= 60 * 2: + return f"{delta.seconds // 60} minutes ago" + else: + return f"{delta.seconds} seconds ago" + + def main(): spec = argparse.ArgumentParser( description="Combines screenshots from the Forge and Fabric tests into a single HTML page." @@ -96,10 +112,17 @@ def main(): "Forge": "projects/forge", "Fabric": "projects/fabric", }.items(): - dir = os.path.join(dir, "build", "testScreenshots") - for file in sorted(os.listdir(dir)): - name = [project, *os.path.splitext(file)[0].split(".")] - images.append(Image(name, os.path.join(dir, file))) + for file in sorted(pathlib.Path(dir).glob("build/gametest/*/screenshots/*.png")): + name = [project, *(_normalise_id(x) for x in file.stem.split("."))] + + mtime = datetime.fromtimestamp(file.stat().st_mtime, tz=timezone.utc) + delta = datetime.now(tz=timezone.utc) - mtime + + print( + f"""{" » ".join(name[:-1]):>50} » \x1b[1m{name[-1]:25} \x1b[0;33m({_format_timedelta(delta)})\x1b[0m""" + ) + + images.append(Image(name, str(file))) out_file = "build/screenshots.html" with open(out_file, encoding="utf-8", mode="w") as out: