Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion modules/Gem/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
<id>placeholderapi</id>
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
</repository>
<repository>
<id>helpch</id>
<url>https://repo.helpch.at/releases/</url>
</repository>
</repositories>

<dependencies>
Expand All @@ -78,7 +82,7 @@
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.11.1</version>
<version>2.11.5</version>
<scope>provided</scope>
</dependency>
</dependencies>
Expand Down
26 changes: 25 additions & 1 deletion modules/GemShop/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,34 @@
</dependency>
<dependency>
<groupId>com.mcatk</groupId>
<artifactId>gem</artifactId>
<artifactId>Gem</artifactId>
<type>jar</type>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>5.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.14.17</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.14.17</version>
<scope>test</scope>
</dependency>
</dependencies>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,28 @@
import com.mcatk.gemshop.Message;
import org.apache.commons.lang.text.StrBuilder;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

public class ItemShop {
private final ExecutorService ioExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "GemShop-IO-Thread");
t.setDaemon(true);
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While daemon threads prevent JVM hang on shutdown, they also mean that pending writes can be abruptly terminated when the plugin disables, potentially leading to data loss or corrupted config files. Consider making the thread non-daemon, or implementing a graceful shutdown mechanism that waits for pending writes to complete using awaitTermination() with a reasonable timeout in a shutdown method.

Copilot uses AI. Check for mistakes.
return t;
Comment on lines +21 to +26

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ensure async saves are flushed on disable

Because the executor thread is marked as a daemon and never shut down, any pending async config writes can be dropped on server shutdown or plugin reload, and stale queued writes from an old plugin instance can still run and overwrite newer config data. This is most visible if an item is added/removed and the server/plugin is stopped or reloaded shortly after. Consider shutting down the executor and awaiting completion (or switching to Bukkit’s async task scheduler with proper shutdown hooks) so saves are guaranteed to finish.

Useful? React with 👍 / 👎.

}
});
Comment on lines +21 to +28
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ExecutorService is never shut down, which will prevent the JVM from exiting cleanly and may cause resource leaks. Since ItemShop is instantiated once during plugin initialization (in ShopFactory constructor), you need to add a shutdown method to properly clean up the executor when the plugin is disabled. Consider adding a public method like shutdown() that calls ioExecutor.shutdown() or ioExecutor.shutdownNow(), and invoke this from GemShop's onDisable() method.

Copilot uses AI. Check for mistakes.

private HashMap<String, Items> itemsMap;

public ItemShop() {
Expand Down Expand Up @@ -58,14 +74,40 @@ public void addItem(Player player, String shopId, String itemId, String price) {
}
itemsMap.get(shopId).getMap().put(itemId, item);
GemShop.getPlugin().getConfig().set("Items." + shopId + "." + itemId, item);
GemShop.getPlugin().saveConfig();
saveConfigAsync();
player.sendMessage(Message.INFO + "添加成功:" + itemId + " " + price + "宝石");
}

public void delItem(String shopId, String itemId) {
itemsMap.get(shopId).getMap().remove(itemId);
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delItem method does not check if itemsMap.get(shopId) is null before calling getMap().remove(). If called with an invalid shopId, this will throw a NullPointerException. Consider adding a null check and appropriate error handling or logging to prevent crashes from invalid input.

Suggested change
itemsMap.get(shopId).getMap().remove(itemId);
Items items = itemsMap.get(shopId);
if (items == null) {
GemShop.getPlugin().getLogger().warning(
"Attempted to delete item '" + itemId + "' from unknown shopId '" + shopId + "'.");
return;
}
items.getMap().remove(itemId);

Copilot uses AI. Check for mistakes.
GemShop.getPlugin().getConfig().set("Items." + shopId + "." + itemId, null);
GemShop.getPlugin().saveConfig();
saveConfigAsync();
}

private void saveConfigAsync() {
if (!(GemShop.getPlugin().getConfig() instanceof YamlConfiguration)) {
GemShop.getPlugin().saveConfig();
return;
}

YamlConfiguration yaml = (YamlConfiguration) GemShop.getPlugin().getConfig();
final String data = yaml.saveToString();
final File file = new File(GemShop.getPlugin().getDataFolder(), "config.yml");
Comment on lines +87 to +95
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If GemShop.getPlugin() returns null (which could happen during certain initialization or shutdown sequences), this will throw a NullPointerException. Consider adding a null check at the start of the method to handle this edge case gracefully.

Copilot uses AI. Check for mistakes.

ioExecutor.submit(new Runnable() {
@Override
public void run() {
try {
if (file.getParentFile() != null && !file.getParentFile().exists()) {
file.getParentFile().mkdirs();
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mkdirs() method returns a boolean indicating success or failure, but this return value is ignored. If directory creation fails, Files.write() will subsequently throw an IOException, which is caught but only logged. Consider checking the return value of mkdirs() and logging a more specific error message if directory creation fails, to help with debugging configuration issues.

Suggested change
file.getParentFile().mkdirs();
boolean created = file.getParentFile().mkdirs();
if (!created && !file.getParentFile().exists()) {
GemShop.getPlugin().getLogger().severe("Could not create parent directories for config.yml, aborting async save.");
return;
}

Copilot uses AI. Check for mistakes.
}
Files.write(file.toPath(), data.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
GemShop.getPlugin().getLogger().severe("Could not save config.yml asynchronously!");
e.printStackTrace();
}
}
});
}

public Items getItems(String shopId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.mcatk.gemshop.shops.itemshop;

import com.mcatk.gemshop.GemShop;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import java.io.File;
import java.lang.reflect.Field;
import java.util.logging.Logger;

import static org.junit.Assert.assertTrue;
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import for assertTrue is unused. It should be removed or the test should use proper JUnit assertions instead of throwing RuntimeException.

Suggested change
import static org.junit.Assert.assertTrue;

Copilot uses AI. Check for mistakes.
import static org.mockito.Mockito.*;

public class ItemShopBenchmarkTest {

private GemShop mockPlugin;
private YamlConfiguration mockConfig;
private Player mockPlayer;
private PlayerInventory mockInventory;

@Before
public void setUp() throws Exception {
mockPlugin = Mockito.mock(GemShop.class);
mockConfig = Mockito.mock(YamlConfiguration.class);
mockPlayer = Mockito.mock(Player.class);
mockInventory = Mockito.mock(PlayerInventory.class);
Logger mockLogger = Mockito.mock(Logger.class);

// Inject mock plugin
Field pluginField = GemShop.class.getDeclaredField("plugin");
pluginField.setAccessible(true);
pluginField.set(null, mockPlugin);

when(mockPlugin.getConfig()).thenReturn(mockConfig);
when(mockPlugin.getLogger()).thenReturn(mockLogger);
// Fix NPE for new File(dataFolder, ...)
File tempFolder = new File(System.getProperty("java.io.tmpdir"));
when(mockPlugin.getDataFolder()).thenReturn(tempFolder);
Comment on lines +44 to +46
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test writes to the system temp directory but doesn't clean up the config.yml file afterwards. This can leave test artifacts on the filesystem and potentially cause test interactions. Consider creating a unique temporary directory for each test run and cleaning it up in tearDown, or mock the file writing completely to avoid actual I/O during unit tests.

Copilot uses AI. Check for mistakes.

when(mockPlayer.getInventory()).thenReturn(mockInventory);
when(mockInventory.getItemInMainHand()).thenReturn(Mockito.mock(ItemStack.class));

when(mockConfig.getConfigurationSection(anyString())).thenReturn(null);
when(mockConfig.saveToString()).thenReturn("key: value");
}

@After
public void tearDown() throws Exception {
// Reset the static field
Field pluginField = GemShop.class.getDeclaredField("plugin");
pluginField.setAccessible(true);
pluginField.set(null, null);
}
Comment on lines +55 to +61
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test creates an ItemShop instance with an ExecutorService but never shuts it down. This can lead to test thread leaks and prevent the test JVM from exiting cleanly. Consider adding a cleanup step in the tearDown method to shut down the executor, or create a shutdown method in ItemShop that can be called during test cleanup.

Copilot uses AI. Check for mistakes.

@Test
public void testAddItemPerformance() {
// Setup slow saveConfig (which should NOT be called now)
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
Thread.sleep(50); // Simulate 50ms Disk I/O
return null;
}
}).when(mockPlugin).saveConfig();

ItemShop itemShop = new ItemShop();

long startTime = System.currentTimeMillis();
// Price "100" as string
itemShop.addItem(mockPlayer, "shop1", "item1", "100");
long endTime = System.currentTimeMillis();

long duration = endTime - startTime;
System.out.println("Benchmark Execution time: " + duration + "ms");

// Assert that saveConfig was NOT called (because we use async save)
verify(mockPlugin, never()).saveConfig();

// Assert that saveToString WAS called
verify(mockConfig).saveToString();

// In optimized code, duration should be near 0ms
if (duration >= 50) {
throw new RuntimeException("Execution time too slow: " + duration + "ms. Optimization failed?");
}
}
Comment on lines +91 to +94
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a RuntimeException for test failures is not the standard JUnit pattern. The test should use JUnit assertions like assertTrue(duration < 50, "Execution time too slow...") or assertThat(duration).isLessThan(50). This would provide better test reporting and integration with test runners.

Suggested change
if (duration >= 50) {
throw new RuntimeException("Execution time too slow: " + duration + "ms. Optimization failed?");
}
}
assertTrue("Execution time too slow: " + duration + "ms. Optimization failed?", duration < 50);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +94
Copy link

Copilot AI Feb 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test measures execution time immediately after calling addItem, but the actual file write happens asynchronously. This test doesn't verify that the async write actually completes successfully - it only verifies that the main thread doesn't block. Consider adding a mechanism to wait for the async operation to complete (e.g., using CountDownLatch or similar) and verify that the file write was actually attempted.

Copilot uses AI. Check for mistakes.

@Test
public void testDelItemOptimization() {
// Setup initial state
ItemShop itemShop = new ItemShop();
itemShop.addItem(mockPlayer, "shop1", "item1", "100");

// Reset mocks to clear interactions from addItem
Mockito.clearInvocations(mockConfig, mockPlugin);

itemShop.delItem("shop1", "item1");

// Assert that saveConfig was NOT called
verify(mockPlugin, never()).saveConfig();

// Assert that saveToString WAS called
verify(mockConfig).saveToString();
}
}
Loading