diff --git a/.github/contributing.md b/.github/contributing.md index d18757a7..d30758ac 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -23,6 +23,8 @@ This way the team can discuss with you whether or not we want this in HySkript. - Use descriptive commit messages - Use descriptive PR titles - Ensure you follow the code style of this project +- Do make sure you add tests and run the test to make sure it works. + - See the [Testing Guide](https://github.com/SkriptDev/HySkript/tree/master/src/test/README.md) for more information. ### Don't: - Don't commit directly to `master` @@ -60,3 +62,6 @@ This way the team can discuss with you whether or not we want this in HySkript. - For expressions, please provide an example of using the getter as well as each changer you have applied. - For all others, please provide at least one example per pattern. - Please see other examples in HySkript for further inspiration. + +### Tests: +See the [Testing Guide](https://github.com/SkriptDev/HySkript/tree/master/src/test/README.md) for more information. diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 1c4a8308..2c39e737 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -5,7 +5,7 @@ # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle -name: Java CI with Gradle +name: Build and Upload Artifact on: push: diff --git a/.gitignore b/.gitignore index 8098e8e9..95247ca2 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ bin/ ### Mac OS ### .DS_Store **/.DS_Store + +### Run Folder ### +run/ diff --git a/build.gradle.kts b/build.gradle.kts index 62c02447..a18fcff7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,6 +15,11 @@ val hytaleVersion = "2026.02.06-aa1b071c2" // https://maven.hytale.com/pre-release/com/hypixel/hytale/Server/maven-metadata.xml // (Pre-releases shouldn't be used for production) +// Location of the Hytale Server Assets +// This is used in testing +// Change this to wherever you have it on your computer +val assetLocation = "/Users/ShaneBee/Desktop/Server/Hytale/Assets/Assets.zip" + repositories { mavenCentral() mavenLocal() @@ -34,6 +39,12 @@ dependencies { } } +// This is used to enable Gson in the test environment via HytaleServer +val testRunnerClasspath by configurations.creating { + extendsFrom(configurations.compileOnly.get()) + isCanBeResolved = true +} + tasks { register("server", Copy::class) { dependsOn("jar") @@ -42,6 +53,14 @@ tasks { destinationDir = file("/Users/ShaneBee/Desktop/Server/Hytale/Creative/mods/") } } + register("testRunner") { + dependsOn("jar") + group = "application" + mainClass.set("com.github.skriptdev.skript.api.skript.testing.TestRunnerMain") + args(hytaleVersion, assetLocation) + + classpath = sourceSets["main"].runtimeClasspath + testRunnerClasspath + } processResources { filesNotMatching("assets/**") { expand("pluginVersion" to projectVersion, "hytaleVersion" to hytaleVersion) diff --git a/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestProperties.java b/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestProperties.java new file mode 100644 index 00000000..34d4b6ab --- /dev/null +++ b/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestProperties.java @@ -0,0 +1,8 @@ +package com.github.skriptdev.skript.api.skript.testing; + +public class TestProperties { + + public static final boolean ENABLED = Boolean.parseBoolean(System.getProperty("skript.test.enabled", "false")); + public static final String TEST_SCRIPTS_FOLDER = System.getProperty("skript.test.scripts", "mods/skript_HySkript/tests"); + +} diff --git a/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestResults.java b/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestResults.java new file mode 100644 index 00000000..d44c0bb7 --- /dev/null +++ b/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestResults.java @@ -0,0 +1,85 @@ +package com.github.skriptdev.skript.api.skript.testing; + +import com.github.skriptdev.skript.api.utils.Utils; +import com.github.skriptdev.skript.plugin.HySk; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +public class TestResults { + + private boolean success = true; + private int failCount = 0; + private final Map> successMap = new TreeMap<>(); + private final Map> failureMap = new TreeMap<>(); + + public boolean isSuccess() { + return this.success; + } + + public int getFailCount() { + return this.failCount; + } + + public Map> getSuccessMap() { + return this.successMap; + } + + public Map> getFailureMap() { + return this.failureMap; + } + + public void addSuccess(String test, String value) { + this.successMap.computeIfAbsent(test, _ -> new ArrayList<>()).add(value); + } + + public void addFailure(String test, String value) { + this.success = false; + this.failCount++; + this.failureMap.computeIfAbsent(test, _ -> new ArrayList<>()).add(value); + } + + public void process() { + this.failureMap.forEach((test, _) -> { + // We don't care about success if other tests failed in that test + this.successMap.remove(test); + }); + + } + + public void clear() { + this.success = true; + this.successMap.clear(); + this.failureMap.clear(); + } + + @SuppressWarnings({"CallToPrintStackTrace"}) + public void printToJsonFile() { + Path resolve = HySk.getInstance().getDataDirectory().resolve("test-results.json"); + + try { + Files.createDirectories(resolve.getParent()); + } catch (IOException e) { + throw new RuntimeException("Failed to create directories for " + resolve.toAbsolutePath(), e); + } + + Gson gson = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create(); + + try (BufferedWriter writer = Files.newBufferedWriter(resolve, StandardCharsets.UTF_8)) { + gson.toJson(this, writer); + Utils.log("Test-Results successfully written to " + resolve.toAbsolutePath()); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestRunner.java b/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestRunner.java new file mode 100644 index 00000000..47413325 --- /dev/null +++ b/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestRunner.java @@ -0,0 +1,148 @@ +package com.github.skriptdev.skript.api.skript.testing; + +import com.github.skriptdev.skript.api.skript.testing.elements.EvtTest.TestContext; +import com.github.skriptdev.skript.api.utils.Utils; +import com.hypixel.hytale.server.core.HytaleServer; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.util.MessageUtil; +import fi.sulku.hytale.TinyMsg; +import io.github.syst3ms.skriptparser.lang.Statement; +import io.github.syst3ms.skriptparser.lang.Trigger; +import io.github.syst3ms.skriptparser.lang.TriggerMap; +import io.github.syst3ms.skriptparser.log.LogEntry; +import io.github.syst3ms.skriptparser.log.LogType; +import io.github.syst3ms.skriptparser.parsing.ScriptLoader; +import io.github.syst3ms.skriptparser.registration.SkriptAddon; +import io.github.syst3ms.skriptparser.variables.Variables; + +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class TestRunner { + + private static final ScheduledExecutorService SCHEDULED = HytaleServer.SCHEDULED_EXECUTOR; + private final TestResults testResults = new TestResults(); + private final World world = Universe.get().getWorld("default"); + + @SuppressWarnings("DataFlowIssue") + public void start() { + Runnable runTestsRunnable = () -> { + Utils.log("Running tests in world 'default'..."); + runTests(); + }; + Runnable loadTestsRunnable = () -> { + Utils.log("Testing has started!"); + Utils.log("Loading test scripts..."); + loadTests(); + Utils.log("Finished loading test scripts!"); + + // Make sure the world isn't paused + if (this.world.isPaused()) this.world.setPaused(false); + + // Run our tests in the world to make sure we have access to blocks/entities + this.world.execute(runTestsRunnable); + }; + + // Delay start to make sure the server has finished loading + SCHEDULED.schedule(loadTestsRunnable, 1, TimeUnit.SECONDS); + } + + private void loadTests() { + Path path = Path.of(TestProperties.TEST_SCRIPTS_FOLDER); + loadScripts(path); + } + + private void runTests() { + // Catch exceptions and treat them as failures + Statement.setExceptionHandler(e -> + this.testResults.addFailure("Exception", + e.getClass().getSimpleName() + ": " + e.getMessage())); + + // Run all the test triggers + for (Trigger allTrigger : TriggerMap.getAllTriggers()) { + TestContext context = new TestContext(this.testResults, this.world); + Statement.runAll(allTrigger, context); + Variables.clearLocalVariables(context); + } + + Runnable finishingRunnable = () -> { + // Process results + this.testResults.process(); + + // Print results + if (this.testResults.isSuccess()) { + Message message = TinyMsg.parse("All tests passed!"); + Utils.log(MessageUtil.toAnsiString(message).toAnsi()); + } else { + Utils.error(this.testResults.getFailCount() + " tests failed!"); + this.testResults.getFailureMap().forEach((test, failure) -> + Utils.error(" - [" + test + "]: " + failure)); + } + + Utils.log("Finished running tests!"); + + // Print results to file + this.testResults.printToJsonFile(); + this.testResults.clear(); + + // Shutdown server + // Give it a bit of delay to really make sure we're finished + SCHEDULED.schedule(() -> HytaleServer.get().shutdownServer(), 1, TimeUnit.SECONDS); + }; + + // Delay finish to allow waits in tests + SCHEDULED.schedule(finishingRunnable, 1, TimeUnit.SECONDS); + } + + private void loadScripts(Path directory) { + File scriptsDirectory = directory.toFile(); + Utils.log("Loading test directory '" + scriptsDirectory.getAbsolutePath() + "'..."); + List scriptNames = loadScriptsInDirectory(scriptsDirectory); + SkriptAddon.getAddons().forEach(SkriptAddon::finishedLoading); + + Utils.log("Loaded " + scriptNames.size() + " scripts!"); + } + + private List loadScriptsInDirectory(File directory) { + if (directory == null || !directory.isDirectory()) return List.of(); + + List loadedScripts = new ArrayList<>(); + + File[] files = directory.listFiles(); + if (files == null) return loadedScripts; + + Arrays.sort(files, + Comparator.comparing(File::isDirectory).reversed() // Directories first + .thenComparing(File::getName, String.CASE_INSENSITIVE_ORDER)); // Then sort by name alphabetically + + for (File file : files) { + // Skip disabled files and hidden files + String fileName = file.getName(); + if (fileName.startsWith("-") || fileName.startsWith(".")) continue; + if (file.isDirectory()) { + loadedScripts.addAll(loadScriptsInDirectory(file)); + } else { + if (!fileName.endsWith(".sk")) continue; + Utils.log("Loading script '" + fileName + "'..."); + List logEntries = ScriptLoader.loadScript(file.toPath(), false); + for (LogEntry logEntry : logEntries) { + Utils.log(null, logEntry); + if (logEntry.getType() == LogType.ERROR) { + this.testResults.addFailure("Parsing Error:" + fileName, logEntry.getMessage()); + } + } + loadedScripts.add(fileName.substring(0, fileName.length() - 3)); + } + } + return loadedScripts; + } + +} diff --git a/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestRunnerMain.java b/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestRunnerMain.java new file mode 100644 index 00000000..64e1b476 --- /dev/null +++ b/src/main/java/com/github/skriptdev/skript/api/skript/testing/TestRunnerMain.java @@ -0,0 +1,155 @@ +package com.github.skriptdev.skript.api.skript.testing; + +import com.google.gson.Gson; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +public class TestRunnerMain { + + public static final String GREEN = "\u001B[92m"; + public static final String LIGHT_GREY = "\u001B[37m"; + public static final String RED = "\u001B[91m"; + public static final String RESET = "\u001B[0m"; + + private static String serverVersion; + private static String assetPath; + + static void main(String[] args) { + serverVersion = args[0]; + assetPath = args[1]; + + System.out.println("Downloading Hytale Server..."); + downloadHytaleServer(); + System.out.println("Download complete!"); + + System.out.println("Moving plugin to mods folder..."); + movePlugin(); + System.out.println("Plugin moved!"); + + System.out.println("Starting Hytale server..."); + runServer(); + } + + @SuppressWarnings({"resource", "CallToPrintStackTrace"}) + private static void downloadHytaleServer() { + String url = "https://maven.hytale.com/release/com/hypixel/hytale/Server/" + + serverVersion + "/Server-" + serverVersion + ".jar"; + String targetDir = "run/testServer/"; + String newName = "HytaleServer.jar"; + + try { + // 1. Create the directory if it doesn't exist + Path directoryPath = Paths.get(targetDir); + if (!Files.exists(directoryPath)) { + Files.createDirectories(directoryPath); + } + + // 2. Download the file to a temporary location + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .build(); + + // We download directly to a path to save memory + Path tempFile = Files.createTempFile("tempDownload", ".tmp"); + client.send(request, HttpResponse.BodyHandlers.ofFile(tempFile)); + + // 3. Move and Rename the file + Path finalPath = directoryPath.resolve(newName); + Files.move(tempFile, finalPath, StandardCopyOption.REPLACE_EXISTING); + + System.out.println("File saved to: " + finalPath.toAbsolutePath()); + + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private static void movePlugin() { + File file = new File("run/testServer/mods"); + file.mkdirs(); + try { + Files.copy(Path.of("build/libs/HySkript-1.0.0-pre-release-3.jar"), + Path.of("run/testServer/mods/HySkript-1.0.0-pre-release-3.jar"), + StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("CallToPrintStackTrace") + private static void runServer() { + try { + File serverFolder = new File("run/testServer/"); + ProcessBuilder processBuilder = new ProcessBuilder( + "java", + "-Xms2G", + "-Xmx2G", + "-Dskript.test.enabled=true", + "-Dskript.test.scripts=../../src/test/skript/tests", + "-jar", "HytaleServer.jar", + "--assets", assetPath + ); + processBuilder.inheritIO(); + processBuilder.directory(serverFolder); + Process process = processBuilder.start(); + int exitCode = process.waitFor(); + + // Read results written by the plugin (no Gson required). + Path resultsPath = Path.of("run/testServer/mods/skript_HySkript/test-results.json"); + if (!Files.exists(resultsPath)) { + throw new IllegalStateException( + "Test results file not found at " + resultsPath.toAbsolutePath() + + " (server exit code was " + exitCode + ")" + ); + } + + Gson gson = new Gson(); + TestResults results; + + try (BufferedReader reader = Files.newBufferedReader(resultsPath, StandardCharsets.UTF_8)) { + results = gson.fromJson(reader, TestResults.class); + + System.out.println("Successfully loaded results from: " + resultsPath.getFileName()); + } catch (Exception e) { + System.err.println("Could not read test results: " + e.getMessage()); + e.printStackTrace(); + return; + } + + System.out.println("Succeeded:"); + if (!results.getSuccessMap().isEmpty()) { + results.getSuccessMap().forEach((test, _) -> + System.out.println(" - " + GREEN + test + RESET)); + } else { + System.out.println(" - none"); + } + + System.out.println("Failed:"); + if (results.getFailCount() > 0) { + results.getFailureMap().forEach((test, errorList) -> + errorList.forEach(error -> + System.out.println(" - " + RED + test + LIGHT_GREY + ": " + error + RESET))); + } else { + System.out.println(" - none"); + } + + System.exit(results.getFailCount()); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + +} diff --git a/src/main/java/com/github/skriptdev/skript/api/skript/testing/elements/EffAssert.java b/src/main/java/com/github/skriptdev/skript/api/skript/testing/elements/EffAssert.java new file mode 100644 index 00000000..e3d9ffca --- /dev/null +++ b/src/main/java/com/github/skriptdev/skript/api/skript/testing/elements/EffAssert.java @@ -0,0 +1,85 @@ +package com.github.skriptdev.skript.api.skript.testing.elements; + +import com.github.skriptdev.skript.api.skript.registration.SkriptRegistration; +import com.github.skriptdev.skript.api.skript.testing.TestResults; +import com.github.skriptdev.skript.api.skript.testing.elements.EvtTest.TestContext; +import io.github.syst3ms.skriptparser.lang.Effect; +import io.github.syst3ms.skriptparser.lang.Expression; +import io.github.syst3ms.skriptparser.lang.TriggerContext; +import io.github.syst3ms.skriptparser.log.SkriptLogger; +import io.github.syst3ms.skriptparser.parsing.ParseContext; +import io.github.syst3ms.skriptparser.parsing.SyntaxParser; +import io.github.syst3ms.skriptparser.types.PatternType; +import io.github.syst3ms.skriptparser.types.TypeManager; +import org.jetbrains.annotations.NotNull; + +import java.util.Optional; + +public class EffAssert extends Effect { + + public static void register(SkriptRegistration reg) { + reg.newEffect(EffAssert.class, "assert <.+> with %string%") + .noDoc() + .register(); + } + + private String fileName; + private int lineNumber; + private Expression condition; + private String conditionString; + private Expression message; + + @SuppressWarnings("unchecked") + @Override + public boolean init(Expression[] expressions, int matchedPattern, @NotNull ParseContext parseContext) { + this.message = (Expression) expressions[0]; + + SkriptLogger logger = parseContext.getLogger(); + this.fileName = logger.getFileName(); + this.lineNumber = logger.getLine() + 1; // I think it gets the last line?!?! + this.conditionString = parseContext.getMatches().getFirst().group(); + Optional> patternType = TypeManager.getPatternType("boolean"); + if (patternType.isEmpty()) { + return false; + } + + Optional> expression = SyntaxParser.parseExpression(this.conditionString, + patternType.get(), + parseContext.getParserState(), + parseContext.getLogger()); + if (expression.isEmpty() || expression.get().getReturnType() != Boolean.class) { + return false; + } + this.condition = (Expression) expression.get(); + return true; + } + + @Override + protected void execute(@NotNull TriggerContext ctx) { + if (!(ctx instanceof TestContext context)) return; + + Optional b = this.condition.getSingle(ctx).filter(Boolean::booleanValue); + TestResults testResults = context.getTestResults(); + + if (b.isEmpty()) { + // Test failed + String message = this.message.getSingle(ctx).orElseThrow(); + String failure = String.format("assert '%s' failed with message \"%s\" {%s:%d}", + this.conditionString, + message, + this.fileName, + this.lineNumber); + testResults.addFailure(context.getTestSubject(), failure); + } else { + String success = String.format("assert '%s' passed", + this.conditionString); + testResults.addSuccess(context.getTestSubject(), success); + } + } + + @Override + public String toString(@NotNull TriggerContext ctx, boolean debug) { + return "assert " + this.condition.toString(ctx, debug) + " with " + this.message.toString(ctx, debug); + } + +} diff --git a/src/main/java/com/github/skriptdev/skript/api/skript/testing/elements/ElementHandler.java b/src/main/java/com/github/skriptdev/skript/api/skript/testing/elements/ElementHandler.java new file mode 100644 index 00000000..9ab1a652 --- /dev/null +++ b/src/main/java/com/github/skriptdev/skript/api/skript/testing/elements/ElementHandler.java @@ -0,0 +1,12 @@ +package com.github.skriptdev.skript.api.skript.testing.elements; + +import com.github.skriptdev.skript.api.skript.registration.SkriptRegistration; + +public class ElementHandler { + + public static void register(SkriptRegistration reg) { + EffAssert.register(reg); + EvtTest.register(reg); + } + +} diff --git a/src/main/java/com/github/skriptdev/skript/api/skript/testing/elements/EvtTest.java b/src/main/java/com/github/skriptdev/skript/api/skript/testing/elements/EvtTest.java new file mode 100644 index 00000000..1cb48831 --- /dev/null +++ b/src/main/java/com/github/skriptdev/skript/api/skript/testing/elements/EvtTest.java @@ -0,0 +1,79 @@ +package com.github.skriptdev.skript.api.skript.testing.elements; + +import com.github.skriptdev.skript.api.skript.event.WorldContext; +import com.github.skriptdev.skript.api.skript.registration.SkriptRegistration; +import com.github.skriptdev.skript.api.skript.testing.TestResults; +import com.github.skriptdev.skript.api.utils.Utils; +import com.hypixel.hytale.server.core.universe.world.World; +import io.github.syst3ms.skriptparser.lang.Expression; +import io.github.syst3ms.skriptparser.lang.TriggerContext; +import io.github.syst3ms.skriptparser.lang.VariableString; +import io.github.syst3ms.skriptparser.lang.event.SkriptEvent; +import io.github.syst3ms.skriptparser.parsing.ParseContext; +import org.jetbrains.annotations.NotNull; + +public class EvtTest extends SkriptEvent { + + public static void register(SkriptRegistration reg) { + reg.newEvent(EvtTest.class, "test %*string%") + .setHandledContexts(TestContext.class) + .noDoc() + .register(); + } + + private String testSubject; + + @SuppressWarnings({"OptionalGetWithoutIsPresent"}) + @Override + public boolean init(Expression[] expressions, int matchedPattern, ParseContext parseContext) { + this.testSubject = ((VariableString) expressions[0]).getSingle(TriggerContext.DUMMY).get(); + return true; + } + + @Override + public boolean check(TriggerContext triggerContext) { + if (!(triggerContext instanceof TestContext context)) return false; + context.setTestSubject(this.testSubject); + Utils.logColored("Running test: '" + this.testSubject + "'"); + return true; + } + + @Override + public String toString(@NotNull TriggerContext triggerContext, boolean b) { + return "test " + this.testSubject; + } + + public static final class TestContext implements WorldContext { + private final TestResults testResults; + private String testSubject; + private final World world; + + public TestContext(TestResults testResults, World world) { + this.testResults = testResults; + this.world = world; + } + + public void setTestSubject(String testSubject) { + this.testSubject = testSubject; + } + + public String getTestSubject() { + return testSubject; + } + + public TestResults getTestResults() { + return this.testResults; + } + + @Override + public World getWorld() { + return this.world; + } + + @Override + public String getName() { + return "test context"; + } + } + +} diff --git a/src/main/java/com/github/skriptdev/skript/api/utils/Utils.java b/src/main/java/com/github/skriptdev/skript/api/utils/Utils.java index bbbd17f5..0963c6bc 100644 --- a/src/main/java/com/github/skriptdev/skript/api/utils/Utils.java +++ b/src/main/java/com/github/skriptdev/skript/api/utils/Utils.java @@ -79,6 +79,11 @@ public static void log(String message, Object... args) { log(null, Level.INFO, message, args); } + public static void logColored(String message, Object... args) { + String ansi = MessageUtil.toAnsiString(TinyMsg.parse(message)).toAnsi(); + log(null, Level.INFO, ansi, args); + } + public static void log(IMessageReceiver receiver, String message, Object... args) { log(receiver, Level.INFO, message, args); } diff --git a/src/main/java/com/github/skriptdev/skript/plugin/Skript.java b/src/main/java/com/github/skriptdev/skript/plugin/Skript.java index e15bad3e..0cce07af 100644 --- a/src/main/java/com/github/skriptdev/skript/plugin/Skript.java +++ b/src/main/java/com/github/skriptdev/skript/plugin/Skript.java @@ -6,6 +6,8 @@ import com.github.skriptdev.skript.api.skript.command.ArgUtils; import com.github.skriptdev.skript.api.skript.config.SkriptConfig; import com.github.skriptdev.skript.api.skript.registration.SkriptRegistration; +import com.github.skriptdev.skript.api.skript.testing.TestProperties; +import com.github.skriptdev.skript.api.skript.testing.TestRunner; import com.github.skriptdev.skript.api.skript.variables.JsonVariableStorage; import com.github.skriptdev.skript.api.utils.ReflectionUtils; import com.github.skriptdev.skript.api.utils.Utils; @@ -106,6 +108,12 @@ private void setupSkript() { Utils.debug("Hytale finished booting, starting post-load triggers..."); // Start any post-load triggers after Hytale finishes booting. getAddons().forEach(SkriptAddon::finishedLoading); + + // RUN TESTS + if (TestProperties.ENABLED) { + TestRunner testRunner = new TestRunner(); + testRunner.start(); + } }); } @@ -189,7 +197,7 @@ private void loadVariables() { * * @return The Skript registration. */ - public @NotNull io.github.syst3ms.skriptparser.registration.SkriptRegistration getSkriptRegistration() { + public @NotNull SkriptRegistration getSkriptRegistration() { return this.registration; } diff --git a/src/main/java/com/github/skriptdev/skript/plugin/elements/ElementRegistration.java b/src/main/java/com/github/skriptdev/skript/plugin/elements/ElementRegistration.java index 7954536a..8fb341be 100644 --- a/src/main/java/com/github/skriptdev/skript/plugin/elements/ElementRegistration.java +++ b/src/main/java/com/github/skriptdev/skript/plugin/elements/ElementRegistration.java @@ -1,6 +1,8 @@ package com.github.skriptdev.skript.plugin.elements; import com.github.skriptdev.skript.api.skript.registration.SkriptRegistration; +import com.github.skriptdev.skript.api.skript.testing.TestProperties; +import com.github.skriptdev.skript.api.skript.testing.elements.ElementHandler; import com.github.skriptdev.skript.api.utils.Utils; import com.github.skriptdev.skript.plugin.elements.command.ScriptCommand; import com.github.skriptdev.skript.plugin.elements.command.ScriptSubCommand; @@ -61,6 +63,11 @@ public void registerElements() { ScriptCommand.register(this.registration); ScriptSubCommand.register(this.registration); + // TEST ELEMENTS + if (TestProperties.ENABLED) { + ElementHandler.register(this.registration); + } + // FINALIZE SETUP this.registration.register(); diff --git a/src/test/README.md b/src/test/README.md new file mode 100644 index 00000000..cc64be41 --- /dev/null +++ b/src/test/README.md @@ -0,0 +1,64 @@ +# TESTS +> [!IMPORTANT] +> This page is a work in progress. + +## Running: + +To run tests, there are a few steps: + +1) In the build.gradle.kts file, you will need to change this line: + `val assetLocation = "/Users/ShaneBee/Desktop/Server/Hytale/Assets/Assets.zip"` + This will need to be changed to the location of your Assets.zip file on your computer. +2) After that you can run tests with `./gradlew testRunner` + The server will start up, the tests will run and you will see the outcome. + +## Making Tests: + +### Location of Tests: + +Tests are in this package under `skript/tests`. +Please make sure to add your test in a matching package to their respective Java packages. + +### Naming Tests: + +The file should match the name of the class you are testing for. +Ex: `ExprSomeThing.java` -> `ExprSomeThing.sk` + +In the test file, the test name should match the class/file name. +Ex: + +```applescript +test "ExprSomeThing": + some test of some sort +``` + +### Available Syntaxes: + +#### Test Event: + +The test event is the top level structure used for your tests to run. +See above for naming conventions. +You can use commands/functions in your tests, but other events will not be called. + +#### Test Event Context Values: + +- `event-world` = The world the tests happen in (which is "default") + +#### Assert Effect: + +The assert effect is used to test conditions and make sure the test can pass. + +Pattern: `assert %=boolean% with %string%` +Example: `assert 1 = 1 with "1 should equal 1"` + +If the test fails, your message and the condition will be sent to the TestResults class, which the TestRunner will use +to decide the outcome of all tests. + +#### Others: + +For the most part, all other syntaxes will be available. +Some notes: + +- Do not use async effects/sections (They'll most likely cause issues) +- Use wait/delay effect with extreme caution + - Delays must be less than a second, or the server is going to stop before your test finishes diff --git a/src/test/skript/tests/elements/hyskript/expressions/block/ExprBlockAt.sk b/src/test/skript/tests/elements/hyskript/expressions/block/ExprBlockAt.sk new file mode 100644 index 00000000..a6258936 --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/expressions/block/ExprBlockAt.sk @@ -0,0 +1,13 @@ +test "ExprBlockAt": + set {_loc} to location(0, 200, 0, event-world) + assert block at {_loc} is set with "The block should be set" + assert blocktype of block at {_loc} == Empty with "High chance a block that high is empty/air, but found: %block type of block at {_loc}%" + + set blocktype of block at {_loc} to rock_stone + assert blocktype of block at {_loc} == rock_stone with "Blocktype should be rock_stone" + + # Let's set it back in case we re-run a test on the same server + set blocktype of block at {_loc} to empty + + set {_loc} to location(0, 0, 0, event-world) + assert blocktype of block at {_loc} == rock_bedrock with "It should be bedrock down there" diff --git a/src/test/skript/tests/elements/hyskript/expressions/entity/ExprNPCType.sk b/src/test/skript/tests/elements/hyskript/expressions/entity/ExprNPCType.sk new file mode 100644 index 00000000..5ecdeb9b --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/expressions/entity/ExprNPCType.sk @@ -0,0 +1,6 @@ +test "ExprNPCType": + set {_loc} to world spawn of event-world + spawn a sheep at {_loc}: + set {_e} to event-spawned-npc + + assert npc type of {_e} == Sheep with "Should be a sheep, but found %npc type of {_e}%" diff --git a/src/test/skript/tests/elements/hyskript/expressions/other/ExprCast.sk b/src/test/skript/tests/elements/hyskript/expressions/other/ExprCast.sk new file mode 100644 index 00000000..769f1a2d --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/expressions/other/ExprCast.sk @@ -0,0 +1,7 @@ +test "ExprCast": + set {_a} to ingredient_poop as BlockType + assert class info of {_a} is "BlockType" with "The type should be BlockType" + + set {_a} to 1 as float + assert {_a} is 1.0 with "The cast should work" + assert class info of {_a} is "Float" with "The type should be Float" diff --git a/src/test/skript/tests/elements/hyskript/expressions/other/ExprClassInfoOf.sk b/src/test/skript/tests/elements/hyskript/expressions/other/ExprClassInfoOf.sk new file mode 100644 index 00000000..98801802 --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/expressions/other/ExprClassInfoOf.sk @@ -0,0 +1,12 @@ +test "ExprClassInfoOf": + set {_i} to itemstack of ingredient_poop + assert classinfo of {_i} = "Item Stack" with "Should be an itemstack but found %classinfo of {_i}%" + + set {_loc} to location(0, 0, 0, event-world) + assert classinfo of {_loc} = "Location" with "Should be a Location, but found %classinfo of {_loc}%" + + set {_b} to block at {_loc} + assert classinfo of {_b} = "Block" with "Should be a block, but found %classinfo of {_b}%" + + set {_bt} to blocktype of {_b} + assert classinfo of {_bt} = "BlockType" with "Should be a BlockType but found %classinfo of {_bt}%" diff --git a/src/test/skript/tests/elements/hyskript/expressions/other/ExprDistance.sk b/src/test/skript/tests/elements/hyskript/expressions/other/ExprDistance.sk new file mode 100644 index 00000000..ae270e3b --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/expressions/other/ExprDistance.sk @@ -0,0 +1,4 @@ +test "ExprDistance": + set {_a} to location(0, 0, 0, event-world) + set {_b} to location(0, 100, 0, event-world) + assert distance between {_a} and {_b} is 100 with "The locations are 100 blocks apart" diff --git a/src/test/skript/tests/elements/hyskript/expressions/other/ExprLocationDirection.sk b/src/test/skript/tests/elements/hyskript/expressions/other/ExprLocationDirection.sk new file mode 100644 index 00000000..3542a6d6 --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/expressions/other/ExprLocationDirection.sk @@ -0,0 +1,5 @@ +test "ExprLocationDirection": + set {_l1} to location(0, 0, 0, event-world) + set {_l2} to location 10 blocks north of {_l1} + set {_distance} to distance between {_l1} and {_l2} + assert {_distance} is 10 with "The new location should be 10 blocks away, but found %{_distance}%" diff --git a/src/test/skript/tests/elements/hyskript/expressions/other/ExprLocationOf.sk b/src/test/skript/tests/elements/hyskript/expressions/other/ExprLocationOf.sk new file mode 100644 index 00000000..eb649bfa --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/expressions/other/ExprLocationOf.sk @@ -0,0 +1,4 @@ +test "ExprLocationOf": + set {_loc} to location(0, 0, 0, event-world) + set {_b} to block at {_loc} + assert location of {_b} = {_loc} with "The location of the block should be the same as the original location" diff --git a/src/test/skript/tests/elements/hyskript/expressions/world/ExprWorld.sk b/src/test/skript/tests/elements/hyskript/expressions/world/ExprWorld.sk new file mode 100644 index 00000000..6ff36373 --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/expressions/world/ExprWorld.sk @@ -0,0 +1,3 @@ +test "ExprWorld": + set {_w} to world named "default" + assert {_w} = event-world with "The default world should match our event-world" diff --git a/src/test/skript/tests/elements/hyskript/expressions/world/ExprWorldSpawn.sk b/src/test/skript/tests/elements/hyskript/expressions/world/ExprWorldSpawn.sk new file mode 100644 index 00000000..e4aecd7c --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/expressions/world/ExprWorldSpawn.sk @@ -0,0 +1,8 @@ +test "ExprWorldSpawn": + set {_a} to world spawn of event-world + assert {_a} is set with "The variable should be set to spawn" + + set {_w} to world("default") + assert {_w} = event-world with "The world should match" + set {_b} to world spawn of {_w} + assert {_b} = {_a} with "The spawn should be the same" diff --git a/src/test/skript/tests/elements/hyskript/sections/SecDropItem.sk b/src/test/skript/tests/elements/hyskript/sections/SecDropItem.sk new file mode 100644 index 00000000..efdb103d --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/sections/SecDropItem.sk @@ -0,0 +1,6 @@ +test "SecDropItem": + set {_l} to world spawn of event-world + drop ingredient_stick at {_l}: + set {_i} to event-item-component + + assert {_i} is set with "The item should have dropped" diff --git a/src/test/skript/tests/elements/hyskript/sections/SecSpawnNPC.sk b/src/test/skript/tests/elements/hyskript/sections/SecSpawnNPC.sk new file mode 100644 index 00000000..3e94869e --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/sections/SecSpawnNPC.sk @@ -0,0 +1,7 @@ +test "SecSpawnNPC": + set {_l} to world spawn location of event-world + spawn a sheep at {_l}: + set {_e} to event-spawned-npc + + assert {_e} is set with "The sheep should have spawned" + assert "%{_e}%" = "NPCEntity{role=Sheep}" with "It should be a sheep" diff --git a/src/test/skript/tests/elements/hyskript/structures/ScriptCommand.sk b/src/test/skript/tests/elements/hyskript/structures/ScriptCommand.sk new file mode 100644 index 00000000..29726d7f --- /dev/null +++ b/src/test/skript/tests/elements/hyskript/structures/ScriptCommand.sk @@ -0,0 +1,8 @@ +command /letest: + trigger: + set {-var::in_command} to true + +test "ScriptCommand": + execute console command "/letest" + wait 1 tick # The command is async so we have to wait + assert {-var::in_command} is true with "The command should run and set the var" diff --git a/src/test/skript/tests/elements/skript-parser/conditions/CondExprCompare.sk b/src/test/skript/tests/elements/skript-parser/conditions/CondExprCompare.sk new file mode 100644 index 00000000..06522661 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/conditions/CondExprCompare.sk @@ -0,0 +1,10 @@ +test "skript-parser-CondCompare": + assert 1 = 1 with "1 should equal 1" + assert 1 != 2 with "1 should not equal 2" + + assert 1 < 2 with "1 should be less than 2" + assert 2 > 1 with "2 should be greater than 1" + + set {_a} to ingredient_poop + set {_b} to ingredient_stick + assert {_a} != {_b} with "Poop and a stick should not be equal" diff --git a/src/test/skript/tests/elements/skript-parser/conditions/CondExprContains.sk b/src/test/skript/tests/elements/skript-parser/conditions/CondExprContains.sk new file mode 100644 index 00000000..4bfa5b86 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/conditions/CondExprContains.sk @@ -0,0 +1,7 @@ +test "skript-parser-CondExprContains": + assert {_blah::*} does not contain "blah" with "The variable shouldn't contain the string" + add "blah" to {_blah::*} + assert {_blah::*} contains "blah" with "The variable should contain the string" + + assert "blah blah blah" does not contain "cheese" with "The string shouldn't contain the substring" + assert "I love cheese" contains "cheese" with "The string should contain the substring" diff --git a/src/test/skript/tests/elements/skript-parser/conditions/CondExprIsDivisible.sk b/src/test/skript/tests/elements/skript-parser/conditions/CondExprIsDivisible.sk new file mode 100644 index 00000000..974cca24 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/conditions/CondExprIsDivisible.sk @@ -0,0 +1,3 @@ +test "skript-parser-CondExprIsDivisible": + #assert 10 is not divisible by 7 with "10 should not be divisible by 7" TODO bug, CondExprIsDivisible isnt doing a neg check + assert 10 is divisible by 2 with "10 should be divisible by 2" diff --git a/src/test/skript/tests/elements/skript-parser/conditions/CondExprIsEmpty.sk b/src/test/skript/tests/elements/skript-parser/conditions/CondExprIsEmpty.sk new file mode 100644 index 00000000..8c7a77ce --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/conditions/CondExprIsEmpty.sk @@ -0,0 +1,4 @@ +test "skript-parser-CondExprIsEmpty": + #assert {_a::*} is empty with "The unset variable should be empty" TODO this fails, tries to compare to "empty" (Item/BlockType probably) + set {_a::*} to 1, 2 and 3 + assert {_a::*} is not empty with "The variable should not be empty after setting it" diff --git a/src/test/skript/tests/elements/skript-parser/conditions/CondExprIsSet.sk b/src/test/skript/tests/elements/skript-parser/conditions/CondExprIsSet.sk new file mode 100644 index 00000000..386d8ff7 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/conditions/CondExprIsSet.sk @@ -0,0 +1,6 @@ +test "skript-parser-CondExprIsSet": + assert {_a} is not set with "The variable should not be set yet" + set {_a} to "blah" + assert {_a} is set with "The variable should now be set" + delete {_a} + assert {_a} is not set with "The variable should not be set after deleting" diff --git a/src/test/skript/tests/elements/skript-parser/conditions/CondExprStartsEnds.sk b/src/test/skript/tests/elements/skript-parser/conditions/CondExprStartsEnds.sk new file mode 100644 index 00000000..9a65bdc2 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/conditions/CondExprStartsEnds.sk @@ -0,0 +1,4 @@ +test "skript-parser-CondExprStartsEnds": + # TODO neither of these seem to work + #assert ("blah" does not start with "cheese") with "Blah shouldn't start with cheese" + #assert ("cheese is the best" starts with "cheese") with "Cheese is the best should start with cheese" diff --git a/src/test/skript/tests/elements/skript-parser/effects/EffChange.sk b/src/test/skript/tests/elements/skript-parser/effects/EffChange.sk new file mode 100644 index 00000000..d1f950b9 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/effects/EffChange.sk @@ -0,0 +1,13 @@ +test "skript-parser-EffChange": + set {_a} to "ayy" + assert {_a} is "ayy" with "The var should be set" + + set {_b} to 0 + add 10 to {_b} + assert {_b} is 10 with "The var should be 10 after adding" + + remove 5 from {_b} + assert {_b} is 5 with "The var should be 5 after removing" + + delete {_b} + assert {_b} is not set with "The var should be deleted" diff --git a/src/test/skript/tests/elements/skript-parser/effects/EffDoIf.sk b/src/test/skript/tests/elements/skript-parser/effects/EffDoIf.sk new file mode 100644 index 00000000..db60cb57 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/effects/EffDoIf.sk @@ -0,0 +1,6 @@ +test "skript-parser-EffDoIf": + do set {_a} to 1 if 1 = 10 + assert {_a} is not set with "The do should have been skipped" + + do set {_a} to 1 if 1 = 1 + assert {_a} is 1 with "The do should have been executed" diff --git a/src/test/skript/tests/elements/skript-parser/effects/EffExit.sk b/src/test/skript/tests/elements/skript-parser/effects/EffExit.sk new file mode 100644 index 00000000..2ee7c5cb --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/effects/EffExit.sk @@ -0,0 +1,7 @@ +test "skript-parser-EffExit": + assert {_a} is not set with "The variable should not be set yet" + loop 10 times: + set {_a} to loop-value + exit loop + + assert {_a} = 1 with "The loop should have stopped after 1 iteration" diff --git a/src/test/skript/tests/elements/skript-parser/effects/EffReturn.sk b/src/test/skript/tests/elements/skript-parser/effects/EffReturn.sk new file mode 100644 index 00000000..67158a74 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/effects/EffReturn.sk @@ -0,0 +1,6 @@ +function testFunc(s: string) :: string: + return "this is a test %{_s}%" + +test "skript-parser-EffReturn": + set {_a} to testFunc("blah") + assert {_a} is "this is a test blah" with "The function should return the value, but found: %{_a}%" diff --git a/src/test/skript/tests/elements/skript-parser/expressions/ExprAmount.sk b/src/test/skript/tests/elements/skript-parser/expressions/ExprAmount.sk new file mode 100644 index 00000000..70e33646 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/expressions/ExprAmount.sk @@ -0,0 +1,4 @@ +test "skript-parser-ExprAmount": + assert size of {_a::*} = 0 with "The unset variable should be empty" + set {_a::*} to 1, 2 and 3 + assert size of {_a::*} = 3 with "The variable should have three elements" diff --git a/src/test/skript/tests/elements/skript-parser/expressions/ExprArithmeticOperators.sk b/src/test/skript/tests/elements/skript-parser/expressions/ExprArithmeticOperators.sk new file mode 100644 index 00000000..85120fe5 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/expressions/ExprArithmeticOperators.sk @@ -0,0 +1,3 @@ +test "skript-parser-ExprArithmeticOperators": + assert 1 + 2 == 3 with "1 + 2 should equal 3" + assert 1 + 2 != 5 with "1 + 2 should not equal 5" diff --git a/src/test/skript/tests/elements/skript-parser/expressions/ExprBooleanOperators.sk b/src/test/skript/tests/elements/skript-parser/expressions/ExprBooleanOperators.sk new file mode 100644 index 00000000..b90c24b4 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/expressions/ExprBooleanOperators.sk @@ -0,0 +1,3 @@ +test "skript-parser-ExprBooleanOperators": + assert ((1 + 1 == 2) && (2 + 2 == 4)) with "Both booleans should match" + assert ((1 + 1 == 5) || (2 + 2 == 4)) with "One of the booleans (the second) should match" diff --git a/src/test/skript/tests/elements/skript-parser/expressions/ExprElement.sk b/src/test/skript/tests/elements/skript-parser/expressions/ExprElement.sk new file mode 100644 index 00000000..7b999af5 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/expressions/ExprElement.sk @@ -0,0 +1,4 @@ +test "skript-parser-ExprElement": + set {_a::*} to "one", "two" and "three" + set {_b} to 2nd element of {_a::*} + assert {_b} is "two" with "The second element should have been 'two'" diff --git a/src/test/skript/tests/elements/skript-parser/expressions/ExprLength.sk b/src/test/skript/tests/elements/skript-parser/expressions/ExprLength.sk new file mode 100644 index 00000000..025560e2 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/expressions/ExprLength.sk @@ -0,0 +1,2 @@ +test "skript-parser-ExprLength": + assert length of "hi bob" = 6 with "There should be 6 characters in the string." diff --git a/src/test/skript/tests/elements/skript-parser/expressions/ExprMutableList.sk b/src/test/skript/tests/elements/skript-parser/expressions/ExprMutableList.sk new file mode 100644 index 00000000..82f27b35 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/expressions/ExprMutableList.sk @@ -0,0 +1,7 @@ +test "skript-parser-ExprMutableList": + set {_a::*} to "nine", "three", "four" and "ten" + set {_a::*} to sorted {_a::*} + assert 1st element of {_a::*} = "four" with "Four should have been the first after sorting" + + set {_a::*} to reversed {_a::*} + assert 1st element of {_a::*} = "three" with "Three should have been the first after reversing" diff --git a/src/test/skript/tests/elements/skript-parser/expressions/ExprParseAs.sk b/src/test/skript/tests/elements/skript-parser/expressions/ExprParseAs.sk new file mode 100644 index 00000000..ea2f94ef --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/expressions/ExprParseAs.sk @@ -0,0 +1,5 @@ +test "skript-parser-ExprParseAs": + set {_b} to "1" parsed as Number + + # You broken too? TODO fix + # assert {_b} = 1 with "The string should have parsed as the number 1, but found %{_b}%//%class info of {_b}%" diff --git a/src/test/skript/tests/elements/skript-parser/expressions/ExprTernary.sk b/src/test/skript/tests/elements/skript-parser/expressions/ExprTernary.sk new file mode 100644 index 00000000..bc3a8769 --- /dev/null +++ b/src/test/skript/tests/elements/skript-parser/expressions/ExprTernary.sk @@ -0,0 +1,7 @@ +test "skript-parser-ExprTernary": + set {_a} to "test" + set {_b} to {_a} is set ? {_a} : "something else" + assert {_b} = "test" with "It should have picked up {_a}" + + set {_b} to {_c} is set ? {_c} : "something else" + assert {_b} = "something else" with "It should not have picked up {_c}" diff --git a/src/test/skript/tests/test-script.sk b/src/test/skript/tests/test-script.sk new file mode 100644 index 00000000..363fe81d --- /dev/null +++ b/src/test/skript/tests/test-script.sk @@ -0,0 +1,7 @@ +test "Basic HySkript Test": + set {_spawn} to world spawn of world("default") + assert {_spawn} is set with "The main spawn point of the world should be set" + + spawn a sheep at {_spawn}: + set {_sheep} to event-spawned-entity + assert {_sheep} is set with "The sheep entity should be set"