Skip to content

Commit

Permalink
Add async JUnit tests, on broadcast event and hover list test (#7053)
Browse files Browse the repository at this point in the history
* init commit

* add broadcasting

* add broadcasting tests

* add test for async test

* make broadcast get called async

* update events

* update ExprMessage

* fixed regular tests

* fixed remove in ExprHoverList

* fixed burning tests not completing

* use pattern matching

* remove double if using junit check, add starting values

* revert ExprHoverList changes

* revert ExprHoverList changes

* fix tests

* Update src/main/java/ch/njol/skript/Skript.java

Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com>

* Update src/main/java/ch/njol/skript/Skript.java

Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com>

* update annot

* oops

---------

Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com>
Co-authored-by: Moderocky <admin@moderocky.com>
  • Loading branch information
3 people authored Oct 13, 2024
1 parent e926b38 commit b36ca00
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 108 deletions.
176 changes: 107 additions & 69 deletions src/main/java/ch/njol/skript/Skript.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,7 @@
import ch.njol.skript.registrations.Classes;
import ch.njol.skript.registrations.EventValues;
import ch.njol.skript.registrations.Feature;
import ch.njol.skript.test.runner.EffObjectives;
import ch.njol.skript.test.runner.SkriptJUnitTest;
import ch.njol.skript.test.runner.SkriptTestEvent;
import ch.njol.skript.test.runner.TestMode;
import ch.njol.skript.test.runner.TestTracker;
import ch.njol.skript.test.runner.*;
import ch.njol.skript.timings.SkriptTimings;
import ch.njol.skript.update.ReleaseManifest;
import ch.njol.skript.update.ReleaseStatus;
Expand Down Expand Up @@ -96,6 +92,7 @@
import org.junit.After;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.skriptlang.skript.bukkit.SkriptMetrics;
import org.skriptlang.skript.bukkit.displays.DisplayModule;
import org.skriptlang.skript.lang.comparator.Comparator;
Expand Down Expand Up @@ -132,6 +129,8 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Filter;
Expand Down Expand Up @@ -672,7 +671,10 @@ protected void afterErrors() {
// Ignore late init (scripts, etc.) in test mode
Bukkit.getScheduler().runTaskLater(Skript.this, () -> {
// Delay is in Minecraft ticks.
long shutdownDelay = 0;
AtomicLong shutdownDelay = new AtomicLong(0);
List<Class<?>> asyncTests = new ArrayList<>();
CompletableFuture<Void> onAsyncComplete = CompletableFuture.completedFuture(null);

if (TestMode.GEN_DOCS) {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "skript gen-docs");
} else if (TestMode.DEV_MODE) { // Developer controlled environment.
Expand Down Expand Up @@ -702,91 +704,80 @@ protected void afterErrors() {
TestTracker.testFailed("exception was thrown during execution");
}
if (TestMode.JUNIT) {
info("Running all JUnit tests...");
long milliseconds = 0, tests = 0, fails = 0, ignored = 0, size = 0;
AtomicLong milliseconds = new AtomicLong(0),
tests = new AtomicLong(0), fails = new AtomicLong(0),
ignored = new AtomicLong(0), size = new AtomicLong(0);

info("Running sync JUnit tests...");
try {
List<Class<?>> classes = Lists.newArrayList(Utils.getClasses(Skript.getInstance(), "org.skriptlang.skript.test", "tests"));
// Don't attempt to run inner/anonymous classes as tests
classes.removeIf(Class::isAnonymousClass);
classes.removeIf(Class::isLocalClass);
// Test that requires package access. This is only present when compiling with src/test.
classes.add(Class.forName("ch.njol.skript.variables.FlatFileStorageTest"));
size = classes.size();
size.set(classes.size());
for (Class<?> clazz : classes) {
// Reset class SkriptJUnitTest which stores test requirements.
String test = clazz.getName();
SkriptJUnitTest.setCurrentJUnitTest(test);
SkriptJUnitTest.setShutdownDelay(0);

info("Running JUnit test '" + test + "'");
Result junit = JUnitCore.runClasses(clazz);
TestTracker.testStarted("JUnit: '" + test + "'");

/**
* Usage of @After is pointless if the JUnit class requires delay. As the @After will happen instantly.
* The JUnit must override the 'cleanup' method to avoid Skript automatically cleaning up the test data.
*/
boolean overrides = false;
for (Method method : clazz.getDeclaredMethods()) {
if (!method.isAnnotationPresent(After.class))
continue;
if (SkriptJUnitTest.getShutdownDelay() > 1)
warning("Using @After in JUnit classes, happens instantaneously, and JUnit class '" + test + "' requires a delay. Do your test cleanup in the script junit file or 'cleanup' method.");
if (method.getName().equals("cleanup"))
overrides = true;
if (SkriptAsyncJUnitTest.class.isAssignableFrom(clazz)) {
asyncTests.add(clazz); // do these later, all together
continue;
}
if (SkriptJUnitTest.getShutdownDelay() > 1 && !overrides)
error("The JUnit class '" + test + "' does not override the method 'cleanup' thus the test data will instantly be cleaned up. " +
"This JUnit test requires longer shutdown time: " + SkriptJUnitTest.getShutdownDelay());

// Collect all data from the current JUnit test.
shutdownDelay = Math.max(shutdownDelay, SkriptJUnitTest.getShutdownDelay());
tests += junit.getRunCount();
milliseconds += junit.getRunTime();
ignored += junit.getIgnoreCount();
fails += junit.getFailureCount();

// If JUnit failures are present, add them to the TestTracker.
junit.getFailures().forEach(failure -> {
String message = failure.getMessage() == null ? "" : " " + failure.getMessage();
TestTracker.JUnitTestFailed(test, message);
Skript.exception(failure.getException(), "JUnit test '" + failure.getTestHeader() + " failed.");
});
if (SkriptJUnitTest.class.isAssignableFrom(clazz))
((SkriptJUnitTest) clazz.getConstructor().newInstance()).cleanup();
SkriptJUnitTest.clearJUnitTest();

runTest(clazz, shutdownDelay, tests, milliseconds, ignored, fails);
}
} catch (IOException e) {
Skript.exception(e, "Failed to execute JUnit runtime tests.");
} catch (ClassNotFoundException e) {
// Should be the Skript test jar gradle task.
assert false : "Class 'ch.njol.skript.variables.FlatFileStorageTest' was not found.";
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException |
InvocationTargetException | NoSuchMethodException | SecurityException e) {
Skript.exception(e, "Failed to initalize test JUnit classes.");
}
if (ignored > 0)
if (ignored.get() > 0)
Skript.warning("There were " + ignored + " ignored test cases! This can mean they are not properly setup in order in that class!");

info("Completed " + tests + " JUnit tests in " + size + " classes with " + fails + " failures in " + milliseconds + " milliseconds.");
onAsyncComplete = CompletableFuture.runAsync(() -> {
info("Running async JUnit tests...");
try {
for (Class<?> clazz : asyncTests) {
runTest(clazz, shutdownDelay, tests, milliseconds, ignored, fails);
}
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException |
InvocationTargetException | NoSuchMethodException | SecurityException e) {
Skript.exception(e, "Failed to initalize test JUnit classes.");
}
if (ignored.get() > 0)
Skript.warning("There were " + ignored + " ignored test cases! " +
"This can mean they are not properly setup in order in that class!");

info("Completed " + tests + " JUnit tests in " + size + " classes with " + fails +
" failures in " + milliseconds + " milliseconds.");
});
}
}
double display = shutdownDelay / 20;
info("Testing done, shutting down the server in " + display + " second" + (display <= 1D ? "" : "s") + "...");
// Delay server shutdown to stop the server from crashing because the current tick takes a long time due to all the tests
Bukkit.getScheduler().runTaskLater(Skript.this, () -> {
if (TestMode.JUNIT && !EffObjectives.isJUnitComplete())
EffObjectives.fail();

info("Collecting results to " + TestMode.RESULTS_FILE);
String results = new Gson().toJson(TestTracker.collectResults());
try {
Files.write(TestMode.RESULTS_FILE, results.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
Skript.exception(e, "Failed to write test results.");
}

Bukkit.getServer().shutdown();
}, shutdownDelay);
onAsyncComplete.thenRun(() -> {
double display = shutdownDelay.get() / 20.0;
info("Testing done, shutting down the server in " + display + " second" + (display == 1 ? "" : "s") + "...");

// Delay server shutdown to stop the server from crashing because the current tick takes a long time due to all the tests
Bukkit.getScheduler().runTaskLater(Skript.this, () -> {
info("Shutting down server.");
if (TestMode.JUNIT && !EffObjectives.isJUnitComplete())
EffObjectives.fail();

info("Collecting results to " + TestMode.RESULTS_FILE);
String results = new Gson().toJson(TestTracker.collectResults());
try {
Files.write(TestMode.RESULTS_FILE, results.getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
Skript.exception(e, "Failed to write test results.");
}

Bukkit.getServer().shutdown();
}, shutdownDelay.get());
});
}, 100);
}

Expand Down Expand Up @@ -897,6 +888,53 @@ public void onServerReload(ServerLoadEvent event) {
SkriptTimings.setSkript(this);
}

private void runTest(Class<?> clazz, AtomicLong shutdownDelay, AtomicLong tests,
AtomicLong milliseconds, AtomicLong ignored, AtomicLong fails)
throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
String test = clazz.getName();
SkriptJUnitTest.setCurrentJUnitTest(test);
SkriptJUnitTest.setShutdownDelay(0);

info("Running JUnit test '" + test + "'");
Result result = JUnitCore.runClasses(clazz);
TestTracker.testStarted("JUnit: '" + test + "'");

/*
* Usage of @After is pointless if the JUnit class requires delay. As the @After will happen instantly.
* The JUnit must override the 'cleanup' method to avoid Skript automatically cleaning up the test data.
*/
boolean overrides = false;
for (Method method : clazz.getDeclaredMethods()) {
if (!method.isAnnotationPresent(After.class))
continue;
if (SkriptJUnitTest.getShutdownDelay() > 1)
warning("Methods annotated with @After in happen instantaneously, and '" + test + "' requires a delay. Do test cleanup in the junit script file or 'cleanup' method.");
if (method.getName().equals("cleanup"))
overrides = true;
}
if (SkriptJUnitTest.getShutdownDelay() > 1 && !overrides)
error("The JUnit class '" + test + "' does not override the method 'cleanup', thus the test data will instantly be cleaned up " +
"despite requiring a longer shutdown time: " + SkriptJUnitTest.getShutdownDelay());

shutdownDelay.set(Math.max(shutdownDelay.get(), SkriptJUnitTest.getShutdownDelay()));
tests.getAndAdd(result.getRunCount());
milliseconds.getAndAdd(result.getRunTime());
ignored.getAndAdd(result.getIgnoreCount());
fails.getAndAdd(result.getFailureCount());

// If JUnit failures are present, add them to the TestTracker.
for (Failure failure : result.getFailures()) {
String message = failure.getMessage() == null ? "" : " " + failure.getMessage();
TestTracker.JUnitTestFailed(test, message);
Skript.exception(failure.getException(), "JUnit test '" + failure.getTestHeader() + " failed.");
}

if (SkriptJUnitTest.class.isAssignableFrom(clazz) &&
!SkriptAsyncJUnitTest.class.isAssignableFrom(clazz)) // can't access blocks, entities async
((SkriptJUnitTest) clazz.getConstructor().newInstance()).cleanup();
SkriptJUnitTest.clearJUnitTest();
}

/**
* Handles -Dskript.stuff command line arguments.
*/
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/ch/njol/skript/events/SimpleEvents.java
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
import org.bukkit.event.player.PlayerToggleFlightEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.bukkit.event.player.PlayerToggleSprintEvent;
import org.bukkit.event.server.BroadcastMessageEvent;
import org.bukkit.event.server.ServerListPingEvent;
import org.bukkit.event.vehicle.VehicleCreateEvent;
import org.bukkit.event.vehicle.VehicleDamageEvent;
Expand Down Expand Up @@ -820,5 +821,13 @@ public class SimpleEvents {
.since("2.9.0")
.requiredPlugins("Paper");
}

Skript.registerEvent("Broadcast", SimpleEvent.class, BroadcastMessageEvent.class, "broadcast")
.description("Called when a message is broadcasted.")
.examples(
"on broadcast:",
"\tset broadcast-message to \"&c[BROADCAST] %broadcasted message%\""
)
.since("INSERT VERSION");
}
}
75 changes: 36 additions & 39 deletions src/main/java/ch/njol/skript/expressions/ExprMessage.java
Original file line number Diff line number Diff line change
@@ -1,53 +1,32 @@
/**
* This file is part of Skript.
*
* Skript is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Skript is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Skript. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright Peter Güttinger, SkriptLang team and contributors
*/
package ch.njol.skript.expressions;

import org.bukkit.event.Event;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerKickEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.jetbrains.annotations.Nullable;

import ch.njol.skript.Skript;
import ch.njol.skript.classes.Changer.ChangeMode;
import ch.njol.skript.doc.Description;
import ch.njol.skript.doc.Events;
import ch.njol.skript.doc.Examples;
import ch.njol.skript.doc.Name;
import ch.njol.skript.doc.Since;
import ch.njol.skript.doc.*;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.ExpressionType;
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.lang.util.SimpleExpression;
import ch.njol.skript.log.ErrorQuality;
import ch.njol.util.Kleenean;
import ch.njol.util.coll.CollectionUtils;
import org.bukkit.event.Event;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerKickEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.server.BroadcastMessageEvent;
import org.jetbrains.annotations.Nullable;

/**
* @author Peter Güttinger
*/
@SuppressWarnings("deprecation")
@Name("Message")
@Description("The (chat) message of a chat event, the join message of a join event, the quit message of a quit event, or the death message on a death event. This expression is mostly useful for being changed.")
@Description(
"The (chat) message of a chat event, the join message of a join event, the quit message of a quit event, " +
"the death message of a death event or the broadcasted message in a broadcast event. " +
"This expression is mostly useful for being changed."
)
@Examples({
"on chat:",
"\tplayer has permission \"admin\"",
Expand All @@ -67,9 +46,13 @@
"\t\tset quit message to \"%player% left this awesome server!\"",
"",
"on death:",
"\tset the death message to \"%player% died!\""})
@Since("1.4.6 (chat message), 1.4.9 (join & quit messages), 2.0 (death message), 2.9.0 (clear message)")
@Events({"chat", "join", "quit", "death"})
"\tset the death message to \"%player% died!\"",
"",
"on broadcast:",
"\tset broadcast message to \"&a[BROADCAST] %broadcast message%\""
})
@Since("1.4.6 (chat message), 1.4.9 (join & quit messages), 2.0 (death message), 2.9.0 (clear message), INSERT VERSION (broadcasted message)")
@Events({"chat", "join", "quit", "death", "broadcast"})
public class ExprMessage extends SimpleExpression<String> {

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -129,6 +112,20 @@ void set(final Event e, final String message) {
if (e instanceof PlayerDeathEvent)
((PlayerDeathEvent) e).setDeathMessage(message);
}
},
BROADCAST("broadcast", "broadcast(-|[ed] )message", BroadcastMessageEvent.class) {
@Override
@Nullable String get(Event event) {
if (event instanceof BroadcastMessageEvent broadcastMessageEvent)
return broadcastMessageEvent.getMessage();
return null;
}

@Override
void set(Event event, String message) {
if (event instanceof BroadcastMessageEvent broadcastMessageEvent)
broadcastMessageEvent.setMessage(message);
}
};

final String name;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ch.njol.skript.test.runner;

/**
* All methods in this class marked with @Test will be run on a different thread from the main one.
* All tests are still done sequentially.
*/
public abstract class SkriptAsyncJUnitTest extends SkriptJUnitTest {

}
Loading

0 comments on commit b36ca00

Please sign in to comment.