Skip to content

Commit af0c0fc

Browse files
committed
fix: number attributes within their group, not globally
getAttributeOrder now filters by attribute group (data, inline, bench) before computing the index. Test attribute excluded from RUNNABLE_ATTRIBUTES since the method already gets its own gutter line marker. https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN
1 parent 4726b88 commit af0c0fc

6 files changed

Lines changed: 150 additions & 19 deletions

File tree

CLAUDE.md

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,17 +141,45 @@ The plugin registers extensions in `plugin.xml` under two namespaces:
141141

142142
The plugin recognizes PHP attributes defined in `TestoClasses.kt`. Constants are grouped into arrays for reuse across the codebase:
143143

144-
| Group (array) | Attributes (FQN) |
145-
|------------------------|-------------------------------------------------------------------------------------------------------|
146-
| `TEST_ATTRIBUTES` | `\Testo\Attribute\Test` (new), `\Testo\Application\Attribute\Test` (old) |
147-
| `TEST_INLINE_ATTRIBUTES` | `\Testo\Sample\TestInline` (old), `\Testo\Inline\TestInline` (new) |
148-
| `DATA_ATTRIBUTES` | `\Testo\Sample\DataProvider`, `\Testo\Data\DataProvider`, `\Testo\Sample\DataSet`, `\Testo\Data\DataSet`, `\Testo\Data\DataUnion`, `\Testo\Data\DataCross`, `\Testo\Data\DataZip` |
149-
| `BENCH_ATTRIBUTES` | `\Testo\Bench\BenchWith` |
144+
| Group (array) | Attributes (FQN) |
145+
|--------------------------|-----------------------------------------------------------------------------------|
146+
| `TEST_ATTRIBUTES` | `\Testo\Test`, `\Testo\Inline\TestInline` |
147+
| `TEST_INLINE_ATTRIBUTES` | `\Testo\Inline\TestInline` |
148+
| `DATA_ATTRIBUTES` | `\Testo\Data\DataProvider`, `\Testo\Data\DataSet`, `\Testo\Data\DataUnion`, `\Testo\Data\DataCross`, `\Testo\Data\DataZip` |
149+
| `BENCH_ATTRIBUTES` | `\Testo\Bench` |
150150

151151
Other constants: `ASSERT` (`\Testo\Assert`), `EXPECT` (`\Testo\Expect`), `ASSERTION_EXCEPTION`.
152152

153153
These arrays are spread into `RUNNABLE_ATTRIBUTES` (line markers) and `MEANINGFUL_ATTRIBUTES` (PsiUtil) — adding a new attribute to the group array automatically propagates it everywhere.
154154

155+
### Attribute Group Numbering
156+
157+
Attributes on a function/method are numbered **within their own group**, not globally. Each group has independent 0-based indexing. The groups are defined in `PsiUtil.ATTRIBUTE_GROUPS`:
158+
159+
| Group | Source array | Used for |
160+
|-------------------|---------------------------|-----------------------------------------------|
161+
| test data | `DATA_ATTRIBUTES` | Data providers for `#[Test]` methods |
162+
| inline | `TEST_INLINE_ATTRIBUTES` | Inline test cases (`#[TestInline]`) |
163+
| bench | `BENCH_ATTRIBUTES` | Benchmark data (`#[Bench]`) |
164+
165+
The `#[Test]` attribute itself is **not numbered** — it is a marker only. The method/function name already gets its own gutter line marker.
166+
167+
Example for a function `foo` with multiple attributes:
168+
```
169+
#[Test] → type=test (no index, marker only)
170+
#[DataProvider(...)] → type=test, foo:0
171+
#[DataSet([...])] → type=test, foo:1
172+
#[DataZip(...)] → type=test, foo:2
173+
#[DataCross(...)] → type=test, foo:3
174+
#[TestInline(...)] → type=inline, foo:0
175+
#[TestInline(...)] → type=inline, foo:1
176+
#[TestInline(...)] → type=inline, foo:2
177+
#[Bench(...)] → type=bench, foo:0
178+
#[Bench(...)] → type=bench, foo:1
179+
```
180+
181+
`RUNNABLE_ATTRIBUTES` (used for gutter line markers) contains `TEST_INLINE_ATTRIBUTES + BENCH_ATTRIBUTES + DATA_ATTRIBUTES` — it does **not** include `Test` since the method already gets a run marker via `getLocationInfo`.
182+
155183
### Test Detection Logic (mixin.kt)
156184

157185
A PHP element is recognized as a Testo test when:

src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ object TestoClasses {
3030
TEST,
3131
TEST_INLINE,
3232
)
33+
val TEST_INLINE_ATTRIBUTES = arrayOf(
34+
TEST_INLINE,
35+
)
3336
val BENCH_ATTRIBUTES = arrayOf(
3437
BENCH,
3538
)

src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ class TestoTestRunLineMarkerProvider : RunLineMarkerContributor() {
6464

6565
val attributesOwner = attribute.owner as PhpAttributesOwner
6666
val index = PsiUtil.getAttributeOrder(attribute, attributesOwner)
67-
68-
getInlineTestLocationHint(attributesOwner, index)
67+
if (index < 0) getLocationInfo(attributesOwner)
68+
else getInlineTestLocationHint(attributesOwner, index)
6969
}
7070

7171
element is PhpNamedElement -> {
@@ -96,7 +96,7 @@ class TestoTestRunLineMarkerProvider : RunLineMarkerContributor() {
9696

9797
companion object Companion {
9898
val RUNNABLE_ATTRIBUTES = arrayOf(
99-
*TestoClasses.TEST_ATTRIBUTES,
99+
*TestoClasses.TEST_INLINE_ATTRIBUTES,
100100
*TestoClasses.BENCH_ATTRIBUTES,
101101
*TestoClasses.DATA_ATTRIBUTES,
102102
)

src/main/kotlin/com/github/xepozz/testo/util/PsiUtil.kt

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,23 @@ object PsiUtil {
1313
*TestoClasses.BENCH_ATTRIBUTES,
1414
)
1515

16-
fun getAttributeOrder(attribute: PhpAttribute, owner: PhpAttributesOwner): Int = owner
17-
.attributes
18-
.filter { it.fqn in MEANINGFUL_ATTRIBUTES }
19-
.indexOf(attribute)
16+
val ATTRIBUTE_GROUPS: Array<Array<String>> = arrayOf(
17+
TestoClasses.DATA_ATTRIBUTES,
18+
TestoClasses.TEST_INLINE_ATTRIBUTES,
19+
TestoClasses.BENCH_ATTRIBUTES,
20+
)
21+
22+
fun getAttributeGroup(fqn: String?): Array<String>? =
23+
ATTRIBUTE_GROUPS.firstOrNull { fqn in it }
24+
25+
fun getAttributeOrder(attribute: PhpAttribute, owner: PhpAttributesOwner): Int {
26+
val group = getAttributeGroup(attribute.fqn) ?: return -1
27+
return owner.attributes
28+
.filter { it.fqn in group }
29+
.indexOf(attribute)
30+
}
2031

2132
fun getExitStatementOrder(element: PsiElement, function: Function): Int = ExitStatementsVisitor(element)
2233
.apply { function.accept(this) }
2334
.index
24-
}
35+
}

src/test/kotlin/com/github/xepozz/testo/PsiUtilTest.kt

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,88 @@ class PsiUtilTest : TestCase() {
3030
val attrs = PsiUtil.MEANINGFUL_ATTRIBUTES
3131
assertEquals(attrs.size, attrs.toSet().size)
3232
}
33+
34+
// --- Attribute group tests ---
35+
36+
fun testGetAttributeGroup_dataProviderInDataGroup() {
37+
val group = PsiUtil.getAttributeGroup(TestoClasses.DATA_PROVIDER)
38+
assertNotNull("DataProvider should have a group", group)
39+
assertSame(TestoClasses.DATA_ATTRIBUTES, group)
40+
}
41+
42+
fun testGetAttributeGroup_dataSetInDataGroup() {
43+
val group = PsiUtil.getAttributeGroup(TestoClasses.DATA_SET)
44+
assertNotNull("DataSet should have a group", group)
45+
assertSame(TestoClasses.DATA_ATTRIBUTES, group)
46+
}
47+
48+
fun testGetAttributeGroup_dataUnionInDataGroup() {
49+
val group = PsiUtil.getAttributeGroup(TestoClasses.DATA_UNION)
50+
assertNotNull("DataUnion should have a group", group)
51+
assertSame(TestoClasses.DATA_ATTRIBUTES, group)
52+
}
53+
54+
fun testGetAttributeGroup_dataCrossInDataGroup() {
55+
val group = PsiUtil.getAttributeGroup(TestoClasses.DATA_CROSS)
56+
assertNotNull("DataCross should have a group", group)
57+
assertSame(TestoClasses.DATA_ATTRIBUTES, group)
58+
}
59+
60+
fun testGetAttributeGroup_dataZipInDataGroup() {
61+
val group = PsiUtil.getAttributeGroup(TestoClasses.DATA_ZIP)
62+
assertNotNull("DataZip should have a group", group)
63+
assertSame(TestoClasses.DATA_ATTRIBUTES, group)
64+
}
65+
66+
fun testGetAttributeGroup_testInlineInInlineGroup() {
67+
val group = PsiUtil.getAttributeGroup(TestoClasses.TEST_INLINE)
68+
assertNotNull("TestInline should have a group", group)
69+
assertSame(TestoClasses.TEST_INLINE_ATTRIBUTES, group)
70+
}
71+
72+
fun testGetAttributeGroup_benchInBenchGroup() {
73+
val group = PsiUtil.getAttributeGroup(TestoClasses.BENCH)
74+
assertNotNull("Bench should have a group", group)
75+
assertSame(TestoClasses.BENCH_ATTRIBUTES, group)
76+
}
77+
78+
fun testGetAttributeGroup_testHasNoGroup() {
79+
val group = PsiUtil.getAttributeGroup(TestoClasses.TEST)
80+
assertNull("Plain Test attribute should not have a numbered group", group)
81+
}
82+
83+
fun testGetAttributeGroup_nullReturnsNull() {
84+
assertNull(PsiUtil.getAttributeGroup(null))
85+
}
86+
87+
fun testGetAttributeGroup_unknownReturnsNull() {
88+
assertNull(PsiUtil.getAttributeGroup("\\Some\\Unknown\\Attribute"))
89+
}
90+
91+
fun testAttributeGroups_allGroupedAttributesAreInMeaningful() {
92+
val meaningful = PsiUtil.MEANINGFUL_ATTRIBUTES.toSet()
93+
for (group in PsiUtil.ATTRIBUTE_GROUPS) {
94+
for (attr in group) {
95+
assertTrue("Grouped attribute $attr should be in MEANINGFUL_ATTRIBUTES", meaningful.contains(attr))
96+
}
97+
}
98+
}
99+
100+
fun testAttributeGroups_noOverlapBetweenGroups() {
101+
val groups = PsiUtil.ATTRIBUTE_GROUPS
102+
for (i in groups.indices) {
103+
for (j in i + 1 until groups.size) {
104+
val overlap = groups[i].toSet().intersect(groups[j].toSet())
105+
assertTrue("Groups $i and $j should not overlap, but share: $overlap", overlap.isEmpty())
106+
}
107+
}
108+
}
109+
110+
fun testAttributeGroups_totalCount() {
111+
val totalGrouped = PsiUtil.ATTRIBUTE_GROUPS.sumOf { it.size }
112+
val expectedGrouped = TestoClasses.DATA_ATTRIBUTES.size +
113+
TestoClasses.TEST_INLINE_ATTRIBUTES.size +
114+
TestoClasses.BENCH_ATTRIBUTES.size
115+
assertEquals(expectedGrouped, totalGrouped)
116+
}
33117
}

src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,24 @@ class TestoLineMarkerCompanionTest : TestCase() {
2121

2222
fun testRunnableAttributes_containsBenchAttribute() {
2323
val runnable = TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.toSet()
24-
assertTrue("Should contain \\Testo\\Bench\\Bench", runnable.contains(TestoClasses.BENCH))
24+
assertTrue("Should contain \\Testo\\Bench", runnable.contains(TestoClasses.BENCH))
2525
}
2626

27-
fun testRunnableAttributes_doesNotContainPlainTestAttributes() {
27+
fun testRunnableAttributes_containsTestInlineAttributes() {
2828
val runnable = TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.toSet()
29-
for (attr in TestoClasses.TEST_ATTRIBUTES) {
30-
assertFalse("Should not contain plain test attribute: $attr", runnable.contains(attr))
29+
for (attr in TestoClasses.TEST_INLINE_ATTRIBUTES) {
30+
assertTrue("Missing test inline attribute: $attr", runnable.contains(attr))
3131
}
3232
}
3333

34+
fun testRunnableAttributes_doesNotContainPlainTestAttribute() {
35+
val runnable = TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.toSet()
36+
assertFalse("Should not contain plain Test attribute", runnable.contains(TestoClasses.TEST))
37+
}
38+
3439
fun testRunnableAttributes_totalCount() {
3540
val expected = TestoClasses.DATA_ATTRIBUTES.size +
36-
TestoClasses.TEST_ATTRIBUTES.size +
41+
TestoClasses.TEST_INLINE_ATTRIBUTES.size +
3742
TestoClasses.BENCH_ATTRIBUTES.size
3843
assertEquals(expected, TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.size)
3944
}

0 commit comments

Comments
 (0)