refactor: migrate package from com.loki to dev.loki

- Change base package from com.loki to dev.loki across all Java files
- Update plugin.yml main class path
- Fix ConfigParseException imports in test files
- Update build.gradle.kts relocate patterns
- Add missing imports in ConfigLoaderTest.java
- Update TODO.md with current status

BREAKING CHANGE: Package namespace changed from com.loki to dev.loki
This commit is contained in:
loki5512344 2026-05-31 21:34:44 +02:00
parent 41924ea97f
commit f4e9458544
87 changed files with 2185 additions and 1767 deletions

View file

@ -0,0 +1,151 @@
package dev.loki.lomines.command;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.core.Mine;
import dev.loki.lomines.core.Mines;
import net.kyori.adventure.text.Component;
import org.bukkit.command.CommandSender;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import static org.mockito.Mockito.*;
/**
* Unit tests for AdminCommands class.
*/
class AdminCommandsTest {
@Mock
private LoMinesPlugin plugin;
@Mock
private Mines mines;
@Mock
private CommandSender sender;
@Mock
private Mine mine;
private AdminCommands commands;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
when(plugin.getMines()).thenReturn(mines);
commands = new AdminCommands(plugin);
}
@Test
void testCreateMineSuccess() throws IOException {
String mineName = "testmine";
commands.create(sender, mineName);
verify(mines).create(mineName);
verify(sender, times(2)).sendMessage(any(Component.class));
}
@Test
void testCreateMineAlreadyExists() throws IOException {
String mineName = "existing";
doThrow(new IllegalArgumentException("Mine already exists")).when(mines).create(mineName);
commands.create(sender, mineName);
verify(sender).sendMessage(any(Component.class));
}
@Test
void testDeleteMineSuccess() throws IOException {
String mineName = "testmine";
commands.delete(sender, mineName);
verify(mines).delete(mineName);
verify(sender).sendMessage(any(Component.class));
}
@Test
void testDeleteMineNotFound() throws IOException {
String mineName = "nonexistent";
doThrow(new IllegalArgumentException("Mine not found")).when(mines).delete(mineName);
commands.delete(sender, mineName);
verify(sender).sendMessage(any(Component.class));
}
@Test
void testResetMineSuccess() {
String mineName = "testmine";
when(mines.get(mineName)).thenReturn(mine);
commands.reset(sender, mineName, false);
verify(mine).reset(false);
verify(sender).sendMessage(any(Component.class));
}
@Test
void testResetMineSilent() {
String mineName = "testmine";
when(mines.get(mineName)).thenReturn(mine);
commands.reset(sender, mineName, true);
verify(mine).reset(true);
verify(sender).sendMessage(any(Component.class));
}
@Test
void testResetMineNotFound() {
String mineName = "nonexistent";
when(mines.get(mineName)).thenThrow(new IllegalArgumentException("Mine not found"));
commands.reset(sender, mineName, false);
verify(sender).sendMessage(any(Component.class));
}
@Test
void testReloadSuccess() throws IOException {
Collection<Mine> mineList = Arrays.asList(mine, mine);
when(mines.getAll()).thenReturn(mineList);
commands.reload(sender);
verify(mine, times(2)).stop();
verify(mines).loadAll();
verify(sender).sendMessage(any(Component.class));
}
@Test
void testListMinesEmpty() {
when(mines.getAll()).thenReturn(List.of());
commands.list(sender);
verify(sender).sendMessage(any(Component.class));
}
@Test
void testListMinesWithData() {
when(mines.getAll()).thenReturn(Arrays.asList(mine, mine));
when(mine.getName()).thenReturn("mine1");
when(mine.getBlocks()).thenReturn(100);
when(mine.getTotalVolume()).thenReturn(200);
when(mine.getPercentFilled()).thenReturn(50.0);
commands.list(sender);
verify(sender, atLeast(3)).sendMessage(any(Component.class));
}
}

View file

@ -0,0 +1,117 @@
package dev.loki.lomines.core;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.block.BukkitBlockSetter;
import dev.loki.lomines.block.ItemsAdderBlockSetter;
import dev.loki.lomines.block.OraxenBlockSetter;
import dev.loki.lomines.data.MineConfig;
import org.bukkit.Location;
import org.bukkit.World;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
/**
* Unit tests for Mine class, specifically testing BlockSetter creation logic.
*/
class MineTest {
@Mock
private LoMinesPlugin plugin;
@Mock
private World world;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testCreateBlockSetter_WithOraxenPrefix_CreatesOraxenBlockSetter() {
// Arrange
Map<String, Double> blocks = new HashMap<>();
blocks.put("oraxen:custom_ore", 50.0);
blocks.put("oraxen:rare_ore", 30.0);
MineConfig config = createTestConfig(blocks);
// Act
Mine mine = new Mine("test_mine", config, plugin);
// Assert
assertNotNull(mine.getBlockSetter());
assertInstanceOf(OraxenBlockSetter.class, mine.getBlockSetter(), "Expected OraxenBlockSetter for oraxen: prefix");
}
@Test
void testCreateBlockSetter_WithItemsAdderPrefix_CreatesItemsAdderBlockSetter() {
// Arrange
Map<String, Double> blocks = new HashMap<>();
blocks.put("itemsadder:custom_block", 60.0);
blocks.put("itemsadder:special_block", 40.0);
MineConfig config = createTestConfig(blocks);
// Act
Mine mine = new Mine("test_mine", config, plugin);
// Assert
assertNotNull(mine.getBlockSetter());
assertInstanceOf(ItemsAdderBlockSetter.class, mine.getBlockSetter(), "Expected ItemsAdderBlockSetter for itemsadder: prefix");
}
@Test
void testCreateBlockSetter_WithoutPrefix_CreatesBukkitBlockSetter() {
// Arrange
Map<String, Double> blocks = new HashMap<>();
blocks.put("STONE", 50.0);
blocks.put("COAL_ORE", 30.0);
blocks.put("IRON_ORE", 20.0);
MineConfig config = createTestConfig(blocks);
// Act
Mine mine = new Mine("test_mine", config, plugin);
// Assert
assertNotNull(mine.getBlockSetter());
assertInstanceOf(BukkitBlockSetter.class, mine.getBlockSetter(), "Expected BukkitBlockSetter for vanilla blocks without prefix");
}
@Test
void testCreateBlockSetter_WithEmptyBlocks_ThrowsException() {
// Arrange
Map<String, Double> blocks = new HashMap<>();
MineConfig config = createTestConfig(blocks);
// Act & Assert
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> new Mine("test_mine", config, plugin)
);
assertEquals("Mine must have at least one block type", exception.getMessage());
}
private MineConfig createTestConfig(Map<String, Double> blocks) {
when(world.getName()).thenReturn("world");
Location loc1 = new Location(world, 0, 0, 0);
Location loc2 = new Location(world, 10, 10, 10);
return MineConfig.builder()
.selections(List.of(loc1, loc2))
.blocks(blocks)
.resetTicks(6000)
.build();
}
}

View file

@ -0,0 +1,56 @@
package dev.loki.lomines.core;
import dev.loki.lomines.LoMinesPlugin;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
/**
* Unit tests for MineTicker class.
* Note: Full integration testing requires a running Bukkit server.
* These tests verify basic construction and null safety.
*/
class MineTickerTest {
@Mock
private LoMinesPlugin plugin;
@Mock
private Mines mines;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testConstruction_WithValidParameters_DoesNotThrow() {
// Act & Assert
assertDoesNotThrow(() -> new MineTicker(mines, plugin));
}
@Test
void testStop_WhenNotRunning_DoesNotThrow() {
// Arrange
MineTicker ticker = new MineTicker(mines, plugin);
// Act & Assert
assertDoesNotThrow(() -> ticker.stop());
}
@Test
void testMultipleStops_DoNotThrow() {
// Arrange
MineTicker ticker = new MineTicker(mines, plugin);
// Act & Assert
assertDoesNotThrow(() -> {
ticker.stop();
ticker.stop();
ticker.stop();
});
}
}

View file

@ -0,0 +1,400 @@
package dev.loki.lomines.core;
import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.core.LoLogger;
import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.block.data.BlockData;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for Mines registry.
* Tests loading and creating mines.
*/
class MinesTest {
@TempDir
Path tempDir;
private Mines mines;
private LoMinesPlugin plugin;
private Scheduler scheduler;
@BeforeEach
void setUp() {
plugin = mock(LoMinesPlugin.class);
Logger logger = mock(Logger.class);
LoLogger loLogger = mock(LoLogger.class);
scheduler = mock(Scheduler.class);
when(plugin.getDataFolder()).thenReturn(tempDir.toFile());
when(plugin.getLogger()).thenReturn(logger);
when(plugin.loLogger()).thenReturn(loLogger);
mines = new Mines(plugin);
}
/**
* Helper method to set up Bukkit and Scheduler mocking for tests that create mines.
*/
private void setupBukkitAndScheduler(MockedStatic<Bukkit> bukkit, MockedStatic<Scheduler> schedulerStatic) {
Server server = mock(Server.class);
World world = mock(World.class);
BlockData blockData = mock(BlockData.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
bukkit.when(() -> Bukkit.createBlockData(any(Material.class))).thenReturn(blockData);
when(world.getName()).thenReturn("world");
schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler);
// Mock scheduler methods to execute callbacks immediately for testing
ScheduledTask task = mock(ScheduledTask.class);
when(scheduler.runTimer(any(Runnable.class), anyLong(), anyLong())).thenReturn(task);
}
@Test
void testLoadAllWithNoMinesFolder() throws IOException {
// When mines folder doesn't exist, loadAll should not throw
assertDoesNotThrow(() -> mines.loadAll());
// No mines should be loaded
assertEquals(0, mines.getAll().size());
}
@Test
void testLoadAllWithEmptyMinesFolder() throws IOException {
// Create empty mines folder
Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder);
// Load all mines
mines.loadAll();
// No mines should be loaded
assertEquals(0, mines.getAll().size());
}
@Test
void testLoadAllWithValidMine() throws IOException {
// Create mines folder
Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder);
// Create a valid mine config file
String yamlContent = """
selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0
contents:
stone: 100
reset:
ticks: 6000
percent: 10.0
reset-on-percent: false
actionbar:
enabled: true
message: "Test mine"
range: 50.0
timer-format: "mm:ss"
teleport-on-reset: false
reset-commands: []
broadcast-reset: ""
random-rewards: []
""";
Path configFile = minesFolder.resolve("testmine.yml");
Files.writeString(configFile, yamlContent);
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines
mines.loadAll();
// Verify mine was loaded
assertEquals(1, mines.getAll().size());
assertTrue(mines.find("testmine").isPresent());
Mine mine = mines.get("testmine");
assertNotNull(mine);
assertEquals("testmine", mine.getName());
}
}
@Test
void testLoadAllWithMultipleMines() throws IOException {
// Create mines folder
Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder);
// Create multiple mine config files
String yamlContent = """
selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0
contents:
stone: 100
reset:
ticks: 6000
percent: 10.0
reset-on-percent: false
actionbar:
enabled: true
message: "Test mine"
range: 50.0
timer-format: "mm:ss"
teleport-on-reset: false
reset-commands: []
broadcast-reset: ""
random-rewards: []
""";
Files.writeString(minesFolder.resolve("mine1.yml"), yamlContent);
Files.writeString(minesFolder.resolve("mine2.yml"), yamlContent);
Files.writeString(minesFolder.resolve("mine3.yml"), yamlContent);
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines
mines.loadAll();
// Verify all mines were loaded
assertEquals(3, mines.getAll().size());
assertTrue(mines.find("mine1").isPresent());
assertTrue(mines.find("mine2").isPresent());
assertTrue(mines.find("mine3").isPresent());
}
}
@Test
void testLoadAllIgnoresNonYamlFiles() throws IOException {
// Create mines folder
Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder);
// Create a valid mine config file
String yamlContent = """
selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0
contents:
stone: 100
reset:
ticks: 6000
percent: 10.0
""";
Files.writeString(minesFolder.resolve("validmine.yml"), yamlContent);
Files.writeString(minesFolder.resolve("readme.txt"), "This is not a mine config");
Files.writeString(minesFolder.resolve("backup.bak"), "Backup file");
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines
mines.loadAll();
// Only the .yml file should be loaded
assertEquals(1, mines.getAll().size());
assertTrue(mines.find("validmine").isPresent());
}
}
@Test
void testLoadAllWithInvalidMine() throws IOException {
// Create mines folder
Path minesFolder = tempDir.resolve("mines");
Files.createDirectories(minesFolder);
// Create an invalid mine config file (missing required fields)
String invalidYaml = """
selection.1: world;0;64;0;0.0;0.0
""";
Files.writeString(minesFolder.resolve("invalidmine.yml"), invalidYaml);
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Load all mines - should not throw, but should log error
assertDoesNotThrow(() -> mines.loadAll());
// Invalid mine should not be loaded
assertEquals(0, mines.getAll().size());
// Verify error was logged
verify(plugin.loLogger(), atLeastOnce()).error(anyString());
}
}
@Test
void testCreateNewMine() throws IOException {
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a new mine
mines.create("newmine");
// Verify mine was created
assertEquals(1, mines.getAll().size());
assertTrue(mines.find("newmine").isPresent());
Mine mine = mines.get("newmine");
assertNotNull(mine);
assertEquals("newmine", mine.getName());
// Verify config file was created
Path configFile = tempDir.resolve("mines/newmine.yml");
assertTrue(Files.exists(configFile));
// Verify logger was called
verify(plugin.loLogger()).info("Created mine: newmine");
}
}
@Test
void testCreateMineCreatesFolder() throws IOException {
// Ensure mines folder doesn't exist
Path minesFolder = tempDir.resolve("mines");
assertFalse(Files.exists(minesFolder));
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a new mine
mines.create("testmine");
// Verify mines folder was created
assertTrue(Files.exists(minesFolder));
assertTrue(Files.isDirectory(minesFolder));
}
}
@Test
void testCreateDuplicateMineThrowsException() throws IOException {
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine
mines.create("duplicate");
// Try to create the same mine again
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
mines.create("duplicate");
});
assertTrue(exception.getMessage().contains("Mine already exists"));
}
}
@Test
void testCreateMineIsCaseInsensitive() throws IOException {
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine with mixed case
mines.create("TestMine");
// Try to create the same mine with different case
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
mines.create("testmine");
});
assertTrue(exception.getMessage().contains("Mine already exists"));
}
}
@Test
void testGetMineByName() throws IOException {
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine
mines.create("testmine");
// Get mine by exact name
Mine mine = mines.get("testmine");
assertNotNull(mine);
assertEquals("testmine", mine.getName());
// Get mine by different case
Mine mine2 = mines.get("TestMine");
assertNotNull(mine2);
assertEquals("testmine", mine2.getName());
// Same mine instance
assertSame(mine, mine2);
}
}
@Test
void testGetNonExistentMineThrowsException() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
mines.get("nonexistent");
});
assertTrue(exception.getMessage().contains("Mine not found"));
}
@Test
void testFindMineReturnsOptional() throws IOException {
// Mock Bukkit and Scheduler
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class);
MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
setupBukkitAndScheduler(bukkit, schedulerStatic);
// Create a mine
mines.create("testmine");
// Find existing mine
assertTrue(mines.find("testmine").isPresent());
// Find non-existent mine
assertFalse(mines.find("nonexistent").isPresent());
}
}
}

View file

@ -0,0 +1,536 @@
package dev.loki.lomines.data;
import dev.loki.lomines.data.config.ConfigLoader;
import dev.loki.lomines.data.config.MineConfig;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.data.reward.Reward;
import org.bukkit.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
/**
* Unit tests for ConfigLoader.
* Tests parsing of YAML configuration files into MineConfig objects.
*/
class ConfigLoaderTest {
@TempDir
Path tempDir;
private ConfigLoader configLoader;
@BeforeEach
void setUp() {
configLoader = new ConfigLoader();
}
@Test
void testLoadValidConfig() throws Exception {
// Create a valid YAML config file
String yamlContent = """
selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0
contents:
stone: 50
coal_ore: 30
iron_ore: 20
reset:
ticks: 6000
percent: 10.0
reset-on-percent: false
actionbar:
enabled: true
message: "Mine resetting soon"
range: 50.0
timer-format: "mm:ss"
teleport-on-reset: false
reset-commands:
- "say Mine reset!"
broadcast-reset: "Mine has been reset"
random-rewards:
- chance: 1.0
prevent-drops: false
blocks:
- diamond_ore
items:
- type: diamond
amount: 1
commands:
- "eco give %player% 100"
""";
Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent);
// Mock Bukkit
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class);
World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world");
// Load the config
MineConfig config = configLoader.load(configFile);
// Verify selections
assertNotNull(config.getSelections());
assertEquals(2, config.getSelections().size());
// Verify blocks
assertNotNull(config.getBlocks());
assertEquals(3, config.getBlocks().size());
assertTrue(config.getBlocks().containsKey("stone"));
assertTrue(config.getBlocks().containsKey("coal_ore"));
assertTrue(config.getBlocks().containsKey("iron_ore"));
assertEquals(50.0, config.getBlocks().get("stone"));
assertEquals(30.0, config.getBlocks().get("coal_ore"));
assertEquals(20.0, config.getBlocks().get("iron_ore"));
// Verify rewards
assertNotNull(config.getRewards());
assertEquals(1, config.getRewards().size());
Reward reward = config.getRewards().get(0);
assertEquals(1.0, reward.getChance());
assertFalse(reward.isPreventDrops());
assertEquals(1, reward.getMaterials().size());
assertTrue(reward.getMaterials().contains(Material.DIAMOND_ORE));
// Verify reset settings
assertEquals(6000, config.getResetTicks());
assertEquals(10.0, config.getResetPercent());
assertFalse(config.isResetOnPercentEnabled());
// Verify action bar settings
assertTrue(config.isActionBarEnabled());
assertEquals("Mine resetting soon", config.getActionBarMessage());
assertEquals(50.0, config.getActionBarRange());
// Verify other settings
assertEquals("mm:ss", config.getTimerFormat());
assertFalse(config.isTeleportOnReset());
assertNull(config.getTeleportLocation());
assertEquals(1, config.getResetCommands().size());
assertEquals("say Mine reset!", config.getResetCommands().get(0));
assertEquals("Mine has been reset", config.getBroadcastReset());
}
}
@Test
void testLoadMissingFile() {
Path nonExistentFile = tempDir.resolve("nonexistent.yml");
assertThrows(IOException.class, () -> {
configLoader.load(nonExistentFile);
});
}
@Test
void testLoadNullPath() {
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(null);
});
assertTrue(exception.getMessage().contains("cannot be null"));
}
@Test
void testLoadMissingSelections() throws Exception {
String yamlContent = """
contents:
stone: 100
""";
Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent);
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile);
});
assertTrue(exception.getMessage().contains("No selection coordinates found"));
}
@Test
void testLoadOddNumberOfSelections() throws Exception {
String yamlContent = """
selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0
selection.3: world;20;64;20;0.0;0.0
contents:
stone: 100
""";
Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class);
World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world");
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile);
});
assertTrue(exception.getMessage().contains("must be in pairs"));
}
}
@Test
void testLoadMissingContents() throws Exception {
String yamlContent = """
selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0
""";
Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class);
World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world");
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile);
});
assertTrue(exception.getMessage().contains("Missing required 'contents' section"));
}
}
@Test
void testLoadInvalidMaterial() throws Exception {
String yamlContent = """
selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0
contents:
invalid_material: 100
""";
Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class);
World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world");
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile);
});
assertTrue(exception.getMessage().contains("Unknown material"));
}
}
@Test
void testLoadInvalidBlockWeight() throws Exception {
String yamlContent = """
selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0
contents:
stone: -10
""";
Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class);
World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world");
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile);
});
assertTrue(exception.getMessage().contains("must be positive"));
}
}
@Test
void testLoadInvalidRewardChance() throws Exception {
String yamlContent = """
selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0
contents:
stone: 100
random-rewards:
- chance: 150
blocks:
- diamond_ore
""";
Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class);
World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world");
ConfigParseException exception = assertThrows(ConfigParseException.class, () -> {
configLoader.load(configFile);
});
assertTrue(exception.getMessage().contains("between 0 and 100"));
}
}
@Test
void testLoadWithTeleportLocation() throws Exception {
String yamlContent = """
selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0
contents:
stone: 100
teleport-on-reset: true
teleport-location: world;5;70;5;90.0;0.0
""";
Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent);
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class);
World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world");
MineConfig config = configLoader.load(configFile);
assertTrue(config.isTeleportOnReset());
assertNotNull(config.getTeleportLocation());
assertEquals(5.0, config.getTeleportLocation().getX());
assertEquals(70.0, config.getTeleportLocation().getY());
assertEquals(5.0, config.getTeleportLocation().getZ());
}
}
@Test
void testSaveAndLoadRoundTrip() throws Exception {
// Create a config file to load (without items to avoid ItemStack initialization issues)
String yamlContent = """
selection.1: world;0;64;0;0.0;0.0
selection.2: world;10;74;10;0.0;0.0
contents:
stone: 50
coal_ore: 30
reset:
ticks: 6000
percent: 10.0
reset-on-percent: false
actionbar:
enabled: true
message: "Mine resetting soon"
range: 50.0
timer-format: "mm:ss"
teleport-on-reset: false
reset-commands:
- "say Mine reset!"
broadcast-reset: "Mine has been reset"
random-rewards:
- chance: 1.0
prevent-drops: false
blocks:
- diamond_ore
commands:
- "eco give %player% 100"
""";
Path configFile = tempDir.resolve("test-mine.yml");
Files.writeString(configFile, yamlContent);
// Mock Bukkit
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class);
World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world");
// Load the config
MineConfig config = configLoader.load(configFile);
// Save to a new file
Path savedFile = tempDir.resolve("saved-mine.yml");
configLoader.save(config, savedFile);
// Verify the file was created
assertTrue(Files.exists(savedFile));
// Verify the content is valid YAML
String savedContent = Files.readString(savedFile);
assertFalse(savedContent.isEmpty());
// Load the saved config
MineConfig loadedConfig = configLoader.load(savedFile);
// Verify key properties match
assertEquals(config.getResetTicks(), loadedConfig.getResetTicks());
assertEquals(config.getResetPercent(), loadedConfig.getResetPercent());
assertEquals(config.isResetOnPercentEnabled(), loadedConfig.isResetOnPercentEnabled());
assertEquals(config.isActionBarEnabled(), loadedConfig.isActionBarEnabled());
assertEquals(config.getActionBarMessage(), loadedConfig.getActionBarMessage());
assertEquals(config.getActionBarRange(), loadedConfig.getActionBarRange());
assertEquals(config.getTimerFormat(), loadedConfig.getTimerFormat());
assertEquals(config.isTeleportOnReset(), loadedConfig.isTeleportOnReset());
assertEquals(config.getBroadcastReset(), loadedConfig.getBroadcastReset());
assertEquals(config.getResetCommands().size(), loadedConfig.getResetCommands().size());
assertEquals(config.getRewards().size(), loadedConfig.getRewards().size());
}
}
@Test
void testSaveNullConfig() {
Path configFile = tempDir.resolve("test.yml");
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
configLoader.save(null, configFile);
});
assertTrue(exception.getMessage().contains("Config cannot be null"));
}
@Test
void testSaveNullPath() throws Exception {
// Create a minimal config
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class);
World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world");
Location loc1 = new Location(world, 0, 64, 0, 0, 0);
Location loc2 = new Location(world, 10, 74, 10, 0, 0);
MineConfig config = MineConfig.builder()
.selections(List.of(loc1, loc2))
.blocks(Map.of("stone", 100.0))
.rewards(List.of())
.build();
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
configLoader.save(config, null);
});
assertTrue(exception.getMessage().contains("Config file path cannot be null"));
}
}
@Test
void testSaveWithTeleportLocation() throws Exception {
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class);
World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world");
Location loc1 = new Location(world, 0, 64, 0, 0, 0);
Location loc2 = new Location(world, 10, 74, 10, 0, 0);
Location teleportLoc = new Location(world, 5, 70, 5, 90, 0);
MineConfig config = MineConfig.builder()
.selections(List.of(loc1, loc2))
.blocks(Map.of("stone", 100.0))
.rewards(List.of())
.teleportOnReset(true)
.teleportLocation(teleportLoc)
.build();
Path savedFile = tempDir.resolve("with-teleport.yml");
configLoader.save(config, savedFile);
// Verify the file was created
assertTrue(Files.exists(savedFile));
// Load and verify
MineConfig loadedConfig = configLoader.load(savedFile);
assertTrue(loadedConfig.isTeleportOnReset());
assertNotNull(loadedConfig.getTeleportLocation());
assertEquals(5.0, loadedConfig.getTeleportLocation().getX());
assertEquals(70.0, loadedConfig.getTeleportLocation().getY());
assertEquals(5.0, loadedConfig.getTeleportLocation().getZ());
}
}
@Test
void testSaveEmptyRewards() throws Exception {
try (MockedStatic<Bukkit> bukkit = mockStatic(Bukkit.class)) {
Server server = mock(Server.class);
World world = mock(World.class);
bukkit.when(Bukkit::getServer).thenReturn(server);
bukkit.when(() -> Bukkit.getWorld(anyString())).thenReturn(world);
when(world.getName()).thenReturn("world");
Location loc1 = new Location(world, 0, 64, 0, 0, 0);
Location loc2 = new Location(world, 10, 74, 10, 0, 0);
MineConfig config = MineConfig.builder()
.selections(List.of(loc1, loc2))
.blocks(Map.of("stone", 100.0))
.rewards(List.of())
.build();
Path savedFile = tempDir.resolve("no-rewards.yml");
configLoader.save(config, savedFile);
// Verify the file was created
assertTrue(Files.exists(savedFile));
// Load and verify
MineConfig loadedConfig = configLoader.load(savedFile);
assertTrue(loadedConfig.getRewards().isEmpty());
}
}
}

View file

@ -0,0 +1,156 @@
package dev.loki.lomines.data;
import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.core.LoLogger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Integration tests for Leaderboard caching with StatsManager.
* Verifies that cache invalidation works correctly when statistics change.
*/
class LeaderboardCacheIntegrationTest {
private StatsManager statsManager;
private Leaderboard leaderboard;
@BeforeEach
void setUp() {
// Create a mock plugin
LoMinesPlugin mockPlugin = mock(LoMinesPlugin.class);
LoLogger loLogger = mock(LoLogger.class);
File mockDataFolder = new File(System.getProperty("java.io.tmpdir"), "lomines-test");
when(mockPlugin.getDataFolder()).thenReturn(mockDataFolder);
when(mockPlugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("TestLogger"));
when(mockPlugin.loLogger()).thenReturn(loLogger);
statsManager = new StatsManager(mockPlugin);
leaderboard = statsManager.getLeaderboard();
}
@Test
void testCacheInvalidatedOnIncrementBlocks() {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
// Set initial stats
statsManager.getOrCreate(player1).setTotalBlocks(100);
// Build cache
List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10);
assertEquals(1, top1.size(), "Should have 1 player initially");
assertEquals(player1, top1.get(0).getPlayerId());
// Increment blocks for player2 - this should invalidate cache
statsManager.incrementBlocks(player2, "mine1");
// Get top again - should rebuild cache with new player
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10);
assertEquals(2, top2.size(), "Should have 2 players after increment");
// Verify order (player1 has 100, player2 has 1)
assertEquals(player1, top2.get(0).getPlayerId(), "Player1 should still be first");
assertEquals(player2, top2.get(1).getPlayerId(), "Player2 should be second");
}
@Test
void testCacheRebuildsWithCorrectOrder() {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
// Set initial stats
statsManager.getOrCreate(player1).setTotalBlocks(50);
statsManager.getOrCreate(player2).setTotalBlocks(100);
// Build cache
List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10);
assertEquals(player2, top1.get(0).getPlayerId(), "Player2 should be first initially");
// Increment player1 many times to overtake player2
for (int i = 0; i < 60; i++) {
statsManager.incrementBlocks(player1, "mine1");
}
// Get top again - cache should be rebuilt with new order
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10);
assertEquals(player1, top2.get(0).getPlayerId(), "Player1 should be first after increments");
assertEquals(110, top2.get(0).getCount(), "Player1 should have 110 blocks");
assertEquals(player2, top2.get(1).getPlayerId(), "Player2 should be second");
assertEquals(100, top2.get(1).getCount(), "Player2 should still have 100 blocks");
}
@Test
void testMultipleIncrementsInvalidateCache() {
UUID player1 = UUID.randomUUID();
// Build empty cache
List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10);
assertTrue(top1.isEmpty(), "Should be empty initially");
// Increment blocks multiple times
for (int i = 0; i < 5; i++) {
statsManager.incrementBlocks(player1, "mine1");
}
// Get top - should show updated count
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10);
assertEquals(1, top2.size());
assertEquals(5, top2.get(0).getCount(), "Should have 5 blocks after 5 increments");
}
@Test
void testCachePerformance() {
// Add many players
for (int i = 0; i < 100; i++) {
UUID playerId = UUID.randomUUID();
statsManager.getOrCreate(playerId).setTotalBlocks(i + 1);
}
// First call - builds cache (slower)
long start1 = System.nanoTime();
List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10);
long time1 = System.nanoTime() - start1;
// Second call - uses cache (should be much faster)
long start2 = System.nanoTime();
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10);
long time2 = System.nanoTime() - start2;
assertEquals(10, top1.size());
assertEquals(10, top2.size());
// Cache hit should be significantly faster (at least 2x)
assertTrue(time2 < time1,
String.format("Cache hit (%d ns) should be faster than cache miss (%d ns)", time2, time1));
}
@Test
void testDifferentLimitsUseSameCache() {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
UUID player3 = UUID.randomUUID();
statsManager.getOrCreate(player1).setTotalBlocks(100);
statsManager.getOrCreate(player2).setTotalBlocks(200);
statsManager.getOrCreate(player3).setTotalBlocks(300);
// Build cache with limit 10
List<LeaderboardEntry> top10 = leaderboard.getTopTotal(10);
assertEquals(3, top10.size());
// Get with limit 2 - should use same cache
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(2);
assertEquals(2, top2.size());
assertEquals(player3, top2.get(0).getPlayerId(), "Should have highest player");
assertEquals(player2, top2.get(1).getPlayerId(), "Should have second highest player");
}
}

View file

@ -0,0 +1,206 @@
package dev.loki.lomines.data;
import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.core.LoLogger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for Leaderboard class.
* Tests caching behavior, sorting, and filtering.
*/
class LeaderboardTest {
private StatsManager statsManager;
private Leaderboard leaderboard;
@BeforeEach
void setUp() {
// Create a mock plugin
LoMinesPlugin mockPlugin = mock(LoMinesPlugin.class);
LoLogger loLogger = mock(LoLogger.class);
File mockDataFolder = new File(System.getProperty("java.io.tmpdir"), "lomines-test");
when(mockPlugin.getDataFolder()).thenReturn(mockDataFolder);
when(mockPlugin.getLogger()).thenReturn(java.util.logging.Logger.getLogger("TestLogger"));
when(mockPlugin.loLogger()).thenReturn(loLogger);
statsManager = new StatsManager(mockPlugin);
leaderboard = statsManager.getLeaderboard();
}
@Test
void testGetTopTotal_EmptyStats() {
List<LeaderboardEntry> top = leaderboard.getTopTotal(10);
assertTrue(top.isEmpty(), "Top list should be empty when no stats exist");
}
@Test
void testGetTopTotal_SinglePlayer() {
UUID playerId = UUID.randomUUID();
PlayerStats stats = statsManager.getOrCreate(playerId);
stats.setTotalBlocks(100);
List<LeaderboardEntry> top = leaderboard.getTopTotal(10);
assertEquals(1, top.size());
assertEquals(playerId, top.get(0).getPlayerId());
assertEquals(100, top.get(0).getCount());
}
@Test
void testGetTopTotal_SortedDescending() {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
UUID player3 = UUID.randomUUID();
statsManager.getOrCreate(player1).setTotalBlocks(50);
statsManager.getOrCreate(player2).setTotalBlocks(200);
statsManager.getOrCreate(player3).setTotalBlocks(100);
List<LeaderboardEntry> top = leaderboard.getTopTotal(10);
assertEquals(3, top.size());
assertEquals(player2, top.get(0).getPlayerId()); // 200 blocks
assertEquals(player3, top.get(1).getPlayerId()); // 100 blocks
assertEquals(player1, top.get(2).getPlayerId()); // 50 blocks
}
@Test
void testGetTopTotal_LimitRespected() {
for (int i = 0; i < 10; i++) {
UUID playerId = UUID.randomUUID();
statsManager.getOrCreate(playerId).setTotalBlocks(i + 1);
}
List<LeaderboardEntry> top = leaderboard.getTopTotal(5);
assertEquals(5, top.size(), "Should respect the limit parameter");
}
@Test
void testGetTopTotal_FiltersZeroBlocks() {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
statsManager.getOrCreate(player1).setTotalBlocks(100);
statsManager.getOrCreate(player2).setTotalBlocks(0);
List<LeaderboardEntry> top = leaderboard.getTopTotal(10);
assertEquals(1, top.size(), "Should filter out players with 0 blocks");
assertEquals(player1, top.get(0).getPlayerId());
}
@Test
void testGetTopTotal_CachingWorks() {
UUID playerId = UUID.randomUUID();
statsManager.getOrCreate(playerId).setTotalBlocks(100);
// First call - builds cache
List<LeaderboardEntry> top1 = leaderboard.getTopTotal(10);
// Second call - should use cache (same reference)
List<LeaderboardEntry> top2 = leaderboard.getTopTotal(10);
assertEquals(top1.size(), top2.size());
assertEquals(top1.get(0).getPlayerId(), top2.get(0).getPlayerId());
}
@Test
void testInvalidateCache_ClearsCache() {
UUID playerId = UUID.randomUUID();
statsManager.getOrCreate(playerId).setTotalBlocks(100);
// Build cache
leaderboard.getTopTotal(10);
// Invalidate cache
leaderboard.invalidateCache();
// Add new player
UUID player2 = UUID.randomUUID();
statsManager.getOrCreate(player2).setTotalBlocks(200);
// Should rebuild cache with new player
List<LeaderboardEntry> top = leaderboard.getTopTotal(10);
assertEquals(2, top.size(), "Cache should be rebuilt after invalidation");
assertEquals(player2, top.get(0).getPlayerId(), "New player should be first");
}
@Test
void testGetTopByMine_SortedDescending() {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
UUID player3 = UUID.randomUUID();
statsManager.getOrCreate(player1).setMineBlocks("mine1", 50);
statsManager.getOrCreate(player2).setMineBlocks("mine1", 200);
statsManager.getOrCreate(player3).setMineBlocks("mine1", 100);
List<LeaderboardEntry> top = leaderboard.getTopByMine("mine1", 10);
assertEquals(3, top.size());
assertEquals(player2, top.get(0).getPlayerId()); // 200 blocks
assertEquals(player3, top.get(1).getPlayerId()); // 100 blocks
assertEquals(player1, top.get(2).getPlayerId()); // 50 blocks
}
@Test
void testGetTopByMine_FiltersZeroBlocks() {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
statsManager.getOrCreate(player1).setMineBlocks("mine1", 100);
statsManager.getOrCreate(player2).setMineBlocks("mine1", 0);
List<LeaderboardEntry> top = leaderboard.getTopByMine("mine1", 10);
assertEquals(1, top.size(), "Should filter out players with 0 blocks");
assertEquals(player1, top.get(0).getPlayerId());
}
@Test
void testGetTopByMine_OnlyIncludesSpecificMine() {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
statsManager.getOrCreate(player1).setMineBlocks("mine1", 100);
statsManager.getOrCreate(player2).setMineBlocks("mine2", 200);
List<LeaderboardEntry> top = leaderboard.getTopByMine("mine1", 10);
assertEquals(1, top.size(), "Should only include players with blocks in specified mine");
assertEquals(player1, top.get(0).getPlayerId());
}
@Test
void testGetPosition_ReturnsCorrectPosition() {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
UUID player3 = UUID.randomUUID();
statsManager.getOrCreate(player1).setTotalBlocks(50);
statsManager.getOrCreate(player2).setTotalBlocks(200);
statsManager.getOrCreate(player3).setTotalBlocks(100);
assertEquals(1, leaderboard.getPosition(player2), "Player with most blocks should be position 1");
assertEquals(2, leaderboard.getPosition(player3), "Player with second most blocks should be position 2");
assertEquals(3, leaderboard.getPosition(player1), "Player with least blocks should be position 3");
}
@Test
void testGetPosition_ReturnsMinusOneForNonExistentPlayer() {
UUID playerId = UUID.randomUUID();
assertEquals(-1, leaderboard.getPosition(playerId), "Should return -1 for non-existent player");
}
@Test
void testGetPosition_ReturnsMinusOneForZeroBlocks() {
UUID playerId = UUID.randomUUID();
statsManager.getOrCreate(playerId).setTotalBlocks(0);
assertEquals(-1, leaderboard.getPosition(playerId), "Should return -1 for player with 0 blocks");
}
}

View file

@ -0,0 +1,260 @@
package dev.loki.lomines.data;
import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.core.LoLogger;
import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.MockedStatic;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
import java.util.logging.Logger;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for StatsManager.
* Tests loading, saving, and auto-saving of player statistics.
*/
class StatsManagerTest {
@TempDir
Path tempDir;
private StatsManager statsManager;
private LoMinesPlugin plugin;
private Scheduler scheduler;
@BeforeEach
void setUp() {
plugin = mock(LoMinesPlugin.class);
LoLogger loLogger = mock(LoLogger.class);
scheduler = mock(Scheduler.class);
when(plugin.getDataFolder()).thenReturn(tempDir.toFile());
when(plugin.getLogger()).thenReturn(Logger.getLogger("TestLogger"));
when(plugin.loLogger()).thenReturn(loLogger);
statsManager = new StatsManager(plugin);
}
@Test
void testGetOrCreate() {
UUID playerId = UUID.randomUUID();
PlayerStats stats1 = statsManager.getOrCreate(playerId);
assertNotNull(stats1);
assertEquals(playerId, stats1.getPlayerId());
// Should return the same instance
PlayerStats stats2 = statsManager.getOrCreate(playerId);
assertSame(stats1, stats2);
}
@Test
void testIncrementBlocks() {
UUID playerId = UUID.randomUUID();
String mineName = "TestMine";
statsManager.incrementBlocks(playerId, mineName);
PlayerStats stats = statsManager.getOrCreate(playerId);
assertEquals(1, stats.getTotalBlocks());
assertEquals(1, stats.getMineBlocks(mineName));
// Increment again
statsManager.incrementBlocks(playerId, mineName);
assertEquals(2, stats.getTotalBlocks());
assertEquals(2, stats.getMineBlocks(mineName));
}
@Test
void testIncrementBlocksMultipleMines() {
UUID playerId = UUID.randomUUID();
statsManager.incrementBlocks(playerId, "Mine1");
statsManager.incrementBlocks(playerId, "Mine2");
statsManager.incrementBlocks(playerId, "Mine1");
PlayerStats stats = statsManager.getOrCreate(playerId);
assertEquals(3, stats.getTotalBlocks());
assertEquals(2, stats.getMineBlocks("Mine1"));
assertEquals(1, stats.getMineBlocks("Mine2"));
}
@Test
void testSaveEmptyStats() throws IOException {
statsManager.save();
Path statsFile = tempDir.resolve("stats.yml");
assertTrue(Files.exists(statsFile));
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(statsFile.toFile());
assertNull(yaml.getConfigurationSection("players"));
}
@Test
void testSaveWithStats() throws IOException {
UUID playerId = UUID.randomUUID();
statsManager.incrementBlocks(playerId, "TestMine");
statsManager.incrementBlocks(playerId, "TestMine");
statsManager.incrementBlocks(playerId, "AnotherMine");
statsManager.save();
Path statsFile = tempDir.resolve("stats.yml");
assertTrue(Files.exists(statsFile));
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(statsFile.toFile());
assertNotNull(yaml.getConfigurationSection("players"));
String playerPath = "players." + playerId;
assertEquals(3, yaml.getLong(playerPath + ".total"));
assertEquals(2, yaml.getLong(playerPath + ".mines.testmine"));
assertEquals(1, yaml.getLong(playerPath + ".mines.anothermine"));
}
@Test
void testLoadNonExistentFile() throws IOException {
// Should not throw exception, just log info
statsManager.load();
// Stats should be empty
assertTrue(statsManager.getAllStats().isEmpty());
}
@Test
void testLoadEmptyFile() throws IOException {
Path statsFile = tempDir.resolve("stats.yml");
Files.writeString(statsFile, "");
statsManager.load();
assertTrue(statsManager.getAllStats().isEmpty());
}
@Test
void testLoadValidStats() throws IOException {
UUID playerId = UUID.randomUUID();
String yamlContent = String.format("""
players:
%s:
total: 100
mines:
mine1: 60
mine2: 40
""", playerId);
Path statsFile = tempDir.resolve("stats.yml");
Files.writeString(statsFile, yamlContent);
statsManager.load();
PlayerStats stats = statsManager.getOrCreate(playerId);
assertEquals(100, stats.getTotalBlocks());
assertEquals(60, stats.getMineBlocks("mine1"));
assertEquals(40, stats.getMineBlocks("mine2"));
}
@Test
void testLoadInvalidUUID() throws IOException {
String yamlContent = """
players:
invalid-uuid:
total: 100
""";
Path statsFile = tempDir.resolve("stats.yml");
Files.writeString(statsFile, yamlContent);
// Should not throw exception, just log warning
statsManager.load();
assertTrue(statsManager.getAllStats().isEmpty());
}
@Test
void testSaveAndLoadRoundTrip() throws IOException {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
statsManager.incrementBlocks(player1, "Mine1");
statsManager.incrementBlocks(player1, "Mine1");
statsManager.incrementBlocks(player1, "Mine2");
statsManager.incrementBlocks(player2, "Mine1");
statsManager.save();
// Create new manager and load
StatsManager newManager = new StatsManager(plugin);
newManager.load();
PlayerStats stats1 = newManager.getOrCreate(player1);
assertEquals(3, stats1.getTotalBlocks());
assertEquals(2, stats1.getMineBlocks("Mine1"));
assertEquals(1, stats1.getMineBlocks("Mine2"));
PlayerStats stats2 = newManager.getOrCreate(player2);
assertEquals(1, stats2.getTotalBlocks());
assertEquals(1, stats2.getMineBlocks("Mine1"));
}
@Test
void testStartAutoSave() {
try (MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
ScheduledTask task = mock(ScheduledTask.class);
schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler);
when(scheduler.runTimerAsync(any(Runnable.class), anyLong(), anyLong())).thenReturn(task);
statsManager.startAutoSave();
// Verify that runTimerAsync was called with correct interval (5 minutes = 6000 ticks)
verify(scheduler).runTimerAsync(any(Runnable.class), eq(6000L), eq(6000L));
}
}
@Test
void testStopAutoSave() {
try (MockedStatic<Scheduler> schedulerStatic = mockStatic(Scheduler.class)) {
ScheduledTask task = mock(ScheduledTask.class);
schedulerStatic.when(() -> Scheduler.get(plugin)).thenReturn(scheduler);
when(scheduler.runTimerAsync(any(Runnable.class), anyLong(), anyLong())).thenReturn(task);
statsManager.startAutoSave();
statsManager.stopAutoSave();
verify(task).cancel();
}
}
@Test
void testStopAutoSaveWithoutStart() {
// Should not throw exception
assertDoesNotThrow(() -> statsManager.stopAutoSave());
}
@Test
void testGetAllStats() {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
statsManager.incrementBlocks(player1, "Mine1");
statsManager.incrementBlocks(player2, "Mine2");
assertEquals(2, statsManager.getAllStats().size());
}
@Test
void testGetLeaderboard() {
assertNotNull(statsManager.getLeaderboard());
}
}

View file

@ -0,0 +1,156 @@
package dev.loki.lomines.util;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.*;
/**
* Unit tests for ChunkUtils.
* Tests Paper detection and chunk update functionality.
*/
class ChunkUtilsTest {
@Mock
private World world;
@Mock
private Chunk chunk;
@Mock
private Player player;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testUpdateChunks_withNullRegion_throwsException() {
assertThrows(IllegalArgumentException.class, () -> {
ChunkUtils.updateChunks(null);
});
}
@Test
void testUpdateChunks_withSingleChunkRegion() {
// Create a small region within a single chunk
Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 5, 70, 5);
Cuboid region = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.singletonList(player));
when(chunk.getWorld()).thenReturn(world);
when(chunk.getX()).thenReturn(0);
when(chunk.getZ()).thenReturn(0);
// Should not throw exception
assertDoesNotThrow(() -> ChunkUtils.updateChunks(region));
// Verify chunk was retrieved
verify(world, atLeastOnce()).getChunkAt(0, 0);
}
@Test
void testUpdateChunks_withMultipleChunks() {
// Create a region spanning multiple chunks (16 blocks per chunk)
Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 32, 70, 32);
Cuboid region = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.singletonList(player));
when(chunk.getWorld()).thenReturn(world);
when(chunk.getX()).thenReturn(0);
when(chunk.getZ()).thenReturn(0);
assertDoesNotThrow(() -> ChunkUtils.updateChunks(region));
// Should retrieve multiple chunks (0,0), (0,1), (0,2), (1,0), (1,1), (1,2), (2,0), (2,1), (2,2)
// That's 9 chunks total for a 3x3 chunk area
verify(world, atLeast(9)).getChunkAt(anyInt(), anyInt());
}
@Test
void testUpdateChunks_withNegativeCoordinates() {
// Test with negative coordinates
Location loc1 = new Location(world, -16, 64, -16);
Location loc2 = new Location(world, -1, 70, -1);
Cuboid region = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.singletonList(player));
when(chunk.getWorld()).thenReturn(world);
when(chunk.getX()).thenReturn(-1);
when(chunk.getZ()).thenReturn(-1);
assertDoesNotThrow(() -> ChunkUtils.updateChunks(region));
// Verify chunk was retrieved for negative coordinates
verify(world, atLeastOnce()).getChunkAt(-1, -1);
}
@Test
void testUpdateChunks_withLargeRegion() {
// Test with a large region spanning many chunks
Location loc1 = new Location(world, 0, 0, 0);
Location loc2 = new Location(world, 100, 100, 100);
Cuboid region = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.emptyList());
when(chunk.getWorld()).thenReturn(world);
assertDoesNotThrow(() -> ChunkUtils.updateChunks(region));
// Should handle large regions without issues
// 100 blocks = ~7 chunks per axis, so ~49 chunks total
verify(world, atLeast(40)).getChunkAt(anyInt(), anyInt());
}
@Test
void testUpdateChunks_withNoPlayers() {
// Test behavior when no players are online
Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 5, 70, 5);
Cuboid region = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.emptyList());
when(chunk.getWorld()).thenReturn(world);
// Should not throw exception even with no players
assertDoesNotThrow(() -> ChunkUtils.updateChunks(region));
}
@Test
void testChunkCoordinateCalculation() {
// Test that chunk coordinates are calculated correctly
// Block 0-15 = chunk 0, block 16-31 = chunk 1, etc.
Location loc1 = new Location(world, 0, 64, 0);
Location loc2 = new Location(world, 15, 70, 15);
Cuboid singleChunk = new Cuboid(loc1, loc2);
when(world.getChunkAt(anyInt(), anyInt())).thenReturn(chunk);
when(world.getPlayers()).thenReturn(Collections.emptyList());
when(chunk.getWorld()).thenReturn(world);
ChunkUtils.updateChunks(singleChunk);
// Should only access chunk (0,0)
verify(world, times(1)).getChunkAt(0, 0);
}
}

View file

@ -0,0 +1,180 @@
package dev.loki.lomines.util;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class LocationParserTest {
@Test
void testParseValidLocation() throws ConfigParseException {
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
World mockWorld = mock(World.class);
when(mockWorld.getName()).thenReturn("world");
bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mockWorld);
Location location = LocationParser.parse("world;100.5;64.0;-200.75;90.0;45.0");
assertNotNull(location);
assertEquals(mockWorld, location.getWorld());
assertEquals(100.5, location.getX(), 0.001);
assertEquals(64.0, location.getY(), 0.001);
assertEquals(-200.75, location.getZ(), 0.001);
assertEquals(90.0f, location.getYaw(), 0.001);
assertEquals(45.0f, location.getPitch(), 0.001);
}
}
@Test
void testParseIntegerCoordinates() throws ConfigParseException {
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
World mockWorld = mock(World.class);
when(mockWorld.getName()).thenReturn("world_nether");
bukkit.when(() -> Bukkit.getWorld("world_nether")).thenReturn(mockWorld);
Location location = LocationParser.parse("world_nether;0;128;0;0;0");
assertNotNull(location);
assertEquals(0.0, location.getX(), 0.001);
assertEquals(128.0, location.getY(), 0.001);
assertEquals(0.0, location.getZ(), 0.001);
}
}
@Test
void testParseNegativeCoordinates() throws ConfigParseException {
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
World mockWorld = mock(World.class);
bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mockWorld);
Location location = LocationParser.parse("world;-100;-64;-200;-180;-90");
assertNotNull(location);
assertEquals(-100.0, location.getX(), 0.001);
assertEquals(-64.0, location.getY(), 0.001);
assertEquals(-200.0, location.getZ(), 0.001);
assertEquals(-180.0f, location.getYaw(), 0.001);
assertEquals(-90.0f, location.getPitch(), 0.001);
}
}
@Test
void testParseNullString() {
ConfigParseException exception = assertThrows(
ConfigParseException.class,
() -> LocationParser.parse(null)
);
assertTrue(exception.getMessage().contains("cannot be null or empty"));
}
@Test
void testParseEmptyString() {
ConfigParseException exception = assertThrows(
ConfigParseException.class,
() -> LocationParser.parse("")
);
assertTrue(exception.getMessage().contains("cannot be null or empty"));
}
@Test
void testParseInvalidFormat() {
ConfigParseException exception = assertThrows(
ConfigParseException.class,
() -> LocationParser.parse("world;100;64")
);
assertTrue(exception.getMessage().contains("Invalid location format"));
assertTrue(exception.getMessage().contains("Expected format"));
}
@Test
void testParseInvalidNumbers() {
ConfigParseException exception = assertThrows(
ConfigParseException.class,
() -> LocationParser.parse("world;abc;64;0;0;0")
);
assertTrue(exception.getMessage().contains("Invalid location format"));
}
@Test
void testParseNonExistentWorld() {
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
bukkit.when(() -> Bukkit.getWorld("nonexistent")).thenReturn(null);
ConfigParseException exception = assertThrows(
ConfigParseException.class,
() -> LocationParser.parse("nonexistent;0;64;0;0;0")
);
assertTrue(exception.getMessage().contains("does not exist"));
assertTrue(exception.getMessage().contains("nonexistent"));
}
}
@Test
void testFormatValidLocation() {
World mockWorld = mock(World.class);
when(mockWorld.getName()).thenReturn("world");
Location location = new Location(mockWorld, 100.5, 64.0, -200.75, 90.0f, 45.0f);
String formatted = LocationParser.format(location);
assertEquals("world;100.50;64.00;-200.75;90.00;45.00", formatted);
}
@Test
void testFormatIntegerCoordinates() {
World mockWorld = mock(World.class);
when(mockWorld.getName()).thenReturn("world_nether");
Location location = new Location(mockWorld, 0, 128, 0, 0, 0);
String formatted = LocationParser.format(location);
assertEquals("world_nether;0.00;128.00;0.00;0.00;0.00", formatted);
}
@Test
void testFormatNullLocation() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> LocationParser.format(null)
);
assertTrue(exception.getMessage().contains("Location cannot be null"));
}
@Test
void testFormatNullWorld() {
Location location = new Location(null, 0, 0, 0);
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> LocationParser.format(location)
);
assertTrue(exception.getMessage().contains("world cannot be null"));
}
@Test
void testRoundTrip() throws ConfigParseException {
try (MockedStatic<Bukkit> bukkit = Mockito.mockStatic(Bukkit.class)) {
World mockWorld = mock(World.class);
when(mockWorld.getName()).thenReturn("world");
bukkit.when(() -> Bukkit.getWorld("world")).thenReturn(mockWorld);
Location original = new Location(mockWorld, 100.5, 64.0, -200.75, 90.0f, 45.0f);
String formatted = LocationParser.format(original);
Location parsed = LocationParser.parse(formatted);
assertEquals(original.getX(), parsed.getX(), 0.01);
assertEquals(original.getY(), parsed.getY(), 0.01);
assertEquals(original.getZ(), parsed.getZ(), 0.01);
assertEquals(original.getYaw(), parsed.getYaw(), 0.01);
assertEquals(original.getPitch(), parsed.getPitch(), 0.01);
}
}
}

View file

@ -0,0 +1,128 @@
package dev.loki.lomines.util;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class SelectionManagerTest {
private SelectionManager manager;
private UUID playerId;
@BeforeEach
void setUp() {
manager = new SelectionManager();
playerId = UUID.randomUUID();
}
@Test
void getSelection_createsNewSelectionIfNotExists() {
Selection selection = manager.getSelection(playerId);
assertNotNull(selection);
assertTrue(manager.hasSelection(playerId));
}
@Test
void getSelection_returnsSameInstanceOnMultipleCalls() {
Selection first = manager.getSelection(playerId);
Selection second = manager.getSelection(playerId);
assertSame(first, second);
}
@Test
void getSelection_throwsExceptionForNullPlayerId() {
assertThrows(IllegalArgumentException.class, () -> {
manager.getSelection(null);
});
}
@Test
void findSelection_returnsEmptyWhenNotExists() {
Optional<Selection> result = manager.findSelection(playerId);
assertTrue(result.isEmpty());
}
@Test
void findSelection_returnsSelectionWhenExists() {
Selection created = manager.getSelection(playerId);
Optional<Selection> found = manager.findSelection(playerId);
assertTrue(found.isPresent());
assertSame(created, found.get());
}
@Test
void findSelection_throwsExceptionForNullPlayerId() {
assertThrows(IllegalArgumentException.class, () -> {
manager.findSelection(null);
});
}
@Test
void hasSelection_returnsFalseWhenNotExists() {
assertFalse(manager.hasSelection(playerId));
}
@Test
void hasSelection_returnsTrueWhenExists() {
manager.getSelection(playerId);
assertTrue(manager.hasSelection(playerId));
}
@Test
void hasSelection_throwsExceptionForNullPlayerId() {
assertThrows(IllegalArgumentException.class, () -> {
manager.hasSelection(null);
});
}
@Test
void clearSelection_removesSelection() {
manager.getSelection(playerId);
assertTrue(manager.hasSelection(playerId));
manager.clearSelection(playerId);
assertFalse(manager.hasSelection(playerId));
}
@Test
void clearSelection_doesNothingWhenSelectionNotExists() {
assertDoesNotThrow(() -> {
manager.clearSelection(playerId);
});
}
@Test
void clearSelection_throwsExceptionForNullPlayerId() {
assertThrows(IllegalArgumentException.class, () -> {
manager.clearSelection(null);
});
}
@Test
void multiplePlayersHaveIndependentSelections() {
UUID player1 = UUID.randomUUID();
UUID player2 = UUID.randomUUID();
Selection selection1 = manager.getSelection(player1);
Selection selection2 = manager.getSelection(player2);
assertNotSame(selection1, selection2);
assertTrue(manager.hasSelection(player1));
assertTrue(manager.hasSelection(player2));
manager.clearSelection(player1);
assertFalse(manager.hasSelection(player1));
assertTrue(manager.hasSelection(player2));
}
}

View file

@ -0,0 +1,222 @@
package dev.loki.lomines.util;
import org.bukkit.Location;
import org.bukkit.World;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class SelectionTest {
private World world;
private Selection selection;
@BeforeEach
void setUp() {
world = mock(World.class);
selection = new Selection();
when(world.getName()).thenReturn("world");
}
@Test
void testSetAndGetPoint() {
Location loc = new Location(world, 10, 64, 20);
selection.setPoint(0, loc);
Optional<Location> result = selection.getPoint(0);
assertTrue(result.isPresent());
assertEquals(loc, result.get());
}
@Test
void testGetPointReturnsEmptyWhenNotSet() {
Optional<Location> result = selection.getPoint(5);
assertFalse(result.isPresent());
}
@Test
void testSetPointInvalidIndexThrows() {
Location loc = new Location(world, 10, 64, 20);
assertThrows(IllegalArgumentException.class, () -> selection.setPoint(-1, loc));
assertThrows(IllegalArgumentException.class, () -> selection.setPoint(10, loc));
}
@Test
void testGetPointInvalidIndexThrows() {
assertThrows(IllegalArgumentException.class, () -> selection.getPoint(-1));
assertThrows(IllegalArgumentException.class, () -> selection.getPoint(10));
}
@Test
void testGetPair() {
Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30);
selection.setPoint(0, loc1);
selection.setPoint(1, loc2);
Location[] pair = selection.getPair(0);
assertEquals(2, pair.length);
assertEquals(loc1, pair[0]);
assertEquals(loc2, pair[1]);
}
@Test
void testGetPairWithNulls() {
Location loc1 = new Location(world, 10, 64, 20);
selection.setPoint(2, loc1);
Location[] pair = selection.getPair(1);
assertEquals(2, pair.length);
assertEquals(loc1, pair[0]);
assertNull(pair[1]);
}
@Test
void testGetPairInvalidIndexThrows() {
assertThrows(IllegalArgumentException.class, () -> selection.getPair(-1));
assertThrows(IllegalArgumentException.class, () -> selection.getPair(5));
}
@Test
void testHasPairReturnsTrueWhenBothSet() {
Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30);
selection.setPoint(0, loc1);
selection.setPoint(1, loc2);
assertTrue(selection.hasPair(0));
}
@Test
void testHasPairReturnsFalseWhenOnlyOneSet() {
Location loc1 = new Location(world, 10, 64, 20);
selection.setPoint(0, loc1);
assertFalse(selection.hasPair(0));
}
@Test
void testHasPairReturnsFalseWhenNoneSet() {
assertFalse(selection.hasPair(0));
}
@Test
void testHasPairInvalidIndexThrows() {
assertThrows(IllegalArgumentException.class, () -> selection.hasPair(-1));
assertThrows(IllegalArgumentException.class, () -> selection.hasPair(5));
}
@Test
void testClear() {
Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30);
selection.setPoint(0, loc1);
selection.setPoint(1, loc2);
selection.setPoint(5, loc1);
selection.clear();
assertFalse(selection.getPoint(0).isPresent());
assertFalse(selection.getPoint(1).isPresent());
assertFalse(selection.getPoint(5).isPresent());
}
@Test
void testToCuboidsWithNoPairs() {
List<Cuboid> cuboids = selection.toCuboids();
assertTrue(cuboids.isEmpty());
}
@Test
void testToCuboidsWithOnePair() {
Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30);
selection.setPoint(0, loc1);
selection.setPoint(1, loc2);
List<Cuboid> cuboids = selection.toCuboids();
assertEquals(1, cuboids.size());
assertTrue(cuboids.get(0).contains(new Location(world, 15, 69, 25)));
}
@Test
void testToCuboidsWithMultiplePairs() {
Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30);
Location loc3 = new Location(world, 30, 64, 40);
Location loc4 = new Location(world, 40, 74, 50);
selection.setPoint(0, loc1);
selection.setPoint(1, loc2);
selection.setPoint(2, loc3);
selection.setPoint(3, loc4);
List<Cuboid> cuboids = selection.toCuboids();
assertEquals(2, cuboids.size());
}
@Test
void testToCuboidsSkipsIncompletePairs() {
Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30);
Location loc3 = new Location(world, 30, 64, 40);
selection.setPoint(0, loc1);
selection.setPoint(1, loc2);
selection.setPoint(2, loc3);
// Point 3 not set
List<Cuboid> cuboids = selection.toCuboids();
assertEquals(1, cuboids.size());
}
@Test
void testPairInvariant() {
// Test that pairs are organized as 2i and 2i+1
Location loc1 = new Location(world, 10, 64, 20);
Location loc2 = new Location(world, 20, 74, 30);
// Pair 0: indices 0 and 1
selection.setPoint(0, loc1);
selection.setPoint(1, loc2);
assertTrue(selection.hasPair(0));
// Pair 1: indices 2 and 3
selection.setPoint(2, loc1);
selection.setPoint(3, loc2);
assertTrue(selection.hasPair(1));
// Pair 2: indices 4 and 5
selection.setPoint(4, loc1);
selection.setPoint(5, loc2);
assertTrue(selection.hasPair(2));
// Pair 3: indices 6 and 7
selection.setPoint(6, loc1);
selection.setPoint(7, loc2);
assertTrue(selection.hasPair(3));
// Pair 4: indices 8 and 9
selection.setPoint(8, loc1);
selection.setPoint(9, loc2);
assertTrue(selection.hasPair(4));
}
}

View file

@ -0,0 +1,176 @@
package dev.loki.lomines.util;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TimeFormatterTest {
@Test
void testFormatMinutesSeconds() {
// 1 minute = 60 seconds = 1200 ticks
assertEquals("01:00", TimeFormatter.format(1200, "mm:ss"));
// 2 minutes 30 seconds = 150 seconds = 3000 ticks
assertEquals("02:30", TimeFormatter.format(3000, "mm:ss"));
// 0 seconds
assertEquals("00:00", TimeFormatter.format(0, "mm:ss"));
// 59 seconds = 1180 ticks
assertEquals("00:59", TimeFormatter.format(1180, "mm:ss"));
// 10 minutes 5 seconds = 605 seconds = 12100 ticks
assertEquals("10:05", TimeFormatter.format(12100, "mm:ss"));
}
@Test
void testFormatHoursMinutesSeconds() {
// 1 hour = 3600 seconds = 72000 ticks
assertEquals("01:00:00", TimeFormatter.format(72000, "hh:mm:ss"));
// 1 hour 30 minutes 45 seconds = 5445 seconds = 108900 ticks
assertEquals("01:30:45", TimeFormatter.format(108900, "hh:mm:ss"));
// 0 seconds
assertEquals("00:00:00", TimeFormatter.format(0, "hh:mm:ss"));
// 2 hours 5 minutes 3 seconds = 7503 seconds = 150060 ticks
assertEquals("02:05:03", TimeFormatter.format(150060, "hh:mm:ss"));
// 23 hours 59 minutes 59 seconds = 86399 seconds = 1727980 ticks
assertEquals("23:59:59", TimeFormatter.format(1727980, "hh:mm:ss"));
}
@Test
void testFormatSeconds() {
// 100 ticks = 5 seconds
assertEquals("5", TimeFormatter.format(100, "s"));
// 1200 ticks = 60 seconds
assertEquals("60", TimeFormatter.format(1200, "s"));
// 0 ticks = 0 seconds
assertEquals("0", TimeFormatter.format(0, "s"));
// 6000 ticks = 300 seconds (5 minutes)
assertEquals("300", TimeFormatter.format(6000, "s"));
}
@Test
void testFormatMinutes() {
// 1200 ticks = 60 seconds = 1 minute
assertEquals("1", TimeFormatter.format(1200, "m"));
// 6000 ticks = 300 seconds = 5 minutes
assertEquals("5", TimeFormatter.format(6000, "m"));
// 0 ticks = 0 minutes
assertEquals("0", TimeFormatter.format(0, "m"));
// 72000 ticks = 3600 seconds = 60 minutes
assertEquals("60", TimeFormatter.format(72000, "m"));
// 1100 ticks = 55 seconds = 0 minutes (truncated)
assertEquals("0", TimeFormatter.format(1100, "m"));
}
@Test
void testFormatHours() {
// 72000 ticks = 3600 seconds = 1 hour
assertEquals("1", TimeFormatter.format(72000, "h"));
// 144000 ticks = 7200 seconds = 2 hours
assertEquals("2", TimeFormatter.format(144000, "h"));
// 0 ticks = 0 hours
assertEquals("0", TimeFormatter.format(0, "h"));
// 360000 ticks = 18000 seconds = 5 hours
assertEquals("5", TimeFormatter.format(360000, "h"));
// 71999 ticks = 3599 seconds = 0 hours (truncated)
assertEquals("0", TimeFormatter.format(71999, "h"));
}
@Test
void testFormatCaseInsensitive() {
assertEquals("01:00", TimeFormatter.format(1200, "MM:SS"));
assertEquals("01:00:00", TimeFormatter.format(72000, "HH:MM:SS"));
assertEquals("5", TimeFormatter.format(100, "S"));
assertEquals("1", TimeFormatter.format(1200, "M"));
assertEquals("1", TimeFormatter.format(72000, "H"));
}
@Test
void testFormatNegativeTicks() {
// Negative ticks should be treated as 0
assertEquals("00:00", TimeFormatter.format(-100, "mm:ss"));
assertEquals("00:00:00", TimeFormatter.format(-1000, "hh:mm:ss"));
assertEquals("0", TimeFormatter.format(-50, "s"));
}
@Test
void testFormatNullFormat() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> TimeFormatter.format(1200, null)
);
assertTrue(exception.getMessage().contains("cannot be null or empty"));
}
@Test
void testFormatEmptyFormat() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> TimeFormatter.format(1200, "")
);
assertTrue(exception.getMessage().contains("cannot be null or empty"));
}
@Test
void testFormatWhitespaceFormat() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> TimeFormatter.format(1200, " ")
);
assertTrue(exception.getMessage().contains("cannot be null or empty"));
}
@Test
void testFormatUnknownFormat() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> TimeFormatter.format(1200, "invalid")
);
assertTrue(exception.getMessage().contains("Unknown time format"));
assertTrue(exception.getMessage().contains("invalid"));
assertTrue(exception.getMessage().contains("Supported formats"));
}
@Test
void testFormatLargeValues() {
// 1 million ticks = 50000 seconds = 833 minutes = 13 hours 53 minutes 20 seconds
assertEquals("833:20", TimeFormatter.format(1000000, "mm:ss"));
assertEquals("13:53:20", TimeFormatter.format(1000000, "hh:mm:ss"));
assertEquals("50000", TimeFormatter.format(1000000, "s"));
assertEquals("833", TimeFormatter.format(1000000, "m"));
assertEquals("13", TimeFormatter.format(1000000, "h"));
}
@Test
void testFormatResetTimerExample() {
// Typical reset timer: 6000 ticks (5 minutes)
assertEquals("05:00", TimeFormatter.format(6000, "mm:ss"));
// After 1 minute: 4800 ticks remaining
assertEquals("04:00", TimeFormatter.format(4800, "mm:ss"));
// After 4 minutes 30 seconds: 600 ticks remaining
assertEquals("00:30", TimeFormatter.format(600, "mm:ss"));
// Just before reset: 20 ticks remaining (1 second)
assertEquals("00:01", TimeFormatter.format(20, "mm:ss"));
}
}