Skip to content

Commit fd9aad0

Browse files
authored
Reach 100% runtime coverage and fix node, refund, and persistence bugs (#13)
* test: cover Dowsing runtime and fix regression bugs * fix: contain resource saves and align CI dependency version
1 parent 8622a49 commit fd9aad0

33 files changed

Lines changed: 3537 additions & 129 deletions

‎.github/workflows/build.yml‎

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

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ jobs:
7474
echo "jar=target/$final_name.jar" >> "$GITHUB_OUTPUT"
7575
mvn -B --no-transfer-progress clean verify -DskipTests=false -Dmaven.test.skip=false
7676
77+
- name: Upload coverage report
78+
if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }}
79+
uses: actions/upload-artifact@v7
80+
with:
81+
name: coverage-report-${{ github.run_id }}-${{ github.run_attempt }}
82+
path: target/site/jacoco/
83+
if-no-files-found: error
84+
7785
- name: Stage only the release JAR
7886
env:
7987
ARTIFACT_PATH: ${{ steps.maven.outputs.jar }}

‎README.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,12 @@ Copyright (c) 2026 TF-Minecraft contributors.
3636
TF-Minecraft-authored material in this repository is licensed under the
3737
[Artistic License 2.0](LICENSE). Third-party dependencies and pre-existing
3838
material retain their own licenses.
39+
40+
## Tests and coverage
41+
42+
Run `mvn clean verify` with Java 21. The build runs the unit tests and enforces
43+
100% executable runtime **line coverage** with JaCoCo, without production-class
44+
exclusions. Instruction and branch coverage are reported separately.
45+
46+
The HTML report is `target/site/jacoco/index.html`; the machine-readable report is
47+
`target/site/jacoco/jacoco.xml`. CI uploads these reports alongside test results.

‎pom.xml‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
<url>https://github.com/TF-Minecraft/Dowsing</url>
1313

1414
<properties>
15+
<denareconomy.version>0.2.4</denareconomy.version>
16+
<argLine></argLine>
1517
<maven.compiler.release>21</maven.compiler.release>
1618
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
1719
<maven.compiler.proc>full</maven.compiler.proc>
@@ -21,6 +23,22 @@
2123
</properties>
2224

2325
<dependencies>
26+
<dependency>
27+
<groupId>beer.devs</groupId>
28+
<artifactId>FastNbt-jar</artifactId>
29+
<version>1.4.22</version>
30+
<scope>test</scope>
31+
</dependency>
32+
<dependency>
33+
<groupId>net.tfminecraft</groupId>
34+
<artifactId>denareconomy</artifactId>
35+
<version>${denareconomy.version}</version>
36+
<!-- SimpleFactions Bank signatures expose this server-provided API. -->
37+
<scope>provided</scope>
38+
<exclusions>
39+
<exclusion><groupId>*</groupId><artifactId>*</artifactId></exclusion>
40+
</exclusions>
41+
</dependency>
2442
<dependency>
2543
<groupId>io.papermc.paper</groupId>
2644
<artifactId>paper-api</artifactId>
@@ -94,7 +112,7 @@
94112
<dependency>
95113
<groupId>org.mockito</groupId>
96114
<artifactId>mockito-core</artifactId>
97-
<version>5.20.0</version>
115+
<version>5.23.0</version>
98116
<scope>test</scope>
99117
</dependency>
100118
<dependency>
@@ -109,6 +127,12 @@
109127
</exclusion>
110128
</exclusions>
111129
</dependency>
130+
<dependency>
131+
<groupId>org.mockbukkit.mockbukkit</groupId>
132+
<artifactId>mockbukkit-v1.21</artifactId>
133+
<version>4.95.0</version>
134+
<scope>test</scope>
135+
</dependency>
112136
</dependencies>
113137

114138
<repositories>
@@ -137,6 +161,14 @@
137161
</resource>
138162
</resources>
139163
<plugins>
164+
<plugin>
165+
<groupId>org.apache.maven.plugins</groupId>
166+
<artifactId>maven-dependency-plugin</artifactId>
167+
<version>3.7.0</version>
168+
<executions>
169+
<execution><goals><goal>properties</goal></goals></execution>
170+
</executions>
171+
</plugin>
140172
<plugin>
141173
<groupId>org.apache.maven.plugins</groupId>
142174
<artifactId>maven-compiler-plugin</artifactId>
@@ -146,6 +178,42 @@
146178
<groupId>org.apache.maven.plugins</groupId>
147179
<artifactId>maven-surefire-plugin</artifactId>
148180
<version>3.5.4</version>
181+
<configuration>
182+
<argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off</argLine>
183+
<failIfNoTests>true</failIfNoTests>
184+
</configuration>
185+
</plugin>
186+
<plugin>
187+
<groupId>org.jacoco</groupId>
188+
<artifactId>jacoco-maven-plugin</artifactId>
189+
<version>0.8.15</version>
190+
<executions>
191+
<execution><goals><goal>prepare-agent</goal></goals></execution>
192+
<execution>
193+
<id>coverage-report</id>
194+
<phase>verify</phase>
195+
<goals><goal>report</goal></goals>
196+
</execution>
197+
<execution>
198+
<id>coverage-check</id>
199+
<phase>verify</phase>
200+
<goals><goal>check</goal></goals>
201+
<configuration>
202+
<rules>
203+
<rule>
204+
<element>BUNDLE</element>
205+
<limits>
206+
<limit>
207+
<counter>LINE</counter>
208+
<value>MISSEDCOUNT</value>
209+
<maximum>0</maximum>
210+
</limit>
211+
</limits>
212+
</rule>
213+
</rules>
214+
</configuration>
215+
</execution>
216+
</executions>
149217
</plugin>
150218
</plugins>
151219
</build>

‎src/main/java/net/tfminecraft/dowsing/loaders/BlockLoader.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public static NodeBlock getByBlock(Material m) {
3333
String plugin = b.getBlock().split("\\.")[0];
3434
if(!plugin.equalsIgnoreCase("v")) continue;
3535
String type = b.getBlock().split("\\.")[1];
36-
if(m.equals(Material.valueOf(type.toUpperCase()))) return new NodeBlock(b);
36+
if(m.equals(Material.valueOf(type.toUpperCase(java.util.Locale.ROOT)))) return new NodeBlock(b);
3737
}
3838
return null;
3939
}

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String
2424
sender.sendMessage("§cYou do not have access to this command!");
2525
return false;
2626
}
27+
if (args.length == 0) {
28+
sender.sendMessage("§eUsage: /dowsing <reload|createresource|deleteresource>");
29+
return false;
30+
}
2731
if(args[0].equalsIgnoreCase("reload")) {
2832
if(sender instanceof Player) {
2933
Player p = (Player) sender;
@@ -35,6 +39,10 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String
3539
if(sender instanceof Player) {
3640
Player p = (Player) sender;
3741
if(args[0].equalsIgnoreCase("createresource")) {
42+
if (args.length < 4) {
43+
sender.sendMessage("§eUsage: /dowsing createresource <id> <material> <yield>");
44+
return false;
45+
}
3846
String id = args[1];
3947
String line = args[2]+"."+args[3];
4048
try {
@@ -45,6 +53,10 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String
4553
e.printStackTrace();
4654
}
4755
} else if(args[0].equalsIgnoreCase("deleteresource")) {
56+
if (args.length < 2) {
57+
sender.sendMessage("§eUsage: /dowsing deleteresource <id>");
58+
return false;
59+
}
4860
String id = args[1];
4961
db.removeResource(id);
5062
p.sendMessage("§eResource deleted");

‎src/main/java/net/tfminecraft/dowsing/managers/InventoryManager.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,10 +200,12 @@ public void updateNodeView(Player p, Node n, Inventory i) {
200200
i.setItem(15, createGlobe(n));
201201
if(NodeManager.canPurchaseCapacity(n.getGuild())) {
202202
i.setItem(24, createCapacityButton(n));
203+
} else {
204+
i.setItem(24, null);
203205
}
204206
i.setItem(16, createCycle(n));
205207
i.setItem(17, createStatus(n));
206-
i.setItem(18, createDeleteButton());
208+
i.setItem(18, n.getBlock().isBreakable() || Permissions.isAdmin(p) ? createDeleteButton() : null);
207209
Integer slot = 0;
208210
while(slot < i.getSize()) {
209211
if(i.getItem(slot) == null) {

‎src/main/java/net/tfminecraft/dowsing/managers/NodeManager.java‎

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package net.tfminecraft.dowsing.managers;
22

33
import java.util.ArrayList;
4+
import java.util.Collection;
45
import java.util.HashMap;
56
import java.util.List;
67
import java.util.Map;
@@ -128,9 +129,9 @@ public static Integer getNodeCapacity(Guild g) {
128129
if(added > Cache.maxMemberCapacity) {
129130
added = Cache.maxMemberCapacity;
130131
}
131-
capacity = capacity+added+getExtraCapacity(g);
132+
capacity = capacity+added;
132133
}
133-
return capacity;
134+
return capacity+getExtraCapacity(g);
134135
}
135136
public Node getByLocation(Location loc) {
136137
for(Node n : nodes) {
@@ -139,7 +140,7 @@ public Node getByLocation(Location loc) {
139140
return null;
140141
}
141142
public String getClickedFurniture(Block b) {
142-
List<Entity> nearbyEntities = (List<Entity>) b.getWorld().getNearbyEntities(b.getLocation(), 0.2, 0.2, 0.2);
143+
Collection<Entity> nearbyEntities = b.getWorld().getNearbyEntities(b.getLocation(), 0.2, 0.2, 0.2);
143144
for(Entity a : b.getWorld().getEntities()){
144145
if(nearbyEntities.contains(a)){
145146
CustomFurniture f = CustomFurniture.byAlreadySpawned(a);
@@ -230,12 +231,14 @@ public void validate() {
230231
Node n = nodes.get(i);
231232
if(!n.getLoc().getBlock().getType().equals(Material.BARRIER)) {
232233
n.breakNode();
233-
nodes.remove(i);
234+
nodes.remove(i--);
234235
}
235236
}
236237
}
237238
public void confirmClick(Player p, Node n, ConfirmType t) {
238239
if(t != ConfirmType.DEACTIVATE && blockPendingRefund(p, n)) return;
240+
if(t != ConfirmType.DEACTIVATE && blockActiveChange(p, n,
241+
t == ConfirmType.DELETE_NODE ? "delete node" : "change type")) return;
239242
if(t.equals(ConfirmType.DEACTIVATE)) {
240243
boolean wasActive = n.getIsActive();
241244
n.deActivate();
@@ -260,12 +263,28 @@ public void confirmClick(Player p, Node n, ConfirmType t) {
260263
inv.nodeView(p, n);
261264
currentNode.put(p, n);
262265
}
266+
confirm.remove(p);
263267
}
264268
private boolean blockPendingRefund(Player p, Node n) {
265269
if(!n.hasPendingRefund()) return false;
266270
p.sendMessage("§cRestore the barrel and hopper, then click Retry Refund before changing this node.");
267271
return true;
268272
}
273+
private boolean canManageNode(Player p, Node n) {
274+
if(p.hasPermission("dowsing.admin")) return true;
275+
Guild memberGuild = FactionManager.getGuildByMember(p.getName());
276+
Guild owner = n.getGuild();
277+
if(memberGuild != null && owner != null && owner.getId().equalsIgnoreCase(memberGuild.getId())) return true;
278+
p.sendMessage("§cCannot change another guild's node");
279+
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
280+
return false;
281+
}
282+
private boolean blockActiveChange(Player p, Node n, String action) {
283+
if(!n.getIsActive()) return false;
284+
p.sendMessage("§cCannot "+action+" while node is active");
285+
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
286+
return true;
287+
}
269288
@EventHandler(ignoreCancelled = true)
270289
public void placeVanillaNode(BlockPlaceEvent e) {
271290
NodeBlock b = BlockLoader.getByBlock(e.getBlock().getType());
@@ -407,11 +426,7 @@ public void invenClick(InventoryClickEvent e) {
407426
return;
408427
}
409428
Guild g = FactionManager.getGuildByMember(p.getName());
410-
if(!p.hasPermission("dowsing.admin") && (g == null || n.getGuild() == null || !n.getGuild().getId().equalsIgnoreCase(g.getId()))) {
411-
p.sendMessage("§cCannot change another guild's node");
412-
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
413-
return;
414-
}
429+
if(!canManageNode(p, n)) return;
415430
if(e.getSlot() != 17 && blockPendingRefund(p, n)) return;
416431
if(e.getSlot() == 8) {
417432
if(n.getIsActive()) {
@@ -517,6 +532,8 @@ public void invenClick(InventoryClickEvent e) {
517532
return;
518533
}
519534
NodeSlot slot = currentSlot.get(p);
535+
if(!canManageNode(p, n)) return;
536+
if(blockActiveChange(p, n, "change production methods")) return;
520537
ItemStack i = e.getCurrentItem();
521538
if(i == null) return;
522539
ProductionMethod pm = PMLoader.getByItemName(i.getItemMeta().getDisplayName());
@@ -553,6 +570,8 @@ public void invenClick(InventoryClickEvent e) {
553570
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BIT, 1f, 1f);
554571
return;
555572
}
573+
if(!canManageNode(p, n)) return;
574+
if(blockActiveChange(p, n, "change type")) return;
556575
ItemStack i = e.getCurrentItem();
557576
if(i == null) return;
558577
NodeType t = TypeLoader.getByItemName(i.getItemMeta().getDisplayName());
@@ -582,10 +601,10 @@ public void invenClick(InventoryClickEvent e) {
582601
return;
583602
}
584603
if(!confirm.containsKey(p)) return;
604+
if(e.getSlot() == 11 && !canManageNode(p, n)) return;
585605
p.playSound(p.getLocation(), Sound.BLOCK_NOTE_BLOCK_BIT, 1f, 1f);
586606
if(e.getSlot() == 11) {
587607
confirmClick(p, n, confirm.get(p));
588-
confirm.remove(p);
589608
} else if(e.getSlot() == 15) {
590609
inv.nodeView(p, n);
591610
currentNode.put(p, n);

‎src/main/java/net/tfminecraft/dowsing/managers/ResourceManager.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import java.io.IOException;
44
import java.util.HashMap;
5-
import java.util.List;
5+
import java.util.Collection;
66

77
import org.bukkit.ChatColor;
88
import org.bukkit.block.Block;
@@ -57,7 +57,7 @@ public void dowsingEvent(PlayerInteractEvent e) throws NumberFormatException, IO
5757
}
5858
}
5959
public Boolean clickedIsFurniture( Block b, String station) {
60-
List<Entity> nearbyEntities = (List<Entity>) b.getWorld().getNearbyEntities(b.getLocation(), 0.2, 0.2, 0.2);
60+
Collection<Entity> nearbyEntities = b.getWorld().getNearbyEntities(b.getLocation(), 0.2, 0.2, 0.2);
6161
for(Entity a : b.getWorld().getEntities()){
6262
if(nearbyEntities.contains(a)){
6363
CustomFurniture f = CustomFurniture.byAlreadySpawned(a);

‎src/main/java/net/tfminecraft/dowsing/objects/Node.java‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ public Node(Location l, Guild g, NodeBlock b) {
304304
this.cycleTime = 0;
305305
this.currentType = b.getTypes().get(0);
306306
this.upkeep = 0.0;
307+
this.naturalYield = 0;
307308
try {
308309
this.naturalYield = getNaturalYieldFromChunk(this.currentType.getResource(), this.loc);
309310
} catch (IOException e) {
@@ -601,13 +602,7 @@ Double getAddedPrestige(String e) {
601602
return 0.0;
602603
}
603604
public int getCapacity() {
604-
int capacity = 1;
605-
if(Cache.extraCapacity && resolveGuild() != null) {
606-
int members = this.guild.getMembers().size();
607-
int added = (int) Math.floorDiv(members, Cache.membersPerCapacity);
608-
capacity = capacity+added+NodeManager.getExtraCapacity(this.guild);
609-
}
610-
return capacity;
605+
return NodeManager.getNodeCapacity(resolveGuild());
611606
}
612607
void setCompleteDrops() {
613608
this.completeDrop.clear();

0 commit comments

Comments
 (0)