mirror of
https://github.com/neoforged/NeoForge.git
synced 2025-12-10 00:22:25 -06:00
Test & error UX improvements (#2347)
This commit is contained in:
parent
0b63a7c7a1
commit
865eb559dc
2
.github/workflows/build-prs.yml
vendored
2
.github/workflows/build-prs.yml
vendored
@ -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
|
||||
|
||||
1
.github/workflows/check-local-changes.yml
vendored
1
.github/workflows/check-local-changes.yml
vendored
@ -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
|
||||
|
||||
1
.github/workflows/test-prs.yml
vendored
1
.github/workflows/test-prs.yml
vendored
@ -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
|
||||
|
||||
@ -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_);
|
||||
}
|
||||
|
||||
@ -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_);
|
||||
+ }
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 + "]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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"));
|
||||
|
||||
@ -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) -> {}));
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user