diff --git a/CHANGELOG.md b/CHANGELOG.md index 091b9755..94079a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ## [Unreleased] +### Added + +- The run configuration's Type filter takes several types as tags, each passed as its own `--type`; existing configurations keep their type. + ### Fixed - List tests sharing a data provider in source order for a consistent default selection. diff --git a/CLAUDE.md b/CLAUDE.md index 8966cfd4..3d5d91c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -337,8 +337,8 @@ Requires IDEA Ultimate or PhpStorm — the plugin cannot load without PHP suppor The `--teamcity` flag is what makes Testo emit the SM service messages this plugin parses. - Runner flags from `TestoRunnerSettings` (only emitted when non-empty): `--type`, `--suite`, `--group`, plus one `--filter ` per entry in `rerunFilters` (no `--parallel` — Testo's CLI does not take it yet). - `suites`, `groups`/`excludeGroups` are persisted - **lists** (`@XCollection`), one `--group` flag each — Testo ORs repeated `--group`s and reads a `!name` prefix as an + `testoTypes`, `suites`, `groups`/`excludeGroups` are persisted + **lists** (`@XCollection`), one flag each — Testo ORs repeated `--type`s and `--group`s and reads a `!name` prefix as an exclusion, which is the only exclusion form its CLI has. A name is opaque: whatever `#[Group]` spells reaches the CLI untouched. - `--log-html`/`--log-junit` at an IDE-managed path (`TestoReportFlags`, both on by default), @@ -654,7 +654,7 @@ Non-obvious constraints already paid for in blood — read before touching the r `RunTab.TopToolbar`, which the debug tab does not use — so the action's hide branch is gated on *not* the Debug executor. A descriptor's restart actions are constructor-only, so this cannot be fixed by handing the debug `RunContentDescriptor` its own. -- **Suite and group names are lists everywhere.** `TestoRunnerSettings` persists `suites`/`groups`/`excludeGroups` +- **Type, suite and group names are lists everywhere.** `TestoRunnerSettings` persists `testoTypes`/`suites`/`groups`/`excludeGroups` via `@XCollection`, the editor shows them as tags (`TestoTagsField`), and a name is never split on anything — the comma lives only in the legacy persisted form. `migrateLegacyNames` folds that form in, called from `TestoRunConfigurationSettings.getTestoRunnerSettings` — the first point every reader passes after deserialization. diff --git a/src/main/kotlin/com/github/xepozz/testo/launch/TestoCommandLine.kt b/src/main/kotlin/com/github/xepozz/testo/launch/TestoCommandLine.kt index 16625be7..1e1ecbbf 100644 --- a/src/main/kotlin/com/github/xepozz/testo/launch/TestoCommandLine.kt +++ b/src/main/kotlin/com/github/xepozz/testo/launch/TestoCommandLine.kt @@ -12,9 +12,9 @@ object TestoCommandLine { /** `--type`, `--suite`, `--group`, excluded `--group !…` and rerun `--filter`, in that order. */ fun selectionArguments(selection: TestoRunSelection): List = buildList { - if (selection.testoType.isNotEmpty()) { + for (type in selection.testoTypes) { add("--type") - add(selection.testoType) + add(type) } for (suite in selection.suites) { add("--suite") diff --git a/src/main/kotlin/com/github/xepozz/testo/launch/TestoConfigurationNames.kt b/src/main/kotlin/com/github/xepozz/testo/launch/TestoConfigurationNames.kt index 4febe654..eaec82db 100644 --- a/src/main/kotlin/com/github/xepozz/testo/launch/TestoConfigurationNames.kt +++ b/src/main/kotlin/com/github/xepozz/testo/launch/TestoConfigurationNames.kt @@ -28,7 +28,11 @@ object TestoConfigurationNames { val quoted = suites.joinToString(", ") { "'$it'" } return if (suites.size == 1) "Suite $quoted" else "Suites $quoted" } - if (selection.testoType.isNotEmpty()) return "Type '${selection.testoType}'" + val types = selection.testoTypes + if (types.isNotEmpty()) { + val quoted = types.joinToString(", ") { "'$it'" } + return if (types.size == 1) "Type $quoted" else "Types $quoted" + } } qualifiedMethodTail(selection)?.let { @@ -63,7 +67,7 @@ object TestoConfigurationNames { return selection.groups.isNotEmpty() || selection.excludeGroups.isNotEmpty() || selection.suites.isNotEmpty() - || selection.testoType.isNotEmpty() + || selection.testoTypes.isNotEmpty() || selection.rerunFilters.isNotEmpty() } } diff --git a/src/main/kotlin/com/github/xepozz/testo/launch/TestoRunContexts.kt b/src/main/kotlin/com/github/xepozz/testo/launch/TestoRunContexts.kt index d56054ef..7bedbe2f 100644 --- a/src/main/kotlin/com/github/xepozz/testo/launch/TestoRunContexts.kt +++ b/src/main/kotlin/com/github/xepozz/testo/launch/TestoRunContexts.kt @@ -80,7 +80,7 @@ class TestoRunContexts(private val host: Host) { // where it lives. ConfigurationFile scope is what keeps the path/filter flags out of the command line. selection.scope = TestoScope.CONFIGURATION_FILE selection.groups = groups - selection.testoType = "" + selection.testoTypes = emptyList() selection.dataProviderIndex = -1 selection.dataSetIndex = -1 return element @@ -92,7 +92,7 @@ class TestoRunContexts(private val host: Host) { val phpClass = view.owner!! if (!phpClass.isTestoClass()) return null setup(selection, phpClass, element.containingFile.virtualFile) ?: return null - selection.testoType = testoTypeOf(element) + selection.testoTypes = typesOf(testoTypeOf(element)) return element } if (view is PhpAttributeView) { @@ -107,7 +107,7 @@ class TestoRunContexts(private val host: Host) { if (index == -1) { selection.dataProviderIndex = -1 selection.dataSetIndex = -1 - selection.testoType = testoTypeOf(element) + selection.testoTypes = typesOf(testoTypeOf(element)) return element } @@ -115,7 +115,7 @@ class TestoRunContexts(private val host: Host) { selection.methodName += ":$index" selection.dataProviderIndex = index selection.dataSetIndex = -1 - selection.testoType = testoTypeOf(element) + selection.testoTypes = typesOf(testoTypeOf(element)) return element } @@ -216,7 +216,7 @@ class TestoRunContexts(private val host: Host) { private fun classMatches(selection: TestoRunSelection, phpClass: PsiElement, testoType: String): Boolean = selection.scope == TestoScope.FILE && selection.filePath == phpClass.containingFile.virtualFile.path - && selection.testoType == testoType + && selection.testoTypes == typesOf(testoType) /** * Whether [selection] is the run of a results-tree node announced as [target]; null to leave it to the @@ -227,7 +227,7 @@ class TestoRunContexts(private val host: Host) { // announced with, and reusing it would run the whole file. The match is therefore exact on all three: a class // node and a method node of the same class agree on suite and type, so those alone are not enough. if (selection.suites != listOfNotNull(target.suite?.takeIf { it.isNotBlank() })) return false - if (selection.testoType != target.type.orEmpty()) return false + if (selection.testoTypes != typesOf(target.type.orEmpty())) return false target.filter?.let { return selection.scope == TestoScope.METHOD && selection.methodName == it @@ -238,7 +238,7 @@ class TestoRunContexts(private val host: Host) { val found = element?.let { findTestElement(it) } if (found != null && php.view(found) is PhpClassView) return classMatches(selection, found, target.type.orEmpty()) - // A free function falls through untyped: its element-based check never compares testoType, so once Testo + // A free function falls through untyped: its element-based check never compares types, so once Testo // sends `testType` a typed function configuration could pass for an untyped one — the mirror of the class // case above. Latent until then: without the attributes such a target is empty and never stored. return null @@ -316,6 +316,9 @@ class TestoRunContexts(private val host: Host) { } } + /** The `--type` list of a run narrowed to [type], empty for none. */ + fun typesOf(type: String): List = listOfNotNull(type.takeIf { it.isNotEmpty() }) + private fun testoTypeOfAttribute(attribute: PhpAttributeView): String { val fqn = attribute.fqn ?: return "" return when (fqn) { @@ -347,7 +350,7 @@ class TestoRunContexts(private val host: Host) { /** Lays what a results-tree node announced over the run its source element produced. */ fun applyTreeTarget(selection: TestoRunSelection, target: TestoRunTarget) { target.suite?.takeIf { it.isNotBlank() }?.let { selection.suites = listOf(it) } - target.type?.takeIf { it.isNotBlank() }?.let { selection.testoType = it } + target.type?.takeIf { it.isNotBlank() }?.let { selection.testoTypes = listOf(it) } // The whole selector, class and all: a bare method name would also match a namesake in the same file, and // a case needs it too, since `--path` alone runs every case the file declares. `--path` stays as it was. diff --git a/src/main/kotlin/com/github/xepozz/testo/launch/TestoRunSelection.kt b/src/main/kotlin/com/github/xepozz/testo/launch/TestoRunSelection.kt index 8a0b3ff5..064624e1 100644 --- a/src/main/kotlin/com/github/xepozz/testo/launch/TestoRunSelection.kt +++ b/src/main/kotlin/com/github/xepozz/testo/launch/TestoRunSelection.kt @@ -21,7 +21,7 @@ data class TestoRunSelection( var configurationFilePath: String? = null, var testRunnerOptions: String? = null, var command: String = "run", - var testoType: String = "", + var testoTypes: List = emptyList(), var suites: List = emptyList(), var groups: List = emptyList(), var excludeGroups: List = emptyList(), @@ -60,7 +60,7 @@ data class TestoRunSelection( configurationFilePath = other.configurationFilePath testRunnerOptions = other.testRunnerOptions command = other.command - testoType = other.testoType + testoTypes = other.testoTypes suites = other.suites groups = other.groups excludeGroups = other.excludeGroups diff --git a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoTagsField.kt b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoTagsField.kt index c3bfa9b4..3a5fac5a 100644 --- a/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoTagsField.kt +++ b/src/main/kotlin/com/github/xepozz/testo/tests/run/TestoTagsField.kt @@ -21,7 +21,7 @@ import javax.swing.SwingUtilities import javax.swing.border.CompoundBorder /** - * A list of names shown as removable tags. A suite or group name is opaque to the toolchain — whatever the CLI is + * A list of names shown as removable tags. A suite, group or type name is opaque to the toolchain — whatever the CLI is * handed is what it selects — so a free-form field had no separator it could safely own; tags remove the question. */ class TestoTagsField( diff --git a/src/main/resources/messages/TestoBundle.properties b/src/main/resources/messages/TestoBundle.properties index ead12cd1..843f25fa 100644 --- a/src/main/resources/messages/TestoBundle.properties +++ b/src/main/resources/messages/TestoBundle.properties @@ -166,6 +166,8 @@ testo.tags.groups.add=Add a group testo.tags.groups.exclude.add=Exclude a group testo.tags.suites.empty=No suites testo.tags.suites.add=Suite name, Enter to add +testo.tags.types.empty=No types +testo.tags.types.add=Type name, Enter to add action.testo.mutate.description=Run Infection on this run's coverage and JUnit reports, without re-running the tests infection.missing.notFinished=Mutation testing starts from a finished run with Coverage and the coverage-xml and JUnit reports on infection.missing.pending=Mutation testing starts once the tests of this run pass diff --git a/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/PhpStormRunSelection.kt b/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/PhpStormRunSelection.kt index 2da024ad..fff646fd 100644 --- a/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/PhpStormRunSelection.kt +++ b/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/PhpStormRunSelection.kt @@ -7,7 +7,7 @@ import com.github.xepozz.testo.phpstorm.tests.run.TestoRunnerSettings import com.jetbrains.php.phpunit.coverage.PhpUnitCoverageEngine.CoverageEngine import com.jetbrains.php.testFramework.run.PhpTestRunnerSettings.Scope -/** The saved settings as a [TestoRunSelection]. The pre-list `group`/`exclude_group`/`suite` forms are not part of it. */ +/** The saved settings as a [TestoRunSelection]. The pre-list `group`/`exclude_group`/`suite`/`testo_type` forms are not part of it. */ fun TestoRunnerSettings.toSelection() = TestoRunSelection( scope = scope.toTesto(), selectedType = selectedType, @@ -18,7 +18,7 @@ fun TestoRunnerSettings.toSelection() = TestoRunSelection( configurationFilePath = configurationFilePath, testRunnerOptions = testRunnerOptions, command = command, - testoType = testoType, + testoTypes = testoTypes.toList(), suites = suites.toList(), groups = groups.toList(), excludeGroups = excludeGroups.toList(), @@ -57,7 +57,7 @@ fun TestoRunnerSettings.load(selection: TestoRunSelection) { configurationFilePath = selection.configurationFilePath testRunnerOptions = selection.testRunnerOptions command = selection.command - testoType = selection.testoType + testoTypes = selection.testoTypes.toMutableList() suites = selection.suites.toMutableList() groups = selection.groups.toMutableList() excludeGroups = selection.excludeGroups.toMutableList() diff --git a/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/tests/run/TestoRunnerSettings.kt b/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/tests/run/TestoRunnerSettings.kt index 2447d306..f74b977a 100644 --- a/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/tests/run/TestoRunnerSettings.kt +++ b/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/tests/run/TestoRunnerSettings.kt @@ -22,9 +22,6 @@ class TestoRunnerSettings( @Attribute("parallel") var parallel: Int = 1, - @Attribute("testo_type") - var testoType: String = "", - // Which coverage reports a Coverage run requests via CLI flags. Clover is off by default: cobertura carries // the same lines and branches. coverage-xml adds the per-test overlay. @Attribute("coverage_clover") @@ -84,6 +81,10 @@ class TestoRunnerSettings( @Attribute("infection_options") var infectionOptions: String = "", ) : PhpTestRunnerSettings() { + /** Types to run, one `--type` flag each (Testo ORs them); a `!` prefix excludes the type. */ + @get:XCollection(propertyElementName = "testo_types", style = XCollection.Style.v2) + var testoTypes: MutableList = mutableListOf() + /** Suite names to run, one `--suite` flag each (Testo ORs them). A name is opaque — spaces and all. */ @get:XCollection(propertyElementName = "suites", style = XCollection.Style.v2) var suites: MutableList = mutableListOf() @@ -111,6 +112,10 @@ class TestoRunnerSettings( @get:Attribute("suite") var legacySuite: String = "" + /** The single-type persisted form of [testoTypes]; see [legacySuite]. */ + @get:Attribute("testo_type") + var legacyTestoType: String = "" + // Set only on a "Rerun Failed Tests" clone, never persisted to the saved configuration. @get:Transient var rerunFilters: List = emptyList() @@ -129,6 +134,10 @@ class TestoRunnerSettings( suites = mutableListOf(legacySuite) legacySuite = "" } + if (legacyTestoType.isNotEmpty()) { + testoTypes = mutableListOf(legacyTestoType) + legacyTestoType = "" + } } fun copyInfectionFrom(other: TestoRunnerSettings) { @@ -208,8 +217,9 @@ class TestoRunnerSettings( runnerSettings.legacyGroup = settings.legacyGroup runnerSettings.legacyExcludeGroup = settings.legacyExcludeGroup runnerSettings.legacySuite = settings.legacySuite + runnerSettings.legacyTestoType = settings.legacyTestoType runnerSettings.parallel = settings.parallel - runnerSettings.testoType = settings.testoType + runnerSettings.testoTypes = settings.testoTypes.toMutableList() runnerSettings.coverageClover = settings.coverageClover runnerSettings.coverageCobertura = settings.coverageCobertura runnerSettings.coverageXml = settings.coverageXml diff --git a/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/tests/run/TestoTestRunConfigurationEditor.kt b/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/tests/run/TestoTestRunConfigurationEditor.kt index 7b351133..3b59923e 100644 --- a/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/tests/run/TestoTestRunConfigurationEditor.kt +++ b/src/phpstorm/kotlin/com/github/xepozz/testo/phpstorm/tests/run/TestoTestRunConfigurationEditor.kt @@ -35,6 +35,10 @@ class TestoTestRunConfigurationEditor( private val parentEditor: PhpTestRunConfigurationEditor, val configuration: TestoRunConfiguration ) : SettingsEditor() { + private val typeField = TestoTagsField( + TestoBundle.message("testo.tags.types.empty"), + TestoBundle.message("testo.tags.types.add"), + ) private val suiteField = TestoTagsField( TestoBundle.message("testo.tags.suites.empty"), TestoBundle.message("testo.tags.suites.add"), @@ -108,6 +112,15 @@ class TestoTestRunConfigurationEditor( } group("Filter") { + row { + label("Type") + .gap(RightGap.COLUMNS) + cell(typeField) + .align(AlignX.FILL) + } + .layout(RowLayout.PARENT_GRID) + .rowComment("One --type= per tag, e.g. test, inline, bench; ! excludes the type") + row { label("Suite") .gap(RightGap.COLUMNS) @@ -255,6 +268,7 @@ class TestoTestRunConfigurationEditor( override fun textChanged(e: DocumentEvent) = listener() } + typeField.addChangeListener(listener) suiteField.addChangeListener(listener) groupField.addChangeListener(listener) excludeGroupField.addChangeListener(listener) @@ -282,7 +296,8 @@ class TestoTestRunConfigurationEditor( override fun isSpecificallyModified(): Boolean { val runner = configuration.testoSettings.runnerSettings - return suiteField.names != runner.suites + return typeField.names != runner.testoTypes + || suiteField.names != runner.suites || groupField.names != runner.groups || excludeGroupField.names != runner.excludeGroups || (parallelField.value as Int) != runner.parallel @@ -308,6 +323,7 @@ class TestoTestRunConfigurationEditor( override fun resetEditorFrom(testoRunConfiguration: TestoRunConfiguration) { val runnerSettings = testoRunConfiguration.testoSettings.runnerSettings + typeField.names = runnerSettings.testoTypes suiteField.names = runnerSettings.suites groupField.names = runnerSettings.groups excludeGroupField.names = runnerSettings.excludeGroups @@ -350,6 +366,7 @@ class TestoTestRunConfigurationEditor( } ?: parentEditor.applyTo(testoRunConfiguration) val runnerSettings = testoRunConfiguration.testoSettings.runnerSettings + runnerSettings.testoTypes = typeField.names.toMutableList() runnerSettings.suites = suiteField.names.toMutableList() runnerSettings.groups = groupField.names.toMutableList() runnerSettings.excludeGroups = excludeGroupField.names.toMutableList() diff --git a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/PhpStormRunSelectionTest.kt b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/PhpStormRunSelectionTest.kt index a2e42590..61c1677b 100644 --- a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/PhpStormRunSelectionTest.kt +++ b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/PhpStormRunSelectionTest.kt @@ -22,7 +22,7 @@ class PhpStormRunSelectionTest : TestCase() { configurationFilePath = "/work/app/testo.php", testRunnerOptions = "-q --stop-on-failure", command = "list", - testoType = "bench", + testoTypes = listOf("bench"), suites = listOf("Unit", "My, Suite"), groups = listOf("db", "a,b"), excludeGroups = listOf("slow"), diff --git a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunConfigurationHandlerTest.kt b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunConfigurationHandlerTest.kt index ea68c526..970e6d7b 100644 --- a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunConfigurationHandlerTest.kt +++ b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunConfigurationHandlerTest.kt @@ -55,7 +55,7 @@ class TestoRunConfigurationHandlerTest : TestCase() { fun testPrepareArguments_withTestoType_bench() { val settings = TestoRunConfigurationSettings() - settings.runnerSettings.testoType = "bench" + settings.runnerSettings.testoTypes = mutableListOf("bench") val arguments = mutableListOf() TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) @@ -67,7 +67,7 @@ class TestoRunConfigurationHandlerTest : TestCase() { fun testPrepareArguments_withTestoType_test() { val settings = TestoRunConfigurationSettings() - settings.runnerSettings.testoType = "test" + settings.runnerSettings.testoTypes = mutableListOf("test") val arguments = mutableListOf() TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) @@ -77,9 +77,19 @@ class TestoRunConfigurationHandlerTest : TestCase() { assertEquals("test", arguments[1]) } + fun testPrepareArguments_severalTypes_oneFlagEach() { + val settings = TestoRunConfigurationSettings() + settings.runnerSettings.testoTypes = mutableListOf("test", "inline", "!bench") + val arguments = mutableListOf() + + TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) + + assertEquals(listOf("--type", "test", "--type", "inline", "--type", "!bench"), arguments) + } + fun testPrepareArguments_withTestoType_inline() { val settings = TestoRunConfigurationSettings() - settings.runnerSettings.testoType = "inline" + settings.runnerSettings.testoTypes = mutableListOf("inline") val arguments = mutableListOf() TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) @@ -91,12 +101,12 @@ class TestoRunConfigurationHandlerTest : TestCase() { fun testPrepareArguments_withTestoType_empty_skipped() { val settings = TestoRunConfigurationSettings() - settings.runnerSettings.testoType = "" + settings.runnerSettings.testoTypes = mutableListOf() val arguments = mutableListOf() TestoRunConfigurationHandler.INSTANCE.prepareArguments(arguments, settings) - assertTrue("Empty testoType should not add arguments", arguments.isEmpty()) + assertTrue("Empty testoTypes should not add arguments", arguments.isEmpty()) } fun testPrepareArguments_defaultTestoType_noTypeFlag() { @@ -109,7 +119,7 @@ class TestoRunConfigurationHandlerTest : TestCase() { assertEquals(2, arguments.size) assertEquals("--suite", arguments[0]) assertEquals("unit", arguments[1]) - assertFalse("No --type flag when testoType is default", arguments.contains("--type")) + assertFalse("No --type flag when testoTypes is default", arguments.contains("--type")) } fun testPrepareArguments_withSuite() { @@ -226,7 +236,7 @@ class TestoRunConfigurationHandlerTest : TestCase() { fun testPrepareArguments_allOptions() { val settings = TestoRunConfigurationSettings() - settings.runnerSettings.testoType = "bench" + settings.runnerSettings.testoTypes = mutableListOf("bench") settings.runnerSettings.suites = mutableListOf("integration") settings.runnerSettings.groups = mutableListOf("db") settings.runnerSettings.excludeGroups = mutableListOf("slow") @@ -301,7 +311,7 @@ class TestoRunConfigurationHandlerTest : TestCase() { fun testPrepareArguments_orderIsCorrect() { val settings = TestoRunConfigurationSettings() - settings.runnerSettings.testoType = "bench" + settings.runnerSettings.testoTypes = mutableListOf("bench") settings.runnerSettings.suites = mutableListOf("unit") settings.runnerSettings.groups = mutableListOf("fast") val arguments = mutableListOf() diff --git a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunConfigurationProducerPsiTest.kt b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunConfigurationProducerPsiTest.kt index 68314d88..025ca318 100644 --- a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunConfigurationProducerPsiTest.kt +++ b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunConfigurationProducerPsiTest.kt @@ -51,7 +51,7 @@ class TestoRunConfigurationProducerPsiTest : BasePlatformTestCase() { val result = producer.setupConfiguration(settings, attribute, attribute.containingFile.virtualFile) assertNotNull("#[Test] attribute must produce a run configuration", result) - assertEquals("#[Test] runs with --type=test", "test", settings.testoType) + assertEquals("#[Test] runs with --type=test", listOf("test"), settings.testoTypes) assertEquals("#[Test] is not a data provider", -1, settings.dataProviderIndex) assertEquals("#[Test] is not a dataset", -1, settings.dataSetIndex) assertFalse( @@ -85,7 +85,7 @@ class TestoRunConfigurationProducerPsiTest : BasePlatformTestCase() { val result = producer.setupConfiguration(settings, attribute, attribute.containingFile.virtualFile) assertNotNull("#[Test] on a DataProvider method must still produce a config", result) - assertEquals("#[Test] runs with --type=test", "test", settings.testoType) + assertEquals("#[Test] runs with --type=test", listOf("test"), settings.testoTypes) assertFalse( "#[Test] must NOT inherit a :index suffix from sibling DataProvider, was '${settings.methodName}'", settings.methodName.matches(Regex(".*:\\d+$")) @@ -117,7 +117,7 @@ class TestoRunConfigurationProducerPsiTest : BasePlatformTestCase() { val result = producer.setupConfiguration(settings, attribute, attribute.containingFile.virtualFile) assertNotNull("#[DataProvider] attribute must produce a run configuration", result) - assertEquals("#[DataProvider] runs with --type=test", "test", settings.testoType) + assertEquals("#[DataProvider] runs with --type=test", listOf("test"), settings.testoTypes) assertEquals("First data attribute has index 0", 0, settings.dataProviderIndex) assertEquals("Not a dataset run", -1, settings.dataSetIndex) assertTrue( @@ -144,8 +144,8 @@ class TestoRunConfigurationProducerPsiTest : BasePlatformTestCase() { assertNotNull("#[TestRectorFixtures] must produce a run configuration", result) assertEquals( "A rule's fixtures run as the synthesized rector-fixture type", - TestoRunConfigurationProducer.RECTOR_FIXTURE_TYPE, - settings.testoType, + listOf(TestoRunConfigurationProducer.RECTOR_FIXTURE_TYPE), + settings.testoTypes, ) assertEquals("The case is the class, so it is run through its file", Scope.File, settings.scope) assertTrue("No method selector for a case class", settings.methodName.isNullOrEmpty()) @@ -166,7 +166,7 @@ class TestoRunConfigurationProducerPsiTest : BasePlatformTestCase() { val result = producer.setupConfiguration(settings, phpClass, psiFile.virtualFile) assertNotNull("Running the rule class itself must produce a configuration", result) - assertEquals("Running a class runs everything it holds — no type filter", "", settings.testoType) + assertEquals("Running a class runs everything it holds — no type filter", emptyList(), settings.testoTypes) assertEquals(Scope.File, settings.scope) } @@ -180,7 +180,7 @@ class TestoRunConfigurationProducerPsiTest : BasePlatformTestCase() { val settings = TestoRunnerSettings() producer.setupConfiguration(settings, phpClass, psiFile.virtualFile) - assertEquals("A plain test class run stays untyped", "", settings.testoType) + assertEquals("A plain test class run stays untyped", emptyList(), settings.testoTypes) } fun testSetupConfiguration_classWithTestAttribute_keepsNoTestoType() { @@ -202,7 +202,7 @@ class TestoRunConfigurationProducerPsiTest : BasePlatformTestCase() { // `--type=test` here would silently drop the class's #[Bench] methods from the run; that narrowing is only // applied when the run starts from the #[Test] attribute itself. - assertEquals("Running the class itself must stay untyped", "", settings.testoType) + assertEquals("Running the class itself must stay untyped", emptyList(), settings.testoTypes) } fun testIsConfigurationFromContext_typedAndUntypedClassRunsAreDifferentContexts() { @@ -251,7 +251,7 @@ class TestoRunConfigurationProducerPsiTest : BasePlatformTestCase() { assertNotNull("#[Test] on a class runs that class", result) assertEquals(Scope.File, settings.scope) - assertEquals("Running from the attribute narrows to its type", "test", settings.testoType) + assertEquals("Running from the attribute narrows to its type", listOf("test"), settings.testoTypes) } // ---- #[Group] — run everything in that group, nothing else ---- @@ -281,7 +281,7 @@ class TestoRunConfigurationProducerPsiTest : BasePlatformTestCase() { ) assertTrue("No path selector for a group run", settings.filePath.isNullOrEmpty()) assertTrue("No method selector for a group run", settings.methodName.isNullOrEmpty()) - assertEquals("No --type for a group run", "", settings.testoType) + assertEquals("No --type for a group run", emptyList(), settings.testoTypes) assertEquals(-1, settings.dataProviderIndex) assertEquals(-1, settings.dataSetIndex) } diff --git a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunnerSettingsSerializationTest.kt b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunnerSettingsSerializationTest.kt index 1c6d3014..ff3f3509 100644 --- a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunnerSettingsSerializationTest.kt +++ b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunnerSettingsSerializationTest.kt @@ -112,6 +112,19 @@ class TestoRunnerSettingsSerializationTest : TestCase() { assertFalse("The legacy attribute is gone after a save", serialize(restored).contains("suite=\"")) } + fun testLegacySingleTypeBecomesAOneItemList() { + val restored = deserialize("""""") + + restored.migrateLegacyNames() + + assertEquals(listOf("bench"), restored.testoTypes) + assertEquals("", restored.legacyTestoType) + val xml = serialize(restored) + assertFalse("The legacy attribute is gone after a save", xml.contains("testo_type=\"")) + assertTrue(xml.contains("")) + assertEquals(listOf("bench"), deserialize(xml).testoTypes) + } + fun testMigrationIsIdempotentAndKeepsExistingLists() { val settings = TestoRunnerSettings().apply { groups = mutableListOf("db") } diff --git a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunnerSettingsTest.kt b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunnerSettingsTest.kt index 47b3c67d..e059a0fc 100644 --- a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunnerSettingsTest.kt +++ b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/TestoRunnerSettingsTest.kt @@ -16,7 +16,7 @@ class TestoRunnerSettingsTest : TestCase() { assertTrue(settings.groups.isEmpty()) assertTrue(settings.excludeGroups.isEmpty()) assertEquals(1, settings.parallel) - assertEquals("", settings.testoType) + assertTrue(settings.testoTypes.isEmpty()) assertEquals("auto", settings.coverageLevel) assertEquals("--type=!bench", settings.coverageOptions) assertTrue("rerunFilters defaults to empty", settings.rerunFilters.isEmpty()) @@ -29,8 +29,8 @@ class TestoRunnerSettingsTest : TestCase() { parallelTestingEnabled = true, command = "list", parallel = 4, - testoType = "bench", ).apply { + testoTypes = mutableListOf("bench") suites = mutableListOf("unit") groups = mutableListOf("fast") excludeGroups = mutableListOf("slow") @@ -43,7 +43,7 @@ class TestoRunnerSettingsTest : TestCase() { assertEquals(listOf("fast"), settings.groups) assertEquals(listOf("slow"), settings.excludeGroups) assertEquals(4, settings.parallel) - assertEquals("bench", settings.testoType) + assertEquals(listOf("bench"), settings.testoTypes) } fun testFromPhpTestRunnerSettings_baseSettings() { @@ -74,7 +74,7 @@ class TestoRunnerSettingsTest : TestCase() { assertTrue(result.groups.isEmpty()) assertTrue(result.excludeGroups.isEmpty()) assertEquals(1, result.parallel) - assertEquals("", result.testoType) + assertTrue(result.testoTypes.isEmpty()) } fun testFromPhpTestRunnerSettings_testoSettings() { @@ -84,10 +84,10 @@ class TestoRunnerSettingsTest : TestCase() { parallelTestingEnabled = true, command = "debug", parallel = 8, - testoType = "inline", coverageLevel = "branch", coverageOptions = "--type=!bench", ) + source.testoTypes = mutableListOf("inline") source.suites = mutableListOf("integration") source.groups = mutableListOf("database") source.excludeGroups = mutableListOf("slow") @@ -111,7 +111,7 @@ class TestoRunnerSettingsTest : TestCase() { assertEquals(listOf("database"), result.groups) assertEquals(listOf("slow"), result.excludeGroups) assertEquals(8, result.parallel) - assertEquals("inline", result.testoType) + assertEquals(listOf("inline"), result.testoTypes) assertEquals("branch", result.coverageLevel) assertEquals("--type=!bench", result.coverageOptions) // rerunFilters is @Transient and intentionally NOT copied — stays empty on the result. diff --git a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/Baselines.kt b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/Baselines.kt index fcfff1f0..64235bc9 100644 --- a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/Baselines.kt +++ b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/Baselines.kt @@ -18,7 +18,7 @@ object Baselines { appendLine(" configurationFilePath=${settings.configurationFilePath}") appendLine(" testRunnerOptions=${settings.testRunnerOptions}") appendLine(" command=${settings.command}") - appendLine(" testoType=${settings.testoType}") + appendLine(" testoTypes=${settings.testoTypes}") appendLine(" suites=${settings.suites}") appendLine(" groups=${settings.groups}") appendLine(" excludeGroups=${settings.excludeGroups}") diff --git a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/CommandBaselineTest.kt b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/CommandBaselineTest.kt index 694b0f3e..03202b1b 100644 --- a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/CommandBaselineTest.kt +++ b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/CommandBaselineTest.kt @@ -137,7 +137,7 @@ class CommandBaselineTest : BasePlatformTestCase() { run(configuration { scope = Scope.ConfigurationFile suites = mutableListOf("Unit", "My Suite") - testoType = "bench" + testoTypes = mutableListOf("bench") }) } out.case("rerun filters") { diff --git a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/NamesBaselineTest.kt b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/NamesBaselineTest.kt index 639deef6..84a73899 100644 --- a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/NamesBaselineTest.kt +++ b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/NamesBaselineTest.kt @@ -24,9 +24,9 @@ class NamesBaselineTest : BasePlatformTestCase() { out.case("two groups") { scope = Scope.ConfigurationFile; groups = mutableListOf("db", "slow") } out.case("one suite") { scope = Scope.ConfigurationFile; suites = mutableListOf("Unit") } out.case("two suites") { scope = Scope.ConfigurationFile; suites = mutableListOf("Unit", "My Suite") } - out.case("type only") { scope = Scope.ConfigurationFile; testoType = "bench" } + out.case("type only") { scope = Scope.ConfigurationFile; testoTypes = mutableListOf("bench") } out.case("groups win over suites and type") { - scope = Scope.ConfigurationFile; groups = mutableListOf("db"); suites = mutableListOf("Unit"); testoType = "test" + scope = Scope.ConfigurationFile; groups = mutableListOf("db"); suites = mutableListOf("Unit"); testoTypes = mutableListOf("test") } out.case("excluded groups only") { scope = Scope.ConfigurationFile; excludeGroups = mutableListOf("slow") } out.case("rerun filters only") { scope = Scope.ConfigurationFile; rerunFilters = listOf("\\App\\CalcTest::adds") } diff --git a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/PersistenceBaselineTest.kt b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/PersistenceBaselineTest.kt index 44456386..92749458 100644 --- a/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/PersistenceBaselineTest.kt +++ b/src/phpstormTest/kotlin/com/github/xepozz/testo/phpstorm/baseline/PersistenceBaselineTest.kt @@ -37,7 +37,7 @@ class PersistenceBaselineTest : BasePlatformTestCase() { configurationFilePath = "/work/app/testo.php" testRunnerOptions = "-q --stop-on-failure" command = "list" - testoType = "bench" + testoTypes = mutableListOf("bench") suites = mutableListOf("Unit", "My, Suite") groups = mutableListOf("db", "a,b") excludeGroups = mutableListOf("slow") @@ -77,10 +77,11 @@ class PersistenceBaselineTest : BasePlatformTestCase() { fun testLegacyShape() { val saved = JDOMUtil.load(write(full())) val runner = checkNotNull(find(saved, "TestoRunnerSettings")) { "No TestoRunnerSettings in the saved XML" } - runner.children.filter { it.name in setOf("groups", "exclude_groups", "suites") }.forEach { runner.removeContent(it) } + runner.children.filter { it.name in setOf("groups", "exclude_groups", "suites", "testo_types") }.forEach { runner.removeContent(it) } runner.setAttribute("group", "db, slow ,,x") runner.setAttribute("exclude_group", "flaky") runner.setAttribute("suite", "Unit, slow") + runner.setAttribute("testo_type", "bench") val legacy = JDOMUtil.write(saved) val restored = BaselineSettings.newConfiguration(project, "Legacy") diff --git a/src/test/kotlin/com/github/xepozz/testo/baseline/RunContextsBaselineTest.kt b/src/test/kotlin/com/github/xepozz/testo/baseline/RunContextsBaselineTest.kt index 20c927d1..2d29b341 100644 --- a/src/test/kotlin/com/github/xepozz/testo/baseline/RunContextsBaselineTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/baseline/RunContextsBaselineTest.kt @@ -121,7 +121,7 @@ class RunContextsBaselineTest : BasePlatformTestCase() { appendLine(" configurationFilePath=$configurationFilePath") appendLine(" testRunnerOptions=$testRunnerOptions") appendLine(" command=$command") - appendLine(" testoType=$testoType") + appendLine(" testoTypes=$testoTypes") appendLine(" suites=$suites") appendLine(" groups=$groups") appendLine(" excludeGroups=$excludeGroups") diff --git a/src/test/kotlin/com/github/xepozz/testo/launch/TestoCommandLineTest.kt b/src/test/kotlin/com/github/xepozz/testo/launch/TestoCommandLineTest.kt index 246c7bf7..69b08d8d 100644 --- a/src/test/kotlin/com/github/xepozz/testo/launch/TestoCommandLineTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/launch/TestoCommandLineTest.kt @@ -10,7 +10,7 @@ class TestoCommandLineTest { @Test fun selectionArgumentsComeInAFixedOrder() { val selection = TestoRunSelection( - testoType = "bench", + testoTypes = listOf("bench", "!inline"), suites = listOf("Unit", "My Suite"), groups = listOf("db"), excludeGroups = listOf("slow", "!flaky"), @@ -19,7 +19,7 @@ class TestoCommandLineTest { assertEquals( listOf( - "--type", "bench", + "--type", "bench", "--type", "!inline", "--suite", "Unit", "--suite", "My Suite", "--group", "db", "--group", "!slow", "--group", "!flaky", diff --git a/src/test/kotlin/com/github/xepozz/testo/launch/TestoConfigurationNamesTest.kt b/src/test/kotlin/com/github/xepozz/testo/launch/TestoConfigurationNamesTest.kt index d7d82bc2..fa00c8d2 100644 --- a/src/test/kotlin/com/github/xepozz/testo/launch/TestoConfigurationNamesTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/launch/TestoConfigurationNamesTest.kt @@ -14,7 +14,8 @@ class TestoConfigurationNamesTest { assertEquals("Group 'db'", TestoConfigurationNames.suggestedName(filterOnly.copy(groups = listOf("db")))) assertEquals("Groups 'db', 'slow'", TestoConfigurationNames.suggestedName(filterOnly.copy(groups = listOf("db", "slow")))) assertEquals("Suite 'Unit'", TestoConfigurationNames.suggestedName(filterOnly.copy(suites = listOf("Unit")))) - assertEquals("Type 'bench'", TestoConfigurationNames.suggestedName(filterOnly.copy(testoType = "bench"))) + assertEquals("Type 'bench'", TestoConfigurationNames.suggestedName(filterOnly.copy(testoTypes = listOf("bench")))) + assertEquals("Types 'test', '!bench'", TestoConfigurationNames.suggestedName(filterOnly.copy(testoTypes = listOf("test", "!bench")))) assertEquals("Group 'db'", TestoConfigurationNames.suggestedName(filterOnly.copy(groups = listOf("db"), suites = listOf("Unit")))) } @@ -33,7 +34,7 @@ class TestoConfigurationNamesTest { assertTrue(TestoConfigurationNames.isFilterOnlyRun(filterOnly.copy(groups = listOf("db")))) assertTrue(TestoConfigurationNames.isFilterOnlyRun(filterOnly.copy(excludeGroups = listOf("slow")))) assertTrue(TestoConfigurationNames.isFilterOnlyRun(filterOnly.copy(suites = listOf("Unit")))) - assertTrue(TestoConfigurationNames.isFilterOnlyRun(filterOnly.copy(testoType = "test"))) + assertTrue(TestoConfigurationNames.isFilterOnlyRun(filterOnly.copy(testoTypes = listOf("test")))) assertTrue(TestoConfigurationNames.isFilterOnlyRun(filterOnly.copy(rerunFilters = listOf("\\A::b")))) assertFalse("Nothing selects it", TestoConfigurationNames.isFilterOnlyRun(filterOnly)) assertFalse( diff --git a/src/test/kotlin/com/github/xepozz/testo/launch/TestoTreeTargetTest.kt b/src/test/kotlin/com/github/xepozz/testo/launch/TestoTreeTargetTest.kt index f97d6f99..39e70f11 100644 --- a/src/test/kotlin/com/github/xepozz/testo/launch/TestoTreeTargetTest.kt +++ b/src/test/kotlin/com/github/xepozz/testo/launch/TestoTreeTargetTest.kt @@ -1,6 +1,10 @@ package com.github.xepozz.testo.launch import com.github.xepozz.testo.tests.console.TestoRunTarget +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiDirectory +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile import org.junit.Assert.assertEquals import org.junit.Test @@ -14,7 +18,7 @@ class TestoTreeTargetTest { assertEquals(TestoScope.METHOD, selection.scope) assertEquals("\\App\\CalcTest::med:0:1", selection.methodName) assertEquals(listOf("Unit"), selection.suites) - assertEquals("test", selection.testoType) + assertEquals(listOf("test"), selection.testoTypes) } @Test @@ -24,6 +28,25 @@ class TestoTreeTargetTest { assertEquals(TestoScope.CONFIGURATION_FILE, selection.scope) assertEquals(emptyList(), selection.suites) - assertEquals("", selection.testoType) + assertEquals(emptyList(), selection.testoTypes) + } + + @Test + fun aRunOfSeveralTypesIsNotTheRunOfANodeOfOneOfThem() { + val contexts = TestoRunContexts(object : TestoRunContexts.Host { + override fun baseSetup(selection: TestoRunSelection, element: PsiElement, file: VirtualFile): PsiElement? = null + override fun baseMatches(selection: TestoRunSelection, element: PsiElement) = false + override fun isForeignTestFile(file: PsiFile) = false + override fun directoryHasPhpFiles(directory: PsiDirectory) = false + }) + val selection = TestoRunSelection( + scope = TestoScope.METHOD, + methodName = "\\App\\CalcTest::med", + testoTypes = listOf("test", "bench"), + ) + val target = TestoRunTarget("php_qn:///app/tests/CalcTest.php::\\App\\CalcTest::med", null, "test") + + assertEquals(false, contexts.treeNodeMatches(selection, target, null)) + assertEquals(true, contexts.treeNodeMatches(selection.copy(testoTypes = listOf("test")), target, null)) } } diff --git a/src/test/testData/baseline/persistence-legacy.txt b/src/test/testData/baseline/persistence-legacy.txt index 774f5c5d..b3a80400 100644 --- a/src/test/testData/baseline/persistence-legacy.txt +++ b/src/test/testData/baseline/persistence-legacy.txt @@ -9,7 +9,7 @@ - +