From 0ff6b0ca70fcef52ed0acca424b61c1601922d05 Mon Sep 17 00:00:00 2001 From: Jonathan Coates Date: Fri, 20 Aug 2021 13:32:42 +0100 Subject: [PATCH] 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). --- .github/workflows/main-ci.yml | 11 +- build.gradle | 92 +-- .../computercraft/ingame/MonitorTest.kt | 22 + .../ingame/api/ComputerState.java | 2 +- .../ingame/api/TestExtensions.kt | 132 +++- .../ingame/mod/CCTestCommand.java | 68 +- .../computercraft/ingame/mod/ClientHooks.java | 101 +++ .../computercraft/ingame/mod/Copier.java | 17 +- .../computercraft/ingame/mod/ImageUtils.java | 82 +++ .../monitor_test.looks_acceptable.png | Bin 0 -> 35796 bytes .../monitor_test.looks_acceptable.snbt | 657 ++++++++++++++++++ 11 files changed, 1125 insertions(+), 59 deletions(-) create mode 100644 src/testMod/java/dan200/computercraft/ingame/mod/ClientHooks.java create mode 100644 src/testMod/java/dan200/computercraft/ingame/mod/ImageUtils.java create mode 100644 src/testMod/server-files/screenshots/monitor_test.looks_acceptable.png create mode 100644 src/testMod/server-files/structures/monitor_test.looks_acceptable.snbt diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index d3a482960..8dc8e9e22 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -32,7 +32,7 @@ jobs: run: | ./gradlew assemble || ./gradlew assemble ./gradlew downloadAssets || ./gradlew downloadAssets - ./gradlew build + xvfb-run ./gradlew build - name: Upload Jar uses: actions/upload-artifact@v2 @@ -40,6 +40,15 @@ jobs: name: CC-Tweaked path: build/libs + - name: Upload Screnshots + uses: actions/upload-artifact@v2 + with: + name: Screenshots + path: test-files/client/screenshots + if-no-files-found: ignore + retention-days: 5 + if: failure() + - name: Upload Coverage uses: codecov/codecov-action@v1 diff --git a/build.gradle b/build.gradle index 290595250..4f578960a 100644 --- a/build.gradle +++ b/build.gradle @@ -80,6 +80,17 @@ minecraft { args '--mod', 'computercraft', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') } + testClient { + workingDirectory project.file('test-files/client') + parent runs.client + + mods { + cctest { + source sourceSets.testMod + } + } + } + testServer { workingDirectory project.file('test-files/server') parent runs.server @@ -352,50 +363,53 @@ task setupServer(type: Copy) { into "test-files/server" } -tasks.register('testInGame', JavaExec.class).configure { - it.group('test server') - it.description("Runs tests on a temporary Minecraft server.") - it.dependsOn(setupServer, 'prepareRunTestServer', 'cleanTestInGame', 'compileTestModJava') +["Client", "Server"].forEach {name -> + tasks.register("test$name", JavaExec.class).configure { + it.group('In-game tests') + it.description("Runs tests on a temporary Minecraft instance.") + it.dependsOn(setupServer, "prepareRunTest$name", "cleanTest$name", 'compileTestModJava') - // Copy from runTestServer. We do it in this slightly odd way as runTestServer - // isn't created until the task is configured (which is no good for us). - JavaExec exec = tasks.getByName('runTestServer') - exec.copyTo(it) - it.setClasspath(exec.getClasspath()) - it.mainClass = exec.mainClass - it.setArgs(exec.getArgs()) + // Copy from runTestServer. We do it in this slightly odd way as runTestServer + // isn't created until the task is configured (which is no good for us). + JavaExec exec = tasks.getByName("runTest$name") + exec.copyTo(it) + it.setClasspath(exec.getClasspath()) + it.mainClass = exec.mainClass + it.setArgs(exec.getArgs()) - it.systemProperty('forge.logging.console.level', 'info') - it.systemProperty('cctest.run', 'true') + it.systemProperty('forge.logging.console.level', 'info') + it.systemProperty('cctest.run', 'true') - // Jacoco and modlauncher don't play well together as the classes loaded in-game don't - // match up with those written to disk. We get Jacoco to dump all classes to disk, and - // use that when generating the report. - def coverageOut = new File(buildDir, 'jacocoClassDump/testInGame') - jacoco.applyTo(it) - it.jacoco.setIncludes(["dan200.computercraft.*"]) - it.jacoco.setClassDumpDir(coverageOut) - it.outputs.dir(coverageOut) - // Older versions of modlauncher don't include a protection domain (and thus no code - // source). Jacoco skips such classes by default, so we need to explicitly include them. - it.jacoco.setIncludeNoLocationClasses(true) -} - -tasks.register('jacocoTestInGameReport', JacocoReport.class).configure { - it.group('test server') - it.description('Generate coverage reports for in-game tests (testInGame)') - it.dependsOn('testInGame') - - it.executionData(new File(buildDir, 'jacoco/testInGame.exec')) - it.sourceDirectories.from(sourceSets.main.allJava.srcDirs) - it.classDirectories.from(new File(buildDir, 'jacocoClassDump/testInGame')) - - it.reports { - xml.enabled true - html.enabled true + // Jacoco and modlauncher don't play well together as the classes loaded in-game don't + // match up with those written to disk. We get Jacoco to dump all classes to disk, and + // use that when generating the report. + def coverageOut = new File(buildDir, "jacocoClassDump/test$name") + jacoco.applyTo(it) + it.jacoco.setIncludes(["dan200.computercraft.*"]) + it.jacoco.setClassDumpDir(coverageOut) + it.outputs.dir(coverageOut) + // Older versions of modlauncher don't include a protection domain (and thus no code + // source). Jacoco skips such classes by default, so we need to explicitly include them. + it.jacoco.setIncludeNoLocationClasses(true) } + + tasks.register("jacocoTest${name}Report", JacocoReport.class).configure { + it.group('In-game') + it.description("Generate coverage reports for test$name") + it.dependsOn("test$name") + + it.executionData(new File(buildDir, "jacoco/test${name}.exec")) + it.sourceDirectories.from(sourceSets.main.allJava.srcDirs) + it.classDirectories.from(new File(buildDir, "jacocoClassDump/$name")) + + it.reports { + xml.enabled true + html.enabled true + } + } + + check.dependsOn("jacocoTest${name}Report") } -check.dependsOn('jacocoTestInGameReport') // Upload tasks diff --git a/src/testMod/java/dan200/computercraft/ingame/MonitorTest.kt b/src/testMod/java/dan200/computercraft/ingame/MonitorTest.kt index b4f1ca9fa..644dc9e44 100644 --- a/src/testMod/java/dan200/computercraft/ingame/MonitorTest.kt +++ b/src/testMod/java/dan200/computercraft/ingame/MonitorTest.kt @@ -1,6 +1,7 @@ package dan200.computercraft.ingame import dan200.computercraft.ingame.api.* +import dan200.computercraft.shared.Capabilities import dan200.computercraft.shared.Registry import dan200.computercraft.shared.peripheral.monitor.TileMonitor import net.minecraft.block.Blocks @@ -40,4 +41,25 @@ class Monitor_Test { } } } + + @GameTest(batch = "client:Monitor_Test.Looks_acceptable", timeoutTicks = 400) + fun Looks_acceptable(helper: GameTestHelper) = helper.sequence { + this + .thenExecute { helper.normaliseScene() } + .thenExecute { + helper.positionAtArmorStand() + + // Get the monitor and peripheral. This forces us to create a server monitor at this location. + val monitor = helper.getBlockEntity(BlockPos(2, 2, 2)) as TileMonitor + monitor.getCapability(Capabilities.CAPABILITY_PERIPHERAL) + + val terminal = monitor.cachedServerMonitor.terminal + terminal.write("Hello, world!") + terminal.setCursorPos(1, 2) + terminal.textColour = 2 + terminal.backgroundColour = 3 + terminal.write("Some coloured text") + } + .thenScreenshot() + } } diff --git a/src/testMod/java/dan200/computercraft/ingame/api/ComputerState.java b/src/testMod/java/dan200/computercraft/ingame/api/ComputerState.java index 84bf31f4b..7727fa177 100644 --- a/src/testMod/java/dan200/computercraft/ingame/api/ComputerState.java +++ b/src/testMod/java/dan200/computercraft/ingame/api/ComputerState.java @@ -37,7 +37,7 @@ public class ComputerState public void check( @Nonnull String marker ) { if( !markers.contains( marker ) ) throw new IllegalStateException( "Not yet at " + marker ); - if( error != null ) throw new AssertionError( error ); + if( error != null ) throw new RuntimeException( error ); } public static ComputerState get( int id ) diff --git a/src/testMod/java/dan200/computercraft/ingame/api/TestExtensions.kt b/src/testMod/java/dan200/computercraft/ingame/api/TestExtensions.kt index f826f8f12..fa9256f05 100644 --- a/src/testMod/java/dan200/computercraft/ingame/api/TestExtensions.kt +++ b/src/testMod/java/dan200/computercraft/ingame/api/TestExtensions.kt @@ -1,9 +1,22 @@ package dan200.computercraft.ingame.api -import dan200.computercraft.ComputerCraft +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. @@ -16,6 +29,74 @@ fun GameTestSequence.thenComputerOk(id: Int, marker: String = ComputerState.DONE ComputerState.get(id).check(marker) } +/** + * Run a task on the client + */ +fun GameTestSequence.thenOnClient(task: ClientTestHelper.() -> Unit): GameTestSequence { + var future: CompletableFuture? = 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. */ @@ -32,14 +113,45 @@ fun GameTestHelper.sequence(run: GameTestSequence.() -> GameTestSequence) { */ fun GameTestHelper.setBlock(pos: BlockPos, state: BlockStateInput) = state.place(level, absolutePos(pos), 3) - -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()) +/** + * "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) } } } diff --git a/src/testMod/java/dan200/computercraft/ingame/mod/CCTestCommand.java b/src/testMod/java/dan200/computercraft/ingame/mod/CCTestCommand.java index d05880b05..0a726c409 100644 --- a/src/testMod/java/dan200/computercraft/ingame/mod/CCTestCommand.java +++ b/src/testMod/java/dan200/computercraft/ingame/mod/CCTestCommand.java @@ -7,14 +7,23 @@ package dan200.computercraft.ingame.mod; import com.mojang.brigadier.CommandDispatcher; import dan200.computercraft.ComputerCraft; +import net.minecraft.client.Minecraft; import net.minecraft.command.CommandSource; import net.minecraft.data.NBTToSNBTConverter; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.entity.item.ArmorStandEntity; +import net.minecraft.entity.player.ServerPlayerEntity; +import net.minecraft.nbt.CompoundNBT; import net.minecraft.server.MinecraftServer; import net.minecraft.test.*; +import net.minecraft.tileentity.StructureBlockTileEntity; import net.minecraft.util.ResourceLocation; +import net.minecraft.util.math.BlockPos; import net.minecraft.util.text.StringTextComponent; import net.minecraft.util.text.TextFormatting; import net.minecraft.world.storage.FolderName; +import net.minecraftforge.fml.loading.FMLLoader; import javax.annotation.Nonnull; import java.io.IOException; @@ -58,6 +67,37 @@ class CCTestCommand result.addFailureListener( x -> TestRegistry.rememberFailedTest( x.getTestFunction() ) ); return 0; } ) ) + + .then( literal( "promote" ).executes( context -> { + if( !FMLLoader.getDist().isClient() ) return error( context.getSource(), "Cannot run on server" ); + + promote(); + return 0; + } ) ) + .then( literal( "marker" ).executes( context -> { + ServerPlayerEntity player = context.getSource().getPlayerOrException(); + BlockPos pos = StructureHelper.findNearestStructureBlock( player.blockPosition(), 15, player.getLevel() ); + if( pos == null ) return error( context.getSource(), "No nearby test" ); + + StructureBlockTileEntity structureBlock = (StructureBlockTileEntity) player.getLevel().getBlockEntity( pos ); + TestFunctionInfo info = TestRegistry.getTestFunction( structureBlock.getStructurePath() ); + + // Kill the existing armor stand + player + .getLevel().getEntities() + .filter( x -> x.isAlive() && x instanceof ArmorStandEntity && x.getName().getString().equals( info.getTestName() ) ) + .forEach( Entity::remove ); + + // And create a new one + CompoundNBT nbt = new CompoundNBT(); + nbt.putBoolean( "Marker", true ); + nbt.putBoolean( "Invisible", true ); + ArmorStandEntity armorStand = EntityType.ARMOR_STAND.create( player.getLevel() ); + armorStand.readAdditionalSaveData( nbt ); + armorStand.copyPosition( player ); + armorStand.setCustomName( new StringTextComponent( info.getTestName() ) ); + return 0; + } ) ) ); } @@ -73,7 +113,7 @@ class CCTestCommand } } - public static void exportFiles( MinecraftServer server ) + static void exportFiles( MinecraftServer server ) { try { @@ -85,12 +125,28 @@ class CCTestCommand } } + private static void promote() + { + try + { + Copier.replicate( + Minecraft.getInstance().gameDirectory.toPath().resolve( "screenshots" ), + TestMod.sourceDir.resolve( "screenshots" ), + x -> !x.toFile().getName().endsWith( ".diff.png" ) + ); + } + catch( IOException e ) + { + throw new UncheckedIOException( e ); + } + } + private static class Callback implements ITestCallback { private final CommandSource source; private final TestResultList result; - public Callback( CommandSource source, TestResultList result ) + Callback( CommandSource source, TestResultList result ) { this.source = source; this.result = result; @@ -106,7 +162,13 @@ class CCTestCommand { if( !tracker.isDone() ) return; - source.sendFailure( new StringTextComponent( result.getFailedRequiredCount() + " required tests failed" ).withStyle( TextFormatting.RED ) ); + error( source, result.getFailedRequiredCount() + " required tests failed" ); } } + + private static int error( CommandSource source, String message ) + { + source.sendFailure( new StringTextComponent( message ).withStyle( TextFormatting.RED ) ); + return 0; + } } diff --git a/src/testMod/java/dan200/computercraft/ingame/mod/ClientHooks.java b/src/testMod/java/dan200/computercraft/ingame/mod/ClientHooks.java new file mode 100644 index 000000000..c9e75c669 --- /dev/null +++ b/src/testMod/java/dan200/computercraft/ingame/mod/ClientHooks.java @@ -0,0 +1,101 @@ +/* + * 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.block.Blocks; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screen.MainMenuScreen; +import net.minecraft.client.settings.CloudOption; +import net.minecraft.client.settings.ParticleStatus; +import net.minecraft.client.tutorial.TutorialSteps; +import net.minecraft.util.datafix.codec.DatapackCodec; +import net.minecraft.util.registry.DynamicRegistries; +import net.minecraft.util.registry.Registry; +import net.minecraft.world.*; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.Biomes; +import net.minecraft.world.gen.FlatChunkGenerator; +import net.minecraft.world.gen.FlatGenerationSettings; +import net.minecraft.world.gen.FlatLayerInfo; +import net.minecraft.world.gen.settings.DimensionGeneratorSettings; +import net.minecraft.world.gen.settings.DimensionStructuresSettings; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.GuiScreenEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Collections; +import java.util.Optional; + +import static net.minecraft.world.gen.settings.DimensionGeneratorSettings.withOverworld; + +@Mod.EventBusSubscriber( modid = TestMod.MOD_ID, value = Dist.CLIENT ) +public final class ClientHooks +{ + private static final Logger LOG = LogManager.getLogger( TestHooks.class ); + + private static boolean triggered = false; + + private ClientHooks() + { + } + + @SubscribeEvent + public static void onGuiInit( GuiScreenEvent.InitGuiEvent event ) + { + if( triggered || !(event.getGui() instanceof MainMenuScreen) ) return; + triggered = true; + + ClientHooks.openWorld(); + } + + private static void openWorld() + { + Minecraft minecraft = Minecraft.getInstance(); + + // Clear some options before we get any further. + minecraft.options.autoJump = false; + minecraft.options.renderClouds = CloudOption.OFF; + minecraft.options.particles = ParticleStatus.MINIMAL; + minecraft.options.tutorialStep = TutorialSteps.NONE; + minecraft.options.renderDistance = 6; + minecraft.options.gamma = 1.0; + + if( minecraft.getLevelSource().levelExists( "test" ) ) + { + LOG.info( "World exists, loading it" ); + Minecraft.getInstance().loadLevel( "test" ); + } + else + { + LOG.info( "World does not exist, creating it for the first time" ); + + DynamicRegistries.Impl registries = DynamicRegistries.builtin(); + + Registry dimensions = registries.registryOrThrow( Registry.DIMENSION_TYPE_REGISTRY ); + Registry biomes = registries.registryOrThrow( Registry.BIOME_REGISTRY ); + DimensionGeneratorSettings generator = new DimensionGeneratorSettings( 0, false, false, withOverworld( + dimensions, + DimensionType.defaultDimensions( dimensions, biomes, registries.registryOrThrow( Registry.NOISE_GENERATOR_SETTINGS_REGISTRY ), 0 ), + new FlatChunkGenerator( new FlatGenerationSettings( + biomes, + new DimensionStructuresSettings( Optional.empty(), Collections.emptyMap() ), + Collections.singletonList( new FlatLayerInfo( 4, Blocks.WHITE_CONCRETE ) ), + false, false, + Optional.of( () -> biomes.getOrThrow( Biomes.DESERT ) ) + ) ) + ) ); + + WorldSettings settings = new WorldSettings( + "test", GameType.CREATIVE, false, Difficulty.PEACEFUL, true, + new GameRules(), DatapackCodec.DEFAULT + ); + Minecraft.getInstance().createLevel( "test", settings, registries, generator ); + } + } +} diff --git a/src/testMod/java/dan200/computercraft/ingame/mod/Copier.java b/src/testMod/java/dan200/computercraft/ingame/mod/Copier.java index 00b2fb9a2..baef379bc 100644 --- a/src/testMod/java/dan200/computercraft/ingame/mod/Copier.java +++ b/src/testMod/java/dan200/computercraft/ingame/mod/Copier.java @@ -13,23 +13,25 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.function.Predicate; public final class Copier extends SimpleFileVisitor { private final Path sourceDir; private final Path targetDir; + private final Predicate predicate; - private Copier( Path sourceDir, Path targetDir ) + private Copier( Path sourceDir, Path targetDir, Predicate predicate ) { this.sourceDir = sourceDir; this.targetDir = targetDir; + this.predicate = predicate; } @Override public FileVisitResult visitFile( Path file, BasicFileAttributes attributes ) throws IOException { - Path targetFile = targetDir.resolve( sourceDir.relativize( file ) ); - Files.copy( file, targetFile ); + if( predicate.test( file ) ) Files.copy( file, targetDir.resolve( sourceDir.relativize( file ) ) ); return FileVisitResult.CONTINUE; } @@ -43,12 +45,17 @@ public final class Copier extends SimpleFileVisitor public static void copy( Path from, Path to ) throws IOException { - Files.walkFileTree( from, new Copier( from, to ) ); + Files.walkFileTree( from, new Copier( from, to, p -> true ) ); } public static void replicate( Path from, Path to ) throws IOException + { + replicate( from, to, p -> true ); + } + + public static void replicate( Path from, Path to, Predicate check ) throws IOException { if( Files.exists( to ) ) MoreFiles.deleteRecursively( to ); - copy( from, to ); + Files.walkFileTree( from, new Copier( from, to, check ) ); } } diff --git a/src/testMod/java/dan200/computercraft/ingame/mod/ImageUtils.java b/src/testMod/java/dan200/computercraft/ingame/mod/ImageUtils.java new file mode 100644 index 000000000..0a5dff2f9 --- /dev/null +++ b/src/testMod/java/dan200/computercraft/ingame/mod/ImageUtils.java @@ -0,0 +1,82 @@ +/* + * 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 org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Path; + +public final class ImageUtils +{ + private static final Logger LOG = LogManager.getLogger( ImageUtils.class ); + + /** + * Allow 0.05% of pixels to fail. This allows for slight differences at the edges. + */ + private static final double PIXEL_THRESHOLD = 0.0005; + + /** + * Maximum possible distance between two colours. Floating point differences means we need some fuzziness here. + */ + public static final int DISTANCE_THRESHOLD = 5; + + private ImageUtils() + { + } + + public static boolean areSame( BufferedImage left, BufferedImage right ) + { + int width = left.getWidth(), height = left.getHeight(); + if( width != right.getWidth() || height != right.getHeight() ) return false; + + int failed = 0, threshold = (int) (width * height * PIXEL_THRESHOLD); + for( int x = 0; x < width; x++ ) + { + for( int y = 0; y < height; y++ ) + { + int l = left.getRGB( x, y ), r = right.getRGB( x, y ); + if( (l & 0xFFFFFF) != (r & 0xFFFFFF) && distance( l, r, 0 ) + distance( l, r, 8 ) + distance( l, r, 16 ) >= DISTANCE_THRESHOLD ) + { + failed++; + } + } + } + + if( failed > 0 ) LOG.warn( "{} pixels failed comparing (threshold is {})", failed, threshold ); + return failed <= threshold; + } + + public static void writeDifference( Path path, BufferedImage left, BufferedImage right ) throws IOException + { + int width = left.getWidth(), height = left.getHeight(); + + BufferedImage copy = new BufferedImage( width, height, left.getType() ); + for( int x = 0; x < width; x++ ) + { + for( int y = 0; y < height; y++ ) + { + int l = left.getRGB( x, y ), r = right.getRGB( x, y ); + copy.setRGB( x, y, difference( l, r, 0 ) | difference( l, r, 8 ) | difference( l, r, 16 ) | 0xFF000000 ); + } + } + + ImageIO.write( copy, "png", path.toFile() ); + } + + private static int difference( int l, int r, int shift ) + { + return Math.abs( ((l >> shift) & 0xFF) - ((r >> shift) & 0xFF) ) << shift; + } + + private static int distance( int l, int r, int shift ) + { + return Math.abs( ((l >> shift) & 0xFF) - ((r >> shift) & 0xFF) ); + } +} diff --git a/src/testMod/server-files/screenshots/monitor_test.looks_acceptable.png b/src/testMod/server-files/screenshots/monitor_test.looks_acceptable.png new file mode 100644 index 0000000000000000000000000000000000000000..85d194053f48418002a2cdde4dbc01e515499e3e GIT binary patch literal 35796 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 N002ovPDHLkV1i2*mXZJf literal 0 HcmV?d00001 diff --git a/src/testMod/server-files/structures/monitor_test.looks_acceptable.snbt b/src/testMod/server-files/structures/monitor_test.looks_acceptable.snbt new file mode 100644 index 000000000..c1c44576c --- /dev/null +++ b/src/testMod/server-files/structures/monitor_test.looks_acceptable.snbt @@ -0,0 +1,657 @@ +{ + size: [5, 5, 5], + entities: [ + { + nbt: { + Brain: { + memories: {} + }, + HurtByTimestamp: 0, + Attributes: [ + { + Base: 0.699999988079071d, + Name: "minecraft:generic.movement_speed" + } + ], + Invulnerable: 0b, + FallFlying: 0b, + ShowArms: 0b, + PortalCooldown: 0, + AbsorptionAmount: 0.0f, + FallDistance: 0.0f, + DisabledSlots: 0, + CanUpdate: 1b, + DeathTime: 0s, + Pose: {}, + id: "minecraft:armor_stand", + Invisible: 1b, + UUID: [I; -1245769654, -1089124211, -1971323071, 221540869], + Motion: [0.0d, 0.0d, 0.0d], + Small: 0b, + Health: 20.0f, + Air: 300s, + OnGround: 1b, + Marker: 1b, + Rotation: [-0.2999616f, 16.965813f], + HandItems: [ + {}, + {} + ], + CustomName: '{"text":"monitor_test.looks_acceptable"}', + Pos: [121.57308543720325d, 6.0d, 139.40392497295767d], + Fire: 0s, + ArmorItems: [ + {}, + {}, + {}, + {} + ], + NoBasePlate: 0b, + HurtTime: 0s + }, + blockPos: [2, 1, 0], + pos: [2.573085437203247d, 1.0d, 0.40392497295766816d] + } + ], + blocks: [ + { + pos: [0, 0, 0], + state: 0 + }, + { + pos: [0, 0, 1], + state: 0 + }, + { + pos: [0, 0, 2], + state: 0 + }, + { + pos: [0, 0, 3], + state: 0 + }, + { + pos: [0, 0, 4], + state: 0 + }, + { + pos: [1, 0, 0], + state: 0 + }, + { + pos: [1, 0, 1], + state: 0 + }, + { + pos: [1, 0, 2], + state: 0 + }, + { + pos: [1, 0, 3], + state: 0 + }, + { + pos: [1, 0, 4], + state: 0 + }, + { + pos: [2, 0, 0], + state: 0 + }, + { + pos: [2, 0, 1], + state: 0 + }, + { + pos: [2, 0, 2], + state: 0 + }, + { + pos: [2, 0, 3], + state: 0 + }, + { + pos: [2, 0, 4], + state: 0 + }, + { + pos: [3, 0, 0], + state: 0 + }, + { + pos: [3, 0, 1], + state: 0 + }, + { + pos: [3, 0, 2], + state: 0 + }, + { + pos: [3, 0, 3], + state: 0 + }, + { + pos: [3, 0, 4], + state: 0 + }, + { + pos: [4, 0, 0], + state: 0 + }, + { + pos: [4, 0, 1], + state: 0 + }, + { + pos: [4, 0, 2], + state: 0 + }, + { + pos: [4, 0, 3], + state: 0 + }, + { + pos: [4, 0, 4], + state: 0 + }, + { + pos: [0, 1, 0], + state: 1 + }, + { + pos: [0, 1, 1], + state: 1 + }, + { + pos: [0, 1, 2], + state: 1 + }, + { + pos: [0, 1, 3], + state: 1 + }, + { + pos: [0, 1, 4], + state: 1 + }, + { + pos: [1, 1, 0], + state: 1 + }, + { + pos: [1, 1, 1], + state: 1 + }, + { + pos: [1, 1, 3], + state: 1 + }, + { + pos: [1, 1, 4], + state: 1 + }, + { + pos: [2, 1, 0], + state: 1 + }, + { + pos: [2, 1, 1], + state: 1 + }, + { + pos: [2, 1, 3], + state: 1 + }, + { + pos: [2, 1, 4], + state: 1 + }, + { + pos: [3, 1, 0], + state: 1 + }, + { + pos: [3, 1, 1], + state: 1 + }, + { + pos: [3, 1, 3], + state: 1 + }, + { + pos: [3, 1, 4], + state: 1 + }, + { + pos: [4, 1, 0], + state: 1 + }, + { + pos: [4, 1, 1], + state: 1 + }, + { + pos: [4, 1, 2], + state: 1 + }, + { + pos: [4, 1, 3], + state: 1 + }, + { + pos: [4, 1, 4], + state: 1 + }, + { + pos: [0, 2, 0], + state: 1 + }, + { + pos: [0, 2, 1], + state: 1 + }, + { + pos: [0, 2, 2], + state: 1 + }, + { + pos: [0, 2, 3], + state: 1 + }, + { + pos: [0, 2, 4], + state: 1 + }, + { + pos: [1, 2, 0], + state: 1 + }, + { + pos: [1, 2, 1], + state: 1 + }, + { + pos: [1, 2, 3], + state: 1 + }, + { + pos: [1, 2, 4], + state: 1 + }, + { + pos: [2, 2, 0], + state: 1 + }, + { + pos: [2, 2, 1], + state: 1 + }, + { + pos: [2, 2, 3], + state: 1 + }, + { + pos: [2, 2, 4], + state: 1 + }, + { + pos: [3, 2, 0], + state: 1 + }, + { + pos: [3, 2, 1], + state: 1 + }, + { + pos: [3, 2, 3], + state: 1 + }, + { + pos: [3, 2, 4], + state: 1 + }, + { + pos: [4, 2, 0], + state: 1 + }, + { + pos: [4, 2, 1], + state: 1 + }, + { + pos: [4, 2, 2], + state: 1 + }, + { + pos: [4, 2, 3], + state: 1 + }, + { + pos: [4, 2, 4], + state: 1 + }, + { + pos: [0, 3, 0], + state: 1 + }, + { + pos: [0, 3, 1], + state: 1 + }, + { + pos: [0, 3, 2], + state: 1 + }, + { + pos: [0, 3, 3], + state: 1 + }, + { + pos: [0, 3, 4], + state: 1 + }, + { + pos: [1, 3, 0], + state: 1 + }, + { + pos: [1, 3, 1], + state: 1 + }, + { + pos: [1, 3, 2], + state: 1 + }, + { + pos: [1, 3, 3], + state: 1 + }, + { + pos: [1, 3, 4], + state: 1 + }, + { + pos: [2, 3, 0], + state: 1 + }, + { + pos: [2, 3, 1], + state: 1 + }, + { + pos: [2, 3, 2], + state: 1 + }, + { + pos: [2, 3, 3], + state: 1 + }, + { + pos: [2, 3, 4], + state: 1 + }, + { + pos: [3, 3, 0], + state: 1 + }, + { + pos: [3, 3, 1], + state: 1 + }, + { + pos: [3, 3, 2], + state: 1 + }, + { + pos: [3, 3, 3], + state: 1 + }, + { + pos: [3, 3, 4], + state: 1 + }, + { + pos: [4, 3, 0], + state: 1 + }, + { + pos: [4, 3, 1], + state: 1 + }, + { + pos: [4, 3, 2], + state: 1 + }, + { + pos: [4, 3, 3], + state: 1 + }, + { + pos: [4, 3, 4], + state: 1 + }, + { + pos: [0, 4, 0], + state: 1 + }, + { + pos: [0, 4, 1], + state: 1 + }, + { + pos: [0, 4, 2], + state: 1 + }, + { + pos: [0, 4, 3], + state: 1 + }, + { + pos: [0, 4, 4], + state: 1 + }, + { + pos: [1, 4, 0], + state: 1 + }, + { + pos: [1, 4, 1], + state: 1 + }, + { + pos: [1, 4, 2], + state: 1 + }, + { + pos: [1, 4, 3], + state: 1 + }, + { + pos: [1, 4, 4], + state: 1 + }, + { + pos: [2, 4, 0], + state: 1 + }, + { + pos: [2, 4, 1], + state: 1 + }, + { + pos: [2, 4, 2], + state: 1 + }, + { + pos: [2, 4, 3], + state: 1 + }, + { + pos: [2, 4, 4], + state: 1 + }, + { + pos: [3, 4, 0], + state: 1 + }, + { + pos: [3, 4, 1], + state: 1 + }, + { + pos: [3, 4, 2], + state: 1 + }, + { + pos: [3, 4, 3], + state: 1 + }, + { + pos: [3, 4, 4], + state: 1 + }, + { + pos: [4, 4, 0], + state: 1 + }, + { + pos: [4, 4, 1], + state: 1 + }, + { + pos: [4, 4, 2], + state: 1 + }, + { + pos: [4, 4, 3], + state: 1 + }, + { + pos: [4, 4, 4], + state: 1 + }, + { + nbt: { + XIndex: 2, + Height: 2, + id: "computercraft:monitor_advanced", + Width: 3, + YIndex: 0 + }, + pos: [1, 1, 2], + state: 2 + }, + { + nbt: { + XIndex: 1, + Height: 2, + id: "computercraft:monitor_advanced", + Width: 3, + YIndex: 0 + }, + pos: [2, 1, 2], + state: 3 + }, + { + nbt: { + XIndex: 0, + Height: 2, + id: "computercraft:monitor_advanced", + Width: 3, + YIndex: 0 + }, + pos: [3, 1, 2], + state: 4 + }, + { + nbt: { + XIndex: 2, + Height: 2, + id: "computercraft:monitor_advanced", + Width: 3, + YIndex: 1 + }, + pos: [1, 2, 2], + state: 5 + }, + { + nbt: { + XIndex: 1, + Height: 2, + id: "computercraft:monitor_advanced", + Width: 3, + YIndex: 1 + }, + pos: [2, 2, 2], + state: 6 + }, + { + nbt: { + XIndex: 0, + Height: 2, + id: "computercraft:monitor_advanced", + Width: 3, + YIndex: 1 + }, + pos: [3, 2, 2], + state: 7 + } + ], + palette: [ + { + Name: "minecraft:polished_andesite" + }, + { + Name: "minecraft:air" + }, + { + Properties: { + orientation: "north", + facing: "north", + state: "lu" + }, + Name: "computercraft:monitor_advanced" + }, + { + Properties: { + orientation: "north", + facing: "north", + state: "lru" + }, + Name: "computercraft:monitor_advanced" + }, + { + Properties: { + orientation: "north", + facing: "north", + state: "ru" + }, + Name: "computercraft:monitor_advanced" + }, + { + Properties: { + orientation: "north", + facing: "north", + state: "ld" + }, + Name: "computercraft:monitor_advanced" + }, + { + Properties: { + orientation: "north", + facing: "north", + state: "lrd" + }, + Name: "computercraft:monitor_advanced" + }, + { + Properties: { + orientation: "north", + facing: "north", + state: "rd" + }, + Name: "computercraft:monitor_advanced" + } + ], + DataVersion: 2586 +}