@@ -24,21 +24,27 @@ import javax.inject.Inject
2424 * - Standard JVM arguments for profiler testing (attach self, error files, etc.)
2525 * - Java executable selection (JAVA_TEST_HOME or JAVA_HOME) on ALL platforms
2626 * - Common environment variables (CI, rate limiting)
27- * - Unified --tests flag support across all platforms
27+ * - Unified -Ptests flag support across all platforms
2828 * - Automatic multi-config test task generation from NativeBuildExtension
2929 *
3030 * Implementation:
31- * - glibc/macOS: Uses native Test tasks with direct --tests flag support
32- * - musl (Alpine): Uses Test tasks that delegate to Exec tasks (workaround for Gradle 9 toolchain probe)
33- * - Unified interface: --tests flag works identically on all platforms
31+ * - glibc/macOS: Uses native Test tasks with Gradle's JUnit Platform integration
32+ * - musl (Alpine): Uses Exec tasks with custom ProfilerTestRunner (bypasses toolchain probe)
33+ * - Unified interface: -Ptests property works identically on all platforms
3434 * - Supports multi-JDK testing via JAVA_TEST_HOME on all platforms
3535 * - Same task names everywhere (testdebug, testrelease, unwindingReportRelease)
3636 *
3737 * Platform Detection:
3838 * - Uses PlatformUtils.isMusl() at configuration time to select task implementation
39- * - musl systems: Test task captures --tests filter, delegates to hidden Exec task
39+ * - musl systems: Exec task with ProfilerTestRunner (uses JUnit Platform Launcher API directly)
4040 * - glibc/macOS: Normal Test task with native JUnit integration
4141 *
42+ * Custom Test Runner:
43+ * - ProfilerTestRunner uses JUnit Platform Launcher API directly
44+ * - Avoids Console Launcher issues (assertions, JVM args, NoSuchMethodError on musl + JDK 11)
45+ * - Same API used by IDEs and Gradle's Test task internally
46+ * - Supports test filtering via -Dtest.filter system property
47+ *
4248 * Usage:
4349 * ```kotlin
4450 * plugins {
@@ -63,7 +69,9 @@ import javax.inject.Inject
6369 * }
6470 *
6571 * // Run tests (all platforms use same syntax):
66- * ./gradlew :ddprof-test:testdebug --tests=ClassName.methodName
72+ * ./gradlew :ddprof-test:testdebug -Ptests=ClassName.methodName
73+ * ./gradlew :ddprof-test:testdebug -Ptests=ClassName
74+ * ./gradlew :ddprof-test:testdebug -Ptests="*.Pattern*"
6775 * ```
6876 */
6977class ProfilerTestPlugin : Plugin <Project > {
@@ -162,7 +170,7 @@ class ProfilerTestPlugin : Plugin<Project> {
162170
163171 /* *
164172 * Create native Test task for glibc/macOS (normal path).
165- * Uses Gradle's Test task with --tests flag support.
173+ * Uses Gradle's Test task with -Ptests property support.
166174 */
167175 private fun createTestTask (
168176 project : Project ,
@@ -192,9 +200,7 @@ class ProfilerTestPlugin : Plugin<Project> {
192200 // Configure Java executable - bypasses toolchain system
193201 testTask.setExecutable(PlatformUtils .testJavaExecutable())
194202
195- // Environment variables
196- testTask.environment(" DDPROF_TEST_DISABLE_RATE_LIMIT" , " 1" )
197- testTask.environment(" CI" , project.hasProperty(" CI" ) || System .getenv(" CI" )?.toBoolean() ? : false )
203+ // Environment variables (from testConfig which already includes DDPROF_TEST_DISABLE_RATE_LIMIT and CI)
198204 testConfig.environmentVariables.forEach { (key, value) ->
199205 testTask.environment(key, value)
200206 }
@@ -206,6 +212,25 @@ class ProfilerTestPlugin : Plugin<Project> {
206212 logging.showStandardStreams = true
207213 }
208214
215+ // UNIFIED INTERFACE: Support -Ptests property (same as musl)
216+ val testsFilter = project.findProperty(" tests" ) as String?
217+ if (testsFilter != null ) {
218+ // Forward -Ptests to Test task's filter
219+ testTask.filter.includeTestsMatching(testsFilter)
220+ }
221+
222+ // Warn if --tests flag was used instead of -Ptests
223+ testTask.doFirst {
224+ val filterPatterns = testTask.filter.includePatterns
225+ if (filterPatterns.isNotEmpty() && testsFilter == null ) {
226+ project.logger.warn(" " )
227+ project.logger.warn(" WARNING: --tests flag detected. While it works on glibc/macOS, it will FAIL on musl systems." )
228+ project.logger.warn(" For consistent behavior across all platforms, please use -Ptests instead:" )
229+ project.logger.warn(" ./gradlew :ddprof-test:${testTask.name} -Ptests=${filterPatterns.first()} " )
230+ project.logger.warn(" " )
231+ }
232+ }
233+
209234 // JVM arguments and system properties - configure in doFirst like main does
210235 testTask.doFirst {
211236 val allArgs = mutableListOf<String >()
@@ -232,6 +257,90 @@ class ProfilerTestPlugin : Plugin<Project> {
232257 }
233258 }
234259
260+ /* *
261+ * Create Exec task with custom test runner for musl platforms.
262+ * Uses ProfilerTestRunner with JUnit Platform Launcher API directly.
263+ * Supports unified -Ptests property interface for test filtering.
264+ */
265+ private fun createExecTestTask (
266+ project : Project ,
267+ extension : ProfilerTestExtension ,
268+ testConfig : TestTaskConfiguration ,
269+ testCfg : Configuration ,
270+ sourceSets : SourceSetContainer
271+ ) {
272+ project.tasks.register(" test${testConfig.configName} " , Exec ::class .java) {
273+ val execTask = this
274+ execTask.description = " Runs unit tests with the ${testConfig.configName} library variant (musl workaround)"
275+ execTask.group = " verification"
276+ execTask.onlyIf { testConfig.isActive && ! project.hasProperty(" skip-tests" ) }
277+
278+ // Dependencies
279+ execTask.dependsOn(project.tasks.named(" compileTestJava" ))
280+ execTask.dependsOn(testCfg)
281+ execTask.dependsOn(sourceSets.getByName(" test" ).output)
282+
283+ // Configure at execution time to capture -Ptests filter
284+ execTask.doFirst {
285+ execTask.executable = PlatformUtils .testJavaExecutable()
286+
287+ val allArgs = mutableListOf<String >()
288+
289+ // JVM args
290+ allArgs.addAll(testConfig.standardJvmArgs)
291+ if (extension.nativeLibDir.isPresent) {
292+ allArgs.add(" -Djava.library.path=${extension.nativeLibDir.get().asFile.absolutePath} " )
293+ }
294+ allArgs.addAll(testConfig.extraJvmArgs)
295+
296+ // System properties
297+ testConfig.systemProperties.forEach { (key, value) ->
298+ allArgs.add(" -D$key =$value " )
299+ }
300+
301+ // UNIFIED INTERFACE: Test filter from -Ptests property
302+ val testsFilter = project.findProperty(" tests" ) as String?
303+ if (testsFilter != null ) {
304+ allArgs.add(" -Dtest.filter=$testsFilter " )
305+ }
306+
307+ // Classpath (includes custom test runner)
308+ allArgs.add(" -cp" )
309+ allArgs.add(testConfig.testClasspath.asPath)
310+
311+ // Use custom test runner (NOT ConsoleLauncher)
312+ allArgs.add(" com.datadoghq.profiler.test.ProfilerTestRunner" )
313+
314+ execTask.args = allArgs
315+
316+ // Debug logging
317+ project.logger.info(" Exec task: ${execTask.executable} with ${testConfig.testClasspath.files.size} classpath entries" )
318+ }
319+
320+ // Environment variables
321+ testConfig.environmentVariables.forEach { (key, value) ->
322+ execTask.environment(key, value)
323+ }
324+
325+ // CRITICAL FIX: Remove LD_LIBRARY_PATH to let RPATH work correctly
326+ // The test JDK's launcher has RPATH set to find its own libraries ($ORIGIN/../lib/jli)
327+ // But LD_LIBRARY_PATH overrides RPATH and causes it to load the wrong libjli.so
328+ // Solution: Unset LD_LIBRARY_PATH entirely to let RPATH take precedence
329+ execTask.doFirst {
330+ val currentLdLibPath = (execTask.environment[" LD_LIBRARY_PATH" ] as ? String ) ? : System .getenv(" LD_LIBRARY_PATH" )
331+ if (! currentLdLibPath.isNullOrEmpty()) {
332+ project.logger.info(" Removing LD_LIBRARY_PATH to prevent cross-JDK library conflicts (was: $currentLdLibPath )" )
333+ execTask.environment.remove(" LD_LIBRARY_PATH" )
334+ }
335+ }
336+
337+ // Sanitizer conditions
338+ when (testConfig.configName) {
339+ " asan" -> execTask.onlyIf { PlatformUtils .locateLibasan() != null }
340+ " tsan" -> execTask.onlyIf { PlatformUtils .locateLibtsan() != null }
341+ }
342+ }
343+ }
235344
236345 private fun generateMultiConfigTasks (project : Project , extension : ProfilerTestExtension ) {
237346 val nativeBuildExt = project.rootProject.extensions.findByType(NativeBuildExtension ::class .java)
@@ -289,10 +398,16 @@ class ProfilerTestPlugin : Plugin<Project> {
289398 // Build shared configuration
290399 val testConfig = buildTestConfiguration(project, extension, config, testCfg, sourceSets)
291400
292- // Use Test tasks on all platforms
293- // Setting executable directly bypasses Gradle's toolchain probing
294- project.logger.info(" Creating Test task for $configName " )
295- createTestTask(project, extension, testConfig, testCfg, sourceSets)
401+ // Platform-conditional task creation
402+ // Check both PlatformUtils.isMusl() and LIBC environment variable (set by Docker)
403+ val isMuslSystem = PlatformUtils .isMusl() || System .getenv(" LIBC" ) == " musl"
404+ if (isMuslSystem) {
405+ project.logger.info(" Creating Exec task for $configName (musl workaround, LIBC=${System .getenv(" LIBC" )} )" )
406+ createExecTestTask(project, extension, testConfig, testCfg, sourceSets)
407+ } else {
408+ project.logger.info(" Creating Test task for $configName (glibc/macOS, LIBC=${System .getenv(" LIBC" )} )" )
409+ createTestTask(project, extension, testConfig, testCfg, sourceSets)
410+ }
296411
297412 // Create application tasks for specified configs
298413 if (configName in applicationConfigs && appMainClass.isNotEmpty()) {
0 commit comments