Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
93eb223
Add CLAUDE.md with full project documentation
claude Mar 17, 2026
81b91a2
Merge remote-tracking branch 'origin/main' into claude/create-claude-…
claude Mar 17, 2026
c32af9a
Update CLAUDE.md to reflect grouped attribute arrays in TestoClasses
claude Mar 17, 2026
6971453
Add unit tests for core plugin components
claude Mar 17, 2026
93bb124
Add platform-level tests with BasePlatformTestCase and PHP PSI
claude Mar 17, 2026
d22509d
Refactor run configuration for flexible argument passing
claude Mar 17, 2026
9c1add6
Add #[Bench] attribute support and --type bench for benchmark runs
claude Mar 17, 2026
f7af254
Fix Bench attribute FQN to \Testo\Bench (root namespace)
claude Mar 17, 2026
c7dfd82
Replace selectedType with custom testoType field for bench type
claude Mar 17, 2026
42964a4
refactor: remove deprecated inline test attributes
xepozz Mar 17, 2026
5e46dfd
Add --type flag for all attribute types: test, inline, bench
claude Mar 17, 2026
b6afcb0
Only set --type when running from attribute, not from function/method
claude Mar 17, 2026
0425e04
Add tests for testoType: test, inline, bench and default empty
claude Mar 17, 2026
92a1b54
Add Run gutter icon for ApplicationConfig files (#39)
claude Mar 17, 2026
809910f
Fix: gutter icon only on `new ApplicationConfig`, not `use` statement
claude Mar 17, 2026
fe180dc
feat: add bench method live template
xepozz Mar 17, 2026
4ca227a
refactor: simplify `Testo` namespace usage in template
xepozz Mar 17, 2026
5912961
Add gutter run icon for SuiteConfig with --suite flag
claude Mar 17, 2026
3346f72
Merge remote-tracking branch 'origin/claude/create-claude-md-docs-TbE…
xepozz Mar 17, 2026
4726b88
refactor: simplify suite name extraction in `TestoRunConfigurationPro…
xepozz Mar 17, 2026
af0c0fc
fix: number attributes within their group, not globally
claude Mar 18, 2026
cdefecd
fix: remove TEST_INLINE_ATTRIBUTES, group by FQN instead
claude Mar 18, 2026
5ff34b8
fix: restore TEST_ATTRIBUTES in RUNNABLE, add TEST_INLINE_ATTRIBUTES …
claude Mar 18, 2026
9f48830
fix: put Test attribute in same group as DATA_ATTRIBUTES (TEST_DATA_A…
claude Mar 18, 2026
ea9f51b
fix: Test attribute is runnable but not numbered
claude Mar 18, 2026
3620075
fix: make getLocationInfo and getVersionOptions public for tests
claude Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
227 changes: 227 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# CLAUDE.md

## Project Overview

IntelliJ IDEA / PhpStorm plugin for **Testo** — a PHP testing framework.
Provides full IDE integration: test discovery, run configurations, code generation, inspections, and navigation.

- **Plugin ID:** `com.github.xepozz.testo`
- **Plugin Name:** Testo PHP
- **Author:** Dmitrii Derepko (@xepozz)
- **Repository:** https://github.com/j-plugins/testo-plugin
- **Marketplace:** JetBrains Marketplace

## Tech Stack

| Component | Version / Value |
|----------------------|---------------------------|
| Language | Kotlin 2.3.0 |
| JVM Toolchain | Java 21 |
| IntelliJ Platform | 2024.3.4 (IU — Ultimate) |
| Min platform build | 243 (2024.3.x) |
| Build system | Gradle 9.3.0 |
| IntelliJ Plugin SDK | `org.jetbrains.intellij.platform` 2.11.0 |
| Changelog plugin | `org.jetbrains.changelog` 2.5.0 |
| Code quality | Qodana 2025.3.1 |
| Coverage | Kover 0.9.4 |
| Test framework | JUnit 4.13.2, OpenTest4J 1.3.0 |

## Build & Run Commands

```bash
# Build the plugin
./gradlew buildPlugin

# Run tests
./gradlew check

# Run IDE with plugin loaded (for manual testing)
./gradlew runIde

# Verify plugin compatibility
./gradlew verifyPlugin

# Run UI tests (requires robot-server)
./gradlew runIdeForUiTests
```

## Project Structure

```
src/main/kotlin/com/github/xepozz/testo/
├── TestoBundle.kt # i18n message bundle
├── TestoClasses.kt # FQN constants for Testo PHP classes/attributes
├── TestoContext.kt # Live template context
├── TestoIcons.kt # Icon definitions
├── TestoUtil.kt # Project-level Testo availability check
├── TestoComposerConfig.kt # Composer package detection
├── mixin.kt # PSI extension functions (isTestoMethod, isTestoClass, etc.)
├── PsiUtil.kt # General PSI utilities
├── ExitStatementsVisitor.kt # PHP exit statement analysis
├── SpellcheckingDictionaryProvider.kt
├── actions/ # Code generation actions
│ ├── TestoGenerateTestMethodAction.kt
│ └── TestoGenerateMethodActionBase.kt
├── index/ # File-based index for data providers
│ ├── TestoDataProvidersIndex.kt
│ └── TestoDataProviderUtils.kt
├── references/ # Reference resolution & implicit usage
│ └── TestFunctionImplicitUsageProvider.kt
├── tests/ # Core test framework integration
│ ├── TestoFrameworkType.kt # PhpTestFrameworkType implementation
│ ├── TestoTestDescriptor.kt # Test class/method discovery
│ ├── TestoTestLocator.kt # Stack trace → source navigation
│ ├── TestoTestRunLineMarkerProvider.kt # Gutter run icons
│ ├── TestoStackTraceParser.kt # Test output parsing
│ ├── TestoConsoleProperties.kt # Console configuration
│ ├── TestoVersionDetector.kt # Testo version detection
│ │
│ ├── actions/ # Test-specific actions
│ │ ├── TestoNewTestFromClassAction.kt
│ │ ├── TestoTestActionProvider.kt
│ │ ├── TestoRerunFailedTestsAction.kt
│ │ └── TestoRunCommandAction.kt
│ │
│ ├── inspections/
│ │ └── TestoInspectionSuppressor.kt
│ │
│ ├── overrides/ # UI customization
│ │
│ ├── run/ # Run configuration subsystem
│ │ ├── TestoRunConfigurationType.kt
│ │ ├── TestoRunConfiguration.kt
│ │ ├── TestoRunConfigurationFactory.kt
│ │ ├── TestoRunConfigurationProducer.kt # Context-based config creation
│ │ ├── TestoRunConfigurationHandler.kt
│ │ ├── TestoRunConfigurationSettings.kt
│ │ ├── TestoRunTestConfigurationEditor.kt
│ │ ├── TestoTestRunnerSettingsValidator.kt
│ │ ├── TestoTestMethodFinder.kt
│ │ ├── TestoRunnerSettings.kt
│ │ └── TestoDebugRunner.kt
│ │
│ └── runAnything/
│ └── TestoRunAnythingProvider.kt
└── ui/ # UI components
├── TestoIconProvider.kt
├── TestoStackTraceConsoleFolding.kt
└── PhpRunInheritorsListCellRenderer.kt

src/main/resources/
├── META-INF/plugin.xml # Plugin descriptor (extensions, actions)
├── fileTemplates/ # New file templates (Testo Test.php.ft)
├── icons/ # SVG icons (light + dark variants)
├── liveTemplates/Testo.xml # Live templates: `test`, `data`
├── messages/TestoBundle.properties # i18n strings
└── testo.dic # Spellchecker dictionary

src/test/ # Unit tests (JUnit 4 + BasePlatformTestCase)
```

## Architecture

### Plugin Extension Points

The plugin registers extensions in `plugin.xml` under two namespaces:

- **`com.intellij`** — standard IntelliJ extensions: `fileType`, `runLineMarkerContributor`, `configurationType`, `runConfigurationProducer`, `programRunner`, `implicitUsageProvider`, `iconProvider`, `fileBasedIndex`, `console.folding`, `lang.inspectionSuppressor`, `testActionProvider`, live templates, etc.
- **`com.jetbrains.php`** — PHP-specific: `testFrameworkType` (TestoFrameworkType), `composerConfigClient` (TestoComposerConfig).

### Required Plugin Dependencies

- `com.intellij.modules.platform` — IntelliJ Platform core
- `com.jetbrains.php` — PHP language support (makes this plugin work in PhpStorm / IDEA Ultimate with PHP plugin)

### Testo PHP Framework — Supported Attributes

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

| Group (array) | Attributes (FQN) |
|----------------------------|-----------------------------------------------------------------------------------|
| `TEST_ATTRIBUTES` | `\Testo\Test`, `\Testo\Inline\TestInline` |
| `TEST_INLINE_ATTRIBUTES` | `\Testo\Inline\TestInline` |
| `DATA_ATTRIBUTES` | `\Testo\Data\DataProvider`, `\Testo\Data\DataSet`, `\Testo\Data\DataUnion`, `\Testo\Data\DataCross`, `\Testo\Data\DataZip` |
| `BENCH_ATTRIBUTES` | `\Testo\Bench` |

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

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.

### Attribute Group Numbering

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`:

| Group | Source array | Used for |
|-------------------|---------------------------|-----------------------------------------------|
| data | `DATA_ATTRIBUTES` | Data providers, numbered together |
| inline | `TEST_INLINE_ATTRIBUTES` | Inline test cases (`#[TestInline]`) |
| bench | `BENCH_ATTRIBUTES` | Benchmark data (`#[Bench]`) |

`#[Test]` is **not numbered** — it is runnable (in `RUNNABLE_ATTRIBUTES`) but has no index. It runs the test with `--type=test`.

Example for a function `foo` with multiple attributes:
```
#[Test] → runnable, no index (--type=test)
#[DataProvider(...)] → type=test, foo:0
#[DataSet([...])] → type=test, foo:1
#[DataZip(...)] → type=test, foo:2
#[DataCross(...)] → type=test, foo:3
#[TestInline(...)] → type=inline, foo:0
#[TestInline(...)] → type=inline, foo:1
#[TestInline(...)] → type=inline, foo:2
#[Bench(...)] → type=bench, foo:0
#[Bench(...)] → type=bench, foo:1
```

`RUNNABLE_ATTRIBUTES` (used for gutter line markers) contains `TEST_ATTRIBUTES + BENCH_ATTRIBUTES + DATA_ATTRIBUTES`.

### Test Detection Logic (mixin.kt)

A PHP element is recognized as a Testo test when:
- **Method:** public + name starts with `test`, OR has any `TEST_ATTRIBUTES`
- **Function:** has any `TEST_ATTRIBUTES` (standalone test functions)
- **Benchmark:** has any `BENCH_ATTRIBUTES`
- **Class:** name ends with `Test` or `TestBase`, OR contains test/bench methods
- **File:** filename matches test class pattern, OR contains test classes/functions/benchmarks

### Key Subsystems

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.

2. **Line Markers** (`TestoTestRunLineMarkerProvider`) — adds green play buttons in the gutter next to test methods, classes, and data providers.

3. **Data Provider Index** (`index/TestoDataProvidersIndex`) — file-based index that maps test methods to their data providers for quick lookup across the project.

4. **Code Generation** — "Create Test from Class" action and "Generate Test Method" action integrated into IDE menus.

5. **Stack Trace Navigation** (`TestoTestLocator`) — click-to-navigate from test output to source code.

## Constraints & Important Notes

- **Platform:** IntelliJ IDEA Ultimate or PhpStorm only (requires `com.jetbrains.php` plugin)
- **Min IDE version:** 2024.3 (build 243+)
- **Kotlin stdlib is NOT bundled** (`kotlin.stdlib.default.dependency = false`) — uses the one shipped with IntelliJ
- **Gradle Configuration Cache** and **Build Cache** are enabled
- **Code and comments language:** English
- **Plugin description** is extracted from `README.md` between `<!-- Plugin description -->` markers during build
- **Signing & publishing** require environment variables: `CERTIFICATE_CHAIN`, `PRIVATE_KEY`, `PRIVATE_KEY_PASSWORD`, `PUBLISH_TOKEN`

## CI/CD

- **build.yml** (on push to main / PRs): build → test (with Kover coverage → Codecov) → Qodana inspections → plugin verification → draft release
- **release.yml** (on GitHub release): publish to JetBrains Marketplace, update changelog
- **run-ui-tests.yml** (manual): UI tests on Ubuntu, Windows, macOS via robot-server

## Conventions

- All source code is in Kotlin
- Package root: `com.github.xepozz.testo`
- i18n strings go in `messages/TestoBundle.properties`, accessed via `TestoBundle`
- Icons follow IntelliJ conventions: SVG with `_dark` suffix variant
- New extension points must be registered in `plugin.xml`
- Version follows SemVer; `pluginVersion` in `gradle.properties` is the single source of truth
32 changes: 14 additions & 18 deletions src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt
Original file line number Diff line number Diff line change
@@ -1,43 +1,39 @@
package com.github.xepozz.testo

object TestoClasses {
const val TEST_NEW = "\\Testo\\Attribute\\Test"
const val TEST_OLD = "\\Testo\\Application\\Attribute\\Test"
const val TEST_INLINE_OLD = "\\Testo\\Sample\\TestInline"
const val TEST_INLINE_NEW = "\\Testo\\Inline\\TestInline"
const val TEST = "\\Testo\\Test"
const val TEST_INLINE = "\\Testo\\Inline\\TestInline"

const val DATA_PROVIDER_OLD = "\\Testo\\Sample\\DataProvider"
const val DATA_SET_OLD = "\\Testo\\Sample\\DataSet"
const val DATA_PROVIDER_NEW = "\\Testo\\Data\\DataProvider"
const val DATA_SET_NEW = "\\Testo\\Data\\DataSet"
const val DATA_PROVIDER = "\\Testo\\Data\\DataProvider"
const val DATA_SET = "\\Testo\\Data\\DataSet"
const val DATA_UNION = "\\Testo\\Data\\DataUnion"
const val DATA_CROSS = "\\Testo\\Data\\DataCross"
const val DATA_ZIP = "\\Testo\\Data\\DataZip"

const val BENCH_WITH = "\\Testo\\Bench\\BenchWith"
const val BENCH = "\\Testo\\Bench"

const val APPLICATION_CONFIG = "\\Testo\\Application\\Config\\ApplicationConfig"
const val SUITE_CONFIG = "\\Testo\\Application\\Config\\SuiteConfig"

const val ASSERT = "\\Testo\\Assert"
const val ASSERTION_EXCEPTION = "\\Testo\\Assert\\State\\Assertion\\AssertionException"
const val EXPECT = "\\Testo\\Expect"

val DATA_ATTRIBUTES = arrayOf(
DATA_PROVIDER_OLD,
DATA_PROVIDER_NEW,
DATA_SET_OLD,
DATA_SET_NEW,
DATA_PROVIDER,
DATA_SET,
DATA_UNION,
DATA_CROSS,
DATA_ZIP,
)
val TEST_ATTRIBUTES = arrayOf(
TEST_NEW,
TEST_OLD,
TEST,
TEST_INLINE,
)
val TEST_INLINE_ATTRIBUTES = arrayOf(
TEST_INLINE_OLD,
TEST_INLINE_NEW,
TEST_INLINE,
)
val BENCH_ATTRIBUTES = arrayOf(
BENCH_WITH,
BENCH,
)
}
11 changes: 8 additions & 3 deletions src/main/kotlin/com/github/xepozz/testo/mixin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.jetbrains.php.lang.psi.PhpFile
import com.jetbrains.php.lang.psi.elements.Method
import com.jetbrains.php.lang.psi.elements.Function
import com.jetbrains.php.lang.psi.elements.PhpAttributesOwner
import com.jetbrains.php.lang.psi.elements.ClassReference
import com.jetbrains.php.lang.psi.elements.NewExpression
import com.jetbrains.php.lang.psi.elements.PhpClass

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

fun PsiElement.isTestoFunction() = when(this) {
is Function -> hasAnyAttribute(*TestoClasses.TEST_ATTRIBUTES, *TestoClasses.TEST_INLINE_ATTRIBUTES)
is Function -> hasAnyAttribute(*TestoClasses.TEST_ATTRIBUTES)
else -> false
}

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

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

fun PsiFile.isTestoFile() = when (this) {
is PhpFile -> TestoTestDescriptor.isTestClassName(name.substringBeforeLast(".")) || (isTestoClassFile() || isTestoFunctionFile() || isTestBenchFile())
is PhpFile -> TestoTestDescriptor.isTestClassName(name.substringBeforeLast(".")) || isTestoClassFile() || isTestoFunctionFile() || isTestBenchFile() || isTestoConfigFile()
else -> false
}

fun PhpFile.isTestoConfigFile() = PsiTreeUtil.findChildrenOfType(this, ClassReference::class.java)
.any { it.parent is NewExpression && it.fqn == TestoClasses.APPLICATION_CONFIG }

fun PhpFile.isTestoClassFile() = PsiTreeUtil.findChildrenOfType(this, PhpClass::class.java)
.any { it.isTestoClass() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class TestoTestLocator(pathMapper: PhpPathMapper) :
* - path/to/file.php::\Full\Qualified\ClassName::methodName
* - path/to/file.php::\Full\Qualified\FunctionName
*/
override fun getLocationInfo(link: String): LocationInfo? {
public override fun getLocationInfo(link: String): LocationInfo? {
val locations = link.split("::").dropLastWhile { it.isEmpty() }
// println("locations: $locations, link: $link")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.jetbrains.php.lang.lexer.PhpTokenTypes
import com.jetbrains.php.lang.psi.PhpPsiUtil
import com.jetbrains.php.lang.psi.elements.ClassReference
import com.jetbrains.php.lang.psi.elements.Method
import com.jetbrains.php.lang.psi.elements.NewExpression
import com.jetbrains.php.lang.psi.elements.Function
import com.jetbrains.php.lang.psi.elements.PhpAttribute
import com.jetbrains.php.lang.psi.elements.PhpAttributesOwner
Expand Down Expand Up @@ -49,14 +50,22 @@ class TestoTestRunLineMarkerProvider : RunLineMarkerContributor() {
val element = leaf.parent as? PhpPsiElement ?: return null

return when {
element is ClassReference && element.parent is NewExpression && element.fqn == TestoClasses.APPLICATION_CONFIG -> {
getLocationHint(element.containingFile)
}

element is ClassReference && element.parent is NewExpression && element.fqn == TestoClasses.SUITE_CONFIG -> {
getLocationHint(element.containingFile)
}

element is ClassReference && element.parent is PhpAttribute -> {
val attribute = element.parent as PhpAttribute
if (attribute.fqn !in RUNNABLE_ATTRIBUTES) return null

val attributesOwner = attribute.owner as PhpAttributesOwner
val index = PsiUtil.getAttributeOrder(attribute, attributesOwner)

getInlineTestLocationHint(attributesOwner, index)
if (index < 0) getLocationInfo(attributesOwner)
else getInlineTestLocationHint(attributesOwner, index)
}

element is PhpNamedElement -> {
Expand Down Expand Up @@ -87,9 +96,9 @@ class TestoTestRunLineMarkerProvider : RunLineMarkerContributor() {

companion object Companion {
val RUNNABLE_ATTRIBUTES = arrayOf(
*TestoClasses.DATA_ATTRIBUTES,
*TestoClasses.TEST_INLINE_ATTRIBUTES,
*TestoClasses.TEST_ATTRIBUTES,
*TestoClasses.BENCH_ATTRIBUTES,
*TestoClasses.DATA_ATTRIBUTES,
)

fun getLocationHint(element: Function) = when (element) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import com.jetbrains.php.PhpTestFrameworkVersionDetector
object TestoVersionDetector : PhpTestFrameworkVersionDetector<String>() {
override fun getPresentableName() = TestoBundle.message("testo.local.run.display.name")

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

public override fun parse(s: String): String {
val version = s.substringAfter("Testo ")
Expand Down
Loading
Loading