Skip to content

Commit 2621676

Browse files
authored
Merge pull request #47 from j-plugins/claude/create-claude-md-docs-TbEpZ
Add comprehensive test suite and improve run configuration UI
2 parents c670f06 + 3620075 commit 2621676

35 files changed

Lines changed: 2109 additions & 85 deletions

CLAUDE.md

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# CLAUDE.md
2+
3+
## Project Overview
4+
5+
IntelliJ IDEA / PhpStorm plugin for **Testo** — a PHP testing framework.
6+
Provides full IDE integration: test discovery, run configurations, code generation, inspections, and navigation.
7+
8+
- **Plugin ID:** `com.github.xepozz.testo`
9+
- **Plugin Name:** Testo PHP
10+
- **Author:** Dmitrii Derepko (@xepozz)
11+
- **Repository:** https://github.com/j-plugins/testo-plugin
12+
- **Marketplace:** JetBrains Marketplace
13+
14+
## Tech Stack
15+
16+
| Component | Version / Value |
17+
|----------------------|---------------------------|
18+
| Language | Kotlin 2.3.0 |
19+
| JVM Toolchain | Java 21 |
20+
| IntelliJ Platform | 2024.3.4 (IU — Ultimate) |
21+
| Min platform build | 243 (2024.3.x) |
22+
| Build system | Gradle 9.3.0 |
23+
| IntelliJ Plugin SDK | `org.jetbrains.intellij.platform` 2.11.0 |
24+
| Changelog plugin | `org.jetbrains.changelog` 2.5.0 |
25+
| Code quality | Qodana 2025.3.1 |
26+
| Coverage | Kover 0.9.4 |
27+
| Test framework | JUnit 4.13.2, OpenTest4J 1.3.0 |
28+
29+
## Build & Run Commands
30+
31+
```bash
32+
# Build the plugin
33+
./gradlew buildPlugin
34+
35+
# Run tests
36+
./gradlew check
37+
38+
# Run IDE with plugin loaded (for manual testing)
39+
./gradlew runIde
40+
41+
# Verify plugin compatibility
42+
./gradlew verifyPlugin
43+
44+
# Run UI tests (requires robot-server)
45+
./gradlew runIdeForUiTests
46+
```
47+
48+
## Project Structure
49+
50+
```
51+
src/main/kotlin/com/github/xepozz/testo/
52+
├── TestoBundle.kt # i18n message bundle
53+
├── TestoClasses.kt # FQN constants for Testo PHP classes/attributes
54+
├── TestoContext.kt # Live template context
55+
├── TestoIcons.kt # Icon definitions
56+
├── TestoUtil.kt # Project-level Testo availability check
57+
├── TestoComposerConfig.kt # Composer package detection
58+
├── mixin.kt # PSI extension functions (isTestoMethod, isTestoClass, etc.)
59+
├── PsiUtil.kt # General PSI utilities
60+
├── ExitStatementsVisitor.kt # PHP exit statement analysis
61+
├── SpellcheckingDictionaryProvider.kt
62+
63+
├── actions/ # Code generation actions
64+
│ ├── TestoGenerateTestMethodAction.kt
65+
│ └── TestoGenerateMethodActionBase.kt
66+
67+
├── index/ # File-based index for data providers
68+
│ ├── TestoDataProvidersIndex.kt
69+
│ └── TestoDataProviderUtils.kt
70+
71+
├── references/ # Reference resolution & implicit usage
72+
│ └── TestFunctionImplicitUsageProvider.kt
73+
74+
├── tests/ # Core test framework integration
75+
│ ├── TestoFrameworkType.kt # PhpTestFrameworkType implementation
76+
│ ├── TestoTestDescriptor.kt # Test class/method discovery
77+
│ ├── TestoTestLocator.kt # Stack trace → source navigation
78+
│ ├── TestoTestRunLineMarkerProvider.kt # Gutter run icons
79+
│ ├── TestoStackTraceParser.kt # Test output parsing
80+
│ ├── TestoConsoleProperties.kt # Console configuration
81+
│ ├── TestoVersionDetector.kt # Testo version detection
82+
│ │
83+
│ ├── actions/ # Test-specific actions
84+
│ │ ├── TestoNewTestFromClassAction.kt
85+
│ │ ├── TestoTestActionProvider.kt
86+
│ │ ├── TestoRerunFailedTestsAction.kt
87+
│ │ └── TestoRunCommandAction.kt
88+
│ │
89+
│ ├── inspections/
90+
│ │ └── TestoInspectionSuppressor.kt
91+
│ │
92+
│ ├── overrides/ # UI customization
93+
│ │
94+
│ ├── run/ # Run configuration subsystem
95+
│ │ ├── TestoRunConfigurationType.kt
96+
│ │ ├── TestoRunConfiguration.kt
97+
│ │ ├── TestoRunConfigurationFactory.kt
98+
│ │ ├── TestoRunConfigurationProducer.kt # Context-based config creation
99+
│ │ ├── TestoRunConfigurationHandler.kt
100+
│ │ ├── TestoRunConfigurationSettings.kt
101+
│ │ ├── TestoRunTestConfigurationEditor.kt
102+
│ │ ├── TestoTestRunnerSettingsValidator.kt
103+
│ │ ├── TestoTestMethodFinder.kt
104+
│ │ ├── TestoRunnerSettings.kt
105+
│ │ └── TestoDebugRunner.kt
106+
│ │
107+
│ └── runAnything/
108+
│ └── TestoRunAnythingProvider.kt
109+
110+
└── ui/ # UI components
111+
├── TestoIconProvider.kt
112+
├── TestoStackTraceConsoleFolding.kt
113+
└── PhpRunInheritorsListCellRenderer.kt
114+
115+
src/main/resources/
116+
├── META-INF/plugin.xml # Plugin descriptor (extensions, actions)
117+
├── fileTemplates/ # New file templates (Testo Test.php.ft)
118+
├── icons/ # SVG icons (light + dark variants)
119+
├── liveTemplates/Testo.xml # Live templates: `test`, `data`
120+
├── messages/TestoBundle.properties # i18n strings
121+
└── testo.dic # Spellchecker dictionary
122+
123+
src/test/ # Unit tests (JUnit 4 + BasePlatformTestCase)
124+
```
125+
126+
## Architecture
127+
128+
### Plugin Extension Points
129+
130+
The plugin registers extensions in `plugin.xml` under two namespaces:
131+
132+
- **`com.intellij`** — standard IntelliJ extensions: `fileType`, `runLineMarkerContributor`, `configurationType`, `runConfigurationProducer`, `programRunner`, `implicitUsageProvider`, `iconProvider`, `fileBasedIndex`, `console.folding`, `lang.inspectionSuppressor`, `testActionProvider`, live templates, etc.
133+
- **`com.jetbrains.php`** — PHP-specific: `testFrameworkType` (TestoFrameworkType), `composerConfigClient` (TestoComposerConfig).
134+
135+
### Required Plugin Dependencies
136+
137+
- `com.intellij.modules.platform` — IntelliJ Platform core
138+
- `com.jetbrains.php` — PHP language support (makes this plugin work in PhpStorm / IDEA Ultimate with PHP plugin)
139+
140+
### Testo PHP Framework — Supported Attributes
141+
142+
The plugin recognizes PHP attributes defined in `TestoClasses.kt`. Constants are grouped into arrays for reuse across the codebase:
143+
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` |
150+
151+
Other constants: `ASSERT` (`\Testo\Assert`), `EXPECT` (`\Testo\Expect`), `ASSERTION_EXCEPTION`.
152+
153+
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.
154+
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+
| data | `DATA_ATTRIBUTES` | Data providers, numbered together |
162+
| inline | `TEST_INLINE_ATTRIBUTES` | Inline test cases (`#[TestInline]`) |
163+
| bench | `BENCH_ATTRIBUTES` | Benchmark data (`#[Bench]`) |
164+
165+
`#[Test]` is **not numbered** — it is runnable (in `RUNNABLE_ATTRIBUTES`) but has no index. It runs the test with `--type=test`.
166+
167+
Example for a function `foo` with multiple attributes:
168+
```
169+
#[Test] → runnable, no index (--type=test)
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_ATTRIBUTES + BENCH_ATTRIBUTES + DATA_ATTRIBUTES`.
182+
183+
### Test Detection Logic (mixin.kt)
184+
185+
A PHP element is recognized as a Testo test when:
186+
- **Method:** public + name starts with `test`, OR has any `TEST_ATTRIBUTES`
187+
- **Function:** has any `TEST_ATTRIBUTES` (standalone test functions)
188+
- **Benchmark:** has any `BENCH_ATTRIBUTES`
189+
- **Class:** name ends with `Test` or `TestBase`, OR contains test/bench methods
190+
- **File:** filename matches test class pattern, OR contains test classes/functions/benchmarks
191+
192+
### Key Subsystems
193+
194+
1. **Run Configuration** (`tests/run/`) — creates and manages run/debug configurations for Testo tests. `TestoRunConfigurationProducer` is the largest file (~527 lines) handling context-based config creation for methods, classes, files, data providers, and datasets.
195+
196+
2. **Line Markers** (`TestoTestRunLineMarkerProvider`) — adds green play buttons in the gutter next to test methods, classes, and data providers.
197+
198+
3. **Data Provider Index** (`index/TestoDataProvidersIndex`) — file-based index that maps test methods to their data providers for quick lookup across the project.
199+
200+
4. **Code Generation** — "Create Test from Class" action and "Generate Test Method" action integrated into IDE menus.
201+
202+
5. **Stack Trace Navigation** (`TestoTestLocator`) — click-to-navigate from test output to source code.
203+
204+
## Constraints & Important Notes
205+
206+
- **Platform:** IntelliJ IDEA Ultimate or PhpStorm only (requires `com.jetbrains.php` plugin)
207+
- **Min IDE version:** 2024.3 (build 243+)
208+
- **Kotlin stdlib is NOT bundled** (`kotlin.stdlib.default.dependency = false`) — uses the one shipped with IntelliJ
209+
- **Gradle Configuration Cache** and **Build Cache** are enabled
210+
- **Code and comments language:** English
211+
- **Plugin description** is extracted from `README.md` between `<!-- Plugin description -->` markers during build
212+
- **Signing & publishing** require environment variables: `CERTIFICATE_CHAIN`, `PRIVATE_KEY`, `PRIVATE_KEY_PASSWORD`, `PUBLISH_TOKEN`
213+
214+
## CI/CD
215+
216+
- **build.yml** (on push to main / PRs): build → test (with Kover coverage → Codecov) → Qodana inspections → plugin verification → draft release
217+
- **release.yml** (on GitHub release): publish to JetBrains Marketplace, update changelog
218+
- **run-ui-tests.yml** (manual): UI tests on Ubuntu, Windows, macOS via robot-server
219+
220+
## Conventions
221+
222+
- All source code is in Kotlin
223+
- Package root: `com.github.xepozz.testo`
224+
- i18n strings go in `messages/TestoBundle.properties`, accessed via `TestoBundle`
225+
- Icons follow IntelliJ conventions: SVG with `_dark` suffix variant
226+
- New extension points must be registered in `plugin.xml`
227+
- Version follows SemVer; `pluginVersion` in `gradle.properties` is the single source of truth
Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,39 @@
11
package com.github.xepozz.testo
22

33
object TestoClasses {
4-
const val TEST_NEW = "\\Testo\\Attribute\\Test"
5-
const val TEST_OLD = "\\Testo\\Application\\Attribute\\Test"
6-
const val TEST_INLINE_OLD = "\\Testo\\Sample\\TestInline"
7-
const val TEST_INLINE_NEW = "\\Testo\\Inline\\TestInline"
4+
const val TEST = "\\Testo\\Test"
5+
const val TEST_INLINE = "\\Testo\\Inline\\TestInline"
86

9-
const val DATA_PROVIDER_OLD = "\\Testo\\Sample\\DataProvider"
10-
const val DATA_SET_OLD = "\\Testo\\Sample\\DataSet"
11-
const val DATA_PROVIDER_NEW = "\\Testo\\Data\\DataProvider"
12-
const val DATA_SET_NEW = "\\Testo\\Data\\DataSet"
7+
const val DATA_PROVIDER = "\\Testo\\Data\\DataProvider"
8+
const val DATA_SET = "\\Testo\\Data\\DataSet"
139
const val DATA_UNION = "\\Testo\\Data\\DataUnion"
1410
const val DATA_CROSS = "\\Testo\\Data\\DataCross"
1511
const val DATA_ZIP = "\\Testo\\Data\\DataZip"
1612

17-
const val BENCH_WITH = "\\Testo\\Bench\\BenchWith"
13+
const val BENCH = "\\Testo\\Bench"
14+
15+
const val APPLICATION_CONFIG = "\\Testo\\Application\\Config\\ApplicationConfig"
16+
const val SUITE_CONFIG = "\\Testo\\Application\\Config\\SuiteConfig"
1817

1918
const val ASSERT = "\\Testo\\Assert"
2019
const val ASSERTION_EXCEPTION = "\\Testo\\Assert\\State\\Assertion\\AssertionException"
2120
const val EXPECT = "\\Testo\\Expect"
2221

2322
val DATA_ATTRIBUTES = arrayOf(
24-
DATA_PROVIDER_OLD,
25-
DATA_PROVIDER_NEW,
26-
DATA_SET_OLD,
27-
DATA_SET_NEW,
23+
DATA_PROVIDER,
24+
DATA_SET,
2825
DATA_UNION,
2926
DATA_CROSS,
3027
DATA_ZIP,
3128
)
3229
val TEST_ATTRIBUTES = arrayOf(
33-
TEST_NEW,
34-
TEST_OLD,
30+
TEST,
31+
TEST_INLINE,
3532
)
3633
val TEST_INLINE_ATTRIBUTES = arrayOf(
37-
TEST_INLINE_OLD,
38-
TEST_INLINE_NEW,
34+
TEST_INLINE,
3935
)
4036
val BENCH_ATTRIBUTES = arrayOf(
41-
BENCH_WITH,
37+
BENCH,
4238
)
4339
}

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import com.jetbrains.php.lang.psi.PhpFile
88
import com.jetbrains.php.lang.psi.elements.Method
99
import com.jetbrains.php.lang.psi.elements.Function
1010
import com.jetbrains.php.lang.psi.elements.PhpAttributesOwner
11+
import com.jetbrains.php.lang.psi.elements.ClassReference
12+
import com.jetbrains.php.lang.psi.elements.NewExpression
1113
import com.jetbrains.php.lang.psi.elements.PhpClass
1214

1315
fun PsiElement.isTestoExecutable() = isTestoFunction() || isTestoMethod() || isTestoBench()
@@ -18,12 +20,12 @@ fun PsiElement.isTestoBench() = when(this) {
1820
}
1921

2022
fun PsiElement.isTestoFunction() = when(this) {
21-
is Function -> hasAnyAttribute(*TestoClasses.TEST_ATTRIBUTES, *TestoClasses.TEST_INLINE_ATTRIBUTES)
23+
is Function -> hasAnyAttribute(*TestoClasses.TEST_ATTRIBUTES)
2224
else -> false
2325
}
2426

2527
fun PsiElement.isTestoMethod() = when(this) {
26-
is Method -> (modifier.isPublic && name.startsWith("test")) || hasAnyAttribute(*TestoClasses.TEST_ATTRIBUTES, *TestoClasses.TEST_INLINE_ATTRIBUTES)
28+
is Method -> (modifier.isPublic && name.startsWith("test")) || hasAnyAttribute(*TestoClasses.TEST_ATTRIBUTES)
2729
else -> false
2830
}
2931

@@ -42,10 +44,13 @@ fun PsiElement.isTestoClass() = when (this) {
4244
}
4345

4446
fun PsiFile.isTestoFile() = when (this) {
45-
is PhpFile -> TestoTestDescriptor.isTestClassName(name.substringBeforeLast(".")) || (isTestoClassFile() || isTestoFunctionFile() || isTestBenchFile())
47+
is PhpFile -> TestoTestDescriptor.isTestClassName(name.substringBeforeLast(".")) || isTestoClassFile() || isTestoFunctionFile() || isTestBenchFile() || isTestoConfigFile()
4648
else -> false
4749
}
4850

51+
fun PhpFile.isTestoConfigFile() = PsiTreeUtil.findChildrenOfType(this, ClassReference::class.java)
52+
.any { it.parent is NewExpression && it.fqn == TestoClasses.APPLICATION_CONFIG }
53+
4954
fun PhpFile.isTestoClassFile() = PsiTreeUtil.findChildrenOfType(this, PhpClass::class.java)
5055
.any { it.isTestoClass() }
5156

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class TestoTestLocator(pathMapper: PhpPathMapper) :
5454
* - path/to/file.php::\Full\Qualified\ClassName::methodName
5555
* - path/to/file.php::\Full\Qualified\FunctionName
5656
*/
57-
override fun getLocationInfo(link: String): LocationInfo? {
57+
public override fun getLocationInfo(link: String): LocationInfo? {
5858
val locations = link.split("::").dropLastWhile { it.isEmpty() }
5959
// println("locations: $locations, link: $link")
6060

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.jetbrains.php.lang.lexer.PhpTokenTypes
2020
import com.jetbrains.php.lang.psi.PhpPsiUtil
2121
import com.jetbrains.php.lang.psi.elements.ClassReference
2222
import com.jetbrains.php.lang.psi.elements.Method
23+
import com.jetbrains.php.lang.psi.elements.NewExpression
2324
import com.jetbrains.php.lang.psi.elements.Function
2425
import com.jetbrains.php.lang.psi.elements.PhpAttribute
2526
import com.jetbrains.php.lang.psi.elements.PhpAttributesOwner
@@ -49,14 +50,22 @@ class TestoTestRunLineMarkerProvider : RunLineMarkerContributor() {
4950
val element = leaf.parent as? PhpPsiElement ?: return null
5051

5152
return when {
53+
element is ClassReference && element.parent is NewExpression && element.fqn == TestoClasses.APPLICATION_CONFIG -> {
54+
getLocationHint(element.containingFile)
55+
}
56+
57+
element is ClassReference && element.parent is NewExpression && element.fqn == TestoClasses.SUITE_CONFIG -> {
58+
getLocationHint(element.containingFile)
59+
}
60+
5261
element is ClassReference && element.parent is PhpAttribute -> {
5362
val attribute = element.parent as PhpAttribute
5463
if (attribute.fqn !in RUNNABLE_ATTRIBUTES) return null
5564

5665
val attributesOwner = attribute.owner as PhpAttributesOwner
5766
val index = PsiUtil.getAttributeOrder(attribute, attributesOwner)
58-
59-
getInlineTestLocationHint(attributesOwner, index)
67+
if (index < 0) getLocationInfo(attributesOwner)
68+
else getInlineTestLocationHint(attributesOwner, index)
6069
}
6170

6271
element is PhpNamedElement -> {
@@ -87,9 +96,9 @@ class TestoTestRunLineMarkerProvider : RunLineMarkerContributor() {
8796

8897
companion object Companion {
8998
val RUNNABLE_ATTRIBUTES = arrayOf(
90-
*TestoClasses.DATA_ATTRIBUTES,
91-
*TestoClasses.TEST_INLINE_ATTRIBUTES,
99+
*TestoClasses.TEST_ATTRIBUTES,
92100
*TestoClasses.BENCH_ATTRIBUTES,
101+
*TestoClasses.DATA_ATTRIBUTES,
93102
)
94103

95104
fun getLocationHint(element: Function) = when (element) {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import com.jetbrains.php.PhpTestFrameworkVersionDetector
77
object TestoVersionDetector : PhpTestFrameworkVersionDetector<String>() {
88
override fun getPresentableName() = TestoBundle.message("testo.local.run.display.name")
99

10-
override fun getVersionOptions() = arrayOf("--version", "--no-ansi")
10+
public override fun getVersionOptions() = arrayOf("--version", "--no-ansi")
1111

1212
public override fun parse(s: String): String {
1313
val version = s.substringAfter("Testo ")

0 commit comments

Comments
 (0)