Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial unit tests #153

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bb3109d
Add JUnit 5 dependency
MattSturgeon Jan 6, 2024
5358c5e
Add Mockito dependency
MattSturgeon Jan 6, 2024
6b3ffab
Use `run` working directory for tests
MattSturgeon Jan 6, 2024
4130505
Create BootstrapMinecraft annotation
shartte Jan 6, 2024
960e295
Stub FreecamPosition test
MattSturgeon Jan 6, 2024
dd36db9
Continue stubbing initial test
MattSturgeon Jan 6, 2024
badced0
Add AssertJ dependency
MattSturgeon Jan 12, 2024
68dd556
Add JUnit Parameterized Tests dependency
MattSturgeon Jan 12, 2024
c73815a
Add a reflection util
MattSturgeon Jan 12, 2024
248911e
AssertJ setRotation test
MattSturgeon Jan 12, 2024
f452942
Test quaternion is updated by setRotation
MattSturgeon Jan 12, 2024
e068b40
Add EnableMockito annotation
MattSturgeon Jan 19, 2024
ec2adb2
Use EnableMockito annotation
MattSturgeon Jan 19, 2024
e2d915d
Move testing utils to separate project
MattSturgeon Jan 19, 2024
6b507b7
Fix testing utils imports
MattSturgeon Jan 19, 2024
8b100b8
Enable fabric-specific tests
MattSturgeon Jan 19, 2024
8450d32
Audit mixin in fabric environment
MattSturgeon Jan 19, 2024
1563829
Validate Minecraft in fabric environment
MattSturgeon Jan 19, 2024
357f0ae
Test moveForward() together with setRotation()
MattSturgeon Jan 19, 2024
b2a3c0a
Add constructor test for swimming position
MattSturgeon Jan 19, 2024
ff74175
Minor improvements
MattSturgeon Jan 19, 2024
bc48eca
ci: new build workflow
MattSturgeon Jan 14, 2024
41e7fcb
ci: run tests in main workflow
MattSturgeon Jan 19, 2024
953678b
ci: add status badge to README
MattSturgeon Jan 19, 2024
e421196
Add constructor rotation test
MattSturgeon Jan 19, 2024
42b7581
fixup! Add constructor test for swimming position
MattSturgeon Jan 19, 2024
e4e2c18
Test moving position after mirroring
MattSturgeon Jan 19, 2024
e6fa497
Test chunk pos
MattSturgeon Jan 19, 2024
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
35 changes: 35 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Gradle (build & test)

on:
push:
branches: &branches
- 'main'
- '1.16'
- '1.17'
- '1.18'
- '1.19'
pull_request:
branches: *branches
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow is not valid. .github/workflows/main.yml:
Anchors are not currently supported. Remove the anchor 'branches'

I guess GitHub doesn't like yaml anchors...

Suggested change
branches: *branches
branches:
- 'main'
- '1.16'
- '1.17'
- '1.18'
- '1.19'


permissions:
contents: read

jobs:
build_and_test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: microsoft
- name: Build
uses: gradle/gradle-build-action@v2
with:
arguments: build
- name: Test
uses: gradle/gradle-build-action@v2
with:
# We could run `check` instead, but we may want to add checks we don't want to use in CI...?
arguments: test
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Freecam
![CI](https://github.com/MinecraftFreecam/Freecam/actions/workflows/main.yml/badge.svg?event=push)
[![Crowdin](https://badges.crowdin.net/freecam/localized.svg)](https://crowdin.com/project/freecam)

This mod allows you to control your camera separately from your player. While it is enabled, you can fly around and travel through blocks within your render distance. Disabling it will restore you to your original position. This can be useful for quickly inspecting builds and exploring your world.
Expand Down
18 changes: 18 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ subprojects {
officialMojangMappings()
parchment("org.parchmentmc.data:parchment-${parchmentAppendix}:${parchmentVersion}@zip")
}

testImplementation(platform("org.junit:junit-bom:${rootProject.junit_version}"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.junit.jupiter:junit-jupiter-params")
testImplementation "org.assertj:assertj-core:${rootProject.assertj_version}"
testImplementation "org.mockito:mockito-core:${rootProject.mockito_version}"
testImplementation "org.mockito:mockito-junit-jupiter:${rootProject.mockito_version}"

if (project.name != "test-util") {
dependencies.testImplementation dependencies.project(path: ":test-utils", configuration: "namedElements")
}
}

test {
def dir = project.file "run"
dir.mkdirs()
workingDir dir
useJUnitPlatform()
}
}

Expand Down
205 changes: 205 additions & 0 deletions common/src/test/java/net/xolt/freecam/util/FreecamPositionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package net.xolt.freecam.util;

import com.mojang.authlib.GameProfile;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.player.RemotePlayer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.Pose;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.phys.Vec2;
import net.minecraft.world.phys.Vec3;
import net.xolt.freecam.testing.extension.BootstrapMinecraft;
import net.xolt.freecam.testing.extension.EnableMockito;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.withPrecision;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@EnableMockito
@BootstrapMinecraft
class FreecamPositionTest {

Entity entity;
FreecamPosition position;

static double[] distances() {
return new double[] { 1, -1, 2_000_000_001, 0.00456789, -0.0000445646456456060456, 2.5 };
}

static Vec2[] rotations() {
return new Vec2[] { Vec2.ZERO, Vec2.MIN, Vec2.MAX, Vec2.UNIT_X, Vec2.UNIT_Y, Vec2.NEG_UNIT_X, Vec2.NEG_UNIT_Y };
}

static Vec3[] positions() {
return new Vec3[] { Vec3.ZERO, new Vec3(1, 1, 1), new Vec3(1000, 100, 10) };
}

@BeforeEach
void setUp() {
ClientLevel level = mock(ClientLevel.class);
when(level.getSharedSpawnPos()).thenReturn(BlockPos.ZERO);
when(level.getSharedSpawnAngle()).thenReturn(0f);
GameProfile profile = new GameProfile(new UUID(0, 0), "TestPlayer");
entity = new RemotePlayer(level, profile);
position = new FreecamPosition(entity);
}

@AfterEach
void tearDown() {
}

@ParameterizedTest
@EnumSource(Pose.class)
@DisplayName("Use entity position, adjusted for pose")
void init_position(Pose pose) {
entity.setPose(pose);
double diff = entity.getEyeHeight(pose) - entity.getEyeHeight(Pose.SWIMMING);
FreecamPosition swimPos = new FreecamPosition(entity);

assertThat(swimPos.x).as("x is %01.2f".formatted(entity.getX())).isEqualTo(entity.getX());
assertThat(swimPos.y).as("y is %01.2f higher than %01.2f".formatted(diff, entity.getY())).isEqualTo(entity.getY() + diff, withPrecision(0.0000004));
assertThat(swimPos.z).as("z is %01.2f".formatted(entity.getZ())).isEqualTo(entity.getZ());
}

@ParameterizedTest
@MethodSource("rotations")
@DisplayName("Uses entity rotation")
void init_rotation(Vec2 rotation) {
entity.setXRot(rotation.x);
entity.setYRot(rotation.y);
FreecamPosition rotatedPos = new FreecamPosition(entity);

assertThat(rotatedPos.yaw).as("yaw is %01.2f".formatted(rotation.y)).isEqualTo(rotation.y);
assertThat(rotatedPos.pitch).as("pitch is %01.2f".formatted(rotation.x)).isEqualTo(rotation.x);
}

@ParameterizedTest
@MethodSource("distances")
@DisplayName("Moves forward on x axis")
void moveForward_x(double distance) {
float yaw = -90;
float pitch = 0;

double x = position.x;
double y = position.y;
double z = position.z;

position.setRotation(yaw, pitch);
position.moveForward(distance);

assertThat(position.x).as("x increased by " + distance).isEqualTo(x + distance);
assertThat(position.y).as("y is unchanged").isEqualTo(y);
assertThat(position.z).as("z is unchanged").isEqualTo(z);

// Moving the same distance after a mirror should revert
position.mirrorRotation();
position.moveForward(distance);

assertThat(position.x).as("x is reverted").isEqualTo(x);
assertThat(position.y).as("y is unchanged").isEqualTo(y);
assertThat(position.z).as("z is unchanged").isEqualTo(z);
}

@ParameterizedTest
@MethodSource("distances")
@DisplayName("Moves forward on y axis")
void moveForward_y(double distance) {
float yaw = 0;
float pitch = -90;

double x = position.x;
double y = position.y;
double z = position.z;

position.setRotation(yaw, pitch);
position.moveForward(distance);

assertThat(position.x).as("x is unchanged").isEqualTo(x);
assertThat(position.y).as("y increased by " + distance).isEqualTo(y + distance);
assertThat(position.z).as("z is unchanged").isEqualTo(z);

// Moving the same distance after a mirror should revert
position.mirrorRotation();
position.moveForward(distance);

assertThat(position.x).as("x is unchanged").isEqualTo(x);
assertThat(position.y).as("y is reverted").isEqualTo(y);
assertThat(position.z).as("z is unchanged").isEqualTo(z);
}

@ParameterizedTest
@MethodSource("distances")
@DisplayName("Moves forward on z axis")
void moveForward_z(double distance) {
float yaw = 0;
float pitch = 0;

double x = position.x;
double y = position.y;
double z = position.z;

position.setRotation(yaw, pitch);
position.moveForward(distance);

assertThat(position.x).as("x is unchanged").isEqualTo(x);
assertThat(position.y).as("y is unchanged").isEqualTo(y);
assertThat(position.z).as("z increased by " + distance).isEqualTo(z + distance);

// Moving the same distance after a mirror should revert
position.mirrorRotation();
position.moveForward(distance);

assertThat(position.x).as("x is unchanged").isEqualTo(x);
assertThat(position.y).as("y is unchanged").isEqualTo(y);
assertThat(position.z).as("z is reverted").isEqualTo(z);
}

@ParameterizedTest
@DisplayName("setRotation correctly sets yaw & pitch")
@ValueSource(floats = { -16.456f, 0, 10, 2.5f, 2000008896.546f })
void setRotation_YawPitch(float number) {
final float constant = 10;
assertThat(position).isNotNull().satisfies(
position -> {
position.setRotation(number, constant);
assertThat(position).as("Yaw is set correctly").satisfies(
p -> assertThat(p.yaw).as("Yaw is set to (var) " + number).isEqualTo(number),
p -> assertThat(p.pitch).as("Pitch is set to (const) " + constant).isEqualTo(constant)
);
},
position -> {
position.setRotation(constant, number);
assertThat(position).as("Pitch is set correctly").satisfies(
p -> assertThat(p.yaw).as("Yaw is set to (const) " + constant).isEqualTo(constant),
p -> assertThat(p.pitch).as("Pitch is set to (var) " + number).isEqualTo(number)
);
}
);
}

@ParameterizedTest
@MethodSource("positions")
@DisplayName("ChunkPos should be 16 times smaller than position")
void chunkPos(Vec3 pos) {
position.x = pos.x;
position.y = pos.y;
position.z = pos.z;
// Should be 16 times smaller than x y z position, rounded down
int x = (int) (pos.x / 16);
int z = (int) (pos.z / 16);
ChunkPos chunkPos = position.getChunkPos();
assertThat(chunkPos.x).isEqualTo(x);
assertThat(chunkPos.z).isEqualTo(z);
}
}
12 changes: 12 additions & 0 deletions fabric/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ dependencies {
shadowCommon project(path: ":common", configuration: "transformProductionFabric")

shadowCommon project(":variant:api")

// Use fabric's Knot classloader in tests
testImplementation "net.fabricmc:fabric-loader-junit:${rootProject.fabric_loader_version}"
}

variants.each { variant ->
Expand All @@ -58,6 +61,15 @@ variants.each { variant ->
dependencies.add(set.implementationConfigurationName, dependencies.project(path: ":variant:${variant}", configuration: "namedElements"))
dependencies.add(shadowConfig.name, dependencies.project(path: ":variant:${variant}", configuration: "transformProductionFabric"))

// Add the normal variant to the test classpath
// TODO consider testing other variants too
sourceSets {
if (variant == "normal") {
test.compileClasspath += set.compileClasspath
test.runtimeClasspath += set.runtimeClasspath
}
}

// Configure/create a run config
def run
if (variant == "normal") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package net.xolt.freecam.environment;

import net.minecraft.server.Bootstrap;
import net.xolt.freecam.testing.extension.BootstrapMinecraft;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@BootstrapMinecraft
class BootstrapTest {
@Test
@DisplayName("Validate Minecraft is bootstrapped")
void validateBootstrap() {
Bootstrap.validate();
}
}
15 changes: 15 additions & 0 deletions fabric/src/test/java/net/xolt/freecam/environment/MixinTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package net.xolt.freecam.environment;

import net.xolt.freecam.testing.extension.BootstrapMinecraft;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.spongepowered.asm.mixin.MixinEnvironment;

@BootstrapMinecraft
class MixinTest {
@Test
@DisplayName("Audit mixin environment")
void mixinEnvironmentAudit() {
MixinEnvironment.getCurrentEnvironment().audit();
}
}
4 changes: 4 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ neoforge_req=[20,)
# https://mvnrepository.com/artifact/me.shedaniel.cloth/cloth-config?repo=architectury
modmenu_version=9.0.0-pre.1
cloth_version=13.0.114

junit_version=5.10.1
assertj_version=3.25.1
mockito_version=5.8.0
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ include("common")
include("fabric")
include("neoforge")
include("metadata")
include("test-utils")
include("variant", "variant:api", "variant:normal", "variant:modrinth")

rootProject.name = "freecam"
11 changes: 11 additions & 0 deletions test-utils/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
architectury {
common(rootProject.enabled_platforms.split(','))
}

dependencies {
// Needed for Environment annotation
modCompileOnly "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}"
implementation(platform("org.junit:junit-bom:${rootProject.junit_version}"))
implementation("org.junit.jupiter:junit-jupiter-api")
implementation "org.mockito:mockito-junit-jupiter:${rootProject.mockito_version}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.xolt.freecam.testing.extension;

import org.junit.jupiter.api.extension.ExtendWith;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Ensure Minecraft is bootstrapped before running tests.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(BootstrapMinecraftExtension.class)
public @interface BootstrapMinecraft {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package net.xolt.freecam.testing.extension;

import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;

public class BootstrapMinecraftExtension implements Extension, BeforeAllCallback {
@Override
public void beforeAll(ExtensionContext context) {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
}
Loading