Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@ run
classes
logs
run-data
run-test
gource.sh
.noai
34 changes: 32 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ subprojects {
version = project(":").version
}

sourceSets {
test {
compileClasspath += main.compileClasspath + main.output

java {
srcDirs += ['src/main/java', 'src/test/java']
}
resources {
srcDirs += ['src/test/generated']
}
}
}

neoForge {
// we need the subprojects to evaluate first so that the sourceSets are properly constructed
evaluationDependsOnChildren()
Expand All @@ -107,6 +120,8 @@ neoForge {

validateAccessTransformers = true

addModdingDependenciesTo sourceSets.test

mods {
"${mod_id}" {
it.sourceSet this.sourceSets.main
Expand All @@ -115,6 +130,11 @@ neoForge {
'tf-asm' {
it.sourceSet project(":tf-asm").sourceSets.main
}

'test' {
sourceSet sourceSets.main
sourceSet sourceSets.test
}
}

def mainMod = mods."${project.mod_id}"
Expand All @@ -133,13 +153,11 @@ neoForge {

client {
client()
systemProperty 'forge.enabledGameTestNamespaces', mod_id
programArguments.addAll '--username', secrets.getProperty("username") ?: 'Dev', secrets.getProperty("uuid") ? '--uuid' : '', secrets.getProperty("uuid") ?: ''
}

server {
server()
systemProperty 'forge.enabledGameTestNamespaces', mod_id
programArgument '--nogui'
}

Expand All @@ -148,6 +166,14 @@ neoForge {
gameDirectory = project.file('run-data')
programArguments.addAll '--mod', mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath()
}

'gametest-server' {
type = 'gameTestServer'
gameDirectory = project.file('run-test')
systemProperty 'neoforge.enabledGameTestNamespaces', mod_id
sourceSet = sourceSets.test
loadedMods = [mods.'tf-asm', mods.'test']
}
}
}

Expand Down Expand Up @@ -206,6 +232,10 @@ repositories {
dependencies {
jarJar implementation(project(":tf-asm"))

testImplementation("net.neoforged:testframework:${neo_version}") {
transitive = false
}

def beanification = "tamaized:beanification:${project.beanification_version}"
implementation beanification
shade beanification
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ minecraft_version=1.21.1

# NeoForge: https://projects.neoforged.net/neoforged/neoforge
neo_version=21.1.83
mdg_version=2.0.76
mdg_version=2.0.107

# Deps
beanification_version=1.6.108
Expand Down
Binary file not shown.
161 changes: 161 additions & 0 deletions src/test/java/twilightforest/gametest/TFBlockTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package twilightforest.gametest;

import com.google.common.collect.Streams;
import net.minecraft.commands.arguments.EntityAnchorArgument;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.gametest.framework.GameTestHelper;
import net.minecraft.gametest.framework.TestFunction;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import net.neoforged.neoforge.gametest.GameTestHolder;
import net.neoforged.neoforge.registries.DeferredHolder;
import twilightforest.TwilightForestMod;
import twilightforest.init.TFBlocks;

import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;

@GameTestHolder(TwilightForestMod.ID)
public class TFBlockTests {

public static List<TestFunction> generateBlockRegistryTests() {
Collection<DeferredHolder<Block, ? extends Block>> tfBlocks = TFBlocks.BLOCKS.getEntries();

Stream<TestFunction> setBlocks = tfBlocks.stream()
.filter(Predicate.not(EXCLUDE_SETBLOCKS::contains))
.map(TFBlockTests::setBlockTest);

Stream<TestFunction> placeBlocks = tfBlocks.stream()
.filter(Predicate.not(EXCLUDE_PLACEBLOCKS::contains))
.map(DeferredHolder::value)
.map(Block::asItem)
.filter(blockItem -> !blockItem.getDefaultInstance().isEmpty())
.map(TFBlockTests::placeBlockTest);

return Streams.concat(setBlocks, placeBlocks).toList();
}

/**
* Fundamental test that sets every single block.
* <br>
* Helps catch any fundamental problems such as using clientside-specific code that would crash on a dedicated server.
*/
private static TestFunction setBlockTest(DeferredHolder<Block, ? extends Block> blockHolder) {
Consumer<GameTestHelper> setBlockTest = test -> {
for (BlockState state : blockHolder.value().getStateDefinition().getPossibleStates()) {
test.setBlock(BlockPos.ZERO, Blocks.AIR);
test.setBlock(BlockPos.ZERO, state);
test.assertBlockState(BlockPos.ZERO, state::equals, () -> "Expected placement of " + state + ", detected " + test.getBlockState(BlockPos.ZERO));
}
test.succeed(); // Assertion passed
};
return new TestFunction("setBlock", blockHolder.getId().toString(), "twilightforest:empty", Rotation.NONE, 1000, 0, true, false, 1, 1, false, setBlockTest);
}

private static final Set<DeferredHolder<Block, ? extends Block>> EXCLUDE_SETBLOCKS = Set.of(
TFBlocks.TWILIGHT_PORTAL, // Twilight Portal instantly reverts without supporting blocks
TFBlocks.UNCRAFTING_TABLE, // FIXME What's going on with its powered on state?
TFBlocks.CANDELABRA, // FIXME Candle property behavior
TFBlocks.CINDER_FURNACE // Unimplemented block
);

/**
* Fundamental test that places every single block with a mock player.
* <br>
* Triggers `Block#getStateForPlacement`
* <br>
* Helps catch any fundamental problems such as using clientside-specific code that would crash on a dedicated server.
*/
private static TestFunction placeBlockTest(Item item) {
Consumer<GameTestHelper> placeBlockTest = test -> {
test.setBlock(BlockPos.ZERO.below(), Blocks.DIRT); // Plant support
//test.setBlock(BlockPos.ZERO, Blocks.AIR);
ItemStack stack = new ItemStack(item);

Player player = test.makeMockPlayer(GameType.CREATIVE);
{ // Positions the player and makes them look at the block
Vec3 worldVecPos = Vec3.atBottomCenterOf(test.absolutePos(BlockPos.ZERO));
player.teleportTo(worldVecPos.x + 1, worldVecPos.y, worldVecPos.z + 1);
player.lookAt(EntityAnchorArgument.Anchor.EYES, worldVecPos);
}
player.setItemInHand(InteractionHand.MAIN_HAND, stack);

test.placeAt(player, stack, BlockPos.ZERO.above(), Direction.DOWN);
test.assertBlockState(BlockPos.ZERO, state -> !state.is(Blocks.AIR), () -> "Expected placement of " + item + ", detected " + test.getBlockState(BlockPos.ZERO));

test.succeed(); // All assertions passed
};

return new TestFunction(
"placeBlock",
item.toString(),
"twilightforest:empty",
Rotation.NONE,
1000,
0,
true,
false,
1,
1,
false,
placeBlockTest);
}

private static final Set<DeferredHolder<Block, ? extends Block>> EXCLUDE_PLACEBLOCKS = Set.of(
// TODO Requires dirt above
TFBlocks.TORCHBERRY_PLANT,
TFBlocks.ROOT_STRAND,
TFBlocks.TROLLVIDR,
TFBlocks.UNRIPE_TROLLBER,
TFBlocks.TROLLBER,
// TODO Requires water below
TFBlocks.HUGE_LILY_PAD,
TFBlocks.HUGE_WATER_LILY,
// TODO Requires adjacent support
TFBlocks.IRON_LADDER,
TFBlocks.ROPE,
TFBlocks.THORN_ROSE,
// TODO Requires block above
TFBlocks.TWILIGHT_OAK_HANGING_SIGN,
TFBlocks.CANOPY_HANGING_SIGN,
TFBlocks.MANGROVE_HANGING_SIGN,
TFBlocks.DARK_HANGING_SIGN,
TFBlocks.TIME_HANGING_SIGN,
TFBlocks.TRANSFORMATION_HANGING_SIGN,
TFBlocks.MINING_HANGING_SIGN,
TFBlocks.SORTING_HANGING_SIGN,
// TODO Requires block sideways
TFBlocks.TWILIGHT_OAK_WALL_HANGING_SIGN,
TFBlocks.CANOPY_WALL_HANGING_SIGN,
TFBlocks.MANGROVE_WALL_HANGING_SIGN,
TFBlocks.DARK_WALL_HANGING_SIGN,
TFBlocks.TIME_WALL_HANGING_SIGN,
TFBlocks.TRANSFORMATION_WALL_HANGING_SIGN,
TFBlocks.MINING_WALL_HANGING_SIGN,
TFBlocks.SORTING_WALL_HANGING_SIGN,
// FIXME Crashes game, cherry-pick 44c2d5650e41ade9cbca70be7ce9b5b9e402b5ac into 1.21.1
TFBlocks.TROLLSTEINN,
// FIXME why do these fail?
TFBlocks.FALLEN_LEAVES,
TFBlocks.GIANT_COBBLESTONE,
TFBlocks.GIANT_LOG,
TFBlocks.GIANT_LEAVES,
TFBlocks.GIANT_OBSIDIAN,
// NYI
TFBlocks.CINDER_FURNACE
);

}
31 changes: 31 additions & 0 deletions src/test/java/twilightforest/gametest/TFGameTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package twilightforest.gametest;

import net.minecraft.gametest.framework.GameTestGenerator;
import net.minecraft.gametest.framework.TestFunction;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.RegisterGameTestsEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import twilightforest.TwilightForestMod;

import java.util.Collection;

/**
* Entrypoint for Twilight Forest's game-integrated tests
*/
@EventBusSubscriber(bus = EventBusSubscriber.Bus.MOD, modid = TwilightForestMod.ID)
public final class TFGameTests {
public static final Logger LOGGER = LogManager.getLogger(TwilightForestMod.ID + "tests");

@SubscribeEvent
public static void registerBlockTests(RegisterGameTestsEvent event) {
LOGGER.info("Starting registerBlockTests");
event.register(TFGameTests.class);
}

@GameTestGenerator
public static Collection<TestFunction> generateBlockTests() {
return TFBlockTests.generateBlockRegistryTests();
}
}
Loading