build: add ArchUnit, JaCoCo, Spotless, SpotBugs, Error Prone/NullAway, forbidden-apis, CPD, dependency-analysis
This commit is contained in:
parent
7e227e39a4
commit
b771cc1c98
10 changed files with 275 additions and 2 deletions
52
src/test/java/dev/loki/loparkour/arch/ArchitectureTest.java
Normal file
52
src/test/java/dev/loki/loparkour/arch/ArchitectureTest.java
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package dev.loki.loparkour.arch;
|
||||
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchIgnore;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
|
||||
import static com.tngtech.archunit.library.freeze.FreezingArchRule.freeze;
|
||||
|
||||
/**
|
||||
* Enforces the dependency rules from docs/ARCHITECTURE.md. Rules wrapped in {@code freeze} record
|
||||
* existing violations in {@code src/test/resources/archunit_store}; only NEW violations fail the build.
|
||||
*/
|
||||
@AnalyzeClasses(packages = "dev.loki.loparkour", importOptions = ImportOption.DoNotIncludeTests.class)
|
||||
class ArchitectureTest {
|
||||
|
||||
private static final String ROOT = "dev.loki.loparkour";
|
||||
|
||||
// Disabled: the codebase has 100+ package cycles and freeze() cannot track cycle violations
|
||||
// stably (their ids change between runs). Enable once cycles are reduced (TODO task 2).
|
||||
@ArchIgnore
|
||||
@ArchTest
|
||||
static final ArchRule NO_PACKAGE_CYCLES = freeze(
|
||||
slices().matching(ROOT + ".(*)..").should().beFreeOfCycles());
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule API_IS_INDEPENDENT_OF_PLATFORM = freeze(
|
||||
noClasses().that().resideInAPackage(ROOT + ".api..")
|
||||
.should().dependOnClassesThat().resideInAnyPackage(
|
||||
ROOT + ".menu..", ROOT + ".command..", ROOT + ".storage..",
|
||||
ROOT + ".listener..", ROOT + ".hook.."));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule NO_BUKKIT_RUNNABLE_OUTSIDE_SCHEDULER = freeze(
|
||||
noClasses().that().resideOutsideOfPackage(ROOT + ".scheduler..")
|
||||
.should().dependOnClassesThat().areAssignableTo(BukkitRunnable.class)
|
||||
.because("all tasks go through scheduler/ so they work on Folia"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule NO_BUKKIT_SCHEDULER_OUTSIDE_SCHEDULER = freeze(
|
||||
noClasses().that().resideOutsideOfPackage(ROOT + ".scheduler..")
|
||||
.should().callMethod(org.bukkit.Bukkit.class, "getScheduler")
|
||||
.because("Bukkit.getScheduler() does not exist on Folia"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule NO_LEGACY_LOLIB =
|
||||
noClasses().should().dependOnClassesThat().resideInAPackage("dev.lolib..");
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package dev.loki.loparkour.arch;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Hard structure limits: at most 4 files per package folder, at most 200 lines per file.
|
||||
* Existing debt is listed explicitly and must only shrink; new violations fail the build.
|
||||
*/
|
||||
class StructureRulesTest {
|
||||
|
||||
private static final Path MAIN = Path.of("src/main/java");
|
||||
private static final int MAX_FILES_PER_FOLDER = 4;
|
||||
private static final int MAX_LINES_PER_FILE = 200;
|
||||
|
||||
/** Known debt (docs/TODO.md, task 2). Remove entries as folders get split. */
|
||||
private static final Set<String> FOLDER_DEBT = Set.of(
|
||||
"dev/loki/loparkour/command/schematic",
|
||||
"dev/loki/loparkour/elytra/generator",
|
||||
"dev/loki/loparkour/elytra/mode",
|
||||
"dev/loki/loparkour/generator/jump/calculation",
|
||||
"dev/loki/loparkour/generator/jump/placement",
|
||||
"dev/loki/loparkour/player/core");
|
||||
|
||||
@Test
|
||||
void foldersHaveAtMostFourFiles() throws IOException {
|
||||
final Map<String, Long> perFolder = new TreeMap<>();
|
||||
for (final Path file : javaFiles()) {
|
||||
perFolder.merge(MAIN.relativize(file.getParent()).toString(), 1L, Long::sum);
|
||||
}
|
||||
final List<String> violations = perFolder.entrySet().stream()
|
||||
.filter(e -> e.getValue() > MAX_FILES_PER_FOLDER)
|
||||
.map(Map.Entry::getKey)
|
||||
.filter(folder -> !FOLDER_DEBT.contains(folder))
|
||||
.collect(Collectors.toList());
|
||||
assertTrue(violations.isEmpty(), "Folders with more than " + MAX_FILES_PER_FOLDER + " files: " + violations);
|
||||
|
||||
final List<String> fixed = FOLDER_DEBT.stream()
|
||||
.filter(folder -> perFolder.getOrDefault(folder, 0L) <= MAX_FILES_PER_FOLDER)
|
||||
.collect(Collectors.toList());
|
||||
assertTrue(fixed.isEmpty(), "Folders now within the limit — remove them from FOLDER_DEBT: " + fixed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filesHaveAtMost200Lines() throws IOException {
|
||||
final List<String> violations = new java.util.ArrayList<>();
|
||||
for (final Path file : javaFiles()) {
|
||||
final long lines;
|
||||
try (Stream<String> content = Files.lines(file)) {
|
||||
lines = content.count();
|
||||
}
|
||||
if (lines > MAX_LINES_PER_FILE) {
|
||||
violations.add(MAIN.relativize(file) + " (" + lines + ")");
|
||||
}
|
||||
}
|
||||
assertTrue(violations.isEmpty(), "Files longer than " + MAX_LINES_PER_FILE + " lines: " + violations);
|
||||
}
|
||||
|
||||
private static List<Path> javaFiles() throws IOException {
|
||||
try (Stream<Path> walk = Files.walk(MAIN)) {
|
||||
return walk.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/test/resources/archunit.properties
Normal file
4
src/test/resources/archunit.properties
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Violation store for FreezingArchRule: existing debt is recorded, only new violations fail.
|
||||
freeze.store.default.path=src/test/resources/archunit_store
|
||||
freeze.store.default.allowStoreCreation=true
|
||||
freeze.refreeze=false
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
Class <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore$1> extends class <org.bukkit.scheduler.BukkitRunnable> in (MetricsCacheStore.java:0)
|
||||
Constructor <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore$1.<init>(dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore)> calls constructor <org.bukkit.scheduler.BukkitRunnable.<init>()> in (MetricsCacheStore.java:84)
|
||||
Field <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.autoSaveTask> has type <org.bukkit.scheduler.BukkitRunnable> in (MetricsCacheStore.java:0)
|
||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.startAutoSave()> calls constructor <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore$1.<init>(dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore)> in (MetricsCacheStore.java:84)
|
||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.startAutoSave()> calls method <org.bukkit.scheduler.BukkitRunnable.cancel()> in (MetricsCacheStore.java:81)
|
||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.startAutoSave()> calls method <org.bukkit.scheduler.BukkitRunnable.runTaskTimerAsynchronously(org.bukkit.plugin.Plugin, long, long)> in (MetricsCacheStore.java:91)
|
||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.stopAutoSave()> calls method <org.bukkit.scheduler.BukkitRunnable.cancel()> in (MetricsCacheStore.java:96)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Method <dev.loki.loparkour.duels.DuelsCountdown.giveStartItem(dev.loki.loparkour.player.core.ParkourPlayer)> calls method <org.bukkit.Bukkit.getScheduler()> in (DuelsCountdown.java:45)
|
||||
Method <dev.loki.loparkour.duels.DuelsCountdown.start()> calls method <org.bukkit.Bukkit.getScheduler()> in (DuelsCountdown.java:55)
|
||||
Method <dev.loki.loparkour.duels.DuelsGenerator.win(dev.loki.loparkour.player.core.ParkourPlayer)> calls method <org.bukkit.Bukkit.getScheduler()> in (DuelsGenerator.java:137)
|
||||
6
src/test/resources/archunit_store/stored.rules
Normal file
6
src/test/resources/archunit_store/stored.rules
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#
|
||||
#Tue Oct 06 13:42:44 CEST 2026
|
||||
no\ classes\ that\ reside\ in\ a\ package\ 'dev.loki.loparkour.api..'\ should\ depend\ on\ classes\ that\ reside\ in\ any\ package\ ['dev.loki.loparkour.menu..',\ 'dev.loki.loparkour.command..',\ 'dev.loki.loparkour.storage..',\ 'dev.loki.loparkour.listener..',\ 'dev.loki.loparkour.hook..']=d1d8a9d7-476c-4f86-adda-8531054aea1f
|
||||
no\ classes\ that\ reside\ outside\ of\ package\ 'dev.loki.loparkour.scheduler..'\ should\ call\ method\ Bukkit.getScheduler(),\ because\ Bukkit.getScheduler()\ does\ not\ exist\ on\ Folia=f1e3c34b-9b02-40e6-83a6-0269e173eb83
|
||||
no\ classes\ that\ reside\ outside\ of\ package\ 'dev.loki.loparkour.scheduler..'\ should\ depend\ on\ classes\ that\ are\ assignable\ to\ org.bukkit.scheduler.BukkitRunnable,\ because\ all\ tasks\ go\ through\ scheduler/\ so\ they\ work\ on\ Folia=339ab3ea-28aa-4f4b-a1e4-6c69e3ff0a0a
|
||||
slices\ matching\ 'dev.loki.loparkour.(*)..'\ should\ be\ free\ of\ cycles=58784f63-3f60-4edb-9733-fa0d7e11b877
|
||||
Loading…
Add table
Add a link
Reference in a new issue