Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <selector>` 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),
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ object TestoCommandLine {

/** `--type`, `--suite`, `--group`, excluded `--group !…` and rerun `--filter`, in that order. */
fun selectionArguments(selection: TestoRunSelection): List<String> = buildList {
if (selection.testoType.isNotEmpty()) {
for (type in selection.testoTypes) {
add("--type")
add(selection.testoType)
add(type)
}
for (suite in selection.suites) {
add("--suite")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
}
}
19 changes: 11 additions & 8 deletions src/main/kotlin/com/github/xepozz/testo/launch/TestoRunContexts.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -107,15 +107,15 @@ 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
}

selection.methodName += ":$index"
selection.dataProviderIndex = index
selection.dataSetIndex = -1
selection.testoType = testoTypeOf(element)
selection.testoTypes = typesOf(testoTypeOf(element))

return element
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<String> = listOfNotNull(type.takeIf { it.isNotEmpty() })

private fun testoTypeOfAttribute(attribute: PhpAttributeView): String {
val fqn = attribute.fqn ?: return ""
return when (fqn) {
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = emptyList(),
var suites: List<String> = emptyList(),
var groups: List<String> = emptyList(),
var excludeGroups: List<String> = emptyList(),
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/messages/TestoBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(),
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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<String> = 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<String> = mutableListOf()
Expand Down Expand Up @@ -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<String> = emptyList()
Expand All @@ -129,6 +134,10 @@ class TestoRunnerSettings(
suites = mutableListOf(legacySuite)
legacySuite = ""
}
if (legacyTestoType.isNotEmpty()) {
testoTypes = mutableListOf(legacyTestoType)
legacyTestoType = ""
}
}

fun copyInfectionFrom(other: TestoRunnerSettings) {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ class TestoTestRunConfigurationEditor(
private val parentEditor: PhpTestRunConfigurationEditor,
val configuration: TestoRunConfiguration
) : SettingsEditor<TestoRunConfiguration>() {
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"),
Expand Down Expand Up @@ -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=<name> per tag, e.g. test, inline, bench; !<name> excludes the type")

row {
label("Suite")
.gap(RightGap.COLUMNS)
Expand Down Expand Up @@ -255,6 +268,7 @@ class TestoTestRunConfigurationEditor(
override fun textChanged(e: DocumentEvent) = listener()
}

typeField.addChangeListener(listener)
suiteField.addChangeListener(listener)
groupField.addChangeListener(listener)
excludeGroupField.addChangeListener(listener)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Loading
Loading