Browse Source

Some more unit test coverage for tree feller

nossr50 1 week ago
parent
commit
41b5667cd4

+ 3 - 1
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<Block> treeFellerBlocks) {
+    @VisibleForTesting
+    void processTree(Block block, Set<Block> treeFellerBlocks) {
         List<Block> futureCenterBlocks = new ArrayList<>();
 
         // Check the block up and take different behavior (smaller search) if it's a log

+ 124 - 3
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<BlockUtils> 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<Block> 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<Block> 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<BlockUtils> 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<Block> 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();
+    }
+
 }