| name | testing-internals |
| description | Internals of tests.cmd execution flow, test discovery, and troubleshooting. Use when debugging test discovery issues or understanding the test runner. |
Test Execution Internals
This document explains tests.cmd internals and helps troubleshoot test execution issues.
Overview
The test execution chain:
tests.cmd → Bazel → IdeaUltimateRunTestsBuildTarget → TestingTasksImpl → JUnit 5
Key components:
- tests.cmd: Shell script that invokes Bazel with test parameters
- Bazel target:
//build:local_idea_ultimate_run_tests_build_target
- TestingOptions: Parses
-Dintellij.build.test.* system properties
- TestingTasksImpl: Orchestrates classpath assembly and JVM setup
- JUnit runners: Execute tests in a forked JVM process
Common Issues & Solutions
Tests Not Found
Symptoms:
- "No tests found" message
- Test class exists but isn't executed
Causes & Solutions:
-
Pattern mismatch - Simple class names don't work (see Pattern Matching below):
-Dintellij.build.test.patterns=MyTest
-Dintellij.build.test.patterns=*MyTest
-Dintellij.build.test.patterns=com.example.MyTest
-
Test not in classpath - The test class must be in a module that's part of the test classpath. Check if the module is included in the build.
-
Test class not recognized - Ensure class name ends with Test or has JUnit annotations.
OutOfMemoryError
Solution: Increase heap size:
./tests.cmd \
--module <module> \
--test MyTest \
-Dintellij.build.test.jvm.memory.options=-Xmx8g
Bazel Build Fails
Symptoms:
- Build errors before tests run
- Missing dependencies
Solutions:
-
Check module dependencies - Ensure test module has required dependencies in .iml file.
-
Verify BUILD.bazel is synced - Run ./build/jpsModelToBazel.cmd after changing .iml files.
Note: Bazel incremental builds are always correct. Do not use bazel clean - it won't help.
Test Discovery Issues
Symptoms:
- Wrong tests executed
- Tests filtered unexpectedly
Debug steps:
-
Check test groups - If using groups, verify testGroups.properties configuration.
-
Check bucketing - For parallel execution, tests are distributed by hash:
-Didea.test.runners.count=4
-Didea.test.runner.index=0
-
Check class filters - TestCaseLoader applies pattern matching before test execution.
Debug Mode
Enable debug mode to attach a debugger:
./tests.cmd \
--module <module> \
--test MyTest \
-Dintellij.build.test.debug.enabled=true \
-Dintellij.build.test.debug.port=5005 \
-Dintellij.build.test.debug.suspend=true
Then attach debugger to port 5005.
Quick Reference
| Property | Purpose |
|---|
intellij.build.test.patterns | Test class patterns (semicolon-separated) |
intellij.build.test.groups | Test groups to run |
intellij.build.test.attempt.count | Retry count for flaky tests |
intellij.build.test.jvm.memory.options | JVM memory settings |
intellij.build.test.debug.enabled | Enable remote debugging |
Detailed Reference
Execution Flow Diagram
┌─────────────────────────────────────────────────────────────────────────────┐
│ 1. COMMAND LINE │
│ ./tests.cmd --module <module> --test MyTest │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 2. SHELL SCRIPT │
│ tests.cmd → community/build/run_build_target.sh │
│ Maps --module/--test to -D properties, wraps as --jvm_flag=<arg> │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 3. BAZEL │
│ bazel run //build:local_idea_ultimate_run_tests_build_target │
│ (defined in build/BUILD.bazel) │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 4. BUILD TARGET ENTRY POINT │
│ IdeaUltimateRunTestsBuildTarget.main() │
│ → UltimateProjectTestingTasks.runTests() │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 5. TEST OPTIONS PARSING │
│ UltimateProjectTestingOptions (extends TestingOptions) │
│ Reads all -Dintellij.build.test.* system properties │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 6. TEST EXECUTION ORCHESTRATION │
│ TestingTasksImpl.runTests() │
│ - Builds test classpath │
│ - Prepares JVM arguments and system properties │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 7. FORKED TEST PROCESS │
│ TestingTasksImpl.runJUnit5Engine() │
│ Spawns new JVM with bootstrap classpath │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌───────────────────────────────────┐ ┌───────────────────────────────────────┐
│ 8a. JUNIT 5 TESTS │ │ 8b. JUNIT 3/4 TESTS (Legacy) │
│ JUnit5TeamCityRunner.main() │ │ JUnit5TeamCityRunner.main() │
│ - Uses JUnit Platform Launcher │ │ - Uses JUnit Platform Launcher │
│ - ClassNameFilter │ │ - ClassNameFilter │
│ - PostDiscoveryFilter │ │ - PostDiscoveryFilter │
└───────────────────────────────────┘ └───────────────────────────────────────┘
│ │
└───────────────┬───────────────┘
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 9. TEST DISCOVERY & FILTERING │
│ TestCaseLoader │
│ - Loads test classes from classpath roots │
│ - Applies pattern/group filters │
│ - Handles bucketing for parallel execution │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ 10. TEST EXECUTION │
│ JUnit Platform executes tests │
│ TCExecutionListener reports results to TeamCity │
└─────────────────────────────────────────────────────────────────────────────┘
Key Classes Reference
Entry Points & Orchestration
| Class | Purpose |
|---|
IdeaUltimateRunTestsBuildTarget | Ultimate tests entry point, calls UltimateProjectTestingTasks |
CommunityRunTestsBuildTarget | Community tests entry point, calls TestingTasks |
UltimateProjectTestingTasks | Ultimate-specific test orchestration (YourKit, network restrictions) |
TestingTasks | Interface for test execution |
TestingTasksImpl | Core test execution logic |
Test Options
| Class | Purpose |
|---|
TestingOptions | Base class for all test options. Parses -Dintellij.build.test.* properties |
UltimateProjectTestingOptions | Ultimate-specific options (YourKit, skip community tests) |
JUnit 5 Test Runners
| Class | Purpose |
|---|
JUnit5TeamCityRunner | Runs JUnit 3/4 tests using the JUnit Vintage test engine, or JUnit5 tests using the JUnit Jupiter test engine |
TCExecutionListener | Reports test results to TeamCity via service messages |
Test Discovery & Loading
| Class | Purpose |
|---|
TestCaseLoader | Discovers and filters test classes |
TestAll | JUnit 3 test suite, collects all tests |
TestClassesFilter | Pattern/group-based test filtering |
Bucketing & Distribution
| Class | Purpose |
|---|
BucketingScheme | Interface for test distribution |
HashingBucketingScheme | Default: hash-based distribution |
TestsDurationBucketingScheme | Duration-aware distribution |
Test Module Hierarchy
Entry Points by Product
| Product | Entry Point | Default mainModule | Source |
|---|
| IDEA Ultimate | IdeaUltimateRunTestsBuildTarget | intellij.idea.ultimate.tests.main | build/src/ |
| Community | CommunityRunTestsBuildTarget | intellij.idea.community.main.tests | community/build/src/ |
| RustRover | RustRoverRunTestsBuildTarget | intellij.idea.ultimate.tests.main | rustrover/build/src/ |
| RubyMine | RubyRunTestsBuildTarget | intellij.idea.ultimate.tests.main | ruby/build/src/ |
| CLion | CLionRunTestsBuildTarget | intellij.idea.ultimate.tests.main | CIDR/clion-build/src/ |
Note: Product entry points (RustRover, RubyMine, CLion) inherit intellij.idea.ultimate.tests.main as the default, but to run product-specific tests, use the dedicated test module via --module. See TESTING.md for the correct module per product.
CI-Defined Test Modules
From intellij-teamcity-config/.teamcity/src/ijplatform/KnownModules.kt:
| CI Constant | Module Name |
|---|
ULTIMATE_TESTS | intellij.idea.ultimate.tests.main |
COMMUNITY_MAIN | intellij.idea.community.main.tests |
GOLAND_TESTS | intellij.goland.tests |
PYTHON_TESTS | intellij.python.tests |
PHPSTORM_MAIN | intellij.phpstorm.main.tests |
CLION_MAIN | intellij.clion.main.tests |
RUSTROVER_MAIN | intellij.rustrover.main.tests |
KOTLIN_TESTS | intellij.kotlin.tests |
KOTLIN_ULTIMATE_ALL_TESTS | intellij.kotlin-ultimate.all-tests |
DATABASE_TESTS | intellij.database.tests |
DATABASE_SQL_TESTS | intellij.database.sql.tests |
Module Configuration
Default mainModule is set in:
UltimateProjectTestingOptions.kt:36 - Ultimate: intellij.idea.ultimate.tests.main
CommunityRunTestsBuildTarget.kt:28 - Community: intellij.idea.community.main.tests
Ultimate Test Module Tree (Simplified)
intellij.idea.ultimate.tests.main
├── intellij.idea.ultimate.tests
├── intellij.idea.ultimate.tests.kotlin
├── intellij.platform.tests
├── intellij.java.tests
└── ... (hundreds of test modules)
Separate hierarchies (NOT in .main):
├── intellij.idea.ultimate.tests.kotlin.k2
│ └── intellij.devkit.kotlin.fir.tests
├── intellij.idea.ultimate.tests.devBuildTests
└── intellij.kotlin.tests
TestingOptions Properties
TestingOptions Properties
Most options use the intellij.build.test.* prefix. Bucketing uses idea.test.* prefix.
testConfigurations
testPatterns
testGroups
mainModule
attemptCount
jvmMemoryOptions
customRuntimePath
isDebugEnabled
debugPort
isSuspendDebugProcess
bucketsCount
bucketIndex
enableCoverage
coveredClassesPatterns
Bazel Target Configuration
The test target is defined in build/BUILD.bazel:
java_binary(
name = "local_idea_ultimate_run_tests_build_target",
runtime_deps = [":build"],
main_class = "IdeaUltimateRunTestsBuildTarget",
data = ALL_ULTIMATE_TARGETS + [BAZEL_TARGETS_JSON_ULTIMATE],
jvm_flags = [
"-Dintellij.build.console.exporter.to.temp.file=true",
"-Dintellij.build.console.messages.verbose=false",
"-Dintellij.build.clean.output.root=false",
"-Dintellij.build.use.compiled.classes=true",
"-Dintellij.build.bazel.targets.json.file=$(rlocationpath %s)" % BAZEL_TARGETS_JSON_ULTIMATE,
],
add_opens = INTELLIJ_ADD_OPENS,
)
Test Process JVM Configuration
TestingTasksImpl.prepareEnvForTestRun() configures the forked test JVM:
"idea.home.path" → projectHome
"idea.config.path" → tempDir/config
"idea.system.path" → tempDir/system
"java.io.tmpdir" → tempDir
"-XX:+HeapDumpOnOutOfMemoryError"
"-XX:HeapDumpPath=<snapshotsDir>/intellij-tests-oom-<timestamp>.hprof"
"-Xms750m -Xmx1024m"
Passing JVM Args to Test Process
There are two mechanisms for passing JVM arguments to the test JVM process:
1. Memory Options (intellij.build.test.jvm.memory.options)
For JVM memory settings like heap size, use the dedicated property:
./tests.cmd --module <module> --test <pattern> -Dintellij.build.test.jvm.memory.options="-Xmx4g -Xms2g"
Multiple options are space-separated within quotes. These options are added to the beginning of the JVM arguments via VmOptionsGenerator.generate().
Implementation (see TestingTasksImpl.kt, runJUnit5Engine method):
val customMemoryOptions = options.jvmMemoryOptions?.trim()?.split(Regex("\\s+"))?.takeIf { it.isNotEmpty() }
jvmArgs.addAll(
index = 0,
elements = VmOptionsGenerator.generate(
customVmMemoryOptions = if (customMemoryOptions == null) mapOf("-Xms" to "750m", "-Xmx" to "1024m") else emptyMap(),
additionalVmOptions = customMemoryOptions ?: emptyList(),
),
)
2. Pass-through System Properties (pass.* prefix)
To pass arbitrary system properties to the test JVM, use the pass. prefix. The prefix is stripped before passing to the test process:
./tests.cmd --module <module> --test <pattern> -Dpass.my.custom.property=value -Dpass.some.flag=true
Results in test JVM receiving:
-Dmy.custom.property=value
-Dsome.flag=true
Implementation (see TestingTasksImpl.kt, prepareEnvForTestRun method):
for ((key, value) in System.getProperties()) {
key as String
if (key.startsWith("pass.")) {
systemProperties.put(key.substring("pass.".length), value as String)
}
}
This is a TeamCity convention for passing properties to nested processes.
Combined Example
./tests.cmd \
--module <module> \
--test MyTest \
-Dintellij.build.test.jvm.memory.options="-Xmx4g" \
-Dpass.my.test.flag=enabled \
-Dpass.debug.level=verbose
Important: Properties without pass. prefix are consumed by the build scripts, NOT passed to the test JVM.
Test Discovery Flow
Step-by-Step Pattern Matching
┌─────────────────────────────────────────────────────────────────────────────┐
│ PATTERN INPUT │
│ -Dintellij.build.test.patterns=*MyTest │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ PATTERN COMPILATION (TestClassesFilter.compilePattern) │
│ filter.replace("$","\\$").replace(".","\\.");replace("*",".*") │
│ "*MyTest" → ".*MyTest" │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ CLASS DISCOVERY (ClassFinder) │
│ Scans JARs for *Test.class files, extracts FQN: │
│ org/example/MyTest.class → "org.example.MyTest" │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ PATTERN MATCHING (PatternListTestClassFilter.matches) │
│ pattern.matcher(className).matches() ← FULL STRING MATCH │
│ ".*MyTest".matches("org.example.MyTest") → true │
└─────────────────────────────────────────────────────────────────────────────┘
Pattern Transformation Table
| Input Pattern | Compiled Regex | Matches org.example.MyTest? |
|---|
MyTest | MyTest | ❌ NO (not full match) |
*MyTest | .*MyTest | ✅ YES |
org.example.MyTest | org\.example\.MyTest | ✅ YES |
org.example.* | org\.example\..* | ✅ YES |
* | .* | ✅ YES (matches all) |
Key Implementation Details
-
Pattern Compilation (TestClassesFilter.compilePattern()):
filter = filter.replace("$", "\\$").replace(".", "\\.").replace("*", ".*");
return Pattern.compile(filter);
-
Pattern Matching (PatternListTestClassFilter.matches()):
return ContainerUtil.exists(patterns, pattern -> pattern.matcher(className).matches());
- Uses
matches() NOT find() - requires ENTIRE string to match
- className is ALWAYS the fully qualified name (FQN)
-
ClassNameFilter (JUnit 5) - Fast pre-filter on class names
- Calls
TestCaseLoader.isClassNameIncluded(className)
- Applied to EVERY class in classpath (must be fast)
-
PostDiscoveryFilter (JUnit 5) - Post-discovery filter
- Calls
TestCaseLoader.isClassIncluded(className)
- Checks bucketing (which runner should execute this test)
-
TestCaseLoader.fillTestCases() (JUnit 3/4) - Scans classpath roots
- Uses
ClassFinder to find all *Test.class files
- Calls
isPotentiallyTestCase() which calls filter's matches()
- Applies group-based filtering from
testGroups.properties
Why Simple Class Names NEVER Work
Root Cause: Pattern.matches() requires the ENTIRE string to match.
"MyTest".matches("org.example.MyTest")
".*MyTest".matches("org.example.MyTest")
This applies to ALL modules (default and non-default). Always use:
- FQN:
org.example.MyTest
- Or wildcard:
*MyTest
Related Documentation
- TESTING.md - How to run tests via
tests.cmd (quick start, parameters, examples)
- Writing Tests - How to write tests (framework,
@TestApplication, fixtures, EDT)