diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1385a79..aac0100 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,4 +9,10 @@ jobs: java: 17 gradle_tasks: spotbugs reviewdog: | - cat ./build/reports/spotbugs/main/spotbugs.sarif | reviewdog -name="spotbugs" -f=sarif -reporter=github-check -level=warning \ No newline at end of file + cat ./build/reports/spotbugs/main/spotbugs.sarif | reviewdog -name="spotbugs" -f=sarif -reporter=github-check -level=warning + + run_tests: + uses: constellation-mc/actions/.github/workflows/mc-tests.yml@main + with: + java: 17 + server_task: runTestServer \ No newline at end of file diff --git a/.gitignore b/.gitignore index c312fd6..20d3ceb 100644 --- a/.gitignore +++ b/.gitignore @@ -113,6 +113,7 @@ gradle-app.setting # Common working directory run/ +run_test_server/ # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar diff --git a/build.gradle b/build.gradle index 3c295f2..3702040 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ plugins { import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.spotbugs.snom.SpotBugsTask import me.modmuss50.mpp.ReleaseType +import net.fabricmc.loom.api.RemapConfigurationSettings def local = !System.getenv().containsKey("GITHUB_RUN_NUMBER"); @@ -61,6 +62,23 @@ remapJar { } tasks.jar.archiveClassifier = "slim" +sourceSets { + testmod { + compileClasspath += main.compileClasspath + runtimeClasspath += main.runtimeClasspath + } +} + +loom { + addRemapConfiguration("testmodRemapImplementation", (RemapConfigurationSettings configuration) -> { + configuration.getTargetConfigurationName().convention("testmodImplementation") + configuration.getSourceSet().convention(sourceSets.testmod) + configuration.getOnCompileClasspath().convention(true) + configuration.getOnRuntimeClasspath().convention(true) + configuration.getPublishingMode().convention(RemapConfigurationSettings.PublishingMode.NONE) + }) +} + dependencies { // To change the versions see the gradle.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" @@ -80,6 +98,12 @@ dependencies { implementation it shade it } + + testmodImplementation sourceSets.main.output + testmodRemapImplementation(libs.handy.tests) { + exclude(group: "net.fabricmc", module: "fabric-loader") + } + testmodImplementation libs.assertj.core } tasks.register("spotbugs", SpotBugsTask) { task -> @@ -118,6 +142,18 @@ spotless { loom { accessWidenerPath = file("src/main/resources/commander.accesswidener") + runs { + testServer { + server() + source sourceSets.testmod + runDir("run_test_server") + + properties([ + "handy-tests.auto-test": "true" + ]) + } + } + //https://gist.github.com/maityyy/3dbcd558d58a6412c3a2a38c72706e8e afterEvaluate { loom.runs.configureEach { @@ -127,6 +163,14 @@ loom { property("mixin.debug.export", "true") } } + + mods.register(project.name) { + sourceSet project.sourceSets.main + } + + mods.register(project.name + "-testmod") { + sourceSet project.sourceSets.testmod + } } processResources { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7d95608..fd31512 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,13 +4,17 @@ dark-matter = "4.1.0-1.20.1-build.86" [libraries] fabric-loader = { group = "net.fabricmc", name = "fabric-loader", version = "0.16.0" } fabric-api = { group = "net.fabricmc.fabric-api", name = "fabric-api", version = "0.92.1+1.20.1" } -evalx = { group = "me.melontini", name = "evalex", version = "4.0.0-build.6" } +evalx = { group = "me.melontini", name = "evalex", version = "4.0.0-build.7" } mapping-io = { group = "net.fabricmc", name = "mapping-io", version = "0.6.1" } dark-matter-base = { group = "me.melontini", name = "dark-matter-base", version.ref = "dark-matter" } dark-matter-mixin = { group = "me.melontini", name = "dark-matter-mixin", version.ref = "dark-matter" } dark-matter-minecraft = { group = "me.melontini", name = "dark-matter-minecraft", version.ref = "dark-matter" } dark-matter-data = { group = "me.melontini", name = "dark-matter-data", version.ref = "dark-matter" } +handy-tests = { group = "me.melontini", name = "handy-tests", version = "0.3.0-1.20.1-build.16" } +assertj-core = { module = "org.assertj:assertj-core", version = "3.26.3" } +mockito-core = { module = "org.mockito:mockito-core", version = "5.12.0" } + [plugins] fabric-loom = { id = "fabric-loom", version = "1.7.2" } lombok = { id = "io.freefair.lombok", version = "8.6"} diff --git a/src/main/java/me/melontini/commander/impl/expression/extensions/ReflectiveValueConverter.java b/src/main/java/me/melontini/commander/impl/expression/extensions/ReflectiveValueConverter.java index e7f2ec7..75f390e 100644 --- a/src/main/java/me/melontini/commander/impl/expression/extensions/ReflectiveValueConverter.java +++ b/src/main/java/me/melontini/commander/impl/expression/extensions/ReflectiveValueConverter.java @@ -12,10 +12,10 @@ import java.util.Map; import me.melontini.commander.api.expression.extensions.ObjectConverter; import me.melontini.commander.api.expression.extensions.ProxyMap; import me.melontini.commander.impl.expression.EvalUtils; +import me.melontini.commander.impl.expression.extensions.convert.gson.GsonConverter; import me.melontini.commander.impl.expression.extensions.convert.misc.DataAccessorConverter; import me.melontini.commander.impl.expression.extensions.convert.misc.IdentifierConverter; import me.melontini.commander.impl.expression.extensions.convert.misc.OptionalConverter; -import me.melontini.commander.impl.expression.extensions.convert.gson.GsonConverter; import me.melontini.commander.impl.expression.extensions.convert.nbt.NbtConverter; import me.melontini.dark_matter.api.base.util.MathUtil; diff --git a/src/main/java/me/melontini/commander/impl/expression/extensions/convert/misc/IdentifierConverter.java b/src/main/java/me/melontini/commander/impl/expression/extensions/convert/misc/IdentifierConverter.java index 6e414fb..3d90503 100644 --- a/src/main/java/me/melontini/commander/impl/expression/extensions/convert/misc/IdentifierConverter.java +++ b/src/main/java/me/melontini/commander/impl/expression/extensions/convert/misc/IdentifierConverter.java @@ -7,13 +7,13 @@ import com.ezylang.evalex.data.types.StringValue; import net.minecraft.util.Identifier; public class IdentifierConverter implements ConverterIfc { - @Override - public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { - return StringValue.of(object.toString()); - } + @Override + public EvaluationValue convert(Object object, ExpressionConfiguration configuration) { + return StringValue.of(object.toString()); + } - @Override - public boolean canConvert(Object object) { - return object instanceof Identifier; - } + @Override + public boolean canConvert(Object object) { + return object instanceof Identifier; + } } diff --git a/src/testmod/java/me/melontini/commander/test/ConvertersTest.java b/src/testmod/java/me/melontini/commander/test/ConvertersTest.java new file mode 100644 index 0000000..cc2bb48 --- /dev/null +++ b/src/testmod/java/me/melontini/commander/test/ConvertersTest.java @@ -0,0 +1,77 @@ +package me.melontini.commander.test; + +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.data.types.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import java.math.BigDecimal; +import java.util.Map; +import java.util.Optional; +import me.melontini.commander.impl.expression.extensions.ReflectiveValueConverter; +import me.melontini.commander.impl.expression.extensions.convert.nbt.NbtCompoundStruct; +import me.melontini.handytests.server.ServerTestContext; +import me.melontini.handytests.server.ServerTestEntrypoint; +import me.melontini.handytests.util.runner.HandyTest; +import net.minecraft.nbt.*; +import net.minecraft.util.Identifier; +import org.assertj.core.api.Assertions; + +public class ConvertersTest implements ServerTestEntrypoint { + + @HandyTest + void testOptionalConvert(ServerTestContext context) { + Assertions.assertThat(ReflectiveValueConverter.convert(Optional.empty())) + .isEqualTo(NullValue.of()); + + Assertions.assertThat(ReflectiveValueConverter.convert(Optional.of(123))) + .isEqualTo(NumberValue.of(BigDecimal.valueOf(123))); + } + + @HandyTest + void testIdentifierConvert(ServerTestContext context) { + Assertions.assertThat(ReflectiveValueConverter.convert(Identifier.of("cmd", "test_id"))) + .isEqualTo(StringValue.of("cmd:test_id")); + + Assertions.assertThat(ReflectiveValueConverter.convert(new Identifier("test_id"))) + .isEqualTo(StringValue.of("minecraft:test_id")); + } + + @HandyTest + void testNbtConvert(ServerTestContext context) { + Assertions.assertThat(ReflectiveValueConverter.convert(new NbtCompound())) + .isInstanceOf(StructureValue.class) + .isEqualTo(StructureValue.of( + (Map) (Object) new NbtCompoundStruct(new NbtCompound()))); + + Assertions.assertThat(ReflectiveValueConverter.convert(new NbtList())) + .isInstanceOf(ArrayValue.class); + + Assertions.assertThat(ReflectiveValueConverter.convert(NbtString.of("Hello World!"))) + .isEqualTo(StringValue.of("Hello World!")); + + Assertions.assertThat(ReflectiveValueConverter.convert(NbtInt.of(123))) + .isEqualTo(NumberValue.of(BigDecimal.valueOf(123))); + + Assertions.assertThat(ReflectiveValueConverter.convert(NbtDouble.of(123.4587))) + .isEqualTo(NumberValue.of(BigDecimal.valueOf(123.4587))); + } + + @HandyTest + void testGsonConvert(ServerTestContext context) { + Assertions.assertThat(ReflectiveValueConverter.convert(new JsonObject())) + .isInstanceOf(StructureValue.class); + + Assertions.assertThat(ReflectiveValueConverter.convert(new JsonArray())) + .isInstanceOf(ArrayValue.class); + + Assertions.assertThat(ReflectiveValueConverter.convert(new JsonPrimitive("Hello World!"))) + .isEqualTo(StringValue.of("Hello World!")); + + Assertions.assertThat(ReflectiveValueConverter.convert(new JsonPrimitive(123))) + .isEqualTo(NumberValue.of(BigDecimal.valueOf(123))); + + Assertions.assertThat(ReflectiveValueConverter.convert(new JsonPrimitive(123.4587))) + .isEqualTo(NumberValue.of(BigDecimal.valueOf(123.4587))); + } +} diff --git a/src/testmod/java/me/melontini/commander/test/CustomEventsTest.java b/src/testmod/java/me/melontini/commander/test/CustomEventsTest.java new file mode 100644 index 0000000..6efb453 --- /dev/null +++ b/src/testmod/java/me/melontini/commander/test/CustomEventsTest.java @@ -0,0 +1,35 @@ +package me.melontini.commander.test; + +import static me.melontini.commander.test.ExpressionTest.*; + +import lombok.extern.log4j.Log4j2; +import me.melontini.commander.api.event.EventType; +import me.melontini.commander.api.util.EventExecutors; +import me.melontini.commander.impl.Commander; +import me.melontini.dark_matter.api.data.codecs.ExtraCodecs; +import me.melontini.handytests.server.ServerTestContext; +import me.melontini.handytests.server.ServerTestEntrypoint; +import me.melontini.handytests.util.runner.HandyTest; +import net.fabricmc.api.ModInitializer; +import net.minecraft.util.ActionResult; +import org.assertj.core.api.Assertions; + +@Log4j2 +public class CustomEventsTest implements ModInitializer, ServerTestEntrypoint { + + public static final EventType CUSTOM = EventType.builder() + .cancelTerm(ExtraCodecs.enumCodec(ActionResult.class)) + .build(Commander.id("test")); + + @HandyTest // custom_events_test.json + void testCustomEventType(ServerTestContext context) { + Assertions.assertThat(EventExecutors.runActionResult( + CUSTOM, context.context().getOverworld(), () -> emptyContext(context))) + .isEqualTo(ActionResult.FAIL); // Default is pass, the JSON cancels the event with fail. + } + + @Override + public void onInitialize() { + // NOOP + } +} diff --git a/src/testmod/java/me/melontini/commander/test/ExpressionTest.java b/src/testmod/java/me/melontini/commander/test/ExpressionTest.java new file mode 100644 index 0000000..2d37b65 --- /dev/null +++ b/src/testmod/java/me/melontini/commander/test/ExpressionTest.java @@ -0,0 +1,65 @@ +package me.melontini.commander.test; + +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.data.types.NumberValue; +import com.ezylang.evalex.data.types.StringValue; +import com.ezylang.evalex.data.types.StructureValue; +import java.math.BigDecimal; +import java.util.Map; +import me.melontini.commander.api.expression.Expression; +import me.melontini.handytests.server.ServerTestContext; +import me.melontini.handytests.server.ServerTestEntrypoint; +import me.melontini.handytests.util.runner.HandyTest; +import net.minecraft.loot.context.LootContext; +import net.minecraft.loot.context.LootContextParameterSet; +import net.minecraft.loot.context.LootContextTypes; +import org.assertj.core.api.Assertions; + +public class ExpressionTest implements ServerTestEntrypoint { + + @HandyTest + void testNewOperators(ServerTestContext context) { + var lootContext = emptyContext(context); + + Map map = + Map.of("x", NumberValue.of(BigDecimal.valueOf(23)), "y", StringValue.of("Hello")); + + Assertions.assertThat( + parse("struct?.x ? 42").eval(lootContext, Map.of("struct", StructureValue.of(map)))) + .isEqualTo(Expression.Result.convert(BigDecimal.valueOf(23))); + + Assertions.assertThat( + parse("struct?.u").eval(lootContext, Map.of("struct", StructureValue.of(map)))) + .matches(Expression.Result::isNullValue, "result is null"); + + Assertions.assertThat( + parse("null ? 42").eval(lootContext, Map.of("struct", StructureValue.of(map)))) + .isEqualTo(Expression.Result.convert(BigDecimal.valueOf(42))); + + Assertions.assertThat( + parse("struct?.u ? 42").eval(lootContext, Map.of("struct", StructureValue.of(map)))) + .isEqualTo(Expression.Result.convert(BigDecimal.valueOf(42))); + } + + @HandyTest + void testLootContextAccess(ServerTestContext context) { + var lootContext = emptyContext(context); + + Assertions.assertThat(parse("level.dimensionTypeId.location").eval(lootContext)) + .isEqualTo(Expression.Result.convert("minecraft:overworld")); + } + + public static LootContext emptyContext(ServerTestContext context) { + return new LootContext.Builder( + new LootContextParameterSet.Builder(context.server().getOverworld()) + .build(LootContextTypes.EMPTY)) + .build(null); + } + + public static Expression parse(String expression) { + var result = Expression.parse(expression); + if (result.error().isPresent()) + throw new RuntimeException(result.error().get().message()); + return result.result().orElseThrow(); + } +} diff --git a/src/testmod/java/me/melontini/commander/test/VirtualFieldsTest.java b/src/testmod/java/me/melontini/commander/test/VirtualFieldsTest.java new file mode 100644 index 0000000..9eea351 --- /dev/null +++ b/src/testmod/java/me/melontini/commander/test/VirtualFieldsTest.java @@ -0,0 +1,69 @@ +package me.melontini.commander.test; + +import static me.melontini.commander.test.ExpressionTest.emptyContext; +import static me.melontini.commander.test.ExpressionTest.parse; + +import com.ezylang.evalex.data.EvaluationValue; +import com.ezylang.evalex.data.types.BooleanValue; +import com.ezylang.evalex.data.types.StructureValue; +import java.util.Map; +import me.melontini.commander.impl.expression.extensions.convert.nbt.NbtCompoundStruct; +import me.melontini.handytests.server.ServerTestContext; +import me.melontini.handytests.server.ServerTestEntrypoint; +import me.melontini.handytests.util.runner.HandyTest; +import net.minecraft.block.Blocks; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.block.entity.BlockEntityType; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityType; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.predicate.NbtPredicate; +import net.minecraft.util.math.BlockPos; +import org.assertj.core.api.Assertions; + +public class VirtualFieldsTest implements ServerTestEntrypoint { + + @HandyTest + void testStackFields(ServerTestContext context) { + ItemStack stack = Items.ACACIA_LOG.getDefaultStack(); + + var lootContext = emptyContext(context); + Assertions.assertThat(parse("stack.nbt").eval(lootContext, Map.of("stack", stack))) + .isInstanceOf(StructureValue.class) + .isEqualTo(StructureValue.of( + (Map) (Object) new NbtCompoundStruct(stack.getOrCreateNbt()))); + } + + @HandyTest + void testEntityFields(ServerTestContext context) { + Entity entity = EntityType.SLIME.create(context.context().getOverworld()); + + var lootContext = emptyContext(context); + Assertions.assertThat(parse("entity.nbt").eval(lootContext, Map.of("entity", entity))) + .isInstanceOf(StructureValue.class) + .isEqualTo(StructureValue.of((Map) + (Object) new NbtCompoundStruct(NbtPredicate.entityToNbt(entity)))); + } + + @HandyTest + void testBlockEntityFields(ServerTestContext context) { + BlockEntity entity = BlockEntityType.CAMPFIRE.instantiate( + new BlockPos(100, 60, 100), Blocks.CAMPFIRE.getDefaultState()); + + var lootContext = emptyContext(context); + Assertions.assertThat(parse("be.nbt").eval(lootContext, Map.of("be", entity))) + .isInstanceOf(StructureValue.class) + .isEqualTo(StructureValue.of((Map) + (Object) new NbtCompoundStruct(entity.createNbtWithIdentifyingData()))); + } + + @HandyTest + void testBlockStateFields(ServerTestContext context) { + var lootContext = emptyContext(context); + + Assertions.assertThat(parse("state.properties.lit") + .eval(lootContext, Map.of("state", Blocks.CAMPFIRE.getDefaultState()))) + .isEqualTo(BooleanValue.of(true)); + } +} diff --git a/src/testmod/resources/data/commander/commander/events/test/custom_events_test.json b/src/testmod/resources/data/commander/commander/events/test/custom_events_test.json new file mode 100644 index 0000000..0ea806b --- /dev/null +++ b/src/testmod/resources/data/commander/commander/events/test/custom_events_test.json @@ -0,0 +1,13 @@ +{ + "event": "commander:test", + "commands": [ + { + "type": "commander:print", + "text": "Hello from JSON!" + }, + { + "type": "commander:cancel", + "value": "fail" + } + ] +} \ No newline at end of file diff --git a/src/testmod/resources/fabric.mod.json b/src/testmod/resources/fabric.mod.json new file mode 100644 index 0000000..290ed8f --- /dev/null +++ b/src/testmod/resources/fabric.mod.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "id": "commander-testmod", + "version": "1.0.0", + "license": "MIT", + "entrypoints": { + "main": [ + "me.melontini.commander.test.CustomEventsTest" + ], + "handy:server_test": [ + "me.melontini.commander.test.ConvertersTest", + "me.melontini.commander.test.ExpressionTest", + "me.melontini.commander.test.VirtualFieldsTest", + "me.melontini.commander.test.CustomEventsTest" + ] + } +}