Merge branch 'mc-1.20.x' into mc-1.21.x

This commit is contained in:
Jonathan Coates committed 2026-10-25 15:08:19 +00:00
commit 2f6229f253
47 files changed
+439 -247

No files matched your search

+10
View File
@@ -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
+1
View File
@@ -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 {
+8 -5
View File
@@ -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;
+1 -9
View File
@@ -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
}
@@ -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());
}
}
@@ -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);
}
@@ -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()) {
@@ -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;
}
}
@@ -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
@@ -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
));
}
}
@@ -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) {
@@ -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;
@@ -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;
}
@@ -78,7 +78,7 @@ public final class CopyComponents implements RecipeFunction {
}
@Override
public Type<?> getType() {
public Type<CopyComponents> getType() {
return ModRegistry.RecipeFunctions.COPY_COMPONENTS.get();
}
@@ -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.
@@ -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;
}
@@ -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) {
@@ -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;
@@ -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()}")
}
@@ -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(),
),
)
@@ -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;
}
@@ -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)
@@ -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);
+1
View File
@@ -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
View File
@@ -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'])}"
)