From 3bc31085458e34a41de930b7b6939c00caba5083 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 22 Jan 2026 20:47:49 +0100 Subject: [PATCH 1/7] Adding awaitility for testing Concurrency on full encode/decode loop --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index b74082a..3b25204 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ java { repositories { mavenCentral() + mavenLocal() } jacoco { @@ -40,6 +41,7 @@ dependencies { testImplementation platform('org.junit:junit-bom:6.0.2') testImplementation 'org.junit.jupiter:junit-jupiter' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testImplementation 'org.awaitility:awaitility:4.2.1' } test { @@ -47,6 +49,7 @@ test { finalizedBy jacocoTestReport // report is always generated after tests run } + jacocoTestReport { dependsOn test reports { @@ -96,3 +99,4 @@ tasks.register('specsValidation', Test) { include '**/ConformanceTest.class' } + From de26cd1e9b32d2c24db89802748840eb27f70c59 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Thu, 22 Jan 2026 20:50:46 +0100 Subject: [PATCH 2/7] Add Test for Concurrency test for round trip encode/decode JSON Encode/Decode --- .../jtoon/JToonConcurrencyTest.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/test/java/dev/toonformat/jtoon/JToonConcurrencyTest.java diff --git a/src/test/java/dev/toonformat/jtoon/JToonConcurrencyTest.java b/src/test/java/dev/toonformat/jtoon/JToonConcurrencyTest.java new file mode 100644 index 0000000..f25c1b1 --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/JToonConcurrencyTest.java @@ -0,0 +1,108 @@ +package dev.toonformat.jtoon; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadLocalRandom; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.*; + +class JToonConcurrencyTest { + + @Test + void encodeDecodeStressTest() { + int threads = 8; + int tasksPerThread = 5_000; + + final ExecutorService executor = Executors.newFixedThreadPool(threads); + final CountDownLatch latch = new CountDownLatch(threads * tasksPerThread); + final List errors = Collections.synchronizedList(new ArrayList<>()); + + Runnable task = () -> { + try { + // Given + final Map data = new LinkedHashMap<>(); + data.put("id", ThreadLocalRandom.current().nextInt()); + data.put("name", "Alice"); + data.put("tags", List.of("x", "y", "z")); + + // When + final String toon = JToon.encode(data); + + // Then + assertNotNull(toon); + final Object decoded = JToon.decode(toon); + assertNotNull(decoded); + + } catch (Throwable ex) { + errors.add(ex); + } finally { + latch.countDown(); + } + }; + + for (int i = 0; i < threads * tasksPerThread; i++) { + executor.submit(task); + } + + await() + .atMost(10, SECONDS) + .until(() -> latch.getCount() == 0); + + executor.shutdown(); + + assertTrue(errors.isEmpty(), "Errors occurred in threads: " + errors); + } + + void encodeDecodeJSONStressTest() { + int threads = 8; + int tasksPerThread = 5_000; + + final ExecutorService executor = Executors.newFixedThreadPool(threads); + final CountDownLatch latch = new CountDownLatch(threads * tasksPerThread); + final List errors = Collections.synchronizedList(new ArrayList<>()); + + Runnable task = () -> { + try { + // Given + String json = "{\"foo\":123, \"bar\":[\"a\",\"b\"]}"; + + // When + String toon = JToon.encodeJson(json); + + // Then + assertNotNull(toon); + String roundTrip = JToon.decodeToJson(toon); + assertNotNull(roundTrip); + assertTrue(roundTrip.contains("\"foo\":123")); + + } catch (Throwable ex) { + errors.add(ex); + } finally { + latch.countDown(); + } + }; + + for (int i = 0; i < threads * tasksPerThread; i++) { + executor.submit(task); + } + + await() + .atMost(10, SECONDS) + .until(() -> latch.getCount() == 0); + + executor.shutdown(); + + assertTrue(errors.isEmpty(), "Errors occurred in threads: " + errors); + } + +} From b21210ca7ea73d1c8b94dc93a99b26bf49f9937c Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Wed, 28 Jan 2026 17:27:41 +0100 Subject: [PATCH 3/7] Add Fuzztest --- .../dev/toonformat/jtoon/JToonFuzzTest.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/test/java/dev/toonformat/jtoon/JToonFuzzTest.java diff --git a/src/test/java/dev/toonformat/jtoon/JToonFuzzTest.java b/src/test/java/dev/toonformat/jtoon/JToonFuzzTest.java new file mode 100644 index 0000000..ac5ddd9 --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/JToonFuzzTest.java @@ -0,0 +1,60 @@ +package dev.toonformat.jtoon; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.Arrays; +import java.util.SplittableRandom; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +class JToonFuzzTest { + + private static final SplittableRandom RANDOM = new SplittableRandom(); + + @Test + @Tag("fuzz") + void fuzzUnicodeInput() { + final String[] evil = { + "\u0000", // null char + "\uD800", // broken surrogate + "\uFFFF", + "\u2028", // line separator + "💣", // emoji + "漢字" + }; + assertDoesNotThrow(() -> Arrays.stream(evil).forEach(s -> { + try { + JToon.decode("{\"x\":\"" + s + "\"}"); + } catch (RuntimeException e) { + // acceptable + } + })); + } + + + @Test + @Tag("fuzz") + void fuzzDoesNotHang() { + for (int i = 0; i < 1_000; i++) { + byte[] bytes = new byte[RANDOM.nextInt(500)]; + RANDOM.nextBytes(bytes); + String input = new String(bytes); + + assertTimeoutPreemptively( + Duration.ofMillis(100), + () -> { + try { + JToon.decode(input); + } catch (RuntimeException e) { + // expected + } + } + ); + } + } + + +} From 8f88335f3b61e2f5ea2951af2f4e497302acd3f0 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Wed, 28 Jan 2026 17:28:23 +0100 Subject: [PATCH 4/7] Update the lazy loading of ObjectMapper --- .../jtoon/util/ObjectMapperSingleton.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java index a5326e8..8d24086 100644 --- a/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java +++ b/src/main/java/dev/toonformat/jtoon/util/ObjectMapperSingleton.java @@ -15,7 +15,7 @@ public final class ObjectMapperSingleton { /** * Holds the singleton ObjectMapper. */ - private static ObjectMapper INSTANCE; + private static volatile ObjectMapper INSTANCE; private ObjectMapperSingleton() { throw new UnsupportedOperationException("Utility class cannot be instantiated"); @@ -27,14 +27,20 @@ private ObjectMapperSingleton() { * @return ObjectMapper */ public static ObjectMapper getInstance() { - if (INSTANCE == null) { - INSTANCE = JsonMapper.builder() - .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.ALWAYS)) - .addModule(new AfterburnerModule()) // Speeds up Jackson by 20–40% in most real-world cases - .defaultTimeZone(TimeZone.getTimeZone("UTC")) // set a default timezone for dates - .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) - .build(); + ObjectMapper result = INSTANCE; + if (result == null) { + synchronized (ObjectMapperSingleton.class) { + result = INSTANCE; + if (result == null) { + INSTANCE = result = JsonMapper.builder() + .changeDefaultPropertyInclusion(incl -> incl.withValueInclusion(JsonInclude.Include.ALWAYS)) + .addModule(new AfterburnerModule()) // Speeds up Jackson by 20–40% in most real-world cases + .defaultTimeZone(TimeZone.getTimeZone("UTC")) // set a default timezone for dates + .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + .build(); + } + } } - return INSTANCE; + return result; } } From ec7e2bbe5b69c53104dd06fcf169fa0e80150225 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Wed, 28 Jan 2026 17:31:00 +0100 Subject: [PATCH 5/7] Adding RaceCondition test --- .../jtoon/JToonRaceConditionTest.java | 93 +++++++++++++++++++ .../DecodeContextRaceConditionTest.java | 82 ++++++++++++++++ .../decoder/ValueDecoderThreadSafetyTest.java | 43 +++++++++ .../JsonNormalizerThreadSafetyTest.java | 57 ++++++++++++ 4 files changed, 275 insertions(+) create mode 100644 src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java create mode 100644 src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java create mode 100644 src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderThreadSafetyTest.java create mode 100644 src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java diff --git a/src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java b/src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java new file mode 100644 index 0000000..926bd25 --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/JToonRaceConditionTest.java @@ -0,0 +1,93 @@ +package dev.toonformat.jtoon; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class JToonRaceConditionTest { + + @Test + @DisplayName("Should be thread-safe when encoding and decoding concurrently") + void concurrentEncodeDecode() throws InterruptedException, ExecutionException { + int threadCount = 20; + int iterationsPerThread = 100; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + Map input = new LinkedHashMap<>(); + input.put("name", "JToon"); + input.put("version", "1.0.0"); + input.put("tags", List.of("java", "json", "toon")); + input.put("active", true); + + Map metadata = new LinkedHashMap<>(); + metadata.put("author", "dev"); + metadata.put("stars", 100); + metadata.put("created", java.time.LocalDateTime.now()); + input.put("metadata", metadata); + + List> futures = new ArrayList<>(); + + for (int i = 0; i < threadCount * iterationsPerThread; i++) { + futures.add(executor.submit(() -> { + String encoded = JToon.encode(input); + Object decoded = JToon.decode(encoded); + + // When decoding, LocalDateTime becomes a String + // We use toString check for other fields and manual check for metadata + Map decodedMap = (Map) decoded; + assertEquals(input.get("name"), decodedMap.get("name")); + assertEquals(input.get("version"), decodedMap.get("version")); + assertEquals(input.get("active"), decodedMap.get("active")); + + Map decodedMetadata = (Map) decodedMap.get("metadata"); + assertEquals("dev", decodedMetadata.get("author")); + assertEquals(100L, ((Number) decodedMetadata.get("stars")).longValue()); + + return null; + })); + } + + for (Future future : futures) { + future.get(); + } + + executor.shutdown(); + if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } + + @Test + @DisplayName("Should handle different objects concurrently without interference") + void concurrentDifferentObjects() throws InterruptedException, ExecutionException { + int threadCount = 10; + int iterations = 1000; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + List> futures = new ArrayList<>(); + + for (int i = 0; i < iterations; i++) { + final int index = i; + futures.add(executor.submit(() -> { + Map obj = Map.of("key", "value" + index); + String encoded = JToon.encode(obj); + Map decoded = (Map) JToon.decode(encoded); + assertEquals("value" + index, decoded.get("key")); + return null; + })); + } + + for (Future future : futures) { + future.get(); + } + + executor.shutdown(); + } +} diff --git a/src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java b/src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java new file mode 100644 index 0000000..375b18c --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java @@ -0,0 +1,82 @@ +package dev.toonformat.jtoon.decoder; + +import dev.toonformat.jtoon.DecodeOptions; +import dev.toonformat.jtoon.JToon; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +class DecodeContextRaceConditionTest { + + @Test + @DisplayName("Should be thread-safe when decoding multiple inputs concurrently") + void concurrentDecoding() throws InterruptedException, ExecutionException { + int threadCount = 20; + int iterationsPerThread = 100; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + String toonInput = "name: JToon\n" + + "version: 1.0.0\n" + + "tags[3]:\n" + + " - java\n" + + " - json\n" + + " - toon\n" + + "metadata:\n" + + " author: dev\n" + + " active: true"; + + List> futures = new ArrayList<>(); + + for (int i = 0; i < threadCount * iterationsPerThread; i++) { + futures.add(executor.submit(() -> JToon.decode(toonInput))); + } + + for (Future future : futures) { + Object result = future.get(); + if (!(result instanceof Map)) { + fail("Result should be a Map"); + } + Map map = (Map) result; + assertEquals("JToon", map.get("name")); + assertEquals("1.0.0", map.get("version")); + assertEquals(true, ((Map) map.get("metadata")).get("active")); + } + + executor.shutdown(); + if (!executor.awaitTermination(10, TimeUnit.SECONDS)) { + executor.shutdownNow(); + } + } + + @Test + @DisplayName("Should handle different inputs concurrently without interference") + void concurrentDifferentInputs() throws InterruptedException, ExecutionException { + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + + List> futures = new ArrayList<>(); + + for (int i = 0; i < 1000; i++) { + final int index = i; + futures.add(executor.submit(() -> { + String input = "key: value" + index; + Map result = (Map) JToon.decode(input); + assertEquals("value" + index, result.get("key")); + return null; + })); + } + + for (Future future : futures) { + future.get(); + } + + executor.shutdown(); + } +} diff --git a/src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderThreadSafetyTest.java b/src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderThreadSafetyTest.java new file mode 100644 index 0000000..687813b --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderThreadSafetyTest.java @@ -0,0 +1,43 @@ +package dev.toonformat.jtoon.decoder; + +import dev.toonformat.jtoon.DecodeOptions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Execution(ExecutionMode.CONCURRENT) +class ValueDecoderThreadSafetyTest { + + @RepeatedTest(100) + @DisplayName("ValueDecoder should be thread-safe when decoding TOON strings") + void decodeThreadSafety() { + // Given + String id = UUID.randomUUID().toString(); + String toon = "id: " + id + "\n" + + "tags[3]: a, b, c\n" + + "meta:\n" + + " active: true\n" + + " score: 42"; + + // When + Object decoded = ValueDecoder.decode(toon, DecodeOptions.DEFAULT); + + // Then + assertTrue(decoded instanceof Map); + Map map = (Map) decoded; + assertEquals(id, map.get("id")); + assertEquals(List.of("a", "b", "c"), map.get("tags")); + + Map meta = (Map) map.get("meta"); + assertEquals(true, meta.get("active")); + assertEquals(42L, meta.get("score")); + } +} diff --git a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java new file mode 100644 index 0000000..c9c3f25 --- /dev/null +++ b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java @@ -0,0 +1,57 @@ +package dev.toonformat.jtoon.normalizer; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import tools.jackson.databind.JsonNode; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Execution(ExecutionMode.CONCURRENT) +class JsonNormalizerThreadSafetyTest { + + @RepeatedTest(100) + @DisplayName("JsonNormalizer should be thread-safe when normalizing complex objects") + void normalizeThreadSafety() { + // Given + String id = UUID.randomUUID().toString(); + Map input = Map.of( + "id", id, + "timestamp", LocalDateTime.now(), + "tags", List.of("a", "b", "c"), + "nested", Map.of("key", "value") + ); + + // When + JsonNode normalized = JsonNormalizer.normalize(input); + + // Then + assertNotNull(normalized); + assertTrue(normalized.isObject()); + assertTrue(normalized.has("id")); + assertTrue(normalized.get("id").asText().equals(id)); + } + + @RepeatedTest(100) + @DisplayName("JsonNormalizer should be thread-safe when parsing JSON strings") + void parseThreadSafety() { + // Given + String id = UUID.randomUUID().toString(); + String json = "{\"id\":\"" + id + "\",\"active\":true}"; + + // When + JsonNode parsed = JsonNormalizer.parse(json); + + // Then + assertNotNull(parsed); + assertTrue(parsed.isObject()); + assertTrue(parsed.get("id").asText().equals(id)); + } +} From 8243f8cfe7184bf480285812132f8c779a68c9d6 Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Wed, 28 Jan 2026 17:35:46 +0100 Subject: [PATCH 6/7] cleaup RaceCondition test --- .../DecodeContextRaceConditionTest.java | 33 +++++++++++-------- .../decoder/ValueDecoderThreadSafetyTest.java | 14 ++++---- .../JsonNormalizerThreadSafetyTest.java | 7 ++-- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java b/src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java index 375b18c..c86aa32 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/DecodeContextRaceConditionTest.java @@ -1,6 +1,5 @@ package dev.toonformat.jtoon.decoder; -import dev.toonformat.jtoon.DecodeOptions; import dev.toonformat.jtoon.JToon; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -8,7 +7,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.*; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -22,17 +25,19 @@ void concurrentDecoding() throws InterruptedException, ExecutionException { int iterationsPerThread = 100; ExecutorService executor = Executors.newFixedThreadPool(threadCount); - String toonInput = "name: JToon\n" + - "version: 1.0.0\n" + - "tags[3]:\n" + - " - java\n" + - " - json\n" + - " - toon\n" + - "metadata:\n" + - " author: dev\n" + - " active: true"; + String toonInput = new StringBuilder() + .append("name: JToon\n") + .append("version: 1.0.0\n") + .append("tags[3]:\n") + .append(" - java\n") + .append(" - json\n") + .append(" - toon\n") + .append("metadata:\n") + .append(" author: dev\n") + .append(" active: true") + .toString(); - List> futures = new ArrayList<>(); + final List> futures = new ArrayList<>(); for (int i = 0; i < threadCount * iterationsPerThread; i++) { futures.add(executor.submit(() -> JToon.decode(toonInput))); @@ -60,8 +65,8 @@ void concurrentDecoding() throws InterruptedException, ExecutionException { void concurrentDifferentInputs() throws InterruptedException, ExecutionException { int threadCount = 10; ExecutorService executor = Executors.newFixedThreadPool(threadCount); - - List> futures = new ArrayList<>(); + + final List> futures = new ArrayList<>(); for (int i = 0; i < 1000; i++) { final int index = i; diff --git a/src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderThreadSafetyTest.java b/src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderThreadSafetyTest.java index 687813b..ef4c902 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderThreadSafetyTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/ValueDecoderThreadSafetyTest.java @@ -11,7 +11,7 @@ import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; @Execution(ExecutionMode.CONCURRENT) class ValueDecoderThreadSafetyTest { @@ -22,20 +22,20 @@ void decodeThreadSafety() { // Given String id = UUID.randomUUID().toString(); String toon = "id: " + id + "\n" + - "tags[3]: a, b, c\n" + - "meta:\n" + - " active: true\n" + - " score: 42"; + "tags[3]: a, b, c\n" + + "meta:\n" + + " active: true\n" + + " score: 42"; // When Object decoded = ValueDecoder.decode(toon, DecodeOptions.DEFAULT); // Then - assertTrue(decoded instanceof Map); + assertInstanceOf(Map.class, decoded); Map map = (Map) decoded; assertEquals(id, map.get("id")); assertEquals(List.of("a", "b", "c"), map.get("tags")); - + Map meta = (Map) map.get("meta"); assertEquals(true, meta.get("active")); assertEquals(42L, meta.get("score")); diff --git a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java index c9c3f25..db9fa9c 100644 --- a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java +++ b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerThreadSafetyTest.java @@ -11,8 +11,7 @@ import java.util.Map; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; @Execution(ExecutionMode.CONCURRENT) class JsonNormalizerThreadSafetyTest { @@ -36,7 +35,7 @@ void normalizeThreadSafety() { assertNotNull(normalized); assertTrue(normalized.isObject()); assertTrue(normalized.has("id")); - assertTrue(normalized.get("id").asText().equals(id)); + assertEquals(normalized.get("id").asString(), id); } @RepeatedTest(100) @@ -52,6 +51,6 @@ void parseThreadSafety() { // Then assertNotNull(parsed); assertTrue(parsed.isObject()); - assertTrue(parsed.get("id").asText().equals(id)); + assertEquals(parsed.get("id").asString(), id); } } From 74ab1d26510217838c1ace89f644c17674a02cea Mon Sep 17 00:00:00 2001 From: Jens Papenhagen Date: Wed, 28 Jan 2026 17:40:09 +0100 Subject: [PATCH 7/7] remove warnings form spotbugs --- .../java/dev/toonformat/jtoon/TestPojos.java | 10 +++- .../jtoon/decoder/KeyDecoderTest.java | 60 +++++++++++++++++++ .../jtoon/encoder/ListItemEncoderTest.java | 2 - .../jtoon/normalizer/JsonNormalizerTest.java | 9 +-- 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/test/java/dev/toonformat/jtoon/TestPojos.java b/src/test/java/dev/toonformat/jtoon/TestPojos.java index 814112a..ac76d32 100644 --- a/src/test/java/dev/toonformat/jtoon/TestPojos.java +++ b/src/test/java/dev/toonformat/jtoon/TestPojos.java @@ -143,7 +143,7 @@ public record OrderEmployee(String name, int id, Address address) { * Class with Jackson Annotations */ public static class FullEmployee { - public AnnotatedEmployee employee; + public final AnnotatedEmployee employee; private final Map properties; public FullEmployee(AnnotatedEmployee employee, Map properties) { @@ -155,6 +155,10 @@ public FullEmployee(AnnotatedEmployee employee, Map properties) public Map getProperties() { return properties; } + + public AnnotatedEmployee employee() { + return employee; + } } /** @@ -169,7 +173,9 @@ public record HotelInfoLlmRerankDTO(String no, String hotelAddressDistance) { } - public record UserDTO(Integer id, String firstName, String lastName, java.sql.Date lastLogin) {} + public record UserDTO(Integer id, String firstName, String lastName, java.sql.Date lastLogin) { + + } /** * Custom Serializer for HotelInfoLlmRerankDTO diff --git a/src/test/java/dev/toonformat/jtoon/decoder/KeyDecoderTest.java b/src/test/java/dev/toonformat/jtoon/decoder/KeyDecoderTest.java index 5c231ed..09c03df 100644 --- a/src/test/java/dev/toonformat/jtoon/decoder/KeyDecoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/decoder/KeyDecoderTest.java @@ -156,6 +156,66 @@ void testCallsExpandPathIntoMapWhenShouldExpandKeyTrue() { // Then Map expectedNestedMap = new LinkedHashMap<>(); expectedNestedMap.put("bar", expectedArray); + assertNull(result.get("bar")); + assertEquals(result.size(), expectedNestedMap.size()); + } + + @Test + @DisplayName("Given basic keyed array line When processed Then value is placed in map") + void processKeyedArrayLine_givenBasicKeyedArray_whenProcessed_thenValueInMap() { + // Given + Map result = new LinkedHashMap<>(); + String content = "tags[3]: a, b, c"; + String originalKey = "tags"; + DecodeContext context = new DecodeContext(); + context.options = new DecodeOptions(2, Delimiter.COMMA, true, PathExpansion.OFF); + context.delimiter = Delimiter.COMMA; + + // When + KeyDecoder.processKeyedArrayLine(result, content, originalKey, 0, context); + + // Then + List expected = Arrays.asList("a", "b", "c"); + assertEquals(expected, result.get("tags")); + } + + @Test + @DisplayName("Given dotted keyed array line and SAFE expansion When processed Then value is placed in nested map") + void processKeyedArrayLine_givenDottedKeyedArray_whenProcessed_thenNestedMapContainsValue() { + // Given + Map result = new LinkedHashMap<>(); + String content = "user.tags[2]: dev, test"; + String originalKey = "user.tags"; + DecodeContext context = new DecodeContext(); + context.options = new DecodeOptions(2, Delimiter.COMMA, true, PathExpansion.SAFE); + context.delimiter = Delimiter.COMMA; + + // When + KeyDecoder.processKeyedArrayLine(result, content, originalKey, 0, context); + + // Then + assertTrue(result.containsKey("user")); + @SuppressWarnings("unchecked") Map user = (Map) result.get("user"); + List expected = Arrays.asList("dev", "test"); + assertEquals(expected, user.get("tags")); + } + + @Test + @DisplayName("Given dotted keyed array line and expansion conflict in strict mode When processed Then throws exception") + void processKeyedArrayLine_givenExpansionConflictStrict_whenProcessed_thenThrowsException() { + // Given + Map result = new LinkedHashMap<>(); + result.put("user", "not-a-map"); + String content = "user.tags[1]: dev"; + String originalKey = "user.tags"; + DecodeContext context = new DecodeContext(); + context.options = new DecodeOptions(2, Delimiter.COMMA, true, PathExpansion.SAFE); + context.delimiter = Delimiter.COMMA; + + // When / Then + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> KeyDecoder.processKeyedArrayLine(result, content, originalKey, 0, context)); + assertTrue(ex.getMessage().contains("Path expansion conflict")); } @Test diff --git a/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java b/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java index 8a962d2..00f7586 100644 --- a/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java +++ b/src/test/java/dev/toonformat/jtoon/encoder/ListItemEncoderTest.java @@ -128,7 +128,6 @@ void usesTabularFormatForNestedUniformObjectArrays() { EncodeOptions options = EncodeOptions.DEFAULT; LineWriter writer = new LineWriter(options.indent()); - Set rootKeys = new HashSet<>(); // When ArrayEncoder.encodeArray("items",node, writer, 0, options); @@ -153,7 +152,6 @@ void usesListFormatForNestedObjectArraysWithMismatchedKeys() { EncodeOptions options = EncodeOptions.DEFAULT; LineWriter writer = new LineWriter(options.indent()); - Set rootKeys = new HashSet<>(); // When ArrayEncoder.encodeArray("items", node, writer, 0, options); diff --git a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java index ffb64c3..376cd25 100644 --- a/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java +++ b/src/test/java/dev/toonformat/jtoon/normalizer/JsonNormalizerTest.java @@ -1075,14 +1075,7 @@ void testStreamWithNulls() { @DisplayName("POJOs") class POJOs { - static class SimplePojo { - public String name; - public int age; - - SimplePojo(String name, int age) { - this.name = name; - this.age = age; - } + record SimplePojo(String name, int age) { } record PojoWithGetters(String value) {