Skip to content

Commit

Permalink
Implement GameTests (#553)
Browse files Browse the repository at this point in the history
* start on implementing tests.

* add slim jar for publishing.

* it almost works :lets:

* my test cases broke :waaGONE:
  • Loading branch information
screret authored Nov 18, 2023
1 parent ac05926 commit b3a891d
Show file tree
Hide file tree
Showing 16 changed files with 458 additions and 13 deletions.
14 changes: 14 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import net.fabricmc.loom.task.RemapJarTask

plugins {
alias libs.plugins.architectury
alias libs.plugins.architectury.loom apply false
Expand Down Expand Up @@ -79,6 +81,18 @@ subprojects {
annotationProcessor 'org.projectlombok:lombok:1.18.24'

implementation 'com.google.code.findbugs:jsr305:3.0.2'

// tests
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
testImplementation 'io.javalin:javalin:5.4.2'
testImplementation 'org.mockito:mockito-core:5.2.0'
}

tasks.register('remapSlimJar', RemapJarTask) {
dependsOn(jar)
inputFile.set(jar.archiveFile)
addNestedDependencies = false
archiveClassifier.set("slim")
}
}

Expand Down
2 changes: 1 addition & 1 deletion common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ dependencies {
modImplementation(fabric.ae2) { transitive = false }

//AlmostUnified
modImplementation(fabric.almostUnified.common)
modCompileOnly(fabric.almostUnified.common)

// KJS
modCompileOnly fabric.kubejs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import net.fabricmc.api.Environment;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.crafting.RecipeManager;
import org.jetbrains.annotations.VisibleForTesting;

import javax.annotation.Nullable;
import java.util.ArrayList;
Expand Down Expand Up @@ -66,6 +67,7 @@ public enum Status {
protected int fuelMaxTime;
@Getter
protected long timeStamp;
@Getter(onMethod_ = @VisibleForTesting)
protected boolean recipeDirty;
@Persisted
@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.item.crafting.RecipeManager;
import net.minecraft.world.item.crafting.RecipeType;
import org.jetbrains.annotations.VisibleForTesting;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;

Expand All @@ -18,4 +19,8 @@
public interface RecipeManagerAccessor {
@Accessor("recipes")
Map<RecipeType<?>, Map<ResourceLocation, Recipe<?>>> getRawRecipes();

@Accessor("recipes")
@VisibleForTesting
void setRawRecipes(Map<RecipeType<?>, Map<ResourceLocation, Recipe<?>>> recipes);
}
68 changes: 68 additions & 0 deletions common/src/main/java/com/gregtechceu/gtceu/test/GTGameTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.gregtechceu.gtceu.test;

import appeng.server.testworld.PlotTestHelper;
import com.gregtechceu.gtceu.GTCEu;
import com.gregtechceu.gtceu.test.api.machine.trait.ParallelLogicTest;
import com.gregtechceu.gtceu.test.api.machine.trait.RecipeLogicTest;
import com.gregtechceu.gtceu.utils.FormattingUtil;
import com.mojang.datafixers.util.Pair;
import com.simibubi.create.infrastructure.gametest.CreateTestFunction;
import com.simibubi.create.infrastructure.gametest.tests.*;
import io.github.fabricators_of_create.porting_lib.gametest.infrastructure.ExtendedTestFunction;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestGenerator;
import net.minecraft.gametest.framework.GameTestRegistry;
import net.minecraft.gametest.framework.TestFunction;
import net.minecraft.world.level.block.Rotation;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Stream;

public class GTGameTests {
private static final Class<?>[] testHolders = {
RecipeLogicTest.class,
ParallelLogicTest.class
};

@GameTestGenerator
public static Collection<TestFunction> generateTests() {
return getTestsFrom(testHolders);
}

public static Collection<TestFunction> getTestsFrom(Class<?>... classes) {
return Stream.of(classes)
.map(Class::getDeclaredMethods)
.flatMap(Stream::of)
.filter(method -> !method.isSynthetic() && method.getAnnotation(GameTest.class) != null)
.map(method -> Pair.of(method, method.getAnnotation(GameTest.class)))
.map(method -> new TestFunction(
"gtceu",
GTCEu.MOD_ID + "." + method.getFirst().getDeclaringClass().getSimpleName() + "." + method.getFirst().getName(),
method.getSecond().template(),
Rotation.NONE,
method.getSecond().timeoutTicks(),
method.getSecond().setupTicks(),
method.getSecond().required(),
method.getSecond().requiredSuccesses(),
method.getSecond().attempts(),
gameTestHelper -> {
try {
Object object = null;
if (!Modifier.isStatic(method.getFirst().getModifiers())) {
object = method.getFirst().getDeclaringClass().getConstructor().newInstance();
}
method.getFirst().invoke(object, gameTestHelper);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}))
.sorted(Comparator.comparing(TestFunction::getTestName))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.gregtechceu.gtceu.test.api.machine.trait;

import com.gregtechceu.gtceu.api.blockentity.MetaMachineBlockEntity;
import com.gregtechceu.gtceu.api.capability.recipe.FluidRecipeCapability;
import com.gregtechceu.gtceu.api.capability.recipe.IO;
import com.gregtechceu.gtceu.api.capability.recipe.ItemRecipeCapability;
import com.gregtechceu.gtceu.api.machine.MetaMachine;
import com.gregtechceu.gtceu.api.machine.feature.IRecipeLogicMachine;
import com.gregtechceu.gtceu.api.recipe.GTRecipe;
import com.gregtechceu.gtceu.common.data.GTMaterials;
import com.gregtechceu.gtceu.common.data.GTRecipeModifiers;
import com.gregtechceu.gtceu.data.recipe.builder.GTRecipeBuilder;
import com.lowdragmc.lowdraglib.side.fluid.IFluidTransfer;
import com.lowdragmc.lowdraglib.side.item.IItemTransfer;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;

public class ParallelLogicTest {

@GameTest(template = "gtceu:ebf")
public void getMaxRecipeMultiplier_FluidLimitTest(GameTestHelper helper) {
BlockEntity holder = helper.getBlockEntity(new BlockPos(1, 1, 0));
if (!(holder instanceof MetaMachineBlockEntity atte)) {
helper.fail("wrong block at relative pos [1,1,0]!");
return;
}
MetaMachine machine = atte.getMetaMachine();
if (!(machine instanceof IRecipeLogicMachine rlm)) {
helper.fail("wrong machine in MetaMachineBlockEntity!");
return;
}

int parallelLimit = 4;

// Create a simple recipe to be used for testing
GTRecipe recipe = GTRecipeBuilder.ofRaw()
.inputItems(new ItemStack(Blocks.COBBLESTONE))
.inputFluids(GTMaterials.Acetone.getFluid(4000))
.outputItems(new ItemStack(Blocks.STONE))
.blastFurnaceTemp(1000)
.EUt(30).duration(100)
.buildRawRecipe();

((IItemTransfer)rlm.getCapabilitiesProxy().get(IO.IN, ItemRecipeCapability.CAP)).insertItem(0, new ItemStack(Blocks.COBBLESTONE, 16), false);
((IFluidTransfer)rlm.getCapabilitiesProxy().get(IO.IN, FluidRecipeCapability.CAP)).fill(GTMaterials.Acetone.getFluid(8000), false);

var paralleled = GTRecipeModifiers.accurateParallel(machine, recipe, parallelLimit, false);

helper.assertTrue(paralleled.getB() == 2,"Expected Parallel amount to be 2, is %s.".formatted(paralleled.getB()));

helper.succeed();
}

@GameTest(template = "gtceu:ebf")
public void getMaxRecipeMultiplier_LimitFailureTest(GameTestHelper helper) {
BlockEntity holder = helper.getBlockEntity(new BlockPos(1, 1, 0));
if (!(holder instanceof MetaMachineBlockEntity atte)) {
helper.fail("wrong block at relative pos [1,1,0]!");
return;
}
MetaMachine machine = atte.getMetaMachine();
if (!(machine instanceof IRecipeLogicMachine rlm)) {
helper.fail("wrong machine in MetaMachineBlockEntity!");
return;
}

int parallelLimit = 4;

// Create a simple recipe to be used for testing
GTRecipe recipe = GTRecipeBuilder.ofRaw()
.inputItems(new ItemStack(Blocks.COBBLESTONE))
.inputFluids(GTMaterials.Acetone.getFluid(1000))
.outputItems(new ItemStack(Blocks.STONE))
.blastFurnaceTemp(1000)
.EUt(30).duration(100)
.buildRawRecipe();

((IItemTransfer)rlm.getCapabilitiesProxy().get(IO.IN, ItemRecipeCapability.CAP)).insertItem(0, new ItemStack(Blocks.COBBLESTONE, 16), false);
((IFluidTransfer)rlm.getCapabilitiesProxy().get(IO.IN, FluidRecipeCapability.CAP)).fill(GTMaterials.Acetone.getFluid(8000), false);

var paralleled = GTRecipeModifiers.accurateParallel(machine, recipe, parallelLimit, false);

helper.assertTrue(paralleled == null || paralleled.getB() == 0, "Parallel is too high, should be 0, is %s.".formatted(paralleled.getB()));

helper.succeed();
}

@GameTest(template = "gtceu:ebf")
public void getMaxRecipeMultiplier_ItemFailureTest(GameTestHelper helper) {
BlockEntity holder = helper.getBlockEntity(new BlockPos(1, 1, 0));
if (!(holder instanceof MetaMachineBlockEntity atte)) {
helper.fail("wrong block at relative pos [1,1,0]!");
return;
}
MetaMachine machine = atte.getMetaMachine();
if (!(machine instanceof IRecipeLogicMachine rlm)) {
helper.fail("wrong machine in MetaMachineBlockEntity!");
return;
}

int parallelLimit = 4;

// Create a simple recipe to be used for testing
GTRecipe recipe = GTRecipeBuilder.ofRaw()
.inputItems(new ItemStack(Blocks.COBBLESTONE))
.inputFluids(GTMaterials.Acetone.getFluid(100))
.outputItems(new ItemStack(Blocks.STONE))
.blastFurnaceTemp(1000)
.EUt(30).duration(100)
.buildRawRecipe();

((IItemTransfer)rlm.getCapabilitiesProxy().get(IO.IN, ItemRecipeCapability.CAP)).insertItem(0, new ItemStack(Blocks.COBBLESTONE, 16), false);
((IFluidTransfer)rlm.getCapabilitiesProxy().get(IO.IN, FluidRecipeCapability.CAP)).fill(GTMaterials.Naphtha.getFluid(8000), false);

var paralleled = GTRecipeModifiers.accurateParallel(machine, recipe, parallelLimit, false);

helper.assertTrue(paralleled == null || paralleled.getB() == 0, "Parallel is too high, should be 0, is %s.".formatted(paralleled.getB()));

helper.succeed();
}

// TODO add the rest of https://github.com/GregTechCEu/GregTech/blob/master/src/test/java/gregtech/api/recipes/logic/ParallelLogicTest.java.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.gregtechceu.gtceu.test.api.machine.trait;

import com.gregtechceu.gtceu.GTCEu;
import com.gregtechceu.gtceu.api.blockentity.MetaMachineBlockEntity;
import com.gregtechceu.gtceu.api.capability.recipe.IO;
import com.gregtechceu.gtceu.api.capability.recipe.ItemRecipeCapability;
import com.gregtechceu.gtceu.api.machine.MetaMachine;
import com.gregtechceu.gtceu.api.machine.feature.IRecipeLogicMachine;
import com.gregtechceu.gtceu.api.machine.trait.RecipeLogic;
import com.gregtechceu.gtceu.api.recipe.GTRecipe;
import com.gregtechceu.gtceu.common.data.GTRecipeTypes;
import com.gregtechceu.gtceu.core.mixins.RecipeManagerAccessor;
import com.gregtechceu.gtceu.data.recipe.builder.GTRecipeBuilder;
import com.lowdragmc.lowdraglib.side.item.IItemTransfer;
import net.minecraft.core.BlockPos;
import net.minecraft.gametest.framework.BeforeBatch;
import net.minecraft.gametest.framework.GameTest;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;

import java.util.HashMap;

public class RecipeLogicTest {

private static boolean hasInjectedRecipe = false;

@BeforeBatch(batch = GTCEu.MOD_ID)
public static void replaceRecipeManagerEntries(ServerLevel level) {
if (hasInjectedRecipe) return;
var recipes = new HashMap<>(((RecipeManagerAccessor) level.getRecipeManager()).getRawRecipes());
((RecipeManagerAccessor)level.getRecipeManager()).setRawRecipes(recipes);
recipes.replaceAll((k, v) -> new HashMap<>(v));
}

@GameTest(template = "gtceu:recipelogic")
public static void recipeLogicTest(GameTestHelper helper) {
// oops the BeforeBatch isn't registered.
RecipeLogicTest.replaceRecipeManagerEntries(helper.getLevel());

BlockEntity holder = helper.getBlockEntity(new BlockPos(0, 2, 0));
if (!(holder instanceof MetaMachineBlockEntity atte)) {
helper.fail("wrong block at relative pos [0,1,0]!");
return;
}
MetaMachine machine = atte.getMetaMachine();
if (!(machine instanceof IRecipeLogicMachine rlm)) {
helper.fail("wrong machine in MetaMachineBlockEntity!");
return;
}

GTRecipe recipe = GTRecipeBuilder.ofRaw()
.id(GTCEu.id("test"))
.inputItems(new ItemStack(Blocks.COBBLESTONE))
.outputItems(new ItemStack(Blocks.STONE))
.EUt(1).duration(1)
.buildRawRecipe();
// force insert the recipe into the manager.

if (!hasInjectedRecipe) {
((RecipeManagerAccessor) helper.getLevel().getRecipeManager()).getRawRecipes().get(GTRecipeTypes.CHEMICAL_RECIPES).put(GTCEu.id("test"), recipe);
hasInjectedRecipe = true;
}

RecipeLogic arl = rlm.getRecipeLogic();

arl.findAndHandleRecipe();

// no recipe found
helper.assertFalse(arl.isActive(), "Recipe logic is active, even when it shouldn't be");
helper.assertTrue(arl.getLastRecipe() == null, "Recipe logic has somehow found a recipe, when there should be none");

// put an item in the inventory that will trigger recipe recheck
((IItemTransfer)rlm.getCapabilitiesProxy().get(IO.IN, ItemRecipeCapability.CAP).get(0)).insertItem(0, new ItemStack(Blocks.COBBLESTONE, 16), false);
// Inputs change. did we detect it ?
// helper.assertTrue(arl.isRecipeDirty(), "Recipe is not dirty");
arl.findAndHandleRecipe();
helper.assertFalse(arl.getLastRecipe() == null, "Last recipe is empty, even though recipe logic should've found a recipe.");
helper.assertTrue(arl.isActive(), "Recipelogic is inactive, when it should be active.");
int stackCount = ((IItemTransfer)rlm.getCapabilitiesProxy().get(IO.IN, ItemRecipeCapability.CAP).get(0)).getStackInSlot(0).getCount();
helper.assertTrue(stackCount == 15, "Count is wrong (should be 15, when it's %s".formatted(stackCount));

// Save a reference to the old recipe so we can make sure it's getting reused
GTRecipe prev = arl.getLastRecipe();

// Finish the recipe, the output should generate, and the next iteration should begin
arl.serverTick();
helper.assertTrue(arl.getLastRecipe() == prev, "lastRecipe is wrong");
helper.assertTrue(ItemStack.isSameItem(((IItemTransfer)rlm.getCapabilitiesProxy().get(IO.OUT, ItemRecipeCapability.CAP).get(0)).getStackInSlot(0),
new ItemStack(Blocks.STONE, 1)), "wrong output stack.");
helper.assertTrue(arl.isActive(), "RecipeLogic is not active, when it should be.");

// Complete the second iteration, but the machine stops because its output is now full
((IItemTransfer)rlm.getCapabilitiesProxy().get(IO.OUT, ItemRecipeCapability.CAP).get(0)).setStackInSlot(0, new ItemStack(Blocks.STONE, 63));
((IItemTransfer)rlm.getCapabilitiesProxy().get(IO.OUT, ItemRecipeCapability.CAP).get(0)).setStackInSlot(1, new ItemStack(Blocks.STONE, 64));
arl.serverTick();
helper.assertFalse(arl.isActive(), "RecipeLogic is active, when it shouldn't be.");

// Try to process again and get failed out because of full buffer.
arl.serverTick();
helper.assertFalse(arl.isActive(), "Recipelogic is active, when it shouldn't be.");

// Some room is freed in the output bus, so we can continue now.
((IItemTransfer)rlm.getCapabilitiesProxy().get(IO.OUT, ItemRecipeCapability.CAP).get(0)).setStackInSlot(1, ItemStack.EMPTY);
arl.serverTick();
// helper.assertTrue(arl.isActive(), "Recipelogic is inactive.");
helper.assertTrue(ItemStack.isSameItem(((IItemTransfer)rlm.getCapabilitiesProxy().get(IO.OUT, ItemRecipeCapability.CAP).get(0)).getStackInSlot(0), new ItemStack(Blocks.STONE, 1)), "Wrong stack.");

// Finish.
helper.succeed();
}
}
Loading

0 comments on commit b3a891d

Please sign in to comment.