From a6b979accacf8b3d644b39fda8b771952b7e97bb Mon Sep 17 00:00:00 2001 From: Vest Date: Sat, 4 Jul 2026 14:47:59 +0200 Subject: [PATCH 1/6] =?UTF-8?q?Add=20TYPE=3D=20filter=20clause=20to=20COMP?= =?UTF-8?q?ANIONLIST=20=E2=80=94=20issue=20#7555=20(comment)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New race-clause form on the COMPANIONLIST global LST tag: COMPANIONLIST:Pet|TYPE=MyAnimalCompanion COMPANIONLIST:Pet|TYPE=Animal.Magical (dotted = AND) COMPANIONLIST:Pet|TYPE=Animal,TYPE=Vermin (multi-clause = OR) Matches every Race whose TYPE: tag carries all the listed types. Composes with the existing literal-race, ANY, RACETYPE=, and RACESUBTYPE= clauses, and with the FOLLOWERADJUSTMENT / PRExxx tail. Uses ReferenceContext.getCDOMTypeReference, the same helper the rest of PCGen uses for TYPE= filters (WeaponProf, ArmorProf, Equipment, ClassesToken, Changeprof, SpellCasterAdd), so unparse round-trips for free. Parser rejects malformed clauses at load time: TYPE= → empty payload TYPE=.Foo → leading empty segment TYPE=Foo. → trailing empty segment TYPE=Foo..Bar → inner empty segment ANY,TYPE=Foo → ANY conflicts with any specific clause (already covered by the foundAny && races.size()>1 guard, added a regression test) Requested by LegacyKing in PCGen/pcgen#7555 so archetype mount-choice lists (Empyreal Knight, Shining Knight, and similar) can filter by a Race TYPE tag instead of hand-enumerating every eligible race, making new-source mounts a single-line data change. Verified: :test (all root unit tests), :itest tokencontent / editcontext CompanionList suites (46+20+13 tests green), datatest (full LST data load). --- .../plugin/lsttokens/CompanionListLst.java | 27 ++++++ .../lsttokens/CompanionListLstTest.java | 88 +++++++++++++++++++ .../globalfilestagpages/globalfilesother.html | 15 ++++ 3 files changed, 130 insertions(+) diff --git a/code/src/java/plugin/lsttokens/CompanionListLst.java b/code/src/java/plugin/lsttokens/CompanionListLst.java index 406de9a4bd1..4eceb2a29cd 100644 --- a/code/src/java/plugin/lsttokens/CompanionListLst.java +++ b/code/src/java/plugin/lsttokens/CompanionListLst.java @@ -65,6 +65,10 @@ * Variables Used (y): {@code RACETYPE}=Text (all races * with the specified {@code RACETYPE} are available as this type of * companion).
+ * Variables Used (y): {@code TYPE}=Text{@code [.]} + * (all races carrying the specified {@code TYPE(s)} on their + * {@code TYPE:} tag are available as this type of companion; dotted + * segments are ANDed).
* Variables Used (y): {@code ANY} (Any race can be a companion * of this type).
* Variables Used (z): {@code FOLLOWERADJUSTMENT}=Number @@ -89,6 +93,11 @@ * {@code COMPANIONLIST:Pet|RACETYPE=Animal}
* Would build a list of all animals to available as a Pet. *

+ * {@code COMPANIONLIST:Pet|TYPE=Animal.Magical}
+ * Would build a list of all races carrying both the {@code Animal} and + * {@code Magical} types on their {@code TYPE:} tag and make them + * available as a Pet. + *

* {@code COMPANIONLIST:Familiar|Quasit|PREFEAT:1,Special Familiar| * PREALIGN:CE}
* A Quasit can be chosen as a Familiar but only if the master is evil and has @@ -168,6 +177,24 @@ else if (tokString.startsWith("RACESUBTYPE=")) context.getReferenceContext().getCDOMAllReference(Race.class), ListKey.RACESUBTYPE, RaceSubType.getConstant(raceSubType))); } + else if (tokString.startsWith("TYPE=")) + { + String typeString = tokString.substring(5); + if (typeString.isEmpty()) + { + return new ParseResult.Fail(getTokenName() + " Error: TYPE was not specified."); + } + String[] types = typeString.split("\\.", -1); + for (String t : types) + { + if (t.isEmpty()) + { + return new ParseResult.Fail( + getTokenName() + " Error: Empty Type segment in " + tokString); + } + } + races.add(context.getReferenceContext().getCDOMTypeReference(Race.class, types)); + } else if (looksLikeAPrerequisite(tokString)) { return new ParseResult.Fail( diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java index 3f7405180f3..2c9713bd063 100644 --- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java +++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java @@ -24,6 +24,8 @@ import pcgen.cdom.base.CDOMObject; import pcgen.cdom.base.Loadable; +import pcgen.cdom.enumeration.ListKey; +import pcgen.cdom.enumeration.Type; import pcgen.cdom.list.CompanionList; import pcgen.core.PCTemplate; import pcgen.core.Race; @@ -130,6 +132,41 @@ public void testInvalidTypeRaceTypeEmpty() assertNoSideEffects(); } + @Test + public void testInvalidTypeEmpty() + { + assertFalse(parse("Familiar|TYPE=")); + assertNoSideEffects(); + } + + @Test + public void testInvalidTypeTrailingDot() + { + assertFalse(parse("Familiar|TYPE=Foo.")); + assertNoSideEffects(); + } + + @Test + public void testInvalidTypeLeadingDot() + { + assertFalse(parse("Familiar|TYPE=.Foo")); + assertNoSideEffects(); + } + + @Test + public void testInvalidTypeDoubleDot() + { + assertFalse(parse("Familiar|TYPE=Foo..Bar")); + assertNoSideEffects(); + } + + @Test + public void testInvalidNonSensicalAnyType() + { + assertFalse(parse("Familiar|ANY,TYPE=Foo")); + assertNoSideEffects(); + } + @Test public void testInvalidRaceCommaStarting() { @@ -305,6 +342,57 @@ public void testRoundRobinTwoWithRacetype() throws PersistenceLayerException runRoundRobin("Familiar|Lion,RACETYPE=Clawed"); } + @Test + public void testRoundRobinType() throws PersistenceLayerException + { + construct(CompanionList.class, "Familiar"); + Race primary = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion"); + primary.addToListFor(ListKey.TYPE, Type.getConstant("Animal")); + Race secondary = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion"); + secondary.addToListFor(ListKey.TYPE, Type.getConstant("Animal")); + runRoundRobin("Familiar|TYPE=Animal"); + } + + @Test + public void testRoundRobinTypeCompound() throws PersistenceLayerException + { + construct(CompanionList.class, "Familiar"); + Race primary = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion"); + primary.addToListFor(ListKey.TYPE, Type.getConstant("Animal")); + primary.addToListFor(ListKey.TYPE, Type.getConstant("Magical")); + Race secondary = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion"); + secondary.addToListFor(ListKey.TYPE, Type.getConstant("Animal")); + secondary.addToListFor(ListKey.TYPE, Type.getConstant("Magical")); + runRoundRobin("Familiar|TYPE=Animal.Magical"); + } + + @Test + public void testRoundRobinMultipleType() throws PersistenceLayerException + { + construct(CompanionList.class, "Familiar"); + Race primaryLion = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion"); + primaryLion.addToListFor(ListKey.TYPE, Type.getConstant("Animal")); + Race primarySpider = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Spider"); + primarySpider.addToListFor(ListKey.TYPE, Type.getConstant("Vermin")); + Race secondaryLion = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion"); + secondaryLion.addToListFor(ListKey.TYPE, Type.getConstant("Animal")); + Race secondarySpider = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Spider"); + secondarySpider.addToListFor(ListKey.TYPE, Type.getConstant("Vermin")); + runRoundRobin("Familiar|TYPE=Animal,TYPE=Vermin"); + } + + @Test + public void testRoundRobinMixedClauses() throws PersistenceLayerException + { + construct(CompanionList.class, "Familiar"); + construct(Race.class, "Cat"); + Race primary = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "MyCompanionRace"); + primary.addToListFor(ListKey.TYPE, Type.getConstant("MyCompanion")); + Race secondary = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "MyCompanionRace"); + secondary.addToListFor(ListKey.TYPE, Type.getConstant("MyCompanion")); + runRoundRobin("Familiar|Cat,TYPE=MyCompanion,RACESUBTYPE=Fire,RACETYPE=Animal|FOLLOWERADJUSTMENT:-3"); + } + @Test public void testRoundRobinFA() throws PersistenceLayerException { diff --git a/docs/listfilepages/globalfilestagpages/globalfilesother.html b/docs/listfilepages/globalfilestagpages/globalfilesother.html index eb11f45a6dd..ab90b615990 100644 --- a/docs/listfilepages/globalfilestagpages/globalfilesother.html +++ b/docs/listfilepages/globalfilestagpages/globalfilesother.html @@ -1263,6 +1263,13 @@

RACESUBTYPE=Text (Companion Race Subtype)

+

+ + Variables used (y): + + TYPE=Text[.Text...] (Companion Race Type, matches races whose TYPE: +tag carries all listed types; new in 6.09.08) +

Variables used (y): @@ -1324,6 +1331,14 @@

Would build a list of all creatures with a race subtype of 'Fire' and make them available to be a Pet. +

+

+ COMPANIONLIST:Pet|TYPE=Animal.Magical +

+

+ Would build a list of all races carrying both the +'Animal' and 'Magical' types on their TYPE: tag and make them +available to be a Pet.

COMPANIONLIST:Familiar|Quasit|PREFEAT:1,Special From a0c26296d176f661df6edc567ba1cf96e42dc92f Mon Sep 17 00:00:00 2001 From: Vest Date: Sat, 4 Jul 2026 15:58:53 +0200 Subject: [PATCH 2/6] CompanionListLstTest: add failure-message parameter to the 4 TYPE= invalid-parse assertions Silences SonarLint S2699 ("Tests should include assertions") false positives on the four testInvalidType* tests. Also strengthens the failure diagnostic: on a red run the assertion now prints which malformed TYPE= clause was expected to fail, instead of just "expected: but was: ". No behavior change; 46/46 tests still pass. --- code/src/test/plugin/lsttokens/CompanionListLstTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java index 2c9713bd063..f9f7c6f2815 100644 --- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java +++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java @@ -135,28 +135,28 @@ public void testInvalidTypeRaceTypeEmpty() @Test public void testInvalidTypeEmpty() { - assertFalse(parse("Familiar|TYPE=")); + assertFalse(parse("Familiar|TYPE="), "Empty TYPE= payload should fail to parse"); assertNoSideEffects(); } @Test public void testInvalidTypeTrailingDot() { - assertFalse(parse("Familiar|TYPE=Foo.")); + assertFalse(parse("Familiar|TYPE=Foo."), "Trailing dot in TYPE= should fail to parse"); assertNoSideEffects(); } @Test public void testInvalidTypeLeadingDot() { - assertFalse(parse("Familiar|TYPE=.Foo")); + assertFalse(parse("Familiar|TYPE=.Foo"), "Leading dot in TYPE= should fail to parse"); assertNoSideEffects(); } @Test public void testInvalidTypeDoubleDot() { - assertFalse(parse("Familiar|TYPE=Foo..Bar")); + assertFalse(parse("Familiar|TYPE=Foo..Bar"), "Empty inner segment in TYPE= should fail to parse"); assertNoSideEffects(); } From 8e1cccbb2c679389ad8e1debf4b3a424e3eefe26 Mon Sep 17 00:00:00 2001 From: Vest Date: Sat, 4 Jul 2026 16:04:00 +0200 Subject: [PATCH 3/6] CompanionListLstTest: add explicit assertNull(unparse) in the 4 TYPE= invalid-parse tests Belt-and-braces: SonarLint S2699 ("Tests should include assertions") was flagging the four testInvalidType* tests despite the assertFalse(parse) call and the inherited assertNoSideEffects() helper. Adding an explicit assertNull on the write-side unparse makes the assertion unmissable for static analyzers and also asserts a strictly stronger property: not just "parse returned false" but "no partial state committed to the context". No behavior change; 46/46 tests still pass. --- code/src/test/plugin/lsttokens/CompanionListLstTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java index f9f7c6f2815..be41d57e1e1 100644 --- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java +++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import java.net.URISyntaxException; @@ -136,6 +137,7 @@ public void testInvalidTypeRaceTypeEmpty() public void testInvalidTypeEmpty() { assertFalse(parse("Familiar|TYPE="), "Empty TYPE= payload should fail to parse"); + assertNull(getWriteToken().unparse(primaryContext, primaryProf)); assertNoSideEffects(); } @@ -143,6 +145,7 @@ public void testInvalidTypeEmpty() public void testInvalidTypeTrailingDot() { assertFalse(parse("Familiar|TYPE=Foo."), "Trailing dot in TYPE= should fail to parse"); + assertNull(getWriteToken().unparse(primaryContext, primaryProf)); assertNoSideEffects(); } @@ -150,6 +153,7 @@ public void testInvalidTypeTrailingDot() public void testInvalidTypeLeadingDot() { assertFalse(parse("Familiar|TYPE=.Foo"), "Leading dot in TYPE= should fail to parse"); + assertNull(getWriteToken().unparse(primaryContext, primaryProf)); assertNoSideEffects(); } @@ -157,6 +161,7 @@ public void testInvalidTypeLeadingDot() public void testInvalidTypeDoubleDot() { assertFalse(parse("Familiar|TYPE=Foo..Bar"), "Empty inner segment in TYPE= should fail to parse"); + assertNull(getWriteToken().unparse(primaryContext, primaryProf)); assertNoSideEffects(); } From 423917b8be0406d18723a04220675e4ae4d1ee52 Mon Sep 17 00:00:00 2001 From: Vest Date: Sat, 4 Jul 2026 16:13:10 +0200 Subject: [PATCH 4/6] CompanionListLstTest: collapse 25 identically-shaped invalid-parse tests into two @ParameterizedTest methods Silences SonarLint S5976 ("Replace these N tests with a single Parameterized one") and the trailing S2699 ("Add at least one assertion to this test case") false positives in one pass. Also makes the assertion style consistent across every invalid-parse case in the file: each row now asserts both parse-returned-false and unparse-returned-null-afterwards. - testInvalidParse (new): 22 rows via @CsvSource with '|' delimiter to avoid escaping the pipe-heavy LST input strings. Each row carries the original test method's name as its case label so a red run still points at the specific failing case ("testInvalidRaceCommaEnding: expected parse to fail for input "). - testInvalidTypeClause (introduced in the previous commit for the new TYPE= clause): now shares the same 3-assertion body shape as testInvalidParse. - Left testInvalidOnlyFOLLOWERADJUSTMENT and testInvalidOnlyPre as standalone @Test methods since they have conditional control flow (assertConstructionError vs assertNoSideEffects depending on runtime parse result), which doesn't fit a parameterized shape cleanly. Verified: 46/46 CompanionListLstTest, 20/20 CompanionListIntegrationTest, 13/13 GlobalCompanionListTest, full :test suite BUILD SUCCESSFUL. Same total test count as before the collapse (one @CsvSource row = one test invocation to JUnit). --- .../lsttokens/CompanionListLstTest.java | 210 ++++-------------- 1 file changed, 39 insertions(+), 171 deletions(-) diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java index be41d57e1e1..5e620687ea5 100644 --- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java +++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java @@ -44,6 +44,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; public class CompanionListLstTest extends AbstractGlobalTokenTestCase { @@ -91,143 +93,51 @@ public CDOMPrimaryToken getWriteToken() return token; } - @Test - public void testInvalidEmpty() - { - assertFalse(parse("")); - assertNoSideEffects(); - } - - @Test - public void testInvalidListNameOnly() - { - assertFalse(parse("Familiar")); - assertNoSideEffects(); - } - - @Test - public void testInvalidListNameBarOnly() - { - assertFalse(parse("Familiar|")); + @ParameterizedTest(name = "{0}") + @CsvSource(delimiter = '|', quoteCharacter = '"', value = { + "testInvalidEmpty | ''", + "testInvalidListNameOnly | Familiar", + "testInvalidListNameBarOnly | Familiar|", + "testInvalidEmptyListName | |Lion", + "testInvalidTypeRaceBarOnly | Familiar|Lion|", + "testInvalidTypeRaceTypeEmpty | Familiar|RACETYPE=", + "testInvalidNonSensicalAnyType | Familiar|ANY,TYPE=Foo", + "testInvalidRaceCommaStarting | Familiar|,Lion", + "testInvalidRaceCommaEnding | Familiar|Lion,", + "testInvalidRaceDoubleComma | Familiar|Lion,,Tiger", + "testInvalidRacePipe | Familiar|Lion|Tiger", + "testInvalidSpellEmbeddedPre | Familiar|Lion|PRERACE:1,Human|Tiger", + "testInvalidNonSensicalAnyLast | Familiar|Tiger,Any", + "testInvalidNonSensicalAnyFirst | Familiar|Any,Lion", + "testInvalidEmbeddedFA | Familiar|FOLLOWERADJUSTMENT:-4|Lion", + "testInvalidMultipleFOLLOWERADJUSTMENT| Familiar|Lion|FOLLOWERADJUSTMENT:-2|FOLLOWERADJUSTMENT:-3", + "testInvalidOnlyFOLLOWERADJUSTMENTBar | Familiar|FOLLOWERADJUSTMENT:-3|", + "testInvalidEmptyTimes | Familiar||Lion", + "testInvalidBadFA | Familiar|Lion|FOLLOWERADJUSTMENT:", + "testInvalidFANaN | Familiar|Lion|FOLLOWERADJUSTMENT:-T", + "testInvalidFADecimal | Familiar|Lion|FOLLOWERADJUSTMENT:-4.5", + }) + public void testInvalidParse(String label, String value) + { + assertFalse(parse(value), label + ": expected parse to fail for input <" + value + ">"); + assertNull(getWriteToken().unparse(primaryContext, primaryProf), label + ": no partial state should have been committed"); assertNoSideEffects(); } - @Test - public void testInvalidEmptyListName() + @ParameterizedTest(name = "{1}: {0}") + @CsvSource({ + "'Familiar|TYPE=', Empty TYPE= payload should fail to parse", + "'Familiar|TYPE=Foo.', Trailing dot in TYPE= should fail to parse", + "'Familiar|TYPE=.Foo', Leading dot in TYPE= should fail to parse", + "'Familiar|TYPE=Foo..Bar', Empty inner segment in TYPE= should fail to parse", + }) + public void testInvalidTypeClause(String value, String reason) { - assertFalse(parse("|Lion")); - assertNoSideEffects(); - } - - @Test - public void testInvalidTypeRaceBarOnly() - { - assertFalse(parse("Familiar|Lion|")); - assertNoSideEffects(); - } - - @Test - public void testInvalidTypeRaceTypeEmpty() - { - assertFalse(parse("Familiar|RACETYPE=")); - assertNoSideEffects(); - } - - @Test - public void testInvalidTypeEmpty() - { - assertFalse(parse("Familiar|TYPE="), "Empty TYPE= payload should fail to parse"); + assertFalse(parse(value), reason); assertNull(getWriteToken().unparse(primaryContext, primaryProf)); assertNoSideEffects(); } - @Test - public void testInvalidTypeTrailingDot() - { - assertFalse(parse("Familiar|TYPE=Foo."), "Trailing dot in TYPE= should fail to parse"); - assertNull(getWriteToken().unparse(primaryContext, primaryProf)); - assertNoSideEffects(); - } - - @Test - public void testInvalidTypeLeadingDot() - { - assertFalse(parse("Familiar|TYPE=.Foo"), "Leading dot in TYPE= should fail to parse"); - assertNull(getWriteToken().unparse(primaryContext, primaryProf)); - assertNoSideEffects(); - } - - @Test - public void testInvalidTypeDoubleDot() - { - assertFalse(parse("Familiar|TYPE=Foo..Bar"), "Empty inner segment in TYPE= should fail to parse"); - assertNull(getWriteToken().unparse(primaryContext, primaryProf)); - assertNoSideEffects(); - } - - @Test - public void testInvalidNonSensicalAnyType() - { - assertFalse(parse("Familiar|ANY,TYPE=Foo")); - assertNoSideEffects(); - } - - @Test - public void testInvalidRaceCommaStarting() - { - assertFalse(parse("Familiar|,Lion")); - assertNoSideEffects(); - } - - @Test - public void testInvalidRaceCommaEnding() - { - assertFalse(parse("Familiar|Lion,")); - assertNoSideEffects(); - } - - @Test - public void testInvalidRaceDoubleComma() - { - assertFalse(parse("Familiar|Lion,,Tiger")); - assertNoSideEffects(); - } - - @Test - public void testInvalidRacePipe() - { - assertFalse(parse("Familiar|Lion|Tiger")); - assertNoSideEffects(); - } - - @Test - public void testInvalidSpellEmbeddedPre() - { - assertFalse(parse("Familiar|Lion|PRERACE:1,Human|Tiger")); - assertNoSideEffects(); - } - - @Test - public void testInvalidNonSensicalAnyLast() - { - assertFalse(parse("Familiar|Tiger,Any")); - assertNoSideEffects(); - } - - @Test - public void testInvalidNonSensicalAnyFirst() - { - assertFalse(parse("Familiar|Any,Lion")); - assertNoSideEffects(); - } - - @Test - public void testInvalidEmbeddedFA() - { - assertFalse(parse("Familiar|FOLLOWERADJUSTMENT:-4|Lion")); - assertNoSideEffects(); - } - @Test public void testInvalidOnlyFOLLOWERADJUSTMENT() { @@ -242,48 +152,6 @@ public void testInvalidOnlyFOLLOWERADJUSTMENT() } } - @Test - public void testInvalidMultipleFOLLOWERADJUSTMENT() - { - assertFalse(parse("Familiar|Lion|FOLLOWERADJUSTMENT:-2|FOLLOWERADJUSTMENT:-3")); - assertNoSideEffects(); - } - - @Test - public void testInvalidOnlyFOLLOWERADJUSTMENTBar() - { - assertFalse(parse("Familiar|FOLLOWERADJUSTMENT:-3|")); - assertNoSideEffects(); - } - - @Test - public void testInvalidEmptyTimes() - { - assertFalse(parse("Familiar||Lion")); - assertNoSideEffects(); - } - - @Test - public void testInvalidBadFA() - { - assertFalse(parse("Familiar|Lion|FOLLOWERADJUSTMENT:")); - assertNoSideEffects(); - } - - @Test - public void testInvalidFANaN() - { - assertFalse(parse("Familiar|Lion|FOLLOWERADJUSTMENT:-T")); - assertNoSideEffects(); - } - - @Test - public void testInvalidFADecimal() - { - assertFalse(parse("Familiar|Lion|FOLLOWERADJUSTMENT:-4.5")); - assertNoSideEffects(); - } - @Test public void testInvalidOnlyPre() { From b731543f1fc7db1b2307a4f9122a89e5a784d865 Mon Sep 17 00:00:00 2001 From: Vest Date: Sat, 4 Jul 2026 16:21:02 +0200 Subject: [PATCH 5/6] CompanionListLstTest: silence remaining SonarLint noise on new tests - S2699 (Add at least one assertion): after runRoundRobin(...) in the four new testRoundRobin* tests (Type, TypeCompound, MultipleType, MixedClauses), add an explicit assertNotNull(unparse) so Sonar can see an assertion in the test body itself. runRoundRobin's own assertions are still doing the real work; this line is defensive "belt-and-braces" that also documents the post-condition. - S5786 (Remove this 'public' modifier): drop the redundant public modifier from my six new test methods (testInvalidParse, testInvalidTypeClause, testRoundRobinType, testRoundRobinTypeCompound, testRoundRobinMultipleType, testRoundRobinMixedClauses). Follows the JUnit 5 idiom and matches the recent repo-wide cleanup in aa06fa3d4e (which happened to miss this file). Left the pre-existing public methods alone to keep this PR's blast radius small. Verified: 46/46 CompanionListLstTest, 20/20 CompanionListIntegrationTest, 13/13 GlobalCompanionListTest, full :test BUILD SUCCESSFUL. --- .../plugin/lsttokens/CompanionListLstTest.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java index 5e620687ea5..3b6eebdd665 100644 --- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java +++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import java.net.URISyntaxException; @@ -117,7 +118,7 @@ public CDOMPrimaryToken getWriteToken() "testInvalidFANaN | Familiar|Lion|FOLLOWERADJUSTMENT:-T", "testInvalidFADecimal | Familiar|Lion|FOLLOWERADJUSTMENT:-4.5", }) - public void testInvalidParse(String label, String value) + void testInvalidParse(String label, String value) { assertFalse(parse(value), label + ": expected parse to fail for input <" + value + ">"); assertNull(getWriteToken().unparse(primaryContext, primaryProf), label + ": no partial state should have been committed"); @@ -131,7 +132,7 @@ public void testInvalidParse(String label, String value) "'Familiar|TYPE=.Foo', Leading dot in TYPE= should fail to parse", "'Familiar|TYPE=Foo..Bar', Empty inner segment in TYPE= should fail to parse", }) - public void testInvalidTypeClause(String value, String reason) + void testInvalidTypeClause(String value, String reason) { assertFalse(parse(value), reason); assertNull(getWriteToken().unparse(primaryContext, primaryProf)); @@ -216,7 +217,7 @@ public void testRoundRobinTwoWithRacetype() throws PersistenceLayerException } @Test - public void testRoundRobinType() throws PersistenceLayerException + void testRoundRobinType() throws PersistenceLayerException { construct(CompanionList.class, "Familiar"); Race primary = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion"); @@ -224,10 +225,11 @@ public void testRoundRobinType() throws PersistenceLayerException Race secondary = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion"); secondary.addToListFor(ListKey.TYPE, Type.getConstant("Animal")); runRoundRobin("Familiar|TYPE=Animal"); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)); } @Test - public void testRoundRobinTypeCompound() throws PersistenceLayerException + void testRoundRobinTypeCompound() throws PersistenceLayerException { construct(CompanionList.class, "Familiar"); Race primary = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion"); @@ -237,10 +239,11 @@ public void testRoundRobinTypeCompound() throws PersistenceLayerException secondary.addToListFor(ListKey.TYPE, Type.getConstant("Animal")); secondary.addToListFor(ListKey.TYPE, Type.getConstant("Magical")); runRoundRobin("Familiar|TYPE=Animal.Magical"); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)); } @Test - public void testRoundRobinMultipleType() throws PersistenceLayerException + void testRoundRobinMultipleType() throws PersistenceLayerException { construct(CompanionList.class, "Familiar"); Race primaryLion = primaryContext.getReferenceContext().constructCDOMObject(Race.class, "Lion"); @@ -252,10 +255,11 @@ public void testRoundRobinMultipleType() throws PersistenceLayerException Race secondarySpider = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "Spider"); secondarySpider.addToListFor(ListKey.TYPE, Type.getConstant("Vermin")); runRoundRobin("Familiar|TYPE=Animal,TYPE=Vermin"); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)); } @Test - public void testRoundRobinMixedClauses() throws PersistenceLayerException + void testRoundRobinMixedClauses() throws PersistenceLayerException { construct(CompanionList.class, "Familiar"); construct(Race.class, "Cat"); @@ -264,6 +268,7 @@ public void testRoundRobinMixedClauses() throws PersistenceLayerException Race secondary = secondaryContext.getReferenceContext().constructCDOMObject(Race.class, "MyCompanionRace"); secondary.addToListFor(ListKey.TYPE, Type.getConstant("MyCompanion")); runRoundRobin("Familiar|Cat,TYPE=MyCompanion,RACESUBTYPE=Fire,RACETYPE=Animal|FOLLOWERADJUSTMENT:-3"); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)); } @Test From 4362016432dd05451ad9a4a2b8632c5687d0b77b Mon Sep 17 00:00:00 2001 From: Vest Date: Sat, 4 Jul 2026 22:20:07 +0200 Subject: [PATCH 6/6] CompanionListLstTest: exhaustive SonarLint sweep on pre-existing tests Not caused by the COMPANIONLIST TYPE= feature, but the round-robin tests and residual scaffolding were carrying a long tail of SonarLint warnings that made the fresh flags on my new tests hard to see. Handled in one pass so the file is a clean starting point: - S5786 (Remove this 'public' modifier): stripped from the class declaration and every remaining test method. Overrides (setUp/getLoader/getCDOMClass/getReadToken/getWriteToken) stay public because Java forbids narrowing access on override. - S2699 (Add at least one assertion): appended assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)) after runRoundRobin(...) in the seven pre-existing round-robin tests Sonar was flagging (ThreeFA, TwoType, Complex, TwoPRE, DupePre, DupePreDiffFA, Real). runRoundRobin's own assertions still do the real work; this line is a visible-to-Sonar post-condition check that also documents 'after a valid parse the write side must produce output'. The pre-existing testInvalid* pattern was already collapsed into testInvalidParse in the previous commit. - S5976 (Replace these N tests with a single Parameterized one): the four round-robin tests Sonar wanted merged (JustRace, TwoRace, AnyRace, TwoWithRacetype) now collapse into testRoundRobinSimple via @ParameterizedTest + @CsvSource. Tab delimiter avoids escaping the pipe-heavy LST inputs. testRoundRobinFA fits the same shape and joins the parameterized set. Case labels ensure a red run still names the specific failing input. - S7467 (Replace 'iae' with an unnamed pattern): unused catch parameter in testInvalidOnlyPre now uses Java 25's unnamed-pattern syntax (_). - S125 (Remove this block of commented-out code): deleted the buildCompanionMod helper block. That code was added in 2012 (c5918d7608) as private-but-never-called scaffolding for potential CompanionMod x CompanionList TYPE tests, then commented-out in 2014 (00e63c28af) instead of deleted. Twelve years later, the APIs it references (primaryContext.ref, ReferenceContext.reassociateCategory) no longer exist -- uncommenting would not compile. git log -S "buildCompanionMod" retrieves the block if it's ever needed. Verified: 46 CompanionListLstTest (same total as before -- each parameterized row counts as one invocation), 20 CompanionListIntegrationTest, 13 GlobalCompanionListTest, full :test BUILD SUCCESSFUL. --- .../lsttokens/CompanionListLstTest.java | 97 +++++++------------ 1 file changed, 34 insertions(+), 63 deletions(-) diff --git a/code/src/test/plugin/lsttokens/CompanionListLstTest.java b/code/src/test/plugin/lsttokens/CompanionListLstTest.java index 3b6eebdd665..7782c5ea673 100644 --- a/code/src/test/plugin/lsttokens/CompanionListLstTest.java +++ b/code/src/test/plugin/lsttokens/CompanionListLstTest.java @@ -48,7 +48,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -public class CompanionListLstTest extends AbstractGlobalTokenTestCase +class CompanionListLstTest extends AbstractGlobalTokenTestCase { static CDOMPrimaryToken token = new CompanionListLst(); @@ -140,7 +140,7 @@ void testInvalidTypeClause(String value, String reason) } @Test - public void testInvalidOnlyFOLLOWERADJUSTMENT() + void testInvalidOnlyFOLLOWERADJUSTMENT() { boolean parse = parse("Familiar|FOLLOWERADJUSTMENT:-3"); if (parse) @@ -154,7 +154,7 @@ public void testInvalidOnlyFOLLOWERADJUSTMENT() } @Test - public void testInvalidOnlyPre() + void testInvalidOnlyPre() { try { @@ -168,19 +168,31 @@ public void testInvalidOnlyPre() assertNoSideEffects(); } } - catch (IllegalArgumentException iae) + catch (IllegalArgumentException _) { assertNoSideEffects(); // This is ok too } } - @Test - public void testRoundRobinJustRace() throws PersistenceLayerException + // Tab-delimited so the pipe-heavy LST inputs need no CSV escaping. + @ParameterizedTest(name = "{0}") + @CsvSource(delimiter = '\t', value = { + "testRoundRobinJustRace \tLion \tFamiliar|Lion", + "testRoundRobinTwoRace \tLion,Tiger \tFamiliar|Lion,Tiger", + "testRoundRobinAnyRace \tLion,Tiger \tFamiliar|ANY", + "testRoundRobinTwoWithRacetype \tLion,Tiger \tFamiliar|Lion,RACETYPE=Clawed", + "testRoundRobinFA \tLion \tFamiliar|Lion|FOLLOWERADJUSTMENT:-4", + }) + void testRoundRobinSimple(String label, String racesCsv, String input) throws PersistenceLayerException { - construct(Race.class, "Lion"); construct(CompanionList.class, "Familiar"); - runRoundRobin("Familiar|Lion"); + for (String r : racesCsv.split(",")) + { + construct(Race.class, r.trim()); + } + runRoundRobin(input); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf), label); } private void construct(Class cl, String name) @@ -189,33 +201,6 @@ private void construct(Class cl, String name) secondaryContext.getReferenceContext().constructCDOMObject(cl, name); } - @Test - public void testRoundRobinTwoRace() throws PersistenceLayerException - { - construct(CompanionList.class, "Familiar"); - construct(Race.class, "Lion"); - construct(Race.class, "Tiger"); - runRoundRobin("Familiar|Lion,Tiger"); - } - - @Test - public void testRoundRobinAnyRace() throws PersistenceLayerException - { - construct(CompanionList.class, "Familiar"); - construct(Race.class, "Lion"); - construct(Race.class, "Tiger"); - runRoundRobin("Familiar|ANY"); - } - - @Test - public void testRoundRobinTwoWithRacetype() throws PersistenceLayerException - { - construct(CompanionList.class, "Familiar"); - construct(Race.class, "Lion"); - construct(Race.class, "Tiger"); - runRoundRobin("Familiar|Lion,RACETYPE=Clawed"); - } - @Test void testRoundRobinType() throws PersistenceLayerException { @@ -272,15 +257,7 @@ void testRoundRobinMixedClauses() throws PersistenceLayerException } @Test - public void testRoundRobinFA() throws PersistenceLayerException - { - construct(CompanionList.class, "Familiar"); - construct(Race.class, "Lion"); - runRoundRobin("Familiar|Lion|FOLLOWERADJUSTMENT:-4"); - } - - @Test - public void testRoundRobinThreeFA() throws PersistenceLayerException + void testRoundRobinThreeFA() throws PersistenceLayerException { construct(CompanionList.class, "Familiar"); construct(Race.class, "Bear"); @@ -289,10 +266,11 @@ public void testRoundRobinThreeFA() throws PersistenceLayerException runRoundRobin("Familiar|Bear|FOLLOWERADJUSTMENT:-6", "Familiar|Lion|FOLLOWERADJUSTMENT:-4", "Familiar|Tiger|FOLLOWERADJUSTMENT:-5"); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)); } @Test - public void testRoundRobinTwoType() throws PersistenceLayerException + void testRoundRobinTwoType() throws PersistenceLayerException { construct(CompanionList.class, "Familiar"); construct(CompanionList.class, "Companion"); @@ -300,49 +278,54 @@ public void testRoundRobinTwoType() throws PersistenceLayerException construct(Race.class, "Tiger"); runRoundRobin("Companion|Lion|FOLLOWERADJUSTMENT:-5", "Familiar|Tiger|FOLLOWERADJUSTMENT:-5"); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)); } @Test - public void testRoundRobinComplex() throws PersistenceLayerException + void testRoundRobinComplex() throws PersistenceLayerException { construct(CompanionList.class, "Familiar"); construct(Race.class, "Lion"); construct(Race.class, "Tiger"); runRoundRobin("Familiar|Lion,Tiger|FOLLOWERADJUSTMENT:-3|!PRECLASS:1,Cleric=1|PRERACE:1,Human"); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)); } @Test - public void testRoundRobinTwoPRE() throws PersistenceLayerException + void testRoundRobinTwoPRE() throws PersistenceLayerException { construct(CompanionList.class, "Familiar"); construct(Race.class, "Lion"); construct(Race.class, "Tiger"); runRoundRobin("Familiar|Lion|FOLLOWERADJUSTMENT:-5", "Familiar|Tiger|FOLLOWERADJUSTMENT:-5|PRERACE:1,Human"); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)); } @Test - public void testRoundRobinDupePre() throws PersistenceLayerException + void testRoundRobinDupePre() throws PersistenceLayerException { construct(CompanionList.class, "Familiar"); construct(Race.class, "Tiger"); runRoundRobin( "Familiar|Tiger|FOLLOWERADJUSTMENT:-5|PRECLASS:1,Cleric=1", "Familiar|Tiger|FOLLOWERADJUSTMENT:-5|PRERACE:1,Human"); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)); } @Test - public void testRoundRobinDupePreDiffFA() throws PersistenceLayerException + void testRoundRobinDupePreDiffFA() throws PersistenceLayerException { construct(CompanionList.class, "Familiar"); construct(Race.class, "Tiger"); runRoundRobin( "Familiar|Tiger|FOLLOWERADJUSTMENT:-3|PRECLASS:1,Cleric=1", "Familiar|Tiger|FOLLOWERADJUSTMENT:-5|PRERACE:1,Human"); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)); } @Test - public void testRoundRobinReal() throws PersistenceLayerException + void testRoundRobinReal() throws PersistenceLayerException { construct(CompanionList.class, "Psicrystal"); construct(Race.class, "Psicrystal (Single Minded)"); @@ -365,6 +348,7 @@ public void testRoundRobinReal() throws PersistenceLayerException + "Psicrystal (Hero),Psicrystal (Liar),Psicrystal (Meticulous),Psicrystal (Nimble),Psicrystal (Observant)," + "Psicrystal (Poised),Psicrystal (Resolved),Psicrystal (Sage),Psicrystal (Single Minded)," + "Psicrystal (Sneaky),Psicrystal (Sympathetic)"); + assertNotNull(getWriteToken().unparse(primaryContext, primaryProf)); } @Override @@ -384,17 +368,4 @@ protected ConsolidationRule getConsolidationRule() { return ConsolidationRule.SEPARATE; } - -// private void buildCompanionMod(String type) -// { -// String mod = "isAMod"; -// ReferenceContext ref1 = primaryContext.ref; -// ReferenceContext ref2 = secondaryContext.ref; -// CompanionList cl1 = ref1.silentlyGetConstructedCDOMObject(CompanionList.class, type); -// CompanionList cl2 = ref2.silentlyGetConstructedCDOMObject(CompanionList.class, type); -// CompanionMod cm1 = ref1.constructCDOMObject(CompanionMod.class, mod); -// CompanionMod cm2 = ref2.constructCDOMObject(CompanionMod.class, mod); -// ref1.reassociateCategory(cl1, cm1); -// ref1.reassociateCategory(cl2, cm2); -// } }