From 41b5667cd4c7d5e7ddf8a90209340b949023d25b Mon Sep 17 00:00:00 2001 From: nossr50 Date: Fri, 4 Jul 2025 13:27:38 -0700 Subject: [PATCH] Some more unit test coverage for tree feller --- .../woodcutting/WoodcuttingManager.java | 4 +- .../skills/woodcutting/WoodcuttingTest.java | 127 +++++++++++++++++- 2 files changed, 127 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java b/src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java index 17b2d497c..d3a05828f 100644 --- a/src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java +++ b/src/main/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingManager.java @@ -43,6 +43,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.VisibleForTesting; public class WoodcuttingManager extends SkillManager { public static final String SAPLING = "sapling"; @@ -206,7 +207,8 @@ public class WoodcuttingManager extends SkillManager { * and 10-15 milliseconds on jungle trees once the JIT has optimized the function (use the * ability about 4 times before taking measurements). */ - private void processTree(Block block, Set treeFellerBlocks) { + @VisibleForTesting + void processTree(Block block, Set treeFellerBlocks) { List futureCenterBlocks = new ArrayList<>(); // Check the block up and take different behavior (smaller search) if it's a log diff --git a/src/test/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingTest.java b/src/test/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingTest.java index 7e19ac58c..720d450aa 100644 --- a/src/test/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingTest.java +++ b/src/test/java/com/gmail/nossr50/skills/woodcutting/WoodcuttingTest.java @@ -1,24 +1,40 @@ package com.gmail.nossr50.skills.woodcutting; import static java.util.logging.Logger.getLogger; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import com.gmail.nossr50.MMOTestEnvironment; import com.gmail.nossr50.api.exceptions.InvalidSkillException; import com.gmail.nossr50.config.experience.ExperienceConfig; import com.gmail.nossr50.datatypes.skills.PrimarySkillType; import com.gmail.nossr50.datatypes.skills.SubSkillType; +import com.gmail.nossr50.mcMMO; +import com.gmail.nossr50.util.BlockUtils; import com.gmail.nossr50.util.skills.RankUtils; +import java.lang.reflect.Field; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; import java.util.logging.Logger; import org.bukkit.Material; import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import org.mockito.Mockito; class WoodcuttingTest extends MMOTestEnvironment { @@ -69,7 +85,7 @@ class WoodcuttingTest extends MMOTestEnvironment { void harvestLumberShouldDoubleDrop() { mmoPlayer.modifySkill(PrimarySkillType.WOODCUTTING, 1000); - Block block = Mockito.mock(Block.class); + Block block = mock(Block.class); // return empty collection if ItemStack Mockito.when(block.getDrops(any())).thenReturn(Collections.emptyList()); Mockito.when(block.getType()).thenReturn(Material.OAK_LOG); @@ -86,7 +102,7 @@ class WoodcuttingTest extends MMOTestEnvironment { void harvestLumberShouldNotDoubleDrop() { mmoPlayer.modifySkill(PrimarySkillType.WOODCUTTING, 0); - Block block = Mockito.mock(Block.class); + Block block = mock(Block.class); // wire block Mockito.when(block.getDrops(any())).thenReturn(null); @@ -99,7 +115,7 @@ class WoodcuttingTest extends MMOTestEnvironment { @Test void testProcessWoodcuttingBlockXP() { - Block targetBlock = Mockito.mock(Block.class); + Block targetBlock = mock(Block.class); Mockito.when(targetBlock.getType()).thenReturn(Material.OAK_LOG); // wire XP Mockito.when(ExperienceConfig.getInstance() @@ -110,4 +126,109 @@ class WoodcuttingTest extends MMOTestEnvironment { Mockito.verify(mmoPlayer, Mockito.times(1)) .beginXpGain(eq(PrimarySkillType.WOODCUTTING), eq(5F), any(), any()); } + + @Test + void treeFellerShouldStopAtThreshold() { + // Set threshold artificially low + int fakeThreshold = 3; + Mockito.when(generalConfig.getTreeFellerThreshold()).thenReturn(fakeThreshold); + + WoodcuttingManager manager = Mockito.spy(new WoodcuttingManager(mmoPlayer)); + + // Simulate all blocks are logs with XP + MockedStatic mockedBlockUtils = mockStatic(BlockUtils.class); + mockedBlockUtils.when(() -> BlockUtils.hasWoodcuttingXP(any(Block.class))).thenReturn(true); + mockedBlockUtils.when(() -> BlockUtils.isNonWoodPartOfTree(any(Block.class))) + .thenReturn(false); + + // Simulate that block tracker always allows processing + Mockito.when(mcMMO.getUserBlockTracker().isIneligible(any(Block.class))).thenReturn(false); + + // Create distinct mocked blocks to simulate recursion + Block centerBlock = mock(Block.class); + List relatives = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + Block relative = mock(Block.class, "block_" + i); + Mockito.when(relative.getRelative(any(BlockFace.class))).thenReturn(relative); + Mockito.when(relative.getRelative(anyInt(), anyInt(), anyInt())).thenReturn(relative); + relatives.add(relative); + } + + // Wire center block to return a different relative each time + Mockito.when(centerBlock.getRelative(any(BlockFace.class))) + .thenAnswer(inv -> relatives.get(0)); + Mockito.when(centerBlock.getRelative(anyInt(), anyInt(), anyInt())) + .thenAnswer(inv -> relatives.get( + ThreadLocalRandom.current().nextInt(relatives.size()))); + + Set treeFellerBlocks = new HashSet<>(); + manager.processTree(centerBlock, treeFellerBlocks); + + // --- Assertions --- + + // It processed *at least one* block + assertFalse(treeFellerBlocks.isEmpty(), "Tree Feller should process at least one block"); + + // It reached or slightly exceeded the threshold + assertTrue(treeFellerBlocks.size() >= fakeThreshold, + "Tree Feller should process up to the threshold limit"); + + // Confirm it stopped due to the threshold + assertTrue(getPrivateTreeFellerReachedThreshold(manager), + "Tree Feller should set treeFellerReachedThreshold to true"); + + mockedBlockUtils.close(); + } + + private boolean getPrivateTreeFellerReachedThreshold(WoodcuttingManager manager) { + try { + Field field = WoodcuttingManager.class.getDeclaredField("treeFellerReachedThreshold"); + field.setAccessible(true); + return (boolean) field.get(manager); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Test + void treeFellerShouldNotReachThreshold() throws NoSuchFieldException, IllegalAccessException { + int threshold = 10; + Mockito.when(generalConfig.getTreeFellerThreshold()).thenReturn(threshold); + + WoodcuttingManager manager = Mockito.spy(new WoodcuttingManager(mmoPlayer)); + + MockedStatic mockedBlockUtils = mockStatic(BlockUtils.class); + mockedBlockUtils.when(() -> BlockUtils.hasWoodcuttingXP(any(Block.class))).thenReturn(true); + mockedBlockUtils.when(() -> BlockUtils.isNonWoodPartOfTree(any(Block.class))) + .thenReturn(false); + Mockito.when(mcMMO.getUserBlockTracker().isIneligible(any(Block.class))).thenReturn(false); + + // Create 4 blocks (well below threshold) + Block b0 = mock(Block.class, "b0"); + Block b1 = mock(Block.class, "b1"); + Block b2 = mock(Block.class, "b2"); + Block b3 = mock(Block.class, "b3"); + + // Deterministically chain recursion: b0 → b1 → b2 → b3 → null + Mockito.when(b0.getRelative(anyInt(), anyInt(), anyInt())).thenReturn(b1); + Mockito.when(b1.getRelative(anyInt(), anyInt(), anyInt())).thenReturn(b2); + Mockito.when(b2.getRelative(anyInt(), anyInt(), anyInt())).thenReturn(b3); + Mockito.when(b3.getRelative(anyInt(), anyInt(), anyInt())).thenReturn(null); + + Mockito.when(b0.getRelative(any(BlockFace.class))).thenReturn(b1); + Mockito.when(b1.getRelative(any(BlockFace.class))).thenReturn(b2); + Mockito.when(b2.getRelative(any(BlockFace.class))).thenReturn(b3); + Mockito.when(b3.getRelative(any(BlockFace.class))).thenReturn(null); + + Set processed = new HashSet<>(); + manager.processTree(b0, processed); + + assertEquals(3, processed.size(), "Should process exactly 4 blocks"); + assertFalse(getPrivateTreeFellerReachedThreshold(manager), + "treeFellerReachedThreshold should remain false"); + + mockedBlockUtils.close(); + } + }