Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,11 @@ jobs:
name: unit-test-reports-${{ github.run_id }}-${{ github.run_attempt }}
path: target/surefire-reports/
if-no-files-found: error

- name: Upload coverage report
if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }}
uses: actions/upload-artifact@v7
with:
name: coverage-report-${{ github.run_id }}-${{ github.run_attempt }}
path: target/site/jacoco/
if-no-files-found: error
8 changes: 8 additions & 0 deletions .github/workflows/maven-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ jobs:
echo "jar=target/$final_name.jar" >> "$GITHUB_OUTPUT"
mvn -B --no-transfer-progress clean verify -DskipTests=false -Dmaven.test.skip=false

- name: Upload coverage report
if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }}
uses: actions/upload-artifact@v7
with:
name: coverage-report-${{ github.run_id }}-${{ github.run_attempt }}
path: target/site/jacoco/
if-no-files-found: error

- name: Stage only the release JAR
env:
ARTIFACT_PATH: ${{ steps.maven.outputs.jar }}
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ Copyright (c) 2026 TF-Minecraft contributors.
TF-Minecraft-authored material in this repository is licensed under the
[Artistic License 2.0](LICENSE). Third-party dependencies and bundled material
retain their own licenses.

## Tests and coverage

Run `mvn clean verify` with Java 21 after installing the pinned dependencies. JaCoCo checks 100% executable runtime line coverage with no production-code exclusions. Instruction and branch coverage are reported separately in `target/site/jacoco/`; CI uploads the report for every build and release.
45 changes: 38 additions & 7 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.proc>full</maven.compiler.proc>
<argLine></argLine>
<tlibs.version>2.0.0</tlibs.version>
</properties>

Expand Down Expand Up @@ -44,13 +45,13 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<version>5.14.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.14.2</version>
<version>5.23.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -106,14 +107,44 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<version>3.5.4</version>
<configuration>
<argLine>-javaagent:${org.mockito:mockito-core:jar} -Xshare:off</argLine>
<classpathDependencyExcludes>
<classpathDependencyExclude>io.papermc.paper:paper-api</classpathDependencyExclude>
</classpathDependencyExcludes>
<argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off</argLine>
<failIfNoTests>true</failIfNoTests>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.15</version>
<executions>
<execution><goals><goal>prepare-agent</goal></goals></execution>
<execution>
<id>coverage-report</id>
<phase>verify</phase>
<goals><goal>report</goal></goals>
</execution>
<execution>
<id>coverage-check</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>MISSEDCOUNT</value>
<maximum>0</maximum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.bukkit.Bukkit;
Expand All @@ -14,6 +16,7 @@
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.plugin.IllegalPluginAccessException;

import net.tfminecraft.permcleaner.PermCleaner;
import net.tfminecraft.permcleaner.lp.CleanResult;
Expand Down Expand Up @@ -43,10 +46,10 @@ public boolean onCommand(CommandSender sender, Command command, String label, St
String sub = args[0].toLowerCase(Locale.ROOT);
switch (sub) {
case "reload" -> reload(sender);
case "status" -> status(sender, args);
case "inspect" -> inspect(sender, args);
case "clean" -> clean(sender, args, false);
case "force" -> clean(sender, args, true);
case "status" -> resolveTarget(sender, args, target -> status(sender, target));
case "inspect" -> resolveTarget(sender, args, target -> inspect(sender, target));
case "clean" -> resolveTarget(sender, args, target -> clean(sender, target, false));
case "force" -> resolveTarget(sender, args, target -> clean(sender, target, true));
default -> sender.sendMessage(ChatColor.RED + USAGE);
}
return true;
Expand All @@ -64,11 +67,7 @@ private void reload(CommandSender sender) {

// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
private void status(CommandSender sender, String[] args) {
Target target = resolveTarget(sender, args);
if (target == null) {
return;
}
private void status(CommandSender sender, Target target) {
String stored = plugin.stamps() == null
? null
: plugin.stamps().getSeason(target.uuid).orElse(null);
Expand All @@ -86,11 +85,7 @@ private void status(CommandSender sender, String[] args) {

// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
private void inspect(CommandSender sender, String[] args) {
Target target = resolveTarget(sender, args);
if (target == null) {
return;
}
private void inspect(CommandSender sender, Target target) {
sender.sendMessage(ChatColor.GREEN + "Inspecting " + ChatColor.WHITE + target.name + ChatColor.GREEN + "...");
plugin.cleaner().inspect(target.uuid, result -> {
if (!sender.equals(Bukkit.getConsoleSender()) && sender instanceof Player player && !player.isOnline()) {
Expand Down Expand Up @@ -120,9 +115,9 @@ private void inspect(CommandSender sender, String[] args) {

// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
private void clean(CommandSender sender, String[] args, boolean force) {
Target target = resolveTarget(sender, args);
if (target == null) {
private void clean(CommandSender sender, Target target, boolean force) {
if (plugin.seasonId().isBlank()) {
sender.sendMessage(ChatColor.RED + "Cleaning is disabled: season-id is blank.");
return;
}
if (!force && plugin.stamps() != null && !plugin.stamps().needsClean(target.uuid, plugin.seasonId())) {
Expand All @@ -148,26 +143,63 @@ private void clean(CommandSender sender, String[] args, boolean force) {

// Keep the existing legacy text representation, formatting, and exact-string comparisons.
@SuppressWarnings("deprecation")
private Target resolveTarget(CommandSender sender, String[] args) {
private void resolveTarget(CommandSender sender, String[] args, Consumer<Target> resolved) {
if (args.length < 2) {
if (sender instanceof Player player) {
return new Target(player.getUniqueId(), player.getName());
resolved.accept(new Target(player.getUniqueId(), player.getName()));
} else {
sender.sendMessage(ChatColor.RED + "Console must specify a player.");
}
sender.sendMessage(ChatColor.RED + "Console must specify a player.");
return null;
return;
}
String name = args[1];
if (name.isBlank()) {
sender.sendMessage(ChatColor.RED + "Unknown player " + name);
return;
}
Player online = Bukkit.getPlayerExact(args[1]);
Player online = Bukkit.getPlayerExact(name);
if (online != null) {
return new Target(online.getUniqueId(), online.getName());
resolved.accept(new Target(online.getUniqueId(), online.getName()));
return;
}
@SuppressWarnings("deprecation")
OfflinePlayer offline = Bukkit.getOfflinePlayer(args[1]);
OfflinePlayer cached = Bukkit.getOfflinePlayerIfCached(name);
if (cached != null) {
resolveOffline(sender, name, cached, resolved);
return;
}

try {
// Paper's resolver also honors proxy online-mode and offline UUID policy.
CompletableFuture.supplyAsync(() -> Bukkit.getPlayerUniqueId(name),
task -> Bukkit.getScheduler().runTaskAsynchronously(plugin, task)).whenComplete((uuid, failure) -> {
if (!plugin.isEnabled()) return;
try {
Bukkit.getScheduler().runTask(plugin, () -> {
if (!plugin.isEnabled() || (sender instanceof Player player && !player.isOnline())) return;
if (failure != null) {
sender.sendMessage(ChatColor.RED + "Player lookup failed for " + name + ". Please try again.");
} else if (uuid == null) {
sender.sendMessage(ChatColor.RED + "Unknown player " + name);
} else {
resolveOffline(sender, name, Bukkit.getOfflinePlayer(uuid), resolved);
}
});
} catch (IllegalPluginAccessException ignored) {
// The plugin was disabled between completing the lookup and scheduling the result.
}
});
} catch (IllegalPluginAccessException ignored) {
// The plugin was disabled before the lookup could be submitted.
}
}

@SuppressWarnings("deprecation")
private void resolveOffline(CommandSender sender, String name, OfflinePlayer offline, Consumer<Target> resolved) {
if (offline.hasPlayedBefore() && offline.getUniqueId() != null) {
String name = offline.getName() != null ? offline.getName() : args[1];
return new Target(offline.getUniqueId(), name);
resolved.accept(new Target(offline.getUniqueId(), offline.getName() != null ? offline.getName() : name));
} else {
sender.sendMessage(ChatColor.RED + "Unknown player " + name);
}
sender.sendMessage(ChatColor.RED + "Unknown player " + args[1]);
return null;
}

@Override
Expand Down
Loading