diff --git a/Human-Tests.md b/Human-Tests.md index 055a782..ddeadad 100644 --- a/Human-Tests.md +++ b/Human-Tests.md @@ -85,6 +85,7 @@ Run `./gradlew test` for the server-free JUnit suite. It has 26 tests covering: - `MetropolisPlotGoCommandTest`: self and target teleportation, ambiguous memberships, absent membership, and unsafe-destination feedback. - `MetropolisPlotReserveCommandTest`: parsing six coordinates and rejecting malformed coordinates. - `MetropolisHomeAcquireTest`: acquisition below the limit and refusal at the limit. +- `MetropolisDebugGenerateTestHomesCommandTest`: asynchronous tick-queued generation, one home per interval, no overlapping batches, and continuing test-player names across repeated runs. The two opt-in tests in `src/integrationTest` require a running Paper server and RCON. They check that Metropolis appears in `/plugins` and that a coordinate reservation is persisted by WorldGuard. They do not currently verify restart persistence, join allocation, WorldEdit-selection reservations, permissions, teleports, generated blocks, protection behavior, or the other command workflows above. Run them with the environment variables documented in `README.md`; use a disposable server because the reservation test changes its WorldGuard data. diff --git a/README.md b/README.md index 4ffa5ae..5346207 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Assign these permission nodes to groups or individual users in your permission p | `/metropolis-plot-reserve ` | `metropolis.plot.reserve` | Creates a named reservation from explicit bounds. A player can instead run `/metropolis-plot-reserve ` with a WorldEdit selection. | | `/metropolis-plot-go [playerName]` | Reservation owner/member; `metropolis.plot.go` to target others or bypass membership | Teleports the sender or an online target to a reservation they own or belong to. If they have several, specify one with `/metropolis-plot-go [playerName]`. | | `/metropolis-flag-reset` | `metropolis.flag.reset` | Reapplies Metropolis protection flags to the City and home regions. | -| `/metropolis-debug-generatetesthomes ` | `metropolis.debug` | Generates test homes; intended for development servers. | +| `/metropolis-debug-generatetesthomes ` | `metropolis.debug` | Queues test-home generation on the main thread, creating one home every two ticks; intended for development servers. | ## License diff --git a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisDebugGenerateTestHomesCommand.java b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisDebugGenerateTestHomesCommand.java index d89c90f..deafb94 100644 --- a/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisDebugGenerateTestHomesCommand.java +++ b/src/main/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisDebugGenerateTestHomesCommand.java @@ -17,15 +17,22 @@ along with Metropolis. If not, see . package com.majinnaibu.bukkitplugins.metropolis.commands; -import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin; +import java.util.logging.Level; +import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin; +import com.majinnaibu.bukkitplugins.metropolis.PlayerHome; + +import org.bukkit.OfflinePlayer; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.scheduler.BukkitTask; public class MetropolisDebugGenerateTestHomesCommand implements CommandExecutor { + private static final long TICKS_BETWEEN_HOMES = 2L; - private MetropolisPlugin _plugin; + private final MetropolisPlugin _plugin; + private BukkitTask _activeTask; public MetropolisDebugGenerateTestHomesCommand(MetropolisPlugin plugin) { _plugin = plugin; @@ -33,15 +40,102 @@ public class MetropolisDebugGenerateTestHomesCommand implements CommandExecutor @Override public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) { + if (args.length != 1) { + sender.sendMessage("Usage: /metropolis-debug-generatetesthomes "); + return false; + } + + int numHomes; try { - int numHomes = Integer.parseInt(args[0]); - for (int i = 1; i <= numHomes; i++) { - _plugin.generateHome(String.format("test%d", i)); + numHomes = Integer.parseInt(args[0]); + } catch (NumberFormatException ex) { + sender.sendMessage("Usage: /metropolis-debug-generatetesthomes "); + return false; + } + + if (numHomes < 1) { + sender.sendMessage("Usage: /metropolis-debug-generatetesthomes "); + return false; + } + + if (_activeTask != null && !_activeTask.isCancelled()) { + sender.sendMessage("[Metropolis] Test-home generation is already in progress."); + return false; + } + + TestHomeBatch batch = new TestHomeBatch(sender, numHomes); + _activeTask = + _plugin.getServer().getScheduler().runTaskTimer(_plugin, batch, 1L, TICKS_BETWEEN_HOMES); + sender.sendMessage( + "[Metropolis] Queued " + + numHomes + + " test homes; generating one every " + + TICKS_BETWEEN_HOMES + + " ticks."); + return true; + } + + private final class TestHomeBatch implements Runnable { + private final CommandSender sender; + private final int requestedHomes; + private int generatedHomes; + private int nextTestNumber = 1; + private boolean finished; + + private TestHomeBatch(CommandSender sender, int requestedHomes) { + this.sender = sender; + this.requestedHomes = requestedHomes; + } + + @Override + public void run() { + if (finished) { + return; } - return true; - } catch (NumberFormatException ex) { - return false; + int testNumber = nextTestNumber++; + String testName = "Test" + testNumber; + try { + OfflinePlayer testPlayer = _plugin.getServer().getOfflinePlayer(testName); + if (_plugin.getNumPlots(testPlayer.getUniqueId()) > 0) { + return; + } + + PlayerHome home = _plugin.generateHome(testPlayer, 1); + if (home == null) { + sender.sendMessage( + "[Metropolis] Could not generate " + + testName + + "; completed " + + generatedHomes + + " of " + + requestedHomes + + " test homes."); + finish(); + return; + } + + generatedHomes++; + if (generatedHomes == requestedHomes) { + sender.sendMessage("[Metropolis] Generated " + generatedHomes + " test homes."); + finish(); + } + } catch (RuntimeException exception) { + _plugin + .getLogger() + .log(Level.SEVERE, "Failed to generate test home " + testName, exception); + sender.sendMessage( + "[Metropolis] Test-home generation failed after " + generatedHomes + " homes."); + finish(); + } + } + + private void finish() { + finished = true; + if (_activeTask != null) { + _activeTask.cancel(); + _activeTask = null; + } } } } diff --git a/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisDebugGenerateTestHomesCommandTest.java b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisDebugGenerateTestHomesCommandTest.java new file mode 100644 index 0000000..4d7b013 --- /dev/null +++ b/src/test/java/com/majinnaibu/bukkitplugins/metropolis/commands/MetropolisDebugGenerateTestHomesCommandTest.java @@ -0,0 +1,129 @@ +/* +This file is part of Metropolis. + +Metropolis is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Metropolis is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with Metropolis. If not, see . +*/ + +package com.majinnaibu.bukkitplugins.metropolis.commands; + +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.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import com.majinnaibu.bukkitplugins.metropolis.MetropolisPlugin; +import com.majinnaibu.bukkitplugins.metropolis.PlayerHome; + +import org.bukkit.OfflinePlayer; +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.scheduler.BukkitScheduler; +import org.bukkit.scheduler.BukkitTask; +import org.junit.jupiter.api.Test; + +class MetropolisDebugGenerateTestHomesCommandTest { + @Test + void repeatedRunsContinueWithTheNextUnusedTestName() { + MetropolisPlugin plugin = mock(MetropolisPlugin.class); + Server server = mock(Server.class); + BukkitScheduler scheduler = mock(BukkitScheduler.class); + CommandSender sender = mock(CommandSender.class); + Set existingHomeOwners = new HashSet<>(); + List generatedNames = new ArrayList<>(); + List scheduledBatches = new ArrayList<>(); + + when(plugin.getServer()).thenReturn(server); + when(server.getScheduler()).thenReturn(scheduler); + when(scheduler.runTaskTimer(eq(plugin), any(Runnable.class), eq(1L), eq(2L))) + .thenAnswer( + invocation -> { + scheduledBatches.add(invocation.getArgument(1)); + return mock(BukkitTask.class); + }); + when(server.getOfflinePlayer(anyString())) + .thenAnswer( + invocation -> { + String name = invocation.getArgument(0); + OfflinePlayer player = mock(OfflinePlayer.class); + UUID playerId = UUID.nameUUIDFromBytes(name.getBytes(StandardCharsets.UTF_8)); + when(player.getName()).thenReturn(name); + when(player.getUniqueId()).thenReturn(playerId); + return player; + }); + when(plugin.getNumPlots(any(UUID.class))) + .thenAnswer(invocation -> existingHomeOwners.contains(invocation.getArgument(0)) ? 1 : 0); + when(plugin.generateHome(any(OfflinePlayer.class), eq(1))) + .thenAnswer( + invocation -> { + OfflinePlayer player = invocation.getArgument(0); + generatedNames.add(player.getName()); + existingHomeOwners.add(player.getUniqueId()); + return mock(PlayerHome.class); + }); + + MetropolisDebugGenerateTestHomesCommand executor = + new MetropolisDebugGenerateTestHomesCommand(plugin); + + assertTrue(executor.onCommand(sender, mock(Command.class), "testhomes", new String[] {"10"})); + assertTrue(generatedNames.isEmpty()); + assertEquals(1, scheduledBatches.size()); + assertFalse(executor.onCommand(sender, mock(Command.class), "testhomes", new String[] {"10"})); + + scheduledBatches.getFirst().run(); + assertEquals(List.of("Test1"), generatedNames); + for (int tick = 1; tick < 10; tick++) { + scheduledBatches.getFirst().run(); + } + assertEquals(10, generatedNames.size()); + + assertTrue(executor.onCommand(sender, mock(Command.class), "testhomes", new String[] {"10"})); + assertEquals(2, scheduledBatches.size()); + for (int tick = 0; tick < 20; tick++) { + scheduledBatches.get(1).run(); + } + + List expectedNames = new ArrayList<>(); + for (int testNumber = 1; testNumber <= 20; testNumber++) { + expectedNames.add("Test" + testNumber); + } + assertEquals(expectedNames, generatedNames); + } + + @Test + void rejectsNonPositiveCountsWithoutGeneratingHomes() { + MetropolisPlugin plugin = mock(MetropolisPlugin.class); + MetropolisDebugGenerateTestHomesCommand executor = + new MetropolisDebugGenerateTestHomesCommand(plugin); + + assertFalse( + executor.onCommand( + mock(CommandSender.class), mock(Command.class), "testhomes", new String[] {"0"})); + + verify(plugin, never()).generateHome(any(OfflinePlayer.class), eq(1)); + } +}