Skip to content

Commit

Permalink
Add connection test helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
Pyrofab committed Dec 6, 2021
1 parent 2b9d71d commit ec13f7e
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 7 deletions.
37 changes: 36 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,43 @@ plugins {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17

archivesBaseName = project.mod_name
archivesBaseName = project.archives_base_name
version = project.mod_version
group = project.maven_group

sourceSets {
testmod {
compileClasspath += main.compileClasspath
runtimeClasspath += main.runtimeClasspath
}
}

loom {
runs {
gametest {
server()
ideConfigGenerated true
source sourceSets.testmod

name "Elmendorf Game Test"

// Enable the gametest runner
vmArg "-Dfabric-api.gametest"
vmArg "-Dfabric-api.gametest.report-file=${project.buildDir}/junit.xml"
runDir "build/gametest"
}
autoTestServer {
server()
ideConfigGenerated true
source sourceSets.testmod

name "Auto Test Server"

vmArg "-Dfabric.autoTest"
}
}
}

repositories {
// Add repositories to retrieve artifacts from in here.
// You should only use this when depending on other mods because
Expand All @@ -34,6 +67,8 @@ dependencies {

// Fabric API. This is technically optional, but you probably want it anyway.
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
api 'junit:junit:4.13'
testmodImplementation sourceSets.main.output
}

processResources {
Expand Down
7 changes: 7 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
------------------------------------------------------
Version 0.2.0
------------------------------------------------------
**Additions**
- Added a test framework for clientbound packet sending
- Added some helper methods for assertions

------------------------------------------------------
Version 0.1.1
------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ org.gradle.jvmargs=-Xmx1G
loader_version=0.12.6

# Mod Properties
mod_version = 0.1.1
mod_version = 0.2.0
maven_group = io.github.ladysnake
archives_base_name = elmendorf

Expand Down
2 changes: 1 addition & 1 deletion release.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ artifactory {
}
}

artifactoryPublish.dependsOn(checkGitStatus)
//artifactoryPublish.dependsOn(checkGitStatus)
artifactoryPublish.dependsOn build

task release(dependsOn: [tasks.publish, tasks.githubRelease, tasks.artifactoryPublish]) {
Expand Down
2 changes: 2 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ pluginManagement {
gradlePluginPortal()
}
}

rootProject.name = 'elmendorf'
66 changes: 66 additions & 0 deletions src/main/java/io/github/ladysnake/elmendorf/ConnectionChecker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2021 Ladysnake
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package io.github.ladysnake.elmendorf;

import net.minecraft.network.Packet;
import net.minecraft.util.Identifier;

import java.util.List;
import java.util.Queue;
import java.util.function.Consumer;
import java.util.function.Predicate;

public interface ConnectionChecker {
PacketChecker sent(Class<? extends Packet<?>> packetType);

PacketChecker sent(Identifier channelId);

PacketChecker sent(Predicate<Packet<?>> test, String errorMessage);

void sentPackets(Consumer<Queue<Packet<?>>> test);

final class PacketChecker {
private final List<Packet<?>> packets;
private final String defaultErrorMessage;

public PacketChecker(List<Packet<?>> packets, String defaultErrorMessage) {
this.packets = packets;
this.defaultErrorMessage = defaultErrorMessage;
}

public void atLeast(int times) {
GameTestUtil.assertTrue("%s to be sent at least %d times, was %d".formatted(defaultErrorMessage, times, this.packets.size()), this.packets.size() >= times);
}

public void atLeast(String errorMessage, int times) {
GameTestUtil.assertTrue(errorMessage, this.packets.size() >= times);
}

public void exactly(int times) {
GameTestUtil.assertTrue("%s to be sent %d times, was %d".formatted(defaultErrorMessage, times, this.packets.size()), this.packets.size() == times);
}

public void exactly(int times, String errorMessage) {
GameTestUtil.assertTrue(errorMessage, this.packets.size() == times);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (C) 2021 Ladysnake
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package io.github.ladysnake.elmendorf;

public interface ConnectionTestConfiguration {
void toFlushPacketsEachTick();
}
39 changes: 35 additions & 4 deletions src/main/java/io/github/ladysnake/elmendorf/GameTestUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,54 @@
package io.github.ladysnake.elmendorf;

import com.mojang.authlib.GameProfile;
import io.github.ladysnake.elmendorf.impl.MockClientConnection;
import net.minecraft.network.NetworkSide;
import net.minecraft.server.network.ServerPlayNetworkHandler;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.test.GameTestException;
import net.minecraft.test.TestContext;
import net.minecraft.util.math.Vec3d;
import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
import org.junit.function.ThrowingRunnable;

import java.util.UUID;
import java.util.concurrent.Callable;

public final class GameTestUtil {
public static void assertTrue(boolean b, String errorMessage) {
public static void assertTrue(String errorMessage, boolean b) {
if (!b) throw new GameTestException(errorMessage);
}
public static void assertFalse(String errorMessage, boolean b) {
if (b) throw new GameTestException(errorMessage);
}

public static void assertThrows(Class<? extends Throwable> expectedThrowable, ThrowingRunnable runnable) {
assertThrows(null, expectedThrowable, runnable);
}

public static void assertThrows(@Nullable String errorMessage, Class<? extends Throwable> expectedThrowable, ThrowingRunnable runnable) {
try {
Assert.assertThrows(errorMessage, expectedThrowable, runnable);
} catch (AssertionError e) {
throw new GameTestException(e.getMessage());
}
}

public static ServerPlayerEntity spawnPlayer(TestContext ctx, double x, double y, double z) {
ServerPlayerEntity mockPlayer = new ServerPlayerEntity(ctx.getWorld().getServer(), ctx.getWorld(), new GameProfile(UUID.randomUUID(), "test-mock-player"));
Vec3d vec3d = ctx.getAbsolute(new Vec3d(x, y, z));
mockPlayer.refreshPositionAndAngles(vec3d.x, vec3d.y, vec3d.z, mockPlayer.getYaw(), mockPlayer.getPitch());
var mockPlayer = new ServerPlayerEntity(ctx.getWorld().getServer(), ctx.getWorld(), new GameProfile(UUID.randomUUID(), "test-mock-player"));
var connection = new MockClientConnection(NetworkSide.CLIENTBOUND);
mockPlayer.setPosition(ctx.getAbsolute(new Vec3d(x, y, z)));
mockPlayer.networkHandler = new ServerPlayNetworkHandler(ctx.getWorld().getServer(), connection, mockPlayer);
ctx.getWorld().spawnEntity(mockPlayer);
return mockPlayer;
}

public static ConnectionTestConfiguration configureConnection(ServerPlayerEntity player) {
return ((MockClientConnection) player.networkHandler.connection);
}

public static ConnectionChecker verifyConnection(ServerPlayerEntity player) {
return ((MockClientConnection) player.networkHandler.connection);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright (C) 2021 Ladysnake
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package io.github.ladysnake.elmendorf.impl;

import io.github.ladysnake.elmendorf.ConnectionChecker;
import io.github.ladysnake.elmendorf.ConnectionTestConfiguration;
import io.github.ladysnake.elmendorf.GameTestUtil;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.NetworkSide;
import net.minecraft.network.Packet;
import net.minecraft.network.packet.s2c.play.CustomPayloadS2CPacket;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayDeque;
import java.util.List;
import java.util.Objects;
import java.util.Queue;
import java.util.function.Consumer;
import java.util.function.Predicate;

public class MockClientConnection extends ClientConnection implements ConnectionChecker, ConnectionTestConfiguration {
private final Queue<Packet<?>> packetQueue = new ArrayDeque<>();
private boolean flushEachTick;

public MockClientConnection(NetworkSide side) {
super(side);
}

@Override
public void toFlushPacketsEachTick() {
this.flushEachTick = true;
}

@Override
public void tick() {
super.tick();
if (this.flushEachTick) {
this.packetQueue.clear();
}
}

@Override
public PacketChecker sent(Class<? extends Packet<?>> packetType) {
return sent(packetType::isInstance, "Expected packet of type " + packetType.getTypeName());
}

@Override
public PacketChecker sent(Identifier channelId) {
return sent(packet -> packet instanceof CustomPayloadS2CPacket p && Objects.equals(p.getChannel(), channelId), "Expected packet for channel " + channelId);
}

@Override
public PacketChecker sent(Predicate<Packet<?>> test, String errorMessage) {
List<Packet<?>> packets = this.packetQueue.stream().filter(test).toList();
GameTestUtil.assertFalse(errorMessage, packets.isEmpty());
return new PacketChecker(packets, errorMessage);
}

@Override
public void sentPackets(Consumer<Queue<Packet<?>>> test) {
test.accept(this.packetQueue);
}

@Override
public boolean isOpen() {
return true;
}

@Override
public void send(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> callback) {
this.packetQueue.add(packet);
}

public Queue<Packet<?>> getPacketQueue() {
return packetQueue;
}

}
43 changes: 43 additions & 0 deletions src/testmod/java/io/github/ladysnake/ripstop/RipstopTestSuite.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2021 Ladysnake
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package io.github.ladysnake.ripstop;

import io.github.ladysnake.elmendorf.GameTestUtil;
import net.fabricmc.fabric.api.gametest.v1.FabricGameTest;
import net.minecraft.network.packet.s2c.play.ClearTitleS2CPacket;
import net.minecraft.test.GameTest;
import net.minecraft.test.GameTestException;
import net.minecraft.test.TestContext;
import net.minecraft.util.Identifier;

public class RipstopTestSuite implements FabricGameTest {
@GameTest(structureName = EMPTY_STRUCTURE)
public void test(TestContext ctx) {
var player = GameTestUtil.spawnPlayer(ctx, 5, 5, 5);
player.networkHandler.sendPacket(new ClearTitleS2CPacket(true));
player.networkHandler.sendPacket(new ClearTitleS2CPacket(false));
GameTestUtil.verifyConnection(player).sent(ClearTitleS2CPacket.class).atLeast(2);
GameTestUtil.assertThrows(GameTestException.class,
() -> GameTestUtil.verifyConnection(player).sent(new Identifier("ribbit")));
ctx.complete();
}
}
Loading

0 comments on commit ec13f7e

Please sign in to comment.