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: on:
push: push:
branches: branches:
- main - main
jobs: 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: build-and-publish:
runs-on: docker runs-on: docker
needs: test
steps: steps:
- name: Checkout Code - name: Checkout Code
uses: actions/checkout@v3 uses: actions/checkout@v3

View file

@ -16,6 +16,10 @@ This is a standard Paper plugin using Gradle.
To build, use: `./gradlew build` 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 ## License
This plugin is licensed under the Apache License (version 2.0). For more information, please read the NOTICE and LICENSE files. 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 { processResources {
def props = [version: version] def props = [
version: version,
launchClass: ("true".equals(project.test)) ? project.testClass : project.mainClass
]
inputs.properties props inputs.properties props
filteringCharset 'UTF-8' filteringCharset 'UTF-8'
filesMatching('plugin.yml') { 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; import javax.crypto.SecretKey;
@SuppressWarnings("unused") public class BlazingGames extends JavaPlugin {
public final class BlazingGames extends JavaPlugin {
public boolean API_AVAILABLE = false; public boolean API_AVAILABLE = false;
// Gson // 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 name: blazinggames
version: '${version}' version: '${version}'
main: de.blazemcworld.blazinggames.BlazingGames main: '${launchClass}'
api-version: '1.21' api-version: '1.21'
prefix: BlazingGames prefix: BlazingGames
authors: [BlazeMCworld, sbot50, 'Ivy Collective (ivyc.)', XTerPL] authors: [BlazeMCworld, sbot50, 'Ivy Collective (ivyc.)', XTerPL]