From 180afe56ddde69d8af671fd84c5bcd03e6d1531d Mon Sep 17 00:00:00 2001 From: Martin Jakobsson Date: Sun, 13 Oct 2024 18:32:43 +0200 Subject: [PATCH] Fixed flaky integration tests --- .../scala/hexacraft/client/GameClient.scala | 4 ++ .../scala/hexacraft/main/GameSceneTest.scala | 36 ++++------ .../hexacraft/world/ServerWorldTest.scala | 66 +++++++------------ 3 files changed, 42 insertions(+), 64 deletions(-) diff --git a/client/src/main/scala/hexacraft/client/GameClient.scala b/client/src/main/scala/hexacraft/client/GameClient.scala index 0c26d54b..89ce1baa 100644 --- a/client/src/main/scala/hexacraft/client/GameClient.scala +++ b/client/src/main/scala/hexacraft/client/GameClient.scala @@ -252,6 +252,10 @@ class GameClient( private val leftMouseButtonTimer: TickableTimer = TickableTimer(10, initEnabled = false) private val walkSoundTimer: TickableTimer = TickableTimer(20, initEnabled = false) + def isReadyToPlay: Boolean = { + this.world.getChunk(this.camera.blockCoords.getChunkRelWorld).isDefined + } + private def setUniforms(windowAspectRatio: Float): Unit = { setProjMatrixForAll() worldRenderer.onTotalSizeChanged(world.size.totalSize) diff --git a/main/src/test/scala/hexacraft/main/GameSceneTest.scala b/main/src/test/scala/hexacraft/main/GameSceneTest.scala index 7d591a6f..b6ccfdb8 100644 --- a/main/src/test/scala/hexacraft/main/GameSceneTest.scala +++ b/main/src/test/scala/hexacraft/main/GameSceneTest.scala @@ -23,6 +23,14 @@ class GameSceneTest extends FunSuite { private val windowSize = WindowSize(Vector2i(1920, 1080), Vector2i(1920, 1080)) + def waitFor(maxIterations: Int, waitTimeMs: Int)(success: => Boolean)(iterate: => Unit): Boolean = { + (0 until maxIterations).exists { _ => + iterate + Thread.sleep(waitTimeMs) + success + } + } + test("GameScene.unload frees all shader programs owned by the GameScene") { OpenGL._enterTestMode() @@ -149,17 +157,9 @@ class GameSceneTest extends FunSuite { // ensure the spawn chunk gets loaded val tickContext = TickContext(windowSize, MousePosition(Vector2f(0, 0)), MousePosition(Vector2f(0, 0))) - gameScene.tick(tickContext) - Thread.sleep(20) - gameScene.tick(tickContext) - Thread.sleep(20) - gameScene.tick(tickContext) - Thread.sleep(20) - gameScene.tick(tickContext) - Thread.sleep(20) - gameScene.tick(tickContext) - Thread.sleep(20) - gameScene.tick(tickContext) + assert(waitFor(20, 10)(gameScene.client.isReadyToPlay) { + gameScene.tick(tickContext) + }) // start listening for audio events val audioTracker = Tracker.withStorage[AudioSystem.Event] @@ -212,17 +212,9 @@ class GameSceneTest extends FunSuite { // ensure the spawn chunk gets loaded val tickContext = TickContext(windowSize, MousePosition(Vector2f(0, 0)), MousePosition(Vector2f(0, 0))) - gameScene.tick(tickContext) - Thread.sleep(20) - gameScene.tick(tickContext) - Thread.sleep(20) - gameScene.tick(tickContext) - Thread.sleep(20) - gameScene.tick(tickContext) - Thread.sleep(20) - gameScene.tick(tickContext) - Thread.sleep(20) - gameScene.tick(tickContext) + assert(waitFor(20, 10)(gameScene.client.isReadyToPlay) { + gameScene.tick(tickContext) + }) // start listening for audio events val audioTracker = Tracker.withStorage[AudioSystem.Event] diff --git a/server/src/test/scala/hexacraft/world/ServerWorldTest.scala b/server/src/test/scala/hexacraft/world/ServerWorldTest.scala index 7089cecc..b4562a69 100644 --- a/server/src/test/scala/hexacraft/world/ServerWorldTest.scala +++ b/server/src/test/scala/hexacraft/world/ServerWorldTest.scala @@ -2,10 +2,11 @@ package hexacraft.world import hexacraft.server.ServerWorld import hexacraft.world.block.{Block, BlockState} -import hexacraft.world.chunk.Chunk -import hexacraft.world.coord.{BlockCoords, BlockRelWorld, ChunkRelWorld, CylCoords} +import hexacraft.world.chunk.{Chunk, ChunkColumnData, ChunkColumnHeightMap, ChunkData} +import hexacraft.world.coord.{BlockCoords, BlockRelWorld, ChunkRelWorld, ColumnRelWorld, CylCoords} import hexacraft.world.entity.{BoundsComponent, Entity, MotionComponent, TransformComponent} +import com.martomate.nbt.Nbt import munit.FunSuite import java.util.UUID @@ -13,6 +14,14 @@ import java.util.UUID class ServerWorldTest extends FunSuite { given CylinderSize = CylinderSize(8) + def waitFor(maxIterations: Int, waitTimeMs: Int)(success: => Boolean)(iterate: => Unit): Boolean = { + (0 until maxIterations).exists { _ => + iterate + Thread.sleep(waitTimeMs) + success + } + } + test("the world should not crash") { val provider = new FakeWorldProvider(1234) val world = ServerWorld(provider, provider.getWorldInfo) @@ -72,17 +81,7 @@ class ServerWorldTest extends FunSuite { assert(world.getChunk(cCoords).isEmpty) // Run the game a bit - world.tick(Seq(camera), Seq(cCoords), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(cCoords), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) + assert(waitFor(20, 10)(world.getChunk(cCoords).isDefined)(world.tick(Seq(camera), Seq(cCoords), Seq()))) // The chunk should be loaded assert(world.getChunk(cCoords).isDefined) @@ -100,17 +99,9 @@ class ServerWorldTest extends FunSuite { camera.setPosition(BlockCoords(BlockRelWorld(8, 8, 8, cCoords)).toCylCoords.toVector3d) // Run the game a bit - world.tick(Seq(camera), Seq(cCoords), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(cCoords), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) + assert(waitFor(20, 10)(world.getChunk(cCoords).isDefined) { + world.tick(Seq(camera), Seq(cCoords), Seq()) + }) // The chunk should be loaded assert(world.getChunk(cCoords).isDefined) @@ -119,12 +110,9 @@ class ServerWorldTest extends FunSuite { camera.setPosition(BlockCoords(BlockRelWorld(8, 8, 8, cCoords.offset(100, 0, 0))).toCylCoords.toVector3d) // Run the game a bit - world.tick(Seq(camera), Seq(), Seq(cCoords)) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) - - // The chunk should be unloaded - assert(world.getChunk(cCoords).isEmpty) + assert(waitFor(20, 10)(world.getChunk(cCoords).isEmpty) { + world.tick(Seq(camera), Seq(), Seq(cCoords)) + }) // Clean up world.unload() @@ -132,6 +120,9 @@ class ServerWorldTest extends FunSuite { test("the world should allow entities to be added to and removed from a loaded chunk") { val provider = new FakeWorldProvider(1234) + provider.saveColumnData(ChunkColumnData(Some(ChunkColumnHeightMap.from((_, _) => 0))).toNBT, ColumnRelWorld(0, 0)) + provider.saveChunkData(ChunkData.fromNBT(Nbt.makeMap()).toNBT, ChunkRelWorld(0, 0, 0)) + val world = ServerWorld(provider, provider.getWorldInfo) val camera = new Camera(new CameraProjection(70, 1.6f, 0.01f, 1000f)) @@ -139,19 +130,10 @@ class ServerWorldTest extends FunSuite { // Make sure the chunk is loaded camera.setPosition(entityPosition.toVector3d) - world.tick(Seq(camera), Seq(ChunkRelWorld(0, 0, 0)), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(ChunkRelWorld(0, 0, 0)), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) - Thread.sleep(20) - world.tick(Seq(camera), Seq(), Seq()) - assert(world.getChunk(ChunkRelWorld(0, 0, 0)).isDefined) + assert(waitFor(20, 10)(world.getChunk(ChunkRelWorld(0, 0, 0)).isDefined) { + world.tick(Seq(camera), Seq(ChunkRelWorld(0, 0, 0)), Seq()) + }) val entity = Entity( UUID.randomUUID(),