Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ Returning to the safe area clears the warning, keeping the border feedback tied

Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs).

## Tests and coverage

Run `mvn clean verify` with Java 21 and the pinned plugin dependencies installed.
The build runs the tests and enforces **100% executable-line coverage** across
all production Java classes, with no coverage exclusions. JaCoCo's HTML and XML
reports are written to `target/site/jacoco/` and uploaded by the Build workflow.
Branch and instruction coverage are reported separately; the enforced threshold
is line coverage. Build and maintenance scripts are outside this runtime-code metric.

## License

Copyright (c) 2026 TF-Minecraft contributors.
Expand Down
68 changes: 68 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,28 @@
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.proc>full</maven.compiler.proc>
<argLine></argLine>
</properties>

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.14.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.23.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.95.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
Expand Down Expand Up @@ -48,6 +67,55 @@
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.7.0</version>
<executions>
<execution><goals><goal>properties</goal></goals></execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<configuration>
<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>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
Expand Down
117 changes: 117 additions & 0 deletions src/test/java/net/tfminecraft/worldborder/WorldBorderTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package net.tfminecraft.worldborder;

import net.tfminecraft.worldborder.cache.Cache;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockbukkit.mockbukkit.MockBukkit;
import org.mockbukkit.mockbukkit.ServerMock;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.logging.Logger;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.contains;
import static org.mockito.Mockito.*;

class WorldBorderTest {
@TempDir Path directory;
private ServerMock server;
private WorldBorder plugin;

@BeforeEach
void setUp() {
Cache.worlds = new ArrayList<>();
Cache.borders = new LinkedHashMap<>();
Cache.tickIntervalTicks = 10;
Cache.damage = 8.0;
Cache.graceInset = 20;
Cache.titleWarning = "§cTurn back";
Cache.subtitleWarning = "§cYou feel continuing further would be very dangerous...";
server = MockBukkit.mock();
plugin = MockBukkit.load(WorldBorder.class);
}

@AfterEach
void tearDown() {
MockBukkit.unmock();
WorldBorder.plugin = null;
Cache.worlds = new ArrayList<>();
Cache.borders = new LinkedHashMap<>();
}

@Test
void enablesWithBundledConfigAndCommand() {
assertSame(plugin, WorldBorder.plugin);
assertTrue(Files.isRegularFile(plugin.getDataFolder().toPath().resolve("config.yml")));
assertEquals(10, Cache.tickIntervalTicks);
assertEquals(8.0, Cache.damage);
assertNotNull(plugin.getCommand("worldborder").getExecutor());
assertSame(plugin.getCommand("worldborder").getExecutor(),
plugin.getCommand("worldborder").getTabCompleter());
server.getScheduler().performTicks(10);
}

@Test
void reloadsExistingConfigWithoutReplacingIt() throws Exception {
Path config = plugin.getDataFolder().toPath().resolve("config.yml");
String custom = "tick-interval-ticks: 5\ndamage: 3\nborders: {}\n";
Files.writeString(config, custom);
plugin.reloadAll();
assertEquals(5, Cache.tickIntervalTicks);
assertEquals(3, Cache.damage);
plugin.onEnable();
assertEquals(custom, Files.readString(config));
plugin.onDisable();
plugin.onDisable();
}

@Test
void createsDataDirectoryAndReportsMissingCommand() throws Exception {
WorldBorder controlled = spy(plugin);
Path data = directory.resolve("new-data");
doReturn(data.toFile()).when(controlled).getDataFolder();
doReturn(null).when(controlled).getCommand("worldborder");
Logger logger = mock(Logger.class);
doReturn(logger).when(controlled).getLogger();
controlled.onEnable();
assertTrue(Files.isRegularFile(data.resolve("config.yml")));
verify(logger).severe("Command 'worldborder' missing from plugin.yml");
controlled.onDisable();
}

@Test
void reportsMissingBundledResource() {
WorldBorder controlled = spy(plugin);
doReturn(directory.toFile()).when(controlled).getDataFolder();
doReturn(null).when(controlled).getResource("config.yml");
Logger logger = mock(Logger.class);
doReturn(logger).when(controlled).getLogger();
controlled.onEnable();
assertFalse(Files.exists(directory.resolve("config.yml")));
verify(logger).warning("Missing bundled resource: config.yml");
verify(logger).severe(contains("Failed to load config.yml:"));
controlled.onDisable();
}

@Test
void reportsResourceReadFailureAndClosesItsStream() throws Exception {
WorldBorder controlled = spy(plugin);
doReturn(directory.toFile()).when(controlled).getDataFolder();
InputStream broken = mock(InputStream.class);
when(broken.transferTo(any())).thenThrow(new IOException("read failed"));
doReturn(broken).when(controlled).getResource("config.yml");
Logger logger = mock(Logger.class);
doReturn(logger).when(controlled).getLogger();
controlled.onEnable();
verify(logger).severe("Failed to copy default resource config.yml: read failed");
verify(broken).close();
controlled.onDisable();
}
}
Loading