Skip to content

Commit 93bb124

Browse files
committed
Add platform-level tests with BasePlatformTestCase and PHP PSI
Based on IntelliJ Platform SDK testing guidelines: - MixinPsiTest: 17 tests for isTestoClass, isTestoMethod, isTestoFile, isTestoExecutable, isTestoDataProviderLike using configureByText with PHP PSI - ExitStatementsVisitorTest: 5 tests for yield/return counting in PHP functions - TestoLineMarkerPsiTest: 6 tests for location hint generation (class, method, file, data provider, inline test hints) - TestoTestLocatorTest: 6 tests for getLocationInfo URL parsing - TestoDataProvidersIndexTest: 8 tests for DataProviderUsage record, externalizer serialization round-trips, index metadata - Fix MyPluginTest: remove reference to non-existent MyProjectService - Add PHP testData files for mixin tests https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN
1 parent 6971453 commit 93bb124

11 files changed

Lines changed: 684 additions & 8 deletions
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.github.xepozz.testo
2+
3+
import com.intellij.psi.util.PsiTreeUtil
4+
import com.intellij.testFramework.fixtures.BasePlatformTestCase
5+
import com.github.xepozz.testo.util.ExitStatementsVisitor
6+
import com.jetbrains.php.lang.PhpFileType
7+
import com.jetbrains.php.lang.psi.elements.Function
8+
import com.jetbrains.php.lang.psi.elements.Method
9+
import com.jetbrains.php.lang.psi.elements.PhpReturn
10+
import com.jetbrains.php.lang.psi.elements.PhpYield
11+
12+
class ExitStatementsVisitorTest : BasePlatformTestCase() {
13+
14+
fun testVisitor_countsYieldStatements() {
15+
val psiFile = myFixture.configureByText(
16+
PhpFileType.INSTANCE,
17+
"""<?php
18+
class Foo {
19+
public static function data(): iterable {
20+
yield [1, 2];
21+
yield [3, 4];
22+
yield [5, 6];
23+
}
24+
}"""
25+
)
26+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
27+
val yields = PsiTreeUtil.findChildrenOfType(method, PhpYield::class.java).toList()
28+
29+
assertEquals("Should find 3 yield statements", 3, yields.size)
30+
31+
// Visit up to the last yield - index should be 2 (0-based, incremented before stop)
32+
val visitor = ExitStatementsVisitor(yields.last())
33+
method.accept(visitor)
34+
assertEquals("Index should be 2 for the third yield (0-indexed)", 2, visitor.index)
35+
}
36+
37+
fun testVisitor_countsReturnStatements() {
38+
val psiFile = myFixture.configureByText(
39+
PhpFileType.INSTANCE,
40+
"""<?php
41+
class Foo {
42+
public static function data(): array {
43+
return [1, 2];
44+
}
45+
}"""
46+
)
47+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
48+
val returns = PsiTreeUtil.findChildrenOfType(method, PhpReturn::class.java).toList()
49+
50+
assertEquals("Should find 1 return statement", 1, returns.size)
51+
52+
val visitor = ExitStatementsVisitor(returns.first())
53+
method.accept(visitor)
54+
assertEquals("Index should be 0 for the first return", 0, visitor.index)
55+
}
56+
57+
fun testVisitor_stopsAtTargetElement() {
58+
val psiFile = myFixture.configureByText(
59+
PhpFileType.INSTANCE,
60+
"""<?php
61+
class Foo {
62+
public static function data(): iterable {
63+
yield [1];
64+
yield [2];
65+
yield [3];
66+
}
67+
}"""
68+
)
69+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
70+
val yields = PsiTreeUtil.findChildrenOfType(method, PhpYield::class.java).toList()
71+
72+
// Stop at the second yield
73+
val visitor = ExitStatementsVisitor(yields[1])
74+
method.accept(visitor)
75+
assertEquals("Index should be 1 when stopping at second yield", 1, visitor.index)
76+
}
77+
78+
fun testVisitor_firstYield() {
79+
val psiFile = myFixture.configureByText(
80+
PhpFileType.INSTANCE,
81+
"""<?php
82+
class Foo {
83+
public static function data(): iterable {
84+
yield [1];
85+
yield [2];
86+
}
87+
}"""
88+
)
89+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
90+
val yields = PsiTreeUtil.findChildrenOfType(method, PhpYield::class.java).toList()
91+
92+
val visitor = ExitStatementsVisitor(yields[0])
93+
method.accept(visitor)
94+
assertEquals("Index should be 0 for the first yield", 0, visitor.index)
95+
}
96+
97+
fun testVisitor_initialIndex() {
98+
val psiFile = myFixture.configureByText(
99+
PhpFileType.INSTANCE,
100+
"""<?php function noop(): void {}"""
101+
)
102+
val function = PsiTreeUtil.findChildrenOfType(psiFile, Function::class.java)
103+
.first { it !is Method }
104+
val visitor = ExitStatementsVisitor(function)
105+
assertEquals("Initial index should be -1", -1, visitor.index)
106+
}
107+
}
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
package com.github.xepozz.testo
2+
3+
import com.intellij.psi.util.PsiTreeUtil
4+
import com.intellij.testFramework.TestDataPath
5+
import com.intellij.testFramework.fixtures.BasePlatformTestCase
6+
import com.jetbrains.php.lang.PhpFileType
7+
import com.jetbrains.php.lang.psi.PhpFile
8+
import com.jetbrains.php.lang.psi.elements.Function
9+
import com.jetbrains.php.lang.psi.elements.Method
10+
import com.jetbrains.php.lang.psi.elements.PhpClass
11+
12+
@TestDataPath("\$CONTENT_ROOT/src/test/testData")
13+
class MixinPsiTest : BasePlatformTestCase() {
14+
15+
override fun getTestDataPath() = "src/test/testData"
16+
17+
// ---- isTestoClass ----
18+
19+
fun testIsTestoClass_classWithTestSuffix() {
20+
val psiFile = myFixture.configureByText(
21+
PhpFileType.INSTANCE,
22+
"""<?php class UserTest { public function testSomething(): void {} }"""
23+
)
24+
val phpClass = PsiTreeUtil.findChildOfType(psiFile, PhpClass::class.java)!!
25+
assertTrue("Class ending with 'Test' should be a Testo class", phpClass.isTestoClass())
26+
}
27+
28+
fun testIsTestoClass_classWithTestBaseSuffix() {
29+
val psiFile = myFixture.configureByText(
30+
PhpFileType.INSTANCE,
31+
"""<?php class AbstractTestBase { public function testBase(): void {} }"""
32+
)
33+
val phpClass = PsiTreeUtil.findChildOfType(psiFile, PhpClass::class.java)!!
34+
assertTrue("Class ending with 'TestBase' should be a Testo class", phpClass.isTestoClass())
35+
}
36+
37+
fun testIsTestoClass_regularClass() {
38+
val psiFile = myFixture.configureByText(
39+
PhpFileType.INSTANCE,
40+
"""<?php class UserService { public function getUser(): void {} }"""
41+
)
42+
val phpClass = PsiTreeUtil.findChildOfType(psiFile, PhpClass::class.java)!!
43+
assertFalse("Regular class should not be a Testo class", phpClass.isTestoClass())
44+
}
45+
46+
fun testIsTestoClass_classWithTestMethodButNoSuffix() {
47+
val psiFile = myFixture.configureByText(
48+
PhpFileType.INSTANCE,
49+
"""<?php class MyFeature { public function testSomething(): void {} }"""
50+
)
51+
val phpClass = PsiTreeUtil.findChildOfType(psiFile, PhpClass::class.java)!!
52+
assertTrue("Class with test methods should be a Testo class", phpClass.isTestoClass())
53+
}
54+
55+
// ---- isTestoMethod ----
56+
57+
fun testIsTestoMethod_publicMethodStartingWithTest() {
58+
val psiFile = myFixture.configureByText(
59+
PhpFileType.INSTANCE,
60+
"""<?php class Foo { public function testSomething(): void {} }"""
61+
)
62+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
63+
assertTrue("Public method starting with 'test' should be a Testo method", method.isTestoMethod())
64+
}
65+
66+
fun testIsTestoMethod_privateMethodStartingWithTest() {
67+
val psiFile = myFixture.configureByText(
68+
PhpFileType.INSTANCE,
69+
"""<?php class Foo { private function testSomething(): void {} }"""
70+
)
71+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
72+
assertFalse("Private method starting with 'test' should not be a Testo method", method.isTestoMethod())
73+
}
74+
75+
fun testIsTestoMethod_publicMethodNotStartingWithTest() {
76+
val psiFile = myFixture.configureByText(
77+
PhpFileType.INSTANCE,
78+
"""<?php class Foo { public function helper(): void {} }"""
79+
)
80+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
81+
assertFalse("Public method not starting with 'test' should not be a Testo method", method.isTestoMethod())
82+
}
83+
84+
fun testIsTestoMethod_protectedMethodStartingWithTest() {
85+
val psiFile = myFixture.configureByText(
86+
PhpFileType.INSTANCE,
87+
"""<?php class Foo { protected function testProtected(): void {} }"""
88+
)
89+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
90+
assertFalse("Protected method starting with 'test' should not be a Testo method", method.isTestoMethod())
91+
}
92+
93+
// ---- isTestoDataProviderLike ----
94+
95+
fun testIsTestoDataProviderLike_publicStaticMethod() {
96+
val psiFile = myFixture.configureByText(
97+
PhpFileType.INSTANCE,
98+
"""<?php class Foo { public static function provideData(): iterable { yield [1]; } }"""
99+
)
100+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
101+
assertTrue("Public static method should be data provider like", method.isTestoDataProviderLike())
102+
}
103+
104+
fun testIsTestoDataProviderLike_publicNonStaticMethod() {
105+
val psiFile = myFixture.configureByText(
106+
PhpFileType.INSTANCE,
107+
"""<?php class Foo { public function provideData(): iterable { yield [1]; } }"""
108+
)
109+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
110+
assertFalse("Public non-static method should not be data provider like", method.isTestoDataProviderLike())
111+
}
112+
113+
fun testIsTestoDataProviderLike_privateStaticMethod() {
114+
val psiFile = myFixture.configureByText(
115+
PhpFileType.INSTANCE,
116+
"""<?php class Foo { private static function provideData(): iterable { yield [1]; } }"""
117+
)
118+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
119+
assertFalse("Private static method should not be data provider like", method.isTestoDataProviderLike())
120+
}
121+
122+
fun testIsTestoDataProviderLike_function() {
123+
val psiFile = myFixture.configureByText(
124+
PhpFileType.INSTANCE,
125+
"""<?php function provideData(): iterable { yield [1]; }"""
126+
)
127+
val function = PsiTreeUtil.findChildrenOfType(psiFile, Function::class.java)
128+
.first { it !is Method }
129+
assertTrue("Standalone function should be data provider like", function.isTestoDataProviderLike())
130+
}
131+
132+
// ---- isTestoFile ----
133+
134+
fun testIsTestoFile_fileNameEndingWithTest() {
135+
val psiFile = myFixture.configureByText(
136+
"UserTest.php",
137+
"""<?php class UserTest { public function testSomething(): void {} }"""
138+
)
139+
assertTrue("File named *Test.php should be a Testo file", psiFile.isTestoFile())
140+
}
141+
142+
fun testIsTestoFile_regularFile() {
143+
val psiFile = myFixture.configureByText(
144+
"UserService.php",
145+
"""<?php class UserService { public function getUser(): void {} }"""
146+
)
147+
assertFalse("Regular PHP file should not be a Testo file", psiFile.isTestoFile())
148+
}
149+
150+
fun testIsTestoFile_fileWithTestClass() {
151+
val psiFile = myFixture.configureByText(
152+
"Features.php",
153+
"""<?php class FeatureTest { public function testFeature(): void {} }"""
154+
)
155+
assertTrue("File containing test class should be a Testo file", psiFile.isTestoFile())
156+
}
157+
158+
// ---- isTestoExecutable ----
159+
160+
fun testIsTestoExecutable_testMethod() {
161+
val psiFile = myFixture.configureByText(
162+
PhpFileType.INSTANCE,
163+
"""<?php class Foo { public function testSomething(): void {} }"""
164+
)
165+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
166+
assertTrue("Test method should be executable", method.isTestoExecutable())
167+
}
168+
169+
fun testIsTestoExecutable_nonTestMethod() {
170+
val psiFile = myFixture.configureByText(
171+
PhpFileType.INSTANCE,
172+
"""<?php class Foo { public function helper(): void {} }"""
173+
)
174+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
175+
assertFalse("Non-test method should not be executable", method.isTestoExecutable())
176+
}
177+
178+
// ---- isTestoClassFile / isTestoFunctionFile ----
179+
180+
fun testIsTestoClassFile() {
181+
val psiFile = myFixture.configureByText(
182+
PhpFileType.INSTANCE,
183+
"""<?php class SomeTest { public function testA(): void {} }"""
184+
) as PhpFile
185+
assertTrue("File with test class should be a Testo class file", psiFile.isTestoClassFile())
186+
}
187+
188+
fun testIsTestoClassFile_noTestClass() {
189+
val psiFile = myFixture.configureByText(
190+
PhpFileType.INSTANCE,
191+
"""<?php class Helper { public function doStuff(): void {} }"""
192+
) as PhpFile
193+
assertFalse("File without test class should not be a Testo class file", psiFile.isTestoClassFile())
194+
}
195+
196+
// ---- Multiple methods in one class ----
197+
198+
fun testMultipleMethods_mixedTestAndNonTest() {
199+
val psiFile = myFixture.configureByText(
200+
PhpFileType.INSTANCE,
201+
"""<?php
202+
class UserTest {
203+
public function testCreate(): void {}
204+
public function testDelete(): void {}
205+
public function setUp(): void {}
206+
private function helper(): void {}
207+
}"""
208+
)
209+
val methods = PsiTreeUtil.findChildrenOfType(psiFile, Method::class.java)
210+
val testMethods = methods.filter { it.isTestoMethod() }
211+
val nonTestMethods = methods.filter { !it.isTestoMethod() }
212+
213+
assertEquals("Should find 2 test methods", 2, testMethods.size)
214+
assertEquals("Should find 2 non-test methods", 2, nonTestMethods.size)
215+
assertTrue(testMethods.any { it.name == "testCreate" })
216+
assertTrue(testMethods.any { it.name == "testDelete" })
217+
}
218+
219+
// ---- Edge cases ----
220+
221+
fun testIsTestoClass_emptyClass() {
222+
val psiFile = myFixture.configureByText(
223+
PhpFileType.INSTANCE,
224+
"""<?php class EmptyTest {}"""
225+
)
226+
val phpClass = PsiTreeUtil.findChildOfType(psiFile, PhpClass::class.java)!!
227+
assertTrue("Empty class ending with 'Test' should still be a Testo class", phpClass.isTestoClass())
228+
}
229+
230+
fun testIsTestoMethod_methodNamedExactlyTest() {
231+
val psiFile = myFixture.configureByText(
232+
PhpFileType.INSTANCE,
233+
"""<?php class Foo { public function test(): void {} }"""
234+
)
235+
val method = PsiTreeUtil.findChildOfType(psiFile, Method::class.java)!!
236+
assertTrue("Method named exactly 'test' should be a Testo method", method.isTestoMethod())
237+
}
238+
}

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package com.github.xepozz.testo
22

33
import com.intellij.ide.highlighter.XmlFileType
4-
import com.intellij.openapi.components.service
54
import com.intellij.psi.xml.XmlFile
65
import com.intellij.testFramework.TestDataPath
76
import com.intellij.testFramework.fixtures.BasePlatformTestCase
87
import com.intellij.util.PsiErrorElementUtil
9-
import com.github.xepozz.testo.services.MyProjectService
108

119
@TestDataPath("\$CONTENT_ROOT/src/test/testData")
1210
class MyPluginTest : BasePlatformTestCase() {
@@ -29,11 +27,5 @@ class MyPluginTest : BasePlatformTestCase() {
2927
myFixture.testRename("foo.xml", "foo_after.xml", "a2")
3028
}
3129

32-
fun testProjectService() {
33-
val projectService = project.service<MyProjectService>()
34-
35-
assertNotSame(projectService.getRandomNumber(), projectService.getRandomNumber())
36-
}
37-
3830
override fun getTestDataPath() = "src/test/testData/rename"
3931
}

0 commit comments

Comments
 (0)