diff --git a/pom.xml b/pom.xml index 166bc7e..bad763c 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,13 @@ jnativehook 2.2.2 + + + org.junit.jupiter + junit-jupiter + 6.1.3 + test + @@ -48,6 +55,12 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 3.6.0 + + org.sonarsource.scanner.maven sonar-maven-plugin diff --git a/src/main/java/wtf/beatrice/autosqueal/util/SystemUtil.java b/src/main/java/wtf/beatrice/autosqueal/util/SystemUtil.java index 4df5939..3fe43d0 100644 --- a/src/main/java/wtf/beatrice/autosqueal/util/SystemUtil.java +++ b/src/main/java/wtf/beatrice/autosqueal/util/SystemUtil.java @@ -6,14 +6,18 @@ public class SystemUtil { public static OperatingSystem getHostSystem() { - String osName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); - if (osName.contains("win")) { + return fromOsName(System.getProperty("os.name")); + } + + public static OperatingSystem fromOsName(String osName) { + String normalizedOsName = osName.toLowerCase(Locale.ENGLISH); + if (normalizedOsName.contains("win")) { return OperatingSystem.WINDOWS; - } else if (osName.contains("nix") || - osName.contains("nux") || - osName.contains("aix")) { + } else if (normalizedOsName.contains("nix") || + normalizedOsName.contains("nux") || + normalizedOsName.contains("aix")) { return OperatingSystem.LINUX; - } else if (osName.contains("mac")) { + } else if (normalizedOsName.contains("mac")) { return OperatingSystem.MAC_OS; } else { return OperatingSystem.UNKNOWN; diff --git a/src/test/java/wtf/beatrice/autosqueal/controls/SingleStepMovementTaskTest.java b/src/test/java/wtf/beatrice/autosqueal/controls/SingleStepMovementTaskTest.java new file mode 100644 index 0000000..bf1cb42 --- /dev/null +++ b/src/test/java/wtf/beatrice/autosqueal/controls/SingleStepMovementTaskTest.java @@ -0,0 +1,117 @@ +package wtf.beatrice.autosqueal.controls; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; + +class SingleStepMovementTaskTest +{ + private static final int MAX_TICKS = 20_000; + private static final float DELTA = 0.0f; + + @Test + void noStepWhenAlreadyAtDestination() { + assertEquals(0.0f, SingleStepMovementTask.computeStepX(0, 0), DELTA); + assertEquals(0.0f, SingleStepMovementTask.computeStepY(0, 0), DELTA); + } + + @Test + void axisAlignedMovementsProduceNoInfiniteSteps() { + // moving only horizontally: the y step must be 0, not Infinity or NaN + assertEquals(1.0f, SingleStepMovementTask.computeStepX(100, 0), DELTA); + assertEquals(0.0f, SingleStepMovementTask.computeStepY(100, 0), DELTA); + + // moving only vertically: the x step must be 0, not Infinity or NaN + assertEquals(0.0f, SingleStepMovementTask.computeStepX(0, 100), DELTA); + assertEquals(1.0f, SingleStepMovementTask.computeStepY(0, 100), DELTA); + } + + @Test + void diagonalMovementsMoveBothAxes() { + assertEquals(1.0f, SingleStepMovementTask.computeStepX(100, 100), DELTA); + assertEquals(1.0f, SingleStepMovementTask.computeStepY(100, 100), DELTA); + + assertEquals(0.5f, SingleStepMovementTask.computeStepX(50, 100), DELTA); + assertEquals(1.0f, SingleStepMovementTask.computeStepY(50, 100), DELTA); + + assertEquals(1.0f, SingleStepMovementTask.computeStepX(100, 50), DELTA); + assertEquals(0.5f, SingleStepMovementTask.computeStepY(100, 50), DELTA); + } + + @Test + void stepsAreNeverInfiniteOrNaN() { + for (int lengthX = 0; lengthX <= 400; lengthX += 7) { + for (int lengthY = 0; lengthY <= 400; lengthY += 7) { + float stepX = SingleStepMovementTask.computeStepX(lengthX, lengthY); + float stepY = SingleStepMovementTask.computeStepY(lengthX, lengthY); + + assertFalse(Float.isInfinite(stepX), "infinite stepX for lengths [" + lengthX + ", " + lengthY + "]"); + assertFalse(Float.isNaN(stepX), "NaN stepX for lengths [" + lengthX + ", " + lengthY + "]"); + assertFalse(Float.isInfinite(stepY), "infinite stepY for lengths [" + lengthX + ", " + lengthY + "]"); + assertFalse(Float.isNaN(stepY), "NaN stepY for lengths [" + lengthX + ", " + lengthY + "]"); + } + } + } + + @Test + void advanceMovesTowardTheDestination() { + assertEquals(5.0f, SingleStepMovementTask.advance(5.0f, 5.0f, 1.0f), DELTA); // already there + assertEquals(9.0f, SingleStepMovementTask.advance(10.0f, 5.0f, 1.0f), DELTA); // decreasing + assertEquals(6.0f, SingleStepMovementTask.advance(5.0f, 10.0f, 1.0f), DELTA); // increasing + } + + @Test + void adjustedStepUsesExactRemainderWhenClose() { + assertEquals(0.4f, SingleStepMovementTask.adjustedStep(10.0f, 10.4f, 1.0f), 0.00001f); + assertEquals(0.4f, SingleStepMovementTask.adjustedStep(10.8f, 10.4f, 1.0f), 0.00001f); + assertEquals(1.0f, SingleStepMovementTask.adjustedStep(10.0f, 20.0f, 1.0f), DELTA); + } + + @Test + void movementConvergesToDestination() { + record MovementCase(float startX, float startY, int destX, int destY) {} + + MovementCase[] cases = { + new MovementCase(100, 100, 500, 500), // plain diagonal + new MovementCase(500, 500, 100, 100), // going up-left + new MovementCase(100, 100, 500, 100), // horizontal (used to break: infinite step) + new MovementCase(500, 100, 100, 100), // horizontal, other direction + new MovementCase(100, 100, 100, 500), // vertical (used to break: infinite step) + new MovementCase(100, 500, 100, 100), // vertical, other direction + new MovementCase(105, 105, 105, 105), // already at destination + new MovementCase(0, 0, 2000, 1000), // steep diagonal + new MovementCase(2000, 1000, 0, 0), // same, backwards + new MovementCase(37, 173, 2311, 997), // random-ish, both directions mixed + }; + + for (MovementCase move : cases) { + float x = move.startX(); + float y = move.startY(); + + int lengthX = Math.round(Math.abs(x - move.destX())); + int lengthY = Math.round(Math.abs(y - move.destY())); + float stepX = SingleStepMovementTask.computeStepX(lengthX, lengthY); + float stepY = SingleStepMovementTask.computeStepY(lengthX, lengthY); + + boolean converged = false; + for (int tick = 0; tick < MAX_TICKS; tick++) { + if (Math.round(x) == move.destX() && Math.round(y) == move.destY()) { + converged = true; + break; + } + + stepX = SingleStepMovementTask.adjustedStep(x, move.destX(), stepX); + stepY = SingleStepMovementTask.adjustedStep(y, move.destY(), stepY); + x = SingleStepMovementTask.advance(x, move.destX(), stepX); + y = SingleStepMovementTask.advance(y, move.destY(), stepY); + } + + if (!converged) { + fail("movement did not converge: stuck at [" + Math.round(x) + ", " + Math.round(y) + + "] while heading to [" + move.destX() + ", " + move.destY() + "]"); + } + } + } +} \ No newline at end of file diff --git a/src/test/java/wtf/beatrice/autosqueal/util/SystemUtilTest.java b/src/test/java/wtf/beatrice/autosqueal/util/SystemUtilTest.java new file mode 100644 index 0000000..f2f1427 --- /dev/null +++ b/src/test/java/wtf/beatrice/autosqueal/util/SystemUtilTest.java @@ -0,0 +1,34 @@ +package wtf.beatrice.autosqueal.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SystemUtilTest +{ + @Test + void detectsWindows() { + assertEquals(SystemUtil.OperatingSystem.WINDOWS, SystemUtil.fromOsName("Windows 11")); + } + + @Test + void detectsLinux() { + assertEquals(SystemUtil.OperatingSystem.LINUX, SystemUtil.fromOsName("Linux")); + assertEquals(SystemUtil.OperatingSystem.LINUX, SystemUtil.fromOsName("GNU/Linux")); + } + + @Test + void detectsMacOS() { + assertEquals(SystemUtil.OperatingSystem.MAC_OS, SystemUtil.fromOsName("Mac OS X")); + } + + @Test + void unknownForOtherSystems() { + assertEquals(SystemUtil.OperatingSystem.UNKNOWN, SystemUtil.fromOsName("SunOS")); + } + + @Test + void hostSystemMatchesOsName() { + assertEquals(SystemUtil.fromOsName(System.getProperty("os.name")), SystemUtil.getHostSystem()); + } +} \ No newline at end of file