add unit testing ""framework""

This commit is contained in:
Ivy Collective 2025-01-17 14:39:06 -05:00
parent 7a08b8ca82
commit d98941ffe9
12 changed files with 384 additions and 6 deletions

View file

@ -1,11 +1,47 @@
name: Build and Publish
name: Build, Test and Publish
on:
push:
branches:
- main
jobs:
test:
runs-on: docker
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '21'
- name: Setup Gradle
uses: actions/gradle/setup-gradle@v4
with:
gradle-version: '8.11'
- name: Setup Paper
run: |
mkdir testsrv
curl -o testsrv/server.jar https://api.papermc.io/v2/projects/paper/versions/1.21/builds/130/downloads/paper-1.21-130.jar
echo "eula=true" > testsrv/eula.txt
- name: Build test jar with Gradle
run: gradle -Pversion=test-${{ github.run_number }} -Ptest=true build
- name: Install plugin
run: |
mkdir testsrv/plugins
cp build/libs/blazinggames-test-${{ github.run_number }}.jar testsrv/plugins/blazinggames.jar
- name: Run unit tests
timeout-minutes: 30
run: |
java -Xms2048M -Xmx2048M -jar testsrv/server.jar nogui --nogui
build-and-publish:
runs-on: docker
needs: test
steps:
- name: Checkout Code
uses: actions/checkout@v3

View file

@ -16,6 +16,10 @@ This is a standard Paper plugin using Gradle.
To build, use: `./gradlew build`
## Testing
This plugins supports testing. To run tests, use: `./gradlew build -Ptest=true`, and load the plugin normally. Once tests are done running, the file `TESTS_RESULT` in the server files directory will contain `true` if tests passed or `false` if tests failed.
## License
This plugin is licensed under the Apache License (version 2.0). For more information, please read the NOTICE and LICENSE files.

View file

@ -70,7 +70,10 @@ tasks.withType(JavaCompile).configureEach {
}
processResources {
def props = [version: version]
def props = [
version: version,
launchClass: ("true".equals(project.test)) ? project.testClass : project.mainClass
]
inputs.properties props
filteringCharset 'UTF-8'
filesMatching('plugin.yml') {

View file

@ -1 +1,5 @@
version = STAGING
version = STAGING
test = false
mainClass = de.blazemcworld.blazinggames.BlazingGames
testClass = de.blazemcworld.blazinggames.testing.TestBlazingGames

View file

@ -54,8 +54,7 @@ import java.util.Objects;
import javax.crypto.SecretKey;
@SuppressWarnings("unused")
public final class BlazingGames extends JavaPlugin {
public class BlazingGames extends JavaPlugin {
public boolean API_AVAILABLE = false;
// Gson

View file

@ -0,0 +1,48 @@
/*
* Copyright 2025 The Blazing Games Maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.blazemcworld.blazinggames.testing;
public abstract class BlazingTest {
public abstract boolean runAsync();
public boolean run() {
try {
runTest();
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
protected abstract void runTest() throws Exception;
// methods for test runner
protected void assertBoolean(String condition, boolean assertion) throws TestFailedException {
if (!assertion) {
throw new TestFailedException(getClass(), "assertBoolean failed: \"" + condition + "\"");
}
}
protected void assertEquals(Object expected, Object actual) throws TestFailedException {
if (expected == null && actual == null) {
return;
}
if (expected == null || actual == null || !expected.equals(actual)) {
throw new TestFailedException(getClass(), "assertEquals expected \"" + expected + "\" but got \"" + actual + "\"");
}
}
}

View file

@ -0,0 +1,169 @@
/*
* Copyright 2025 The Blazing Games Maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.blazemcworld.blazinggames.testing;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Bukkit;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import de.blazemcworld.blazinggames.BlazingGames;
public class TestBlazingGames extends BlazingGames {
public static final int TEST_RUNNERS = 5;
private static final ArrayList<TestList> tests = new ArrayList<>(List.of(TestList.values()));
private static final ArrayList<TestList> failedTests = new ArrayList<>();
private static boolean started = false;
private static int remainingRunners = TEST_RUNNERS;
@Override
public void onEnable() {
try {
super.onEnable();
if (!started) {
started = true;
if (tests.isEmpty()) {
getLogger().severe("No tests found!");
exit(false);
return;
}
getLogger().info("Starting " + tests.size() + " tests with " + TEST_RUNNERS + " runners...");
int runners = TEST_RUNNERS > tests.size() ? tests.size() : TEST_RUNNERS;
if (runners != TEST_RUNNERS) {
getLogger().warning("Not enough tests to run with " + TEST_RUNNERS + " runners! Running with " + runners + " instead");
remainingRunners = tests.size();
}
for (int i = 0; i < runners; i++) {
scheduleNextTest(null, false);
}
getLogger().info("Tests starting soon...");
} else {
getLogger().severe("Plugin was loaded twice. Exiting.");
exit(false);
}
} catch (Exception e) {
e.printStackTrace();
getLogger().severe("An unhandled exception occurred in onEnable. Exiting.");
exit(false);
}
}
@Override
public void onDisable() {
try {
super.onDisable();
} catch (Exception e) {
e.printStackTrace();
getLogger().severe("An unhandled exception occurred in onDisable. Exiting.");
exit(false);
}
}
@Override
public @NotNull FileConfiguration getConfig() {
var config = new YamlConfiguration();
// defaults (config.yml)
try (
InputStream stream = getClass().getResourceAsStream("/config.yml");
Reader reader = new InputStreamReader(stream);
) {
config.setDefaults(YamlConfiguration.loadConfiguration(reader));
} catch (IOException e) {
e.printStackTrace();
return null;
}
// overrides (config.testing.yml)
try (
InputStream stream = getClass().getResourceAsStream("/config.testing.yml");
Reader reader = new InputStreamReader(stream);
) {
config.load(reader);
} catch (IOException | InvalidConfigurationException e) {
e.printStackTrace();
return null;
}
return config;
}
public static void exit(boolean success) {
File file = new File("TESTS_RESULT");
file.delete();
try (FileWriter writer = new FileWriter(file)) {
writer.write(success ? "true" : "false");
} catch (IOException e) {
e.printStackTrace();
} finally {
Bukkit.getServer().shutdown();
}
}
public static synchronized void scheduleNextTest(final TestList currentTest, final boolean passed) {
TestList test = nextTest(currentTest, passed);
if (test != null) {
if (test.test.runAsync()) {
Bukkit.getScheduler().runTaskLaterAsynchronously(get(), new TestRunner(test, get().getLogger()), 5);
} else {
Bukkit.getScheduler().runTaskLater(get(), new TestRunner(test, get().getLogger()), 5);
}
} else {
remainingRunners--;
if (remainingRunners <= 0) {
final boolean isSuccess = failedTests.isEmpty();
get().getLogger().info("All tests are done.");
if (isSuccess) {
get().getLogger().info("All tests passed! Exiting cleanly.");
} else {
get().getLogger().severe("Some tests failed:");
for (TestList testList : failedTests) {
get().getLogger().severe("* " + testList.name());
}
}
Bukkit.getScheduler().runTaskLater(get(), () -> exit(isSuccess), 20);
}
}
}
public static synchronized TestList nextTest(final TestList currentTest, final boolean passed) {
if (currentTest != null) {
if (!passed) {
failedTests.add(currentTest);
}
}
if (tests.isEmpty()) {
return null;
}
return tests.remove(0);
}
}

View file

@ -0,0 +1,22 @@
/*
* Copyright 2025 The Blazing Games Maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.blazemcworld.blazinggames.testing;
public class TestFailedException extends Exception {
public TestFailedException(Class<?> clazz, String message) {
super("Test failed: " + clazz.getName() + ": " + message);
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2025 The Blazing Games Maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.blazemcworld.blazinggames.testing;
public enum TestList {
;
public final BlazingTest test;
TestList(BlazingTest test) {
this.test = test;
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2025 The Blazing Games Maintainers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package de.blazemcworld.blazinggames.testing;
import java.util.logging.Logger;
public class TestRunner implements Runnable {
private final TestList test;
private final Logger logger;
public TestRunner(TestList test, Logger logger) {
this.test = test;
this.logger = logger;
}
@Override
public void run() {
logger.info("Running test: " + test.name());
boolean passed = test.test.run();
if (!passed) {
logger.severe("Test failed: " + test.name());
} else {
logger.info("Test passed: " + test.name());
}
TestBlazingGames.scheduleNextTest(test, passed);
}
}

View file

@ -0,0 +1,27 @@
# overrides of config.yml for when unit testing is enabled
jda:
enabled: false
logging:
log-error: true
log-info: true
log-debug: true
notify-ops-on-error: false
computing:
local:
disable-computers: false
privileges:
chunkloading: true
net: true
microsoft:
spoof-ms-server: true
jwt:
secret-key: randomize-on-server-start
secret-key-is-password: false
services:
blazing-api:
enabled: true
blazing-wss:
enabled: true

View file

@ -1,6 +1,6 @@
name: blazinggames
version: '${version}'
main: de.blazemcworld.blazinggames.BlazingGames
main: '${launchClass}'
api-version: '1.21'
prefix: BlazingGames
authors: [BlazeMCworld, sbot50, 'Ivy Collective (ivyc.)', XTerPL]