Skip to content

Commit dc44e23

Browse files
committed
test: add JUnit 4 + IntelliJ Platform tests for existing functionality
Covers all production code currently shipped by the plugin: - MixinTest — PhpClass.isActivity/isWorkflow, Method.isActivity/isWorkflow tolerance rules, hasAttribute FQN matching (7 cases). - PhpActivityMethodInspectionTest — warning detection + quick-fix application (4 cases). - PhpActivityMethodUsageInspectionTest — warning at the call site for missing #[ActivityMethod] attributes (3 cases). - PhpActivityClassIndexTest / PhpActivityMethodIndexTest / PhpWorkflowClassIndexTest / PhpWorkflowMethodIndexTest — file-based index coverage (class + method, with/without attribute, negative cases). Infrastructure: - TemporalPhpTestCase base class wires VfsRootAccess for src/test/resources and pre-copies a Temporal PHP stubs file so #[ActivityInterface], #[ActivityMethod], #[WorkflowInterface] etc. resolve to the same FQNs TemporalClasses.kt declares. - Switch test IDE target from IntelliJ IDEA to PhpStorm so the PHP plugin and its file type are registered during tests (PHP is bundled in PhpStorm; com.jetbrains.php is now listed as a bundled plugin instead of a marketplace plugin). 21 tests, all green.
1 parent 6f89b20 commit dc44e23

25 files changed

Lines changed: 758 additions & 3 deletions

build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ dependencies {
3636

3737
// IntelliJ Platform Gradle Plugin Dependencies Extension - read more: https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-dependencies-extension.html
3838
intellijPlatform {
39-
intellijIdea(providers.gradleProperty("platformVersion"))
39+
// Use PhpStorm as the target IDE: it bundles PHP support so that
40+
// both runIde and tests have the PHP file type / PSI registered.
41+
phpstorm(providers.gradleProperty("platformVersion"))
4042

4143
// Plugin Dependencies. Uses `platformBundledPlugins` property from the gradle.properties file for bundled IntelliJ Platform plugins.
4244
bundledPlugins(providers.gradleProperty("platformBundledPlugins").map { it.split(',') })

gradle.properties

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ platformVersion = 2025.1.1
1414

1515
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
1616
# Example: platformPlugins = com.jetbrains.php:203.4449.22, org.intellij.scala:2023.3.27@EAP
17-
platformPlugins = com.jetbrains.php:251.23774.16,com.jetbrains.hackathon.indices.viewer:1.30,com.github.xepozz.metastorm:2025.1.26
17+
# PHP is bundled in the PhpStorm target (see `phpstorm(...)` in build.gradle.kts) — declare it as a bundled plugin.
18+
platformPlugins = com.jetbrains.hackathon.indices.viewer:1.30,com.github.xepozz.metastorm:2025.1.26
1819
# Example: platformBundledPlugins = com.intellij.java
19-
platformBundledPlugins =
20+
platformBundledPlugins = com.jetbrains.php
2021
# Example: platformBundledModules = intellij.spellchecker
2122
platformBundledModules =
2223

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.github.xepozz.temporal.languages.php
2+
3+
import com.github.xepozz.temporal.testing.TemporalPhpTestCase
4+
import com.intellij.psi.util.PsiTreeUtil
5+
import com.jetbrains.php.lang.psi.PhpFile
6+
import com.jetbrains.php.lang.psi.elements.Method
7+
import com.jetbrains.php.lang.psi.elements.PhpClass
8+
9+
class MixinTest : TemporalPhpTestCase() {
10+
11+
private lateinit var classes: Map<String, PhpClass>
12+
13+
override fun setUp() {
14+
super.setUp()
15+
val file = myFixture.configureByFile("php/mixin/MixinFixtures.php") as PhpFile
16+
classes = PsiTreeUtil
17+
.findChildrenOfType(file, PhpClass::class.java)
18+
.associateBy { it.name }
19+
}
20+
21+
fun testPhpClassIsActivityReflectsAttributePresence() {
22+
assertTrue(classes["MyActivityInterface"]!!.isActivity())
23+
assertTrue(classes["ConcreteActivityClass"]!!.isActivity())
24+
assertFalse(classes["MyWorkflowInterface"]!!.isActivity())
25+
assertFalse(classes["ConcreteWorkflowClass"]!!.isActivity())
26+
assertFalse(classes["PlainClass"]!!.isActivity())
27+
}
28+
29+
fun testPhpClassIsWorkflowReflectsAttributePresence() {
30+
assertTrue(classes["MyWorkflowInterface"]!!.isWorkflow())
31+
assertTrue(classes["ConcreteWorkflowClass"]!!.isWorkflow())
32+
assertFalse(classes["MyActivityInterface"]!!.isWorkflow())
33+
assertFalse(classes["PlainClass"]!!.isWorkflow())
34+
}
35+
36+
fun testHasAttributeMatchesTemporalClassFqns() {
37+
val activity = classes["ConcreteActivityClass"]!!
38+
assertTrue(activity.hasAttribute(TemporalClasses.ACTIVITY))
39+
assertFalse(activity.hasAttribute(TemporalClasses.WORKFLOW))
40+
41+
val workflow = classes["ConcreteWorkflowClass"]!!
42+
assertTrue(workflow.hasAttribute(TemporalClasses.WORKFLOW))
43+
assertFalse(workflow.hasAttribute(TemporalClasses.ACTIVITY))
44+
45+
val plain = classes["PlainClass"]!!
46+
assertFalse(plain.hasAttribute(TemporalClasses.ACTIVITY))
47+
assertFalse(plain.hasAttribute(TemporalClasses.WORKFLOW))
48+
}
49+
50+
fun testMethodIsActivityIsTolerantForPublicInstanceMethodsInActivityClass() {
51+
val klass = classes["ConcreteActivityClass"]!!
52+
53+
assertTrue("explicit #[ActivityMethod]", klass.methodByName("withAttr").isActivity())
54+
assertTrue("public non-static method tolerated", klass.methodByName("withoutAttr").isActivity())
55+
}
56+
57+
fun testMethodIsActivityExcludesStaticMagicAndNonPublicMethods() {
58+
val klass = classes["ConcreteActivityClass"]!!
59+
60+
assertFalse("static", klass.methodByName("staticMethod").isActivity())
61+
assertFalse("magic __construct", klass.methodByName("__construct").isActivity())
62+
assertFalse("non-public", klass.methodByName("protectedMethod").isActivity())
63+
}
64+
65+
fun testMethodIsWorkflowIsTolerantForPublicInstanceMethodsInWorkflowClass() {
66+
val klass = classes["ConcreteWorkflowClass"]!!
67+
68+
assertTrue("explicit #[WorkflowMethod]", klass.methodByName("run").isWorkflow())
69+
assertTrue("tolerant", klass.methodByName("helperWithoutAttribute").isWorkflow())
70+
}
71+
72+
fun testMethodIsActivityReturnsFalseOutsideOfActivityClass() {
73+
val plainMethod = classes["PlainClass"]!!.methodByName("method")
74+
assertFalse(plainMethod.isActivity())
75+
assertFalse(plainMethod.isWorkflow())
76+
}
77+
78+
private fun PhpClass.methodByName(name: String): Method =
79+
ownMethods.firstOrNull { it.name == name }
80+
?: throw AssertionError("Method '$name' not found in ${this.name}; available: ${ownMethods.joinToString { it.name }}")
81+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.github.xepozz.temporal.languages.php.index
2+
3+
import com.github.xepozz.temporal.testing.TemporalPhpTestCase
4+
import com.intellij.psi.search.GlobalSearchScope
5+
import com.intellij.util.indexing.FileBasedIndex
6+
7+
class PhpActivityClassIndexTest : TemporalPhpTestCase() {
8+
9+
fun testIndexesOnlyClassesWithActivityInterfaceAttribute() {
10+
myFixture.copyFileToProject(
11+
"php/index/OrderActivityInterface.php",
12+
"src/Activity/OrderActivity.php",
13+
)
14+
myFixture.copyFileToProject(
15+
"php/index/PaymentActivityInterface.php",
16+
"src/Activity/PaymentActivity.php",
17+
)
18+
myFixture.copyFileToProject(
19+
"php/index/NotAnActivity.php",
20+
"src/Service/NotAnActivity.php",
21+
)
22+
23+
val keys = collectLiveKeys()
24+
25+
assertContainsElements(
26+
keys,
27+
"\\App\\Activity\\OrderActivity",
28+
"\\App\\Activity\\PaymentActivity",
29+
)
30+
assertDoesntContain(keys, "\\App\\Service\\Foo", "\\App\\Service\\BarInterface")
31+
}
32+
33+
fun testEmptyProjectHasNoActivityClasses() {
34+
val keys = collectLiveKeys()
35+
36+
assertEmpty(keys)
37+
}
38+
39+
private fun collectLiveKeys(): List<String> {
40+
val idx = FileBasedIndex.getInstance()
41+
val scope = GlobalSearchScope.projectScope(project)
42+
return idx.getAllKeys(PhpActivityClassIndex.NAME, project)
43+
.filter { key ->
44+
idx.getContainingFiles(PhpActivityClassIndex.NAME, key, scope).isNotEmpty()
45+
}
46+
}
47+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.github.xepozz.temporal.languages.php.index
2+
3+
import com.github.xepozz.temporal.testing.TemporalPhpTestCase
4+
import com.intellij.psi.search.GlobalSearchScope
5+
import com.intellij.util.indexing.FileBasedIndex
6+
7+
class PhpActivityMethodIndexTest : TemporalPhpTestCase() {
8+
9+
fun testIndexesEveryPublicConcreteMethodInsideAnActivityClass() {
10+
myFixture.copyFileToProject(
11+
"php/index/OrderActivityInterface.php",
12+
"src/Activity/OrderActivity.php",
13+
)
14+
myFixture.copyFileToProject(
15+
"php/index/PaymentActivityInterface.php",
16+
"src/Activity/PaymentActivity.php",
17+
)
18+
19+
val keys = collectLiveKeys()
20+
21+
// Method.isActivity() is tolerant — public non-static non-abstract methods
22+
// in a class carrying #[ActivityInterface] are indexed whether or not
23+
// they declare #[ActivityMethod] themselves.
24+
assertContainsElements(
25+
keys,
26+
"\\App\\Activity\\OrderActivity::reserve",
27+
"\\App\\Activity\\OrderActivity::cancel",
28+
"\\App\\Activity\\OrderActivity::track",
29+
"\\App\\Activity\\PaymentActivity::charge",
30+
"\\App\\Activity\\PaymentActivity::refund",
31+
)
32+
}
33+
34+
fun testDoesNotIndexMethodsOfNonActivityClasses() {
35+
myFixture.copyFileToProject(
36+
"php/index/NotAnActivity.php",
37+
"src/Service/NotAnActivity.php",
38+
)
39+
40+
val keys = collectLiveKeys()
41+
42+
assertDoesntContain(
43+
keys,
44+
"\\App\\Service\\Foo::bar",
45+
"\\App\\Service\\BarInterface::baz",
46+
)
47+
}
48+
49+
private fun collectLiveKeys(): List<String> {
50+
val idx = FileBasedIndex.getInstance()
51+
val scope = GlobalSearchScope.projectScope(project)
52+
return idx.getAllKeys(PhpActivityMethodIndex.NAME, project)
53+
.filter { key ->
54+
idx.getContainingFiles(PhpActivityMethodIndex.NAME, key, scope).isNotEmpty()
55+
}
56+
}
57+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.github.xepozz.temporal.languages.php.index
2+
3+
import com.github.xepozz.temporal.testing.TemporalPhpTestCase
4+
import com.intellij.psi.search.GlobalSearchScope
5+
import com.intellij.util.indexing.FileBasedIndex
6+
7+
class PhpWorkflowClassIndexTest : TemporalPhpTestCase() {
8+
9+
fun testIndexesOnlyClassesWithWorkflowInterfaceAttribute() {
10+
myFixture.copyFileToProject(
11+
"php/index/OrderWorkflowInterface.php",
12+
"src/Workflow/OrderWorkflow.php",
13+
)
14+
myFixture.copyFileToProject(
15+
"php/index/ReportWorkflowInterface.php",
16+
"src/Workflow/ReportWorkflow.php",
17+
)
18+
myFixture.copyFileToProject(
19+
"php/index/NotAnActivity.php",
20+
"src/Service/NotAnActivity.php",
21+
)
22+
23+
val keys = collectLiveKeys()
24+
25+
assertContainsElements(
26+
keys,
27+
"\\App\\Workflow\\OrderWorkflow",
28+
"\\App\\Workflow\\ReportWorkflow",
29+
)
30+
assertDoesntContain(keys, "\\App\\Service\\Foo", "\\App\\Service\\BarInterface")
31+
}
32+
33+
private fun collectLiveKeys(): List<String> {
34+
val idx = FileBasedIndex.getInstance()
35+
val scope = GlobalSearchScope.projectScope(project)
36+
return idx.getAllKeys(PhpWorkflowClassIndex.NAME, project)
37+
.filter { key ->
38+
idx.getContainingFiles(PhpWorkflowClassIndex.NAME, key, scope).isNotEmpty()
39+
}
40+
}
41+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.github.xepozz.temporal.languages.php.index
2+
3+
import com.github.xepozz.temporal.testing.TemporalPhpTestCase
4+
import com.intellij.psi.search.GlobalSearchScope
5+
import com.intellij.util.indexing.FileBasedIndex
6+
7+
class PhpWorkflowMethodIndexTest : TemporalPhpTestCase() {
8+
9+
fun testIndexesMethodsOfWorkflowClasses() {
10+
myFixture.copyFileToProject(
11+
"php/index/OrderWorkflowInterface.php",
12+
"src/Workflow/OrderWorkflow.php",
13+
)
14+
myFixture.copyFileToProject(
15+
"php/index/ReportWorkflowInterface.php",
16+
"src/Workflow/ReportWorkflow.php",
17+
)
18+
19+
val keys = collectLiveKeys()
20+
21+
assertContainsElements(
22+
keys,
23+
"\\App\\Workflow\\OrderWorkflow::run",
24+
"\\App\\Workflow\\OrderWorkflow::cancel",
25+
"\\App\\Workflow\\ReportWorkflow::generate",
26+
)
27+
}
28+
29+
fun testDoesNotIndexMethodsOfNonWorkflowClasses() {
30+
myFixture.copyFileToProject(
31+
"php/index/NotAnActivity.php",
32+
"src/Service/NotAnActivity.php",
33+
)
34+
35+
val keys = collectLiveKeys()
36+
37+
assertDoesntContain(
38+
keys,
39+
"\\App\\Service\\Foo::bar",
40+
"\\App\\Service\\BarInterface::baz",
41+
)
42+
}
43+
44+
private fun collectLiveKeys(): List<String> {
45+
val idx = FileBasedIndex.getInstance()
46+
val scope = GlobalSearchScope.projectScope(project)
47+
return idx.getAllKeys(PhpWorkflowMethodIndex.NAME, project)
48+
.filter { key ->
49+
idx.getContainingFiles(PhpWorkflowMethodIndex.NAME, key, scope).isNotEmpty()
50+
}
51+
}
52+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.github.xepozz.temporal.languages.php.inspections
2+
3+
import com.github.xepozz.temporal.TemporalBundle
4+
import com.github.xepozz.temporal.testing.TemporalPhpTestCase
5+
6+
class PhpActivityMethodInspectionTest : TemporalPhpTestCase() {
7+
8+
override fun setUp() {
9+
super.setUp()
10+
myFixture.enableInspections(PhpActivityMethodInspection::class.java)
11+
}
12+
13+
fun testWarnsOnActivityMethodWithoutAttribute() {
14+
myFixture.configureByFile("php/inspections/activityMethodMissingAttribute.php")
15+
16+
myFixture.checkHighlighting(/* checkWarnings = */ true, false, false)
17+
}
18+
19+
fun testNoWarningsWhenEveryMethodIsAnnotated() {
20+
myFixture.configureByFile("php/inspections/activityMethodAllAnnotated.php")
21+
22+
myFixture.checkHighlighting(true, false, false)
23+
}
24+
25+
fun testIgnoresClassesWithoutActivityInterfaceAttribute() {
26+
myFixture.configureByFile("php/inspections/nonActivityClass.php")
27+
28+
myFixture.checkHighlighting(true, false, false)
29+
}
30+
31+
fun testQuickFixAddsActivityMethodAttribute() {
32+
myFixture.configureByFile("php/inspections/activityMethodMissingAttributeFix.php")
33+
34+
val expected = TemporalBundle.message("inspection.php.activity.method.attribute.missing.quick.fix")
35+
val fix = myFixture.getAllQuickFixes().firstOrNull { it.text == expected }
36+
?: throw AssertionError(
37+
"No quick fix named '$expected'; available: " +
38+
myFixture.getAllQuickFixes().joinToString { it.text }
39+
)
40+
myFixture.launchAction(fix)
41+
42+
myFixture.checkResultByFile("php/inspections/activityMethodMissingAttributeFix.after.php")
43+
}
44+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.github.xepozz.temporal.languages.php.inspections
2+
3+
import com.github.xepozz.temporal.testing.TemporalPhpTestCase
4+
5+
class PhpActivityMethodUsageInspectionTest : TemporalPhpTestCase() {
6+
7+
override fun setUp() {
8+
super.setUp()
9+
myFixture.enableInspections(PhpActivityMethodUsageInspection::class.java)
10+
}
11+
12+
fun testWarnsAtCallSiteWhenTargetMethodLacksActivityMethodAttribute() {
13+
myFixture.configureByFile("php/inspections/activityMethodUsageMissing.php")
14+
15+
myFixture.checkHighlighting(/* checkWarnings = */ true, false, false)
16+
}
17+
18+
fun testNoWarningsWhenEveryTargetMethodIsAnnotated() {
19+
myFixture.configureByFile("php/inspections/activityMethodUsageOk.php")
20+
21+
myFixture.checkHighlighting(true, false, false)
22+
}
23+
24+
fun testIgnoresCallsToNonActivityClasses() {
25+
myFixture.configureByFile("php/inspections/nonActivityUsage.php")
26+
27+
myFixture.checkHighlighting(true, false, false)
28+
}
29+
}

0 commit comments

Comments
 (0)