Test & error UX improvements (#2347)

This commit is contained in:
Matyrobbrt 2025-07-08 13:54:11 +03:00 committed by GitHub
parent 0b63a7c7a1
commit 865eb559dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
38 changed files with 251 additions and 245 deletions

View File

@ -26,6 +26,8 @@ jobs:
uses: neoforged/actions/setup-java@main
with:
java-version: 21
# Exclude minecraft sources from emitting annotation warnings since we cannot point to them anyway
warning-file-path: '(?!.+\/projects\/neoforge\/src\/)[^:]+'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

View File

@ -19,6 +19,7 @@ jobs:
uses: neoforged/actions/setup-java@main
with:
java-version: 21
problem-matcher: false
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

View File

@ -23,6 +23,7 @@ jobs:
uses: neoforged/actions/setup-java@main
with:
java-version: 21
problem-matcher: false
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

View File

@ -18,11 +18,12 @@
private static boolean isFaceOccludedByState(Direction p_110980_, float p_110981_, BlockState p_110983_) {
VoxelShape voxelshape = p_110983_.getFaceOcclusionShape(p_110980_.getOpposite());
if (voxelshape == Shapes.empty()) {
@@ -64,14 +_,20 @@
@@ -64,14 +_,21 @@
return isFaceOccludedByState(p_110963_.getOpposite(), 1.0F, p_110962_);
}
+ /** @deprecated Neo: use overload that accepts BlockState */
+ @Deprecated
public static boolean shouldRenderFace(FluidState p_203169_, BlockState p_203170_, Direction p_203171_, FluidState p_203172_) {
return !isFaceOccludedBySelf(p_203170_, p_203171_) && !isNeighborSameFluid(p_203169_, p_203172_);
}

View File

@ -1,10 +1,11 @@
--- a/net/minecraft/data/tags/EnchantmentTagsProvider.java
+++ b/net/minecraft/data/tags/EnchantmentTagsProvider.java
@@ -13,8 +_,12 @@
@@ -13,8 +_,13 @@
import net.minecraft.world.item.enchantment.Enchantment;
public abstract class EnchantmentTagsProvider extends KeyTagProvider<Enchantment> {
+ /** @deprecated Forge: Use the {@linkplain #EnchantmentTagsProvider(PackOutput, CompletableFuture, String) mod id variant} */
+ @Deprecated
public EnchantmentTagsProvider(PackOutput p_341044_, CompletableFuture<HolderLookup.Provider> p_341146_) {
super(p_341044_, Registries.ENCHANTMENT, p_341146_);
+ }

View File

@ -1,6 +1,6 @@
--- a/net/minecraft/data/tags/TagsProvider.java
+++ b/net/minecraft/data/tags/TagsProvider.java
@@ -32,26 +_,48 @@
@@ -32,26 +_,49 @@
private final CompletableFuture<TagsProvider.TagLookup<T>> parentProvider;
protected final ResourceKey<? extends Registry<T>> registryKey;
protected final Map<ResourceLocation, TagBuilder> builders = Maps.newLinkedHashMap();
@ -9,6 +9,7 @@
+ /**
+ * @deprecated Forge: Use the {@linkplain #TagsProvider(PackOutput, ResourceKey, CompletableFuture, String) mod id variant}
+ */
+ @Deprecated
protected TagsProvider(PackOutput p_256596_, ResourceKey<? extends Registry<T>> p_255886_, CompletableFuture<HolderLookup.Provider> p_256513_) {
- this(p_256596_, p_255886_, p_256513_, CompletableFuture.completedFuture(TagsProvider.TagLookup.empty()));
+ this(p_256596_, p_255886_, p_256513_, "vanilla");

View File

@ -34,3 +34,33 @@
return true;
}
@@ -216,8 +_,14 @@
GlobalTestReporter.finish();
LOGGER.info("========= {} GAME TESTS COMPLETE IN {} ======================", this.testTracker.getTotalCount(), this.stopwatch.stop());
if (this.testTracker.hasFailedRequired()) {
- LOGGER.info("{} required tests failed :(", this.testTracker.getFailedRequiredCount());
+ LOGGER.error("{} required tests failed :(", this.testTracker.getFailedRequiredCount());
this.testTracker.getFailedRequired().forEach(GameTestServer::logFailedTest);
+
+ // Neo: when running in GitHub actions emit actions-specific error annotations to make finding the error message easier
+ // See https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#example-creating-an-annotation-for-an-error
+ if (System.getenv().getOrDefault("CI", "false").equals("true") && System.getenv().getOrDefault("GITHUB_ACTIONS", "false").equals("true")) {
+ System.out.printf("\n::error title=GameTest Failure::%s required game tests failed: %s\n\n", testTracker.getFailedRequiredCount(), testTracker.getFailedRequired().stream().map(info -> info.id().toString()).collect(java.util.stream.Collectors.joining(", ")));
+ }
} else {
LOGGER.info("All {} required tests passed :)", this.testTracker.getTotalCount());
}
@@ -233,11 +_,11 @@
private static void logFailedTest(GameTestInfo p_401161_) {
if (p_401161_.getRotation() != Rotation.NONE) {
- LOGGER.info(
+ LOGGER.error(
" - {} with rotation {}: {}", p_401161_.id(), p_401161_.getRotation().getSerializedName(), p_401161_.getError().getDescription().getString()
);
} else {
- LOGGER.info(" - {}: {}", p_401161_.id(), p_401161_.getError().getDescription().getString());
+ LOGGER.error(" - {}: {}", p_401161_.id(), p_401161_.getError().getDescription().getString());
}
}

View File

@ -334,6 +334,7 @@ public class NeoForgeAdvancementProvider extends AdvancementProvider {
}
@Nullable
@SuppressWarnings("removal")
private Advancement.Builder findAndReplaceInHolder(AdvancementHolder advancementHolder, HolderLookup.Provider registries) {
Advancement advancement = advancementHolder.value();
Advancement.Builder builder = Advancement.Builder.advancement();

View File

@ -36,7 +36,9 @@ public interface Test extends Groupable {
/**
* A list of the groups of this test. <br>
* If this list is empty, the test will be only in the {@code ungrouped} group.
* If this list is empty, the test will be put in the {@code ungrouped} group.
* <p>
* Tests without a {@link #asGameTest() game test} will also be automatically put in the {@code manual} group.
*
* @return the groups of this test
*/
@ -197,10 +199,15 @@ public interface Test extends Groupable {
/**
* Represents the status of a test.
*
* @param result the result
* @param message the message, providing additional context if the test failed
* @param result the result
* @param message the message, providing additional context if the test failed
* @param exception the exception with which the test failed. Can be {@code null} if the test did not fail or if it failed without throwing an exception
*/
record Status(Result result, String message) {
record Status(Result result, String message, @Nullable Exception exception) {
public Status(Result result, String message) {
this(result, message, null);
}
public static final Status DEFAULT = new Status(Result.NOT_PROCESSED, "");
public static final Status PASSED = new Status(Result.PASSED, "");
@ -213,7 +220,11 @@ public interface Test extends Groupable {
}
public static Status failed(String message) {
return new Status(Result.FAILED, message);
return failed(message, null);
}
public static Status failed(String message, @Nullable Exception exception) {
return new Status(Result.FAILED, message, exception);
}
public MutableComponent asComponent() {
@ -230,7 +241,7 @@ public interface Test extends Groupable {
if (message.isBlank()) {
return "[result=" + result + "]";
} else {
return "[result=" + result + ",message=" + message + "]";
return "[result=" + result + ",message=" + message + ",exception=" + exception + "]";
}
}
}

View File

@ -42,6 +42,7 @@ import org.lwjgl.glfw.GLFW;
@ParametersAreNonnullByDefault
public abstract class AbstractTestScreen extends Screen {
protected final MutableTestFramework framework;
private final Screen outer = this;
public AbstractTestScreen(Component title, MutableTestFramework framework) {
super(title);
@ -186,7 +187,7 @@ public abstract class AbstractTestScreen extends Screen {
graphics.blitSprite(RenderPipelines.GUI_TEXTURED, icon, pLeft, pTop, 9, 9, renderTransparent ? (alpha | 0x00FFFFFF) : 0xFFFFFFFF);
final Component title = TestsOverlay.statusColoured(test.visuals().title(), status);
graphics.drawString(font, title, pLeft + 11, pTop, renderTransparent ? (alpha | 0xffffff0) : 0xffffff);
graphics.drawString(font, title, pLeft + 11, pTop, renderTransparent ? (alpha | 0x00FFFFFF) : 0xFFFFFFFF);
}
@Override
@ -266,9 +267,9 @@ public abstract class AbstractTestScreen extends Screen {
final List<Test> all = group.resolveAll();
final int enabledCount = (int) all.stream().filter(it -> framework.tests().isEnabled(it.id())).count();
if (enabledCount == all.size()) {
graphics.setTooltipForNextFrame(font, Component.literal("All tests in group are enabled!").withStyle(ChatFormatting.GREEN), mouseX, mouseY);
graphics.setTooltipForNextFrame(font, Component.literal("All tests in group (" + all.size() + ") are enabled!").withStyle(ChatFormatting.GREEN), mouseX, mouseY);
} else if (enabledCount == 0) {
graphics.setTooltipForNextFrame(font, Component.literal("All tests in group are disabled!").withStyle(ChatFormatting.GRAY), mouseX, mouseY);
graphics.setTooltipForNextFrame(font, Component.literal("All tests in group (" + all.size() + ") are disabled!").withStyle(ChatFormatting.GRAY), mouseX, mouseY);
} else {
graphics.setTooltipForNextFrame(font, Component.literal(enabledCount + "/" + all.size() + " tests enabled!").withStyle(ChatFormatting.BLUE), mouseX, mouseY);
}
@ -291,7 +292,7 @@ public abstract class AbstractTestScreen extends Screen {
}
private void openBrowseGUI() {
Minecraft.getInstance().pushGuiLayer(new TestScreen(
Minecraft.getInstance().setScreen(new TestScreen(
Component.literal("Tests of group ").append(getTitle()),
framework, List.of(group)) {
@Override
@ -302,7 +303,7 @@ public abstract class AbstractTestScreen extends Screen {
showAsGroup.setValue(false);
groupableList.resetRows("");
addRenderableWidget(Button.builder(CommonComponents.GUI_BACK, (p_97691_) -> this.onClose())
addRenderableWidget(Button.builder(CommonComponents.GUI_BACK, (p_97691_) -> minecraft.setScreen(outer))
.size(60, 20)
.pos(this.width - 20 - 60, this.height - 29)
.build());

View File

@ -70,7 +70,7 @@ public final class GameTestRegistration {
@Override
public void testFailed(GameTestInfo info, GameTestRunner runner) {
framework.changeStatus(test, Test.Status.failed("GameTest fail: " + info.getError().getMessage()), null);
framework.changeStatus(test, Test.Status.failed("GameTest failure: " + info.getError().getMessage(), info.getError()), null);
disable();
}
@ -88,7 +88,7 @@ public final class GameTestRegistration {
try {
game.function().accept(helper);
} catch (GameTestAssertException assertion) {
((MutableTestFramework) framework).tests().setStatus(test.id(), Test.Status.failed("GameTest fail: " + assertion.getMessage()));
((MutableTestFramework) framework).tests().setStatus(test.id(), Test.Status.failed("GameTest failure: " + assertion.getMessage(), assertion));
throw assertion;
}
} catch (GameTestAssertException exception) {

View File

@ -21,6 +21,7 @@ import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@ -124,7 +125,7 @@ public class TestFrameworkImpl implements MutableTestFramework {
boolean isGameTestRun = event.getServer() instanceof GameTestServer;
// Summarise test results
var builder = new TestSummary.Builder(id(), isGameTestRun);
var builder = new TestSummary.Builder(this, isGameTestRun);
tests().all().forEach(test -> {
String id = test.id();
Test.Status status = tests().getStatus(id);
@ -316,7 +317,9 @@ public class TestFrameworkImpl implements MutableTestFramework {
tests.globalListeners.forEach(listener -> listener.onStatusChange(this, test, oldStatus, newStatus, changer));
test.listeners().forEach(listener -> listener.onStatusChange(this, test, oldStatus, newStatus, changer));
logger.info("Status of test '{}' has had status changed to {}{}.", test.id(), newStatus, changer instanceof Player player ? " by " + player.getGameProfile().getName() : "");
BiConsumer<String, Object[]> logger = newStatus.result() == Test.Result.FAILED ? this.logger::error : this.logger::info;
logger.accept("Test '{}' has had status changed to {}{}.", new Object[] { test.id(), newStatus, changer instanceof Player player ? " by " + player.getGameProfile().getName() : "" });
if (server == null && !inClientWorld) return;
@ -460,6 +463,10 @@ public class TestFrameworkImpl implements MutableTestFramework {
test.groups().forEach(group -> getOrCreateGroup(group).add(test));
}
test.init(TestFrameworkImpl.this);
if (test.asGameTest() == null) {
getOrCreateGroup("manual").add(test);
}
}
private Group addGroupToParents(Group group) {

View File

@ -351,6 +351,11 @@ public abstract class AbstractTest implements Test {
public void pass() {
DynamicTest.super.pass();
}
@Nullable
public Method getMethod() {
return null;
}
}
protected interface AnnotationHolder {

View File

@ -13,6 +13,7 @@ import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.event.IModBusEvent;
import net.neoforged.testframework.Test;
import net.neoforged.testframework.impl.ReflectionUtils;
import org.jetbrains.annotations.Nullable;
public class MethodBasedEventTest extends AbstractTest.Dynamic {
protected MethodHandle handle;
@ -59,4 +60,10 @@ public class MethodBasedEventTest extends AbstractTest.Dynamic {
}
});
}
@Nullable
@Override
public Method getMethod() {
return method;
}
}

View File

@ -14,6 +14,7 @@ import net.neoforged.testframework.TestFramework;
import net.neoforged.testframework.gametest.EmptyTemplate;
import net.neoforged.testframework.gametest.GameTest;
import net.neoforged.testframework.impl.ReflectionUtils;
import org.jetbrains.annotations.Nullable;
public class MethodBasedGameTestTest extends AbstractTest.Dynamic {
protected MethodHandle handle;
@ -46,4 +47,10 @@ public class MethodBasedGameTestTest extends AbstractTest.Dynamic {
}
});
}
@Nullable
@Override
public Method getMethod() {
return method;
}
}

View File

@ -11,6 +11,7 @@ import net.neoforged.testframework.TestFramework;
import net.neoforged.testframework.gametest.EmptyTemplate;
import net.neoforged.testframework.gametest.GameTest;
import net.neoforged.testframework.impl.ReflectionUtils;
import org.jetbrains.annotations.Nullable;
public class MethodBasedTest extends AbstractTest.Dynamic {
protected MethodHandle handle;
@ -40,4 +41,10 @@ public class MethodBasedTest extends AbstractTest.Dynamic {
throw new RuntimeException("Encountered exception initiating method-based test: " + method, e);
}
}
@Nullable
@Override
public Method getMethod() {
return method;
}
}

View File

@ -18,7 +18,7 @@ public interface FileSummaryDumper extends SummaryDumper {
default void dump(TestSummary summary, Logger logger) {
logger.info("Test summary processing...");
Path outputPath = outputPath(summary.frameworkId());
Path outputPath = outputPath(summary.framework().id());
try {
Files.createDirectories(outputPath.getParent());
try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(outputPath))) {

View File

@ -6,19 +6,35 @@
package net.neoforged.testframework.summary;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.stream.Collectors;
import net.minecraft.resources.ResourceLocation;
import net.neoforged.testframework.Test;
import net.neoforged.testframework.impl.test.AbstractTest;
import net.neoforged.testframework.summary.md.Alignment;
import net.neoforged.testframework.summary.md.Table;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.slf4j.Logger;
public class GitHubActionsStepSummaryDumper implements FileSummaryDumper {
private static final String SOURCE_FILE_ROOTS_PROPERTY = "net.neoforged.testframework.sourceFileRoots";
private final Function<TestSummary, String> heading;
public GitHubActionsStepSummaryDumper() {
@ -77,6 +93,108 @@ public class GitHubActionsStepSummaryDumper implements FileSummaryDumper {
}
writer.println();
writer.println(builder.build());
// Generate check run annotations for failed tests that are @TestHolder methods
if (!failedTests.isEmpty() && System.getProperty(SOURCE_FILE_ROOTS_PROPERTY) != null) {
var roots = Arrays.stream(System.getProperty(SOURCE_FILE_ROOTS_PROPERTY).split(",")).map(Path::of).toList();
record TestLocation(Path path, Method method, String message, int line) {}
List<TestLocation> locations = new ArrayList<>();
for (var testInfo : failedTests) {
var test = summary.framework().tests().byId(testInfo.testId()).orElseThrow();
if (!(test instanceof AbstractTest.Dynamic dynamic)) continue;
var method = dynamic.getMethod();
if (method == null) continue;
var declaring = method.getDeclaringClass();
// Try to find the method's class file and read its bytecode to figure out the name of the source file and the line number range of the test method
try (var is = declaring.getClassLoader().getResourceAsStream(declaring.getName().replace(".", "/") + ".class")) {
if (is == null) continue;
AtomicReference<String> source = new AtomicReference<>();
// We collect both the first and the last line of the method to be able to find stack track elements included within the method's bounds
AtomicInteger firstLine = new AtomicInteger(-1), lastLine = new AtomicInteger();
var desc = Type.getMethodDescriptor(method);
new ClassReader(is).accept(new ClassVisitor(Opcodes.ASM9) {
@Override
public void visitSource(String s, String debug) {
source.set(s);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (source.get() != null && name.equals(method.getName()) && desc.equals(descriptor)) {
return new MethodVisitor(Opcodes.ASM9) {
private int lastFoundLine;
@Override
public void visitLineNumber(int line, Label start) {
if (firstLine.get() == -1) {
firstLine.set(line);
}
lastFoundLine = line;
}
@Override
public void visitEnd() {
lastLine.set(lastFoundLine);
}
};
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
}, ClassReader.SKIP_FRAMES);
// If we cannot find the method within the class file or if it doesn't have line number information we can't emit any annotation
if (firstLine.get() == -1) continue;
var relativeClassPath = declaring.getPackageName().replace(".", "/") + "/" + source.get();
for (Path root : roots) {
// Try to find the first source root folder where a source file with the name found in the bytecode and corresponding package exists
var possibleFile = root.resolve(relativeClassPath);
if (Files.exists(possibleFile)) {
int line = firstLine.get();
var exception = testInfo.status().exception();
if (exception != null) {
// If we have an exception, try to point the annotation at the first line of the exception within the same source file
// and within the lines of the method, otherwise, we point it at the first line of the method test that failed
for (StackTraceElement element : exception.getStackTrace()) {
if (Objects.equals(element.getFileName(), source.get()) && firstLine.get() <= element.getLineNumber() && element.getLineNumber() <= lastLine.get()) {
line = element.getLineNumber();
break;
}
}
}
locations.add(new TestLocation(possibleFile.toAbsolutePath(), method, testInfo.message(), line));
break;
}
}
} catch (Exception ex) {
logger.error("Failed to read class declaring method {}", method, ex);
}
}
if (!locations.isEmpty()) {
// Finally, emit the annotations but make sure to first relativise all paths to the workspace folder
var workspace = Path.of(System.getenv("GITHUB_WORKSPACE")).toAbsolutePath();
var errorMessage = locations.stream()
.map(loc -> "::error file=" + workspace.relativize(loc.path())
+ ",line=" + loc.line() + ",title=Test " + loc.method() + " failed::" + loc.message())
.collect(Collectors.joining("\n"));
// Print an empty line before to flush any dangling ANSI modifiers
System.out.println();
System.out.println(errorMessage);
// And an empty line after for symmetry
System.out.println();
}
}
}
protected String formatStatus(Test.Result result, boolean optional) {

View File

@ -59,7 +59,7 @@ public class JUnitSummaryDumper implements FileSummaryDumper {
DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
Document document = documentBuilder.newDocument();
Element testsuites = document.createElement("testsuites");
testsuites.setAttribute("name", summary.frameworkId().toString());
testsuites.setAttribute("name", summary.framework().id().toString());
testsuites.setAttribute("tests", Integer.toString(root.tests));
testsuites.setAttribute("failures", Integer.toString(root.failures));
testsuites.setAttribute("skipped", Integer.toString(root.skipped));

View File

@ -8,10 +8,10 @@ package net.neoforged.testframework.summary;
import com.google.common.collect.ImmutableList;
import java.util.List;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.neoforged.testframework.Test;
import net.neoforged.testframework.TestFramework;
public record TestSummary(ResourceLocation frameworkId, boolean isGameTestRun, List<TestInfo> testInfos) {
public record TestSummary(TestFramework framework, boolean isGameTestRun, List<TestInfo> testInfos) {
public record TestInfo(
String testId,
Component name,
@ -31,12 +31,12 @@ public record TestSummary(ResourceLocation frameworkId, boolean isGameTestRun, L
}
public static class Builder {
private final ResourceLocation frameworkId;
private final TestFramework framework;
private final boolean isGameTestRun;
private final ImmutableList.Builder<TestInfo> tests = ImmutableList.builder();
public Builder(ResourceLocation frameworkId, boolean isGameTestRun) {
this.frameworkId = frameworkId;
public Builder(TestFramework framework, boolean isGameTestRun) {
this.framework = framework;
this.isGameTestRun = isGameTestRun;
}
@ -45,7 +45,7 @@ public record TestSummary(ResourceLocation frameworkId, boolean isGameTestRun, L
}
public TestSummary build() {
return new TestSummary(frameworkId, isGameTestRun, tests.build());
return new TestSummary(framework, isGameTestRun, tests.build());
}
}
}

View File

@ -82,6 +82,9 @@ neoDev {
configureEach {
gameDirectory = layout.projectDir.dir("run/$name")
systemProperty("terminal.ansi", "true")
// This property allows the test framework to find source files and correctly annotate test failures in action runs
systemProperty('net.neoforged.testframework.sourceFileRoots', project.file('src/main/java').toPath().toAbsolutePath().toString())
}
client {
client()

View File

@ -21,7 +21,7 @@ import net.neoforged.testframework.annotation.ForEachTest;
import net.neoforged.testframework.annotation.TestHolder;
import net.neoforged.testframework.gametest.EmptyTemplate;
@ForEachTest(side = Dist.CLIENT, groups = { DimensionTransitionScreenTests.GROUP, "manual" })
@ForEachTest(side = Dist.CLIENT, groups = DimensionTransitionScreenTests.GROUP)
public class DimensionTransitionScreenTests {
public static final String GROUP = "dimension_transition";
public static final ResourceLocation NETHER_BG = ResourceLocation.withDefaultNamespace("textures/block/netherrack.png");

View File

@ -32,14 +32,12 @@ import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.testframework.DynamicTest;
import net.neoforged.testframework.annotation.ForEachTest;
import net.neoforged.testframework.annotation.TestHolder;
import net.neoforged.testframework.gametest.EmptyTemplate;
import org.joml.Matrix4f;
@ForEachTest(side = Dist.CLIENT, groups = MapDecorationRenderTests.GROUP)
public class MapDecorationRenderTests {
public static final String GROUP = "map_decoration_render";
@EmptyTemplate
@TestHolder(description = "Tests if custom map decoration renderers work", enabledByDefault = true)
static void customRenderer(DynamicTest test) {
var decorationType = test.registrationHelper().registrar(Registries.MAP_DECORATION_TYPE).register(
@ -70,7 +68,6 @@ public class MapDecorationRenderTests {
});
}
@EmptyTemplate
@TestHolder(description = "Tests if custom map decoration render state data works")
static void customRenderData(DynamicTest test) {
var key = new ContextKey<Integer>(ResourceLocation.fromNamespaceAndPath(test.createModId(), "custom_color"));

View File

@ -23,8 +23,6 @@ import net.neoforged.neoforge.internal.versions.neoforge.NeoForgeVersion;
import net.neoforged.testframework.DynamicTest;
import net.neoforged.testframework.annotation.ForEachTest;
import net.neoforged.testframework.annotation.TestHolder;
import net.neoforged.testframework.gametest.EmptyTemplate;
import net.neoforged.testframework.gametest.GameTest;
@ForEachTest(side = Dist.CLIENT, groups = { "client.texture_atlas", "texture_atlas" })
public class TextureAtlasTests {
@ -69,8 +67,6 @@ public class TextureAtlasTests {
}
@TestHolder(description = { "Tests that custom sprite metadata sections get passed through resource reloading properly" }, enabledByDefault = true)
@GameTest
@EmptyTemplate
static void defaultSpriteMetadataSections(final DynamicTest test) {
String modId = test.createModId();

View File

@ -10,6 +10,7 @@ import net.minecraft.core.registries.Registries;
import net.minecraft.data.recipes.RecipeCategory;
import net.minecraft.data.recipes.RecipeOutput;
import net.minecraft.data.recipes.RecipeProvider;
import net.minecraft.gametest.framework.GameTestServer;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
@ -85,6 +86,8 @@ public class CustomFeatureFlagsTests {
.registerSimpleItem("ext_range_disabled_test", new Item.Properties().requiredFeatures(extRangeDisabledTestFlag));
test.eventListeners().forge().addListener((ServerStartedEvent event) -> {
if (event.getServer() instanceof GameTestServer) return; // The gametest server enables all flags, so we're not interested in running the check
FeatureFlagSet flagSet = event.getServer().getLevel(Level.OVERWORLD).enabledFeatures();
if (!baseRangeEnabledTestItem.get().isEnabled(flagSet)) {
test.fail("Item with enabled custom flag in base mask range was unexpectedly disabled");

View File

@ -27,7 +27,7 @@ public class DatapackEntryTests {
@GameTest
@EmptyTemplate
@TestHolder(description = "Tests that datapack entry conditions are generated correctly", enabledByDefault = true)
@TestHolder(description = "Tests that datapack entry conditions are generated correctly")
static void conditionalDatapackEntries(final DynamicTest test, final RegistrationHelper reg) {
ResourceKey<DamageType> CONDITIONAL_FALSE_DAMAGE_TYPE = ResourceKey.create(Registries.DAMAGE_TYPE, ResourceLocation.fromNamespaceAndPath(reg.modId(), "conditional_false"));
ResourceKey<DamageType> CONDITIONAL_TRUE_DAMAGE_TYPE = ResourceKey.create(Registries.DAMAGE_TYPE, ResourceLocation.fromNamespaceAndPath(reg.modId(), "conditional_true"));

View File

@ -6,6 +6,7 @@
package net.neoforged.neoforge.debug.entity;
import java.util.function.Consumer;
import java.util.function.Supplier;
import net.minecraft.client.renderer.entity.NoopRenderer;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
@ -20,6 +21,8 @@ import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.MobCategory;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.ValueInput;
@ -42,12 +45,14 @@ public class EntityTests {
@EmptyTemplate
@TestHolder(description = "Tests if custom fence gates without wood types work, allowing for the use of the vanilla block for non-wooden gates")
static void customSpawnLogic(final DynamicTest test, final RegistrationHelper reg) {
Supplier<AttributeSupplier.Builder> attr = () -> AttributeSupplier.builder()
.add(Attributes.MAX_HEALTH, 1);
final var usingForgeAdvancedSpawn = reg.entityTypes().registerEntityType("complex_spawn", CustomComplexSpawnEntity::new, MobCategory.AMBIENT, builder -> builder.sized(1, 1))
.withLang("Custom complex spawn egg").withRenderer(() -> NoopRenderer::new);
.withLang("Custom complex spawn egg").withAttributes(attr).withRenderer(() -> NoopRenderer::new);
final var usingCustomPayloadsSpawn = reg.entityTypes().registerEntityType("adapted_spawn", AdaptedSpawnEntity::new, MobCategory.AMBIENT, builder -> builder.sized(1, 1))
.withLang("Adapted complex spawn egg").withRenderer(() -> NoopRenderer::new);
.withLang("Adapted complex spawn egg").withAttributes(attr).withRenderer(() -> NoopRenderer::new);
final var simpleSpawn = reg.entityTypes().registerEntityType("simple_spawn", SimpleEntity::new, MobCategory.AMBIENT, builder -> builder.sized(1, 1))
.withLang("Simple spawn egg").withRenderer(() -> NoopRenderer::new);
.withLang("Simple spawn egg").withAttributes(attr).withRenderer(() -> NoopRenderer::new);
reg.eventListeners().accept((Consumer<RegisterPayloadHandlersEvent>) event -> event.registrar("1")
.playToClient(EntityTests.CustomSyncPayload.TYPE, CustomSyncPayload.STREAM_CODEC, (payload, context) -> {}));

View File

@ -84,6 +84,7 @@ public class AdvancementTests {
@GameTest
@EmptyTemplate
@SuppressWarnings("removal")
@TestHolder(description = "Tests if custom advancement predicates work")
static void customPredicateTest(final DynamicTest test, final RegistrationHelper reg) {
DataComponentPredicate.Type<CustomNamePredicate> type = new DataComponentPredicate.Type<>(RecordCodecBuilder.create(g -> g.group(

View File

@ -9,11 +9,13 @@ import java.util.concurrent.atomic.AtomicInteger;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.testframework.annotation.ForEachTest;
import net.neoforged.testframework.annotation.TestHolder;
import net.neoforged.testframework.gametest.EmptyTemplate;
import net.neoforged.testframework.gametest.ExtendedGameTestHelper;
import net.neoforged.testframework.gametest.GameTest;
@ForEachTest(groups = "fml")
public class MultipleEntrypointsTest {
private static final String MOD_ID = "multiple_entrypoints_test";
private static final AtomicInteger CLIENT_COUNTER = new AtomicInteger();

View File

@ -28,6 +28,7 @@ import net.neoforged.testframework.annotation.TestHolder;
public class ModDatapackTest {
public static final String GROUP = "resources";
@SuppressWarnings("removal")
@TestHolder(description = "Tests that mod datapacks are loaded properly on initial load and reload", enabledByDefault = true)
static void modDatapack(final DynamicTest test) {
final ResourceLocation testAdvancement = ResourceLocation.fromNamespaceAndPath(test.createModId(), "recipes/misc/test_advancement");

View File

@ -22,9 +22,9 @@ import org.apache.commons.lang3.mutable.MutableBoolean;
public class RichTranslationsTest {
public static final String GROUP = "resources";
@TestHolder(description = "Tests that rich translations work properly", enabledByDefault = true)
@GameTest
@EmptyTemplate("1x1x1")
@TestHolder(description = "Tests that rich translations work properly")
static void richTranslations(final DynamicTest test) {
test.onGameTest(helper -> {
String arg = "Example argument";

View File

@ -522,6 +522,7 @@ public class DataGeneratorTest {
private static class Advancements implements AdvancementSubProvider {
@Override
@SuppressWarnings("removal")
public void generate(HolderLookup.Provider registries, Consumer<AdvancementHolder> saver) {
var obtainDirt = Advancement.Builder.advancement()
.display(Items.DIRT,

View File

@ -1,38 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.neoforged.neoforge.oldtest;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.neoforge.registries.DeferredHolder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Checks that {@link DeferredHolder} works correctly, specifically that get() functions immediately
* after construction, if registries are already populated.
*/
@Mod(DeferredHolderTest.MODID)
public class DeferredHolderTest {
static final String MODID = "deferred_holder_test";
private static final boolean ENABLED = true;
private static final Logger LOGGER = LogManager.getLogger();
public DeferredHolderTest(IEventBus modBus) {
if (!ENABLED) return;
modBus.addListener(this::commonSetup);
}
public void commonSetup(FMLCommonSetupEvent event) {
LOGGER.info("Stone 1: {}", DeferredHolder.create(Registries.BLOCK, ResourceLocation.fromNamespaceAndPath("minecraft", "stone")).get());
}
}

View File

@ -1,67 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.neoforged.neoforge.oldtest;
import com.google.common.collect.Sets;
import java.util.Set;
import java.util.stream.Collectors;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.BlockTags;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.server.ServerStartedEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Tests that the values for defaulted optional tags defined in multiple places are combined.
*
* <p>The optional tag defined by this mod is deliberately not defined in a data pack, to cause it to 'default' and
* trigger the behavior being tested.</p>
*
* @see <a href="https://github.com/MinecraftForge/MinecraftForge/issues/7570">MinecraftForge/MinecraftForge#7570</a>
*/
@Mod(DuplicateOptionalTagTest.MODID)
public class DuplicateOptionalTagTest {
private static final Logger LOGGER = LogManager.getLogger();
static final String MODID = "duplicate_optional_tag_test";
private static final ResourceLocation TAG_NAME = ResourceLocation.fromNamespaceAndPath(MODID, "test_optional_tag");
private static final Set<Block> TAG_A_DEFAULTS = Set.of(Blocks.BEDROCK);
private static final Set<Block> TAG_B_DEFAULTS = Set.of(Blocks.WHITE_WOOL);
private static final TagKey<Block> TAG_A = BlockTags.create(TAG_NAME);
private static final TagKey<Block> TAG_B = BlockTags.create(TAG_NAME);
public DuplicateOptionalTagTest() {
NeoForge.EVENT_BUS.addListener(this::onServerStarted);
}
private void onServerStarted(ServerStartedEvent event) {
Set<Block> tagAValues = BuiltInRegistries.BLOCK.get(TAG_A).map(tag -> tag.stream().map(Holder::value).collect(Collectors.toUnmodifiableSet())).orElse(TAG_A_DEFAULTS);
Set<Block> tagBValues = BuiltInRegistries.BLOCK.get(TAG_B).map(tag -> tag.stream().map(Holder::value).collect(Collectors.toUnmodifiableSet())).orElse(TAG_B_DEFAULTS);
if (!tagAValues.equals(tagBValues)) {
LOGGER.error("Values of both optional tag instances are not the same: first instance: {}, second instance: {}", tagAValues, tagBValues);
return;
}
final Set<Block> expected = Sets.union(TAG_A_DEFAULTS, TAG_B_DEFAULTS).stream().collect(Collectors.toUnmodifiableSet());
if (!tagAValues.equals(expected)) {
IllegalStateException e = new IllegalStateException("Optional tag values do not match!");
LOGGER.error("Values of the optional tag do not match the expected union of their defaults: expected {}, got {}", expected, tagAValues, e);
return;
}
LOGGER.info("Optional tag instances match each other and the expected union of their defaults");
}
}

View File

@ -23,7 +23,7 @@ import org.apache.logging.log4j.Logger;
@Mod("permissiontest")
public class PermissionTest {
private static final boolean ENABLED = true;
private static final boolean ENABLED = false;
private static final Logger LOGGER = LogManager.getLogger();
private static final PermissionNode<Boolean> boolPerm = new PermissionNode<>("permissiontest", "test.blob", PermissionTypes.BOOLEAN, (player, playerUUID, context) -> true);

View File

@ -1,93 +0,0 @@
/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.neoforged.neoforge.oldtest.misc;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.mojang.datafixers.util.Pair;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.Registry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.Tag;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* This test mod show a few example usages of {@link Registry#byNameCodec()} to serialize and deserialize registry entries to JSON or NBT.
* There are 4 tested cases :
* 1. json -> Pair
* 2. Pair -> nbt
* 3. Pair -> compressed json
* 4. compressed json -> Pair
* For each test the result will be logged.
*/
@Mod("registry_codec_test")
public class RegistryCodecTest {
private static final Logger LOGGER = LogManager.getLogger("Codec Registry Test");
/**
* This Codec can serialize and deserialize a {@code Pair<Item, Block>}.
* The resulting JSON (or NBT equivalent) will have this structure:
*
* <pre>{@code
* {
* "block": "block_registry_name",
* "item": "item_registry_name"
* }
* }</pre>
*/
private static final Codec<Pair<Block, Item>> CODEC = RecordCodecBuilder.create(codecInstance -> codecInstance.group(
BuiltInRegistries.BLOCK.byNameCodec().fieldOf("block").forGetter(Pair::getFirst),
BuiltInRegistries.ITEM.byNameCodec().fieldOf("item").forGetter(Pair::getSecond)).apply(codecInstance, Pair::of));
public RegistryCodecTest(IEventBus modEventBus) {
modEventBus.addListener(this::commonSetup);
}
public void commonSetup(final FMLCommonSetupEvent event) {
//Create our Json to decode
JsonObject json = new JsonObject();
json.addProperty("block", "minecraft:diamond_block");
json.addProperty("item", "minecraft:diamond_pickaxe");
//Decode our Json and log an info in case of success or a warning in case of error
DataResult<Pair<Pair<Block, Item>, JsonElement>> result = CODEC.decode(JsonOps.INSTANCE, json);
result.resultOrPartial(LOGGER::warn).ifPresent(pair -> LOGGER.info("Successfully decoded a diamond block and a diamond pickaxe from json to Block/Item"));
//Create a Pair<Block, Item> to test the serialization of our codec
Pair<Block, Item> pair = Pair.of(Blocks.DIAMOND_BLOCK, Items.DIAMOND_PICKAXE);
//Serialize the Pair to NBT, and log an info in case of success or a warning in case of error
DataResult<Tag> result2 = CODEC.encodeStart(NbtOps.INSTANCE, pair);
result2.resultOrPartial(LOGGER::warn).ifPresent(tag -> LOGGER.info("Successfully encoded a Pair<Block, Item> to a nbt tag: {}", tag));
//Serialize the Pair to JSON using the COMPRESSED JsonOps, this will use the int registry id instead of the ResourceLocation one,
//This is not recommended because int IDs can change, so you should not rely on them
DataResult<JsonElement> result3 = CODEC.encodeStart(JsonOps.COMPRESSED, pair);
result3.resultOrPartial(LOGGER::warn).ifPresent(compressedJson -> LOGGER.info("Successfully encoded a Pair<Block, Item> to a compressed json: {}", compressedJson));
//Create a json to decode using numerical IDs, to be decoded by JsonOps.COMPRESSED
JsonArray jsonCompressed = new JsonArray();
jsonCompressed.add(BuiltInRegistries.BLOCK.getId(Blocks.DIAMOND_BLOCK));
jsonCompressed.add(BuiltInRegistries.ITEM.getId(Items.DIAMOND_PICKAXE));
//Decode a compressed json to the corresponding Pair<Block, Item>, this time using Codec#parse
DataResult<Pair<Block, Item>> result4 = CODEC.parse(JsonOps.COMPRESSED, jsonCompressed);
result4.resultOrPartial(LOGGER::warn).ifPresent(pair2 -> LOGGER.info("Successfully decoded a diamond block and a diamond pickaxe from compressed json to Block/Item"));
}
}

View File

@ -74,7 +74,7 @@ import org.slf4j.Logger;
public class LoginPacketSplitTest {
public static final Logger LOG = LogUtils.getLogger();
public static final String MOD_ID = "login_packet_split_test";
public static final boolean ENABLED = true;
public static final boolean ENABLED = false;
private static final Gson GSON = new Gson();
public static final ResourceKey<Registry<BigData>> BIG_DATA = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(MOD_ID, "big_data"));

View File

@ -175,18 +175,12 @@ modId="part_entity_test"
[[mods]]
modId="custom_armor_model_test"
[[mods]]
modId="duplicate_optional_tag_test"
[[mods]]
modId="custom_particle_type_test"
[[mods]]
modId="renderable_test"
[[mods]]
modId="ingredient_invalidation"
[[mods]]
modId="registry_codec_test"
[[mods]]
modId="deferred_holder_test"
[[mods]]
modId="gametest_test"
[[mods]]
modId="many_mob_effects_test"