Skip to content

Commit a439be0

Browse files
authored
test: enforce full coverage and fix crafting edge cases (#28)
* test: enforce full coverage and fix crafting edge cases * test: prove null YAML icons use defaults
1 parent 024321a commit a439be0

50 files changed

Lines changed: 4991 additions & 80 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/build.yml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,11 @@ jobs:
7474
name: unit-test-reports-${{ github.run_id }}-${{ github.run_attempt }}
7575
path: target/surefire-reports/
7676
if-no-files-found: error
77+
78+
- name: Upload code coverage reports
79+
if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }}
80+
uses: actions/upload-artifact@v7
81+
with:
82+
name: code-coverage-${{ github.run_id }}-${{ github.run_attempt }}
83+
path: target/site/jacoco/
84+
if-no-files-found: error

‎.github/workflows/maven-release.yml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ jobs:
9090
if-no-files-found: error
9191
retention-days: 30
9292

93+
- name: Upload code coverage reports
94+
if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }}
95+
uses: actions/upload-artifact@v7
96+
with:
97+
name: code-coverage-${{ github.run_id }}-${{ github.run_attempt }}
98+
path: target/site/jacoco/
99+
if-no-files-found: error
100+
93101
publish:
94102
needs: build
95103
runs-on: ubuntu-24.04

‎README.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,23 @@ Copyright (c) 2026 TF-Minecraft contributors.
3939
TF-Minecraft-authored material in this repository is licensed under the
4040
[Artistic License 2.0](LICENSE). Third-party dependencies and bundled material
4141
retain their own licenses.
42+
43+
## Tests and coverage
44+
45+
Use Java 21 and Maven 3.9. Install the pinned plugin dependencies with the
46+
repository's existing `.github/scripts/prepare-release.sh` workflow, then run:
47+
48+
```sh
49+
mvn -B --no-transfer-progress clean verify -DskipTests=false -Dmaven.test.skip=false
50+
```
51+
52+
The JUnit 5 suite uses MockBukkit for server state and Mockito at external plugin
53+
boundaries. JaCoCo measures every production class; no production packages,
54+
classes, or methods are excluded. `verify` requires **100% instruction, branch,
55+
and line coverage**, and fails when any counter falls below that threshold.
56+
The HTML report is `target/site/jacoco/index.html`; XML/CSV are alongside it.
57+
Build and release CI upload the coverage report as an artifact, including failed
58+
runs when a report was generated.
59+
60+
Coverage proves the exercised Java behavior. It does not replace a live Paper
61+
server integration check with the pinned ItemsAdder/MMOItems/TLibs versions.

‎pom.xml‎

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,32 @@
1717
</properties>
1818

1919
<dependencies>
20+
<!-- ItemsAdder loads FastNBT dynamically on a server; mocks need its API types. -->
2021
<dependency>
21-
<groupId>junit</groupId>
22-
<artifactId>junit</artifactId>
23-
<version>4.13.2</version>
22+
<groupId>beer.devs</groupId>
23+
<artifactId>FastNbt-jar</artifactId>
24+
<version>1.4.22</version>
2425
<scope>test</scope>
2526
</dependency>
27+
<dependency>
28+
<groupId>org.junit.jupiter</groupId>
29+
<artifactId>junit-jupiter</artifactId>
30+
<version>5.10.2</version>
31+
<scope>test</scope>
32+
</dependency>
33+
<dependency>
34+
<groupId>org.mockito</groupId>
35+
<artifactId>mockito-core</artifactId>
36+
<version>5.20.0</version>
37+
<scope>test</scope>
38+
</dependency>
39+
<dependency>
40+
<groupId>org.mockbukkit.mockbukkit</groupId>
41+
<artifactId>mockbukkit-v1.21</artifactId>
42+
<version>4.95.0</version>
43+
<scope>test</scope>
44+
</dependency>
45+
2646
<dependency>
2747
<groupId>local</groupId>
2848
<artifactId>gson</artifactId>
@@ -103,6 +123,56 @@
103123
</resource>
104124
</resources>
105125
<plugins>
126+
<plugin>
127+
<groupId>org.apache.maven.plugins</groupId>
128+
<artifactId>maven-surefire-plugin</artifactId>
129+
<version>3.5.4</version>
130+
</plugin>
131+
<plugin>
132+
<groupId>org.jacoco</groupId>
133+
<artifactId>jacoco-maven-plugin</artifactId>
134+
<version>0.8.15</version>
135+
<executions>
136+
<execution>
137+
<goals><goal>prepare-agent</goal></goals>
138+
</execution>
139+
<execution>
140+
<id>coverage-report</id>
141+
<phase>verify</phase>
142+
<goals><goal>report</goal></goals>
143+
</execution>
144+
<execution>
145+
<id>coverage-check</id>
146+
<phase>verify</phase>
147+
<goals><goal>check</goal></goals>
148+
<configuration>
149+
<rules>
150+
<rule>
151+
<element>BUNDLE</element>
152+
<limits>
153+
<limit>
154+
<counter>INSTRUCTION</counter>
155+
<value>COVEREDRATIO</value>
156+
<minimum>1.0</minimum>
157+
</limit>
158+
<limit>
159+
<counter>LINE</counter>
160+
<value>COVEREDRATIO</value>
161+
<minimum>1.0</minimum>
162+
</limit>
163+
<limit>
164+
<counter>BRANCH</counter>
165+
<value>COVEREDRATIO</value>
166+
<minimum>1.0</minimum>
167+
</limit>
168+
</limits>
169+
</rule>
170+
</rules>
171+
</configuration>
172+
</execution>
173+
</executions>
174+
</plugin>
175+
106176
<plugin>
107177
<groupId>org.apache.maven.plugins</groupId>
108178
<artifactId>maven-compiler-plugin</artifactId>

‎src/main/java/net/tfminecraft/advancedcrafting/AdvancedCrafting.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ public void createFolders() {
146146
if(!subFolder.exists()) subFolder.mkdir();
147147
subFolder = new File(getDataFolder(), "colour-schemes");
148148
if(!subFolder.exists()) subFolder.mkdir();
149+
subFolder = new File(getDataFolder(), "model-schemes");
150+
if(!subFolder.exists()) subFolder.mkdir();
149151
subFolder = new File(getDataFolder(), "recipes");
150152
if(!subFolder.exists()) subFolder.mkdir();
151153
subFolder = new File(getDataFolder(), "data");

‎src/main/java/net/tfminecraft/advancedcrafting/database/AlloyRecipeStore.java‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public AlloyRecipe getRecipeByResult(String resultId) {
5454
public void upsert(AlloyRecipe recipe, String resultId) {
5555
File indexFile = recipe.resolveIndexFile(root);
5656
File parent = indexFile.getParentFile();
57-
if (parent != null && !parent.exists()) {
57+
if (!parent.exists()) {
5858
parent.mkdirs();
5959
}
6060
try (PrintWriter writer = new PrintWriter(indexFile, StandardCharsets.UTF_8)) {
@@ -122,7 +122,7 @@ public Map<String, String> loadAllComboResults() {
122122
Map<String, String> map = new LinkedHashMap<>();
123123
for (File indexFile : listIndexFiles()) {
124124
IndexEntry entry = readIndexEntry(indexFile);
125-
if (entry == null || entry.comboKey == null || entry.resultId == null) {
125+
if (entry == null || entry.comboKey == null) {
126126
continue;
127127
}
128128
map.put(entry.comboKey, entry.resultId);
@@ -199,10 +199,10 @@ private AlloyRecipe recipeFromIndexFile(File indexFile) {
199199
if (fileBase.equals(baseId)) {
200200
return new AlloyRecipe(baseId, java.util.List.of());
201201
}
202-
String catalystPart = fileBase.substring(baseId.length() + 2);
203202
if (!fileBase.startsWith(baseId + "__")) {
204203
return null;
205204
}
205+
String catalystPart = fileBase.substring(baseId.length() + 2);
206206
return new AlloyRecipe(baseId, java.util.Arrays.asList(catalystPart.split("__")));
207207
}
208208

‎src/main/java/net/tfminecraft/advancedcrafting/database/Database.java‎

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import java.io.InputStreamReader;
77
import java.io.PrintWriter;
88
import java.util.HashMap;
9+
import java.util.HashSet;
10+
import java.util.Set;
911
import java.util.TreeMap;
1012
import java.util.UUID;
1113

@@ -19,6 +21,7 @@
1921
import com.google.gson.Gson;
2022
import com.google.gson.GsonBuilder;
2123

24+
import net.tfminecraft.advancedcrafting.AdvancedCrafting;
2225
import net.tfminecraft.advancedcrafting.loaders.HitLoader;
2326
import net.tfminecraft.advancedcrafting.loaders.RecipeLoader;
2427
import net.tfminecraft.advancedcrafting.objects.crafting.CraftingRecipe;
@@ -27,15 +30,20 @@
2730

2831
public class Database {
2932
private JSONObject json; // org.json.simple
33+
private final Set<File> rejectedStationFiles = new HashSet<>();
3034
JSONParser parser = new JSONParser();
3135
public HashMap<Location, CraftingStation> loadStations() {
3236
HashMap<Location, CraftingStation> map = new HashMap<>();
33-
File folder = new File("plugins/AdvancedCrafting/data/stations");
34-
for(final File file : folder.listFiles()) {
37+
File folder = new File(AdvancedCrafting.plugin.getDataFolder(), "data/stations");
38+
File[] files = folder.listFiles();
39+
if (files == null) return map;
40+
for(final File file : files) {
3541
if(!file.isDirectory()) {
3642
try {
3743
json = (JSONObject) parser.parse(new InputStreamReader(new FileInputStream(file), "UTF-8"));
38-
Location loc = new Location(Bukkit.getServer().getWorld((String) json.get("world")), (Double) json.get("xPos"),(Double) json.get("yPos"),(Double) json.get("zPos"));
44+
org.bukkit.World world = Bukkit.getServer().getWorld((String) json.get("world"));
45+
if (world == null) throw new IllegalStateException("Station world is not loaded: " + json.get("world"));
46+
Location loc = new Location(world, (Double) json.get("xPos"),(Double) json.get("yPos"),(Double) json.get("zPos"));
3947
CraftingRecipe r = RecipeLoader.getByString((String) json.get("recipe"));
4048
HashMap<String, Integer> materials = new HashMap<>();
4149
int i = 0;
@@ -58,18 +66,22 @@ public HashMap<Location, CraftingStation> loadStations() {
5866
i++;
5967
}
6068
map.put(loc, new CraftingStation(loc, r, materials, hits));
69+
rejectedStationFiles.remove(file);
6170

6271
} catch (Exception ex) {
72+
rejectedStationFiles.add(file);
6373
ex.printStackTrace();
6474
}
6575
}
6676
}
6777
return map;
6878
}
6979
public void clear() {
70-
File folder = new File("plugins/AdvancedCrafting/data/stations");
71-
for(final File file : folder.listFiles()) {
72-
if(!file.isDirectory()) {
80+
File folder = new File(AdvancedCrafting.plugin.getDataFolder(), "data/stations");
81+
File[] files = folder.listFiles();
82+
if (files == null) return;
83+
for(final File file : files) {
84+
if(!file.isDirectory() && !rejectedStationFiles.contains(file)) {
7385
file.delete();
7486
}
7587
}
@@ -78,7 +90,12 @@ public void clear() {
7890
public void saveStation(CraftingStation s) {
7991
if(!s.hasRecipe()) return;
8092
try {
81-
File file = new File("plugins/AdvancedCrafting/data/stations",UUID.randomUUID().toString()+".json");
93+
File folder = new File(AdvancedCrafting.plugin.getDataFolder(), "data/stations");
94+
folder.mkdirs();
95+
File file = new File(folder, UUID.randomUUID().toString()+".json");
96+
while (rejectedStationFiles.contains(file)) {
97+
file = new File(folder, UUID.randomUUID().toString()+".json");
98+
}
8299
if(file.exists() == true) {
83100
file.delete();
84101
}
@@ -90,7 +107,7 @@ public void saveStation(CraftingStation s) {
90107
pw.close();
91108
HashMap<String, Object> defaults = new HashMap<String, Object>();
92109
json = (JSONObject) parser.parse(new InputStreamReader(new FileInputStream(file), "UTF-8"));
93-
defaults.put("world", s.getLoc().getWorld().toString().replace("CraftWorld{name=", "").replace("}", ""));
110+
defaults.put("world", s.getLoc().getWorld().getName());
94111
defaults.put("xPos", s.getLoc().getX());
95112
defaults.put("yPos", s.getLoc().getY());
96113
defaults.put("zPos", s.getLoc().getZ());

‎src/main/java/net/tfminecraft/advancedcrafting/loaders/ConfigLoader.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ private void parseGlobalStatOffset(String entry) {
121121
if (entry == null || entry.isBlank() || !entry.contains("(")) {
122122
return;
123123
}
124+
if (entry.lastIndexOf(')') < entry.indexOf('(')) {
125+
return;
126+
}
124127
String statId = entry.substring(0, entry.indexOf('(')).trim().toLowerCase();
125128
String amountPart = entry.substring(entry.indexOf('(') + 1, entry.lastIndexOf(')')).trim();
126129
try {

‎src/main/java/net/tfminecraft/advancedcrafting/managers/AlloyManager.java‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.bukkit.event.block.BlockBreakEvent;
1616
import org.bukkit.event.player.PlayerInteractEvent;
1717
import org.bukkit.inventory.ItemStack;
18+
import org.bukkit.inventory.EquipmentSlot;
1819
import org.bukkit.scheduler.BukkitRunnable;
1920

2021
import dev.lone.itemsadder.api.CustomStack;
@@ -104,6 +105,7 @@ public void run() {
104105
@SuppressWarnings("deprecation")
105106
@EventHandler
106107
public void addIngredient(PlayerInteractEvent e) {
108+
if (e.isCancelled() || e.getHand() != EquipmentSlot.HAND) return;
107109
if(!e.getAction().equals(Action.RIGHT_CLICK_BLOCK)) return;
108110
Block b = e.getClickedBlock();
109111
if (!isAlloyStation(b)) {
@@ -130,6 +132,10 @@ public void addIngredient(PlayerInteractEvent e) {
130132
return;
131133
}
132134
Ingredient ing = cs.getIngredient();
135+
if (ing == null) {
136+
p.sendMessage("§cThis ingredient is no longer configured.");
137+
return;
138+
}
133139
if (!ProfessionPermissions.canUseIngredient(p, ing)) {
134140
p.sendMessage(ProfessionPermissions.missingIngredientPermissionMessage(
135141
ing.getIngredientData().getPermission()));
@@ -205,8 +211,13 @@ public void nameAlloy(Player p, String s) {
205211
String name = StringFormatter.formatHex(new String(s).replace("_", " "));
206212
NamableAlloy alloy = naming.get(p);
207213
String oldId = alloy.getAlloy().getId();
214+
String id = StringFormatter.clean(s).toLowerCase(java.util.Locale.ROOT);
215+
Alloy existing = getAlloyById(id);
216+
if (existing != null && existing != alloy.getAlloy()) {
217+
p.sendMessage("§cAn alloy with that name already exists.");
218+
return;
219+
}
208220
removeAlloy(oldId);
209-
String id = StringFormatter.clean(s);
210221
alloy.getAlloy().setId(id);
211222
alloy.getAlloy().setName(name);
212223
ItemStack i = alloy.getItem();
@@ -232,6 +243,7 @@ public void nameAlloy(Player p, String s) {
232243

233244
@EventHandler
234245
public void breakStation(BlockBreakEvent e) {
246+
if (e.isCancelled()) return;
235247
Block b = e.getBlock();
236248
if (!isValidAlloyStation(b)) {
237249
return;

‎src/main/java/net/tfminecraft/advancedcrafting/managers/CommandManager.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ private boolean handleAcCommand(CommandSender sender, String[] args) {
137137
p.sendMessage("§cInvalid quality percent. Usage: /ac craft <percent>");
138138
return true;
139139
}
140+
if (!Double.isFinite(percent)) {
141+
p.sendMessage("§cInvalid quality percent. Usage: /ac craft <percent>");
142+
return true;
143+
}
140144
if (percent < 0) {
141145
percent = 0;
142146
}

0 commit comments

Comments
 (0)