diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 853e437..19b494b 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,30 +9,9 @@ jobs: approve-and-merge-dependabot: if: "github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]'" needs: [ "run-ci" ] - runs-on: "ubuntu-latest" + uses: Jikoo/PlanarActions/.github/workflows/dependabot_automerge.yml@master + with: + pr-url: ${{github.event.pull_request.html_url}} permissions: contents: write pull-requests: write - steps: - # Always approve PRs from Dependabot. - - name: Approve - run: gh pr review --approve "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - # Fetch Dependabot metadata for finer decisionmaking later. - - name: Fetch Dependabot metadata - id: metadata - uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - # Enable auto-merge for the PR. - # Auto-merge is used rather than a direct merge so that any other required checks can pass. - - name: Enable auto-merge for minor/patch updates - if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' - run: gh pr merge --auto --squash "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} diff --git a/pom.xml b/pom.xml index 6ad54ac..03e6427 100644 --- a/pom.xml +++ b/pom.xml @@ -59,8 +59,8 @@ - papermc - https://repo.papermc.io/repository/maven-public/ + spigotmc + https://hub.spigotmc.org/nexus/content/groups/public/ jitpack.io @@ -76,8 +76,8 @@ provided - io.papermc.paper - paper-api + org.spigotmc + spigot-api 1.21.11-R0.1-SNAPSHOT provided @@ -91,7 +91,7 @@ com.github.jikoo planarenchanting - fe946163f8 + 3.0.4 compile @@ -175,6 +175,9 @@ true + + + com.github.jikoo:* diff --git a/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java b/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java index 31b8f32..dacc9d7 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPlugin.java @@ -19,24 +19,12 @@ public class EnchantableBlocksPlugin extends JavaPlugin { private EnchantableBlockManager blockManager; - @Override - public void onLoad() { - this.blockManager = new EnchantableBlockManager(this); - } - @Override public void onEnable() { - try { - Class.forName("io.papermc.paper.configuration.ServerConfiguration"); - } catch (ClassNotFoundException e) { - getLogger().severe("EnchantableBlocks requires Paper; Spigot's enchantment API is missing features."); - getLogger().severe("Please vote for https://hub.spigotmc.org/jira/browse/SPIGOT-7838 for Spigot support."); - getServer().getPluginManager().disablePlugin(this); - return; - } - this.saveDefaultConfig(); + this.blockManager = new EnchantableBlockManager(this); + // Register generic listeners for block management. this.getServer().getPluginManager().registerEvents( new WorldListener(this, getBlockManager()), this); @@ -79,7 +67,7 @@ public boolean onCommand( @NotNull String label, @NotNull String @NotNull [] args) { if (args.length < 1 || !args[0].equalsIgnoreCase("reload")) { - sender.sendMessage("EnchantableBlocks v" + getPluginMeta().getVersion()); + sender.sendMessage("EnchantableBlocks v" + getDescription().getVersion()); return false; } @@ -87,7 +75,7 @@ public boolean onCommand( this.blockManager.getRegistry().reload(); sender.sendMessage( "[EnchantableBlocks v" - + getPluginMeta().getVersion() + + getDescription().getVersion() + "] Reloaded config and registry cache."); return true; } diff --git a/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnace.java b/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnace.java index d43f833..cf39f66 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnace.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnace.java @@ -3,7 +3,6 @@ import com.github.jikoo.enchantableblocks.block.EnchantableBlock; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager; import com.github.jikoo.enchantableblocks.util.MathHelper; -import com.github.jikoo.planarenchanting.util.ItemUtil; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.block.BlockState; @@ -11,7 +10,7 @@ import org.bukkit.configuration.ConfigurationSection; import org.bukkit.enchantments.Enchantment; import org.bukkit.event.Event; -import org.bukkit.event.inventory.FurnaceSmeltEvent; +import org.bukkit.event.block.BlockCookEvent; import org.bukkit.inventory.CookingRecipe; import org.bukkit.inventory.FurnaceInventory; import org.bukkit.inventory.ItemStack; @@ -20,6 +19,8 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; +import java.lang.reflect.Method; + /** * Track and manage effects for enchanted furnace variants. */ @@ -144,15 +145,31 @@ public boolean shouldPause(final @Nullable Event event) { ItemStack input; ItemStack result; CookingRecipe recipe; - if (event instanceof FurnaceSmeltEvent smeltEvent) { - // Special case FurnaceSmeltEvent: smelt has not completed, input and result are different. + if (event instanceof BlockCookEvent cookEvent) { + // Special case BlockCookEvent: smelt has not completed, input and result are different. + // On Paper, the BlockCookEvent also provides recipes. + try { + Method getRecipe = BlockCookEvent.class.getDeclaredMethod("getRecipe"); + recipe = (CookingRecipe) getRecipe.invoke(cookEvent); + } catch (ReflectiveOperationException | ClassCastException e) { + recipe = null; + } // Decrease input for post-smelt - input = smeltEvent.getSource().clone(); + input = cookEvent.getSource().clone(); input.setAmount(input.getAmount() - 1); // Use post-smelt result - result = smeltEvent.getResult(); - // FurnaceSmeltEvent is the only pause event that provides the active recipe. - recipe = smeltEvent.getRecipe(); + result = cookEvent.getResult(); + ItemStack output = furnace.getInventory().getResult(); + if (output != null && output.getType() != Material.AIR) { + // There's an edge case where a plugin messes with the event and the existing result might mismatch. + // This will cause undefined implementation-specific behavior. Just let it go. + if (!output.isSimilar(result)) { + return false; + } + // Otherwise, clone and add up amounts. + result = result.clone(); + result.setAmount(result.getAmount() + output.getAmount()); + } } else { // In all other cases use current contents of furnace. FurnaceInventory inventory = furnace.getInventory(); @@ -198,13 +215,13 @@ private boolean isFreezableState( @Nullable CookingRecipe recipe ) { // Is there no input? - if (ItemUtil.isEmpty(input)) { + if (input == null || input.getType() == Material.AIR || input.getAmount() <= 0) { return true; } // Is the result slot too full for more product? - if (!ItemUtil.isEmpty(result)) { - int stack = result.getType().getMaxStackSize(); + if (result != null && result.getType() != Material.AIR) { + int stack = result.getMaxStackSize(); if (result.getAmount() >= stack) { return true; } @@ -278,9 +295,12 @@ public boolean resume(boolean checkState) { return false; } - furnace.setBurnTime( - MathHelper.clampPositiveShort(((long) furnace.getBurnTime()) + this.getFrozenTicks())); - furnace.update(true); + // If the furnace is burning for longer than the client can display correctly, don't clamp it. + // Whoever caused the problem should probably deal with it. + if (furnace.getBurnTime() < Short.MAX_VALUE) { + furnace.setBurnTime(MathHelper.clampPositiveShort(furnace.getBurnTime() + this.getFrozenTicks())); + furnace.update(true); + } this.setFrozenTicks((short) 0); return true; } diff --git a/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java b/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java index 39341e5..ad72cc7 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListener.java @@ -106,7 +106,7 @@ void applyFortune( @NotNull FurnaceSmeltEvent event, @NotNull IntSupplier bonusCalculator) { ItemStack result = event.getResult(); - int tillFullStack = result.getType().getMaxStackSize() - result.getAmount(); + int tillFullStack = result.getMaxStackSize() - result.getAmount(); // Ignore results that are already full. if (tillFullStack == 0) { diff --git a/src/main/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfig.java b/src/main/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfig.java index d026ad8..24d555d 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfig.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfig.java @@ -38,7 +38,8 @@ protected EnchantableBlockConfig(@NotNull ConfigurationSection configurationSect this.tableEnchantability = new EnchantabilitySetting( section, "tableEnchantability", - Enchantability.STONE); + new Enchantability(5) + ); this.tableDisabledEnchants = new SetEnchantSetting( section, "tableDisabledEnchantments", diff --git a/src/main/java/com/github/jikoo/enchantableblocks/config/data/EnchantabilitySetting.java b/src/main/java/com/github/jikoo/enchantableblocks/config/data/EnchantabilitySetting.java index 4930613..870f7ea 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/config/data/EnchantabilitySetting.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/config/data/EnchantabilitySetting.java @@ -1,9 +1,8 @@ package com.github.jikoo.enchantableblocks.config.data; import com.github.jikoo.planarenchanting.table.Enchantability; +import com.github.jikoo.planarenchanting.table.EnchantabilityCategory; import com.github.jikoo.planarwrappers.config.ParsedSimpleSetting; -import java.lang.reflect.Field; -import java.util.Locale; import org.bukkit.configuration.ConfigurationSection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -31,17 +30,7 @@ public EnchantabilitySetting( } catch (NumberFormatException ignored) { // Not a number, may be a field name. } - value = value.toUpperCase(Locale.ROOT); - try { - Field field = Enchantability.class.getDeclaredField(value); - Object fieldValue = field.get(null); - if (fieldValue instanceof Enchantability enchantability) { - return enchantability; - } - return null; - } catch (NoSuchFieldException | IllegalAccessException e) { - return null; - } + return EnchantabilityCategory.get(value); } } diff --git a/src/main/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanter.java b/src/main/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanter.java index 94e61d2..0e186e8 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanter.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanter.java @@ -2,8 +2,16 @@ import com.github.jikoo.enchantableblocks.registry.EnchantableBlockRegistry; import com.github.jikoo.enchantableblocks.util.enchant.BlockAnvil; +import com.github.jikoo.enchantableblocks.util.enchant.BlockAnvilBehavior; import com.github.jikoo.planarenchanting.anvil.AnvilResult; -import com.github.jikoo.planarenchanting.util.ItemUtil; +import com.github.jikoo.planarenchanting.anvil.ComponentAnvilFunctions; +import com.github.jikoo.planarenchanting.anvil.ComponentTemperer; +import com.github.jikoo.planarenchanting.anvil.ComponentViewState; +import com.github.jikoo.planarenchanting.anvil.MetaAnvilFunctions; +import com.github.jikoo.planarenchanting.anvil.MetaTemperer; +import com.github.jikoo.planarenchanting.anvil.MetaViewState; +import com.github.jikoo.planarenchanting.anvil.WorkPiece; +import com.github.jikoo.planarenchanting.util.ServerCapabilities; import org.bukkit.Material; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -23,6 +31,7 @@ public class AnvilEnchanter implements Listener { private final Plugin plugin; private final EnchantableBlockRegistry registry; + private final BlockAnvil anvil; /** * Construct a new {@code AnvilEnchanter} to provide enchantments for blocks. @@ -33,6 +42,18 @@ public class AnvilEnchanter implements Listener { public AnvilEnchanter(@NotNull Plugin plugin, @NotNull EnchantableBlockRegistry registry) { this.plugin = plugin; this.registry = registry; + + if (ServerCapabilities.DATA_COMPONENT) { + anvil = new BlockAnvil<>( + view1 -> new WorkPiece<>(new ComponentViewState(view1), ComponentTemperer.INSTANCE), + ComponentAnvilFunctions.INSTANCE + ); + } else { + anvil = new BlockAnvil<>( + view1 -> new WorkPiece<>(new MetaViewState(view1), MetaTemperer.INSTANCE), + MetaAnvilFunctions.INSTANCE + ); + } } @EventHandler(priority = EventPriority.HIGH) @@ -55,8 +76,7 @@ void onPrepareAnvil(@NotNull PrepareAnvilEvent event) { return; } - var operation = new BlockAnvil(registration, clicker.getWorld().getName()); - final var result = operation.getResult(view); + final var result = anvil.getResult(view, new BlockAnvilBehavior<>(registration, clicker.getWorld().getName())); if (result == AnvilResult.EMPTY) { return; @@ -95,9 +115,11 @@ void onPrepareAnvil(@NotNull PrepareAnvilEvent event) { boolean areItemsInvalid( @Nullable ItemStack base, @Nullable ItemStack addition) { - return ItemUtil.isEmpty(base) + return base == null + || base.getType() == Material.AIR || base.getAmount() != 1 - || ItemUtil.isEmpty(addition) + || addition == null + || addition.getAmount() < 1 || (addition.getType() != Material.ENCHANTED_BOOK && addition.getType() != base.getType()); } diff --git a/src/main/java/com/github/jikoo/enchantableblocks/listener/TableEnchanter.java b/src/main/java/com/github/jikoo/enchantableblocks/listener/TableEnchanter.java index 0a82ee5..318a3de 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/listener/TableEnchanter.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/listener/TableEnchanter.java @@ -4,8 +4,6 @@ import com.github.jikoo.planarenchanting.table.EnchantingTable; import com.github.jikoo.planarenchanting.table.TableEnchantListener; import com.google.common.collect.Multimap; -import java.util.ArrayList; -import java.util.function.BiPredicate; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; @@ -14,10 +12,14 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.function.BiPredicate; + /** * Listener for handling enchanting in an enchantment table. */ public class TableEnchanter extends TableEnchantListener { + private final EnchantableBlockRegistry registry; /** diff --git a/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java b/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java index 51064f4..241a461 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipe.java @@ -22,15 +22,18 @@ public EmptyCookingRecipe(@NotNull NamespacedKey key) { key, new ItemStack(Material.DIRT), new RecipeChoice() { - @NotNull + /** + * @deprecated This exists only because the Bukkit API is very, very backwards-compatibility-friendly. + * @return an empty ItemStack + */ + @Deprecated(since = "1.13.1") @Override - public ItemStack getItemStack() { + public @NotNull ItemStack getItemStack() { return new ItemStack(Material.AIR); } - @NotNull @Override - public RecipeChoice clone() { + public @NotNull RecipeChoice clone() { try { return (RecipeChoice) super.clone(); } catch (CloneNotSupportedException e) { @@ -56,7 +59,8 @@ public int hashCode() { } }, 0, - 0); + 0 + ); } @Override diff --git a/src/main/java/com/github/jikoo/enchantableblocks/util/MathHelper.java b/src/main/java/com/github/jikoo/enchantableblocks/util/MathHelper.java index b90739b..2d7efcc 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/util/MathHelper.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/util/MathHelper.java @@ -9,7 +9,7 @@ public final class MathHelper { * @return the clamped value */ public static short clampPositiveShort(double value) { - return clampPositiveShort((long) value); + return (short) Math.clamp(value, 0D, Short.MAX_VALUE); } /** @@ -18,8 +18,8 @@ public static short clampPositiveShort(double value) { * @param value the value to clamp * @return the clamped value */ - public static short clampPositiveShort(long value) { - return (short) Math.max(0, Math.min(Short.MAX_VALUE, value)); + public static short clampPositiveShort(int value) { + return (short) Math.clamp(value, 0, Short.MAX_VALUE); } /** diff --git a/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvil.java b/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvil.java index e39bda5..740aeaf 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvil.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvil.java @@ -1,49 +1,53 @@ package com.github.jikoo.enchantableblocks.util.enchant; -import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; import com.github.jikoo.planarenchanting.anvil.Anvil; -import com.github.jikoo.planarenchanting.anvil.AnvilFunctions; +import com.github.jikoo.planarenchanting.anvil.AnvilFunctionsProvider; import com.github.jikoo.planarenchanting.anvil.AnvilResult; -import com.github.jikoo.planarenchanting.anvil.AnvilState; +import com.github.jikoo.planarenchanting.anvil.WorkPiece; import org.bukkit.inventory.view.AnvilView; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NullMarked; + +import java.util.function.Function; /** - * A simplified {@link Anvil} designed for use with - * {@link com.github.jikoo.enchantableblocks.block.EnchantableBlock} implementations. Unlike a - * normal operation, this operation does not validate items! + * A minimal {@link com.github.jikoo.planarenchanting.anvil.Anvil}-like representation of + * the operations performed to produce an anvil result. */ -public class BlockAnvil extends Anvil { +@NullMarked +public class BlockAnvil { + + private final Function> createWork; + private final AnvilFunctionsProvider functions; /** * A simplified {@link Anvil} designed for use with * {@link com.github.jikoo.enchantableblocks.block.EnchantableBlock} implementations. * - * @param registration the {@link EnchantableRegistration} for the block - * @param worldName the name of the world the operation is applied in + * @param createWork the method for creating a new {@link WorkPiece} + * @param functions the provider for default anvil functions */ public BlockAnvil( - @NotNull EnchantableRegistration registration, - @NotNull String worldName) { - super(new BlockAnvilBehavior(registration, worldName)); + Function> createWork, + AnvilFunctionsProvider functions + ) { + this.createWork = createWork; + this.functions = functions; } - @Override - public @NotNull AnvilResult getResult(@NotNull AnvilView view) { - var state = new AnvilState(view); - // Base and addition have already been validated. - - // Apply base cost. - apply(state, AnvilFunctions.PRIOR_WORK_LEVEL_COST); + public AnvilResult getResult(AnvilView view, BlockAnvilBehavior behavior) { + WorkPiece workPiece = createWork.apply(view); + // Base and addition have already been validated. Vanilla handles the rename-only case. + // Apply level cost increase from prior work. + workPiece.apply(behavior, functions.addPriorWorkLevelCost()); // Apply the rename function first, then update prior work cost - both update prior work. - apply(state, AnvilFunctions.RENAME); - apply(state, AnvilFunctions.UPDATE_PRIOR_WORK_COST); + workPiece.apply(behavior, functions.rename()); + workPiece.apply(behavior, functions.setItemPriorWork()); // Combine enchantments. - apply(state, AnvilFunctions.COMBINE_ENCHANTMENTS_JAVA_EDITION); + workPiece.apply(behavior, functions.combineEnchantsJava()); - return forge(state); + return workPiece.temper(); } } diff --git a/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehavior.java b/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehavior.java index a5f9094..53d76a9 100644 --- a/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehavior.java +++ b/src/main/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehavior.java @@ -3,22 +3,22 @@ import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; import com.github.jikoo.planarenchanting.anvil.AnvilBehavior; -import com.github.jikoo.planarenchanting.util.MetaCachedStack; import com.google.common.collect.Multimap; import org.bukkit.enchantments.Enchantment; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NullMarked; import java.util.Collection; -public class BlockAnvilBehavior implements AnvilBehavior { +@NullMarked +public class BlockAnvilBehavior implements AnvilBehavior { - private final @NotNull EnchantableBlockConfig config; - private final @NotNull String worldName; - private final @NotNull Collection enchants; + private final EnchantableBlockConfig config; + private final String worldName; + private final Collection enchants; public BlockAnvilBehavior( - @NotNull EnchantableRegistration registration, - @NotNull String worldName + EnchantableRegistration registration, + String worldName ) { this.config = registration.getConfig(); this.worldName = worldName; @@ -26,28 +26,28 @@ public BlockAnvilBehavior( } @Override - public boolean enchantApplies(@NotNull Enchantment enchantment, @NotNull MetaCachedStack base) { + public boolean enchantApplies(Enchantment enchantment, T base) { return enchants.contains(enchantment) && !config.anvilDisabledEnchants().get(worldName).contains(enchantment); } @Override - public boolean enchantsConflict(@NotNull Enchantment enchant1, @NotNull Enchantment enchant2) { + public boolean enchantsConflict(Enchantment enchant1, Enchantment enchant2) { Multimap conflicts = config.anvilEnchantmentConflicts().get(worldName); return conflicts.containsEntry(enchant1, enchant2) || conflicts.containsEntry(enchant2, enchant1); } @Override - public int getEnchantMaxLevel(@NotNull Enchantment enchantment) { + public int getEnchantMaxLevel(Enchantment enchantment) { return config.anvilEnchantmentMax().get(worldName, enchantment); } @Override - public boolean itemsCombineEnchants(@NotNull MetaCachedStack base, @NotNull MetaCachedStack addition) { + public boolean itemsCombineEnchants(T base, T addition) { return true; } @Override - public boolean itemRepairedBy(@NotNull MetaCachedStack repaired, @NotNull MetaCachedStack repairMat) { + public boolean itemRepairedBy(T repaired, T repairMat) { return false; } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 6d7d179..1fe243b 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -12,7 +12,7 @@ blocks: - raw_copper - raw_gold - raw_iron - tableEnchantability: STONE + tableEnchantability: STONE_TOOL tableDisabledEnchantments: [] tableEnchantmentConflicts: silk_touch: diff --git a/src/test/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPluginTest.java b/src/test/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPluginTest.java index 00aecfe..3358651 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPluginTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/EnchantableBlocksPluginTest.java @@ -1,27 +1,36 @@ package com.github.jikoo.enchantableblocks; -import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.mock.world.WorldMocks; +import org.bukkit.Bukkit; import org.bukkit.Chunk; +import org.bukkit.Registry; +import org.bukkit.Server; import org.bukkit.command.Command; +import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.Listener; import org.bukkit.plugin.InvalidDescriptionException; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.scheduler.BukkitScheduler; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; import org.mockito.invocation.InvocationOnMock; import java.io.BufferedReader; +import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; +import java.lang.reflect.Field; import java.nio.file.Path; import java.util.List; import java.util.function.Supplier; @@ -30,46 +39,70 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; @DisplayName("Feature: Plugin should load and enable features.") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class EnchantableBlocksPluginTest { + private MockedStatic bukkit; private EnchantableBlocksPlugin plugin; + @BeforeAll + void setUp() { + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> mock(Enchantment.class)).when(registry).getOrThrow(any()); + } + return registry; + }); + var factory = ItemFactoryMocks.mockFactory(); + bukkit.when(Bukkit::getItemFactory).thenReturn(factory); + } + @BeforeEach - void beforeEach() throws FileNotFoundException, InvalidDescriptionException { - var server = ServerMocks.mockServer(); + void beforeEach() throws FileNotFoundException, InvalidDescriptionException, ReflectiveOperationException { + Server server = mock(); var pluginManager = mock(PluginManager.class); when(server.getPluginManager()).thenReturn(pluginManager); var scheduler = mock(BukkitScheduler.class); when(server.getScheduler()).thenReturn(scheduler); - var factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); + + plugin = mock(EnchantableBlocksPlugin.class, withSettings().defaultAnswer(InvocationOnMock::callRealMethod)); + + doReturn(server).when(plugin).getServer(); + + Logger logger = mock(); + doReturn(logger).when(plugin).getLogger(); var description = new PluginDescriptionFile(new BufferedReader(new FileReader( Path.of(".", "src", "main", "resources", "plugin.yml").toFile()))); + doReturn(description).when(plugin).getDescription(); + var dataFolder = Path.of(".", "src", "test", "resources", description.getName()).toFile(); - plugin = mock(EnchantableBlocksPlugin.class, withSettings().defaultAnswer(InvocationOnMock::callRealMethod)); + doReturn(dataFolder).when(plugin).getDataFolder(); + + Field configFile = JavaPlugin.class.getDeclaredField("configFile"); + configFile.setAccessible(true); + configFile.set(plugin, new File(dataFolder, "config.yml")); - plugin.init( - mock(), - server, - description, - dataFolder, - mock(), - EnchantableBlocksPlugin.class.getClassLoader() - ); + Field classLoader = JavaPlugin.class.getDeclaredField("classLoader"); + classLoader.setAccessible(true); + classLoader.set(plugin, EnchantableBlocksPlugin.class.getClassLoader()); } - @AfterEach - void afterEach() { - ServerMocks.unsetBukkitServer(); + @AfterAll + void tearDown() { + bukkit.close(); } @DisplayName("Plugin has no-arg constructor.") @@ -81,7 +114,6 @@ void testNoArgConstructor() { @DisplayName("Plugin registers events.") @Test void testEventRegistration() { - plugin.onLoad(); verify(plugin.getServer().getPluginManager(), times(0)) .registerEvents(any(Listener.class), any(Plugin.class)); plugin.onEnable(); @@ -106,11 +138,6 @@ void testPluginLoad() { return null; }); - plugin.onLoad(); - - Logger logger = mock(); - doReturn(logger).when(plugin).getLogger(); - plugin.onEnable(); verify(plugin.getLogger()).info(any(Supplier.class)); @@ -119,7 +146,6 @@ void testPluginLoad() { @DisplayName("Reload command functions as expected.") @Test void testCommandBase() { - plugin.onLoad(); plugin.onEnable(); var command = mock(Command.class); var player = mock(Player.class); @@ -133,7 +159,6 @@ void testCommandBase() { @DisplayName("No argument command displays version and tells server to show help") @Test void testCommandNoArgs() { - plugin.onLoad(); plugin.onEnable(); var command = mock(Command.class); var player = mock(Player.class); @@ -147,7 +172,6 @@ void testCommandNoArgs() { @DisplayName("Invalid argument command displays version and tells server to show help") @Test void testCommandInvalidArgs() { - plugin.onLoad(); plugin.onEnable(); var command = mock(Command.class); var player = mock(Player.class); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java index 41990be..033e85d 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/EnchantableBlockTest.java @@ -1,22 +1,24 @@ package com.github.jikoo.enchantableblocks.block; import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; -import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; -import com.github.jikoo.enchantableblocks.mock.inventory.ItemStackMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; import com.jparams.verifier.tostring.ToStringVerifier; import com.jparams.verifier.tostring.preset.Presets; +import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.Registry; import org.bukkit.block.Block; import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.mockito.MockedStatic; import java.lang.annotation.Annotation; import java.util.Arrays; @@ -24,12 +26,14 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -39,6 +43,7 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class EnchantableBlockTest { + private MockedStatic bukkit; private EnchantableRegistration registration; private Block block; private ItemStack itemStack; @@ -46,9 +51,21 @@ class EnchantableBlockTest { @BeforeAll void beforeAll() { - var server = ServerMocks.mockServer(); + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> mock(Enchantment.class)).when(registry).getOrThrow(any()); + } + return registry; + }); var factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); + bukkit.when(Bukkit::getItemFactory).thenReturn(factory); + } + + @AfterAll + void tearDown() { + bukkit.close(); } @BeforeEach @@ -82,7 +99,7 @@ void testGetItemStack() { assertThat("Item is clone", internalStack, is(itemStackClone)); verify(itemStackClone).setAmount(1); // Directly returning the internal ItemStack instance allows subclasses to manipulate it. - assertThat("Same item is returned", internalStack == enchantableBlock.getItemStack()); + assertThat("Same item is returned", enchantableBlock.getItemStack(), is(sameInstance(internalStack))); } @DisplayName("Block checks against in-world type.") @@ -197,7 +214,7 @@ void testToString() { }) .withPreset(Presets.INTELLI_J) .withIgnoredFields("registration", "storage", "dirty", "updating") - .withValueProvider(ItemStack.class, path -> ItemStackMocks.newItemMock(ItemType.AIR, 1)) + .withValueProvider(ItemStack.class, path -> new ItemStack(Material.AIR)) .withFailOnExcludedFields(true).verify(); } diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceConfigTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceConfigTest.java index af141c2..a400158 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceConfigTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceConfigTest.java @@ -1,25 +1,34 @@ package com.github.jikoo.enchantableblocks.block.impl.furnace; -import static org.hamcrest.CoreMatchers.both; -import static org.hamcrest.CoreMatchers.everyItem; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.in; -import static org.hamcrest.Matchers.is; - -import com.github.jikoo.enchantableblocks.mock.ServerMocks; -import com.github.jikoo.enchantableblocks.mock.enchantments.EnchantmentMocks; import com.github.jikoo.planarwrappers.config.Setting; -import java.io.File; -import java.nio.file.Path; -import java.util.Collection; -import java.util.Set; +import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.enchantments.Enchantment; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.mockito.MockedStatic; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Set; + +import static org.hamcrest.CoreMatchers.both; +import static org.hamcrest.CoreMatchers.everyItem; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.in; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; @DisplayName("Feature: Configuration for furnace-specific details.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -29,18 +38,34 @@ class EnchantableFurnaceConfigTest { private static final String ORE_WORLD = "mining_dimension"; private static final String VANILLA_WORLD = "lame_vanilla_world"; + private MockedStatic bukkit; private EnchantableFurnaceConfig config; @BeforeAll void beforeAll() { - ServerMocks.mockServer(); - EnchantmentMocks.init(); + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> mock(Enchantment.class)).when(registry).getOrThrow(any()); + } + if (Material.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> Material.matchMaterial(invocation1.getArgument(0, NamespacedKey.class).getKey())) + .when(registry).get(any()); + } + return registry; + }); File configFile = Path.of(".", "src", "test", "resources", "furnace_config.yml").toFile(); YamlConfiguration configuration = YamlConfiguration.loadConfiguration(configFile); this.config = new EnchantableFurnaceConfig(configuration); } + @AfterAll + void tearDown() { + bukkit.close(); + } + @DisplayName("Fortune list should be customizable per-world.") @Test void testFortuneList() { diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceRegistrationTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceRegistrationTest.java index 9d51b65..2a1bdf1 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceRegistrationTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceRegistrationTest.java @@ -1,31 +1,34 @@ package com.github.jikoo.enchantableblocks.block.impl.furnace; -import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.InventoryMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.Server; import org.bukkit.block.BlastFurnace; import org.bukkit.block.Block; import org.bukkit.block.Furnace; import org.bukkit.block.Smoker; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.enchantments.Enchantment; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.BlastingRecipe; import org.bukkit.inventory.CampfireRecipe; import org.bukkit.inventory.CookingRecipe; import org.bukkit.inventory.FurnaceInventory; import org.bukkit.inventory.FurnaceRecipe; +import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.Recipe; import org.bukkit.inventory.ShapelessRecipe; import org.bukkit.inventory.SmokingRecipe; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginManager; -import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -33,6 +36,7 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.MockedStatic; import java.util.ArrayList; import java.util.List; @@ -50,37 +54,46 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @DisplayName("Feature: Registration for EnchantableFurnace.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class EnchantableFurnaceRegistrationTest { + private MockedStatic bukkit; private Plugin plugin; private EnchantableFurnaceRegistration registration; private FurnaceInventory[] furnaces; @BeforeAll void beforeAll() { - var server = ServerMocks.mockServer(); - + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> mock(Enchantment.class)).when(registry).getOrThrow(any()); + } + return registry; + }); + ItemFactory factory = ItemFactoryMocks.mockFactory(); + bukkit.when(Bukkit::getItemFactory).thenReturn(factory); + + Server server = mock(); var pluginManager = mock(PluginManager.class); - when(server.getPluginManager()).thenReturn(pluginManager); - var factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); + doReturn(pluginManager).when(server).getPluginManager(); plugin = mock(Plugin.class); - when(plugin.getName()).thenReturn(getClass().getSimpleName()); - doReturn(plugin.getName().toLowerCase()).when(plugin).namespace(); - when(plugin.getConfig()).thenReturn(new YamlConfiguration()); - when(plugin.getServer()).thenReturn(server); + doReturn(getClass().getSimpleName()).when(plugin).getName(); + doReturn(new YamlConfiguration()).when(plugin).getConfig(); + doReturn(server).when(plugin).getServer(); var logger = mock(Logger.class); - when(plugin.getLogger()).thenReturn(logger); + doReturn(logger).when(plugin).getLogger(); // Add some sample recipes to ensure tests actually cover code List recipes = new ArrayList<>(); @@ -101,7 +114,12 @@ void beforeAll() { recipes.add(new CampfireRecipe(new NamespacedKey(plugin, "smores1"), new ItemStack(Material.DIRT), Material.DIAMOND, 0f, 0)); recipes.add(new CampfireRecipe(new NamespacedKey(plugin, "hotdog2"), new ItemStack(Material.OAK_LOG), Material.COAL, 0f, 0)); recipes.add(new CampfireRecipe(new NamespacedKey(plugin, "beancan3"), new ItemStack(Material.COAL_ORE), Material.COAL_BLOCK, 0f, 0)); - when(server.recipeIterator()).thenAnswer(invocation -> recipes.iterator()); + bukkit.when(Bukkit::recipeIterator).thenReturn(recipes.iterator()); + } + + @AfterAll + void tearDown() { + bukkit.close(); } @BeforeEach @@ -111,13 +129,13 @@ void beforeEach() { var furnaceInventory = InventoryMocks.newFurnaceMock(); var furnace = mock(Furnace.class); - when(furnaceInventory.getHolder()).thenReturn(furnace); + doReturn(furnace).when(furnaceInventory).getHolder(); var blastFurnaceInventory = InventoryMocks.newFurnaceMock(InventoryType.BLAST_FURNACE); var blastFurnace = mock(BlastFurnace.class); - when(blastFurnaceInventory.getHolder()).thenReturn(blastFurnace); + doReturn(blastFurnace).when(blastFurnaceInventory).getHolder(); var smokerInventory = InventoryMocks.newFurnaceMock(InventoryType.SMOKER); var smoker = mock(Smoker.class); - when(smokerInventory.getHolder()).thenReturn(smoker); + doReturn(smoker).when(smokerInventory).getHolder(); furnaces = new FurnaceInventory[] { furnaceInventory, blastFurnaceInventory, smokerInventory }; } @@ -125,10 +143,10 @@ void beforeEach() { @DisplayName("Listeners are registered as required.") @Test void testRegisterEvents() { - var server = Bukkit.getServer(); // Reset PluginManager so we can verify event registrations properly. var pluginManager = mock(PluginManager.class); - when(server.getPluginManager()).thenReturn(pluginManager); + Server server = plugin.getServer(); + doReturn(pluginManager).when(server).getPluginManager(); var manager = mock(EnchantableBlockManager.class); @@ -211,17 +229,8 @@ void testGetFurnaceRecipe(ItemStack item) { static Stream getModernItems() { return Stream.of(Material.values()) - .filter(material -> !material.name().startsWith("LEGACY_") && material != Material.AIR && material.isItem()) - .map(material -> new ItemStack(material) { - @Override - public @NotNull ItemStack clone() { - super.clone(); // Shh, IDE. - // Because these are backed by mocks, each clone results in a new backing mock. - // As mocks override equals, toString, and hashCode, we cannot mock those. - // Instead, we bypass the attempt to not alter the original when getting the cache key. - return this; - } - }); + .filter(material -> !material.name().startsWith("LEGACY_") && material != Material.AIR) + .map(ItemStack::new); } @DisplayName("Recipe lookup ignores null tile.") diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java index 0058922..049ab09 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/EnchantableFurnaceTest.java @@ -1,13 +1,14 @@ package com.github.jikoo.enchantableblocks.block.impl.furnace; import com.github.jikoo.enchantableblocks.block.EnchantableBlock; -import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.InventoryMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager; import com.github.jikoo.planarwrappers.util.StringConverters; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.Server; import org.bukkit.block.Block; import org.bukkit.block.Chest; import org.bukkit.block.Furnace; @@ -18,10 +19,8 @@ import org.bukkit.inventory.FurnaceInventory; import org.bukkit.inventory.FurnaceRecipe; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; import org.bukkit.inventory.RecipeChoice; import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginManager; import org.bukkit.scheduler.BukkitScheduler; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; @@ -35,6 +34,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; @@ -43,13 +43,16 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyShort; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -59,6 +62,7 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class EnchantableFurnaceTest { + private MockedStatic bukkit; private FurnaceRecipe recipe; private EnchantableFurnaceRegistration reg; private Block block; @@ -67,27 +71,44 @@ class EnchantableFurnaceTest { private ItemStack input; @BeforeAll - void beforeAll() { - var server = ServerMocks.mockServer(); - + void setUp() { + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> mock(Enchantment.class)).when(registry).getOrThrow(any()); + } + return registry; + }); var factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); - var pluginManager = mock(PluginManager.class); - when(server.getPluginManager()).thenReturn(pluginManager); + bukkit.when(Bukkit::getItemFactory).thenReturn(factory); recipe = new FurnaceRecipe( Objects.requireNonNull(StringConverters.toNamespacedKey("sample:text")), - ItemType.COARSE_DIRT.createItemStack(), Material.DIRT, 0, 200); + new ItemStack(Material.COARSE_DIRT) { + @Override + public int getMaxStackSize() { + return 64; + } + }, + Material.DIRT, + 0, + 200 + ); + } + + @AfterAll + void tearDown() { + bukkit.close(); } @BeforeEach void beforeEach() { reg = mock(EnchantableFurnaceRegistration.class); block = mock(Block.class); - itemStack = ItemType.FURNACE.createItemStack(); + itemStack = new ItemStack(Material.FURNACE); storage = mock(ConfigurationSection.class); - input = ItemType.DIRT.createItemStack(); - doReturn(64).when(ItemType.COARSE_DIRT).getMaxStackSize(); + input = new ItemStack(Material.DIRT); // Set up matching recipe when(reg.getFurnaceRecipe(any())).thenAnswer(invocation -> { @@ -133,7 +154,11 @@ void testConstructorLegacyData() { var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage); assertThat("Data needs saving", enchantableFurnace.isDirty()); assertThat("Legacy frozen ticks are preserved", enchantableFurnace.getFrozenTicks(), is(legacyFrozenTicks)); - verify(enchantableFurnace.getItemStack()).addUnsafeEnchantment(Enchantment.SILK_TOUCH, 1); + assertThat( + "Legacy frozen ticks are removed from item", + enchantableFurnace.getItemStack().getEnchantmentLevel(Enchantment.SILK_TOUCH), + is(1) + ); } @DisplayName("New data is created") @@ -145,7 +170,7 @@ void testConstructorNewData() { assertThat("Data needs saving", enchantableFurnace.isDirty()); assertThat("Silk touch can pause", enchantableFurnace.canPause()); assertThat("No free frozen tick", enchantableFurnace.getFrozenTicks(), is((short) 0)); - verify(enchantableFurnace.getItemStack(), times(0)).addUnsafeEnchantment(Enchantment.SILK_TOUCH, 1); + assertThat("Item was copied", enchantableFurnace.getItemStack(), not(sameInstance(itemStack))); } @DisplayName("New data does not fetch silk level") @@ -154,7 +179,6 @@ void testConstructorNewNonSilk() { var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage); assertThat("Data needs saving", enchantableFurnace.isDirty()); assertThat("Non-silk cannot pause", enchantableFurnace.canPause(), is(false)); - verify(enchantableFurnace.getItemStack(), times(0)).getEnchantmentLevel(Enchantment.SILK_TOUCH); } @DisplayName("Existing data is used") @@ -169,7 +193,6 @@ void testConstructorExistingData() { assertThat("Data needs saving", enchantableFurnace.isDirty()); assertThat("Can pause", enchantableFurnace.canPause()); assertThat("No free frozen tick", enchantableFurnace.getFrozenTicks(), is(frozenTicks)); - verify(enchantableFurnace.getItemStack(), times(0)).addUnsafeEnchantment(Enchantment.SILK_TOUCH, 1); } @DisplayName("Block provides initializing registration.") @@ -287,8 +310,12 @@ void testShouldPauseCannotPause() { @DisplayName("Furnace with null tile should not pause.") @Test void testShouldPauseNullTile() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + }; assertThat( "Furnace with null tile should not pause", enchantableFurnace.shouldPause(null), @@ -310,9 +337,17 @@ void testShouldPauseInternal() { @DisplayName("Furnace that is already paused should not pause.") @Test void testShouldPauseHasFrozenTicks() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); - when(enchantableFurnace.getFrozenTicks()).thenReturn((short) 10); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + + @Override + short getFrozenTicks() { + return 10; + } + }; setUpTile(); assertThat( "Furnace that is already paused should not pause", @@ -323,8 +358,12 @@ void testShouldPauseHasFrozenTicks() { @DisplayName("Furnace with no input should pause.") @Test void testShouldPauseNoInput() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + }; setUpTile(); assertThat("Furnace with no input should pause", enchantableFurnace.shouldPause(null)); } @@ -332,13 +371,17 @@ void testShouldPauseNoInput() { @DisplayName("Furnace with full result should pause.") @Test void testShouldPauseFullResult() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + }; var tile = setUpTile(); var inv = tile.getInventory(); inv.setSmelting(input); var result = recipe.getResult(); - result.setAmount(result.getType().getMaxStackSize()); + result.setAmount(result.getMaxStackSize()); inv.setResult(result); assertThat("Furnace with no input should pause", enchantableFurnace.shouldPause(null)); @@ -347,8 +390,12 @@ void testShouldPauseFullResult() { @DisplayName("Furnace with no matching recipe should pause.") @Test void testShouldPauseNonmatching() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + }; var tile = setUpTile(); var inv = tile.getInventory(); ItemStack otherInput = new ItemStack(Material.FURNACE); @@ -362,8 +409,12 @@ void testShouldPauseNonmatching() { @DisplayName("Furnace with input not matching recipe input should pause") @Test void testShouldPauseInputNotYieldResult() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + }; var mockRecipe = mock(FurnaceRecipe.class); var choice = mock(RecipeChoice.class); @@ -381,8 +432,12 @@ void testShouldPauseInputNotYieldResult() { @DisplayName("Furnace with null result should not pause") @Test void testShouldPauseNullResult() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + }; var tile = setUpTile(); var inv = tile.getInventory(); inv.setSmelting(input); @@ -396,8 +451,12 @@ void testShouldPauseNullResult() { @DisplayName("Furnace with air result should not pause") @Test void testShouldPauseAirResult() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + }; var tile = setUpTile(); var inv = tile.getInventory(); inv.setSmelting(input); @@ -412,8 +471,12 @@ void testShouldPauseAirResult() { @DisplayName("Furnace with similar result should not pause") @Test void testShouldPauseSimilarResult() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + }; var tile = setUpTile(); var inv = tile.getInventory(); inv.setSmelting(input); @@ -428,8 +491,12 @@ void testShouldPauseSimilarResult() { @DisplayName("Furnace with dissimilar result should pause") @Test void testShouldPauseDissimilarResult() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + }; var tile = setUpTile(); var inv = tile.getInventory(); inv.setSmelting(input); @@ -443,23 +510,29 @@ void testShouldPauseDissimilarResult() { @DisplayName("FurnaceSmeltEvents are handled as if event has completed.") @Test void testShouldPausePostSmelt() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + }; var furnace = setUpTile(); var inv = furnace.getInventory(); - var smelting = input; - inv.setSmelting(smelting); + inv.setSmelting(input); inv.setResult(null); - var event = new FurnaceSmeltEvent(block, smelting, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); assertThat( "Situation does not result in pausing without event context", enchantableFurnace.shouldPause(null), - is(false)); + is(false) + ); assertThat( "Events that cause inventory modification are handled as if modification occurred", - enchantableFurnace.shouldPause(event)); + enchantableFurnace.shouldPause(event), + is(true) + ); } @DisplayName("Furnace that cannot pause will not pause.") @@ -474,34 +547,62 @@ void testPauseCannotPause() { @DisplayName("Furnace that has frozen ticks will not pause.") @Test void testPausePreFrozen() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); - when(enchantableFurnace.getFrozenTicks()).thenReturn((short) 10); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } - enchantableFurnace.pause(); + @Override + short getFrozenTicks() { + return 10; + } + + @Override + void setFrozenTicks(short frozenTicks) { + throw new IllegalStateException(); + } + }; + + assertDoesNotThrow(enchantableFurnace::pause); assertThat("Furnace is not paused", enchantableFurnace.isPaused(), is(false)); - verify(enchantableFurnace, times(0)).setFrozenTicks(anyShort()); } @DisplayName("Furnace that has no tile will not pause.") @Test void testPauseNoTile() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } - enchantableFurnace.pause(); + @Override + void setFrozenTicks(short frozenTicks) { + throw new IllegalStateException(); + } + }; + + assertDoesNotThrow(enchantableFurnace::pause); assertThat("Furnace is not paused", enchantableFurnace.isPaused(), is(false)); - verify(enchantableFurnace, times(0)).setFrozenTicks(anyShort()); } @DisplayName("Furnace freezes ticks when pausing.") @Test void testPause() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); - when(enchantableFurnace.isPaused()).thenAnswer(invocation -> enchantableFurnace.getFrozenTicks() > 0); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + + @Override + public boolean isPaused() { + return getFrozenTicks() > 0; + } + }; var tile = setUpTile(); short frozenTime = 200; tile.setBurnTime(frozenTime); @@ -509,7 +610,6 @@ void testPause() { enchantableFurnace.pause(); assertThat("Furnace is paused", enchantableFurnace.isPaused()); - verify(enchantableFurnace).setFrozenTicks(anyShort()); verify(tile).setBurnTime((short) 0); verify(tile).update(true); assertThat("Furnace has frozen ticks", enchantableFurnace.getFrozenTicks(), is(frozenTime)); @@ -553,9 +653,17 @@ void testResumeForceFreezableTile() { @DisplayName("Furnace resumes as needed.") @Test void testResume() { - var enchantableFurnace = spy(new EnchantableFurnace(reg, block, itemStack, storage)); - when(enchantableFurnace.canPause()).thenReturn(true); - when(enchantableFurnace.isPaused()).thenAnswer(invocation -> enchantableFurnace.getFrozenTicks() > 0); + var enchantableFurnace = new EnchantableFurnace(reg, block, itemStack, storage) { + @Override + public boolean canPause() { + return true; + } + + @Override + public boolean isPaused() { + return getFrozenTicks() > 0; + } + }; short frozenTicks = 200; enchantableFurnace.setFrozenTicks(frozenTicks); var tile = setUpTile(); @@ -608,28 +716,16 @@ class UpdateTests { private Plugin plugin; private ArgumentCaptor taskCaptor; - @BeforeAll - static void beforeAll() { - var server = Bukkit.getServer(); - var scheduler = mock(BukkitScheduler.class); - when(server.getScheduler()).thenReturn(scheduler); - } - - @AfterAll - static void afterAll() { - var server = Bukkit.getServer(); - when(server.getScheduler()).thenReturn(null); - } - @BeforeEach void beforeEach() { plugin = mock(Plugin.class); - var server = Bukkit.getServer(); + Server server = mock(); doReturn(server).when(plugin).getServer(); taskCaptor = ArgumentCaptor.forClass(Runnable.class); - var scheduler = server.getScheduler(); + var scheduler = mock(BukkitScheduler.class); doReturn(null).when(scheduler).runTask(any(), taskCaptor.capture()); + doReturn(scheduler).when(server).getScheduler(); } @DisplayName("Furnaces must have tiles to update.") diff --git a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java index 38b707e..8b23436 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/block/impl/furnace/FurnaceListenerTest.java @@ -1,6 +1,5 @@ package com.github.jikoo.enchantableblocks.block.impl.furnace; -import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.InventoryMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.mock.world.BlockMocks; @@ -10,9 +9,12 @@ import com.github.jikoo.planarwrappers.util.StringConverters; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.Server; import org.bukkit.block.Block; import org.bukkit.block.Furnace; import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.inventory.FurnaceBurnEvent; @@ -30,11 +32,11 @@ import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryView; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.Recipe; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -43,6 +45,7 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; import java.util.List; import java.util.Map; @@ -62,6 +65,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -71,29 +75,46 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class FurnaceListenerTest { + private MockedStatic bukkit; + private Server server; private CookingRecipe recipe; private ItemStack input; @BeforeAll - void beforeAll() { - var server = ServerMocks.mockServer(); + void setUp() { + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> mock(Enchantment.class)).when(registry).getOrThrow(any()); + } + return registry; + }); // Set up item factory for ItemMeta generation and comparison. var factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); + bukkit.when(Bukkit::getItemFactory).thenReturn(factory); + + server = mock(); // Add furnace recipe dirt -> coarse dirt recipe = new FurnaceRecipe( Objects.requireNonNull(StringConverters.toNamespacedKey("sample:text")), - new ItemStack(Material.COARSE_DIRT), Material.DIRT, 0, 200); + new ItemStack(Material.COARSE_DIRT) { + // Set up max stack size for result. + @Override + public int getMaxStackSize() { + return 64; + } + }, Material.DIRT, + 0, + 200 + ); // Set up recipe iterator when(server.recipeIterator()).thenAnswer(invocation -> Set.of((Recipe) recipe).iterator()); - // Set up max stack size for result. - doReturn(64).when(ItemType.COARSE_DIRT).getMaxStackSize(); - // Create input stack. - input = ItemType.DIRT.createItemStack(); + input = new ItemStack(Material.DIRT); // Set up scheduler to run tasks immediately. var scheduler = mock(BukkitScheduler.class); @@ -104,6 +125,11 @@ void beforeAll() { when(server.getScheduler()).thenReturn(scheduler); } + @AfterAll + void tearDown() { + bukkit.close(); + } + @DisplayName("Furnace-specific events") @Nested class FurnaceEventsTest { @@ -115,8 +141,6 @@ class FurnaceEventsTest { @BeforeEach void beforeEach() { - var server = Bukkit.getServer(); - var plugin = mock(Plugin.class); when(plugin.getServer()).thenReturn(server); when(plugin.getName()).thenReturn(getClass().getSimpleName()); @@ -208,7 +232,7 @@ void testFurnaceBurnUnbreaking() { @Test void testFurnaceStartSmeltInvalid() { when(manager.getBlock(block)).thenReturn(null); - var event = new FurnaceStartSmeltEvent(block, input, recipe, recipe.getCookingTime()); + var event = new FurnaceStartSmeltEvent(block, input, recipe); assertDoesNotThrow(() -> listener.onFurnaceStartSmelt(event)); assertThat( "Cook time must not be modified", @@ -220,7 +244,7 @@ void testFurnaceStartSmeltInvalid() { @Test void testFurnaceStartSmeltModifier() { when(enchantableFurnace.applyCookTimeModifiers(anyDouble())).thenAnswer(invocation -> (short) (invocation.getArgument(0, Double.class) + 10)); - var event = new FurnaceStartSmeltEvent(block, input, recipe, recipe.getCookingTime()); + var event = new FurnaceStartSmeltEvent(block, input, recipe); assertDoesNotThrow(() -> listener.onFurnaceStartSmelt(event)); assertThat( "Cook time must be modified", @@ -232,7 +256,7 @@ void testFurnaceStartSmeltModifier() { @Test void testFurnaceSmeltInvalid() { when(manager.getBlock(block)).thenReturn(null); - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); assertThat("Event is never cancelled", !event.isCancelled()); assertThat("Result must not be modified", event.getResult(), isItem(recipe.getResult())); @@ -243,7 +267,7 @@ void testFurnaceSmeltInvalid() { @Test void testFurnaceSmeltInvalidTile() { when(enchantableFurnace.getFurnaceTile()).thenReturn(null); - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); assertThat("Event is never cancelled", !event.isCancelled()); assertThat("Result must not be modified", event.getResult(), isItem(recipe.getResult())); @@ -255,7 +279,7 @@ void testFurnaceSmeltInvalidTile() { void testFurnaceSmeltUnbreakingEfficiency() { when(enchantableFurnace.getCookModifier()).thenReturn(10); when(enchantableFurnace.getBurnModifier()).thenReturn(10); - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); assertThat("Event is never cancelled", !event.isCancelled()); assertThat("Result must not be modified", event.getResult(), isItem(recipe.getResult())); @@ -265,8 +289,8 @@ void testFurnaceSmeltUnbreakingEfficiency() { @Test void testApplyFortuneFull() { var result = recipe.getResult(); - result.setAmount(result.getType().getMaxStackSize()); - var event = new FurnaceSmeltEvent(block, input, result, recipe); + result.setAmount(result.getMaxStackSize()); + var event = new FurnaceSmeltEvent(block, input, result); var supplier = mock(IntSupplier.class); listener.applyFortune(event, supplier); verify(supplier, times(0)).getAsInt(); @@ -277,7 +301,7 @@ void testApplyFortuneFull() { @ParameterizedTest @ValueSource(ints = { -1, 0 }) void testApplyFortuneBelowOne(int value) { - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); listener.applyFortune(event, () -> value); assertThat("Result must not be modified", event.getResult(), isItem(recipe.getResult())); } @@ -286,7 +310,7 @@ void testApplyFortuneBelowOne(int value) { @ParameterizedTest @ValueSource(ints = { 1, 2, 3 }) void testApplyFortunePositive(int value) { - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); listener.applyFortune(event, () -> value); assertThat("Result must be similar", event.getResult(), isSimilar(recipe.getResult())); assertThat("Result must be modified", event.getResult(), not(isItem(recipe.getResult()))); @@ -300,14 +324,14 @@ void testApplyFortunePositive(int value) { void testApplyFortuneReducedSpace(int value) { // Set up event for stack with 1 free slot. var result = recipe.getResult(); - result.setAmount(result.getType().getMaxStackSize() - 1); - var event = new FurnaceSmeltEvent(block, input, result, recipe); + result.setAmount(result.getMaxStackSize() - 1); + var event = new FurnaceSmeltEvent(block, input, result); listener.applyFortune(event, () -> value); // Expect max stack. var expected = recipe.getResult(); - expected.setAmount(result.getType().getMaxStackSize()); + expected.setAmount(result.getMaxStackSize()); assertThat("Result must be full", event.getResult(), isItem(expected)); } @@ -317,7 +341,7 @@ void testFurnaceSmeltFortuneNotBlacklist() { var config = new EnchantableFurnaceConfig(new YamlConfiguration()); when(enchantableFurnace.getConfig()).thenReturn(config); when(enchantableFurnace.getFortune()).thenReturn(10); - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); listener = spy(listener); listener.onFurnaceSmelt(event); @@ -334,7 +358,7 @@ void testFurnaceSmeltFortuneBlacklist() { var config = new EnchantableFurnaceConfig(yaml); when(enchantableFurnace.getConfig()).thenReturn(config); when(enchantableFurnace.getFortune()).thenReturn(10); - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); listener = spy(listener); listener.onFurnaceSmelt(event); @@ -352,7 +376,7 @@ void testFurnaceSmeltFortuneWhitelisted() { var config = new EnchantableFurnaceConfig(yaml); when(enchantableFurnace.getConfig()).thenReturn(config); when(enchantableFurnace.getFortune()).thenReturn(10); - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); listener = spy(listener); listener.onFurnaceSmelt(event); @@ -369,7 +393,7 @@ void testFurnaceSmeltNotWhitelistedFortune() { var config = new EnchantableFurnaceConfig(yaml); when(enchantableFurnace.getConfig()).thenReturn(config); when(enchantableFurnace.getFortune()).thenReturn(10); - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); listener = spy(listener); listener.onFurnaceSmelt(event); @@ -381,7 +405,7 @@ void testFurnaceSmeltNotWhitelistedFortune() { @DisplayName("Furnaces that cannot pause do not attempt to.") @Test void testFurnaceSmeltNoPause() { - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); verify(enchantableFurnace, times(0)).shouldPause(any()); assertThat("Event is never cancelled", !event.isCancelled()); @@ -391,7 +415,7 @@ void testFurnaceSmeltNoPause() { @Test void testFurnaceSmeltTryPause() { when(enchantableFurnace.canPause()).thenReturn(true); - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); verify(enchantableFurnace).shouldPause(any()); verify(enchantableFurnace, times(0)).pause(); @@ -403,7 +427,7 @@ void testFurnaceSmeltTryPause() { void testFurnaceSmeltDoPause() { when(enchantableFurnace.canPause()).thenReturn(true); when(enchantableFurnace.shouldPause(any())).thenReturn(true); - var event = new FurnaceSmeltEvent(block, input, recipe.getResult(), recipe); + var event = new FurnaceSmeltEvent(block, input, recipe.getResult()); assertDoesNotThrow(() -> listener.onFurnaceSmelt(event)); verify(enchantableFurnace).pause(); assertThat("Event is never cancelled", !event.isCancelled()); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfigTest.java b/src/test/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfigTest.java index c19d710..5ff3dde 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfigTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/config/EnchantableBlockConfigTest.java @@ -1,30 +1,44 @@ package com.github.jikoo.enchantableblocks.config; -import static com.github.jikoo.enchantableblocks.mock.matcher.EnchantMatchers.enchant; -import static com.github.jikoo.enchantableblocks.mock.matcher.EnchantMatchers.enchantSet; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.is; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; - -import com.github.jikoo.enchantableblocks.mock.ServerMocks; -import com.github.jikoo.enchantableblocks.mock.enchantments.EnchantmentMocks; import com.github.jikoo.planarenchanting.table.Enchantability; +import com.github.jikoo.planarenchanting.table.EnchantabilityCategory; import com.github.jikoo.planarwrappers.config.Mapping; import com.github.jikoo.planarwrappers.config.Setting; import com.google.common.collect.Multimap; -import java.io.File; -import java.nio.file.Path; -import java.util.Collection; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.Set; +import org.bukkit.Bukkit; +import org.bukkit.Keyed; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.enchantments.Enchantment; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.mockito.MockedStatic; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; + +import static com.github.jikoo.enchantableblocks.mock.matcher.EnchantMatchers.enchant; +import static com.github.jikoo.enchantableblocks.mock.matcher.EnchantMatchers.enchantSet; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; @DisplayName("Feature: Parse complex settings into simple configuration.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -37,18 +51,57 @@ class EnchantableBlockConfigTest { private static final String VANILLA_WORLD = "lame_vanilla_world"; private static final String POWER_WORLD = "busted_endgame_bullhonkey_world"; + private MockedStatic bukkit; private EnchantableBlockConfig config; @BeforeAll void beforeAll() { - ServerMocks.mockServer(); - EnchantmentMocks.init(); + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + // Enchantments are used as keys, so same instances must be used. + Map enchantments = new HashMap<>(); + doAnswer(invocation1 -> { + NamespacedKey key = invocation1.getArgument(0); + try { + // We intentionally use invalid keys in testing; don't create enchantments for them. + Enchantment.class.getDeclaredField(key.getKey().toUpperCase()); + } catch (NoSuchFieldException e) { + return null; + } + return enchantments.computeIfAbsent(key, localKey -> { + Enchantment enchantment = mock(); + doReturn(localKey).when(enchantment).getKey(); + doReturn(localKey).when(enchantment).getKeyOrThrow(); + return enchantment; + }); + }).when(registry).get(any()); + doAnswer(invocation1 -> { + Keyed keyed = registry.get(invocation1.getArgument(0)); + if (keyed != null) { + return keyed; + } + throw new IllegalArgumentException("No enchant matching " + invocation1.getArgument(0)); + }).when(registry).getOrThrow(any()); + } + if (Material.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> Material.matchMaterial(invocation1.getArgument(0, NamespacedKey.class).getKey())) + .when(registry).get(any()); + } + return registry; + }); File configFile = Path.of(".", "src", "test", "resources", "generic_config.yml").toFile(); YamlConfiguration configuration = YamlConfiguration.loadConfiguration(configFile); this.config = new EnchantableBlockConfig(configuration) {}; } + @AfterAll + void tearDown() { + bukkit.close(); + } + @DisplayName("Nonexistent configuration sections should be handled gracefully.") @Test void testLoadNonexistent() { @@ -74,9 +127,9 @@ void testTableEnchantability() { Setting enchantability = config.tableEnchantability(); assertThat("Enchantability should be STONE by default", - enchantability.get(INVALID_WORLD), is(Enchantability.STONE)); + enchantability.get(INVALID_WORLD), is(EnchantabilityCategory.STONE_TOOL)); assertThat("Enchantability should be overridden to GOLD_ARMOR", - enchantability.get(POWER_WORLD), is(Enchantability.GOLD_ARMOR)); + enchantability.get(POWER_WORLD), is(EnchantabilityCategory.GOLD_ARMOR)); } @DisplayName("Disabled enchantments should be customizable per-world.") @@ -117,6 +170,7 @@ void testAnvilEnchantMax() { Mapping enchantmentMax = config.anvilEnchantmentMax(); Enchantment enchantment = Enchantment.SILK_TOUCH; + doReturn(1).when(enchantment).getMaxLevel(); int actual = enchantmentMax.get(INVALID_WORLD, enchantment); assertThat("Max level should default to enchantment max level", actual, is(enchantment.getMaxLevel())); @@ -126,6 +180,7 @@ void testAnvilEnchantMax() { enchantmentMax.get(VANILLA_WORLD, enchantment), is(actual)); enchantment = Enchantment.EFFICIENCY; + doReturn(5).when(enchantment).getMaxLevel(); assertThat("Max level should use specified defaults", enchantmentMax.get(INVALID_WORLD, enchantment), is(4)); assertThat("World overrides should be provided", diff --git a/src/test/java/com/github/jikoo/enchantableblocks/config/data/EnchantMaxLevelMappingTest.java b/src/test/java/com/github/jikoo/enchantableblocks/config/data/EnchantMaxLevelMappingTest.java index 7358a37..5ebfdaa 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/config/data/EnchantMaxLevelMappingTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/config/data/EnchantMaxLevelMappingTest.java @@ -1,35 +1,62 @@ package com.github.jikoo.enchantableblocks.config.data; -import static com.github.jikoo.enchantableblocks.mock.matcher.EnchantMatchers.enchant; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -import com.github.jikoo.enchantableblocks.mock.ServerMocks; -import com.github.jikoo.enchantableblocks.mock.enchantments.EnchantmentMocks; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.enchantments.Enchantment; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.MockedStatic; + +import static com.github.jikoo.enchantableblocks.mock.matcher.EnchantMatchers.enchant; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @DisplayName("Config: Mapping for Enchantments to integers.") @TestInstance(Lifecycle.PER_CLASS) class EnchantMaxLevelMappingTest { + private MockedStatic bukkit; private EnchantMaxLevelMapping mapping; @BeforeAll - void beforeAll() { - ServerMocks.mockServer(); - EnchantmentMocks.init(); + void setUp() { + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> { + Enchantment enchantment = mock(Enchantment.class); + NamespacedKey key = invocation1.getArgument(0); + doReturn(key).when(enchantment).getKey(); + doReturn(key).when(enchantment).getKeyOrThrow(); + return enchantment; + }).when(registry).getOrThrow(any()); + doAnswer(invocation1 -> registry.getOrThrow(invocation1.getArgument(0))).when(registry).get(any()); + } + return registry; + }); + Enchantment enchantment = Enchantment.UNBREAKING; + doReturn(3).when(enchantment).getMaxLevel(); + } + + @AfterAll + void tearDown() { + bukkit.close(); } @BeforeEach @@ -42,7 +69,7 @@ void beforeEach() { @Test void testConvertKey() { Enchantment original = Enchantment.UNBREAKING; - Enchantment converted = mapping.convertKey(original.getKey().toString()); + Enchantment converted = mapping.convertKey(original.getKeyOrThrow().toString()); assertThat("Key is converted to enchantment", converted, is(enchant(original))); } diff --git a/src/test/java/com/github/jikoo/enchantableblocks/config/data/EnchantabilitySettingTest.java b/src/test/java/com/github/jikoo/enchantableblocks/config/data/EnchantabilitySettingTest.java index 66a1d76..bf7492c 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/config/data/EnchantabilitySettingTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/config/data/EnchantabilitySettingTest.java @@ -1,11 +1,7 @@ package com.github.jikoo.enchantableblocks.config.data; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.mockito.Mockito.mock; - import com.github.jikoo.planarenchanting.table.Enchantability; +import com.github.jikoo.planarenchanting.table.EnchantabilityCategory; import org.bukkit.configuration.ConfigurationSection; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -13,11 +9,16 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.mock; + @DisplayName("Config: Setting for Enchantability values.") @TestInstance(Lifecycle.PER_CLASS) class EnchantabilitySettingTest { - private static final Enchantability DEFAULT_VALUE = Enchantability.GOLD_ARMOR; + private static final Enchantability DEFAULT_VALUE = EnchantabilityCategory.GOLD_ARMOR; EnchantabilitySetting setting; @@ -62,8 +63,8 @@ void testConvertFieldNameMissing() { void testConvertFieldName() { assertThat( "Valid field name returns field", - setting.convertString("STONE"), - is(Enchantability.STONE)); + setting.convertString("STONE_TOOL"), + is(EnchantabilityCategory.STONE_TOOL)); } @DisplayName("Non-Enchantability fields yield null.") diff --git a/src/test/java/com/github/jikoo/enchantableblocks/config/data/MultimapEnchantEnchantSettingTest.java b/src/test/java/com/github/jikoo/enchantableblocks/config/data/MultimapEnchantEnchantSettingTest.java index 9c10bc5..2e2af20 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/config/data/MultimapEnchantEnchantSettingTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/config/data/MultimapEnchantEnchantSettingTest.java @@ -1,32 +1,61 @@ package com.github.jikoo.enchantableblocks.config.data; -import static com.github.jikoo.enchantableblocks.mock.matcher.EnchantMatchers.enchant; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.mock; - -import com.github.jikoo.enchantableblocks.mock.ServerMocks; -import com.github.jikoo.enchantableblocks.mock.enchantments.EnchantmentMocks; import com.google.common.collect.HashMultimap; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.enchantments.Enchantment; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.MockedStatic; + +import static com.github.jikoo.enchantableblocks.mock.matcher.EnchantMatchers.enchant; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; @DisplayName("Config: Setting for a Multimap of Enchantments to Enchantments.") @TestInstance(Lifecycle.PER_CLASS) class MultimapEnchantEnchantSettingTest { + private MockedStatic bukkit; + private Enchantment enchantment; private MultimapEnchantEnchantSetting setting; @BeforeAll - void beforeAll() { - ServerMocks.mockServer(); - EnchantmentMocks.init(); + void setUp() { + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> { + Enchantment localEnch = mock(Enchantment.class); + NamespacedKey key = invocation1.getArgument(0); + doReturn(key).when(localEnch).getKey(); + doReturn(key).when(localEnch).getKeyOrThrow(); + return localEnch; + }).when(registry).getOrThrow(any()); + doAnswer(invocation1 -> registry.getOrThrow(invocation1.getArgument(0))).when(registry).get(any()); + } + return registry; + }); + + enchantment = Enchantment.UNBREAKING; + } + + @AfterAll + void tearDown() { + bukkit.close(); } @BeforeEach @@ -38,17 +67,15 @@ void beforeEach() { @DisplayName("Keys are converted to enchantments.") @Test void testConvertKey() { - Enchantment original = Enchantment.UNBREAKING; - Enchantment converted = setting.convertKey(original.getKey().toString()); - assertThat("Key is converted to enchantment", converted, is(enchant(original))); + Enchantment converted = setting.convertKey(enchantment.getKeyOrThrow().toString()); + assertThat("Key is converted to enchantment", converted, is(enchant(enchantment))); } @DisplayName("Values are converted to enchantments.") @Test void testConvertValue() { - Enchantment original = Enchantment.UNBREAKING; - Enchantment converted = setting.convertValue(original.getKey().toString()); - assertThat("Value is converted to enchantment", converted, is(enchant(original))); + Enchantment converted = setting.convertValue(enchantment.getKeyOrThrow().toString()); + assertThat("Value is converted to enchantment", converted, is(enchant(enchantment))); } } \ No newline at end of file diff --git a/src/test/java/com/github/jikoo/enchantableblocks/config/data/SetEnchantSettingTest.java b/src/test/java/com/github/jikoo/enchantableblocks/config/data/SetEnchantSettingTest.java index c1b5357..ca579be 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/config/data/SetEnchantSettingTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/config/data/SetEnchantSettingTest.java @@ -1,32 +1,59 @@ package com.github.jikoo.enchantableblocks.config.data; -import static com.github.jikoo.enchantableblocks.mock.matcher.EnchantMatchers.enchant; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.mockito.Mockito.mock; - -import com.github.jikoo.enchantableblocks.mock.ServerMocks; -import com.github.jikoo.enchantableblocks.mock.enchantments.EnchantmentMocks; -import java.util.Set; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.enchantments.Enchantment; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.mockito.MockedStatic; + +import java.util.Set; + +import static com.github.jikoo.enchantableblocks.mock.matcher.EnchantMatchers.enchant; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; @DisplayName("Config: Setting for a Set of Enchantments") @TestInstance(Lifecycle.PER_CLASS) class SetEnchantSettingTest { + private MockedStatic bukkit; private SetEnchantSetting setting; @BeforeAll - void beforeAll() { - ServerMocks.mockServer(); - EnchantmentMocks.init(); + void setUp() { + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> { + Enchantment enchantment = mock(Enchantment.class); + NamespacedKey key = invocation1.getArgument(0); + doReturn(key).when(enchantment).getKey(); + doReturn(key).when(enchantment).getKeyOrThrow(); + return enchantment; + }).when(registry).getOrThrow(any()); + doAnswer(invocation1 -> registry.getOrThrow(invocation1.getArgument(0))).when(registry).get(any()); + } + return registry; + }); + } + + @AfterAll + void tearDown() { + bukkit.close(); } @BeforeEach @@ -39,7 +66,8 @@ void beforeEach() { @Test void testConvertValue() { Enchantment original = Enchantment.UNBREAKING; - Enchantment converted = setting.convertValue(original.getKey().toString()); + doReturn(NamespacedKey.minecraft("unbreaking")).when(original).getKeyOrThrow(); + Enchantment converted = setting.convertValue(original.getKeyOrThrow().toString()); assertThat("Value is converted to enchantment", converted, is(enchant(original))); } diff --git a/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java b/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java index a6520b2..1628ea3 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/listener/AnvilEnchanterTest.java @@ -1,26 +1,27 @@ package com.github.jikoo.enchantableblocks.listener; import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; -import com.github.jikoo.enchantableblocks.mock.ServerMocks; -import com.github.jikoo.enchantableblocks.mock.enchantments.EnchantmentMocks; import com.github.jikoo.enchantableblocks.mock.inventory.InventoryMocks; -import com.github.jikoo.enchantableblocks.mock.world.WorldMocks; +import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockRegistry; import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.Server; +import org.bukkit.World; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.Player; import org.bukkit.event.inventory.PrepareAnvilEvent; +import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; import org.bukkit.inventory.meta.EnchantmentStorageMeta; import org.bukkit.inventory.view.AnvilView; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -31,6 +32,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; import java.util.Collection; import java.util.List; @@ -50,6 +52,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -59,22 +62,40 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class AnvilEnchanterTest { + private MockedStatic bukkit; private Enchantment enchantment; - private ItemType goodType; - private ItemType badType; - - private AnvilEnchanter enchanter; + private Material goodType; + private Material badType; @BeforeAll - void setUpAll() { - ServerMocks.mockServer(); - EnchantmentMocks.init(); + void setUp() { + bukkit = mockStatic(); + + ItemFactory itemFactory = ItemFactoryMocks.mockFactory(); + bukkit.when(Bukkit::getItemFactory).thenReturn(itemFactory); + + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> mock(Enchantment.class)).when(registry).getOrThrow(any()); + } + return registry; + }); enchantment = Enchantment.EFFICIENCY; - goodType = ItemType.COAL_ORE; - badType = ItemType.REDSTONE_ORE; + doReturn(5).when(enchantment).getMaxLevel(); + + goodType = Material.COAL_ORE; + badType = Material.REDSTONE_ORE; } + @AfterAll + void tearDown() { + bukkit.close(); + } + + private AnvilEnchanter enchanter; + @Nested class ItemsInvalidTest { @@ -94,32 +115,32 @@ void testNullBaseInvalid() { @DisplayName("Items are invalid if addition is empty.") @Test void testNullAdditionInvalid() { - var base = goodType.createItemStack(); + var base = new ItemStack(goodType); assertThat("Items are invalid", enchanter.areItemsInvalid(base, null)); } @DisplayName("Items are invalid if base is stacked.") @Test void testStackedBaseInvalid() { - var base = goodType.createItemStack(); + var base = new ItemStack(goodType); base.setAmount(64); - var addition = goodType.createItemStack(); + var addition = new ItemStack(goodType); assertThat("Items are invalid", enchanter.areItemsInvalid(base, addition)); } @DisplayName("Items are invalid base and addition do not match.") @Test void testDifferentAddition() { - var base = goodType.createItemStack(); - var addition = badType.createItemStack(); + var base = new ItemStack(goodType); + var addition = new ItemStack(badType); assertThat("Items are valid", enchanter.areItemsInvalid(base, addition)); } @DisplayName("Items are valid if base and addition match.") @Test void testSame() { - var base = goodType.createItemStack(); - var addition = goodType.createItemStack(); + var base = new ItemStack(goodType); + var addition = new ItemStack(goodType); assertThat("Items are valid", enchanter.areItemsInvalid(base, addition), is(false)); addition.setAmount(64); assertThat("Items are valid", enchanter.areItemsInvalid(base, addition), is(false)); @@ -128,7 +149,7 @@ void testSame() { @DisplayName("Items are valid if addition is enchanted book.") @Test void testEnchantedBookAddition() { - var base = goodType.createItemStack(); + var base = new ItemStack(goodType); var addition = new ItemStack(Material.ENCHANTED_BOOK); assertThat("Items are valid", enchanter.areItemsInvalid(base, addition), is(false)); } @@ -146,7 +167,7 @@ class PrepareAnvilTest { @BeforeEach void beforeEach() { - var server = Bukkit.getServer(); + Server server = mock(); var scheduler = mock(BukkitScheduler.class); runnableCaptor = ArgumentCaptor.forClass(Runnable.class); @@ -156,7 +177,7 @@ void beforeEach() { var plugin = mock(Plugin.class); when(plugin.getServer()).thenReturn(server); - itemStack = goodType.createItemStack(); + itemStack = new ItemStack(goodType); registry = mock(EnchantableBlockRegistry.class); registration = mock(EnchantableRegistration.class); @@ -174,13 +195,14 @@ void beforeEach() { } private @NotNull AnvilView prepareView() { - var player = mock(Player.class); - var world = WorldMocks.newWorld("world"); - when(player.getWorld()).thenReturn(world); + Player player = mock(); + World world = mock(); + doReturn("world").when(world).getName(); + doReturn(world).when(player).getWorld(); var inventory = InventoryMocks.newAnvilMock(); inventory.setItem(0, itemStack.clone()); - var additionItem = ItemType.ENCHANTED_BOOK.createItemStack(); + var additionItem = new ItemStack(Material.ENCHANTED_BOOK); var additionMeta = additionItem.getItemMeta(); if (additionMeta instanceof EnchantmentStorageMeta storageMeta) { storageMeta.addStoredEnchant(enchantment, enchantment.getMaxLevel(), true); @@ -202,12 +224,6 @@ void beforeEach() { return anvilView; } - @AfterEach - void afterEach() { - var server = Bukkit.getServer(); - when(server.getScheduler()).thenReturn(null); - } - @Test void testInvalidItem() { view.getTopInventory().setItem(0, null); @@ -219,7 +235,7 @@ void testInvalidItem() { @Test void testUnregisteredMaterial() { - ItemStack badStack = badType.createItemStack(); + ItemStack badStack = new ItemStack(badType); view.getTopInventory().setItem(0, badStack); var event = spy(new PrepareAnvilEvent(view, null)); assertDoesNotThrow(() -> enchanter.onPrepareAnvil(event)); @@ -277,15 +293,6 @@ static Collection getSlots() { void testSuccess() { var event = spy(new PrepareAnvilEvent(view, null)); - // Because we can't override .equals for mocks and we need to verify that items - // are unchanged before setting the result, we instead want to ensure that "copy" - // is actually the same object. - // This does cause the original item to be manipulated as a side effect when producing the result. - ItemStack base = view.getTopInventory().getItem(0); - doReturn(base).when(base).clone(); - ItemStack addition = view.getTopInventory().getItem(1); - doReturn(addition).when(addition).clone(); - assertDoesNotThrow(() -> enchanter.onPrepareAnvil(event)); verify(event).setResult(notNull()); assertThat("Result is set", event.getResult(), is(notNullValue())); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/listener/TableEnchanterTest.java b/src/test/java/com/github/jikoo/enchantableblocks/listener/TableEnchanterTest.java index f355a72..737f230 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/listener/TableEnchanterTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/listener/TableEnchanterTest.java @@ -1,14 +1,14 @@ package com.github.jikoo.enchantableblocks.listener; import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; -import com.github.jikoo.enchantableblocks.mock.ServerMocks; -import com.github.jikoo.enchantableblocks.mock.enchantments.EnchantmentMocks; -import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockRegistry; import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; +import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.Registry; +import org.bukkit.Server; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.configuration.file.YamlConfiguration; @@ -22,11 +22,13 @@ import org.bukkit.persistence.PersistentDataContainer; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.mockito.MockedStatic; import java.util.Map; import java.util.Set; @@ -42,8 +44,10 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -52,6 +56,8 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class TableEnchanterTest { + private MockedStatic bukkit; + private Server server; private EnchantableBlockRegistry registry; private EnchantableRegistration registration; private Player player; @@ -59,18 +65,30 @@ class TableEnchanterTest { private ItemStack itemStack; @BeforeAll - void setUpAll() { - var server = ServerMocks.mockServer(); - EnchantmentMocks.init(); - - var factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); + void setUp() { + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry bukkitReg = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> mock(Enchantment.class)).when(bukkitReg).getOrThrow(any()); + } + return bukkitReg; + }); + // Touch to initialize. + Enchantment.EFFICIENCY.getMaxLevel(); + + server = mock(); var scheduler = mock(BukkitScheduler.class); - when(server.getScheduler()).thenReturn(scheduler); + doReturn(scheduler).when(server).getScheduler(); + } + + @AfterAll + void tearDown() { + bukkit.close(); } @BeforeEach - void setUp() { + void setUpEach() { // Set up default registration. registration = mock(EnchantableRegistration.class); doReturn(true).when(registration).hasEnchantPermission(notNull(), anyString()); @@ -81,6 +99,7 @@ void setUp() { when(plugin.getName()).thenReturn(getClass().getSimpleName()); registry = mock(EnchantableBlockRegistry.class); doReturn(registration).when(registry).get(any()); + doReturn(server).when(plugin).getServer(); listener = new TableEnchanter(plugin, registry); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/listener/WorldListenerTest.java b/src/test/java/com/github/jikoo/enchantableblocks/listener/WorldListenerTest.java index e9c53c7..17911fd 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/listener/WorldListenerTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/listener/WorldListenerTest.java @@ -1,13 +1,15 @@ package com.github.jikoo.enchantableblocks.listener; -import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.mock.world.WorldMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager; import com.google.common.base.Preconditions; +import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.Server; import org.bukkit.World; import org.bukkit.block.Block; @@ -29,12 +31,14 @@ import org.bukkit.plugin.PluginManager; import org.bukkit.scheduler.BukkitScheduler; import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.mockito.ArgumentCaptor; +import org.mockito.MockedStatic; import java.util.ArrayList; import java.util.List; @@ -48,6 +52,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -57,6 +62,7 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WorldListenerTest { + private MockedStatic bukkit; private Server server; private ArgumentCaptor runnableCaptor; private PluginManager pluginManager; @@ -68,11 +74,19 @@ class WorldListenerTest { @BeforeAll void setUpAll() { - server = ServerMocks.mockServer(); + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> mock(Registry.class)); + + server = mock(); var factory = ItemFactoryMocks.mockFactory(); when(server.getItemFactory()).thenReturn(factory); } + @AfterAll + void tearDown() { + bukkit.close(); + } + @BeforeEach void setUp() { var world = createWorld(); @@ -106,10 +120,16 @@ void setUp() { listener = new WorldListener(plugin, manager); // Reset block type - block.setType(Material.DIRT); + Material material = mock(); + doReturn(NamespacedKey.minecraft("dirt")).when(material).getKey(); + doReturn(true).when(material).isBlock(); + block.setType(material); // Create default item - itemStack = new ItemStack(Material.COAL_ORE); + material = mock(); + doReturn(NamespacedKey.minecraft("coal_ore")).when(material).getKey(); + doReturn(true).when(material).isBlock(); + itemStack = new ItemStack(material); } private @NotNull World createWorld() { diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/ServerMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/ServerMocks.java deleted file mode 100644 index 5544006..0000000 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/ServerMocks.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.github.jikoo.enchantableblocks.mock; - -import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; -import org.bukkit.Bukkit; -import org.bukkit.Keyed; -import org.bukkit.Material; -import org.bukkit.Server; -import org.bukkit.Tag; -import org.bukkit.UnsafeValues; -import org.bukkit.inventory.ItemFactory; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Field; -import java.util.Set; -import java.util.logging.Logger; - -import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -// These suppressions are for internals we have to mock to get a usable server for testing. -@SuppressWarnings({"deprecation", "UnstableApiUsage"}) -public final class ServerMocks { - - public static @NotNull Server mockServer() { - Server mock = mock(Server.class); - - doReturn(ServerMocks.class.getName()).when(mock).getName(); - doReturn("1.2.3").when(mock).getVersion(); - doReturn("1.2.3-SAMPLETEXT").when(mock).getBukkitVersion(); - - Logger noOp = mock(Logger.class); - when(mock.getLogger()).thenReturn(noOp); - when(mock.isPrimaryThread()).thenReturn(true); - - ItemFactory itemFactory = ItemFactoryMocks.mockFactory(); - when(mock.getItemFactory()).thenReturn(itemFactory); - doAnswer(invocation -> { - UnsafeValues unsafe = mock(); - - ItemStack empty = mock(); - doReturn(Material.AIR).when(empty).getType(); - when(unsafe.createEmptyStack()).thenReturn(empty); - - return unsafe; - }).when(mock).getUnsafe(); - - // Server must be available before tags can be mocked. - Bukkit.setServer(mock); - - // Tags are dependent on registries, but use a different method. - // This will set up blank tags for each constant; all that needs to be done to render them - // functional is to re-mock Tag#getValues. - doAnswer(invocationGetTag -> { - Tag tag = mock(); - doReturn(invocationGetTag.getArgument(1)).when(tag).getKey(); - doReturn(Set.of()).when(tag).getValues(); - doAnswer(invocationIsTagged -> { - Keyed keyed = invocationIsTagged.getArgument(0); - Class type = invocationGetTag.getArgument(2); - if (!type.isAssignableFrom(keyed.getClass())) { - return null; - } - // Since these are mocks, the exact instance might not be equal. Consider equal keys equal. - return tag.getValues().contains(keyed) || tag.getValues().stream().anyMatch(value -> value.getKey().equals(keyed.getKey())); - }).when(tag).isTagged(notNull()); - return tag; - }).when(mock).getTag(notNull(), notNull(), notNull()); - - // Once the server is all set up, touch BlockType and ItemType to initialize. - // This prevents issues when trying to access dependent methods from a Material constant. - try { - Class.forName("org.bukkit.inventory.ItemType"); - Class.forName("org.bukkit.block.BlockType"); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); - } - - return mock; - } - - public static void unsetBukkitServer() { - try - { - Field server = Bukkit.class.getDeclaredField("server"); - server.setAccessible(true); - server.set(null, null); - } - catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) - { - throw new RuntimeException(e); - } - } - - private ServerMocks() {} - -} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java deleted file mode 100644 index 97a9720..0000000 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/enchantments/EnchantmentMocks.java +++ /dev/null @@ -1,454 +0,0 @@ -package com.github.jikoo.enchantableblocks.mock.enchantments; - -import io.papermc.paper.registry.RegistryAccess; -import io.papermc.paper.registry.RegistryKey; -import io.papermc.paper.registry.TypedKey; -import io.papermc.paper.registry.keys.EnchantmentKeys; -import io.papermc.paper.registry.keys.tags.EnchantmentTagKeys; -import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys; -import io.papermc.paper.registry.tag.Tag; -import io.papermc.paper.registry.tag.TagKey; -import org.bukkit.NamespacedKey; -import org.bukkit.Registry; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.UnmodifiableView; -import org.mockito.ArgumentMatchers; - -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.function.IntUnaryOperator; - -import static org.bukkit.enchantments.Enchantment.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; - -public class EnchantmentMocks { - - private static final Map KEYS_TO_ENCHANTS = new HashMap<>(); - private static final Set> ENCHANTING_TABLE_TAGS = new HashSet<>(); - - public static void init() { - // See net.minecraft.world.item.enchantment.Enchantments - config(PROTECTION) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_ARMOR) - .maxLevel(4) - .minModCost(perLvl(1, 11)) - .maxModCost(perLvl(12, 11)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_ARMOR); - config(FIRE_PROTECTION) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_ARMOR) - .weight(5) - .maxLevel(4) - .minModCost(perLvl(10, 8)) - .maxModCost(perLvl(18, 8)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_ARMOR); - config(FEATHER_FALLING) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_FOOT_ARMOR) - .weight(5) - .maxLevel(4) - .minModCost(perLvl(5, 6)) - .maxModCost(perLvl(11, 6)); - config(BLAST_PROTECTION) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_ARMOR) - .weight(2) - .maxLevel(4) - .minModCost(perLvl(5, 8)) - .maxModCost(perLvl(13, 8)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_ARMOR); - config(PROJECTILE_PROTECTION) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_ARMOR) - .weight(5) - .maxLevel(4) - .minModCost(perLvl(3, 6)) - .maxModCost(perLvl(9, 6)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_ARMOR); - - config(RESPIRATION) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_HEAD_ARMOR) - .weight(2) - .maxLevel(3) - .minModCost(perLvl(10, 10)) - .maxModCost(perLvl(40, 10)); - config(AQUA_AFFINITY) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_HEAD_ARMOR) - .weight(2) - .maxLevel(1) - .minModCost(flat(1)) - .maxModCost(flat(41)); - - config(THORNS) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_CHEST_ARMOR) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_ARMOR) - .weight(1) - .maxLevel(3) - .minModCost(perLvl(10, 20)) - .maxModCost(perLvl(60, 20)); - - config(DEPTH_STRIDER) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_FOOT_ARMOR) - .weight(2) - .maxLevel(3) - .minModCost(perLvl(10, 10)) - .maxModCost(perLvl(25, 10)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_BOOTS); - config(FROST_WALKER) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_FOOT_ARMOR) - .weight(2) - .maxLevel(2) - .minModCost(perLvl(10, 10)) - .maxModCost(perLvl(25, 10)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_BOOTS); - - config(BINDING_CURSE) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_EQUIPPABLE) - .weight(1) - .minModCost(flat(25)) - .maxModCost(flat(50)); - - config(SOUL_SPEED) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_FOOT_ARMOR) - .weight(1) - .maxLevel(3) - .minModCost(perLvl(10, 10)) - .maxModCost(perLvl(25, 10)); - config(SWIFT_SNEAK) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_LEG_ARMOR) - .weight(1) - .maxLevel(3) - .minModCost(perLvl(25, 25)) - .maxModCost(perLvl(75, 25)); - - config(SHARPNESS) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_SHARP_WEAPON) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) - .maxLevel(5) - .minModCost(perLvl(1, 11)) - .maxModCost(perLvl(21, 11)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_DAMAGE); - config(SMITE) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_WEAPON) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) - .weight(5) - .maxLevel(5) - .minModCost(perLvl(5, 8)) - .maxModCost(perLvl(25, 8)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_DAMAGE); - config(BANE_OF_ARTHROPODS) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_WEAPON) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) - .weight(5) - .maxLevel(5) - .minModCost(perLvl(5, 8)) - .maxModCost(perLvl(25, 8)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_DAMAGE); - config(KNOCKBACK) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) - .weight(5) - .maxLevel(2) - .minModCost(perLvl(5, 20)) - .maxModCost(perLvl(55, 20)); - config(FIRE_ASPECT) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_FIRE_ASPECT) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) - .weight(2) - .maxLevel(2) - .minModCost(perLvl(10, 20)) - .maxModCost(perLvl(60, 20)); - config(LOOTING) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MELEE_WEAPON) - .weight(2) - .maxLevel(3) - .minModCost(perLvl(15, 9)) - .maxModCost(perLvl(65, 9)); - config(SWEEPING_EDGE) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_SWEEPING) - .weight(2) - .maxLevel(3) - .minModCost(perLvl(5, 9)) - .maxModCost(perLvl(20, 9)); - - config(EFFICIENCY) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MINING) - .maxLevel(5) - .minModCost(perLvl(1, 10)) - .maxModCost(perLvl(51, 10)); - config(SILK_TOUCH) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MINING_LOOT) - .weight(1) - .minModCost(flat(15)) - .maxModCost(flat(65)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_MINING); - config(UNBREAKING) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_DURABILITY) - .weight(5) - .maxLevel(3) - .minModCost(perLvl(5, 8)) - .maxModCost(perLvl(55, 8)); - config(FORTUNE) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MINING_LOOT) - .weight(2) - .maxLevel(3) - .minModCost(perLvl(15, 9)) - .maxModCost(perLvl(65, 9)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_MINING); - - config(POWER) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_BOW) - .maxLevel(5) - .minModCost(perLvl(1, 10)) - .maxModCost(perLvl(16, 10)); - config(PUNCH) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_BOW) - .weight(2) - .maxLevel(2) - .minModCost(perLvl(12, 20)) - .maxModCost(perLvl(37, 20)); - config(FLAME) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_BOW) - .weight(2) - .minModCost(flat(20)) - .maxModCost(flat(50)); - config(INFINITY) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_BOW) - .weight(1) - .minModCost(flat(20)) - .maxModCost(flat(50)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_BOW); - - config(LUCK_OF_THE_SEA) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_FISHING) - .weight(2) - .maxLevel(3) - .minModCost(perLvl(15, 9)) - .maxModCost(perLvl(65, 9)); - config(LURE) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_FISHING) - .weight(2) - .maxLevel(3) - .minModCost(perLvl(15, 9)) - .maxModCost(perLvl(65, 9)); - - config(LOYALTY) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_TRIDENT) - .weight(5) - .maxLevel(3) - .minModCost(perLvl(12, 7)) - .maxModCost(flat(50)); - config(IMPALING) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_TRIDENT) - .weight(2) - .maxLevel(5) - .minModCost(perLvl(1, 8)) - .maxModCost(perLvl(21, 8)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_DAMAGE); - config(RIPTIDE) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_TRIDENT) - .weight(2) - .maxLevel(3) - .minModCost(perLvl(17, 7)) - .maxModCost(flat(50)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_RIPTIDE); - - config(LUNGE) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_LUNGE) - .weight(5) - .maxLevel(3) - .minModCost(perLvl(5, 8)) - .maxModCost(perLvl(25, 8)); - - config(CHANNELING) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_TRIDENT) - .weight(1) - .minModCost(flat(25)) - .maxModCost(flat(50)); - - config(MULTISHOT) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_CROSSBOW) - .weight(2) - .maxLevel(1) - .minModCost(flat(20)) - .maxModCost(flat(50)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_CROSSBOW); - config(QUICK_CHARGE) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_CROSSBOW) - .weight(5) - .maxLevel(3) - .minModCost(perLvl(12, 20)) - .maxModCost(flat(50)); - config(PIERCING) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_CROSSBOW) - .maxLevel(4) - .minModCost(perLvl(1, 10)) - .maxModCost(flat(50)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_CROSSBOW); - - config(DENSITY) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MACE) - .weight(5) - .maxLevel(5) - .minModCost(perLvl(5, 8)) - .maxModCost(perLvl(25, 8)) - .exclusive(EnchantmentTagKeys.EXCLUSIVE_SET_DAMAGE); - config(BREACH) - .tableTarget(ItemTypeTagKeys.ENCHANTABLE_MACE) - .weight(2) - .maxLevel(4) - .minModCost(perLvl(15, 9)) - .maxModCost(perLvl(65, 9)); - config(WIND_BURST) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_MACE) - .weight(2) - .maxLevel(3) - .minModCost(perLvl(15, 9)) - .maxModCost(perLvl(65, 9)); - - config(MENDING) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_DURABILITY) - .weight(2) - .minModCost(perLvl(25, 25)) - .maxModCost(perLvl(75, 25)); - config(VANISHING_CURSE) - .anvilTarget(ItemTypeTagKeys.ENCHANTABLE_VANISHING) - .weight(1) - .minModCost(flat(25)) - .maxModCost(flat(50)); - - Set missingInternalEnchants = new HashSet<>(); - try { - for (Field field : Enchantment.class.getFields()) { - if (Modifier.isStatic(field.getModifiers()) && Enchantment.class.equals(field.getType())) { - Enchantment declaredEnchant = (Enchantment) field.get(null); - Enchantment stored = KEYS_TO_ENCHANTS.get(declaredEnchant.getKey()); - if (stored == null) { - missingInternalEnchants.add(declaredEnchant.getKey().toString()); - } - } - } - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - - if (!missingInternalEnchants.isEmpty()) { - throw new IllegalStateException("Missing enchantment declarations for " + missingInternalEnchants); - } - - Registry registry = RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT); - // When all enchantments are initialized, redirect registry to our map. - // This allows us to add and test custom enchantments much more easily. - doAnswer(invocation -> KEYS_TO_ENCHANTS.get(invocation.getArgument(0, NamespacedKey.class))) - .when(registry).get((NamespacedKey) ArgumentMatchers.notNull()); - doAnswer(invocation -> KEYS_TO_ENCHANTS.values().stream()).when(registry).stream(); - doAnswer(invocation -> Collections.unmodifiableCollection(KEYS_TO_ENCHANTS.values()).iterator()).when(registry).iterator(); - } - - public static void putEnchant(@NotNull Enchantment enchantment) { - KEYS_TO_ENCHANTS.put(enchantment.getKey(), enchantment); - } - - public static @NotNull @UnmodifiableView Set> getEnchantingTableTags() { - return Collections.unmodifiableSet(ENCHANTING_TABLE_TAGS); - } - - private static @NotNull IntUnaryOperator perLvl(int base, int perLevel) { - return level -> base + (level - 1) * perLevel; - } - - private static @NotNull IntUnaryOperator flat(int value) { - return integer -> value; - } - - private static EnchantConfig config(Enchantment enchantment) { - return new EnchantConfig(enchantment); - } - - private record EnchantConfig(Enchantment enchantment) { - - EnchantConfig(Enchantment enchantment) { - this.enchantment = enchantment; - KEYS_TO_ENCHANTS.put(enchantment.getKey(), enchantment); - weight(10); - doReturn(1).when(enchantment).getStartLevel(); - doReturn(1).when(enchantment).getMaxLevel(); - doAnswer(invocation -> { - NamespacedKey otherKey = invocation.getArgument(0, Enchantment.class).getKey(); - return otherKey.equals(enchantment.getKey()); - }).when(enchantment).conflictsWith(any()); - } - - EnchantConfig weight(int weight) { - doReturn(weight).when(enchantment).getWeight(); - - // Anvil cost is technically separate, but in practice is based on enchanting table rarity. - // For known rarities, set it here. - return switch (weight) { - case 10 -> anvilCost(1); - case 5 -> anvilCost(2); - case 2 -> anvilCost(4); - case 1 -> anvilCost(8); - default -> this; - }; - } - - EnchantConfig maxLevel(int maxLevel) { - doReturn(maxLevel).when(enchantment).getMaxLevel(); - return this; - } - - EnchantConfig anvilTarget(TagKey targetKey) { - // Hopefully in the future the enchantment API gets expanded, making separate table+anvil targets available - Tag target = RegistryAccess.registryAccess().getRegistry(RegistryKey.ITEM).getTag(targetKey); - doAnswer(invocation -> { - ItemStack item = invocation.getArgument(0); - return item != null && target.contains(TypedKey.create(RegistryKey.ITEM, item.getType().getKey())); - }).when(enchantment).canEnchantItem(any()); - doReturn(target).when(enchantment).getSupportedItems(); - return this; - } - - EnchantConfig tableTarget(TagKey targetKey) { - Tag target = RegistryAccess.registryAccess().getRegistry(RegistryKey.ITEM).getTag(targetKey); - ENCHANTING_TABLE_TAGS.add(target); - return anvilTarget(targetKey); - } - - EnchantConfig minModCost(IntUnaryOperator cost) { - doAnswer(invocation -> cost.applyAsInt(invocation.getArgument(0, Integer.class))) - .when(enchantment).getMinModifiedCost(anyInt()); - return this; - } - - EnchantConfig maxModCost(IntUnaryOperator cost) { - doAnswer(invocation -> cost.applyAsInt(invocation.getArgument(0, Integer.class))) - .when(enchantment).getMaxModifiedCost(anyInt()); - return this; - } - - EnchantConfig anvilCost(int cost) { - doReturn(cost).when(enchantment).getAnvilCost(); - return this; - } - - EnchantConfig exclusive(TagKey conflict) { - Registry registry = RegistryAccess.registryAccess().getRegistry(RegistryKey.ENCHANTMENT); - var conflicts = registry.getTag(conflict); - doAnswer(invocation -> { - // Apparently no way to map Enchantment -> TypeKey directly? Seems odd. - TypedKey otherKey = EnchantmentKeys.create(invocation.getArgument(0, Enchantment.class).key()); - return conflicts.contains(otherKey); - }).when(enchantment).conflictsWith(any()); - return this; - } - - } - -} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemFactoryMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemFactoryMocks.java index 1181e96..2ad4bb0 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemFactoryMocks.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemFactoryMocks.java @@ -1,6 +1,5 @@ package com.github.jikoo.enchantableblocks.mock.inventory; -import net.kyori.adventure.text.Component; import org.bukkit.Material; import org.bukkit.block.data.BlockData; import org.bukkit.enchantments.Enchantment; @@ -300,13 +299,13 @@ private static boolean equals(@Nullable ItemMeta meta, @Nullable ItemMeta other) private static void meta(@NotNull ItemMeta meta) { // Display name - AtomicReference customName = new AtomicReference<>(); - when(meta.hasCustomName()).thenAnswer(invocation -> customName.get() != null); - when(meta.customName()).thenAnswer(invocation -> customName.get()); + AtomicReference customName = new AtomicReference<>(); + doAnswer(invocation -> customName.get() != null).when(meta).hasDisplayName(); + doAnswer(invocation -> customName.get()).when(meta).getDisplayName(); doAnswer(invocation -> { customName.set(invocation.getArgument(0)); return null; - }).when(meta).customName(any()); + }).when(meta).setDisplayName(any()); // Enchantments Map enchants = new HashMap<>(); diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemStackMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemStackMocks.java deleted file mode 100644 index 0e9e0b9..0000000 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/inventory/ItemStackMocks.java +++ /dev/null @@ -1,158 +0,0 @@ -package com.github.jikoo.enchantableblocks.mock.inventory; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.ItemType; -import org.bukkit.inventory.meta.ItemMeta; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.mockito.stubbing.Answer; - -import java.util.Locale; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -public enum ItemStackMocks { - ; - - public static ItemStack newItemMock(@NotNull ItemType type, int amount) { - ItemStack stack = mock(); - - Material material = Material.getMaterial(type.getKey().getKey().toUpperCase(Locale.ROOT)); - if (material == null) { - throw new IllegalArgumentException("Unable to locate Material for ItemType " + type.getKey().getKey()); - } - doReturn(material).when(stack).getType(); - - // Amount get/set. - AtomicInteger amt = new AtomicInteger(amount); - doAnswer(invocation -> amt.get()).when(stack).getAmount(); - doAnswer(invocation -> { - amt.set(invocation.getArgument(0)); - return null; - }).when(stack).setAmount(anyInt()); - - // Item meta. - AtomicReference meta = new AtomicReference<>(); - doAnswer(invocation -> { - ItemMeta existing = meta.get(); - if (existing != null) { - return existing.clone(); - } - return Bukkit.getItemFactory().getItemMeta(stack.getType()); - }).when(stack).getItemMeta(); - doAnswer(invocation -> { - ItemMeta newMeta = invocation.getArgument(0); - meta.set(newMeta != null ? newMeta.clone() : null); - return null; - }).when(stack).setItemMeta(any()); - doAnswer(invocation -> meta.get() != null).when(stack).hasItemMeta(); - - // Item cloning. - doAnswer(invocation -> { - ItemStack clone = newItemMock(type, amount); - // Must also clone the meta or methods that manipulate the meta directly will mutate both. - clone.setItemMeta(get(meta, null, false).map(ItemMeta::clone).orElse(null)); - return clone; - }).when(stack).clone(); - - // Item similarity. Note that the only thing we track other the meta is the count. - doAnswer(invocation -> { - ItemStack other = invocation.getArgument(0); - if (other == null || other.getType() != stack.getType()) { - return false; - } - boolean haveMeta = stack.hasItemMeta(); - if (haveMeta != other.hasItemMeta()) { - return false; - } - if (!haveMeta) { - return true; - } - return Bukkit.getItemFactory().equals(meta.get(), other.getItemMeta()); - }).when(stack).isSimilar(any()); - - // Enchantments. - doAnswer(invocation -> { - ItemMeta existing = meta.get(); - return existing != null ? existing.getEnchants() : Map.of(); - }).when(stack).getEnchantments(); - doAnswer(invocation -> { - ItemMeta existing = meta.get(); - return existing == null ? 0 : existing.getEnchantLevel(invocation.getArgument(0)); - }).when(stack).getEnchantmentLevel(any()); - doAnswer(invocation -> { - ItemMeta existing = meta.get(); - return existing != null && existing.hasEnchant(invocation.getArgument(0)); - }).when(stack).containsEnchantment(any(Enchantment.class)); - Answer addEnchant = invocation -> { - get(meta, stack.getType(), true).ifPresent(itemMeta -> - itemMeta.addEnchant( - invocation.getArgument(0), - invocation.getArgument(1), - // We aren't winning any performance prizes here, a beautiful DRY hack. - invocation.getMethod().getName().contains("Unsafe") - ) - ); - return null; - }; - doAnswer(addEnchant).when(stack).addEnchantment(any(Enchantment.class), anyInt()); - doAnswer(addEnchant).when(stack).addUnsafeEnchantment(any(Enchantment.class), anyInt()); - Answer addEnchants = invocation -> { - get(meta, stack.getType(), true).ifPresent(itemMeta -> { - // DRY hack again - boolean unsafe = invocation.getMethod().getName().contains("Unsafe"); - Map enchants = invocation.getArgument(0); - for (Map.Entry entry : enchants.entrySet()) { - itemMeta.addEnchant(entry.getKey(), entry.getValue(), unsafe); - } - }); - return null; - }; - doAnswer(addEnchants).when(stack).addEnchantments(any()); - doAnswer(addEnchants).when(stack).addUnsafeEnchantments(any()); - doAnswer(invocation -> { - ItemMeta existing = meta.get(); - if (existing != null) { - existing.removeEnchantments(); - } - return null; - }).when(stack).removeEnchantments(); - doAnswer(invocation -> { - ItemMeta existing = meta.get(); - if (existing != null) { - existing.removeEnchant(invocation.getArgument(0)); - } - return null; - }).when(stack).removeEnchantment(any()); - - return stack; - } - - private static @NotNull Optional get( - @NotNull AtomicReference container, - @Nullable Material createFor, - boolean store - ) { - ItemMeta itemMeta = container.get(); - if (itemMeta == null && createFor != null) { - itemMeta = Bukkit.getItemFactory().getItemMeta(createFor); - if (!store) { - return Optional.ofNullable(itemMeta); - } - container.set(itemMeta); - } - return Optional.ofNullable(itemMeta); - } - -} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/EnchantMatchers.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/EnchantMatchers.java index 4f454a8..77641ec 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/EnchantMatchers.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/matcher/EnchantMatchers.java @@ -1,7 +1,5 @@ package com.github.jikoo.enchantableblocks.mock.matcher; -import java.util.Set; -import java.util.stream.Collectors; import org.bukkit.NamespacedKey; import org.bukkit.enchantments.Enchantment; import org.hamcrest.BaseMatcher; @@ -11,6 +9,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Set; +import java.util.stream.Collectors; + public final class EnchantMatchers { @Contract("_ -> new") @@ -66,7 +67,6 @@ public void describeTo(@NotNull Description description) { } - private EnchantMatchers() {} } diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/RegistryHelper.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/RegistryHelper.java deleted file mode 100644 index 10e3e66..0000000 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/RegistryHelper.java +++ /dev/null @@ -1,186 +0,0 @@ -package com.github.jikoo.enchantableblocks.mock.server; - -import com.github.jikoo.enchantableblocks.mock.inventory.ItemStackMocks; -import io.papermc.paper.dialog.Dialog; -import io.papermc.paper.registry.RegistryKey; -import io.papermc.paper.registry.TypedKey; -import io.papermc.paper.registry.keys.tags.BannerPatternTagKeys; -import io.papermc.paper.registry.keys.tags.BiomeTagKeys; -import io.papermc.paper.registry.keys.tags.BlockTypeTagKeys; -import io.papermc.paper.registry.keys.tags.DamageTypeTagKeys; -import io.papermc.paper.registry.keys.tags.DialogTagKeys; -import io.papermc.paper.registry.keys.tags.EnchantmentTagKeys; -import io.papermc.paper.registry.keys.tags.EntityTypeTagKeys; -import io.papermc.paper.registry.keys.tags.FluidTagKeys; -import io.papermc.paper.registry.keys.tags.GameEventTagKeys; -import io.papermc.paper.registry.keys.tags.InstrumentTagKeys; -import io.papermc.paper.registry.keys.tags.ItemTypeTagKeys; -import io.papermc.paper.registry.keys.tags.PaintingVariantTagKeys; -import io.papermc.paper.registry.keys.tags.StructureTagKeys; -import io.papermc.paper.registry.tag.Tag; -import io.papermc.paper.registry.tag.TagKey; -import org.bukkit.Art; -import org.bukkit.Fluid; -import org.bukkit.GameEvent; -import org.bukkit.Instrument; -import org.bukkit.Keyed; -import org.bukkit.NamespacedKey; -import org.bukkit.block.Biome; -import org.bukkit.block.BlockType; -import org.bukkit.block.Structure; -import org.bukkit.block.banner.PatternType; -import org.bukkit.damage.DamageType; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.entity.EntityType; -import org.bukkit.inventory.ItemType; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.lang.reflect.Field; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; - -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.notNull; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -/** Helper class used to prevent class loading order issues when creating registry. */ -@SuppressWarnings("unchecked") -enum RegistryHelper { - ; - - private static final Map, RegistryKey> LEGACY_REGISTRIES = new HashMap<>(); - - static { - forEachRegistryClass(((field, clazz) -> { - try { - LEGACY_REGISTRIES.put(clazz, (RegistryKey) field.get(null)); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - })); - } - - static RegistryKey getRegistryKey(Class clazz) { - return (RegistryKey) LEGACY_REGISTRIES.get(clazz); - } - - static T getOrThrow(Class clazz, NamespacedKey key) { - // Some classes (like BlockType and ItemType) have extra generics that will be - // erased during runtime calls. To ensure accurate typing, grab the constant's field. - // This approach also allows us to return null for unsupported keys. - Class constantClazz; - try { - constantClazz = (Class) clazz.getField( - key.getKey().toUpperCase(Locale.ROOT).replace('.', '_') - ).getType(); - } catch (ClassCastException | NoSuchFieldException e) { - throw new RuntimeException(e); - } - T keyed = mock(constantClazz); - doReturn(key).when(keyed).getKey(); - doReturn(key).when(keyed).key(); - if (keyed instanceof ItemType itemType) { - mockItemType(itemType); - } - return keyed; - } - - static @Nullable Tag getTag(Class clazz, TagKey tagKey) { - Class source; - if (PatternType.class.isAssignableFrom(clazz)) { - source = BannerPatternTagKeys.class; - } else if (Biome.class.isAssignableFrom(clazz)) { - source = BiomeTagKeys.class; - } else if (BlockType.class.isAssignableFrom(clazz)) { - source = BlockTypeTagKeys.class; - } else if (DamageType.class.isAssignableFrom(clazz)) { - source = DamageTypeTagKeys.class; - } else if (Dialog.class.isAssignableFrom(clazz)) { - source = DialogTagKeys.class; - } else if (Enchantment.class.isAssignableFrom(clazz)) { - source = EnchantmentTagKeys.class; - } else if (EntityType.class.isAssignableFrom(clazz)) { - source = EntityTypeTagKeys.class; - } else if (Fluid.class.isAssignableFrom(clazz)) { - source = FluidTagKeys.class; - } else if (GameEvent.class.isAssignableFrom(clazz)) { - source = GameEventTagKeys.class; - } else if (Instrument.class.isAssignableFrom(clazz)) { - source = InstrumentTagKeys.class; - } else if (ItemType.class.isAssignableFrom(clazz)) { - source = ItemTypeTagKeys.class; - } else if (Art.class.isAssignableFrom(clazz)) { - source = PaintingVariantTagKeys.class; - } else if (Structure.class.isAssignableFrom(clazz)) { - source = StructureTagKeys.class; - } else { - // Throw rather than return null; if we're trying to use a tag that we can't verify - // odds are on that it's a new type rather than an invalid tag. - throw new UnsupportedOperationException(""); - } - - if (!hasField(tagKey.key().value(), source, TagKey.class, clazz)) { - return null; - } - - Tag tag = mock(); - doReturn(tagKey).when(tag).tagKey(); - doReturn(Set.of()).when(tag).values(); - doAnswer(invocationIsTagged -> { - TypedKey keyed = invocationIsTagged.getArgument(0); - // Since these are mocks, the exact instance might not be equal. Consider equal keys equal. - return tag.values().contains(keyed) || tag.values().stream().anyMatch(value -> value.key().equals(keyed.key())); - }).when(tag).contains(notNull()); - return tag; - } - - static void forEachRegistryClass(BiConsumer> consumer) { - for (Field field : RegistryKey.class.getFields()) { - if (field.getType() == RegistryKey.class) { - Class clazz = (Class) getClass(((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]); - consumer.accept(field, clazz); - } - } - } - - private static Class getClass(Type type) { - if (type instanceof Class clazz) { - return clazz; - } else if (type instanceof ParameterizedType parameterized) { - return (Class) parameterized.getRawType(); - } else { - throw new UnsupportedOperationException("Unsupported type " + type.getClass()); - } - } - - private static void mockItemType(@NotNull ItemType itemType) { - // ItemStack creation. - doAnswer(invocation -> itemType.createItemStack(1) - ).when(itemType).createItemStack(); - doAnswer(invocation -> ItemStackMocks.newItemMock(itemType, invocation.getArgument(0))) - .when(itemType).createItemStack(anyInt()); - } - - private static boolean hasField(String fieldName, Class clazz, Class type, Class generic) { - fieldName = fieldName.toUpperCase().replaceAll("\\W", "_"); - try { - Field field = clazz.getDeclaredField(fieldName); - if (!type.equals(field.getType())) { - return false; - } - Class genericClazz = getClass(((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]); - return generic.equals(genericClazz); - } catch (NoSuchFieldException e) { - return false; - } - } - -} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestDummyPluginLoaderHolder.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestDummyPluginLoaderHolder.java deleted file mode 100644 index 5ca89af..0000000 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestDummyPluginLoaderHolder.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.github.jikoo.enchantableblocks.mock.server; - -import org.bukkit.event.Event; -import org.bukkit.event.Listener; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginDescriptionFile; -import org.bukkit.plugin.PluginLoader; -import org.bukkit.plugin.RegisteredListener; -import org.jetbrains.annotations.NotNull; -import org.jspecify.annotations.NonNull; - -import java.io.File; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; - -public class TestDummyPluginLoaderHolder implements PluginLoader { - - @Override - public @NotNull Plugin loadPlugin(@NotNull File file) { - throw new IllegalStateException("Not implemented"); - } - - @Override - public @NotNull PluginDescriptionFile getPluginDescription(@NotNull File file) { - throw new IllegalStateException("Not implemented"); - } - - @Override - public @NonNull @NotNull Pattern[] getPluginFileFilters() { - throw new IllegalStateException("Not implemented"); - } - - @Override - public @NotNull Map, Set> createRegisteredListeners(@NotNull Listener listener, @NotNull Plugin plugin) { - throw new IllegalStateException("Not implemented"); - } - - @Override - public void enablePlugin(@NotNull Plugin plugin) { - throw new IllegalStateException("Not implemented"); - } - - @Override - public void disablePlugin(@NotNull Plugin plugin) { - throw new IllegalStateException("Not implemented"); - } - -} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestRegistryAccess.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestRegistryAccess.java deleted file mode 100644 index b35681e..0000000 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestRegistryAccess.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.github.jikoo.enchantableblocks.mock.server; - -import io.papermc.paper.registry.RegistryAccess; -import io.papermc.paper.registry.RegistryKey; -import io.papermc.paper.registry.tag.Tag; -import io.papermc.paper.registry.tag.TagKey; -import net.kyori.adventure.key.Key; -import org.bukkit.Keyed; -import org.bukkit.NamespacedKey; -import org.bukkit.Registry; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.mockito.stubbing.Answer; - -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; - -public class TestRegistryAccess implements RegistryAccess { - - private static final Map, Registry> REGISTERS = new HashMap<>(); - - @Override - @Deprecated(since = "1.20.6", forRemoval = true) - public @Nullable Registry<@NotNull T> getRegistry(@NotNull Class type) { - return getRegistry(RegistryHelper.getRegistryKey(type)); - } - - @SuppressWarnings("unchecked") - @Override - public @NotNull Registry getRegistry(@NotNull RegistryKey registryKey) { - return (Registry) REGISTERS.computeIfAbsent(registryKey, regKey -> { - Registry registry = mock(); - - AtomicReference> clazzRef = new AtomicReference<>(); - RegistryHelper.forEachRegistryClass((BiConsumer>) (field, clazz) -> { - if (RegistryHelper.getRegistryKey(clazz).equals(regKey)) { - clazzRef.set(clazz); - } - }); - - Map values = new HashMap<>(); - Class clazz = clazzRef.get(); - - // Paper added several helpers and then moved ItemType to them. - // Mock these first so Mockito doesn't try to clobber the original overload - // because NamespacedKey extends Key. - doAnswer(invocation -> { - Key key = invocation.getArgument(0); - return registry.get(Objects.requireNonNull(NamespacedKey.fromString(key.asString()))); - }).when(registry).get(any(Key.class)); - doAnswer(invocation -> { - Key key = invocation.getArgument(0); - return registry.getOrThrow(Objects.requireNonNull(NamespacedKey.fromString(key.asString()))); - }).when(registry).getOrThrow(any(Key.class)); - - Answer getOrThrow = invocationGetEntry -> { - NamespacedKey key = invocationGetEntry.getArgument(0); - return values.computeIfAbsent(key, key1 -> RegistryHelper.getOrThrow(clazz, key1)); - }; - - doAnswer(getOrThrow).when(registry).getOrThrow(any(NamespacedKey.class)); - // For get, return null for nonexistant constants. - doAnswer(invocation -> { - try { - return getOrThrow.answer(invocation); - } catch (RuntimeException e) { - if (e.getCause() instanceof NoSuchFieldException) { - return null; - } - throw e; - } - }).when(registry).get(any(NamespacedKey.class)); - - Map, Tag> tags = new HashMap<>(); - - doAnswer(invocation -> { - TagKey tagKey = invocation.getArgument(0); - return tags.computeIfAbsent(tagKey, key -> RegistryHelper.getTag(clazz, key)); - }).when(registry).getTag(any(TagKey.class)); - - return registry; - }); - } - -} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestServerBuildInfo.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestServerBuildInfo.java deleted file mode 100644 index 8bc1fcc..0000000 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/server/TestServerBuildInfo.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.github.jikoo.enchantableblocks.mock.server; - -import io.papermc.paper.ServerBuildInfo; -import net.kyori.adventure.key.Key; -import org.jetbrains.annotations.NotNull; - -import java.time.Instant; -import java.util.Optional; -import java.util.OptionalInt; - -public class TestServerBuildInfo implements ServerBuildInfo { - - @Override - public @NotNull Key brandId() { - return Key.key("dummyserver:dummy"); - } - - @Override - public boolean isBrandCompatible(@NotNull Key brandId) { - return true; - } - - @Override - public @NotNull String brandName() { - return "dummyserver"; - } - - @Override - public @NotNull String minecraftVersionId() { - return "1"; - } - - @Override - public @NotNull String minecraftVersionName() { - return "1.2.3"; - } - - @Override - public @NotNull OptionalInt buildNumber() { - return OptionalInt.empty(); - } - - @Override - public @NotNull Instant buildTime() { - return Instant.now(); - } - - @Override - public @NotNull Optional gitBranch() { - return Optional.empty(); - } - - @Override - public @NotNull Optional gitCommit() { - return Optional.empty(); - } - - @Override - public @NotNull String asString(@NotNull StringRepresentation representation) { - return ""; - } -} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/world/BlockDataMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/world/BlockDataMocks.java deleted file mode 100644 index b720b38..0000000 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/world/BlockDataMocks.java +++ /dev/null @@ -1,203 +0,0 @@ -package com.github.jikoo.enchantableblocks.mock.world; - -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.EnumSet; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import org.bukkit.Axis; -import org.bukkit.Material; -import org.bukkit.block.BlockFace; -import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.Directional; -import org.bukkit.block.data.MultipleFacing; -import org.bukkit.block.data.Orientable; -import org.bukkit.block.data.Rotatable; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.mockito.ArgumentMatchers; - -public final class BlockDataMocks { - - public static @Nullable BlockData newData(@NotNull Material material) { - if (!material.isBlock()) { - throw new IllegalArgumentException("Material must be a block!"); - } - if (material == Material.AIR) { - return null; - } - Class data = material.data; - - if (!BlockData.class.isAssignableFrom(data)) { - data = BlockData.class; - } - - BlockData mock = (BlockData) mock(data); - generic(material, mock); - - when(mock.clone()).thenAnswer(parameters -> newData(material)); - - if (mock instanceof Directional directional) { - directional(directional); - } - if (mock instanceof MultipleFacing multipleFacing) { - multipleFacing(multipleFacing); - } - if (mock instanceof Orientable orientable) { - orientable(orientable); - } - if (mock instanceof Rotatable rotatable) { - rotatable(rotatable); - } - - return mock; - } - - private static void generic(@NotNull Material material, @NotNull BlockData blockData) { - when(blockData.getMaterial()).thenReturn(material); - } - - public static @NotNull Directional directional(@NotNull Material material) { - Directional mock = mock(Directional.class); - generic(material, mock); - - directional(mock); - - return mock; - } - - private static void directional(@NotNull Directional mock) { - Set availableFaces = EnumSet.range(BlockFace.NORTH, BlockFace.DOWN); - when(mock.getFaces()).thenReturn(availableFaces); - - AtomicReference facing = new AtomicReference<>(BlockFace.NORTH); - when(mock.getFacing()).thenAnswer(parameters -> facing.get()); - - doAnswer(parameters -> { - BlockFace newFacing = parameters.getArgument(0); - if (!availableFaces.contains(newFacing)) { - throw new IllegalArgumentException("Facing not allowed!"); - } - facing.set(newFacing); - return null; - }).when(mock).setFacing(ArgumentMatchers.notNull()); - - when(mock.clone()).thenAnswer(invocation -> { - Directional clone = directional(mock.getMaterial()); - clone.setFacing(facing.get()); - return clone; - }); - } - - public static @NotNull MultipleFacing multipleFacing(@NotNull Material material) { - MultipleFacing mock = mock(MultipleFacing.class); - generic(material, mock); - - multipleFacing(mock); - - return mock; - } - - private static void multipleFacing(@NotNull MultipleFacing mock) { - Set allowedFaces = EnumSet.range(BlockFace.NORTH, BlockFace.DOWN); - when(mock.getAllowedFaces()).thenReturn(allowedFaces); - - Set faces = EnumSet.noneOf(BlockFace.class); - when(mock.getFaces()).thenReturn(faces); - - when(mock.hasFace(ArgumentMatchers.any(BlockFace.class))).thenAnswer(parameters -> { - BlockFace face = parameters.getArgument(0); - return faces.contains(face); - }); - - doAnswer(parameters -> { - BlockFace face = parameters.getArgument(0); - boolean has = parameters.getArgument(1); - - if (!has) { - faces.remove(face); - return null; - } - - if (!allowedFaces.contains(face)) { - throw new IllegalArgumentException("Facing not allowed!"); - } - - faces.add(face); - return null; - }).when(mock).setFace(ArgumentMatchers.notNull(), ArgumentMatchers.anyBoolean()); - - when(mock.clone()).thenAnswer(invocation -> { - MultipleFacing clone = multipleFacing(mock.getMaterial()); - faces.forEach(face -> clone.setFace(face, true)); - return clone; - }); - } - - public static @NotNull Orientable orientable(@NotNull Material material) { - Orientable mock = mock(Orientable.class); - generic(material, mock); - - orientable(mock); - - return mock; - } - - private static void orientable(@NotNull Orientable mock) { - AtomicReference axis = new AtomicReference<>(Axis.Y); - when(mock.getAxis()).thenAnswer(parameters -> axis.get()); - - Set axes = EnumSet.allOf(Axis.class); - when(mock.getAxes()).thenReturn(axes); - - doAnswer(parameters -> { - Axis newAxis = parameters.getArgument(0); - if (!axes.contains(newAxis)) { - throw new IllegalArgumentException("Axis not allowed!"); - } - axis.set(newAxis); - return null; - }).when(mock).setAxis(ArgumentMatchers.notNull()); - - when(mock.clone()).thenAnswer(invocation -> { - Orientable clone = orientable(mock.getMaterial()); - clone.setAxis(axis.get()); - return clone; - }); - } - - public static @NotNull Rotatable rotatable(@NotNull Material material) { - Rotatable mock = mock(Rotatable.class); - generic(material, mock); - - rotatable(mock); - - return mock; - } - - private static void rotatable(@NotNull Rotatable mock) { - AtomicReference axis = new AtomicReference<>(BlockFace.NORTH); - when(mock.getRotation()).thenAnswer(parameters -> axis.get()); - - Set axes = EnumSet.range(BlockFace.NORTH, BlockFace.DOWN); - doAnswer(parameters -> { - BlockFace newAxis = parameters.getArgument(0); - if (!axes.contains(newAxis)) { - throw new IllegalArgumentException("Rotatable may only face an axis!"); - } - axis.set(newAxis); - return null; - }).when(mock).setRotation(ArgumentMatchers.notNull()); - - when(mock.clone()).thenAnswer(invocation -> { - Rotatable clone = rotatable(mock.getMaterial()); - clone.setRotation(axis.get()); - return clone; - }); - } - - private BlockDataMocks() {} - -} diff --git a/src/test/java/com/github/jikoo/enchantableblocks/mock/world/BlockMocks.java b/src/test/java/com/github/jikoo/enchantableblocks/mock/world/BlockMocks.java index c8debd8..6a775fa 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/mock/world/BlockMocks.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/mock/world/BlockMocks.java @@ -1,13 +1,5 @@ package com.github.jikoo.enchantableblocks.mock.world; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.util.concurrent.atomic.AtomicReference; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; @@ -17,6 +9,15 @@ import org.jetbrains.annotations.NotNull; import org.mockito.stubbing.Answer; +import java.util.concurrent.atomic.AtomicReference; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public final class BlockMocks { public static @NotNull Block newBlock(@NotNull World world, int x, int y, int z) { @@ -53,9 +54,6 @@ public static void mockType(@NotNull Block block) { when(block.getType()).thenAnswer(parameters -> type.get()); Answer setType = parameters -> { Material argument = parameters.getArgument(0); - if (!argument.isBlock()) { - throw new IllegalArgumentException("Cannot set block type to non-block material!"); - } type.set(argument); return null; }; @@ -67,9 +65,6 @@ public static void mockType(@NotNull Block block) { Answer setData = parameters -> { BlockData newData = parameters.getArgument(0); Material newType = newData.getMaterial(); - if (!newType.isBlock()) { - throw new IllegalArgumentException("Cannot set block type to non-block material!"); - } type.set(newType); data.set(newData); return null; diff --git a/src/test/java/com/github/jikoo/enchantableblocks/registry/EnchantableBlockManagerTest.java b/src/test/java/com/github/jikoo/enchantableblocks/registry/EnchantableBlockManagerTest.java index bec2610..7fbaf0b 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/registry/EnchantableBlockManagerTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/registry/EnchantableBlockManagerTest.java @@ -2,7 +2,6 @@ import com.github.jikoo.enchantableblocks.block.EnchantableBlock; import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; -import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.answer.SpiedAnswer; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; import com.github.jikoo.enchantableblocks.mock.world.WorldMocks; @@ -11,11 +10,14 @@ import com.github.jikoo.enchantableblocks.util.Region; import com.github.jikoo.enchantableblocks.util.RegionStorage; import com.github.jikoo.planarwrappers.util.Coords; +import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.Material; +import org.bukkit.Registry; import org.bukkit.block.Block; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFactory; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; @@ -30,6 +32,7 @@ import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.MockitoAnnotations; import java.lang.reflect.Field; @@ -51,10 +54,10 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; @DisplayName("Feature: Manage enchantable blocks.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -79,21 +82,40 @@ class EnchantableBlockManagerTest { private Chunk chunkBad; @Captor ArgumentCaptor> loggerCaptor; AutoCloseable mockAnnotations; + private MockedStatic bukkit; @BeforeAll - void beforeAll() { - var server = ServerMocks.mockServer(); - var factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); + void setUp() { + bukkit = mockStatic(); - goodMat = Material.COAL_ORE; - badMat = Material.DIRT; - goodEnchant = Enchantment.EFFICIENCY; + ItemFactory itemFactory = ItemFactoryMocks.mockFactory(); + bukkit.when(Bukkit::getItemFactory).thenReturn(itemFactory); + + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry bukkitReg = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> mock(Enchantment.class)).when(bukkitReg).getOrThrow(any()); + } + return bukkitReg; + }); + + goodMat = mock(); + doReturn(true).when(goodMat).isBlock(); + doReturn("COAL_ORE").when(goodMat).name(); + badMat = mock(); + doReturn(true).when(badMat).isBlock(); + doReturn("DIRT").when(badMat).name(); + goodEnchant = mock(); block = WorldMocks.newWorld(NORMAL_WORLD_NAME).getBlockAt(0, 0, 0); blockDisabledWorld = WorldMocks.newWorld(DISABLED_WORLD_NAME).getBlockAt(0, 0, 0); } + @AfterAll + void tearDown() { + bukkit.close(); + } + @BeforeEach void beforeEach() { mockAnnotations = MockitoAnnotations.openMocks(this); @@ -136,11 +158,6 @@ void afterEach() throws Exception { mockAnnotations.close(); } - @AfterAll - void afterAll() { - ServerMocks.unsetBukkitServer(); - } - @DisplayName("Registry is obtainable for type registration.") @Test void testGetRegistry() { diff --git a/src/test/java/com/github/jikoo/enchantableblocks/registry/RegionInUseCheckTest.java b/src/test/java/com/github/jikoo/enchantableblocks/registry/RegionInUseCheckTest.java index 9ce31a6..ea77e8a 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/registry/RegionInUseCheckTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/registry/RegionInUseCheckTest.java @@ -1,32 +1,9 @@ package com.github.jikoo.enchantableblocks.registry; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableBlockManager.RegionStorageData; import com.github.jikoo.enchantableblocks.util.Region; import com.github.jikoo.enchantableblocks.util.RegionStorage; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Stream; +import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.Plugin; @@ -38,11 +15,38 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.MockedStatic; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @DisplayName("Feature: Load data on demand per region.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class RegionInUseCheckTest { + private MockedStatic bukkit; private Collection worlds; private Path dataDir; private EnchantableBlockManager manager; @@ -50,7 +54,7 @@ class RegionInUseCheckTest { @BeforeAll void beforeAll() { - var server = ServerMocks.mockServer(); + bukkit = mockStatic(); worlds = new ArrayList<>(); for (boolean loaded : new boolean[] { true, false }) { @@ -60,8 +64,7 @@ void beforeAll() { when(world.isChunkLoaded(any())).thenReturn(loaded); when(world.isChunkLoaded(anyInt(), anyInt())).thenReturn(loaded); when(world.getLoadedState()).thenReturn(loaded); - - when(server.getWorld(name)).thenReturn(world); + bukkit.when(() -> Bukkit.getWorld(name)).thenReturn(world); worlds.add(world); } @@ -89,6 +92,8 @@ void afterAll() throws IOException { } }); } + + bukkit.close(); } @DisplayName("Null value is never in use.") diff --git a/src/test/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipeTest.java b/src/test/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipeTest.java index 5b538ce..97e7989 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipeTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/util/EmptyCookingRecipeTest.java @@ -1,11 +1,13 @@ package com.github.jikoo.enchantableblocks.util; -import com.github.jikoo.enchantableblocks.mock.ServerMocks; import com.github.jikoo.enchantableblocks.mock.inventory.ItemFactoryMocks; +import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.RecipeChoice; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -13,25 +15,35 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.MockedStatic; import java.util.stream.Stream; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; @DisplayName("Feature: Placeholder empty unmodifiable cooking recipe") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class EmptyCookingRecipeTest { + private MockedStatic bukkit; private EmptyCookingRecipe recipe; @BeforeAll - void beforeAll() { - var server = ServerMocks.mockServer(); + void setUp() { + bukkit = mockStatic(); var factory = ItemFactoryMocks.mockFactory(); - when(server.getItemFactory()).thenReturn(factory); + bukkit.when(Bukkit::getItemFactory).thenReturn(factory); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> mock(Registry.class)); + } + + @AfterAll + void tearDown() { + bukkit.close(); } @BeforeEach @@ -48,7 +60,9 @@ void testAlwaysInvalid(ItemStack item) { static Stream getModernItems() { return Stream.of(Material.values()) - .filter(material -> !material.name().startsWith("LEGACY_") && material != Material.AIR && material.isItem()) + // Note that this is just modern materials now; modern items are partially data-driven. + // We could set up ItemType, or we could check block-exclusive materials and not worry about it. + .filter(material -> !material.name().startsWith("LEGACY_") && material != Material.AIR) .map(ItemStack::new); } diff --git a/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehaviorTest.java b/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehaviorTest.java index 1859a5f..a5e4dc4 100644 --- a/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehaviorTest.java +++ b/src/test/java/com/github/jikoo/enchantableblocks/util/enchant/BlockAnvilBehaviorTest.java @@ -1,22 +1,22 @@ package com.github.jikoo.enchantableblocks.util.enchant; import com.github.jikoo.enchantableblocks.config.EnchantableBlockConfig; -import com.github.jikoo.enchantableblocks.mock.ServerMocks; -import com.github.jikoo.enchantableblocks.mock.enchantments.EnchantmentMocks; import com.github.jikoo.enchantableblocks.registry.EnchantableRegistration; -import com.github.jikoo.planarenchanting.util.MetaCachedStack; import com.github.jikoo.planarwrappers.config.Mapping; import com.github.jikoo.planarwrappers.config.Setting; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; -import org.bukkit.Material; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemStack; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; +import org.mockito.MockedStatic; import java.util.Set; @@ -25,19 +25,39 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; @DisplayName("Feature: Configurable AnvilBehavior for EnchantableBlocks.") @TestInstance(TestInstance.Lifecycle.PER_CLASS) class BlockAnvilBehaviorTest { + private MockedStatic bukkit; private EnchantableRegistration registration; @BeforeAll - void beforeAll() { - ServerMocks.mockServer(); - EnchantmentMocks.init(); + void setUp() { + bukkit = mockStatic(); + bukkit.when(() -> Bukkit.getRegistry(any())).thenAnswer(invocation -> { + Registry registry = mock(Registry.class); + if (Enchantment.class.isAssignableFrom(invocation.getArgument(0))) { + doAnswer(invocation1 -> { + Enchantment enchantment = mock(Enchantment.class); + NamespacedKey key = invocation1.getArgument(0); + doReturn(key).when(enchantment).getKeyOrThrow(); + doReturn(key).when(enchantment).getKey(); + return enchantment; + }).when(registry).getOrThrow(any()); + } + return registry; + }); + } + + @AfterAll + void tearDown() { + bukkit.close(); } @BeforeEach @@ -61,14 +81,14 @@ void enchantApplies() { var anvilDisabledEnchants = registration.getConfig().anvilDisabledEnchants(); doReturn(Set.of(Enchantment.UNBREAKING)).when(anvilDisabledEnchants).get(anyString()); - var op = new BlockAnvilBehavior(registration, "sample_text"); + var op = new BlockAnvilBehavior<>(registration, "sample_text"); assertThat( "Enabled enchantment applies", - op.enchantApplies(Enchantment.EFFICIENCY, new MetaCachedStack(new ItemStack(Material.AIR)))); + op.enchantApplies(Enchantment.EFFICIENCY, new Object())); assertThat( "Disabled enchantment does not apply", - op.enchantApplies(Enchantment.UNBREAKING, new MetaCachedStack(new ItemStack(Material.AIR))), + op.enchantApplies(Enchantment.UNBREAKING, new Object()), is(false)); } @@ -79,7 +99,7 @@ void enchantsConflict() { conflicts.put(Enchantment.UNBREAKING, Enchantment.EFFICIENCY); doReturn(conflicts).when(anvilEnchantmentConflicts).get(anyString()); - var op = new BlockAnvilBehavior(registration, "sample_text"); + var op = new BlockAnvilBehavior<>(registration, "sample_text"); assertThat( "Enchantments conflict", @@ -106,7 +126,7 @@ void enchantMaxLevel() { Enchantment specifiedEnchant = Enchantment.UNBREAKING; doReturn(specificEnchantLevel).when(anvilEnchantmentMax).get(anyString(), eq(specifiedEnchant)); - var op = new BlockAnvilBehavior(registration, "sample_text"); + var op = new BlockAnvilBehavior<>(registration, "sample_text"); assertThat( "Specified enchantment max is provided", @@ -120,18 +140,18 @@ void enchantMaxLevel() { @Test void itemsCombineEnchants() { - var op = new BlockAnvilBehavior(registration, "sample_text"); + var op = new BlockAnvilBehavior<>(registration, "sample_text"); assertThat( "Items always combine", - op.itemsCombineEnchants(new MetaCachedStack(new ItemStack(Material.AIR)), new MetaCachedStack(new ItemStack(Material.AIR)))); + op.itemsCombineEnchants(new Object(), new Object())); } @Test void itemRepairedBy() { - var op = new BlockAnvilBehavior(registration, "sample_text"); + var op = new BlockAnvilBehavior<>(registration, "sample_text"); assertThat( "Item is never repairable", - !op.itemRepairedBy(new MetaCachedStack(new ItemStack(Material.AIR)), new MetaCachedStack(new ItemStack(Material.AIR)))); + !op.itemRepairedBy(new Object(), new Object())); } } diff --git a/src/test/resources/EnchantableBlocks/config.yml b/src/test/resources/EnchantableBlocks/config.yml index d35d39d..4d61ce7 100644 --- a/src/test/resources/EnchantableBlocks/config.yml +++ b/src/test/resources/EnchantableBlocks/config.yml @@ -6,7 +6,7 @@ blocks: fortuneList: - WET_SPONGE - STONE_BRICKS - tableEnchantability: STONE + tableEnchantability: STONE_TOOL tableDisabledEnchantments: [] tableEnchantmentConflicts: silk_touch: diff --git a/src/test/resources/META-INF/services/com.github.jikoo.planarenchanting.util.EnchantData$Provider b/src/test/resources/META-INF/services/com.github.jikoo.planarenchanting.util.EnchantData$Provider new file mode 100644 index 0000000..7d13224 --- /dev/null +++ b/src/test/resources/META-INF/services/com.github.jikoo.planarenchanting.util.EnchantData$Provider @@ -0,0 +1 @@ +com.github.jikoo.planarenchanting.util.DelegateEnchantProvider diff --git a/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo b/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo deleted file mode 100644 index 655b0c3..0000000 --- a/src/test/resources/META-INF/services/io.papermc.paper.ServerBuildInfo +++ /dev/null @@ -1 +0,0 @@ -com.github.jikoo.enchantableblocks.mock.server.TestServerBuildInfo diff --git a/src/test/resources/META-INF/services/io.papermc.paper.registry.RegistryAccess b/src/test/resources/META-INF/services/io.papermc.paper.registry.RegistryAccess deleted file mode 100644 index c210470..0000000 --- a/src/test/resources/META-INF/services/io.papermc.paper.registry.RegistryAccess +++ /dev/null @@ -1 +0,0 @@ -com.github.jikoo.enchantableblocks.mock.server.TestRegistryAccess diff --git a/src/test/resources/META-INF/services/org.bukkit.plugin.PluginLoader b/src/test/resources/META-INF/services/org.bukkit.plugin.PluginLoader deleted file mode 100644 index b8b2fa5..0000000 --- a/src/test/resources/META-INF/services/org.bukkit.plugin.PluginLoader +++ /dev/null @@ -1 +0,0 @@ -com.github.jikoo.enchantableblocks.mock.server.TestDummyPluginLoaderHolder \ No newline at end of file diff --git a/src/test/resources/generic_config.yml b/src/test/resources/generic_config.yml index 36f6a53..6505b1b 100644 --- a/src/test/resources/generic_config.yml +++ b/src/test/resources/generic_config.yml @@ -1,5 +1,5 @@ enabled: true -tableEnchantability: STONE +tableEnchantability: STONE_TOOL tableDisabledEnchantments: [] tableEnchantmentConflicts: silk_touch: