From 8855f48ae2ef21bcb3a2f9606c1e8753f63b5d78 Mon Sep 17 00:00:00 2001 From: shahondin1624 Date: Fri, 13 Mar 2026 13:00:59 +0100 Subject: [PATCH] test: add serialization round-trip test (Closes #37) --- .../SerializationRoundTripTest.kt | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 sharedUI/src/commonTest/kotlin/org/shahondin1624/SerializationRoundTripTest.kt diff --git a/sharedUI/src/commonTest/kotlin/org/shahondin1624/SerializationRoundTripTest.kt b/sharedUI/src/commonTest/kotlin/org/shahondin1624/SerializationRoundTripTest.kt new file mode 100644 index 0000000..4f396ad --- /dev/null +++ b/sharedUI/src/commonTest/kotlin/org/shahondin1624/SerializationRoundTripTest.kt @@ -0,0 +1,172 @@ +package org.shahondin1624 + +import org.shahondin1624.lib.functions.DataLoader +import org.shahondin1624.model.attributes.Attribute +import org.shahondin1624.model.attributes.AttributeType +import org.shahondin1624.model.attributes.Attributes +import org.shahondin1624.model.characterdata.CharacterData +import org.shahondin1624.model.characterdata.Metatype +import org.shahondin1624.model.charactermodel.DamageMonitor +import org.shahondin1624.model.charactermodel.ShadowrunCharacter +import org.shahondin1624.model.talents.Talent +import org.shahondin1624.model.talents.Talents +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SerializationRoundTripTest { + + /** + * Build a character with non-default values in every field, + * serialize to JSON, deserialize back, and assert deep equality. + * + * Note: DamageMonitor.attributes and DamageMonitor.cache are @Transient + * and are not serialized. After deserialization, ShadowrunCharacter.init + * re-sets the attributes on the DamageMonitor. We therefore keep damage + * current values within the range valid for EMPTY_ATTRIBUTES (max 7 for + * stun/physical, 0 for overflow) so the DamageMonitor init won't throw. + */ + @Test + fun serializeDeserializePreservesAllFields() { + val attributes = Attributes( + body = Attribute(AttributeType.Body, 5), + agility = Attribute(AttributeType.Agility, 6), + reaction = Attribute(AttributeType.Reaction, 4), + strength = Attribute(AttributeType.Strength, 7), + willpower = Attribute(AttributeType.Willpower, 3), + logic = Attribute(AttributeType.Logic, 5), + intuition = Attribute(AttributeType.Intuition, 4), + charisma = Attribute(AttributeType.Charisma, 6), + edge = 3 + ) + + val characterData = CharacterData( + concept = "Street Samurai", + nuyen = 25000, + essence = 3.5f, + name = "Jane 'Razor' Smith", + metatype = Metatype.Elf, + age = 34, + gender = "female", + streetCred = 5, + notoriety = 2, + publicAwareness = 1, + totalKarma = 100, + currentKarma = 42 + ) + + val talents = Talents( + talents = listOf( + Talent(name = "Firearms", attribute = AttributeType.Agility, value = 6, custom = false), + Talent(name = "Blades", attribute = AttributeType.Agility, value = 5, custom = false), + Talent(name = "Custom Hack", attribute = AttributeType.Logic, value = 3, custom = true) + ) + ) + + // Keep damage within EMPTY_ATTRIBUTES bounds: stun/physical max = 7, overflow max = 0 + val damageMonitor = DamageMonitor( + attributes = attributes, + stunCurrent = 3, + physicalCurrent = 2, + overflowCurrent = 0 + ) + + val original = ShadowrunCharacter( + characterData = characterData, + attributes = attributes, + talents = talents, + damageMonitor = damageMonitor + ) + + // Round-trip + val json = DataLoader.serialize(original) + val restored = DataLoader.deserialize(json) + + // CharacterData assertions + assertEquals(original.characterData.concept, restored.characterData.concept) + assertEquals(original.characterData.nuyen, restored.characterData.nuyen) + assertEquals(original.characterData.essence, restored.characterData.essence) + assertEquals(original.characterData.name, restored.characterData.name) + assertEquals(original.characterData.metatype, restored.characterData.metatype) + assertEquals(original.characterData.age, restored.characterData.age) + assertEquals(original.characterData.gender, restored.characterData.gender) + assertEquals(original.characterData.streetCred, restored.characterData.streetCred) + assertEquals(original.characterData.notoriety, restored.characterData.notoriety) + assertEquals(original.characterData.publicAwareness, restored.characterData.publicAwareness) + assertEquals(original.characterData.totalKarma, restored.characterData.totalKarma) + assertEquals(original.characterData.currentKarma, restored.characterData.currentKarma) + assertEquals(original.characterData.version, restored.characterData.version) + + // Attributes assertions + assertEquals(original.attributes.body(), restored.attributes.body()) + assertEquals(original.attributes.agility(), restored.attributes.agility()) + assertEquals(original.attributes.reaction(), restored.attributes.reaction()) + assertEquals(original.attributes.strength(), restored.attributes.strength()) + assertEquals(original.attributes.willpower(), restored.attributes.willpower()) + assertEquals(original.attributes.logic(), restored.attributes.logic()) + assertEquals(original.attributes.intuition(), restored.attributes.intuition()) + assertEquals(original.attributes.charisma(), restored.attributes.charisma()) + assertEquals(original.attributes.edge, restored.attributes.edge) + assertEquals(original.attributes.version, restored.attributes.version) + + // Talents assertions + assertEquals(original.talents.talents.size, restored.talents.talents.size) + original.talents.talents.forEachIndexed { i, orig -> + val rest = restored.talents.talents[i] + assertEquals(orig.name, rest.name, "Talent[$i].name") + assertEquals(orig.attribute, rest.attribute, "Talent[$i].attribute") + assertEquals(orig.value, rest.value, "Talent[$i].value") + assertEquals(orig.custom, rest.custom, "Talent[$i].custom") + } + assertEquals(original.talents.version, restored.talents.version) + + // DamageMonitor assertions (check serialized fields only; @Transient fields are re-set) + assertEquals(original.damageMonitor.stunCurrent(), restored.damageMonitor.stunCurrent()) + assertEquals(original.damageMonitor.physicalCurrent(), restored.damageMonitor.physicalCurrent()) + assertEquals(original.damageMonitor.overflowCurrent(), restored.damageMonitor.overflowCurrent()) + + // Version + assertEquals(original.version, restored.version) + } + + @Test + fun roundTripWithExampleCharacter() { + // EXAMPLE_CHARACTER uses createDamageMonitor which sets damage to max values + // that may exceed the EMPTY_ATTRIBUTES limit during deserialization. + // Create a copy with safe damage values for the round-trip test. + val original = org.shahondin1624.model.EXAMPLE_CHARACTER.copy( + damageMonitor = DamageMonitor( + attributes = org.shahondin1624.model.EXAMPLE_CHARACTER.attributes, + stunCurrent = 0, + physicalCurrent = 0, + overflowCurrent = 0 + ) + ) + val json = DataLoader.serialize(original) + val restored = DataLoader.deserialize(json) + + assertEquals(original.characterData, restored.characterData) + assertEquals(original.attributes.edge, restored.attributes.edge) + assertEquals(original.talents.talents.size, restored.talents.talents.size) + assertEquals(original.version, restored.version) + } + + @Test + fun serializedJsonContainsAllFields() { + val original = org.shahondin1624.model.EXAMPLE_CHARACTER + val json = DataLoader.serialize(original) + + // Verify key fields are present in JSON output + val checks = listOf( + "characterData", "attributes", "talents", "damageMonitor", + "name", "metatype", "nuyen", "essence", "edge", + "body", "agility", "reaction", "strength", + "willpower", "logic", "intuition", "charisma", + "stunCurrent", "physicalCurrent", "overflowCurrent", + "version" + ) + for (field in checks) { + assertTrue(json.contains("\"$field\""), "JSON should contain \"$field\"") + } + } +}