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 uses: neoforged/actions/setup-java@main
with: with:
java-version: 21 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 - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4

View File

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

View File

@ -23,6 +23,7 @@ jobs:
uses: neoforged/actions/setup-java@main uses: neoforged/actions/setup-java@main
with: with:
java-version: 21 java-version: 21
problem-matcher: false
- name: Setup Gradle - name: Setup Gradle
uses: gradle/actions/setup-gradle@v4 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_) { private static boolean isFaceOccludedByState(Direction p_110980_, float p_110981_, BlockState p_110983_) {
VoxelShape voxelshape = p_110983_.getFaceOcclusionShape(p_110980_.getOpposite()); VoxelShape voxelshape = p_110983_.getFaceOcclusionShape(p_110980_.getOpposite());
if (voxelshape == Shapes.empty()) { if (voxelshape == Shapes.empty()) {
@@ -64,14 +_,20 @@ @@ -64,14 +_,21 @@
return isFaceOccludedByState(p_110963_.getOpposite(), 1.0F, p_110962_); return isFaceOccludedByState(p_110963_.getOpposite(), 1.0F, p_110962_);
} }
+ /** @deprecated Neo: use overload that accepts BlockState */ + /** @deprecated Neo: use overload that accepts BlockState */
+ @Deprecated
public static boolean shouldRenderFace(FluidState p_203169_, BlockState p_203170_, Direction p_203171_, FluidState p_203172_) { 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_); return !isFaceOccludedBySelf(p_203170_, p_203171_) && !isNeighborSameFluid(p_203169_, p_203172_);
} }

View File

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

View File

@ -1,6 +1,6 @@
--- a/net/minecraft/data/tags/TagsProvider.java --- a/net/minecraft/data/tags/TagsProvider.java
+++ b/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; private final CompletableFuture<TagsProvider.TagLookup<T>> parentProvider;
protected final ResourceKey<? extends Registry<T>> registryKey; protected final ResourceKey<? extends Registry<T>> registryKey;
protected final Map<ResourceLocation, TagBuilder> builders = Maps.newLinkedHashMap(); 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 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_) { 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_, CompletableFuture.completedFuture(TagsProvider.TagLookup.empty()));
+ this(p_256596_, p_255886_, p_256513_, "vanilla"); + this(p_256596_, p_255886_, p_256513_, "vanilla");

View File

@ -34,3 +34,33 @@
return true; 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 @Nullable
@SuppressWarnings("removal")
private Advancement.Builder findAndReplaceInHolder(AdvancementHolder advancementHolder, HolderLookup.Provider registries) { private Advancement.Builder findAndReplaceInHolder(AdvancementHolder advancementHolder, HolderLookup.Provider registries) {
Advancement advancement = advancementHolder.value(); Advancement advancement = advancementHolder.value();
Advancement.Builder builder = Advancement.Builder.advancement(); 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> * 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 * @return the groups of this test
*/ */
@ -199,8 +201,13 @@ public interface Test extends Groupable {
* *
* @param result the result * @param result the result
* @param message the message, providing additional context if the test failed * @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 DEFAULT = new Status(Result.NOT_PROCESSED, "");
public static final Status PASSED = new Status(Result.PASSED, ""); public static final Status PASSED = new Status(Result.PASSED, "");
@ -213,7 +220,11 @@ public interface Test extends Groupable {
} }
public static Status failed(String message) { 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() { public MutableComponent asComponent() {
@ -230,7 +241,7 @@ public interface Test extends Groupable {
if (message.isBlank()) { if (message.isBlank()) {
return "[result=" + result + "]"; return "[result=" + result + "]";
} else { } 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 @ParametersAreNonnullByDefault
public abstract class AbstractTestScreen extends Screen { public abstract class AbstractTestScreen extends Screen {
protected final MutableTestFramework framework; protected final MutableTestFramework framework;
private final Screen outer = this;
public AbstractTestScreen(Component title, MutableTestFramework framework) { public AbstractTestScreen(Component title, MutableTestFramework framework) {
super(title); 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); graphics.blitSprite(RenderPipelines.GUI_TEXTURED, icon, pLeft, pTop, 9, 9, renderTransparent ? (alpha | 0x00FFFFFF) : 0xFFFFFFFF);
final Component title = TestsOverlay.statusColoured(test.visuals().title(), status); 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 @Override
@ -266,9 +267,9 @@ public abstract class AbstractTestScreen extends Screen {
final List<Test> all = group.resolveAll(); final List<Test> all = group.resolveAll();
final int enabledCount = (int) all.stream().filter(it -> framework.tests().isEnabled(it.id())).count(); final int enabledCount = (int) all.stream().filter(it -> framework.tests().isEnabled(it.id())).count();
if (enabledCount == all.size()) { 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) { } 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 { } else {
graphics.setTooltipForNextFrame(font, Component.literal(enabledCount + "/" + all.size() + " tests enabled!").withStyle(ChatFormatting.BLUE), mouseX, mouseY); 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() { private void openBrowseGUI() {
Minecraft.getInstance().pushGuiLayer(new TestScreen( Minecraft.getInstance().setScreen(new TestScreen(
Component.literal("Tests of group ").append(getTitle()), Component.literal("Tests of group ").append(getTitle()),
framework, List.of(group)) { framework, List.of(group)) {
@Override @Override
@ -302,7 +303,7 @@ public abstract class AbstractTestScreen extends Screen {
showAsGroup.setValue(false); showAsGroup.setValue(false);
groupableList.resetRows(""); 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) .size(60, 20)
.pos(this.width - 20 - 60, this.height - 29) .pos(this.width - 20 - 60, this.height - 29)
.build()); .build());

View File

@ -70,7 +70,7 @@ public final class GameTestRegistration {
@Override @Override
public void testFailed(GameTestInfo info, GameTestRunner runner) { 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(); disable();
} }
@ -88,7 +88,7 @@ public final class GameTestRegistration {
try { try {
game.function().accept(helper); game.function().accept(helper);
} catch (GameTestAssertException assertion) { } 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; throw assertion;
} }
} catch (GameTestAssertException exception) { } catch (GameTestAssertException exception) {

View File

@ -21,6 +21,7 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -124,7 +125,7 @@ public class TestFrameworkImpl implements MutableTestFramework {
boolean isGameTestRun = event.getServer() instanceof GameTestServer; boolean isGameTestRun = event.getServer() instanceof GameTestServer;
// Summarise test results // Summarise test results
var builder = new TestSummary.Builder(id(), isGameTestRun); var builder = new TestSummary.Builder(this, isGameTestRun);
tests().all().forEach(test -> { tests().all().forEach(test -> {
String id = test.id(); String id = test.id();
Test.Status status = tests().getStatus(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)); tests.globalListeners.forEach(listener -> listener.onStatusChange(this, test, oldStatus, newStatus, changer));
test.listeners().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; if (server == null && !inClientWorld) return;
@ -460,6 +463,10 @@ public class TestFrameworkImpl implements MutableTestFramework {
test.groups().forEach(group -> getOrCreateGroup(group).add(test)); test.groups().forEach(group -> getOrCreateGroup(group).add(test));
} }
test.init(TestFrameworkImpl.this); test.init(TestFrameworkImpl.this);
if (test.asGameTest() == null) {
getOrCreateGroup("manual").add(test);
}
} }
private Group addGroupToParents(Group group) { private Group addGroupToParents(Group group) {

View File

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

View File

@ -13,6 +13,7 @@ import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.event.IModBusEvent; import net.neoforged.fml.event.IModBusEvent;
import net.neoforged.testframework.Test; import net.neoforged.testframework.Test;
import net.neoforged.testframework.impl.ReflectionUtils; import net.neoforged.testframework.impl.ReflectionUtils;
import org.jetbrains.annotations.Nullable;
public class MethodBasedEventTest extends AbstractTest.Dynamic { public class MethodBasedEventTest extends AbstractTest.Dynamic {
protected MethodHandle handle; 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.EmptyTemplate;
import net.neoforged.testframework.gametest.GameTest; import net.neoforged.testframework.gametest.GameTest;
import net.neoforged.testframework.impl.ReflectionUtils; import net.neoforged.testframework.impl.ReflectionUtils;
import org.jetbrains.annotations.Nullable;
public class MethodBasedGameTestTest extends AbstractTest.Dynamic { public class MethodBasedGameTestTest extends AbstractTest.Dynamic {
protected MethodHandle handle; 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.EmptyTemplate;
import net.neoforged.testframework.gametest.GameTest; import net.neoforged.testframework.gametest.GameTest;
import net.neoforged.testframework.impl.ReflectionUtils; import net.neoforged.testframework.impl.ReflectionUtils;
import org.jetbrains.annotations.Nullable;
public class MethodBasedTest extends AbstractTest.Dynamic { public class MethodBasedTest extends AbstractTest.Dynamic {
protected MethodHandle handle; protected MethodHandle handle;
@ -40,4 +41,10 @@ public class MethodBasedTest extends AbstractTest.Dynamic {
throw new RuntimeException("Encountered exception initiating method-based test: " + method, e); 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) { default void dump(TestSummary summary, Logger logger) {
logger.info("Test summary processing..."); logger.info("Test summary processing...");
Path outputPath = outputPath(summary.frameworkId()); Path outputPath = outputPath(summary.framework().id());
try { try {
Files.createDirectories(outputPath.getParent()); Files.createDirectories(outputPath.getParent());
try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(outputPath))) { try (PrintWriter writer = new PrintWriter(Files.newBufferedWriter(outputPath))) {

View File

@ -6,19 +6,35 @@
package net.neoforged.testframework.summary; package net.neoforged.testframework.summary;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.neoforged.testframework.Test; 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.Alignment;
import net.neoforged.testframework.summary.md.Table; 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; import org.slf4j.Logger;
public class GitHubActionsStepSummaryDumper implements FileSummaryDumper { public class GitHubActionsStepSummaryDumper implements FileSummaryDumper {
private static final String SOURCE_FILE_ROOTS_PROPERTY = "net.neoforged.testframework.sourceFileRoots";
private final Function<TestSummary, String> heading; private final Function<TestSummary, String> heading;
public GitHubActionsStepSummaryDumper() { public GitHubActionsStepSummaryDumper() {
@ -77,6 +93,108 @@ public class GitHubActionsStepSummaryDumper implements FileSummaryDumper {
} }
writer.println(); writer.println();
writer.println(builder.build()); 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) { protected String formatStatus(Test.Result result, boolean optional) {

View File

@ -59,7 +59,7 @@ public class JUnitSummaryDumper implements FileSummaryDumper {
DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder(); DocumentBuilder documentBuilder = builderFactory.newDocumentBuilder();
Document document = documentBuilder.newDocument(); Document document = documentBuilder.newDocument();
Element testsuites = document.createElement("testsuites"); 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("tests", Integer.toString(root.tests));
testsuites.setAttribute("failures", Integer.toString(root.failures)); testsuites.setAttribute("failures", Integer.toString(root.failures));
testsuites.setAttribute("skipped", Integer.toString(root.skipped)); 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 com.google.common.collect.ImmutableList;
import java.util.List; import java.util.List;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.neoforged.testframework.Test; 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( public record TestInfo(
String testId, String testId,
Component name, Component name,
@ -31,12 +31,12 @@ public record TestSummary(ResourceLocation frameworkId, boolean isGameTestRun, L
} }
public static class Builder { public static class Builder {
private final ResourceLocation frameworkId; private final TestFramework framework;
private final boolean isGameTestRun; private final boolean isGameTestRun;
private final ImmutableList.Builder<TestInfo> tests = ImmutableList.builder(); private final ImmutableList.Builder<TestInfo> tests = ImmutableList.builder();
public Builder(ResourceLocation frameworkId, boolean isGameTestRun) { public Builder(TestFramework framework, boolean isGameTestRun) {
this.frameworkId = frameworkId; this.framework = framework;
this.isGameTestRun = isGameTestRun; this.isGameTestRun = isGameTestRun;
} }
@ -45,7 +45,7 @@ public record TestSummary(ResourceLocation frameworkId, boolean isGameTestRun, L
} }
public TestSummary build() { 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 { configureEach {
gameDirectory = layout.projectDir.dir("run/$name") gameDirectory = layout.projectDir.dir("run/$name")
systemProperty("terminal.ansi", "true") 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 {
client() client()

View File

@ -21,7 +21,7 @@ import net.neoforged.testframework.annotation.ForEachTest;
import net.neoforged.testframework.annotation.TestHolder; import net.neoforged.testframework.annotation.TestHolder;
import net.neoforged.testframework.gametest.EmptyTemplate; 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 class DimensionTransitionScreenTests {
public static final String GROUP = "dimension_transition"; public static final String GROUP = "dimension_transition";
public static final ResourceLocation NETHER_BG = ResourceLocation.withDefaultNamespace("textures/block/netherrack.png"); 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.DynamicTest;
import net.neoforged.testframework.annotation.ForEachTest; import net.neoforged.testframework.annotation.ForEachTest;
import net.neoforged.testframework.annotation.TestHolder; import net.neoforged.testframework.annotation.TestHolder;
import net.neoforged.testframework.gametest.EmptyTemplate;
import org.joml.Matrix4f; import org.joml.Matrix4f;
@ForEachTest(side = Dist.CLIENT, groups = MapDecorationRenderTests.GROUP) @ForEachTest(side = Dist.CLIENT, groups = MapDecorationRenderTests.GROUP)
public class MapDecorationRenderTests { public class MapDecorationRenderTests {
public static final String GROUP = "map_decoration_render"; public static final String GROUP = "map_decoration_render";
@EmptyTemplate
@TestHolder(description = "Tests if custom map decoration renderers work", enabledByDefault = true) @TestHolder(description = "Tests if custom map decoration renderers work", enabledByDefault = true)
static void customRenderer(DynamicTest test) { static void customRenderer(DynamicTest test) {
var decorationType = test.registrationHelper().registrar(Registries.MAP_DECORATION_TYPE).register( 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") @TestHolder(description = "Tests if custom map decoration render state data works")
static void customRenderData(DynamicTest test) { static void customRenderData(DynamicTest test) {
var key = new ContextKey<Integer>(ResourceLocation.fromNamespaceAndPath(test.createModId(), "custom_color")); 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.DynamicTest;
import net.neoforged.testframework.annotation.ForEachTest; import net.neoforged.testframework.annotation.ForEachTest;
import net.neoforged.testframework.annotation.TestHolder; 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" }) @ForEachTest(side = Dist.CLIENT, groups = { "client.texture_atlas", "texture_atlas" })
public class TextureAtlasTests { 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) @TestHolder(description = { "Tests that custom sprite metadata sections get passed through resource reloading properly" }, enabledByDefault = true)
@GameTest
@EmptyTemplate
static void defaultSpriteMetadataSections(final DynamicTest test) { static void defaultSpriteMetadataSections(final DynamicTest test) {
String modId = test.createModId(); 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.RecipeCategory;
import net.minecraft.data.recipes.RecipeOutput; import net.minecraft.data.recipes.RecipeOutput;
import net.minecraft.data.recipes.RecipeProvider; import net.minecraft.data.recipes.RecipeProvider;
import net.minecraft.gametest.framework.GameTestServer;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
@ -85,6 +86,8 @@ public class CustomFeatureFlagsTests {
.registerSimpleItem("ext_range_disabled_test", new Item.Properties().requiredFeatures(extRangeDisabledTestFlag)); .registerSimpleItem("ext_range_disabled_test", new Item.Properties().requiredFeatures(extRangeDisabledTestFlag));
test.eventListeners().forge().addListener((ServerStartedEvent event) -> { 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(); FeatureFlagSet flagSet = event.getServer().getLevel(Level.OVERWORLD).enabledFeatures();
if (!baseRangeEnabledTestItem.get().isEnabled(flagSet)) { if (!baseRangeEnabledTestItem.get().isEnabled(flagSet)) {
test.fail("Item with enabled custom flag in base mask range was unexpectedly disabled"); test.fail("Item with enabled custom flag in base mask range was unexpectedly disabled");

View File

@ -27,7 +27,7 @@ public class DatapackEntryTests {
@GameTest @GameTest
@EmptyTemplate @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) { 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_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")); 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; package net.neoforged.neoforge.debug.entity;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier;
import net.minecraft.client.renderer.entity.NoopRenderer; import net.minecraft.client.renderer.entity.NoopRenderer;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf; 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.Entity;
import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.MobCategory; 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.GameType;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueInput;
@ -42,12 +45,14 @@ public class EntityTests {
@EmptyTemplate @EmptyTemplate
@TestHolder(description = "Tests if custom fence gates without wood types work, allowing for the use of the vanilla block for non-wooden gates") @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) { 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)) 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)) 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)) 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") reg.eventListeners().accept((Consumer<RegisterPayloadHandlersEvent>) event -> event.registrar("1")
.playToClient(EntityTests.CustomSyncPayload.TYPE, CustomSyncPayload.STREAM_CODEC, (payload, context) -> {})); .playToClient(EntityTests.CustomSyncPayload.TYPE, CustomSyncPayload.STREAM_CODEC, (payload, context) -> {}));

View File

@ -84,6 +84,7 @@ public class AdvancementTests {
@GameTest @GameTest
@EmptyTemplate @EmptyTemplate
@SuppressWarnings("removal")
@TestHolder(description = "Tests if custom advancement predicates work") @TestHolder(description = "Tests if custom advancement predicates work")
static void customPredicateTest(final DynamicTest test, final RegistrationHelper reg) { static void customPredicateTest(final DynamicTest test, final RegistrationHelper reg) {
DataComponentPredicate.Type<CustomNamePredicate> type = new DataComponentPredicate.Type<>(RecordCodecBuilder.create(g -> g.group( 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.api.distmarker.Dist;
import net.neoforged.fml.common.Mod; import net.neoforged.fml.common.Mod;
import net.neoforged.fml.loading.FMLLoader; import net.neoforged.fml.loading.FMLLoader;
import net.neoforged.testframework.annotation.ForEachTest;
import net.neoforged.testframework.annotation.TestHolder; import net.neoforged.testframework.annotation.TestHolder;
import net.neoforged.testframework.gametest.EmptyTemplate; import net.neoforged.testframework.gametest.EmptyTemplate;
import net.neoforged.testframework.gametest.ExtendedGameTestHelper; import net.neoforged.testframework.gametest.ExtendedGameTestHelper;
import net.neoforged.testframework.gametest.GameTest; import net.neoforged.testframework.gametest.GameTest;
@ForEachTest(groups = "fml")
public class MultipleEntrypointsTest { public class MultipleEntrypointsTest {
private static final String MOD_ID = "multiple_entrypoints_test"; private static final String MOD_ID = "multiple_entrypoints_test";
private static final AtomicInteger CLIENT_COUNTER = new AtomicInteger(); private static final AtomicInteger CLIENT_COUNTER = new AtomicInteger();

View File

@ -28,6 +28,7 @@ import net.neoforged.testframework.annotation.TestHolder;
public class ModDatapackTest { public class ModDatapackTest {
public static final String GROUP = "resources"; public static final String GROUP = "resources";
@SuppressWarnings("removal")
@TestHolder(description = "Tests that mod datapacks are loaded properly on initial load and reload", enabledByDefault = true) @TestHolder(description = "Tests that mod datapacks are loaded properly on initial load and reload", enabledByDefault = true)
static void modDatapack(final DynamicTest test) { static void modDatapack(final DynamicTest test) {
final ResourceLocation testAdvancement = ResourceLocation.fromNamespaceAndPath(test.createModId(), "recipes/misc/test_advancement"); 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 class RichTranslationsTest {
public static final String GROUP = "resources"; public static final String GROUP = "resources";
@TestHolder(description = "Tests that rich translations work properly", enabledByDefault = true)
@GameTest @GameTest
@EmptyTemplate("1x1x1") @EmptyTemplate("1x1x1")
@TestHolder(description = "Tests that rich translations work properly")
static void richTranslations(final DynamicTest test) { static void richTranslations(final DynamicTest test) {
test.onGameTest(helper -> { test.onGameTest(helper -> {
String arg = "Example argument"; String arg = "Example argument";

View File

@ -522,6 +522,7 @@ public class DataGeneratorTest {
private static class Advancements implements AdvancementSubProvider { private static class Advancements implements AdvancementSubProvider {
@Override @Override
@SuppressWarnings("removal")
public void generate(HolderLookup.Provider registries, Consumer<AdvancementHolder> saver) { public void generate(HolderLookup.Provider registries, Consumer<AdvancementHolder> saver) {
var obtainDirt = Advancement.Builder.advancement() var obtainDirt = Advancement.Builder.advancement()
.display(Items.DIRT, .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") @Mod("permissiontest")
public class 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 Logger LOGGER = LogManager.getLogger();
private static final PermissionNode<Boolean> boolPerm = new PermissionNode<>("permissiontest", "test.blob", PermissionTypes.BOOLEAN, (player, playerUUID, context) -> true); 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 class LoginPacketSplitTest {
public static final Logger LOG = LogUtils.getLogger(); public static final Logger LOG = LogUtils.getLogger();
public static final String MOD_ID = "login_packet_split_test"; 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(); private static final Gson GSON = new Gson();
public static final ResourceKey<Registry<BigData>> BIG_DATA = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(MOD_ID, "big_data")); 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]] [[mods]]
modId="custom_armor_model_test" modId="custom_armor_model_test"
[[mods]] [[mods]]
modId="duplicate_optional_tag_test"
[[mods]]
modId="custom_particle_type_test" modId="custom_particle_type_test"
[[mods]] [[mods]]
modId="renderable_test" modId="renderable_test"
[[mods]] [[mods]]
modId="ingredient_invalidation" modId="ingredient_invalidation"
[[mods]] [[mods]]
modId="registry_codec_test"
[[mods]]
modId="deferred_holder_test"
[[mods]]
modId="gametest_test" modId="gametest_test"
[[mods]] [[mods]]
modId="many_mob_effects_test" modId="many_mob_effects_test"