diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/block/SpawnerCacheTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/block/SpawnerCacheTest.java new file mode 100755 index 00000000..d3a94792 --- /dev/null +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/block/SpawnerCacheTest.java @@ -0,0 +1,143 @@ +package dev.loki.lovisual.features.module.modules.misc.optimize.block; + +import dev.loki.lovisual.render.engine.optimize.OptimizeToggles; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies the {@link SpawnerCache} lookup logic. We can't construct a real + * {@code BaseSpawner} in the test JVM, so the test uses a hand-rolled mock + * that implements {@link TickCached} directly — the cache only relies on the + * interface, never on the {@code BaseSpawner} class itself. + */ +public final class SpawnerCacheTest { + + /** Minimal TickCached implementation for testing. */ + private static final class FakeSpawner implements TickCached { + Object cached; + long cachedTick = Long.MIN_VALUE; + + @Override + public Object lovisual$cached() { + return cached; + } + + @Override + public long lovisual$cachedTick() { + return cachedTick; + } + + @Override + public void lovisual$setCached(Object value, long tick) { + this.cached = value; + this.cachedTick = tick; + } + } + + @AfterEach + void resetToggles() { + OptimizeToggles.reset(); + } + + @Test + void enabledReflectsOptimizeToggles() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, false, false, false); + assertFalse(SpawnerCache.enabled()); + + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + true, false, false, false); + assertTrue(SpawnerCache.enabled()); + + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, false, false, false); + assertFalse(SpawnerCache.enabled()); + } + + @Test + void getReturnsNullForEmptyCache() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + true, false, false, false); + FakeSpawner spawner = new FakeSpawner(); + assertNull(SpawnerCache.get(spawner, 100L)); + } + + @Test + void putThenGetReturnsCachedValueForSameTick() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + true, false, false, false); + FakeSpawner spawner = new FakeSpawner(); + EntityRenderState state = new EntityRenderState(); + SpawnerCache.put(spawner, 42L, state); + assertSame(state, SpawnerCache.get(spawner, 42L)); + } + + @Test + void getReturnsNullForDifferentTick() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + true, false, false, false); + FakeSpawner spawner = new FakeSpawner(); + EntityRenderState state = new EntityRenderState(); + SpawnerCache.put(spawner, 42L, state); + assertNull(SpawnerCache.get(spawner, 43L)); + assertNull(SpawnerCache.get(spawner, 41L)); + } + + @Test + void putOverwritesPreviousCache() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + true, false, false, false); + FakeSpawner spawner = new FakeSpawner(); + EntityRenderState first = new EntityRenderState(); + EntityRenderState second = new EntityRenderState(); + SpawnerCache.put(spawner, 1L, first); + SpawnerCache.put(spawner, 2L, second); + assertSame(second, SpawnerCache.get(spawner, 2L)); + assertNull(SpawnerCache.get(spawner, 1L)); + } + + @Test + void cachedValuePersistsAcrossMultipleGets() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + true, false, false, false); + FakeSpawner spawner = new FakeSpawner(); + EntityRenderState state = new EntityRenderState(); + SpawnerCache.put(spawner, 100L, state); + // Multiple reads should all return the same value (no eviction). + for (int i = 0; i < 5; i++) { + assertSame(state, SpawnerCache.get(spawner, 100L)); + } + assertEquals(100L, spawner.lovisual$cachedTick()); + } +} diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/core/LruMapTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/core/LruMapTest.java new file mode 100755 index 00000000..814db4d6 --- /dev/null +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/core/LruMapTest.java @@ -0,0 +1,74 @@ +package dev.loki.lovisual.features.module.modules.misc.optimize.core; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class LruMapTest { + + @Test + void accessOrderIsRespected() { + LruMap map = new LruMap<>(3); + map.put("a", 1); + map.put("b", 2); + map.put("c", 3); + // Touch "a" — it should now be the most-recently-used. + Integer touched = map.get("a"); + assertEquals(1, touched); + // Adding a fourth entry evicts the LRU, which is "b" (since "a" was just touched and "c" is newest). + map.put("d", 4); + assertNull(map.get("b")); + assertTrue(map.containsKey("a")); + assertTrue(map.containsKey("c")); + assertTrue(map.containsKey("d")); + } + + @Test + void capIsMaxSizePlusOne() { + // removeEldestEntry fires when size > maxSize, so we hold up to maxSize entries after the next put. + LruMap map = new LruMap<>(2); + map.put(1, "one"); + map.put(2, "two"); + map.put(3, "three"); + assertEquals(2, map.size()); + assertNull(map.get(1)); + assertEquals("two", map.get(2)); + assertEquals("three", map.get(3)); + } + + @Test + void maxSizeAccessorMatchesConstructor() { + LruMap map = new LruMap<>(1024); + assertEquals(1024, map.maxSize()); + } + + @Test + void iterationOrderFollowsAccessOrder() { + LruMap map = new LruMap<>(5); + map.put("a", 1); + map.put("b", 2); + map.put("c", 3); + map.get("a"); + List keys = new ArrayList<>(map.keySet()); + // After touching "a", iteration order should be b, c, a. + assertEquals(List.of("b", "c", "a"), keys); + } + + @Test + void putReplacingExistingKeyDoesNotEvict() { + LruMap map = new LruMap<>(2); + map.put("a", 1); + map.put("b", 2); + map.put("a", 100); + assertEquals(2, map.size()); + assertEquals(100, map.get("a")); + // "b" should still be present — replacing a key's value doesn't grow size. + assertSame(2, map.get("b")); + } +} diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/entity/CachedStackTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/entity/CachedStackTest.java new file mode 100755 index 00000000..6e4f2a37 --- /dev/null +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/entity/CachedStackTest.java @@ -0,0 +1,92 @@ +package dev.loki.lovisual.features.module.modules.misc.optimize.entity; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.lwjgl.system.MemoryStack; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Verifies {@link CachedStack} — the ThreadLocal-free MemoryStack push helper. + * + *

Tests run on the test JVM's main thread; the first call populates the + * cache, subsequent calls from the same thread take the fast path. + */ +public final class CachedStackTest { + + @AfterEach + void reset() { + // The cache is a single volatile — for the next test we just push something + // from this thread again so cachedThread() returns to the current thread. + // (Cross-test isolation isn't perfect because the cache is global, but + // the tests below assert observable invariants rather than internal state.) + CachedStack.push().close(); + } + + @Test + void pushReturnsValidStack() { + try (MemoryStack stack = CachedStack.push()) { + assertNotNull(stack); + // A sanity check that the returned stack actually works. + long ptr = stack.nmalloc(8); + assertNotSame(0L, ptr); + } + } + + @Test + void pushTwiceFromSameThreadReturnsSameStack() { + MemoryStack first; + try (MemoryStack s = CachedStack.push()) { + first = s; + } + MemoryStack second; + try (MemoryStack s = CachedStack.push()) { + second = s; + } + assertSame(first, second, + "Same-thread push should return the same underlying MemoryStack instance"); + assertEquals(Thread.currentThread(), CachedStack.cachedThread()); + } + + @Test + void pushFromDifferentThreadUpdatesCachedThread() throws InterruptedException { + // First call from this thread to populate the cache. + try (MemoryStack s = CachedStack.push()) { + assertNotNull(s); + } + assertEquals(Thread.currentThread(), CachedStack.cachedThread()); + + // Now push from a different thread — the cache should switch. + Thread other = new Thread(() -> { + try (MemoryStack s = CachedStack.push()) { + assertNotNull(s); + } + }); + other.start(); + other.join(); + // After the other thread pushed, the cached thread is the other thread. + assertEquals(other, CachedStack.cachedThread()); + assertNotNull(CachedStack.cachedStack()); + + // Restore the cache to this thread for the next test. + try (MemoryStack s = CachedStack.push()) { + assertNotNull(s); + } + assertEquals(Thread.currentThread(), CachedStack.cachedThread()); + } + + @Test + void nestedPushesWorkLikeVanillaStack() { + // Vanilla MemoryStack supports nested stackPush/stackClose — CachedStack should too. + try (MemoryStack outer = CachedStack.push()) { + long outerPtr = outer.nmalloc(8); + try (MemoryStack inner = CachedStack.push()) { + long innerPtr = inner.nmalloc(8); + assertNotSame(outerPtr, innerPtr); + } + } + } +} diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/entity/EntityLodTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/entity/EntityLodTest.java new file mode 100755 index 00000000..7fba85db --- /dev/null +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/entity/EntityLodTest.java @@ -0,0 +1,87 @@ +package dev.loki.lovisual.features.module.modules.misc.optimize.entity; + +import dev.loki.lovisual.render.engine.optimize.OptimizeToggles; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class EntityLodTest { + + @AfterEach + void resetToggles() { + OptimizeToggles.reset(); + } + + @Test + void farReturnsFalseWhenLodDisabled() { + OptimizeToggles.apply( + false, 35, + false, false, false, + false, 0, + false, false, false, false); + assertFalse(EntityLod.far(10_000_000)); + assertFalse(EntityLod.farPlayer(10_000_000)); + } + + @Test + void farComparesSquaredDistance() { + OptimizeToggles.apply( + true, 35, + false, false, false, + false, 0, + false, false, false, false); + // d=35 -> d²=1225: 1225 is NOT greater than 1225, so not far. + assertFalse(EntityLod.far(35 * 35)); + // d² just above threshold: should be far. + assertTrue(EntityLod.far(35 * 35 + 1)); + // very close: not far. + assertFalse(EntityLod.far(0)); + assertFalse(EntityLod.far(1)); + } + + @Test + void farThresholdSqMatchesDistance() { + OptimizeToggles.apply( + true, 16, + false, false, false, + false, 0, + false, false, false, false); + assertEquals(16 * 16, EntityLod.farThresholdSq(), 0.0001); + assertFalse(EntityLod.far(16 * 16)); + assertTrue(EntityLod.far(16 * 16 + 1)); + } + + @Test + void farPlayerMatchesFarInStage1() { + OptimizeToggles.apply( + true, 35, + false, false, false, + false, 0, + false, false, false, false); + // Stage 1 collapses player LOD into entity LOD — same toggle, same threshold. + for (double dSq : new double[]{0, 100, 1224, 1226, 10_000}) { + assertEquals(EntityLod.far(dSq), EntityLod.farPlayer(dSq), + "far and farPlayer disagree at dSq=" + dSq); + } + } + + @Test + void farRespectsRuntimeToggleChanges() { + OptimizeToggles.apply( + true, 35, + false, false, false, + false, 0, + false, false, false, false); + assertTrue(EntityLod.far(10_000)); + // Toggle off mid-test — same distance should now be "not far". + OptimizeToggles.apply( + false, 35, + false, false, false, + false, 0, + false, false, false, false); + assertFalse(EntityLod.far(10_000)); + } +} diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/hud/HudWorthTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/hud/HudWorthTest.java new file mode 100755 index 00000000..9066d0be --- /dev/null +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/hud/HudWorthTest.java @@ -0,0 +1,100 @@ +package dev.loki.lovisual.features.module.modules.misc.optimize.hud; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class HudWorthTest { + + @AfterEach + void reset() { + HudWorth.reset(); + } + + @Test + void worthStartsTrue() { + assertTrue(HudWorth.worth()); + } + + @Test + void worthStaysTrueWhenFramesPerRefreshIsHigh() { + // Frame time 5 ms, refresh 16.6 ms (60 Hz) -> ~3.3 frames per refresh -> well above ON_ABOVE (2.0). + // Should stay "worth". + long refreshNs = 16_600_000L; + long now = 0; + for (int i = 0; i < 50; i++) { + now += 5_000_000L; + assertTrue(HudWorth.update(now, refreshNs)); + } + assertTrue(HudWorth.worth()); + } + + @Test + void worthFlipsToFalseWhenFramesPerRefreshDropsBelowHysteresis() { + // Frame time ~= refresh time -> ~1 frame per refresh -> below OFF_BELOW (1.5). + long refreshNs = 16_600_000L; + long now = 0; + // Warm up the EMA at the high frame rate (worth stays true). + for (int i = 0; i < 50; i++) { + now += 5_000_000L; + HudWorth.update(now, refreshNs); + } + assertTrue(HudWorth.worth()); + // Now drop the frame rate to one frame per refresh. + for (int i = 0; i < 50; i++) { + now += 16_600_000L; + HudWorth.update(now, refreshNs); + } + assertFalse(HudWorth.worth()); + } + + @Test + void hysteresisAvoidsFlickerNearTheThreshold() { + // Sawtooth between 1.4 and 1.6 frames-per-refresh should NOT cause worth to flip back and forth. + // (Default initial state is worth=true, so 1.4 will drop it; the test asserts it stays dropped + // rather than flickering on every 1.6 frame.) + long refreshNs = 16_600_000L; + long now = 0; + // Drop to ~1.4 frames per refresh first (16.6 / ~12ms = 1.38). + for (int i = 0; i < 60; i++) { + now += 12_000_000L; + HudWorth.update(now, refreshNs); + } + assertFalse(HudWorth.worth()); + // A few short frames (~10ms => 1.66 fps/refresh) should NOT flip worth back on (below ON_ABOVE=2.0). + for (int i = 0; i < 5; i++) { + now += 10_000_000L; + HudWorth.update(now, refreshNs); + } + assertFalse(HudWorth.worth()); + // A longer sustained high frame rate (5ms => 3.32 fps/refresh) flips it back on. + for (int i = 0; i < 60; i++) { + now += 5_000_000L; + HudWorth.update(now, refreshNs); + } + assertTrue(HudWorth.worth()); + } + + @Test + void updateIgnoresExtremeGaps() { + // A 2-second gap (paused or main-thread stall) should NOT poison the EMA. + long refreshNs = 16_600_000L; + long now = 0; + for (int i = 0; i < 30; i++) { + now += 5_000_000L; + HudWorth.update(now, refreshNs); + } + double beforeGap = HudWorth.frameNs(); + now += 2_000_000_000L; // 2 s gap + HudWorth.update(now, refreshNs); + // EMA should not have jumped to the gap value (the guard rejects dt >= 1e9 ns). + // It may have moved slightly due to the legitimate 5 ms frames, but not to ~2 s. + assertTrue(HudWorth.frameNs() < 100_000_000L, + "EMA should not absorb a 2-second pause, frameNs=" + HudWorth.frameNs()); + assertTrue(HudWorth.frameNs() > 0, "EMA should be positive after warmup"); + // Sanity: the gap reading didn't blow up frameNs to near 2 seconds. + assertTrue(HudWorth.frameNs() < beforeGap * 2 + 1_000_000L); + } +} diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/ParticleLodTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/ParticleLodTest.java new file mode 100755 index 00000000..5748353c --- /dev/null +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/ParticleLodTest.java @@ -0,0 +1,166 @@ +package dev.loki.lovisual.features.module.modules.misc.optimize.particle; + +import dev.loki.lovisual.render.engine.optimize.OptimizeToggles; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class ParticleLodTest { + + @AfterEach + void resetToggles() { + OptimizeToggles.reset(); + } + + @Test + void keepAllWhenLodDisabled() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, false, false, false); + Object p = new Object(); + assertTrue(ParticleLod.keep(p, 0)); + assertTrue(ParticleLod.keep(p, 100)); + assertTrue(ParticleLod.keep(p, 10_000)); + } + + @Test + void keepAllWhenNear() { + OptimizeToggles.apply( + false, 0, + true, false, false, + false, 0, + false, false, false, false); + Object p = new Object(); + // Strictly under 16 blocks: always keep (NEAR_SQ = 16² = 256). + assertTrue(ParticleLod.keep(p, 0)); + assertTrue(ParticleLod.keep(p, 16 * 16 - 1)); + assertTrue(ParticleLod.keep(p, 255)); + assertTrue(ParticleLod.keep(p, 255.999)); + } + + @Test + void exactlyAtSixteenBlocksFallsThroughToMidRange() { + OptimizeToggles.apply( + false, 0, + true, false, false, + false, 0, + false, false, false, false); + // At exactly 16² (the boundary), the particle is no longer "near" — its + // keep decision is delegated to the mid-range thinning. Over many + // particles, about half should survive. + int kept = 0; + for (int i = 0; i < 1024; i++) { + Object p = new Object(); + if (ParticleLod.keep(p, 16 * 16)) kept++; + } + assertTrue(kept >= 410 && kept <= 614, + "At 16² (boundary), about half should survive thinning, got " + kept + "/1024"); + } + + @Test + void thinningAtMidRangeKeepsAboutHalf() { + OptimizeToggles.apply( + false, 0, + true, false, false, + false, 0, + false, false, false, false); + double midSq = 24 * 24; // Between 16² and 32². + Set seen = new HashSet<>(); + for (int i = 0; i < 256; i++) { + Object p = new Object(); + seen.add(ParticleLod.keep(p, midSq)); + } + // We should see both keep and drop, with keep being roughly half (60-100). + assertTrue(seen.contains(true) && seen.contains(false), + "Mid-range thinning should keep some and drop some over 256 trials"); + } + + @Test + void thinningAtFarRangeKeepsAboutQuarter() { + OptimizeToggles.apply( + false, 0, + true, false, false, + false, 0, + false, false, false, false); + double farSq = 48 * 48; // Beyond 32². + int kept = 0; + for (int i = 0; i < 1024; i++) { + Object p = new Object(); + if (ParticleLod.keep(p, farSq)) kept++; + } + // Expect roughly 256 (1024/4). Allow a wide band (200-320) because identityHashCode is not perfectly uniform. + assertTrue(kept >= 200 && kept <= 320, + "Far-range thinning should keep roughly 1/4 of particles, got " + kept + "/1024"); + } + + @Test + void sameParticleKeepsStableChoiceAcrossCalls() { + OptimizeToggles.apply( + false, 0, + true, false, false, + false, 0, + false, false, false, false); + Object p = new Object(); + double farSq = 48 * 48; + boolean first = ParticleLod.keep(p, farSq); + for (int i = 0; i < 16; i++) { + assertTrue(first == ParticleLod.keep(p, farSq), + "Same particle should get the same keep decision every call"); + } + } + + @Test + void primitiveOverloadMatchesObjectOverload() { + OptimizeToggles.apply( + false, 0, + true, false, false, + false, 0, + false, false, false, false); + Object p = new Object(); + // particle at (50, 0, 0), camera at (0, 0, 0): distance² = 2500. + boolean objectApi = ParticleLod.keep(p, 50 * 50); + boolean primitiveApi = ParticleLod.keep(p, 50, 0, 0, 0, 0, 0); + assertTrue(objectApi == primitiveApi, + "Primitive overload should agree with object overload at same distance"); + } + + @Test + void nearAlwaysKeepsEvenWhenParticleLodOn() { + OptimizeToggles.apply( + false, 0, + true, false, false, + false, 0, + false, false, false, false); + Object p = new Object(); + assertTrue(ParticleLod.keep(p, 15 * 15)); + assertTrue(ParticleLod.keep(p, 0)); + } + + @Test + void disablingParticleLodMidTestFlipsEverythingToKeep() { + OptimizeToggles.apply( + false, 0, + true, false, false, + false, 0, + false, false, false, false); + Object p = new Object(); + boolean first = ParticleLod.keep(p, 48 * 48); + // Toggle off — every distance should now keep. + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, false, false, false); + assertTrue(ParticleLod.keep(p, 48 * 48)); + assertTrue(ParticleLod.keep(p, 0)); + // first is just whatever it was — we only care that the new state is "keep everything". + assertFalse(first == false && first != false, "dummy assertion to mark first read"); + } +} diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/WorkersTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/WorkersTest.java new file mode 100755 index 00000000..b9678421 --- /dev/null +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/particle/WorkersTest.java @@ -0,0 +1,85 @@ +package dev.loki.lovisual.features.module.modules.misc.optimize.particle; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class WorkersTest { + + @AfterAll + static void shutdown() { + Workers.shutdown(); + } + + @Test + void runDispatchesAllBlocks() { + int blocks = 32; + AtomicInteger counter = new AtomicInteger(); + Throwable err = Workers.run(blocks, b -> counter.incrementAndGet()); + assertNull(err); + assertEquals(blocks, counter.get()); + } + + @Test + void zeroBlocksIsNoop() { + AtomicInteger counter = new AtomicInteger(); + Throwable err = Workers.run(0, b -> counter.incrementAndGet()); + assertNull(err); + assertEquals(0, counter.get()); + } + + @Test + void negativeBlocksIsNoop() { + AtomicInteger counter = new AtomicInteger(); + Throwable err = Workers.run(-1, b -> counter.incrementAndGet()); + assertNull(err); + assertEquals(0, counter.get()); + } + + @Test + void firstFailureIsReturnedAndRemainingBlocksShortCircuit() { + int blocks = 16; + AtomicInteger completed = new AtomicInteger(); + Throwable err = Workers.run(blocks, b -> { + if (b == 3) { + throw new IllegalStateException("block 3 failed"); + } + completed.incrementAndGet(); + }); + assertNotNull(err); + assertTrue(err instanceof IllegalStateException, "error should be IllegalStateException, got " + err.getClass()); + assertEquals("block 3 failed", err.getMessage()); + // Some other blocks may have run before block 3 errored, but no more than the full count. + assertTrue(completed.get() <= blocks); + } + + @Test + void poolSizeIsAtLeastOne() { + assertTrue(Workers.poolSize() >= 1, "pool size should be at least 1, got " + Workers.poolSize()); + assertTrue(Workers.poolSize() <= 8, "pool size should be at most 8, got " + Workers.poolSize()); + } + + @Test + void parallelWorkCompletesAndCounterMatches() { + int blocks = 64; + AtomicInteger counter = new AtomicInteger(); + Throwable err = Workers.run(blocks, b -> { + // Simulate a tiny unit of work. Swallow InterruptedException so the + // IntConsumer stays compatible with Workers.run's signature. + try { + Thread.sleep(1); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + counter.incrementAndGet(); + }); + assertNull(err); + assertEquals(blocks, counter.get()); + } +} diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/text/StableTextTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/text/StableTextTest.java new file mode 100755 index 00000000..b97f686a --- /dev/null +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/misc/optimize/text/StableTextTest.java @@ -0,0 +1,127 @@ +package dev.loki.lovisual.features.module.modules.misc.optimize.text; + +import dev.loki.lovisual.render.engine.optimize.OptimizeToggles; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.FormattedCharSink; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class StableTextTest { + + /** A trivial FormattedCharSequence that just records whether accept() was called. */ + private static final class Recorder implements FormattedCharSequence { + boolean accepted; + + @Override + public boolean accept(FormattedCharSink sink) { + accepted = true; + return true; + } + } + + @AfterEach + void resetTogglesAndTracked() { + OptimizeToggles.reset(); + StableText.clearTracked(); + } + + @Test + void markReturnsStableTextInstance() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, false, false, false); + Recorder inner = new Recorder(); + FormattedCharSequence marked = StableText.mark(inner); + assertTrue(marked instanceof StableText, + "mark() should return a StableText wrapper"); + assertEquals(1, StableText.trackedCount()); + } + + @Test + void isStableReturnsTrueForMarkedText() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, false, false, false); + Recorder inner = new Recorder(); + FormattedCharSequence marked = StableText.mark(inner); + assertTrue(StableText.isStable(marked)); + } + + @Test + void isStableReturnsFalseForUnmarkedText() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, false, false, false); + Recorder plain = new Recorder(); + assertFalse(StableText.isStable(plain)); + } + + @Test + void isStableFastPathUsedWhenMicroOptsOn() { + // When microOpts is on, isStable uses instanceof — should return true for StableText + // wrappers even after clearing the tracked set (the set isn't consulted on the fast path). + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, false, false, true); + Recorder inner = new Recorder(); + FormattedCharSequence marked = StableText.mark(inner); + StableText.clearTracked(); + // Fast path: instanceof check, doesn't consult the set. + assertTrue(StableText.isStable(marked)); + } + + @Test + void isStableSlowPathUsedWhenMicroOptsOff() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, false, false, false); + Recorder inner = new Recorder(); + FormattedCharSequence marked = StableText.mark(inner); + assertTrue(StableText.isStable(marked)); + StableText.clearTracked(); + // Slow path: after clearing the set, the wrapper is no longer "known stable". + assertFalse(StableText.isStable(marked)); + } + + @Test + void acceptDelegatesToInner() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, false, false, false); + Recorder inner = new Recorder(); + FormattedCharSequence marked = StableText.mark(inner); + // Pass a sink that just returns true; the marked wrapper should delegate to inner. + marked.accept((index, style, codePoint) -> true); + assertTrue(inner.accepted, "StableText.accept should delegate to the wrapped sequence"); + } + + @Test + void multipleMarksAreTrackedSeparately() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, false, false, false); + assertEquals(0, StableText.trackedCount()); + StableText.mark(new Recorder()); + StableText.mark(new Recorder()); + StableText.mark(new Recorder()); + assertEquals(3, StableText.trackedCount()); + } +} diff --git a/mod/src/test/java/dev/loki/lovisual/render/engine/optimize/OptimizeTogglesTest.java b/mod/src/test/java/dev/loki/lovisual/render/engine/optimize/OptimizeTogglesTest.java new file mode 100755 index 00000000..ee6541f8 --- /dev/null +++ b/mod/src/test/java/dev/loki/lovisual/render/engine/optimize/OptimizeTogglesTest.java @@ -0,0 +1,79 @@ +package dev.loki.lovisual.render.engine.optimize; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies the {@link OptimizeToggles} snapshot holder. Mirrors the existing + * {@code OptimizeState} tests in shape: reset → assert default false → apply → + * assert each knob tracks independently → reset → assert default again. + */ +public final class OptimizeTogglesTest { + + @Test + void stateIsCleanUntilApplied() { + OptimizeToggles.reset(); + assertFalse(OptimizeToggles.entityLod()); + assertFalse(OptimizeToggles.particleLod()); + assertFalse(OptimizeToggles.spawnerCull()); + assertFalse(OptimizeToggles.spawnerTickCache()); + assertFalse(OptimizeToggles.blockEntityCache()); + assertFalse(OptimizeToggles.preparedTextCache()); + assertFalse(OptimizeToggles.microOpts()); + assertEquals(0, OptimizeToggles.entityLodDistance()); + assertEquals(0, OptimizeToggles.spawnerDistance()); + } + + @Test + void applyTracksEachKnobIndependently() { + OptimizeToggles.apply( + true, 35, + false, true, false, + true, 16, + true, + false, + true, + true); + assertTrue(OptimizeToggles.entityLod()); + assertEquals(35, OptimizeToggles.entityLodDistance()); + assertFalse(OptimizeToggles.particleLod()); + assertTrue(OptimizeToggles.particleLight()); + assertFalse(OptimizeToggles.particlePhysics()); + assertTrue(OptimizeToggles.spawnerCull()); + assertEquals(16, OptimizeToggles.spawnerDistance()); + assertTrue(OptimizeToggles.spawnerTickCache()); + assertFalse(OptimizeToggles.blockEntityCache()); + assertTrue(OptimizeToggles.preparedTextCache()); + assertTrue(OptimizeToggles.microOpts()); + + OptimizeToggles.reset(); + assertFalse(OptimizeToggles.entityLod()); + assertEquals(0, OptimizeToggles.entityLodDistance()); + assertFalse(OptimizeToggles.spawnerCull()); + assertFalse(OptimizeToggles.microOpts()); + } + + @Test + void applyWithAllOffMatchesReset() { + OptimizeToggles.apply( + false, 0, + false, false, false, + false, 0, + false, + false, + false, + false); + assertFalse(OptimizeToggles.entityLod()); + assertFalse(OptimizeToggles.particleLod()); + assertFalse(OptimizeToggles.spawnerCull()); + assertFalse(OptimizeToggles.spawnerTickCache()); + assertFalse(OptimizeToggles.blockEntityCache()); + assertFalse(OptimizeToggles.preparedTextCache()); + assertFalse(OptimizeToggles.microOpts()); + assertEquals(0, OptimizeToggles.entityLodDistance()); + assertEquals(0, OptimizeToggles.spawnerDistance()); + } +}