From 076128a37e41a60772575ed2b4e99d2befd6617c Mon Sep 17 00:00:00 2001 From: SomeTroglodyte <63000004+SomeTroglodyte@users.noreply.github.com> Date: Tue, 17 Aug 2021 12:52:41 +0200 Subject: [PATCH] Unit test catching un-annotated lazies (#4886) --- core/src/com/unciv/models/ruleset/Ruleset.kt | 4 +- .../com/unciv/testing/SerializationTests.kt | 110 ++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 tests/src/com/unciv/testing/SerializationTests.kt diff --git a/core/src/com/unciv/models/ruleset/Ruleset.kt b/core/src/com/unciv/models/ruleset/Ruleset.kt index 68340bc392..9a656c7957 100644 --- a/core/src/com/unciv/models/ruleset/Ruleset.kt +++ b/core/src/com/unciv/models/ruleset/Ruleset.kt @@ -444,7 +444,7 @@ class Ruleset { * save all of the loaded rulesets somewhere for later use * */ object RulesetCache : HashMap() { - fun loadRulesets(consoleMode: Boolean = false, printOutput: Boolean = false) { + fun loadRulesets(consoleMode: Boolean = false, printOutput: Boolean = false, noMods: Boolean = true) { clear() for (ruleset in BaseRuleset.values()) { val fileName = "jsons/${ruleset.fullName}" @@ -453,6 +453,8 @@ object RulesetCache : HashMap() { this[ruleset.fullName] = Ruleset().apply { load(fileHandle, printOutput) } } + if (noMods) return + val modsHandles = if (consoleMode) FileHandle("mods").list() else Gdx.files.local("mods").list() diff --git a/tests/src/com/unciv/testing/SerializationTests.kt b/tests/src/com/unciv/testing/SerializationTests.kt new file mode 100644 index 0000000000..c2ce75ec2d --- /dev/null +++ b/tests/src/com/unciv/testing/SerializationTests.kt @@ -0,0 +1,110 @@ +package com.unciv.testing + +import com.unciv.Constants +import com.unciv.UncivGame +import com.unciv.logic.GameInfo +import com.unciv.logic.GameSaver +import com.unciv.logic.GameStarter +import com.unciv.logic.civilization.PlayerType +import com.unciv.logic.map.MapParameters +import com.unciv.logic.map.MapSize +import com.unciv.logic.map.MapSizeNew +import com.unciv.models.metadata.GameParameters +import com.unciv.models.metadata.GameSettings +import com.unciv.models.metadata.Player +import com.unciv.models.ruleset.RulesetCache +import com.unciv.ui.newgamescreen.GameSetupInfo +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(GdxTestRunner::class) +class SerializationTests { + + private var game = GameInfo() + + /** A runtime Class object for [kotlin.SynchronizedLazyImpl] to enable helping Gdx.Json to + * not StackOverflow on them, as a direct compile time retrieval is forbidden */ + private val classSynchronizedLazyImpl: Class<*> by lazy { + // I hope you get the irony... + @Suppress("unused") // No, test is not _directly_ used, only reflected on + class TestWithLazy { val test: Int by lazy { 0 } } + val badInstance = TestWithLazy() + val badField = badInstance::class.java.declaredFields[0] + badField.isAccessible = true + badField.get(badInstance)::class.java + } + + @Before + fun prepareGame() { + RulesetCache.loadRulesets(noMods = true) + + // Create a tiny game with just 1 human player and the barbarians + // Must be 1 human otherwise GameInfo.setTransients crashes on the `if (currentPlayer == "")` line + val param = GameParameters().apply { + numberOfCityStates = 0 + players.clear() + players.add(Player("Rome").apply { playerType = PlayerType.Human }) + players.add(Player("Greece")) + religionEnabled = true + } + val mapParameters = MapParameters().apply { + mapSize = MapSizeNew(MapSize.Tiny) + seed = 42L + } + val setup = GameSetupInfo(param, mapParameters) + UncivGame.Current = UncivGame("") + UncivGame.Current.settings = GameSettings() + game = GameStarter.startNewGame(setup) + UncivGame.Current.gameInfo = game + + // Found a city otherwise too many classes have no instance and are not tested + val civ = game.getCurrentPlayerCivilization() + val unit = civ.getCivUnits().first { it.hasUnique(Constants.settlerUnique) } + val tile = unit.getTile() + unit.civInfo.addCity(tile.position) + if (tile.ruleset.tileImprovements.containsKey("City center")) + tile.improvement = "City center" + unit.destroy() + + // Ensure some diplomacy objects are instantiated + val otherCiv = game.getCivilization("Greece") + civ.makeCivilizationsMeet(otherCiv) + } + + @Test + fun canSerializeGame() { + val json = try { + GameSaver.json().toJson(game) + } catch (ex: Exception) { + "" + } + Assert.assertTrue("This test will only pass when a game can be serialized", json.isNotEmpty()) + } + + @Test + fun serializedLaziesTest() { + val jsonSerializer = com.badlogic.gdx.utils.Json().apply { + setIgnoreDeprecated(true) + setDeprecated(classSynchronizedLazyImpl, "initializer", true) + setDeprecated(classSynchronizedLazyImpl, "lock", true) // this is the culprit as kotlin initializes it to `this@SynchronizedLazyImpl` + } + + val json = try { + jsonSerializer.toJson(game) + } catch (ex: Throwable) { + ex.printStackTrace() + return + } + + val pattern = """\{(\w+)\${'$'}delegate:\{class:kotlin.SynchronizedLazyImpl,""" + val matches = Regex(pattern).findAll(json) + matches.forEach { + println("Lazy missing `@delegate:Transient` annotation: " + it.groups[1]!!.value) + } + val result = matches.any() + Assert.assertFalse("This test will only pass when no serializable lazy fields are found", result) + } +}