mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2026-11-01 20:14:27 +00:00
Merge branch 'mc-1.20.x' into mc-1.21.x
This commit is contained in:
47 files changed
+439
-247
No files matched your search
@@ -59,6 +59,16 @@ jobs:
|
||||
run: ./tools/parse-reports.py
|
||||
if: ${{ failure() }}
|
||||
|
||||
- name: 📤 Upload Test Reports
|
||||
uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: Tests
|
||||
path: |
|
||||
projects/*/build/test-results/test/open-test-report.xml
|
||||
projects/*/build/test-results/runGametest.xml
|
||||
retention-days: 1
|
||||
|
||||
build-core:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
@@ -33,3 +33,4 @@ jobs:
|
||||
with:
|
||||
name: Documentation
|
||||
path: ./projects/web/build/site/
|
||||
retention-days: 1
|
||||
@@ -172,6 +172,14 @@ tasks.test {
|
||||
testLogging {
|
||||
events("skipped", "failed")
|
||||
}
|
||||
|
||||
// Enable open test reporting
|
||||
jvmArgumentProviders.add {
|
||||
listOf(
|
||||
"-Djunit.platform.reporting.open.xml.enabled=true",
|
||||
"-Djunit.platform.reporting.output.dir=${reports.junitXml.outputLocation.getAbsolutePath()}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JacocoReport::class.java).configureEach {
|
||||
|
||||
@@ -54,13 +54,6 @@ val verifyClassesMatch = tasks.register<VerifyJavaCompile>("verifyClassesMatch")
|
||||
check("MissingOverride", CheckSeverity.OFF)
|
||||
check("LoaderOverride", CheckSeverity.DEFAULT)
|
||||
check("MissingLoaderOverride", CheckSeverity.DEFAULT)
|
||||
|
||||
// FIXME: We disable JSpecify in the common classes due to some NullAway bugs. As the two
|
||||
// modes are not compatible, we have to disable NullAway entirely here. This can be removed
|
||||
// when we bump to 0.14.3.
|
||||
check("NullAway", CheckSeverity.WARN)
|
||||
option("NullAway:JSpecifyMode", false)
|
||||
option("NullAway:JSpecifyExperimental", false)
|
||||
}
|
||||
|
||||
source = minecraftEmbeddedProjectResolved.map {
|
||||
|
||||
@@ -53,10 +53,11 @@ create-fabric = "6.0.7.0+mc1.20.1-build.1716"
|
||||
|
||||
# Testing
|
||||
hamcrest = "3.0"
|
||||
jmh = "1.37"
|
||||
jqwik = "1.10.1"
|
||||
junit = "6.1.3"
|
||||
junitPlatform = "6.1.3"
|
||||
jmh = "1.37"
|
||||
openTestReporting = "0.3.0"
|
||||
|
||||
# Build tools
|
||||
cctJavadoc = "1.9.0"
|
||||
@@ -71,7 +72,7 @@ illuaminate = "0.1.0-83-g1131f68"
|
||||
lwjgl = "3.4.3"
|
||||
minotaur = "2.9.0"
|
||||
modDevGradle = "2.0.147"
|
||||
nullAway = "0.14.2"
|
||||
nullAway = "0.14.3"
|
||||
shadow = "9.6.1"
|
||||
spotless = "8.10.4"
|
||||
teavm = "0.16.0"
|
||||
@@ -129,15 +130,17 @@ sodium-forge = { module = "maven.modrinth:sodium", version.ref = "sodium.forge"
|
||||
|
||||
# Testing
|
||||
hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
|
||||
jmh = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
|
||||
jmh-processor = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
|
||||
jqwik-api = { module = "net.jqwik:jqwik-api", version.ref = "jqwik" }
|
||||
jqwik-engine = { module = "net.jqwik:jqwik-engine", version.ref = "jqwik" }
|
||||
junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }
|
||||
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
|
||||
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
|
||||
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junitPlatform" }
|
||||
junit-platform-reporting = { module = "org.junit.platform:junit-platform-reporting", version.ref = "junitPlatform" }
|
||||
openTestReporting = { module = "org.opentest4j.reporting:open-test-reporting-events", version.ref = "openTestReporting" }
|
||||
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
|
||||
jmh = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
|
||||
jmh-processor = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
|
||||
|
||||
# LWJGL
|
||||
lwjgl-bom = { module = "org.lwjgl:lwjgl-bom", version.ref = "lwjgl" }
|
||||
@@ -192,7 +195,7 @@ externalMods-fabric-runtime = ["jei-fabric", "modmenu"]
|
||||
|
||||
# Testing
|
||||
test = ["junit-jupiter-api", "junit-jupiter-params", "hamcrest", "jqwik-api"]
|
||||
testRuntime = ["junit-jupiter-engine", "junit-platform-launcher", "jqwik-engine"]
|
||||
testRuntime = ["junit-jupiter-engine", "junit-platform-launcher", "junit-platform-reporting", "jqwik-engine"]
|
||||
|
||||
# Build tools
|
||||
teavm-api = ["teavm-jso", "teavm-jso-apis", "teavm-platform", "teavm-classlib", "teavm-extension-apis"]
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NullMarked
|
||||
@ApiStatus.Internal
|
||||
package dan200.computercraft.impl;
|
||||
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
|
||||
@@ -8,7 +8,6 @@ import cc.tweaked.gradle.MergeTrees
|
||||
import cc.tweaked.gradle.annotationProcessorEverywhere
|
||||
import cc.tweaked.vanillaextract.configurations.Capabilities.clientClasses
|
||||
import cc.tweaked.vanillaextract.configurations.Capabilities.commonClasses
|
||||
import net.ltgt.gradle.errorprone.errorprone
|
||||
|
||||
plugins {
|
||||
`java-test-fixtures`
|
||||
@@ -69,19 +68,12 @@ dependencies {
|
||||
testModCompileOnly(libs.mixin)
|
||||
testModImplementation(testFixtures(project(":core")))
|
||||
testModImplementation(testFixtures(project(":common")))
|
||||
testModImplementation(libs.openTestReporting)
|
||||
testModImplementation(libs.bundles.kotlin)
|
||||
|
||||
"cctJavadoc"(libs.cctJavadoc)
|
||||
}
|
||||
|
||||
// TODO: Enable JSpecify in the common classes. Unfortunately there's several bugs blocking us right now.
|
||||
tasks.compileJava {
|
||||
options.errorprone {
|
||||
option("NullAway:JSpecifyMode", false)
|
||||
option("NullAway:JSpecifyExperimental", false)
|
||||
}
|
||||
}
|
||||
|
||||
illuaminate {
|
||||
version = libs.versions.illuaminate
|
||||
}
|
||||
|
||||
+2
-3
@@ -7,7 +7,6 @@ package dan200.computercraft.client.integration;
|
||||
import dan200.computercraft.client.render.RenderTypes;
|
||||
import dan200.computercraft.client.render.text.DirectFixedWidthFontRenderer;
|
||||
import dan200.computercraft.client.render.vbo.DirectVertexBuffer;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Optional;
|
||||
@@ -49,10 +48,10 @@ public class ShaderMod {
|
||||
}
|
||||
|
||||
private static final class Storage {
|
||||
static final ShaderMod INSTANCE = Nullability.assertNonNull(ServiceLoader.load(Provider.class)
|
||||
static final ShaderMod INSTANCE = ServiceLoader.load(Provider.class)
|
||||
.stream()
|
||||
.flatMap(x -> x.get().get().stream())
|
||||
.findFirst()
|
||||
.orElseGet(ShaderMod::new));
|
||||
.orElseGet(ShaderMod::new);
|
||||
}
|
||||
}
|
||||
@@ -79,7 +79,7 @@ public abstract class AbstractUpgradeHolder<U extends UpgradeBase> {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public final ItemStack getItem() {
|
||||
public final @Unmodifiable ItemStack getItem() {
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ final class UpgradeHandleImpl<U extends UpgradeBase> implements UpgradeHandle<U>
|
||||
);
|
||||
}
|
||||
|
||||
if (ItemStack.isSameItemSameComponents(holder.getItem(), newItem)) return;
|
||||
if (ItemStack.matches(holder.getItem(), newItem)) return;
|
||||
|
||||
if (!upgrade.value().getCraftingIngredient().test(newItem) || !upgrade.value().isItemSuitable(newItem)) {
|
||||
throw new IllegalArgumentException(
|
||||
|
||||
@@ -483,7 +483,7 @@ public final class ModRegistry {
|
||||
static {
|
||||
register("tracking_field", TrackingFieldArgumentType.class, TrackingFieldArgumentType.metric());
|
||||
register("computer", ComputerArgumentType.class, ComputerArgumentType.get());
|
||||
registerUnsafe("repeat", RepeatArgumentType.class, new RepeatArgumentType.TypeInfo());
|
||||
registerUnsafe("repeat", RepeatArgumentType.class, RepeatArgumentType.type());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-3
@@ -13,13 +13,14 @@ import net.minecraft.commands.synchronization.ArgumentTypeInfo;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Utilities for working with arguments.
|
||||
*
|
||||
* @see net.minecraft.commands.synchronization.ArgumentUtils
|
||||
*/
|
||||
public class ArgumentUtils {
|
||||
class ArgumentUtils {
|
||||
public static <A extends ArgumentType<?>> JsonObject serializeToJson(ArgumentTypeInfo.Template<A> template) {
|
||||
var object = new JsonObject();
|
||||
object.addProperty("type", "argument");
|
||||
@@ -33,7 +34,9 @@ public class ArgumentUtils {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <A extends ArgumentType<?>, T extends ArgumentTypeInfo.Template<A>> void serializeToJson(JsonObject jsonObject, ArgumentTypeInfo<A, T> argumentTypeInfo, ArgumentTypeInfo.Template<A> template) {
|
||||
private static <A extends ArgumentType<?>, T extends ArgumentTypeInfo.@Nullable Template<A>> void serializeToJson(
|
||||
JsonObject jsonObject, ArgumentTypeInfo<A, T> argumentTypeInfo, ArgumentTypeInfo.Template<A> template
|
||||
) {
|
||||
argumentTypeInfo.serializeToJson((T) template, jsonObject);
|
||||
}
|
||||
|
||||
@@ -42,7 +45,9 @@ public class ArgumentUtils {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <A extends ArgumentType<?>, T extends ArgumentTypeInfo.Template<A>> void serializeToNetwork(FriendlyByteBuf buffer, ArgumentTypeInfo<A, T> type, ArgumentTypeInfo.Template<A> template) {
|
||||
private static <A extends ArgumentType<?>, T extends ArgumentTypeInfo.@Nullable Template<A>> void serializeToNetwork(
|
||||
FriendlyByteBuf buffer, ArgumentTypeInfo<A, T> type, ArgumentTypeInfo.Template<A> template
|
||||
) {
|
||||
buffer.writeVarInt(BuiltInRegistries.COMMAND_ARGUMENT_TYPE.getIdOrThrow(type));
|
||||
type.serializeToNetwork((T) template, buffer);
|
||||
}
|
||||
|
||||
+1
-1
@@ -353,7 +353,7 @@ public record ComputerSelector(
|
||||
CompletableFuture<Suggestions> suggest(CommandContext<?> source, SuggestionsBuilder builder);
|
||||
}
|
||||
|
||||
private static SuggestionProvider suggestComputers(Function<ServerComputer, String> renderer) {
|
||||
private static SuggestionProvider suggestComputers(Function<ServerComputer, @Nullable String> renderer) {
|
||||
return (anyContext, builder) -> suggestOnServer(anyContext, context -> {
|
||||
var remaining = builder.getRemaining();
|
||||
for (var computer : ServerContext.get(context.getSource().getServer()).registry().getComputers()) {
|
||||
|
||||
+27
-37
@@ -25,38 +25,26 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
* Reads one argument multiple times.
|
||||
* <p>
|
||||
* Note that this must be the last element in an argument chain: in order to improve the quality of error messages,
|
||||
* we will always try to consume another argument while there is input remaining.
|
||||
* <p>
|
||||
* One problem with how parsers function, is that they must consume some input: and thus we
|
||||
*
|
||||
* @param <T> The type of each value returned
|
||||
* @param <U> The type of the inner parser. This will normally be a {@link List} or {@code T}.
|
||||
* @param <T> The type of each value returned.
|
||||
*/
|
||||
public final class RepeatArgumentType<T, U> implements ArgumentType<List<T>> {
|
||||
private final ArgumentType<U> child;
|
||||
private final BiConsumer<List<T>, U> appender;
|
||||
private final boolean flatten;
|
||||
public final class RepeatArgumentType<T> implements ArgumentType<List<T>> {
|
||||
private final ArgumentType<T> child;
|
||||
private final SimpleCommandExceptionType some;
|
||||
|
||||
private RepeatArgumentType(ArgumentType<U> child, BiConsumer<List<T>, U> appender, boolean flatten, SimpleCommandExceptionType some) {
|
||||
private RepeatArgumentType(ArgumentType<T> child, SimpleCommandExceptionType some) {
|
||||
this.child = child;
|
||||
this.appender = appender;
|
||||
this.flatten = flatten;
|
||||
this.some = some;
|
||||
}
|
||||
|
||||
public static <T> RepeatArgumentType<T, T> some(ArgumentType<T> appender, SimpleCommandExceptionType missing) {
|
||||
return new RepeatArgumentType<>(appender, List::add, false, missing);
|
||||
}
|
||||
|
||||
public static <T> RepeatArgumentType<T, List<T>> someFlat(ArgumentType<List<T>> appender, SimpleCommandExceptionType missing) {
|
||||
return new RepeatArgumentType<>(appender, List::addAll, true, missing);
|
||||
public static <T> RepeatArgumentType<T> some(ArgumentType<T> appender, SimpleCommandExceptionType missing) {
|
||||
return new RepeatArgumentType<>(appender, missing);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,7 +56,7 @@ public final class RepeatArgumentType<T, U> implements ArgumentType<List<T>> {
|
||||
if (!reader.canRead()) break;
|
||||
|
||||
var startParse = reader.getCursor();
|
||||
appender.accept(out, child.parse(reader));
|
||||
out.add(child.parse(reader));
|
||||
hadSome = true;
|
||||
|
||||
if (reader.getCursor() == startParse) {
|
||||
@@ -111,47 +99,49 @@ public final class RepeatArgumentType<T, U> implements ArgumentType<List<T>> {
|
||||
return child.getExamples();
|
||||
}
|
||||
|
||||
public static class TypeInfo implements ArgumentTypeInfo<RepeatArgumentType<?, ?>, Template> {
|
||||
public static <T> ArgumentTypeInfo<RepeatArgumentType<T>, ? extends ArgumentTypeInfo.Template<RepeatArgumentType<T>>> type() {
|
||||
return new TypeInfo<>();
|
||||
}
|
||||
|
||||
private static final class TypeInfo<T> implements ArgumentTypeInfo<RepeatArgumentType<T>, Template<T>> {
|
||||
@Override
|
||||
public void serializeToNetwork(RepeatArgumentType.Template arg, FriendlyByteBuf buf) {
|
||||
buf.writeBoolean(arg.flatten);
|
||||
public void serializeToNetwork(RepeatArgumentType.Template<T> arg, FriendlyByteBuf buf) {
|
||||
ArgumentUtils.serializeToNetwork(buf, arg.child);
|
||||
ComponentSerialization.TRUSTED_CONTEXT_FREE_STREAM_CODEC.encode(buf, ArgumentUtils.getMessage(arg.some));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepeatArgumentType.Template deserializeFromNetwork(FriendlyByteBuf buf) {
|
||||
var isList = buf.readBoolean();
|
||||
var child = ArgumentUtils.deserialize(buf);
|
||||
public RepeatArgumentType.Template<T> deserializeFromNetwork(FriendlyByteBuf buf) {
|
||||
@SuppressWarnings("unchecked")
|
||||
var child = (ArgumentTypeInfo.Template<ArgumentType<T>>) ArgumentUtils.deserialize(buf);
|
||||
|
||||
var message = ComponentSerialization.TRUSTED_CONTEXT_FREE_STREAM_CODEC.decode(buf);
|
||||
return new RepeatArgumentType.Template(this, child, isList, new SimpleCommandExceptionType(message));
|
||||
return new RepeatArgumentType.Template<>(this, child, new SimpleCommandExceptionType(message));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RepeatArgumentType.Template unpack(RepeatArgumentType<?, ?> argumentType) {
|
||||
return new RepeatArgumentType.Template(this, ArgumentTypeInfos.unpack(argumentType.child), argumentType.flatten, argumentType.some);
|
||||
public RepeatArgumentType.Template<T> unpack(RepeatArgumentType<T> argumentType) {
|
||||
return new RepeatArgumentType.Template<T>(this, ArgumentTypeInfos.unpack(argumentType.child), argumentType.some);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serializeToJson(RepeatArgumentType.Template arg, JsonObject json) {
|
||||
json.addProperty("flatten", arg.flatten);
|
||||
public void serializeToJson(RepeatArgumentType.Template<T> arg, JsonObject json) {
|
||||
json.add("child", ArgumentUtils.serializeToJson(arg.child));
|
||||
json.addProperty("error", Component.Serializer.toJson(ArgumentUtils.getMessage(arg.some), RegistryAccess.EMPTY));
|
||||
}
|
||||
}
|
||||
|
||||
public record Template(
|
||||
TypeInfo info, ArgumentTypeInfo.Template<?> child, boolean flatten, SimpleCommandExceptionType some
|
||||
) implements ArgumentTypeInfo.Template<RepeatArgumentType<?, ?>> {
|
||||
private record Template<T>(
|
||||
TypeInfo<T> info, ArgumentTypeInfo.Template<ArgumentType<T>> child, SimpleCommandExceptionType some
|
||||
) implements ArgumentTypeInfo.Template<RepeatArgumentType<T>> {
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public RepeatArgumentType<?, ?> instantiate(CommandBuildContext commandBuildContext) {
|
||||
public RepeatArgumentType<T> instantiate(CommandBuildContext commandBuildContext) {
|
||||
var child = child().instantiate(commandBuildContext);
|
||||
return flatten ? RepeatArgumentType.someFlat((ArgumentType) child, some()) : RepeatArgumentType.some(child, some());
|
||||
return RepeatArgumentType.some(child, some());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArgumentTypeInfo<RepeatArgumentType<?, ?>, ?> type() {
|
||||
public ArgumentTypeInfo<RepeatArgumentType<T>, ?> type() {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-12
@@ -17,7 +17,6 @@ import org.jspecify.annotations.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static dan200.computercraft.shared.command.Exceptions.ARGUMENT_EXPECTED;
|
||||
import static dan200.computercraft.shared.command.builder.HelpingArgumentBuilder.literal;
|
||||
@@ -58,27 +57,19 @@ public class CommandBuilder<S> implements CommandNodeBuilder<S, Command<S>> {
|
||||
}
|
||||
|
||||
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyValue(String name, ArgumentType<T> type, List<T> empty) {
|
||||
return argMany(name, type, () -> empty);
|
||||
return argMany(name, RepeatArgumentType.some(type, ARGUMENT_EXPECTED), empty);
|
||||
}
|
||||
|
||||
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyValue(String name, ArgumentType<T> type, T defaultValue) {
|
||||
return argManyValue(name, type, List.of(defaultValue));
|
||||
}
|
||||
|
||||
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argMany(String name, ArgumentType<T> type, Supplier<List<T>> empty) {
|
||||
return argMany(name, RepeatArgumentType.some(type, ARGUMENT_EXPECTED), empty);
|
||||
}
|
||||
|
||||
public <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argManyFlatten(String name, ArgumentType<List<T>> type, Supplier<List<T>> empty) {
|
||||
return argMany(name, RepeatArgumentType.someFlat(type, ARGUMENT_EXPECTED), empty);
|
||||
}
|
||||
|
||||
private <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argMany(String name, RepeatArgumentType<T, ?> type, Supplier<List<T>> empty) {
|
||||
private <T> CommandNodeBuilder<S, ArgCommand<S, List<T>>> argMany(String name, RepeatArgumentType<T> type, List<T> empty) {
|
||||
if (args.isEmpty()) throw new IllegalStateException("Cannot have empty arg chain builder");
|
||||
|
||||
return command -> {
|
||||
// The node for no arguments
|
||||
var tail = setupTail(ctx -> command.run(ctx, empty.get()));
|
||||
var tail = setupTail(ctx -> command.run(ctx, empty));
|
||||
|
||||
// The node for one or more arguments
|
||||
ArgumentBuilder<S, ?> moreArg = RequiredArgumentBuilder
|
||||
|
||||
+3
-2
@@ -148,13 +148,14 @@ class AddressRuleConfig {
|
||||
}
|
||||
|
||||
private static <T extends Enum<T>> T parseEnum(String field, Class<T> klass, String x) {
|
||||
for (var value : klass.getEnumConstants()) {
|
||||
var values = Objects.requireNonNull(klass.getEnumConstants());
|
||||
for (var value : values) {
|
||||
if (value.name().equalsIgnoreCase(x)) return value;
|
||||
}
|
||||
|
||||
throw new InvalidRuleException(String.format(
|
||||
"Field '%s' should be one of %s, but is '%s'.",
|
||||
field, Arrays.stream(klass.getEnumConstants()).map(Enum::name).toList(), x
|
||||
field, Arrays.stream(values).map(Enum::name).toList(), x
|
||||
));
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -81,7 +81,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity imp
|
||||
private boolean recordPlaying = false;
|
||||
// In order to avoid main-thread calls in the peripheral, we set flags to mark which operation should be performed,
|
||||
// then read them when ticking.
|
||||
private final AtomicReference<RecordCommand> recordQueued = new AtomicReference<>(null);
|
||||
private final AtomicReference<@Nullable RecordCommand> recordQueued = new AtomicReference<>(null);
|
||||
private final AtomicBoolean ejectQueued = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
@@ -172,7 +172,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity imp
|
||||
*/
|
||||
private synchronized void updateMedia() {
|
||||
var newStack = getDiskStack();
|
||||
if (ItemStack.isSameItemSameComponents(newStack, media.stack())) return;
|
||||
if (ItemStack.matches(newStack, media.stack())) return;
|
||||
|
||||
var newMedia = MediaStack.of(newStack);
|
||||
|
||||
@@ -243,7 +243,7 @@ public final class DiskDriveBlockEntity extends AbstractContainerBlockEntity imp
|
||||
*/
|
||||
@GuardedBy("this")
|
||||
private void updateMediaStack(ItemStack stack, boolean immediate) {
|
||||
if (ItemStack.isSameItemSameComponents(media.stack(), stack)) return;
|
||||
if (ItemStack.matches(media.stack(), stack)) return;
|
||||
media = new MediaStack(stack, media.media());
|
||||
|
||||
if (immediate) {
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.MethodResult;
|
||||
import dan200.computercraft.api.peripheral.IComputerAccess;
|
||||
import dan200.computercraft.core.methods.PeripheralMethod;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link PeripheralMethod} along with the method's target.
|
||||
@@ -34,7 +35,7 @@ final class SaturatedMethod {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (!(obj instanceof SaturatedMethod other)) return false;
|
||||
|
||||
|
||||
+4
-4
@@ -389,7 +389,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
public synchronized @Nullable String mount(String desiredLocation, Mount mount) {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
var mounted = computer.mount(desiredLocation, mount, name);
|
||||
mounts.add(mounted);
|
||||
if (mounted != null) mounts.add(mounted);
|
||||
return mounted;
|
||||
}
|
||||
|
||||
@@ -397,7 +397,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
public synchronized @Nullable String mount(String desiredLocation, Mount mount, String driveName) {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
var mounted = computer.mount(desiredLocation, mount, driveName);
|
||||
mounts.add(mounted);
|
||||
if (mounted != null) mounts.add(mounted);
|
||||
return mounted;
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
public synchronized @Nullable String mountWritable(String desiredLocation, WritableMount mount) {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
var mounted = computer.mountWritable(desiredLocation, mount, name);
|
||||
mounts.add(mounted);
|
||||
if (mounted != null) mounts.add(mounted);
|
||||
return mounted;
|
||||
}
|
||||
|
||||
@@ -413,7 +413,7 @@ public abstract class WiredModemPeripheral extends ModemPeripheral implements Wi
|
||||
public synchronized @Nullable String mountWritable(String desiredLocation, WritableMount mount, String driveName) {
|
||||
if (!attached) throw new NotAttachedException();
|
||||
var mounted = computer.mountWritable(desiredLocation, mount, driveName);
|
||||
mounts.add(mounted);
|
||||
if (mounted != null) mounts.add(mounted);
|
||||
return mounted;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ public final class CopyComponents implements RecipeFunction {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<?> getType() {
|
||||
public Type<CopyComponents> getType() {
|
||||
return ModRegistry.RecipeFunctions.COPY_COMPONENTS.get();
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ public interface RecipeFunction {
|
||||
/**
|
||||
* The registry where {@link RecipeFunction}s are registered.
|
||||
*/
|
||||
ResourceKey<Registry<Type<?>>> REGISTRY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "recipe_function"));
|
||||
ResourceKey<Registry<Type<? extends RecipeFunction>>> REGISTRY = ResourceKey.createRegistryKey(ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "recipe_function"));
|
||||
|
||||
/**
|
||||
* The codec to read and write {@link RecipeFunction}s with.
|
||||
@@ -66,7 +66,7 @@ public interface RecipeFunction {
|
||||
*
|
||||
* @return The type of this recipe function.
|
||||
*/
|
||||
Type<?> getType();
|
||||
Type<? extends RecipeFunction> getType();
|
||||
|
||||
/**
|
||||
* Apply this recipe function, modifying the result item.
|
||||
|
||||
+1
-1
@@ -740,7 +740,7 @@ public class TurtleBrain implements TurtleAccessInternal {
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodResult resume(Object[] response) {
|
||||
public MethodResult resume(@Nullable Object[] response) {
|
||||
if (response.length < 3 || !(response[1] instanceof Number id) || !(response[2] instanceof Boolean)) {
|
||||
return pull;
|
||||
}
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ class UpgradeContainer implements Container {
|
||||
|
||||
private final ITurtleAccess turtle;
|
||||
|
||||
private final List<UpgradeData<ITurtleUpgrade>> lastUpgrade = Arrays.asList(null, null);
|
||||
private final List<@Nullable UpgradeData<ITurtleUpgrade>> lastUpgrade = Arrays.asList(null, null);
|
||||
private final NonNullList<ItemStack> lastStack = NonNullList.withSize(2, ItemStack.EMPTY);
|
||||
|
||||
UpgradeContainer(ITurtleAccess turtle) {
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ public class TurtleTool extends AbstractTurtleUpgrade.StackAware {
|
||||
if (!getCraftingIngredient().test(stack) || !isItemSuitable(stack)) return;
|
||||
|
||||
// Early return if the item hasn't changed to avoid redundant syncs with the client.
|
||||
if (ItemStack.isSameItemSameComponents(stack, oldStack)) return;
|
||||
if (ItemStack.matches(stack, oldStack)) return;
|
||||
|
||||
upgrade.setItem(stack);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class IDAssigner {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(IDAssigner.class);
|
||||
@@ -83,7 +84,7 @@ public final class IDAssigner {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Files.createDirectories(idFile.getParent());
|
||||
Files.createDirectories(Objects.requireNonNull(idFile.getParent()));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Cannot create owning directory, IDs will not be persisted", e);
|
||||
}
|
||||
|
||||
@@ -51,15 +51,7 @@ public final class NBTUtil {
|
||||
case Tag.TAG_BYTE, Tag.TAG_SHORT, Tag.TAG_INT, Tag.TAG_LONG -> ((NumericTag) tag).getAsLong();
|
||||
case Tag.TAG_FLOAT, Tag.TAG_DOUBLE -> ((NumericTag) tag).getAsDouble();
|
||||
case Tag.TAG_STRING -> tag.getAsString();
|
||||
case Tag.TAG_COMPOUND -> {
|
||||
var compound = (CompoundTag) tag;
|
||||
Map<String, Object> map = new HashMap<>(compound.size());
|
||||
for (var key : compound.getAllKeys()) {
|
||||
var value = toLua(compound.get(key));
|
||||
if (value != null) map.put(key, value);
|
||||
}
|
||||
yield map;
|
||||
}
|
||||
case Tag.TAG_COMPOUND -> toLua((CompoundTag) tag);
|
||||
case Tag.TAG_LIST -> ((ListTag) tag).stream().map(NBTUtil::toLua).toList();
|
||||
case Tag.TAG_BYTE_ARRAY -> {
|
||||
var array = ((ByteArrayTag) tag).getAsByteArray();
|
||||
@@ -73,6 +65,15 @@ public final class NBTUtil {
|
||||
};
|
||||
}
|
||||
|
||||
public static Map<String, Object> toLua(CompoundTag tag) {
|
||||
Map<String, Object> map = new HashMap<>(tag.size());
|
||||
for (var key : tag.getAllKeys()) {
|
||||
var value = toLua(tag.get(key));
|
||||
if (value != null) map.put(key, value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getNBTHash(@Nullable Tag tag) {
|
||||
if (tag == null) return null;
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ public class ItemStackMatcher extends TypeSafeMatcher<ItemStack> {
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(ItemStack item) {
|
||||
return ItemStack.isSameItemSameComponents(item, stack) && item.getCount() == stack.getCount();
|
||||
return ItemStack.matches(item, stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -118,7 +118,7 @@ class Recipe_Test {
|
||||
for (recipe in recipes) {
|
||||
// Ensure the result matches the input.
|
||||
val result = recipe.getResultItem(context.level.registryAccess())
|
||||
if (!ItemStack.isSameItemSameComponents(result, item) || result.count != item.count) {
|
||||
if (!ItemStack.matches(result, item)) {
|
||||
context.fail("Expected to craft ${item.toStringFull()} but crafted ${result.toStringFull()}")
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -377,7 +377,7 @@ fun GameTestHelper.assertCraftable(items: List<ItemStack>, result: ItemStack, wi
|
||||
|
||||
val actualResult = recipe.get().value.assemble(input, level.registryAccess())
|
||||
|
||||
if (!ItemStack.isSameItemSameComponents(actualResult, result) || actualResult.count != result.count) {
|
||||
if (!ItemStack.matches(actualResult, result)) {
|
||||
fail("Expected $items to craft ${result.toStringFull()}, got ${actualResult.toStringFull()}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ object TestHooks {
|
||||
var structureManager: StructureTemplateManager? = null
|
||||
|
||||
@JvmStatic
|
||||
fun init() {
|
||||
fun init(loader: String) {
|
||||
ServerContext.luaMachine = ManagedComputers
|
||||
ComputerCraftAPI.registerAPIFactory(::TestAPI)
|
||||
StructureUtils.testStructuresDir = sourceDir.resolve("structures").toString()
|
||||
@@ -59,7 +59,7 @@ object TestHooks {
|
||||
try {
|
||||
GlobalTestReporter.replaceWith(
|
||||
MultiTestReporter(
|
||||
JunitTestReporter(File(outputPath)),
|
||||
OtrTestReporter("$loader gametests", File(outputPath)),
|
||||
LogTestReporter(),
|
||||
),
|
||||
)
|
||||
|
||||
+48
-63
@@ -4,22 +4,19 @@
|
||||
|
||||
package dan200.computercraft.gametest.core
|
||||
|
||||
import com.google.common.base.Stopwatch
|
||||
import net.minecraft.gametest.framework.GameTestInfo
|
||||
import net.minecraft.gametest.framework.JUnitLikeTestReporter
|
||||
import net.minecraft.gametest.framework.TestReporter
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Element
|
||||
import java.io.*
|
||||
import java.nio.file.Files.createDirectories
|
||||
import org.opentest4j.reporting.events.api.DocumentWriter
|
||||
import org.opentest4j.reporting.events.api.NamespaceRegistry
|
||||
import org.opentest4j.reporting.events.core.CoreFactory
|
||||
import org.opentest4j.reporting.events.java.JavaFactory
|
||||
import org.opentest4j.reporting.events.root.Events
|
||||
import org.opentest4j.reporting.events.root.Finished
|
||||
import org.opentest4j.reporting.events.root.RootFactory
|
||||
import org.opentest4j.reporting.schema.Namespace
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.xml.parsers.DocumentBuilderFactory
|
||||
import javax.xml.transform.TransformerException
|
||||
import javax.xml.transform.TransformerFactory
|
||||
import javax.xml.transform.dom.DOMSource
|
||||
import javax.xml.transform.stream.StreamResult
|
||||
import org.opentest4j.reporting.events.core.Result as OtrResult
|
||||
|
||||
/**
|
||||
* A test reporter which delegates to a list of other reporters.
|
||||
@@ -41,74 +38,62 @@ class MultiTestReporter(private val reporters: List<TestReporter>) : TestReporte
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports tests to a JUnit XML file. This is equivalent to [JUnitLikeTestReporter], except it ensures the destination
|
||||
* directory exists and includes the stack trace in the error.
|
||||
* A test reporter that writes [Open Test Reporting](https://github.com/ota4j-team/open-test-reporting/) test reports.
|
||||
*
|
||||
* @param name The name of this test suite.
|
||||
* @param destination The destination file to write to.
|
||||
*/
|
||||
open class JunitTestReporter(private val destination: File) : TestReporter {
|
||||
private val document: Document
|
||||
private val testSuite: Element
|
||||
private val stopwatch: Stopwatch = Stopwatch.createStarted()
|
||||
class OtrTestReporter(name: String, destination: File) : TestReporter {
|
||||
private val writer: DocumentWriter<Events>
|
||||
private var hasErrors = false
|
||||
|
||||
init {
|
||||
document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
|
||||
val namespaceRegistry = NamespaceRegistry.builder(Namespace.REPORTING_CORE)
|
||||
.add("e", Namespace.REPORTING_EVENTS)
|
||||
.add("java", Namespace.REPORTING_JAVA)
|
||||
.build()
|
||||
|
||||
testSuite = document.createElement("testsuite")
|
||||
testSuite.setAttribute("timestamp", DateTimeFormatter.ISO_INSTANT.format(Instant.now()))
|
||||
writer = Events.createDocumentWriter(namespaceRegistry, destination.toPath())
|
||||
|
||||
val container = document.createElement("testsuite")
|
||||
container.appendChild(testSuite)
|
||||
document.appendChild(container)
|
||||
writer.append(RootFactory.started(SUITE_ID, Instant.now(), name))
|
||||
}
|
||||
|
||||
private fun createTestCase(testInfo: GameTestInfo, name: String?): Element {
|
||||
val testCase = document.createElement("testcase")
|
||||
testCase.setAttribute("name", name)
|
||||
testCase.setAttribute("classname", testInfo.structureName)
|
||||
testCase.setAttribute("time", (testInfo.runTime.toDouble() / 1000.0).toString())
|
||||
testSuite.appendChild(testCase)
|
||||
return testCase
|
||||
private fun report(testInfo: GameTestInfo, configure: (Finished) -> Unit) {
|
||||
val finished = Instant.now()
|
||||
val started = finished.minusMillis(testInfo.runTime)
|
||||
writer.append(RootFactory.started(testInfo.testName, started, testInfo.testName)) {
|
||||
it.withParentId(SUITE_ID)
|
||||
}
|
||||
writer.append(RootFactory.finished(testInfo.testName, finished), configure)
|
||||
}
|
||||
|
||||
override fun onTestFailed(testInfo: GameTestInfo) {
|
||||
val error = testInfo.error!!
|
||||
val result: Element
|
||||
if (testInfo.isRequired) {
|
||||
result = document.createElement("failure")
|
||||
result.setAttribute("message", error.message)
|
||||
result.setAttribute("type", error.javaClass.name)
|
||||
report(testInfo) { report ->
|
||||
if (testInfo.isRequired) hasErrors = true
|
||||
|
||||
val writer = StringWriter()
|
||||
error.printStackTrace(PrintWriter(writer))
|
||||
result.textContent = writer.toString()
|
||||
} else {
|
||||
result = document.createElement("skipped")
|
||||
result.setAttribute("message", error.message)
|
||||
val status = if (testInfo.isRequired) OtrResult.Status.FAILED else OtrResult.Status.ABORTED
|
||||
report.append(CoreFactory.result(status)) { result ->
|
||||
val error = testInfo.error
|
||||
if (error != null) {
|
||||
result.append(CoreFactory.reason(error.message ?: "unknown"))
|
||||
result.append(JavaFactory.throwable(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createTestCase(testInfo, testInfo.testName).appendChild(result)
|
||||
}
|
||||
|
||||
override fun onTestSuccess(testInfo: GameTestInfo) {
|
||||
createTestCase(testInfo, testInfo.testName)
|
||||
report(testInfo) { it.append(CoreFactory.result(OtrResult.Status.SUCCESSFUL)) }
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
stopwatch.stop()
|
||||
testSuite.setAttribute(
|
||||
"time",
|
||||
(stopwatch.elapsed(TimeUnit.MILLISECONDS).toDouble() / 1000.0).toString(),
|
||||
)
|
||||
|
||||
try {
|
||||
try {
|
||||
createDirectories(destination.toPath().parent)
|
||||
} catch (e: IOException) {
|
||||
throw UncheckedIOException("Failed to create parent directory", e)
|
||||
}
|
||||
|
||||
TransformerFactory.newInstance().newTransformer().transform(DOMSource(document), StreamResult(destination))
|
||||
} catch (transformerException: TransformerException) {
|
||||
throw Error("Couldn't save test report", transformerException)
|
||||
writer.append(RootFactory.finished(SUITE_ID, Instant.now())) {
|
||||
it.append(CoreFactory.result(if (hasErrors) OtrResult.Status.FAILED else OtrResult.Status.SUCCESSFUL))
|
||||
}
|
||||
writer.close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SUITE_ID = "_gametests_"
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import dan200.computercraft.api.lua.IArguments;
|
||||
import dan200.computercraft.api.lua.ILuaAPI;
|
||||
import dan200.computercraft.api.lua.LuaException;
|
||||
import dan200.computercraft.api.lua.LuaFunction;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.core.util.StringUtil;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
@@ -430,7 +429,7 @@ public class OSAPI implements ILuaAPI {
|
||||
@LuaFunction
|
||||
public final Object date(Optional<String> formatA, Optional<Long> timeA) throws LuaException {
|
||||
var format = formatA.orElse("%c");
|
||||
long time = Nullability.assertNonNull(timeA.orElseGet(() -> Instant.now().getEpochSecond()));
|
||||
long time = timeA.orElseGet(() -> Instant.now().getEpochSecond());
|
||||
|
||||
var instant = Instant.ofEpochSecond(time);
|
||||
ZonedDateTime date;
|
||||
|
||||
@@ -86,7 +86,6 @@ public abstract class Resource<R extends Resource<R, S>, S extends Resource.Reso
|
||||
return state.lock();
|
||||
}
|
||||
|
||||
@SuppressWarnings("NullAway") // FIXME: NullAway doesn't support generics.
|
||||
@CheckReturnValue
|
||||
private boolean setState(LockedReference.ReadWriteHandle<@Nullable S> stateHandle, @Nullable S newState) {
|
||||
var oldState = stateHandle.get();
|
||||
|
||||
@@ -27,7 +27,7 @@ final class VarargArguments implements IArguments {
|
||||
private volatile boolean closed;
|
||||
private final VarargArguments root;
|
||||
|
||||
private @Nullable ArraySlice<Object> cache;
|
||||
private @Nullable ArraySlice<@Nullable Object> cache;
|
||||
private @Nullable ArraySlice<String> typeNames;
|
||||
|
||||
private boolean escapes;
|
||||
@@ -230,14 +230,12 @@ final class VarargArguments implements IArguments {
|
||||
}
|
||||
|
||||
@SuppressWarnings("ArrayRecordComponent")
|
||||
private record ArraySlice<T>(@Nullable T[] array, int offset) {
|
||||
// FIXME: We should be able to remove the @Nullables if we update NullAway.
|
||||
|
||||
@Nullable T get(int index) {
|
||||
private record ArraySlice<T extends @Nullable Object>(T[] array, int offset) {
|
||||
T get(int index) {
|
||||
return array[offset + index];
|
||||
}
|
||||
|
||||
void set(int index, @Nullable T value) {
|
||||
void set(int index, T value) {
|
||||
array[offset + index] = value;
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -10,7 +10,6 @@ import com.electronwill.nightconfig.core.file.FileNotFoundAction;
|
||||
import com.electronwill.nightconfig.core.file.FileWatcher;
|
||||
import com.electronwill.nightconfig.core.io.WritingMode;
|
||||
import com.google.errorprone.annotations.concurrent.GuardedBy;
|
||||
import dan200.computercraft.core.util.Nullability;
|
||||
import dan200.computercraft.shared.config.ConfigFile;
|
||||
import org.apache.commons.lang3.function.TriFunction;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -59,7 +58,7 @@ public final class FabricConfigFile extends ConfigFile {
|
||||
|
||||
closeConfig();
|
||||
|
||||
var path = Nullability.assertNonNull(Arrays.stream(paths).filter(Files::exists).findFirst().orElseGet(() -> paths[paths.length - 1]));
|
||||
var path = Arrays.stream(paths).filter(Files::exists).findFirst().orElseGet(() -> paths[paths.length - 1]);
|
||||
var config = this.config = CommentedFileConfig.builder(path).sync()
|
||||
.onFileNotFound(FileNotFoundAction.READ_NOTHING)
|
||||
.writingMode(WritingMode.REPLACE)
|
||||
|
||||
-1
@@ -246,7 +246,6 @@ public class PlatformHelperImpl implements PlatformHelper {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullAway") // NullAway doesn't like the null here.
|
||||
public @Nullable IMedia getMedia(ItemStack stack) {
|
||||
return MediaLookup.get().find(stack, null);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import net.minecraft.resources.ResourceLocation;
|
||||
public class TestMod implements ModInitializer, ClientModInitializer {
|
||||
@Override
|
||||
public void onInitialize() {
|
||||
TestHooks.init();
|
||||
TestHooks.init("Fabric");
|
||||
|
||||
var phase = ResourceLocation.fromNamespaceAndPath(ComputerCraftAPI.MOD_ID, "test_mod");
|
||||
ServerLifecycleEvents.SERVER_STARTED.addPhaseOrdering(Event.DEFAULT_PHASE, phase);
|
||||
|
||||
@@ -214,6 +214,7 @@ dependencies {
|
||||
// Ensure our test fixture dependencies are on the classpath
|
||||
"testAdditionalRuntimeClasspath"(libs.bundles.kotlin)
|
||||
"testAdditionalRuntimeClasspath"(libs.bundles.test)
|
||||
"testAdditionalRuntimeClasspath"(libs.openTestReporting)
|
||||
|
||||
"testWithIris"(libs.iris.forge)
|
||||
"testWithIris"(libs.sodium.forge)
|
||||
|
||||
@@ -22,7 +22,7 @@ import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
@Mod("cctest")
|
||||
public class TestMod {
|
||||
public TestMod(IEventBus modBus) {
|
||||
TestHooks.init();
|
||||
TestHooks.init("Forge");
|
||||
|
||||
var bus = NeoForge.EVENT_BUS;
|
||||
bus.addListener(EventPriority.LOW, (ServerStartedEvent e) -> TestHooks.onServerStarted(e.getServer()));
|
||||
|
||||
@@ -72,7 +72,7 @@ public class Builder {
|
||||
|
||||
// Then finally start the compiler!
|
||||
run(output, remapper, TeaVMTargetType.JAVASCRIPT, TeaVMOptimizationLevel.ADVANCED, minify);
|
||||
run(output, remapper, TeaVMTargetType.WEBASSEMBLY_GC, TeaVMOptimizationLevel.SIMPLE, minify);
|
||||
run(output, remapper, TeaVMTargetType.WEBASSEMBLY_GC, TeaVMOptimizationLevel.ADVANCED, minify);
|
||||
|
||||
try (var runtime = Objects.requireNonNull(Builder.class.getClassLoader()).getResourceAsStream("org/teavm/backend/wasm/wasm-gc-module-runtime.js")) {
|
||||
if (runtime == null) throw new IllegalStateException("Cannot find WASM runtime");
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
package cc.tweaked.web.http;
|
||||
|
||||
import cc.tweaked.web.Main;
|
||||
import com.google.common.base.Splitter;
|
||||
import cc.tweaked.web.js.Console;
|
||||
import cc.tweaked.web.js.abort.AbortController;
|
||||
import cc.tweaked.web.js.fetch.Headers;
|
||||
import cc.tweaked.web.js.fetch.RequestInit;
|
||||
import cc.tweaked.web.js.fetch.Response;
|
||||
import dan200.computercraft.core.Logging;
|
||||
import dan200.computercraft.core.apis.IAPIEnvironment;
|
||||
import dan200.computercraft.core.apis.handles.ArrayByteChannel;
|
||||
@@ -20,8 +24,10 @@ import io.netty.handler.codec.http.HttpMethod;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.ajax.XMLHttpRequest;
|
||||
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||
import org.teavm.jso.core.JSError;
|
||||
import org.teavm.jso.core.JSPromise;
|
||||
import org.teavm.jso.typedarrays.Int8Array;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -74,10 +80,10 @@ final class HttpRequest extends Resource<HttpRequest, HttpRequest.State> {
|
||||
}
|
||||
}
|
||||
|
||||
private record Fetching(XMLHttpRequest request) implements State {
|
||||
private record Fetching(AbortController abort) implements State {
|
||||
@Override
|
||||
public void close() {
|
||||
request.abort();
|
||||
abort.abort();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,18 +94,31 @@ final class HttpRequest extends Resource<HttpRequest, HttpRequest.State> {
|
||||
|
||||
private @Nullable State doRequest(HttpUri uri, HttpMethod method) {
|
||||
try {
|
||||
var request = new XMLHttpRequest();
|
||||
request.setOnReadyStateChange(() -> onResponseStateChange(request));
|
||||
request.setResponseType("arraybuffer");
|
||||
var address = uri.getUri().toASCIIString();
|
||||
request.open(method.toString(), Main.CORS_PROXY.isEmpty() ? address : Main.CORS_PROXY.replace("{}", address));
|
||||
var abort = new AbortController();
|
||||
|
||||
var request = RequestInit.create();
|
||||
request.setMethod(method.name());
|
||||
if (postBuffer != null) request.setBody(Int8Array.fromJavaBuffer(postBuffer));
|
||||
request.setSignal(abort.getSignal());
|
||||
|
||||
var requestHeaders = new Headers();
|
||||
for (var iterator = headers.iteratorAsString(); iterator.hasNext(); ) {
|
||||
var header = iterator.next();
|
||||
request.setRequestHeader(header.getKey(), header.getValue());
|
||||
requestHeaders.append(header.getKey(), header.getValue());
|
||||
}
|
||||
request.setRequestHeader("X-CC-Redirect", followRedirects ? "true" : "false");
|
||||
request.send(postBuffer == null ? null : Int8Array.fromJavaBuffer(postBuffer));
|
||||
return new Fetching(request);
|
||||
requestHeaders.append("X-CC-Redirect", Boolean.toString(followRedirects));
|
||||
|
||||
var address = uri.getUri().toASCIIString();
|
||||
Response.fetch(Main.CORS_PROXY.isEmpty() ? address : Main.CORS_PROXY.replace("{}", address), request)
|
||||
.then(this::handleResponse)
|
||||
.catchError(e -> {
|
||||
if (e instanceof JSError err) Console.error(err);
|
||||
LOG.error(Logging.HTTP_ERROR, "Error in HTTP request", e);
|
||||
close(Fetching.class, () -> queueFailure("Could not connect"));
|
||||
return null;
|
||||
});
|
||||
|
||||
return new Fetching(abort);
|
||||
} catch (Exception e) {
|
||||
LOG.error(Logging.HTTP_ERROR, "Error in HTTP request", e);
|
||||
queueFailure("Could not connect");
|
||||
@@ -107,39 +126,39 @@ final class HttpRequest extends Resource<HttpRequest, HttpRequest.State> {
|
||||
}
|
||||
}
|
||||
|
||||
private void onResponseStateChange(XMLHttpRequest request) {
|
||||
if (request.getReadyState() != XMLHttpRequest.DONE) return;
|
||||
if (request.getStatus() == 0) {
|
||||
private @Nullable JSPromise<? extends @Nullable JSObject> handleResponse(Response response) {
|
||||
if (response.getStatus() == 0) {
|
||||
close(Fetching.class, () -> queueFailure("Could not connect"));
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
var buffer = (ArrayBuffer) request.getResponse();
|
||||
SeekableByteChannel contents = new ArrayByteChannel(new Int8Array(buffer).copyToJavaArray());
|
||||
var reader = new ReadHandle(contents, binary);
|
||||
|
||||
Map<String, String> responseHeaders = new HashMap<>();
|
||||
for (var header : Splitter.on("\r\n").split(request.getAllResponseHeaders())) {
|
||||
var index = header.indexOf(':');
|
||||
if (index < 0) continue;
|
||||
|
||||
response.getHeaders().forEach((v, k, h) -> {
|
||||
// Normalise the header (so "content-type" becomes "Content-Type")
|
||||
var upcase = true;
|
||||
var headerBuilder = new StringBuilder(index);
|
||||
for (var i = 0; i < index; i++) {
|
||||
var c = header.charAt(i);
|
||||
var headerBuilder = new StringBuilder(k.length());
|
||||
for (var i = 0; i < k.length(); i++) {
|
||||
var c = k.charAt(i);
|
||||
headerBuilder.append(upcase ? Character.toUpperCase(c) : c);
|
||||
upcase = c == '-';
|
||||
}
|
||||
responseHeaders.put(headerBuilder.toString(), header.substring(index + 1).trim());
|
||||
}
|
||||
var stream = new HttpResponseHandle(reader, request.getStatus(), request.getStatusText(), responseHeaders);
|
||||
responseHeaders.put(headerBuilder.toString(), v);
|
||||
});
|
||||
|
||||
if (request.getStatus() >= 200 && request.getStatus() < 400) {
|
||||
close(Fetching.class, () -> environment.queueEvent(SUCCESS_EVENT, address, stream));
|
||||
} else {
|
||||
close(Fetching.class, () -> environment.queueEvent(FAILURE_EVENT, address, request.getStatusText(), stream));
|
||||
}
|
||||
return response.bytes().then(body -> {
|
||||
SeekableByteChannel contents = new ArrayByteChannel(new Int8Array(body).copyToJavaArray());
|
||||
var reader = new ReadHandle(contents, binary);
|
||||
|
||||
var stream = new HttpResponseHandle(reader, response.getStatus(), response.getStatusText(), responseHeaders);
|
||||
|
||||
if (response.getStatus() >= 200 && response.getStatus() < 400) {
|
||||
close(Fetching.class, () -> environment.queueEvent(SUCCESS_EVENT, address, stream));
|
||||
} else {
|
||||
close(Fetching.class, () -> environment.queueEvent(FAILURE_EVENT, address, response.getStatusText(), stream));
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private void queueFailure(String message) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: 2026 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js.abort;
|
||||
|
||||
import org.teavm.jso.JSClass;
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.JSProperty;
|
||||
|
||||
/**
|
||||
* The {@link AbortController} interface represents a controller object that allows you to abort one or more Web
|
||||
* requests as and when desired.
|
||||
*
|
||||
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortController">MDN Reference</a>
|
||||
*/
|
||||
@JSClass
|
||||
public class AbortController implements JSObject {
|
||||
public AbortController() {
|
||||
}
|
||||
|
||||
@JSProperty
|
||||
public native AbortSignal getSignal();
|
||||
|
||||
public native void abort();
|
||||
|
||||
public native void abort(String reason);
|
||||
|
||||
public native void abort(JSObject reason);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-FileCopyrightText: 2026 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js.abort;
|
||||
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.JSProperty;
|
||||
import org.teavm.jso.dom.events.EventTarget;
|
||||
|
||||
/**
|
||||
* The {@link AbortSignal} interface represents a signal object that allows you to communicate with an asynchronous
|
||||
* operation (such as a fetch request) and abort it if required via an {@link AbortController} object.
|
||||
*
|
||||
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal">MDN Reference</a>
|
||||
*/
|
||||
public interface AbortSignal extends EventTarget {
|
||||
@JSProperty
|
||||
boolean getAborted();
|
||||
|
||||
@JSProperty
|
||||
JSObject getReason();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2026 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js.fetch;
|
||||
|
||||
import org.teavm.jso.JSClass;
|
||||
import org.teavm.jso.JSFunctor;
|
||||
import org.teavm.jso.JSObject;
|
||||
|
||||
/**
|
||||
* The {@link Headers} interface of the Fetch API allows you to perform various actions on HTTP request and response
|
||||
* headers. These actions include retrieving, setting, adding to, and removing headers from the list of the request's
|
||||
* headers.
|
||||
*
|
||||
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Headers">MDN Reference</a>
|
||||
*/
|
||||
@JSClass
|
||||
public class Headers implements JSObject {
|
||||
public native void append(String key, String value);
|
||||
|
||||
public native void forEach(ForEachConsumer consumer);
|
||||
|
||||
@JSFunctor
|
||||
@FunctionalInterface
|
||||
public interface ForEachConsumer extends JSObject {
|
||||
void accept(String value, String key, Headers object);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-FileCopyrightText: 2026 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js.fetch;
|
||||
|
||||
import cc.tweaked.web.js.abort.AbortSignal;
|
||||
import org.teavm.jso.JSClass;
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.JSProperty;
|
||||
import org.teavm.jso.core.JSMapLike;
|
||||
import org.teavm.jso.core.JSObjects;
|
||||
import org.teavm.jso.typedarrays.ArrayBuffer;
|
||||
import org.teavm.jso.typedarrays.TypedArray;
|
||||
|
||||
/**
|
||||
* The properties that can be passed to {@link Response#fetch(String, RequestInit)}.
|
||||
*
|
||||
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/RequestInit">MDN Reference</a>
|
||||
*/
|
||||
@JSClass(transparent = true)
|
||||
public abstract class RequestInit implements JSObject {
|
||||
private RequestInit() {
|
||||
}
|
||||
|
||||
public static RequestInit create() {
|
||||
return JSObjects.create();
|
||||
}
|
||||
|
||||
@JSProperty
|
||||
public native void setMethod(String method);
|
||||
|
||||
@JSProperty
|
||||
public native void setBody(String type);
|
||||
|
||||
@JSProperty
|
||||
public native void setBody(ArrayBuffer body);
|
||||
|
||||
@JSProperty
|
||||
public native void setBody(TypedArray body);
|
||||
|
||||
@JSProperty
|
||||
public native void setHeaders(Headers headers);
|
||||
|
||||
@JSProperty
|
||||
public native void setHeaders(JSMapLike<String> headers);
|
||||
|
||||
@JSProperty
|
||||
public native void setSignal(AbortSignal signal);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-FileCopyrightText: 2026 The CC: Tweaked Developers
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package cc.tweaked.web.js.fetch;
|
||||
|
||||
import org.teavm.jso.JSBody;
|
||||
import org.teavm.jso.JSObject;
|
||||
import org.teavm.jso.JSProperty;
|
||||
import org.teavm.jso.core.JSPromise;
|
||||
import org.teavm.jso.typedarrays.Uint8Array;
|
||||
|
||||
/**
|
||||
* The {@link Response} interface of the Fetch API represents the response to a request.
|
||||
*
|
||||
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Response">MDN Reference</a>
|
||||
*/
|
||||
public abstract class Response implements JSObject {
|
||||
private Response() {
|
||||
}
|
||||
|
||||
@JSBody(params = "url", script = "return fetch(url)")
|
||||
public static native JSPromise<Response> fetch(String url);
|
||||
|
||||
@JSBody(params = { "url", "options" }, script = "return fetch(url, options)")
|
||||
public static native JSPromise<Response> fetch(String url, RequestInit options);
|
||||
|
||||
@JSProperty
|
||||
public native int getStatus();
|
||||
|
||||
@JSProperty
|
||||
public native String getStatusText();
|
||||
|
||||
@JSProperty
|
||||
public native Headers getHeaders();
|
||||
|
||||
public native JSPromise<Uint8Array> bytes();
|
||||
}
|
||||
+48
-21
@@ -11,11 +11,11 @@ See https://github.com/actions/toolkit/blob/master/docs/problem-matchers.md and
|
||||
https://github.com/actions/toolkit/blob/master/docs/commands.md.
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import os.path
|
||||
import pathlib
|
||||
import re
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Optional, Tuple
|
||||
|
||||
LUA_ERROR_LOCATION = re.compile(r"^\s+(/[\w./-]+):(\d+):", re.MULTILINE)
|
||||
JAVA_LUA_ERROR_LOCATION = re.compile(r"^java.lang.IllegalStateException: (/[\w./-]+):(\d+):")
|
||||
@@ -42,10 +42,8 @@ PROJECT_LOCATIONS = [
|
||||
"projects/forge",
|
||||
]
|
||||
|
||||
TEST_REPORTS = []
|
||||
|
||||
|
||||
def find_file(path: str) -> Optional[str]:
|
||||
def find_file(path: str) -> str | None:
|
||||
while len(path) > 0 and path[0] == "/":
|
||||
path = path[1:]
|
||||
|
||||
@@ -58,7 +56,7 @@ def find_file(path: str) -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def find_location(message: str) -> Optional[Tuple[str, str]]:
|
||||
def find_location(message: str) -> tuple[str, str] | None:
|
||||
location = LUA_ERROR_LOCATION.search(message)
|
||||
if location:
|
||||
file = find_file(location[1])
|
||||
@@ -79,31 +77,60 @@ def find_location(message: str) -> Optional[Tuple[str, str]]:
|
||||
return None
|
||||
|
||||
|
||||
_OTR_CORE = "https://schemas.opentest4j.org/reporting/core/0.2.0"
|
||||
_OTR_EVENTS = "https://schemas.opentest4j.org/reporting/events/0.2.0"
|
||||
_OTR_JAVA = "https://schemas.opentest4j.org/reporting/java/0.2.0"
|
||||
|
||||
|
||||
def _parse_junit_file(path: pathlib.Path):
|
||||
for testcase in ET.parse(path).findall(".//testcase"):
|
||||
for result in testcase:
|
||||
if result.tag == "skipped":
|
||||
tests: dict[str, str] = {}
|
||||
tree = ET.parse(path).getroot()
|
||||
for event in tree:
|
||||
if event.tag == f"{{{_OTR_CORE}}}infrastructure":
|
||||
continue
|
||||
elif event.tag == f"{{{_OTR_EVENTS}}}started":
|
||||
test_id: str = event.get("id")
|
||||
test_name: str = event.get("name")
|
||||
if (parent_id := event.get("parentId")) is not None:
|
||||
test_name = tests[parent_id] + " ⊳ " + test_name
|
||||
|
||||
tests[test_id] = test_name
|
||||
elif event.tag == f"{{{_OTR_EVENTS}}}finished":
|
||||
test_id: str = event.get("id")
|
||||
test_name = tests[test_id]
|
||||
|
||||
result = event.find(f"./{{{_OTR_CORE}}}result")
|
||||
if result is None:
|
||||
continue
|
||||
|
||||
name = f'{testcase.attrib["classname"]}.{testcase.attrib["name"]}'
|
||||
message = result.attrib.get("message")
|
||||
full_message = result.text or message
|
||||
if result.get("status") in ("SUCCESSFUL", "ABORTED"):
|
||||
continue
|
||||
|
||||
reason = result.find(f"./{{{_OTR_CORE}}}reason")
|
||||
throwable = result.find(f"./{{{_OTR_JAVA}}}throwable")
|
||||
|
||||
message = reason.text if reason is not None else ""
|
||||
full_message = throwable.text if throwable is not None else ""
|
||||
|
||||
location = find_location(full_message)
|
||||
error = ERROR_MESSAGE.match(message)
|
||||
if error:
|
||||
error = error[1]
|
||||
if (match := ERROR_MESSAGE.match(message)) is not None:
|
||||
error = match.group(1)
|
||||
else:
|
||||
error = message
|
||||
error = SPACES.sub(' ', error)
|
||||
|
||||
if location:
|
||||
print(f'## {location[0]}:{location[1]}: {name} failed: {SPACES.sub(" ", error)}')
|
||||
print(f"## {location[0]}:{location[1]}: {test_name} failed: {error}")
|
||||
else:
|
||||
print(f"::error::{name} failed")
|
||||
print(f"::error::{test_name} failed: {error}")
|
||||
|
||||
if len(full_message) > 0:
|
||||
print("::group::Full error message")
|
||||
print(full_message)
|
||||
print("::endgroup::")
|
||||
else:
|
||||
print(f"Unknown tag: {event.tag}")
|
||||
|
||||
print("::group::Full error message")
|
||||
print(full_message)
|
||||
print("::endgroup::")
|
||||
|
||||
|
||||
def parse_junit() -> None:
|
||||
@@ -114,7 +141,7 @@ def parse_junit() -> None:
|
||||
print("::add-matcher::.github/matchers/junit.json")
|
||||
|
||||
for project in PROJECT_LOCATIONS:
|
||||
for path in pathlib.Path(os.path.join(project, "build/test-results/test")).glob("TEST-*.xml"):
|
||||
for path in pathlib.Path(os.path.join(project, "build/test-results")).glob("*/open-test-report.xml"):
|
||||
_parse_junit_file(path)
|
||||
|
||||
for path in pathlib.Path(os.path.join(project, "build/test-results")).glob("run*.xml"):
|
||||
@@ -130,7 +157,7 @@ def _parse_checkstyle(path: pathlib.Path):
|
||||
|
||||
attrib = error.attrib
|
||||
print(
|
||||
f'{attrib["severity"]} {filename}:{attrib["line"]}:{attrib.get("column", 1)}: {SPACES.sub(" ", attrib["message"])}'
|
||||
f"{attrib['severity']} {filename}:{attrib['line']}:{attrib.get('column', 1)}: {SPACES.sub(' ', attrib['message'])}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user