Test Filtering
This document describes the internal logic of the filtering plugin: the algorithm, pipeline stages, and criteria combination. If you just need to filter tests when running — see the CLI reference.
Plugin class: FilterPlugin\Testo\Filter\FilterPlugin. Included in ApplicationPlugins\Testo\Application\Config\Plugin\ApplicationPlugins — enabled by default.
Overview
Testo provides a flexible filtering system that operates in multiple stages to progressively narrow down the test set. Filtering can be controlled programmatically via the Filternew Filter(array $suites = [], array $names = [], array $paths = [], ?string $type = null)Immutable DTO containing test filtering criteria. class or automatically from CLI arguments.
Filter
Immutable DTO containing test filtering criteria.
new Filter(array $suites = [], array $names = [], array $paths = [], ?string $type = null)Parameters:
$suites- Test suite names to filter by.
$names- Class, method, or function names. Formats:
ClassName::methodName,Namespace\ClassName, fragmentmethodName. Optional DataProvider indices via colon:name:providerIndex:datasetIndex. $paths- File or directory paths. Supports glob patterns:
*,?,[abc]. $type- Test type:
test,inline,bench, or other custom type. If not specified — all types are run.
Examples:
$filter = new Filter(
suites: ['Unit', 'Integration'],
names: ['UserTest::testLogin', 'testAuthentication'],
paths: ['tests/Unit/*', 'tests/Integration/*'],
type: 'test',
);Usage
Via CLI options — when creating via Application::createFromInput(), the plugin automatically creates Filternew Filter(array $suites = [], array $names = [], array $paths = [], ?string $type = null)Immutable DTO containing test filtering criteria. from command options: --filter, --path, --suite, --type:
$app = Application::createFromInput(
inputOptions: ['filter' => ['UserTest'], 'suite' => ['Unit']],
);
$result = $app->run();Via container — register the Filternew Filter(array $suites = [], array $names = [], array $paths = [], ?string $type = null)Immutable DTO containing test filtering criteria. object directly:
$app = Application::createFromConfig($config);
$app->getContainer()->set(Filter::class, new Filter(
suites: ['Unit'],
names: ['UserTest'],
));
$result = $app->run();When running from CLI, the Filter\Testo\Common\Filter is populated automatically from command arguments via Filter::fromScope().
Filter Combination Logic
Same Type: OR Logic
Multiple values within the same filter type are combined with OR logic:
names: ['test1', 'test2']→ matches if name is test1 OR test2paths: ['path1', 'path2']→ matches if path is path1 OR path2suites: ['Unit', 'Integration']→ matches if suite is Unit OR Integration
Different Types: AND Logic
Different filter types are combined with AND logic:
names: ['test1'], suites: ['Unit']→ matches if name is test1 AND suite is Unitnames: ['UserTest'], paths: ['tests/Unit/*']→ matches if name is UserTest AND path matches tests/Unit/*names: ['test1'], type: 'inline'→ matches if name is test1 AND type is inline
Formula: AND(OR(names), OR(paths), OR(suites), type)
Example:
$filter = new Filter(
names: ['test1', 'test2'], // test1 OR test2
paths: ['path1', 'path2'], // path1 OR path2
suites: ['Unit', 'Critical'], // Unit OR Critical
type: 'test', // regular tests only
);
// Result: (test1 OR test2) AND (path1 OR path2) AND (Unit OR Critical) AND type=testName Filter Behavior
The behavior of name filtering is implemented in FilterInterceptor\Testo\Filter\Internal\FilterInterceptor and depends on the name format:
Method Format (ClassName::methodName)
When using method format with :: separator:
- Only the specified method is matched
- Other methods in the same class are excluded
- Result: Test case with only the specified method
Example:
$filter = new Filter(names: ['UserTest::testLogin']);
// Result: UserTest class with only testLogin methodFQN or Fragment Format
When using FQN (with \) or simple fragment (no separators):
Case 1: Class name matches
- Result: Entire test case with all methods
Case 2: Class name doesn't match
- System checks individual methods/functions
- Result: Test case with only matched methods
- If no methods match: Test case is skipped
Examples:
// FQN - matches entire class
$filter = new Filter(names: ['Tests\Unit\UserTest']);
// Result: UserTest class with all methods
// Fragment - matches entire class
$filter = new Filter(names: ['UserTest']);
// Result: UserTest class with all methods
// Fragment - matches method in any class
$filter = new Filter(names: ['testLogin']);
// Result: All classes with testLogin method, each with only that methodNarrowing by DataProvider and DataSet
After the name, you can narrow down to a specific DataProvider via colon, and further to a specific DataSet within it via another colon.
Format: name:providerIndex:datasetIndex
- The format maps to DataPointer
\Testo\Filter\DataPointerand is passed to the data provider module. - "Provider" here means any attribute that spawns a separate test: #[DataProvider]
#[DataProvider(callable|string $provider)]Provides data for a parameterized test from a method or callable., #[DataSet]#[DataSet(array $arguments, ?string $name = null)]Declares a set of arguments for a parameterized test. Can be used multiple times — each attribute creates a separate test run., #[TestInline]#[TestInline(array $arguments, mixed $result = null)]Declares an inline test on a method or function.,#[Bench], etc. - Indices are 0-based, independent of dataset labels.
datasetIndexis optional — you can specify only the provider.- Works with all name formats (method, FQN, fragment).
Examples:
// First provider
$filter = new Filter(names: ['UserTest::testLogin:0']);
// First provider, second dataset
$filter = new Filter(names: ['UserTest::testLogin:0:1']);
// Second provider, fourth dataset — for any test named testAuth
$filter = new Filter(names: ['testAuth:1:3']);
// First provider — for entire UserTest class
$filter = new Filter(names: ['UserTest:0']);Filtering Pipeline
Filtering operates in five stages:
Stage 1: Suite Filter (Configuration Level)
Input: Filter::$suites
- Filters configuration scopes based on suite names
- Each suite defines file scanning locations and patterns
- Determines initial set of directories to scan
- Multiple suites use OR logic
Stage 2: Path Filter (Finder Level)
Input: Filter::$paths
- Applied at file finder level during directory scanning
- Uses glob patterns to match file paths
- Supports wildcards:
*,?,[abc] - Multiple paths use OR logic
- Returns list of files to be processed
Stage 3: File Filter (Tokenizer Level)
Input: Filter::$namesImplementation: FilterInterceptor::locateFile()
- Pre-filters test files before loading for reflection
- Uses lightweight tokenization instead of full reflection
- Checks if file contains any matching classes, methods, or functions
- Skips files that don't match any patterns
- Multiple names use OR logic
Stage 4: Test Filter (Reflection Level)
Input: Filter::$namesImplementation: FilterInterceptor::locateTestCases()
- Filters individual test cases and methods after reflection
- Implements hierarchical filtering:
- For method format (
::) - filters specific methods only - For FQN/fragment format - checks class name first, then methods
- For method format (
- Extracts DataProvider indices and associates them with matched tests
- Returns filtered test case definitions ready for execution
Stage 5: DataProvider Injection (Execution Level)
Input: DataProvider indices from Stage 4 Implementation: FilterInterceptor::runTest()
- Injects
DataPointerinto test metadata before execution - Makes
DataPointeravailable to other interceptors - If no indices specified: no
DataPointeris injected
Pattern Matching
FilterInterceptor\Testo\Filter\Internal\FilterInterceptor uses whole-word boundary matching with regex:
private static function has(string $needle, string $haystack): bool
{
return \preg_match('/\\b' . \preg_quote($needle, '/') . '\\b$/', $haystack) === 1;
}Behavior:
UsermatchesApp\User✓Userdoes NOT matchApp\UserManager✗testmatchestestMethod✓testdoes NOT matchlatestMethod✗