Some more unit test coverage for tree feller

This commit is contained in:
nossr50
2025-07-04 13:27:38 -07:00
parent b60e478aec
commit 41b5667cd4
2 changed files with 127 additions and 4 deletions

View File

@@ -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

View File

@@ -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();
}
}