From d98941ffe906c37e5464b167af6115d01d35c364 Mon Sep 17 00:00:00 2001 From: Ivy Collective Date: Fri, 17 Jan 2025 14:39:06 -0500 Subject: [PATCH] add unit testing ""framework"" --- .forgejo/workflows/build.yml | 38 +++- README.md | 4 + build.gradle | 5 +- gradle.properties | 6 +- .../blazinggames/BlazingGames.java | 3 +- .../blazinggames/testing/BlazingTest.java | 48 +++++ .../testing/TestBlazingGames.java | 169 ++++++++++++++++++ .../testing/TestFailedException.java | 22 +++ .../blazinggames/testing/TestList.java | 26 +++ .../blazinggames/testing/TestRunner.java | 40 +++++ src/main/resources/config.testing.yml | 27 +++ src/main/resources/plugin.yml | 2 +- 12 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 src/main/java/de/blazemcworld/blazinggames/testing/BlazingTest.java create mode 100644 src/main/java/de/blazemcworld/blazinggames/testing/TestBlazingGames.java create mode 100644 src/main/java/de/blazemcworld/blazinggames/testing/TestFailedException.java create mode 100644 src/main/java/de/blazemcworld/blazinggames/testing/TestList.java create mode 100644 src/main/java/de/blazemcworld/blazinggames/testing/TestRunner.java create mode 100644 src/main/resources/config.testing.yml diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index a943e49..36da442 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -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 diff --git a/README.md b/README.md index fefc89d..bbac752 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/build.gradle b/build.gradle index 13a6b52..df8f6b7 100644 --- a/build.gradle +++ b/build.gradle @@ -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') { diff --git a/gradle.properties b/gradle.properties index 838736c..c46c921 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,5 @@ -version = STAGING \ No newline at end of file +version = STAGING + +test = false +mainClass = de.blazemcworld.blazinggames.BlazingGames +testClass = de.blazemcworld.blazinggames.testing.TestBlazingGames \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/BlazingGames.java b/src/main/java/de/blazemcworld/blazinggames/BlazingGames.java index 895b7c5..0429d8b 100644 --- a/src/main/java/de/blazemcworld/blazinggames/BlazingGames.java +++ b/src/main/java/de/blazemcworld/blazinggames/BlazingGames.java @@ -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 diff --git a/src/main/java/de/blazemcworld/blazinggames/testing/BlazingTest.java b/src/main/java/de/blazemcworld/blazinggames/testing/BlazingTest.java new file mode 100644 index 0000000..bc67c90 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/testing/BlazingTest.java @@ -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 + "\""); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/testing/TestBlazingGames.java b/src/main/java/de/blazemcworld/blazinggames/testing/TestBlazingGames.java new file mode 100644 index 0000000..50e5443 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/testing/TestBlazingGames.java @@ -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 tests = new ArrayList<>(List.of(TestList.values())); + private static final ArrayList 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); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/testing/TestFailedException.java b/src/main/java/de/blazemcworld/blazinggames/testing/TestFailedException.java new file mode 100644 index 0000000..0a2395f --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/testing/TestFailedException.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/testing/TestList.java b/src/main/java/de/blazemcworld/blazinggames/testing/TestList.java new file mode 100644 index 0000000..3827f42 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/testing/TestList.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/testing/TestRunner.java b/src/main/java/de/blazemcworld/blazinggames/testing/TestRunner.java new file mode 100644 index 0000000..fc4f696 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/testing/TestRunner.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/resources/config.testing.yml b/src/main/resources/config.testing.yml new file mode 100644 index 0000000..fcac8b5 --- /dev/null +++ b/src/main/resources/config.testing.yml @@ -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 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 84beb62..ac029aa 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -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]