From 93eb2237ea27ba35bac772cd0fc0a63cc928d2d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 08:40:10 +0000 Subject: [PATCH 01/24] Add CLAUDE.md with full project documentation Describes architecture, tech stack, build commands, project structure, Testo PHP framework integration details, constraints, and CI/CD setup. https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- CLAUDE.md | 200 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..42fbe4a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,200 @@ +# 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 Classes + +The plugin recognizes these PHP namespaces/attributes (defined in `TestoClasses.kt`): + +| Category | Old namespace (`\Testo\Sample\*`, `\Testo\Attribute\*`) | New namespace (`\Testo\Application\*`, `\Testo\Data\*`, etc.) | +|-----------------|----------------------------------------------------------|---------------------------------------------------------------| +| Test attribute | `\Testo\Attribute\Test` | `\Testo\Application\Attribute\Test` | +| Inline test | `\Testo\Sample\TestInline` | `\Testo\Inline\TestInline` | +| Data provider | `\Testo\Sample\DataProvider` | `\Testo\Data\DataProvider` | +| Data set | `\Testo\Sample\DataSet` | `\Testo\Data\DataSet` | +| Data union | — | `\Testo\Data\DataUnion` | +| Data cross | — | `\Testo\Data\DataCross` | +| Data zip | — | `\Testo\Data\DataZip` | +| Benchmark | — | `\Testo\Bench\BenchWith` | +| Assertions | — | `\Testo\Assert`, `\Testo\Expect` | + +### Test Detection Logic (mixin.kt) + +A PHP element is recognized as a Testo test when: +- **Method:** public + name starts with `test`, OR has `#[Test]` / `#[TestInline]` attribute +- **Function:** has `#[Test]` / `#[TestInline]` attribute (standalone test functions) +- **Benchmark:** has `#[BenchWith]` attribute +- **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 `` 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 From c32af9a4a66dae7ca8a373a029200ff73a1efb18 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 08:50:48 +0000 Subject: [PATCH 02/24] Update CLAUDE.md to reflect grouped attribute arrays in TestoClasses Matches refactor from 75a5fcc: attributes are now organized into TEST_ATTRIBUTES, TEST_INLINE_ATTRIBUTES, DATA_ATTRIBUTES, BENCH_ATTRIBUTES arrays and spread across the codebase instead of being duplicated. https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- CLAUDE.md | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 42fbe4a..95f80c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,28 +137,27 @@ The plugin registers extensions in `plugin.xml` under two namespaces: - `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 Classes - -The plugin recognizes these PHP namespaces/attributes (defined in `TestoClasses.kt`): - -| Category | Old namespace (`\Testo\Sample\*`, `\Testo\Attribute\*`) | New namespace (`\Testo\Application\*`, `\Testo\Data\*`, etc.) | -|-----------------|----------------------------------------------------------|---------------------------------------------------------------| -| Test attribute | `\Testo\Attribute\Test` | `\Testo\Application\Attribute\Test` | -| Inline test | `\Testo\Sample\TestInline` | `\Testo\Inline\TestInline` | -| Data provider | `\Testo\Sample\DataProvider` | `\Testo\Data\DataProvider` | -| Data set | `\Testo\Sample\DataSet` | `\Testo\Data\DataSet` | -| Data union | — | `\Testo\Data\DataUnion` | -| Data cross | — | `\Testo\Data\DataCross` | -| Data zip | — | `\Testo\Data\DataZip` | -| Benchmark | — | `\Testo\Bench\BenchWith` | -| Assertions | — | `\Testo\Assert`, `\Testo\Expect` | +### 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\Attribute\Test` (new), `\Testo\Application\Attribute\Test` (old) | +| `TEST_INLINE_ATTRIBUTES` | `\Testo\Sample\TestInline` (old), `\Testo\Inline\TestInline` (new) | +| `DATA_ATTRIBUTES` | `\Testo\Sample\DataProvider`, `\Testo\Data\DataProvider`, `\Testo\Sample\DataSet`, `\Testo\Data\DataSet`, `\Testo\Data\DataUnion`, `\Testo\Data\DataCross`, `\Testo\Data\DataZip` | +| `BENCH_ATTRIBUTES` | `\Testo\Bench\BenchWith` | + +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. ### Test Detection Logic (mixin.kt) A PHP element is recognized as a Testo test when: -- **Method:** public + name starts with `test`, OR has `#[Test]` / `#[TestInline]` attribute -- **Function:** has `#[Test]` / `#[TestInline]` attribute (standalone test functions) -- **Benchmark:** has `#[BenchWith]` attribute +- **Method:** public + name starts with `test`, OR has any `TEST_ATTRIBUTES` / `TEST_INLINE_ATTRIBUTES` +- **Function:** has any `TEST_ATTRIBUTES` / `TEST_INLINE_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 From 6971453866eceb3c0149bb1064af647e77f366c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 09:00:09 +0000 Subject: [PATCH 03/24] Add unit tests for core plugin components Tests cover: - TestoClasses: attribute arrays content, no overlaps, constants - TestoTestDescriptor: isTestClassName logic - TestoVersionDetector: version parsing, error handling - TestoFrameworkType: constants, ID, composer packages - TestoRunnerSettings: defaults, custom values, fromPhpTestRunnerSettings - TestoRunConfigurationHandler: config file option, singleton - MixinExtensions: takeWhileInclusive for sequences and collections - LineMarkerCompanion: RUNNABLE_ATTRIBUTES composition - PsiUtil: MEANINGFUL_ATTRIBUTES composition, no duplicates https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- .../xepozz/testo/MixinExtensionsTest.kt | 51 +++++++++++++++ .../com/github/xepozz/testo/PsiUtilTest.kt | 37 +++++++++++ .../github/xepozz/testo/TestoClassesTest.kt | 64 +++++++++++++++++++ .../xepozz/testo/TestoFrameworkTypeTest.kt | 28 ++++++++ .../testo/TestoLineMarkerCompanionTest.kt | 42 ++++++++++++ .../testo/TestoRunConfigurationHandlerTest.kt | 15 +++++ .../xepozz/testo/TestoRunnerSettingsTest.kt | 51 +++++++++++++++ .../xepozz/testo/TestoTestDescriptorTest.kt | 30 +++++++++ .../xepozz/testo/TestoVersionDetectorTest.kt | 37 +++++++++++ 9 files changed, 355 insertions(+) create mode 100644 src/test/kotlin/com/github/xepozz/testo/MixinExtensionsTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/PsiUtilTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/TestoFrameworkTypeTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/TestoTestDescriptorTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/TestoVersionDetectorTest.kt diff --git a/src/test/kotlin/com/github/xepozz/testo/MixinExtensionsTest.kt b/src/test/kotlin/com/github/xepozz/testo/MixinExtensionsTest.kt new file mode 100644 index 0000000..52b5f37 --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/MixinExtensionsTest.kt @@ -0,0 +1,51 @@ +package com.github.xepozz.testo + +import junit.framework.TestCase + +class MixinExtensionsTest : TestCase() { + + fun testTakeWhileInclusive_sequence_stopsAfterFalse() { + val result = sequenceOf(1, 2, 3, 4, 5).takeWhileInclusive { it < 3 }.toList() + assertEquals(listOf(1, 2, 3), result) + } + + fun testTakeWhileInclusive_sequence_allTrue() { + val result = sequenceOf(1, 2, 3).takeWhileInclusive { it < 10 }.toList() + assertEquals(listOf(1, 2, 3), result) + } + + fun testTakeWhileInclusive_sequence_firstFalse() { + val result = sequenceOf(5, 1, 2).takeWhileInclusive { it < 3 }.toList() + assertEquals(listOf(5), result) + } + + fun testTakeWhileInclusive_sequence_empty() { + val result = emptySequence().takeWhileInclusive { it < 3 }.toList() + assertEquals(emptyList(), result) + } + + fun testTakeWhileInclusive_collection_stopsAfterFalse() { + val result = listOf(1, 2, 3, 4, 5).takeWhileInclusive { it < 3 } + assertEquals(listOf(1, 2, 3), result.toList()) + } + + fun testTakeWhileInclusive_collection_allTrue() { + val result = listOf(1, 2).takeWhileInclusive { it < 10 } + assertEquals(listOf(1, 2), result.toList()) + } + + fun testTakeWhileInclusive_collection_empty() { + val result = emptyList().takeWhileInclusive { it < 3 } + assertTrue(result.isEmpty()) + } + + fun testTakeWhileInclusive_collection_singleElement_true() { + val result = listOf(1).takeWhileInclusive { it < 3 } + assertEquals(listOf(1), result.toList()) + } + + fun testTakeWhileInclusive_collection_singleElement_false() { + val result = listOf(10).takeWhileInclusive { it < 3 } + assertEquals(listOf(10), result.toList()) + } +} diff --git a/src/test/kotlin/com/github/xepozz/testo/PsiUtilTest.kt b/src/test/kotlin/com/github/xepozz/testo/PsiUtilTest.kt new file mode 100644 index 0000000..0ee9e93 --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/PsiUtilTest.kt @@ -0,0 +1,37 @@ +package com.github.xepozz.testo + +import com.github.xepozz.testo.util.PsiUtil +import junit.framework.TestCase + +class PsiUtilTest : TestCase() { + + fun testMeaningfulAttributes_containsAllGroups() { + val meaningful = PsiUtil.MEANINGFUL_ATTRIBUTES.toSet() + + for (attr in TestoClasses.DATA_ATTRIBUTES) { + assertTrue("Missing data attribute: $attr", meaningful.contains(attr)) + } + for (attr in TestoClasses.TEST_ATTRIBUTES) { + assertTrue("Missing test attribute: $attr", meaningful.contains(attr)) + } + for (attr in TestoClasses.TEST_INLINE_ATTRIBUTES) { + assertTrue("Missing inline test attribute: $attr", meaningful.contains(attr)) + } + for (attr in TestoClasses.BENCH_ATTRIBUTES) { + assertTrue("Missing bench attribute: $attr", meaningful.contains(attr)) + } + } + + fun testMeaningfulAttributes_totalCount() { + val expected = TestoClasses.DATA_ATTRIBUTES.size + + TestoClasses.TEST_ATTRIBUTES.size + + TestoClasses.TEST_INLINE_ATTRIBUTES.size + + TestoClasses.BENCH_ATTRIBUTES.size + assertEquals(expected, PsiUtil.MEANINGFUL_ATTRIBUTES.size) + } + + fun testMeaningfulAttributes_noDuplicates() { + val attrs = PsiUtil.MEANINGFUL_ATTRIBUTES + assertEquals(attrs.size, attrs.toSet().size) + } +} diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt new file mode 100644 index 0000000..5f62ade --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt @@ -0,0 +1,64 @@ +package com.github.xepozz.testo + +import junit.framework.TestCase + +class TestoClassesTest : TestCase() { + + fun testTestAttributes_containsExpectedValues() { + val attrs = TestoClasses.TEST_ATTRIBUTES + assertEquals(2, attrs.size) + assertTrue(attrs.contains("\\Testo\\Attribute\\Test")) + assertTrue(attrs.contains("\\Testo\\Application\\Attribute\\Test")) + } + + fun testTestInlineAttributes_containsExpectedValues() { + val attrs = TestoClasses.TEST_INLINE_ATTRIBUTES + assertEquals(2, attrs.size) + assertTrue(attrs.contains("\\Testo\\Sample\\TestInline")) + assertTrue(attrs.contains("\\Testo\\Inline\\TestInline")) + } + + fun testDataAttributes_containsAllDataTypes() { + val attrs = TestoClasses.DATA_ATTRIBUTES + assertEquals(7, attrs.size) + assertTrue(attrs.contains("\\Testo\\Sample\\DataProvider")) + assertTrue(attrs.contains("\\Testo\\Data\\DataProvider")) + assertTrue(attrs.contains("\\Testo\\Sample\\DataSet")) + assertTrue(attrs.contains("\\Testo\\Data\\DataSet")) + assertTrue(attrs.contains("\\Testo\\Data\\DataUnion")) + assertTrue(attrs.contains("\\Testo\\Data\\DataCross")) + assertTrue(attrs.contains("\\Testo\\Data\\DataZip")) + } + + fun testBenchAttributes_containsExpectedValues() { + val attrs = TestoClasses.BENCH_ATTRIBUTES + assertEquals(1, attrs.size) + assertTrue(attrs.contains("\\Testo\\Bench\\BenchWith")) + } + + fun testConstants_assertionException() { + assertEquals("\\Testo\\Assert\\State\\Assertion\\AssertionException", TestoClasses.ASSERTION_EXCEPTION) + } + + fun testConstants_assert() { + assertEquals("\\Testo\\Assert", TestoClasses.ASSERT) + } + + fun testConstants_expect() { + assertEquals("\\Testo\\Expect", TestoClasses.EXPECT) + } + + fun testDataAttributes_noOverlapWithTestAttributes() { + val dataSet = TestoClasses.DATA_ATTRIBUTES.toSet() + val testSet = TestoClasses.TEST_ATTRIBUTES.toSet() + val inlineSet = TestoClasses.TEST_INLINE_ATTRIBUTES.toSet() + val benchSet = TestoClasses.BENCH_ATTRIBUTES.toSet() + + assertTrue(dataSet.intersect(testSet).isEmpty()) + assertTrue(dataSet.intersect(inlineSet).isEmpty()) + assertTrue(dataSet.intersect(benchSet).isEmpty()) + assertTrue(testSet.intersect(inlineSet).isEmpty()) + assertTrue(testSet.intersect(benchSet).isEmpty()) + assertTrue(inlineSet.intersect(benchSet).isEmpty()) + } +} diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoFrameworkTypeTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoFrameworkTypeTest.kt new file mode 100644 index 0000000..5abef0d --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/TestoFrameworkTypeTest.kt @@ -0,0 +1,28 @@ +package com.github.xepozz.testo + +import com.github.xepozz.testo.tests.TestoFrameworkType +import junit.framework.TestCase + +class TestoFrameworkTypeTest : TestCase() { + + fun testConstants() { + assertEquals("Testo", TestoFrameworkType.ID) + assertEquals("php_qn", TestoFrameworkType.SCHEMA) + } + + fun testGetID() { + val type = TestoFrameworkType() + assertEquals("Testo", type.id) + } + + fun testComposerPackageNames() { + val type = TestoFrameworkType() + val packages = type.composerPackageNames + assertTrue(packages.contains("php")) + } + + fun testGetDescriptor() { + val type = TestoFrameworkType() + assertNotNull(type.descriptor) + } +} diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt new file mode 100644 index 0000000..dddc217 --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt @@ -0,0 +1,42 @@ +package com.github.xepozz.testo + +import com.github.xepozz.testo.tests.TestoTestRunLineMarkerProvider +import junit.framework.TestCase + +class TestoLineMarkerCompanionTest : TestCase() { + + fun testRunnableAttributes_containsAllDataAttributes() { + val runnable = TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.toSet() + for (attr in TestoClasses.DATA_ATTRIBUTES) { + assertTrue("Missing data attribute: $attr", runnable.contains(attr)) + } + } + + fun testRunnableAttributes_containsInlineTestAttributes() { + val runnable = TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.toSet() + for (attr in TestoClasses.TEST_INLINE_ATTRIBUTES) { + assertTrue("Missing inline test attribute: $attr", runnable.contains(attr)) + } + } + + fun testRunnableAttributes_containsBenchAttributes() { + val runnable = TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.toSet() + for (attr in TestoClasses.BENCH_ATTRIBUTES) { + assertTrue("Missing bench attribute: $attr", runnable.contains(attr)) + } + } + + fun testRunnableAttributes_doesNotContainPlainTestAttributes() { + val runnable = TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.toSet() + for (attr in TestoClasses.TEST_ATTRIBUTES) { + assertFalse("Should not contain plain test attribute: $attr", runnable.contains(attr)) + } + } + + fun testRunnableAttributes_totalCount() { + val expected = TestoClasses.DATA_ATTRIBUTES.size + + TestoClasses.TEST_INLINE_ATTRIBUTES.size + + TestoClasses.BENCH_ATTRIBUTES.size + assertEquals(expected, TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.size) + } +} diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt new file mode 100644 index 0000000..75be63e --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt @@ -0,0 +1,15 @@ +package com.github.xepozz.testo + +import com.github.xepozz.testo.tests.run.TestoRunConfigurationHandler +import junit.framework.TestCase + +class TestoRunConfigurationHandlerTest : TestCase() { + + fun testGetConfigFileOption() { + assertEquals("--config", TestoRunConfigurationHandler.INSTANCE.configFileOption) + } + + fun testSingletonInstance() { + assertSame(TestoRunConfigurationHandler.INSTANCE, TestoRunConfigurationHandler.INSTANCE) + } +} diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt new file mode 100644 index 0000000..79c393b --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt @@ -0,0 +1,51 @@ +package com.github.xepozz.testo + +import com.github.xepozz.testo.tests.run.TestoRunnerSettings +import com.jetbrains.php.testFramework.run.PhpTestRunnerSettings +import junit.framework.TestCase + +class TestoRunnerSettingsTest : TestCase() { + + fun testDefaults() { + val settings = TestoRunnerSettings() + assertEquals(-1, settings.dataProviderIndex) + assertEquals(-1, settings.dataSetIndex) + assertFalse(settings.parallelTestingEnabled) + } + + fun testCustomValues() { + val settings = TestoRunnerSettings( + dataProviderIndex = 3, + dataSetIndex = 5, + parallelTestingEnabled = true, + ) + assertEquals(3, settings.dataProviderIndex) + assertEquals(5, settings.dataSetIndex) + assertTrue(settings.parallelTestingEnabled) + } + + fun testFromPhpTestRunnerSettings() { + val base = PhpTestRunnerSettings() + base.scope = PhpTestRunnerSettings.Scope.File + base.filePath = "/some/path/TestFile.php" + base.methodName = "testSomething" + base.directoryPath = "/some/dir" + base.isUseAlternativeConfigurationFile = true + base.configurationFilePath = "/config/testo.xml" + base.testRunnerOptions = "--verbose" + + val result = TestoRunnerSettings.fromPhpTestRunnerSettings(base) + + assertEquals(PhpTestRunnerSettings.Scope.File, result.scope) + assertEquals("/some/path/TestFile.php", result.filePath) + assertEquals("testSomething", result.methodName) + assertEquals("/some/dir", result.directoryPath) + assertTrue(result.isUseAlternativeConfigurationFile) + assertEquals("/config/testo.xml", result.configurationFilePath) + assertEquals("--verbose", result.testRunnerOptions) + // Testo-specific fields should be defaults + assertEquals(-1, result.dataProviderIndex) + assertEquals(-1, result.dataSetIndex) + assertFalse(result.parallelTestingEnabled) + } +} diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoTestDescriptorTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoTestDescriptorTest.kt new file mode 100644 index 0000000..d60d991 --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/TestoTestDescriptorTest.kt @@ -0,0 +1,30 @@ +package com.github.xepozz.testo + +import com.github.xepozz.testo.tests.TestoTestDescriptor +import junit.framework.TestCase + +class TestoTestDescriptorTest : TestCase() { + + fun testIsTestClassName_withTestSuffix() { + assertTrue(TestoTestDescriptor.isTestClassName("UserTest")) + assertTrue(TestoTestDescriptor.isTestClassName("SomeFeatureTest")) + } + + fun testIsTestClassName_withTestBaseSuffix() { + assertTrue(TestoTestDescriptor.isTestClassName("AbstractTestBase")) + assertTrue(TestoTestDescriptor.isTestClassName("BaseFeatureTestBase")) + } + + fun testIsTestClassName_withoutTestSuffix() { + assertFalse(TestoTestDescriptor.isTestClassName("UserService")) + assertFalse(TestoTestDescriptor.isTestClassName("TestHelper")) + assertFalse(TestoTestDescriptor.isTestClassName("Testing")) + assertFalse(TestoTestDescriptor.isTestClassName("Contest")) + } + + fun testIsTestClassName_edgeCases() { + assertTrue(TestoTestDescriptor.isTestClassName("Test")) + assertTrue(TestoTestDescriptor.isTestClassName("TestBase")) + assertFalse(TestoTestDescriptor.isTestClassName("")) + } +} diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoVersionDetectorTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoVersionDetectorTest.kt new file mode 100644 index 0000000..79e6c5e --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/TestoVersionDetectorTest.kt @@ -0,0 +1,37 @@ +package com.github.xepozz.testo + +import com.github.xepozz.testo.tests.TestoVersionDetector +import com.intellij.execution.ExecutionException +import junit.framework.TestCase + +class TestoVersionDetectorTest : TestCase() { + + fun testParse_validVersion() { + assertEquals("1.0.0", TestoVersionDetector.parse("Testo 1.0.0")) + } + + fun testParse_versionWithPreRelease() { + assertEquals("2.3.1-beta.1", TestoVersionDetector.parse("Testo 2.3.1-beta.1")) + } + + fun testParse_emptyAfterPrefix() { + try { + TestoVersionDetector.parse("Testo ") + fail("Expected ExecutionException") + } catch (_: ExecutionException) { + // expected + } + } + + fun testParse_noPrefix() { + // "substringAfter" returns original string if delimiter not found + assertEquals("SomeOtherOutput", TestoVersionDetector.parse("SomeOtherOutput")) + } + + fun testGetVersionOptions() { + val options = TestoVersionDetector.getVersionOptions() + assertEquals(2, options.size) + assertEquals("--version", options[0]) + assertEquals("--no-ansi", options[1]) + } +} From 93bb124f749355b6cb7334fdbe27060fd08d8c59 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 09:24:29 +0000 Subject: [PATCH 04/24] 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 --- .../xepozz/testo/ExitStatementsVisitorTest.kt | 107 ++++++++ .../com/github/xepozz/testo/MixinPsiTest.kt | 238 ++++++++++++++++++ .../com/github/xepozz/testo/MyPluginTest.kt | 8 - .../testo/TestoDataProvidersIndexTest.kt | 120 +++++++++ .../xepozz/testo/TestoLineMarkerPsiTest.kt | 79 ++++++ .../xepozz/testo/TestoTestLocatorTest.kt | 53 ++++ src/test/testData/mixin/DataProviderClass.php | 26 ++ src/test/testData/mixin/RegularClass.php | 12 + .../mixin/TestClassWithAttributes.php | 29 +++ .../mixin/TestClassWithTestBaseSuffix.php | 8 + .../mixin/TestClassWithTestSuffix.php | 12 + 11 files changed, 684 insertions(+), 8 deletions(-) create mode 100644 src/test/kotlin/com/github/xepozz/testo/ExitStatementsVisitorTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/MixinPsiTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/TestoDataProvidersIndexTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerPsiTest.kt create mode 100644 src/test/kotlin/com/github/xepozz/testo/TestoTestLocatorTest.kt create mode 100644 src/test/testData/mixin/DataProviderClass.php create mode 100644 src/test/testData/mixin/RegularClass.php create mode 100644 src/test/testData/mixin/TestClassWithAttributes.php create mode 100644 src/test/testData/mixin/TestClassWithTestBaseSuffix.php create mode 100644 src/test/testData/mixin/TestClassWithTestSuffix.php diff --git a/src/test/kotlin/com/github/xepozz/testo/ExitStatementsVisitorTest.kt b/src/test/kotlin/com/github/xepozz/testo/ExitStatementsVisitorTest.kt new file mode 100644 index 0000000..99b2b1c --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/ExitStatementsVisitorTest.kt @@ -0,0 +1,107 @@ +package com.github.xepozz.testo + +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.github.xepozz.testo.util.ExitStatementsVisitor +import com.jetbrains.php.lang.PhpFileType +import com.jetbrains.php.lang.psi.elements.Function +import com.jetbrains.php.lang.psi.elements.Method +import com.jetbrains.php.lang.psi.elements.PhpReturn +import com.jetbrains.php.lang.psi.elements.PhpYield + +class ExitStatementsVisitorTest : BasePlatformTestCase() { + + fun testVisitor_countsYieldStatements() { + val psiFile = myFixture.configureByText( + PhpFileType.INSTANCE, + """() - - assertNotSame(projectService.getRandomNumber(), projectService.getRandomNumber()) - } - override fun getTestDataPath() = "src/test/testData/rename" } diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoDataProvidersIndexTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoDataProvidersIndexTest.kt new file mode 100644 index 0000000..291724e --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/TestoDataProvidersIndexTest.kt @@ -0,0 +1,120 @@ +package com.github.xepozz.testo + +import com.github.xepozz.testo.index.TestoDataProvidersIndex +import com.intellij.openapi.util.Pair +import junit.framework.TestCase +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.DataInputStream +import java.io.DataOutputStream + +class TestoDataProvidersIndexTest : TestCase() { + + fun testIndexKey() { + assertEquals("Testo.DataProviders", TestoDataProvidersIndex.KEY.name) + } + + fun testDataProviderUsage_creation() { + val usage = TestoDataProvidersIndex.DataProviderUsage( + "\\App\\Tests\\UserTest", + "testCreate", + "\\App\\Tests\\UserTest" + ) + assertEquals("\\App\\Tests\\UserTest", usage.classFqn) + assertEquals("testCreate", usage.methodName) + assertEquals("\\App\\Tests\\UserTest", usage.dataProviderFqn) + } + + fun testDataProviderUsage_nullDataProviderFqn() { + val usage = TestoDataProvidersIndex.DataProviderUsage( + "\\App\\Tests\\FooTest", + "testBar", + null + ) + assertNull(usage.dataProviderFqn) + } + + fun testDataProviderUsage_equality() { + val usage1 = TestoDataProvidersIndex.DataProviderUsage("\\FooTest", "testA", "\\FooTest") + val usage2 = TestoDataProvidersIndex.DataProviderUsage("\\FooTest", "testA", "\\FooTest") + val usage3 = TestoDataProvidersIndex.DataProviderUsage("\\FooTest", "testB", "\\FooTest") + + assertEquals(usage1, usage2) + assertFalse(usage1 == usage3) + assertEquals(usage1.hashCode(), usage2.hashCode()) + } + + fun testExternalizer_roundTrip() { + val externalizer = TestoDataProvidersIndex.DataProviderUsageExternalizer.INSTANCE + + val original = mutableSetOf( + TestoDataProvidersIndex.DataProviderUsage("\\Tests\\UserTest", "testCreate", "\\Tests\\UserTest"), + TestoDataProvidersIndex.DataProviderUsage("\\Tests\\OrderTest", "testProcess", null), + ) + + val baos = ByteArrayOutputStream() + val out = DataOutputStream(baos) + externalizer.save(out, original) + out.flush() + + val bais = ByteArrayInputStream(baos.toByteArray()) + val inp = DataInputStream(bais) + val restored = externalizer.read(inp) + + assertEquals("Restored set should have same size", original.size, restored.size) + for (item in original) { + assertTrue("Restored set should contain $item", restored.contains(item)) + } + } + + fun testExternalizer_emptySet() { + val externalizer = TestoDataProvidersIndex.DataProviderUsageExternalizer.INSTANCE + + val original = mutableSetOf() + + val baos = ByteArrayOutputStream() + val out = DataOutputStream(baos) + externalizer.save(out, original) + out.flush() + + val bais = ByteArrayInputStream(baos.toByteArray()) + val inp = DataInputStream(bais) + val restored = externalizer.read(inp) + + assertTrue("Restored set should be empty", restored.isEmpty()) + } + + fun testExternalizer_nullDataProviderFqn_roundTrip() { + val externalizer = TestoDataProvidersIndex.DataProviderUsageExternalizer.INSTANCE + + val original = mutableSetOf( + TestoDataProvidersIndex.DataProviderUsage("\\Tests\\Foo", "testBar", null), + ) + + val baos = ByteArrayOutputStream() + val out = DataOutputStream(baos) + externalizer.save(out, original) + out.flush() + + val bais = ByteArrayInputStream(baos.toByteArray()) + val inp = DataInputStream(bais) + val restored = externalizer.read(inp) + + assertEquals(1, restored.size) + val restoredItem = restored.first() + assertEquals("\\Tests\\Foo", restoredItem.classFqn) + assertEquals("testBar", restoredItem.methodName) + // null is stored as empty string by StringUtil.notNullize, restored as null by StringUtil.nullize + assertNull("Null dataProviderFqn should survive round-trip", restoredItem.dataProviderFqn) + } + + fun testIndexVersion() { + val index = TestoDataProvidersIndex() + assertTrue("Index version should be positive", index.version > 0) + } + + fun testDependsOnFileContent() { + val index = TestoDataProvidersIndex() + assertTrue("Index should depend on file content", index.dependsOnFileContent()) + } +} diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerPsiTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerPsiTest.kt new file mode 100644 index 0000000..c3e30bf --- /dev/null +++ b/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerPsiTest.kt @@ -0,0 +1,79 @@ +package com.github.xepozz.testo + +import com.github.xepozz.testo.tests.TestoTestRunLineMarkerProvider +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import com.jetbrains.php.lang.PhpFileType +import com.jetbrains.php.lang.psi.elements.Method +import com.jetbrains.php.lang.psi.elements.PhpClass + +class TestoLineMarkerPsiTest : BasePlatformTestCase() { + + fun testGetLocationHint_forClass() { + val psiFile = myFixture.configureByText( + PhpFileType.INSTANCE, + """ Date: Tue, 17 Mar 2026 10:10:05 +0000 Subject: [PATCH 05/24] Refactor run configuration for flexible argument passing Based on infection-plugin architecture: - TestoRunnerSettings: add @Tag/@Attribute annotations for XMLB serialization, new fields: command, suite, group, excludeGroup, repeat, parallel. fromPhpTestRunnerSettings() now copies Testo-specific fields. - TestoRunConfigurationHandler: add prepareArguments() that builds argument list from settings (suite, group, excludeGroup, repeat, parallel). Overloaded prepareCommand() supports custom subcommand. Extract parseMethodName() for method#dataProvider parsing. - TestoRunConfiguration: override createCommand() to take full control of command assembly. Extract fillTestRunnerArguments() for scope-based args (Type, Directory, File, Method, ConfigurationFile). - TestoRunConfigurationSettings: add @Property/@Transient for proper XMLB serialization of TestoRunnerSettings. - TestoTestRunConfigurationEditor: add UI fields for command, suite, group, excludeGroup, repeat, parallel with proper reset/apply binding and reflection-based parent editor integration. - Tests: full coverage for prepareArguments(), parseMethodName(), fromPhpTestRunnerSettings() with new fields. https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- .../testo/tests/run/TestoRunConfiguration.kt | 120 +++++++++++++- .../tests/run/TestoRunConfigurationHandler.kt | 73 ++++++--- .../run/TestoRunConfigurationSettings.kt | 16 +- .../testo/tests/run/TestoRunnerSettings.kt | 36 ++++- .../run/TestoTestRunConfigurationEditor.kt | 139 ++++++++++++++-- .../testo/TestoRunConfigurationHandlerTest.kt | 152 ++++++++++++++++++ .../xepozz/testo/TestoRunnerSettingsTest.kt | 60 ++++++- 7 files changed, 553 insertions(+), 43 deletions(-) diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfiguration.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfiguration.kt index f9fc602..0c2bb20 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfiguration.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfiguration.kt @@ -5,21 +5,30 @@ import com.github.xepozz.testo.isTestoExecutable import com.github.xepozz.testo.tests.TestoConsoleProperties import com.github.xepozz.testo.tests.TestoFrameworkType import com.github.xepozz.testo.tests.actions.TestoRerunFailedTestsAction +import com.intellij.execution.ExecutionException import com.intellij.execution.Executor import com.intellij.execution.configurations.ConfigurationFactory +import com.intellij.execution.configurations.ParametersList import com.intellij.execution.configurations.RunConfiguration import com.intellij.execution.testframework.sm.runner.SMTRunnerConsoleProperties import com.intellij.execution.ui.ConsoleView import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.jetbrains.php.PhpBundle import com.jetbrains.php.config.commandLine.PhpCommandLinePathProcessor +import com.jetbrains.php.config.commandLine.PhpCommandSettings +import com.jetbrains.php.config.commandLine.PhpCommandSettingsBuilder +import com.jetbrains.php.config.interpreters.PhpInterpreter import com.jetbrains.php.run.PhpAsyncRunConfiguration import com.jetbrains.php.run.remote.PhpRemoteInterpreterManager import com.jetbrains.php.testFramework.PhpTestFrameworkConfiguration import com.jetbrains.php.testFramework.run.PhpTestRunConfiguration import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationEditor +import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationHandler import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationSettings import com.jetbrains.php.testFramework.run.PhpTestRunnerConfigurationEditor +import com.jetbrains.php.testFramework.run.PhpTestRunnerSettings class TestoRunConfiguration(project: Project, factory: ConfigurationFactory) : PhpTestRunConfiguration( project, @@ -29,6 +38,8 @@ class TestoRunConfiguration(project: Project, factory: ConfigurationFactory) : P TestoTestRunnerSettingsValidator, TestoRunConfigurationHandler.INSTANCE, ), PhpAsyncRunConfiguration { + val myHandler = TestoRunConfigurationHandler.INSTANCE + val testoSettings get() = settings as TestoRunConfigurationSettings @@ -46,9 +57,8 @@ class TestoRunConfiguration(project: Project, factory: ConfigurationFactory) : P override fun getConfigurationEditor(): SettingsEditor { val editor = super.getConfigurationEditor() as PhpTestRunConfigurationEditor - editor.setRunnerOptionsDocumentation("https://infection.github.io/guide/command-line-options.html") + editor.setRunnerOptionsDocumentation("https://github.com/testo/testo") -// return this.addExtensionEditor(editor)!! return TestoTestRunConfigurationEditor(editor, this) } @@ -58,8 +68,54 @@ class TestoRunConfiguration(project: Project, factory: ConfigurationFactory) : P config: PhpTestFrameworkConfiguration? ) = project.basePath + override fun createCommand( + interpreter: PhpInterpreter, + env: MutableMap, + arguments: MutableList, + frameworkConfig: PhpTestFrameworkConfiguration?, + withDebugger: Boolean + ): PhpCommandSettings { + val command = PhpCommandSettingsBuilder(project, interpreter) + .loadAndStartDebug(withDebugger) + .build() + + val executablePath = frameworkConfig?.executablePath + if (frameworkConfig == null || executablePath.isNullOrEmpty()) { + throw ExecutionException( + PhpBundle.message( + "php.interpreter.base.configuration.is.not.provided.or.empty", + frameworkName, + if (command.isRemote) "'${interpreter.name}' interpreter" else "local machine", + ) + ) + } + + val workingDirectory = getWorkingDirectory(project, settings, frameworkConfig) + if (workingDirectory.isNullOrEmpty()) { + throw ExecutionException(PhpBundle.message("php.interpreter.base.configuration.working.directory")) + } + command.setWorkingDir(workingDirectory) + + myHandler.prepareArguments(arguments, testoSettings) + myHandler.prepareCommand(project, command, executablePath, null, testoSettings.runnerSettings.command) + + command.importCommandLineSettings(settings.commandLineSettings, workingDirectory) + command.addEnvs(env) + + fillTestRunnerArguments( + project, + workingDirectory, + settings.runnerSettings, + arguments, + command, + frameworkConfig, + myHandler, + ) + + return command + } + override fun createTestConsoleProperties(executor: Executor): SMTRunnerConsoleProperties { -// println("createTestConsoleProperties") val manager = PhpRemoteInterpreterManager.getInstance() val pathProcessor = when (this.interpreter?.isRemote) { @@ -76,8 +132,60 @@ class TestoRunConfiguration(project: Project, factory: ConfigurationFactory) : P } companion object Companion { - const val ID = "InfectionConsoleCommandRunConfiguration" + const val ID = "TestoConsoleCommandRunConfiguration" + + private fun fillTestRunnerArguments( + project: Project, + workingDirectory: String, + testRunnerSettings: PhpTestRunnerSettings, + arguments: MutableList, + command: PhpCommandSettings, + configuration: PhpTestFrameworkConfiguration?, + handler: PhpTestRunConfigurationHandler, + ) { + val testRunnerOptions = testRunnerSettings.testRunnerOptions + if (StringUtil.isNotEmpty(testRunnerOptions)) { + command.addArguments(ParametersList.parse(testRunnerOptions!!).toList()) + } + + command.addArguments(arguments) + + val configurationFilePath = getConfigurationFile(testRunnerSettings, configuration) + if (!configurationFilePath.isNullOrEmpty()) { + command.addArgument(handler.configFileOption) + command.addPathArgument(configurationFilePath) + } + + val scope = testRunnerSettings.scope + when (scope) { + PhpTestRunnerSettings.Scope.Type -> handler.runType( + project, + command, + StringUtil.notNullize(testRunnerSettings.selectedType), + workingDirectory, + ) + + PhpTestRunnerSettings.Scope.Directory -> handler.runDirectory( + project, + command, + StringUtil.notNullize(testRunnerSettings.directoryPath), + workingDirectory, + ) + + PhpTestRunnerSettings.Scope.File -> handler.runFile( + project, + command, + StringUtil.notNullize(testRunnerSettings.filePath), + workingDirectory, + ) + + PhpTestRunnerSettings.Scope.Method -> { + val filePath = StringUtil.notNullize(testRunnerSettings.filePath) + handler.runMethod(project, command, filePath, testRunnerSettings.methodName, workingDirectory) + } -// val INSTANCE = TestoRunConfiguration() + PhpTestRunnerSettings.Scope.ConfigurationFile -> {} + } + } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt index 19ffd9b..bd606cb 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt @@ -13,15 +13,45 @@ class TestoRunConfigurationHandler : PhpTestRunConfigurationHandler { override fun getConfigFileOption() = "--config" override fun prepareCommand(project: Project, commandSettings: PhpCommandSettings, exe: String, version: String?) { + prepareCommand(project, commandSettings, exe, version, "run") + } + + fun prepareCommand( + project: Project, + commandSettings: PhpCommandSettings, + exe: String, + version: String?, + command: String, + ) { commandSettings.apply { setScript(exe, true) - addArgument("run") -// addArgument("--no-progress") -// addArgument("-n") -// addArgument("-q") -// addArgument("--logger-gitlab=php://stdout") + addArgument(command) + } + } + + fun prepareArguments(arguments: MutableList, testoSettings: TestoRunConfigurationSettings) { + val runner = testoSettings.runnerSettings + + if (runner.suite.isNotEmpty()) { + arguments.add("--suite") + arguments.add(runner.suite) + } + if (runner.group.isNotEmpty()) { + arguments.add("--group") + arguments.add(runner.group) + } + if (runner.excludeGroup.isNotEmpty()) { + arguments.add("--exclude-group") + arguments.add(runner.excludeGroup) + } + if (runner.repeat > 0) { + arguments.add("--repeat") + arguments.add(runner.repeat.toString()) + } + if (runner.parallel > 0) { + arguments.add("--parallel") + arguments.add(runner.parallel.toString()) } -// println("commandSettings: $commandSettings") } override fun runType( @@ -30,8 +60,6 @@ class TestoRunConfigurationHandler : PhpTestRunConfigurationHandler { type: String, workingDirectory: String ) { -// println("runType: $type, $workingDirectory") - phpCommandSettings.apply { addArgument("--suite") addArgument(type) @@ -44,7 +72,6 @@ class TestoRunConfigurationHandler : PhpTestRunConfigurationHandler { directory: String, workingDirectory: String ) { -// println("runDirectory: $directory") if (directory.isEmpty()) return phpCommandSettings.apply { @@ -59,7 +86,6 @@ class TestoRunConfigurationHandler : PhpTestRunConfigurationHandler { file: String, workingDirectory: String ) { -// println("runFile: $file") if (file.isEmpty()) return phpCommandSettings.apply { @@ -75,25 +101,32 @@ class TestoRunConfigurationHandler : PhpTestRunConfigurationHandler { methodName: String, workingDirectory: String ) { -// println("runMethod: $file, $methodName") if (file.isEmpty()) return - val myMethodName = methodName.substringBefore('#') - val dataProvider = methodName.substringAfter('#', "") - -// println("method: $myMethodName, dataProvider: $dataProvider") + val parsed = parseMethodName(methodName) phpCommandSettings.apply { addArgument("--path") addRelativePathArgument(file, workingDirectory) - if (myMethodName.isNotEmpty()) { + if (parsed.method.isNotEmpty()) { addArgument("--filter") - addArgument(myMethodName) + addArgument(parsed.method) } - if (dataProvider.isNotEmpty()) { + if (parsed.dataProvider.isNotEmpty()) { addArgument("--data-provider") - addArgument(dataProvider) + addArgument(parsed.dataProvider) } } } -} \ No newline at end of file + + data class ParsedMethodName( + val method: String, + val dataProvider: String, + ) + + fun parseMethodName(methodName: String): ParsedMethodName { + val method = methodName.substringBefore('#') + val dataProvider = methodName.substringAfter('#', "") + return ParsedMethodName(method, dataProvider) + } +} diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationSettings.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationSettings.kt index 5784ca0..1025db4 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationSettings.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationSettings.kt @@ -1,5 +1,7 @@ package com.github.xepozz.testo.tests.run +import com.intellij.util.xmlb.annotations.Property +import com.intellij.util.xmlb.annotations.Transient import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationSettings import com.jetbrains.php.testFramework.run.PhpTestRunnerSettings @@ -10,22 +12,24 @@ class TestoRunConfigurationSettings : PhpTestRunConfigurationSettings() { override fun getRunnerSettings() = getTestoRunnerSettings() + @Transient override fun setRunnerSettings(runnerSettings: PhpTestRunnerSettings) { super.setRunnerSettings(TestoRunnerSettings.fromPhpTestRunnerSettings(runnerSettings)) } + @Property(surroundWithTag = false) fun getTestoRunnerSettings(): TestoRunnerSettings { val settings = super.getRunnerSettings() if (settings is TestoRunnerSettings) { return settings - } else { - val copy = TestoRunnerSettings.fromPhpTestRunnerSettings(settings) - this.setTestoRunnerSettings(copy) - return copy } + + val copy = TestoRunnerSettings.fromPhpTestRunnerSettings(settings) + setTestoRunnerSettings(copy) + return copy } fun setTestoRunnerSettings(runnerSettings: TestoRunnerSettings) { - this.setRunnerSettings(runnerSettings) + setRunnerSettings(runnerSettings) } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunnerSettings.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunnerSettings.kt index 7b81a3e..96d0848 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunnerSettings.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunnerSettings.kt @@ -1,13 +1,34 @@ package com.github.xepozz.testo.tests.run +import com.intellij.util.xmlb.annotations.Attribute +import com.intellij.util.xmlb.annotations.Tag import com.jetbrains.php.phpunit.coverage.PhpUnitCoverageEngine.CoverageEngine import com.jetbrains.php.testFramework.run.PhpTestRunnerSettings +@Tag("TestoRunnerSettings") class TestoRunnerSettings( var dataProviderIndex: Int = -1, var dataSetIndex: Int = -1, var coverageEngine: CoverageEngine = CoverageEngine.XDEBUG, var parallelTestingEnabled: Boolean = false, + + @Attribute("command") + var command: String = "run", + + @Attribute("suite") + var suite: String = "", + + @Attribute("group") + var group: String = "", + + @Attribute("exclude_group") + var excludeGroup: String = "", + + @Attribute("repeat") + var repeat: Int = 0, + + @Attribute("parallel") + var parallel: Int = 0, ) : PhpTestRunnerSettings() { companion object Companion { @JvmStatic @@ -23,7 +44,20 @@ class TestoRunnerSettings( runnerSettings.configurationFilePath = settings.configurationFilePath runnerSettings.testRunnerOptions = settings.testRunnerOptions + if (settings is TestoRunnerSettings) { + runnerSettings.dataProviderIndex = settings.dataProviderIndex + runnerSettings.dataSetIndex = settings.dataSetIndex + runnerSettings.coverageEngine = settings.coverageEngine + runnerSettings.parallelTestingEnabled = settings.parallelTestingEnabled + runnerSettings.command = settings.command + runnerSettings.suite = settings.suite + runnerSettings.group = settings.group + runnerSettings.excludeGroup = settings.excludeGroup + runnerSettings.repeat = settings.repeat + runnerSettings.parallel = settings.parallel + } + return runnerSettings } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoTestRunConfigurationEditor.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoTestRunConfigurationEditor.kt index 7272144..336f954 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoTestRunConfigurationEditor.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoTestRunConfigurationEditor.kt @@ -1,32 +1,153 @@ package com.github.xepozz.testo.tests.run import com.intellij.openapi.options.SettingsEditor +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.RightGap +import com.intellij.ui.dsl.builder.RowLayout import com.intellij.ui.dsl.builder.panel import com.jetbrains.php.testFramework.run.PhpTestRunConfigurationEditor +import java.lang.reflect.InvocationTargetException +import javax.swing.JComponent +import javax.swing.JSpinner +import javax.swing.SpinnerNumberModel +import javax.swing.event.DocumentEvent class TestoTestRunConfigurationEditor( private val parentEditor: PhpTestRunConfigurationEditor, val configuration: TestoRunConfiguration ) : SettingsEditor() { - private val myMainPanel = panel { - val runnerSettings = configuration.testoSettings.runnerSettings + private val commandField = ComboBox(arrayOf("run")).apply { isEditable = true } + private val suiteField = JBTextField() + private val groupField = JBTextField() + private val excludeGroupField = JBTextField() + private val repeatField = JSpinner(SpinnerNumberModel(0, 0, 10000, 1)) + private val parallelField = JSpinner(SpinnerNumberModel(0, 0, 64, 1)) + private val myMainPanel = panel { row { cell(parentEditor.component) + .align(AlignX.FILL) + }.layout(RowLayout.LABEL_ALIGNED) + + group("Testo Options") { + row { + label("Command") + .gap(RightGap.COLUMNS) + cell(commandField) + .align(AlignX.FILL) + } + .layout(RowLayout.PARENT_GRID) + .rowComment("Subcommand to execute (default: run)") + + row { + label("Suite") + .gap(RightGap.COLUMNS) + cell(suiteField) + .align(AlignX.FILL) + } + .layout(RowLayout.PARENT_GRID) + .rowComment("--suite=") + + row { + label("Group") + .gap(RightGap.COLUMNS) + cell(groupField) + .align(AlignX.FILL) + } + .layout(RowLayout.PARENT_GRID) + .rowComment("--group=") + + row { + label("Exclude group") + .gap(RightGap.COLUMNS) + cell(excludeGroupField) + .align(AlignX.FILL) + } + .layout(RowLayout.PARENT_GRID) + .rowComment("--exclude-group=") + + row { + label("Repeat") + .gap(RightGap.COLUMNS) + cell(repeatField) + } + .layout(RowLayout.PARENT_GRID) + .rowComment("--repeat= (0 = disabled)") + + row { + label("Parallel") + .gap(RightGap.COLUMNS) + cell(parallelField) + } + .layout(RowLayout.PARENT_GRID) + .rowComment("--parallel= (0 = disabled)") } } - override fun createEditor() = myMainPanel + init { + val listener = { fireEditorStateChanged() } + val documentAdapter = object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) = listener() + } + + commandField.addActionListener { listener() } + suiteField.document.addDocumentListener(documentAdapter) + groupField.document.addDocumentListener(documentAdapter) + excludeGroupField.document.addDocumentListener(documentAdapter) + repeatField.addChangeListener { listener() } + parallelField.addChangeListener { listener() } + } - override fun isSpecificallyModified() = myMainPanel.isModified() || parentEditor.isSpecificallyModified + override fun createEditor(): JComponent = myMainPanel + + override fun isSpecificallyModified(): Boolean { + val runner = configuration.testoSettings.runnerSettings + return commandField.selectedItem != runner.command + || suiteField.text != runner.suite + || groupField.text != runner.group + || excludeGroupField.text != runner.excludeGroup + || (repeatField.value as Int) != runner.repeat + || (parallelField.value as Int) != runner.parallel + || parentEditor.isSpecificallyModified + } override fun resetEditorFrom(testoRunConfiguration: TestoRunConfiguration) { - myMainPanel.reset() - parentEditor.resetFrom(testoRunConfiguration) + val runnerSettings = testoRunConfiguration.testoSettings.runnerSettings + commandField.selectedItem = runnerSettings.command + suiteField.text = runnerSettings.suite + groupField.text = runnerSettings.group + excludeGroupField.text = runnerSettings.excludeGroup + repeatField.value = runnerSettings.repeat + parallelField.value = runnerSettings.parallel + + parentEditor.javaClass.declaredMethods.find { it.name == "resetEditorFrom" && it.parameterCount == 1 }?.let { + it.isAccessible = true + it.invoke(parentEditor, testoRunConfiguration) + } ?: parentEditor.resetFrom(testoRunConfiguration) } override fun applyEditorTo(testoRunConfiguration: TestoRunConfiguration) { - parentEditor.applyTo(testoRunConfiguration) - myMainPanel.apply() + parentEditor.javaClass.declaredMethods.find { it.name == "applyEditorTo" && it.parameterCount == 1 }?.let { + it.isAccessible = true + try { + it.invoke(parentEditor, testoRunConfiguration) + } catch (exception: InvocationTargetException) { + if (exception.cause?.javaClass?.simpleName == "ReadOnlyModificationException") { + return@let + } + throw exception + } + } ?: parentEditor.applyTo(testoRunConfiguration) + + val runnerSettings = testoRunConfiguration.testoSettings.runnerSettings + runnerSettings.command = commandField.selectedItem as? String ?: "run" + runnerSettings.suite = suiteField.text + runnerSettings.group = groupField.text + runnerSettings.excludeGroup = excludeGroupField.text + runnerSettings.repeat = repeatField.value as? Int ?: 0 + runnerSettings.parallel = parallelField.value as? Int ?: 0 } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt index 75be63e..a82fd22 100644 --- a/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt @@ -1,6 +1,8 @@ package com.github.xepozz.testo import com.github.xepozz.testo.tests.run.TestoRunConfigurationHandler +import com.github.xepozz.testo.tests.run.TestoRunConfigurationSettings +import com.github.xepozz.testo.tests.run.TestoRunnerSettings import junit.framework.TestCase class TestoRunConfigurationHandlerTest : TestCase() { @@ -12,4 +14,154 @@ class TestoRunConfigurationHandlerTest : TestCase() { fun testSingletonInstance() { assertSame(TestoRunConfigurationHandler.INSTANCE, TestoRunConfigurationHandler.INSTANCE) } + + // ---- parseMethodName ---- + + fun testParseMethodName_simpleMethod() { + val parsed = TestoRunConfigurationHandler.INSTANCE.parseMethodName("testSomething") + assertEquals("testSomething", parsed.method) + assertEquals("", parsed.dataProvider) + } + + fun testParseMethodName_withDataProvider() { + val parsed = TestoRunConfigurationHandler.INSTANCE.parseMethodName("testSomething#provideData") + assertEquals("testSomething", parsed.method) + assertEquals("provideData", parsed.dataProvider) + } + + fun testParseMethodName_withDataProviderAndIndex() { + val parsed = TestoRunConfigurationHandler.INSTANCE.parseMethodName("testSomething#0:2") + assertEquals("testSomething", parsed.method) + assertEquals("0:2", parsed.dataProvider) + } + + fun testParseMethodName_emptyString() { + val parsed = TestoRunConfigurationHandler.INSTANCE.parseMethodName("") + assertEquals("", parsed.method) + assertEquals("", parsed.dataProvider) + } + + // ---- prepareArguments ---- + + fun testPrepareArguments_allDefaults_emptyList() { + val settings = TestoRunConfigurationSettings() + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertTrue("No arguments should be added for default settings", arguments.isEmpty()) + } + + fun testPrepareArguments_withSuite() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.suite = "unit" + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertEquals(2, arguments.size) + assertEquals("--suite", arguments[0]) + assertEquals("unit", arguments[1]) + } + + fun testPrepareArguments_withGroup() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.group = "fast" + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertEquals(2, arguments.size) + assertEquals("--group", arguments[0]) + assertEquals("fast", arguments[1]) + } + + fun testPrepareArguments_withExcludeGroup() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.excludeGroup = "slow" + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertEquals(2, arguments.size) + assertEquals("--exclude-group", arguments[0]) + assertEquals("slow", arguments[1]) + } + + fun testPrepareArguments_withRepeat() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.repeat = 3 + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertEquals(2, arguments.size) + assertEquals("--repeat", arguments[0]) + assertEquals("3", arguments[1]) + } + + fun testPrepareArguments_withParallel() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.parallel = 8 + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertEquals(2, arguments.size) + assertEquals("--parallel", arguments[0]) + assertEquals("8", arguments[1]) + } + + fun testPrepareArguments_zeroRepeatAndParallel_skipped() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.repeat = 0 + settings.runnerSettings.parallel = 0 + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertTrue("Zero repeat/parallel should not add arguments", arguments.isEmpty()) + } + + fun testPrepareArguments_allOptions() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.suite = "integration" + settings.runnerSettings.group = "db" + settings.runnerSettings.excludeGroup = "slow" + settings.runnerSettings.repeat = 2 + settings.runnerSettings.parallel = 4 + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertEquals(10, arguments.size) + assertTrue(arguments.contains("--suite")) + assertTrue(arguments.contains("integration")) + assertTrue(arguments.contains("--group")) + assertTrue(arguments.contains("db")) + assertTrue(arguments.contains("--exclude-group")) + assertTrue(arguments.contains("slow")) + assertTrue(arguments.contains("--repeat")) + assertTrue(arguments.contains("2")) + assertTrue(arguments.contains("--parallel")) + assertTrue(arguments.contains("4")) + } + + fun testPrepareArguments_orderIsCorrect() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.suite = "unit" + settings.runnerSettings.group = "fast" + settings.runnerSettings.parallel = 2 + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + // suite comes first, then group, then parallel + assertEquals("--suite", arguments[0]) + assertEquals("unit", arguments[1]) + assertEquals("--group", arguments[2]) + assertEquals("fast", arguments[3]) + assertEquals("--parallel", arguments[4]) + assertEquals("2", arguments[5]) + } } diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt index 79c393b..e45f019 100644 --- a/src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt @@ -11,6 +11,12 @@ class TestoRunnerSettingsTest : TestCase() { assertEquals(-1, settings.dataProviderIndex) assertEquals(-1, settings.dataSetIndex) assertFalse(settings.parallelTestingEnabled) + assertEquals("run", settings.command) + assertEquals("", settings.suite) + assertEquals("", settings.group) + assertEquals("", settings.excludeGroup) + assertEquals(0, settings.repeat) + assertEquals(0, settings.parallel) } fun testCustomValues() { @@ -18,13 +24,25 @@ class TestoRunnerSettingsTest : TestCase() { dataProviderIndex = 3, dataSetIndex = 5, parallelTestingEnabled = true, + command = "list", + suite = "unit", + group = "fast", + excludeGroup = "slow", + repeat = 3, + parallel = 4, ) assertEquals(3, settings.dataProviderIndex) assertEquals(5, settings.dataSetIndex) assertTrue(settings.parallelTestingEnabled) + assertEquals("list", settings.command) + assertEquals("unit", settings.suite) + assertEquals("fast", settings.group) + assertEquals("slow", settings.excludeGroup) + assertEquals(3, settings.repeat) + assertEquals(4, settings.parallel) } - fun testFromPhpTestRunnerSettings() { + fun testFromPhpTestRunnerSettings_baseSettings() { val base = PhpTestRunnerSettings() base.scope = PhpTestRunnerSettings.Scope.File base.filePath = "/some/path/TestFile.php" @@ -47,5 +65,45 @@ class TestoRunnerSettingsTest : TestCase() { assertEquals(-1, result.dataProviderIndex) assertEquals(-1, result.dataSetIndex) assertFalse(result.parallelTestingEnabled) + assertEquals("run", result.command) + assertEquals("", result.suite) + assertEquals("", result.group) + assertEquals("", result.excludeGroup) + assertEquals(0, result.repeat) + assertEquals(0, result.parallel) + } + + fun testFromPhpTestRunnerSettings_testoSettings() { + val source = TestoRunnerSettings( + dataProviderIndex = 2, + dataSetIndex = 7, + parallelTestingEnabled = true, + command = "debug", + suite = "integration", + group = "database", + excludeGroup = "slow", + repeat = 5, + parallel = 8, + ) + source.scope = PhpTestRunnerSettings.Scope.Method + source.filePath = "/test.php" + source.methodName = "testIt" + + val result = TestoRunnerSettings.fromPhpTestRunnerSettings(source) + + // Base settings + assertEquals(PhpTestRunnerSettings.Scope.Method, result.scope) + assertEquals("/test.php", result.filePath) + assertEquals("testIt", result.methodName) + // Testo-specific settings preserved + assertEquals(2, result.dataProviderIndex) + assertEquals(7, result.dataSetIndex) + assertTrue(result.parallelTestingEnabled) + assertEquals("debug", result.command) + assertEquals("integration", result.suite) + assertEquals("database", result.group) + assertEquals("slow", result.excludeGroup) + assertEquals(5, result.repeat) + assertEquals(8, result.parallel) } } From 9c1add6f737ab0e4503bc160e88796dabb84fdc0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 10:37:46 +0000 Subject: [PATCH 06/24] Add #[Bench] attribute support and --type bench for benchmark runs - Add \Testo\Bench\Bench constant to TestoClasses and BENCH_ATTRIBUTES so #[Bench] attribute gets a gutter run icon via RUNNABLE_ATTRIBUTES - Set selectedType="bench" in TestoRunConfigurationProducer when the element being configured is a bench method (isTestoBench()) - Add --type argument to prepareArguments() based on selectedType, so benchmark runs pass --type bench to the CLI - Tests: update BENCH_ATTRIBUTES count, add --type bench tests, add order verification, add explicit Bench constant check https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- .../com/github/xepozz/testo/TestoClasses.kt | 2 + .../tests/run/TestoRunConfigurationHandler.kt | 4 ++ .../run/TestoRunConfigurationProducer.kt | 13 +++++- .../github/xepozz/testo/TestoClassesTest.kt | 7 ++- .../testo/TestoLineMarkerCompanionTest.kt | 5 +++ .../testo/TestoRunConfigurationHandlerTest.kt | 44 +++++++++++++++---- 6 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt b/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt index 90b3c03..2c1c668 100644 --- a/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt +++ b/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt @@ -14,6 +14,7 @@ object TestoClasses { const val DATA_CROSS = "\\Testo\\Data\\DataCross" const val DATA_ZIP = "\\Testo\\Data\\DataZip" + const val BENCH = "\\Testo\\Bench\\Bench" const val BENCH_WITH = "\\Testo\\Bench\\BenchWith" const val ASSERT = "\\Testo\\Assert" @@ -38,6 +39,7 @@ object TestoClasses { TEST_INLINE_NEW, ) val BENCH_ATTRIBUTES = arrayOf( + BENCH, BENCH_WITH, ) } \ No newline at end of file diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt index bd606cb..4482b89 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt @@ -32,6 +32,10 @@ class TestoRunConfigurationHandler : PhpTestRunConfigurationHandler { fun prepareArguments(arguments: MutableList, testoSettings: TestoRunConfigurationSettings) { val runner = testoSettings.runnerSettings + if (runner.selectedType.isNotEmpty()) { + arguments.add("--type") + arguments.add(runner.selectedType) + } if (runner.suite.isNotEmpty()) { arguments.add("--suite") arguments.add(runner.suite) diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt index fe2c3db..a571e83 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt @@ -2,6 +2,7 @@ package com.github.xepozz.testo.tests.run import com.github.xepozz.testo.TestoUtil import com.github.xepozz.testo.index.TestoDataProviderUtils +import com.github.xepozz.testo.isTestoBench import com.github.xepozz.testo.isTestoClass import com.github.xepozz.testo.isTestoDataProviderLike import com.github.xepozz.testo.isTestoExecutable @@ -67,6 +68,10 @@ class TestoRunConfigurationProducer : PhpTestConfigurationProducer { it.isTestoExecutable() || (it is Method && TestoDataProviderUtils.isDataProvider(it)) } diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt index 5f62ade..c462868 100644 --- a/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt @@ -32,10 +32,15 @@ class TestoClassesTest : TestCase() { fun testBenchAttributes_containsExpectedValues() { val attrs = TestoClasses.BENCH_ATTRIBUTES - assertEquals(1, attrs.size) + assertEquals(2, attrs.size) + assertTrue(attrs.contains("\\Testo\\Bench\\Bench")) assertTrue(attrs.contains("\\Testo\\Bench\\BenchWith")) } + fun testConstants_bench() { + assertEquals("\\Testo\\Bench\\Bench", TestoClasses.BENCH) + } + fun testConstants_assertionException() { assertEquals("\\Testo\\Assert\\State\\Assertion\\AssertionException", TestoClasses.ASSERTION_EXCEPTION) } diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt index dddc217..164c47b 100644 --- a/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt @@ -26,6 +26,11 @@ class TestoLineMarkerCompanionTest : TestCase() { } } + fun testRunnableAttributes_containsBenchAttribute() { + val runnable = TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.toSet() + assertTrue("Should contain \\Testo\\Bench\\Bench", runnable.contains(TestoClasses.BENCH)) + } + fun testRunnableAttributes_doesNotContainPlainTestAttributes() { val runnable = TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.toSet() for (attr in TestoClasses.TEST_ATTRIBUTES) { diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt index a82fd22..0f253fc 100644 --- a/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt @@ -52,6 +52,28 @@ class TestoRunConfigurationHandlerTest : TestCase() { assertTrue("No arguments should be added for default settings", arguments.isEmpty()) } + fun testPrepareArguments_withSelectedType_bench() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.selectedType = "bench" + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertEquals(2, arguments.size) + assertEquals("--type", arguments[0]) + assertEquals("bench", arguments[1]) + } + + fun testPrepareArguments_withSelectedType_empty_skipped() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.selectedType = "" + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertTrue("Empty selectedType should not add arguments", arguments.isEmpty()) + } + fun testPrepareArguments_withSuite() { val settings = TestoRunConfigurationSettings() settings.runnerSettings.suite = "unit" @@ -125,6 +147,7 @@ class TestoRunConfigurationHandlerTest : TestCase() { fun testPrepareArguments_allOptions() { val settings = TestoRunConfigurationSettings() + settings.runnerSettings.selectedType = "bench" settings.runnerSettings.suite = "integration" settings.runnerSettings.group = "db" settings.runnerSettings.excludeGroup = "slow" @@ -134,7 +157,9 @@ class TestoRunConfigurationHandlerTest : TestCase() { TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) - assertEquals(10, arguments.size) + assertEquals(12, arguments.size) + assertTrue(arguments.contains("--type")) + assertTrue(arguments.contains("bench")) assertTrue(arguments.contains("--suite")) assertTrue(arguments.contains("integration")) assertTrue(arguments.contains("--group")) @@ -149,6 +174,7 @@ class TestoRunConfigurationHandlerTest : TestCase() { fun testPrepareArguments_orderIsCorrect() { val settings = TestoRunConfigurationSettings() + settings.runnerSettings.selectedType = "bench" settings.runnerSettings.suite = "unit" settings.runnerSettings.group = "fast" settings.runnerSettings.parallel = 2 @@ -156,12 +182,14 @@ class TestoRunConfigurationHandlerTest : TestCase() { TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) - // suite comes first, then group, then parallel - assertEquals("--suite", arguments[0]) - assertEquals("unit", arguments[1]) - assertEquals("--group", arguments[2]) - assertEquals("fast", arguments[3]) - assertEquals("--parallel", arguments[4]) - assertEquals("2", arguments[5]) + // type comes first, then suite, then group, then parallel + assertEquals("--type", arguments[0]) + assertEquals("bench", arguments[1]) + assertEquals("--suite", arguments[2]) + assertEquals("unit", arguments[3]) + assertEquals("--group", arguments[4]) + assertEquals("fast", arguments[5]) + assertEquals("--parallel", arguments[6]) + assertEquals("2", arguments[7]) } } From f7af25492e91c02d49b94f41b3a760de0c979540 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 10:41:34 +0000 Subject: [PATCH 07/24] Fix Bench attribute FQN to \Testo\Bench (root namespace) https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt | 2 +- src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt b/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt index 2c1c668..57d79a3 100644 --- a/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt +++ b/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt @@ -14,7 +14,7 @@ object TestoClasses { const val DATA_CROSS = "\\Testo\\Data\\DataCross" const val DATA_ZIP = "\\Testo\\Data\\DataZip" - const val BENCH = "\\Testo\\Bench\\Bench" + const val BENCH = "\\Testo\\Bench" const val BENCH_WITH = "\\Testo\\Bench\\BenchWith" const val ASSERT = "\\Testo\\Assert" diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt index c462868..a1b4175 100644 --- a/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt @@ -33,12 +33,12 @@ class TestoClassesTest : TestCase() { fun testBenchAttributes_containsExpectedValues() { val attrs = TestoClasses.BENCH_ATTRIBUTES assertEquals(2, attrs.size) - assertTrue(attrs.contains("\\Testo\\Bench\\Bench")) + assertTrue(attrs.contains("\\Testo\\Bench")) assertTrue(attrs.contains("\\Testo\\Bench\\BenchWith")) } fun testConstants_bench() { - assertEquals("\\Testo\\Bench\\Bench", TestoClasses.BENCH) + assertEquals("\\Testo\\Bench", TestoClasses.BENCH) } fun testConstants_assertionException() { From c7dfd82139ba70030d0a337662e3cb930131d88d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 10:52:44 +0000 Subject: [PATCH 08/24] Replace selectedType with custom testoType field for bench type selectedType is a PhpTestRunnerSettings field tied to Scope.Type, which breaks normal test/method run configurations. Use our own testoType attribute in TestoRunnerSettings instead. https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- .../tests/run/TestoRunConfigurationHandler.kt | 4 ++-- .../tests/run/TestoRunConfigurationProducer.kt | 4 ++-- .../xepozz/testo/tests/run/TestoRunnerSettings.kt | 4 ++++ .../testo/TestoRunConfigurationHandlerTest.kt | 14 +++++++------- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt index 4482b89..89cef47 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationHandler.kt @@ -32,9 +32,9 @@ class TestoRunConfigurationHandler : PhpTestRunConfigurationHandler { fun prepareArguments(arguments: MutableList, testoSettings: TestoRunConfigurationSettings) { val runner = testoSettings.runnerSettings - if (runner.selectedType.isNotEmpty()) { + if (runner.testoType.isNotEmpty()) { arguments.add("--type") - arguments.add(runner.selectedType) + arguments.add(runner.testoType) } if (runner.suite.isNotEmpty()) { arguments.add("--suite") diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt index a571e83..ed006ec 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt @@ -69,7 +69,7 @@ class TestoRunConfigurationProducer : PhpTestConfigurationProducer() TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) @@ -64,14 +64,14 @@ class TestoRunConfigurationHandlerTest : TestCase() { assertEquals("bench", arguments[1]) } - fun testPrepareArguments_withSelectedType_empty_skipped() { + fun testPrepareArguments_withTestoType_empty_skipped() { val settings = TestoRunConfigurationSettings() - settings.runnerSettings.selectedType = "" + settings.runnerSettings.testoType = "" val arguments = mutableListOf() TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) - assertTrue("Empty selectedType should not add arguments", arguments.isEmpty()) + assertTrue("Empty testoType should not add arguments", arguments.isEmpty()) } fun testPrepareArguments_withSuite() { @@ -147,7 +147,7 @@ class TestoRunConfigurationHandlerTest : TestCase() { fun testPrepareArguments_allOptions() { val settings = TestoRunConfigurationSettings() - settings.runnerSettings.selectedType = "bench" + settings.runnerSettings.testoType = "bench" settings.runnerSettings.suite = "integration" settings.runnerSettings.group = "db" settings.runnerSettings.excludeGroup = "slow" @@ -174,7 +174,7 @@ class TestoRunConfigurationHandlerTest : TestCase() { fun testPrepareArguments_orderIsCorrect() { val settings = TestoRunConfigurationSettings() - settings.runnerSettings.selectedType = "bench" + settings.runnerSettings.testoType = "bench" settings.runnerSettings.suite = "unit" settings.runnerSettings.group = "fast" settings.runnerSettings.parallel = 2 From 42964a4261ace1cedcec3fb77c54674f1a488a65 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Tue, 17 Mar 2026 15:24:16 +0400 Subject: [PATCH 09/24] refactor: remove deprecated inline test attributes --- .../com/github/xepozz/testo/TestoClasses.kt | 28 ++++++------------- .../kotlin/com/github/xepozz/testo/mixin.kt | 4 +-- .../tests/TestoTestRunLineMarkerProvider.kt | 4 +-- .../com/github/xepozz/testo/util/PsiUtil.kt | 1 - .../com/github/xepozz/testo/PsiUtilTest.kt | 4 --- .../github/xepozz/testo/TestoClassesTest.kt | 11 -------- .../testo/TestoLineMarkerCompanionTest.kt | 9 +----- 7 files changed, 13 insertions(+), 48 deletions(-) diff --git a/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt b/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt index 57d79a3..8092d9d 100644 --- a/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt +++ b/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt @@ -1,45 +1,33 @@ 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 = "\\Testo\\Bench" - const val BENCH_WITH = "\\Testo\\Bench\\BenchWith" 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, - ) - val TEST_INLINE_ATTRIBUTES = arrayOf( - TEST_INLINE_OLD, - TEST_INLINE_NEW, + TEST, + TEST_INLINE, ) val BENCH_ATTRIBUTES = arrayOf( BENCH, - BENCH_WITH, ) } \ No newline at end of file diff --git a/src/main/kotlin/com/github/xepozz/testo/mixin.kt b/src/main/kotlin/com/github/xepozz/testo/mixin.kt index a26a031..7e719bd 100644 --- a/src/main/kotlin/com/github/xepozz/testo/mixin.kt +++ b/src/main/kotlin/com/github/xepozz/testo/mixin.kt @@ -18,12 +18,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 } diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt b/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt index 78364db..bd8af02 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt @@ -87,9 +87,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) { diff --git a/src/main/kotlin/com/github/xepozz/testo/util/PsiUtil.kt b/src/main/kotlin/com/github/xepozz/testo/util/PsiUtil.kt index 7415120..35de6c5 100644 --- a/src/main/kotlin/com/github/xepozz/testo/util/PsiUtil.kt +++ b/src/main/kotlin/com/github/xepozz/testo/util/PsiUtil.kt @@ -10,7 +10,6 @@ object PsiUtil { val MEANINGFUL_ATTRIBUTES = arrayOf( *TestoClasses.DATA_ATTRIBUTES, *TestoClasses.TEST_ATTRIBUTES, - *TestoClasses.TEST_INLINE_ATTRIBUTES, *TestoClasses.BENCH_ATTRIBUTES, ) diff --git a/src/test/kotlin/com/github/xepozz/testo/PsiUtilTest.kt b/src/test/kotlin/com/github/xepozz/testo/PsiUtilTest.kt index 0ee9e93..63b1c6b 100644 --- a/src/test/kotlin/com/github/xepozz/testo/PsiUtilTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/PsiUtilTest.kt @@ -14,9 +14,6 @@ class PsiUtilTest : TestCase() { for (attr in TestoClasses.TEST_ATTRIBUTES) { assertTrue("Missing test attribute: $attr", meaningful.contains(attr)) } - for (attr in TestoClasses.TEST_INLINE_ATTRIBUTES) { - assertTrue("Missing inline test attribute: $attr", meaningful.contains(attr)) - } for (attr in TestoClasses.BENCH_ATTRIBUTES) { assertTrue("Missing bench attribute: $attr", meaningful.contains(attr)) } @@ -25,7 +22,6 @@ class PsiUtilTest : TestCase() { fun testMeaningfulAttributes_totalCount() { val expected = TestoClasses.DATA_ATTRIBUTES.size + TestoClasses.TEST_ATTRIBUTES.size + - TestoClasses.TEST_INLINE_ATTRIBUTES.size + TestoClasses.BENCH_ATTRIBUTES.size assertEquals(expected, PsiUtil.MEANINGFUL_ATTRIBUTES.size) } diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt index a1b4175..255995f 100644 --- a/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/TestoClassesTest.kt @@ -11,13 +11,6 @@ class TestoClassesTest : TestCase() { assertTrue(attrs.contains("\\Testo\\Application\\Attribute\\Test")) } - fun testTestInlineAttributes_containsExpectedValues() { - val attrs = TestoClasses.TEST_INLINE_ATTRIBUTES - assertEquals(2, attrs.size) - assertTrue(attrs.contains("\\Testo\\Sample\\TestInline")) - assertTrue(attrs.contains("\\Testo\\Inline\\TestInline")) - } - fun testDataAttributes_containsAllDataTypes() { val attrs = TestoClasses.DATA_ATTRIBUTES assertEquals(7, attrs.size) @@ -56,14 +49,10 @@ class TestoClassesTest : TestCase() { fun testDataAttributes_noOverlapWithTestAttributes() { val dataSet = TestoClasses.DATA_ATTRIBUTES.toSet() val testSet = TestoClasses.TEST_ATTRIBUTES.toSet() - val inlineSet = TestoClasses.TEST_INLINE_ATTRIBUTES.toSet() val benchSet = TestoClasses.BENCH_ATTRIBUTES.toSet() assertTrue(dataSet.intersect(testSet).isEmpty()) - assertTrue(dataSet.intersect(inlineSet).isEmpty()) assertTrue(dataSet.intersect(benchSet).isEmpty()) - assertTrue(testSet.intersect(inlineSet).isEmpty()) assertTrue(testSet.intersect(benchSet).isEmpty()) - assertTrue(inlineSet.intersect(benchSet).isEmpty()) } } diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt index 164c47b..0e83a36 100644 --- a/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/TestoLineMarkerCompanionTest.kt @@ -12,13 +12,6 @@ class TestoLineMarkerCompanionTest : TestCase() { } } - fun testRunnableAttributes_containsInlineTestAttributes() { - val runnable = TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.toSet() - for (attr in TestoClasses.TEST_INLINE_ATTRIBUTES) { - assertTrue("Missing inline test attribute: $attr", runnable.contains(attr)) - } - } - fun testRunnableAttributes_containsBenchAttributes() { val runnable = TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.toSet() for (attr in TestoClasses.BENCH_ATTRIBUTES) { @@ -40,7 +33,7 @@ class TestoLineMarkerCompanionTest : TestCase() { fun testRunnableAttributes_totalCount() { val expected = TestoClasses.DATA_ATTRIBUTES.size + - TestoClasses.TEST_INLINE_ATTRIBUTES.size + + TestoClasses.TEST_ATTRIBUTES.size + TestoClasses.BENCH_ATTRIBUTES.size assertEquals(expected, TestoTestRunLineMarkerProvider.RUNNABLE_ATTRIBUTES.size) } From 5e46dfdb60c2e5077acf85553c4605bea3e4f3fd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 11:32:12 +0000 Subject: [PATCH 10/24] Add --type flag for all attribute types: test, inline, bench MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve testoType from the specific attribute FQN: - #[Test], #[Data*] → --type=test - #[TestInline] → --type=inline - #[Bench] → --type=bench https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- .../run/TestoRunConfigurationProducer.kt | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt index ed006ec..986653e 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt @@ -1,12 +1,16 @@ package com.github.xepozz.testo.tests.run +import com.github.xepozz.testo.TestoClasses import com.github.xepozz.testo.TestoUtil +import com.github.xepozz.testo.hasAttribute import com.github.xepozz.testo.index.TestoDataProviderUtils import com.github.xepozz.testo.isTestoBench import com.github.xepozz.testo.isTestoClass import com.github.xepozz.testo.isTestoDataProviderLike import com.github.xepozz.testo.isTestoExecutable import com.github.xepozz.testo.isTestoFile +import com.github.xepozz.testo.isTestoFunction +import com.github.xepozz.testo.isTestoMethod import com.github.xepozz.testo.util.PsiUtil import com.intellij.execution.Location import com.intellij.execution.PsiLocation @@ -67,10 +71,7 @@ class TestoRunConfigurationProducer : PhpTestConfigurationProducer resolveTestoTypeFromAttribute(element) + element.isTestoBench() -> BENCH_TYPE + element.isTestoFunction() && (element as Function).hasAttribute(TestoClasses.TEST_INLINE) -> INLINE_TYPE + element.isTestoMethod() || element.isTestoFunction() -> TEST_TYPE + else -> "" + } + + private fun resolveTestoTypeFromAttribute(attribute: PhpAttribute): String { + val fqn = attribute.fqn ?: return "" + return when (fqn) { + in TestoClasses.BENCH_ATTRIBUTES -> BENCH_TYPE + TestoClasses.TEST_INLINE -> INLINE_TYPE + TestoClasses.TEST -> TEST_TYPE + in TestoClasses.DATA_ATTRIBUTES -> TEST_TYPE + else -> "" + } + } + val METHOD = Condition { it.isTestoExecutable() || (it is Method && TestoDataProviderUtils.isDataProvider(it)) } From b6afcb053e3a9dafc689dd69b6e5aff4c339ea39 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 11:38:38 +0000 Subject: [PATCH 11/24] Only set --type when running from attribute, not from function/method https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- .../xepozz/testo/tests/run/TestoRunConfigurationProducer.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt index 986653e..913caed 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt @@ -120,7 +120,6 @@ class TestoRunConfigurationProducer : PhpTestConfigurationProducer Date: Tue, 17 Mar 2026 11:42:47 +0000 Subject: [PATCH 12/24] Add tests for testoType: test, inline, bench and default empty - TestoRunConfigurationHandlerTest: verify --type=test, --type=inline, --type=bench flags, and no --type when testoType is empty/default - TestoRunnerSettingsTest: cover testoType in defaults, custom values, and fromPhpTestRunnerSettings copy https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- .../testo/TestoRunConfigurationHandlerTest.kt | 37 +++++++++++++++++++ .../xepozz/testo/TestoRunnerSettingsTest.kt | 6 +++ 2 files changed, 43 insertions(+) diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt index d239453..0c80089 100644 --- a/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/TestoRunConfigurationHandlerTest.kt @@ -64,6 +64,30 @@ class TestoRunConfigurationHandlerTest : TestCase() { assertEquals("bench", arguments[1]) } + fun testPrepareArguments_withTestoType_test() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.testoType = "test" + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertEquals(2, arguments.size) + assertEquals("--type", arguments[0]) + assertEquals("test", arguments[1]) + } + + fun testPrepareArguments_withTestoType_inline() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.testoType = "inline" + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertEquals(2, arguments.size) + assertEquals("--type", arguments[0]) + assertEquals("inline", arguments[1]) + } + fun testPrepareArguments_withTestoType_empty_skipped() { val settings = TestoRunConfigurationSettings() settings.runnerSettings.testoType = "" @@ -74,6 +98,19 @@ class TestoRunConfigurationHandlerTest : TestCase() { assertTrue("Empty testoType should not add arguments", arguments.isEmpty()) } + fun testPrepareArguments_defaultTestoType_noTypeFlag() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.suite = "unit" + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertEquals(2, arguments.size) + assertEquals("--suite", arguments[0]) + assertEquals("unit", arguments[1]) + assertFalse("No --type flag when testoType is default", arguments.contains("--type")) + } + fun testPrepareArguments_withSuite() { val settings = TestoRunConfigurationSettings() settings.runnerSettings.suite = "unit" diff --git a/src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt b/src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt index e45f019..74dde51 100644 --- a/src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/TestoRunnerSettingsTest.kt @@ -17,6 +17,7 @@ class TestoRunnerSettingsTest : TestCase() { assertEquals("", settings.excludeGroup) assertEquals(0, settings.repeat) assertEquals(0, settings.parallel) + assertEquals("", settings.testoType) } fun testCustomValues() { @@ -30,6 +31,7 @@ class TestoRunnerSettingsTest : TestCase() { excludeGroup = "slow", repeat = 3, parallel = 4, + testoType = "bench", ) assertEquals(3, settings.dataProviderIndex) assertEquals(5, settings.dataSetIndex) @@ -40,6 +42,7 @@ class TestoRunnerSettingsTest : TestCase() { assertEquals("slow", settings.excludeGroup) assertEquals(3, settings.repeat) assertEquals(4, settings.parallel) + assertEquals("bench", settings.testoType) } fun testFromPhpTestRunnerSettings_baseSettings() { @@ -71,6 +74,7 @@ class TestoRunnerSettingsTest : TestCase() { assertEquals("", result.excludeGroup) assertEquals(0, result.repeat) assertEquals(0, result.parallel) + assertEquals("", result.testoType) } fun testFromPhpTestRunnerSettings_testoSettings() { @@ -84,6 +88,7 @@ class TestoRunnerSettingsTest : TestCase() { excludeGroup = "slow", repeat = 5, parallel = 8, + testoType = "inline", ) source.scope = PhpTestRunnerSettings.Scope.Method source.filePath = "/test.php" @@ -105,5 +110,6 @@ class TestoRunnerSettingsTest : TestCase() { assertEquals("slow", result.excludeGroup) assertEquals(5, result.repeat) assertEquals(8, result.parallel) + assertEquals("inline", result.testoType) } } From 92a1b548005c1b509d7619e4ea3c180c7dce116c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 11:52:46 +0000 Subject: [PATCH 13/24] Add Run gutter icon for ApplicationConfig files (#39) Detect \Testo\Application\Config\ApplicationConfig class references and show a play button in the gutter. Clicking it creates a run configuration with --config= using the existing Scope.ConfigurationFile mechanism. https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- .../kotlin/com/github/xepozz/testo/TestoClasses.kt | 2 ++ .../testo/tests/TestoTestRunLineMarkerProvider.kt | 4 ++++ .../testo/tests/run/TestoRunConfigurationProducer.kt | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt b/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt index 8092d9d..c38cafe 100644 --- a/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt +++ b/src/main/kotlin/com/github/xepozz/testo/TestoClasses.kt @@ -12,6 +12,8 @@ object TestoClasses { const val BENCH = "\\Testo\\Bench" + const val APPLICATION_CONFIG = "\\Testo\\Application\\Config\\ApplicationConfig" + const val ASSERT = "\\Testo\\Assert" const val ASSERTION_EXCEPTION = "\\Testo\\Assert\\State\\Assertion\\AssertionException" const val EXPECT = "\\Testo\\Expect" diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt b/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt index bd8af02..2e07b47 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt @@ -49,6 +49,10 @@ class TestoTestRunLineMarkerProvider : RunLineMarkerContributor() { val element = leaf.parent as? PhpPsiElement ?: return null return when { + element is ClassReference && element.fqn == TestoClasses.APPLICATION_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 diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt index 913caed..7a1d2ae 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt @@ -33,6 +33,7 @@ import com.jetbrains.php.PhpBundle import com.jetbrains.php.PhpIndex import com.jetbrains.php.PhpIndexImpl import com.jetbrains.php.lang.psi.PhpFile +import com.jetbrains.php.lang.psi.elements.ClassReference import com.jetbrains.php.lang.psi.elements.Function import com.jetbrains.php.lang.psi.elements.Method import com.jetbrains.php.lang.psi.elements.PhpAttribute @@ -62,6 +63,12 @@ class TestoRunConfigurationProducer : PhpTestConfigurationProducer false @@ -273,6 +284,7 @@ class TestoRunConfigurationProducer : PhpTestConfigurationProducer target.takeIf { it.fqn == TestoClasses.APPLICATION_CONFIG } is PhpAttribute -> target.takeIf { it.owner.isTestoExecutable() || it.owner.isTestoDataProviderLike() } is Function -> target.takeIf { it.isTestoExecutable() || it.isTestoDataProviderLike() } is PhpClass -> target.takeIf { it.isTestoClass() } From 809910f5357b0cf0ce83c12e33c27c4002fd2d80 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Mar 2026 11:58:13 +0000 Subject: [PATCH 14/24] Fix: gutter icon only on `new ApplicationConfig`, not `use` statement - Restrict line marker and producer to ClassReference inside NewExpression (not use/import statements) - Add isTestoConfigFile() to mixin.kt for detecting config files - Include config files in isTestoFile() so right-click Run works - Handle PhpFile config in setupConfiguration with --config flag https://claude.ai/code/session_01TmFvjfMtoxTvddQRBzsbPN --- src/main/kotlin/com/github/xepozz/testo/mixin.kt | 7 ++++++- .../testo/tests/TestoTestRunLineMarkerProvider.kt | 3 ++- .../tests/run/TestoRunConfigurationProducer.kt | 14 +++++++++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/github/xepozz/testo/mixin.kt b/src/main/kotlin/com/github/xepozz/testo/mixin.kt index 7e719bd..5d64112 100644 --- a/src/main/kotlin/com/github/xepozz/testo/mixin.kt +++ b/src/main/kotlin/com/github/xepozz/testo/mixin.kt @@ -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() @@ -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() } diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt b/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt index 2e07b47..a8526e5 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/TestoTestRunLineMarkerProvider.kt @@ -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 @@ -49,7 +50,7 @@ class TestoTestRunLineMarkerProvider : RunLineMarkerContributor() { val element = leaf.parent as? PhpPsiElement ?: return null return when { - element is ClassReference && element.fqn == TestoClasses.APPLICATION_CONFIG -> { + element is ClassReference && element.parent is NewExpression && element.fqn == TestoClasses.APPLICATION_CONFIG -> { getLocationHint(element.containingFile) } diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt index 7a1d2ae..2cd00a3 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoRunConfigurationProducer.kt @@ -8,6 +8,7 @@ import com.github.xepozz.testo.isTestoBench import com.github.xepozz.testo.isTestoClass import com.github.xepozz.testo.isTestoDataProviderLike import com.github.xepozz.testo.isTestoExecutable +import com.github.xepozz.testo.isTestoConfigFile import com.github.xepozz.testo.isTestoFile import com.github.xepozz.testo.isTestoFunction import com.github.xepozz.testo.isTestoMethod @@ -36,6 +37,7 @@ import com.jetbrains.php.lang.psi.PhpFile import com.jetbrains.php.lang.psi.elements.ClassReference import com.jetbrains.php.lang.psi.elements.Function import com.jetbrains.php.lang.psi.elements.Method +import com.jetbrains.php.lang.psi.elements.NewExpression import com.jetbrains.php.lang.psi.elements.PhpAttribute import com.jetbrains.php.lang.psi.elements.PhpClass import com.jetbrains.php.lang.psi.elements.PhpNamedElement @@ -63,7 +65,7 @@ class TestoRunConfigurationProducer : PhpTestConfigurationProducer target.takeIf { it.fqn == TestoClasses.APPLICATION_CONFIG } + is ClassReference -> target.takeIf { it.parent is NewExpression && it.fqn == TestoClasses.APPLICATION_CONFIG } is PhpAttribute -> target.takeIf { it.owner.isTestoExecutable() || it.owner.isTestoDataProviderLike() } is Function -> target.takeIf { it.isTestoExecutable() || it.isTestoDataProviderLike() } is PhpClass -> target.takeIf { it.isTestoClass() } From fe180dc50b6342d77a4121e3f1391eec90e90fe9 Mon Sep 17 00:00:00 2001 From: Dmitriy Derepko Date: Tue, 17 Mar 2026 16:12:23 +0400 Subject: [PATCH 15/24] feat: add bench method live template --- src/main/resources/liveTemplates/Testo.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/resources/liveTemplates/Testo.xml b/src/main/resources/liveTemplates/Testo.xml index 31456e7..35de52f 100644 --- a/src/main/resources/liveTemplates/Testo.xml +++ b/src/main/resources/liveTemplates/Testo.xml @@ -39,4 +39,24 @@