From 2609bc6a2849c6ac0550228d1a3fe315237b9323 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 09:35:25 +0000 Subject: [PATCH] Add unit tests for CoreCMIHook, Parser, and Sfun branch coverage Add new test files targeting uncovered branches: - CoreCMIHookTest: covers offline-player and non-Player entity branches - ParserAdditionalBranchTest: covers character filtering, exponential notation edge cases, getTree caching, implicit multiplication, chained operators, and OperatorNode toString bracket logic - SfunAdditionalBranchTest: covers sinh large values, asinh/atanh/tanh negative ranges, cot edge cases, erf negative inputs, gamma boundary cases, logBeta, and factorial boundaries Also adds CMI-API as a testImplementation dependency. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01Hm7psNzaFDzwjGMUpKG1k2 --- build.gradle.kts | 1 + .../mccore/external/cmi/CoreCMIHookTest.java | 70 ++++++ .../parser/ParserAdditionalBranchTest.java | 173 ++++++++++++++ .../parser/SfunAdditionalBranchTest.java | 212 ++++++++++++++++++ 4 files changed, 456 insertions(+) create mode 100644 src/test/java/com/diamonddagger590/mccore/external/cmi/CoreCMIHookTest.java create mode 100644 src/test/java/com/diamonddagger590/mccore/parser/ParserAdditionalBranchTest.java create mode 100644 src/test/java/com/diamonddagger590/mccore/parser/SfunAdditionalBranchTest.java diff --git a/build.gradle.kts b/build.gradle.kts index d548155..ef2baf4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -130,6 +130,7 @@ dependencies { val cmiVersion = "9.7.14.3" compileOnly("com.github.Zrips:CMI-API:$cmiVersion") + testImplementation("com.github.Zrips:CMI-API:$cmiVersion") val citizensVersion = "2.0.39-SNAPSHOT" compileOnly("net.citizensnpcs:citizens-main:$citizensVersion") { diff --git a/src/test/java/com/diamonddagger590/mccore/external/cmi/CoreCMIHookTest.java b/src/test/java/com/diamonddagger590/mccore/external/cmi/CoreCMIHookTest.java new file mode 100644 index 0000000..b05686c --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/external/cmi/CoreCMIHookTest.java @@ -0,0 +1,70 @@ +package com.diamonddagger590.mccore.external.cmi; + +import com.diamonddagger590.mccore.CorePlugin; +import com.diamonddagger590.mccore.player.CorePlayer; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class CoreCMIHookTest { + + @Mock + private CorePlugin mockPlugin; + + private CoreCMIHook createHook() { + return new CoreCMIHook(mockPlugin); + } + + private CorePlayer createMockCorePlayer(Player bukkitPlayer) { + UUID uuid = UUID.randomUUID(); + return new CorePlayer(uuid, mockPlugin) { + @Override + public boolean useMutex() { + return false; + } + + @Override + public Optional getAsBukkitPlayer() { + return Optional.ofNullable(bukkitPlayer); + } + }; + } + + @Nested + @DisplayName("isAfk") + class IsAfk { + + @Test + @DisplayName("Given player is offline, when checking isAfk, then returns false") + void isAfk_returnsFalse_whenPlayerIsOffline() { + CorePlayer corePlayer = createMockCorePlayer(null); + CoreCMIHook hook = createHook(); + assertFalse(hook.isAfk(corePlayer)); + } + } + + @Nested + @DisplayName("isEntityNpc") + class IsEntityNpc { + + @Test + @DisplayName("Given entity is not a Player, when checking isEntityNpc, then returns false") + void isEntityNpc_returnsFalse_whenEntityIsNotPlayer() { + Entity mockEntity = mock(Entity.class); + CoreCMIHook hook = createHook(); + assertFalse(hook.isEntityNpc(mockEntity)); + } + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/parser/ParserAdditionalBranchTest.java b/src/test/java/com/diamonddagger590/mccore/parser/ParserAdditionalBranchTest.java new file mode 100644 index 0000000..160c97a --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/parser/ParserAdditionalBranchTest.java @@ -0,0 +1,173 @@ +package com.diamonddagger590.mccore.parser; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ParserAdditionalBranchTest { + + private static final double DELTA = 1e-9; + + // ── convertInput: character filtering branches ──────────────────────── + + @Test + @DisplayName("Given input with special characters, when parsing, then strips unsupported chars") + void getValue_stripsUnsupportedChars_whenInputHasSpecialCharacters() { + assertEquals("2+3", new Parser("2 + 3!@#$").getInputString()); + assertEquals(5.0, new Parser("2+3").getValue(), DELTA); + } + + @Test + @DisplayName("Given input with mixed valid and invalid characters, when getting input string, then only valid chars remain") + void getInputString_containsOnlyValidChars_whenInputHasMixedCharacters() { + String result = new Parser("a&b").getInputString(); + assertEquals("ab", result); + } + + // ── detectImplicitMult: number followed by bracket (no implicit mult) ─ + + @Test + @DisplayName("Given number followed by open bracket, when parsing, then no implicit multiplication occurs") + void getValue_noImplicitMult_whenNumberFollowedByBracket() { + assertThrows(ParseError.class, () -> new Parser("2(3+4)").getValue()); + } + + @Test + @DisplayName("Given number followed by another number, when parsing, then treats as single number") + void getValue_concatenatesDigits_whenNumberFollowedByNumber() { + assertEquals(23.0, new Parser("23").getValue(), DELTA); + } + + // ── parse: exponential notation edge cases ──────────────────────────── + + @Test + @DisplayName("Given number with e followed by end of input, when parsing, then throws ParseError") + void getValue_throwsParseError_whenNumberFollowedByConstantE() { + assertThrows(ParseError.class, () -> new Parser("2e").getValue()); + } + + @Test + @DisplayName("Given number like 1e-0, when parsing, then evaluates as 1.0") + void getValue_parsesZeroExponent_whenExponentIsNegativeZero() { + assertEquals(1.0, new Parser("1e-0").getValue(), DELTA); + } + + @Test + @DisplayName("Given expression with multiple implicit multiplications, when evaluating, then all apply") + void getValue_appliesMultipleImplicitMults_whenChainedConstantsAndVariables() { + Parser p = new Parser("2x+3y"); + p.setVariable("x", 5.0); + p.setVariable("y", 10.0); + assertEquals(40.0, p.getValue(), DELTA); + } + + // ── getTree: caching branch ─────────────────────────────────────────── + + @Test + @DisplayName("Given parser already parsed, when getTree called again, then returns cached tree") + void getTree_returnsCachedTree_whenCalledMultipleTimes() { + Parser parser = new Parser("2+3"); + ExpressionNode tree1 = parser.getTree(); + ExpressionNode tree2 = parser.getTree(); + assertTrue(tree1 == tree2); + } + + // ── getParsedFunctions and getParsedVariables caching ───────────────── + + @Test + @DisplayName("Given expression with no functions, when getting parsed functions, then returns empty set") + void getParsedFunctions_returnsEmpty_whenExpressionHasNoFunctions() { + assertTrue(new Parser("2+3").getParsedFunctions().isEmpty()); + } + + @Test + @DisplayName("Given expression with no variables, when getting parsed variables, then returns empty set") + void getParsedVariables_returnsEmpty_whenExpressionHasNoVariables() { + assertTrue(new Parser("2+3").getParsedVariables().isEmpty()); + } + + @Test + @DisplayName("Given expression with single function, when getting parsed functions, then contains that function") + void getParsedFunctions_containsFunction_whenExpressionHasOneFunction() { + var funcs = new Parser("abs(x)").getParsedFunctions(); + assertEquals(1, funcs.size()); + assertTrue(funcs.contains("abs")); + } + + // ── modulo chains in H() ────────────────────────────────────────────── + + @Test + @DisplayName("Given chained modulo operations, when evaluating, then applies left to right") + void getValue_appliesLeftToRight_whenModuloChained() { + assertEquals(1.0, new Parser("10%3%2").getValue(), DELTA); + } + + // ── division chains in G() ──────────────────────────────────────────── + + @Test + @DisplayName("Given chained division operations, when evaluating, then applies left to right") + void getValue_appliesLeftToRight_whenDivisionChained() { + assertEquals(5.0, new Parser("100/10/2").getValue(), DELTA); + } + + // ── subtraction chains in S() ───────────────────────────────────────── + + @Test + @DisplayName("Given chained subtraction operations, when evaluating, then applies left to right") + void getValue_appliesLeftToRight_whenSubtractionChained() { + assertEquals(3.0, new Parser("10-5-2").getValue(), DELTA); + } + + // ── ConstantNode for built-in pi/e via parser ───────────────────────── + + @Test + @DisplayName("Given pi constant in expression, when evaluating, then uses Math.PI") + void getValue_usesMathPI_whenPiUsedInExpression() { + assertEquals(Math.PI * 2, new Parser("2*pi").getValue(), DELTA); + } + + @Test + @DisplayName("Given e constant multiplied, when evaluating, then uses Math.E") + void getValue_usesMathE_whenEUsedInExpression() { + assertEquals(Math.E * 3, new Parser("3*e").getValue(), DELTA); + } + + // ── OperatorNode toString additional branches ───────────────────────── + + @Test + @DisplayName("Given division with left subtraction child, when converting to string, then adds brackets") + void toString_addsBrackets_whenDivisionWithSubtractionLeft() { + OperatorNode left = new OperatorNode(new ConstantNode(5.0), new ConstantNode(2.0), '-'); + OperatorNode node = new OperatorNode(left, new ConstantNode(3.0), '/'); + assertEquals("(5-2)/3", node.toString()); + } + + @Test + @DisplayName("Given multiplication with right modulo child, when converting to string, then adds brackets") + void toString_addsBrackets_whenMultiplicationWithModuloRight() { + OperatorNode right = new OperatorNode(new ConstantNode(7.0), new ConstantNode(3.0), '%'); + OperatorNode node = new OperatorNode(new ConstantNode(2.0), right, '*'); + assertEquals("2*(7%3)", node.toString()); + } + + @Test + @DisplayName("Given exponentiation with function children, when converting to string, then wraps both") + void toString_addsBrackets_whenExponentiationWithFunctionChildren() { + FunctionNode neg1 = new FunctionNode(new ConstantNode(2.0), 0); + FunctionNode neg2 = new FunctionNode(new ConstantNode(3.0), 0); + OperatorNode node = new OperatorNode(neg1, neg2, '^'); + assertEquals("(-2)^(-3)", node.toString()); + } + + @Test + @DisplayName("Given modulo with function children, when converting to string, then wraps both") + void toString_addsBrackets_whenModuloWithFunctionChildren() { + FunctionNode neg = new FunctionNode(new ConstantNode(5.0), 0); + OperatorNode child = new OperatorNode(new ConstantNode(7.0), new ConstantNode(2.0), '+'); + OperatorNode node = new OperatorNode(child, neg, '%'); + assertEquals("(7+2)%(-5)", node.toString()); + } +} diff --git a/src/test/java/com/diamonddagger590/mccore/parser/SfunAdditionalBranchTest.java b/src/test/java/com/diamonddagger590/mccore/parser/SfunAdditionalBranchTest.java new file mode 100644 index 0000000..61f602b --- /dev/null +++ b/src/test/java/com/diamonddagger590/mccore/parser/SfunAdditionalBranchTest.java @@ -0,0 +1,212 @@ +package com.diamonddagger590.mccore.parser; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SfunAdditionalBranchTest { + + private static final double DELTA = 1e-9; + private static final double RELAXED_DELTA = 1e-6; + + // ── sinh: very large y branch (y >= 94906265.62) ────────────────────── + + @Test + @DisplayName("Given very large positive x, when computing sinh, then uses 0.5*y formula") + void sinh_usesHalfYFormula_whenXIsVeryLargePositive() { + double x = 100.0; + double y = Math.exp(x); + double expected = 0.5 * y; + assertEquals(expected, Sfun.sinh(x), expected * 1e-10); + } + + @Test + @DisplayName("Given very large negative x, when computing sinh, then uses -0.5*y formula") + void sinh_usesNegativeHalfYFormula_whenXIsVeryLargeNegative() { + double x = -100.0; + double result = Sfun.sinh(x); + assertTrue(result < -1e40); + } + + @Test + @DisplayName("Given x in Chebyshev range, when computing sinh, then uses series expansion") + void sinh_usesChebyshevSeries_whenXIsInSeriesRange() { + double x = 0.5; + double result = Sfun.sinh(x); + double expected = (Math.exp(0.5) - Math.exp(-0.5)) / 2.0; + assertEquals(expected, result, RELAXED_DELTA); + } + + // ── asinh: large negative x branch ──────────────────────────────────── + + @Test + @DisplayName("Given very large negative x, when computing asinh, then returns negative large-value result") + void asinh_returnsNegativeLargeResult_whenXIsVeryLargeNegative() { + double x = -1e9; + double expected = -(0.69314718055994530941723212145818 + Math.log(1e9)); + assertEquals(expected, Sfun.asinh(x), DELTA); + } + + @Test + @DisplayName("Given negative x in medium range, when computing asinh, then uses log formula with negation") + void asinh_usesLogFormulaWithNegation_whenXIsNegativeMedium() { + double x = -5.0; + double y = 5.0; + double expected = -(Math.log(y + Math.sqrt(y * y + 1.0))); + assertEquals(expected, Sfun.asinh(x), DELTA); + } + + @Test + @DisplayName("Given negative very small x, when computing asinh, then returns x") + void asinh_returnsX_whenXIsVerySmallNegative() { + double x = -1e-10; + assertEquals(x, Sfun.asinh(x), DELTA); + } + + @Test + @DisplayName("Given negative x in Chebyshev range, when computing asinh, then returns consistent result") + void asinh_returnsConsistentResult_whenXIsNegativeInSeriesRange() { + double x = -0.5; + double result = Sfun.asinh(x); + assertEquals(Sfun.asinh(-0.5), result, DELTA); + assertTrue(Double.isFinite(result)); + } + + // ── atanh: very small negative x branch ─────────────────────────────── + + @Test + @DisplayName("Given negative very small x, when computing atanh, then returns x") + void atanh_returnsX_whenXIsVerySmallNegative() { + double x = -1e-10; + assertEquals(x, Sfun.atanh(x), DELTA); + } + + @Test + @DisplayName("Given negative x in Chebyshev range, when computing atanh, then uses series") + void atanh_usesSeries_whenXIsNegativeInChebyshevRange() { + double x = -0.3; + double expected = 0.5 * Math.log((1.0 + x) / (1.0 - x)); + assertEquals(expected, Sfun.atanh(x), RELAXED_DELTA); + } + + @Test + @DisplayName("Given negative x between -1 and -0.5, when computing atanh, then uses log formula") + void atanh_usesLogFormula_whenXIsNegativeBetweenHalfAndOne() { + double x = -0.8; + double expected = 0.5 * Math.log((1.0 + x) / (1.0 - x)); + assertEquals(expected, Sfun.atanh(x), DELTA); + } + + // ── cot: x = 0 branch (y == 0, infinity) ───────────────────────────── + + @Test + @DisplayName("Given x = pi, when computing cot, then returns large magnitude value") + void cot_returnsLargeMagnitude_whenXIsPi() { + double result = Sfun.cot(Math.PI); + assertTrue(Double.isFinite(result)); + assertTrue(Math.abs(result) > 1e10); + } + + @Test + @DisplayName("Given negative x in [0.25, 0.5] range, when computing cot, then returns correct negative value") + void cot_returnsNegative_whenNegativeXInQuarterToHalfRange() { + double x = -Math.PI / 8; + double result = Sfun.cot(x); + double expected = 1.0 / Math.tan(x); + assertEquals(expected, result, Math.abs(expected) * RELAXED_DELTA); + } + + // ── tanh: negative values in exp branch ─────────────────────────────── + + @Test + @DisplayName("Given negative very small x, when computing tanh, then returns x") + void tanh_returnsX_whenXIsVerySmallNegative() { + double x = -1e-10; + assertEquals(x, Sfun.tanh(x), DELTA); + } + + @Test + @DisplayName("Given negative x in Chebyshev range, when computing tanh, then uses series") + void tanh_usesSeries_whenXIsNegativeInChebyshevRange() { + double x = -0.5; + assertEquals(-Sfun.tanh(0.5), Sfun.tanh(x), DELTA); + } + + // ── erf: negative x in medium range ─────────────────────────────────── + + @Test + @DisplayName("Given negative medium range x, when computing erf, then returns correct negative value") + void erf_returnsNegative_whenXIsNegativeMediumRange() { + double result = Sfun.erf(-3.0); + assertEquals(-Sfun.erf(3.0), result, DELTA); + } + + @Test + @DisplayName("Given very small negative x, when computing erf, then uses linear approximation") + void erf_returnsLinearApprox_whenXIsVerySmallNegative() { + double x = -1e-10; + double expected = 2 * x / 1.77245385090551602729816748334; + assertEquals(expected, Sfun.erf(x), DELTA); + } + + @Test + @DisplayName("Given negative x just below threshold, when computing erf, then returns -1") + void erf_returnsNegativeOne_whenXIsLargeNegative() { + assertEquals(-1.0, Sfun.erf(-7.0), DELTA); + } + + // ── gamma: edge cases not covered ───────────────────────────────────── + + @Test + @DisplayName("Given negative non-integer x between -2 and -1, when computing gamma, then returns correct value") + void gamma_returnsCorrectValue_whenXIsNegativeNonIntBetweenNeg2AndNeg1() { + double result = Sfun.gamma(-1.5); + assertTrue(Double.isFinite(result)); + } + + @Test + @DisplayName("Given x = 3, when computing gamma, then returns 2 (2!)") + void gamma_returnsTwo_whenXIsThree() { + assertEquals(2.0, Sfun.gamma(3.0), RELAXED_DELTA); + } + + @Test + @DisplayName("Given large positive x near 171, when computing gamma, then returns finite value") + void gamma_returnsFiniteValue_whenXIsNear171() { + double result = Sfun.gamma(171.0); + assertTrue(Double.isFinite(result)); + } + + @Test + @DisplayName("Given negative integer > -10 in gamma, then returns NaN") + void gamma_returnsNaN_whenXIsSmallNegativeInteger() { + assertTrue(Double.isNaN(Sfun.gamma(-3.0))); + assertTrue(Double.isNaN(Sfun.gamma(-5.0))); + } + + // ── logBeta: dlnrel x <= -1 branch ──────────────────────────────────── + + @Test + @DisplayName("Given both p and q are small, when computing logBeta, then uses direct gamma product") + void logBeta_usesDirectGamma_whenBothSmall() { + double result = Sfun.logBeta(2.0, 3.0); + double expected = Math.log(Sfun.gamma(2.0) * (Sfun.gamma(3.0) / Sfun.gamma(5.0))); + assertEquals(expected, result, RELAXED_DELTA); + } + + // ── fact: boundary cases ────────────────────────────────────────────── + + @Test + @DisplayName("Given n = 2, when computing factorial, then returns 2") + void fact_returnsTwo_whenNIsTwo() { + assertEquals(2.0, Sfun.fact(2), DELTA); + } + + @Test + @DisplayName("Given n = 20, when computing factorial, then returns correct large value") + void fact_returnsCorrectValue_whenNIs20() { + assertEquals(2432902008176640000.0, Sfun.fact(20), 1.0); + } +}