Skip to content

Commit e553f15

Browse files
jbachorikclaude
andcommitted
Unified test execution with platform-conditional tasks
Implements platform-conditional test execution to support both glibc/macOS and musl environments with a unified -Ptests interface. Changes: - Add ProfilerTestRunner with JUnit Platform Launcher API - Create Test tasks on glibc/macOS, Exec tasks on musl - Fix JDK 11 + musl by removing LD_LIBRARY_PATH (prevents cross-JDK library loading) - Support both . and # method separators with proper heuristic - Update all docs to use -Ptests syntax consistently - Add warning when --tests flag used on Test tasks - Switch from junit-platform-console to junit-platform-launcher - Add build-logic to Dependabot monitoring - Improve error handling in javadocJar task Verified on musl + JDK 11, musl + JDK 21, and macOS. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 70e04c8 commit e553f15

9 files changed

Lines changed: 320 additions & 33 deletions

File tree

.github/dependabot.yml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
version: 2
22
updates:
3-
# Gradle dependencies
3+
# Gradle dependencies (root project)
44
- package-ecosystem: "gradle"
55
directory: "/"
66
schedule:
@@ -13,6 +13,19 @@ updates:
1313
- "minor"
1414
- "patch"
1515

16+
# Gradle dependencies (build-logic composite build)
17+
- package-ecosystem: "gradle"
18+
directory: "/build-logic"
19+
schedule:
20+
interval: "weekly"
21+
day: "monday"
22+
open-pull-requests-limit: 5
23+
groups:
24+
gradle-minor:
25+
update-types:
26+
- "minor"
27+
- "patch"
28+
1629
# GitHub Actions
1730
- package-ecosystem: "github-actions"
1831
directory: "/"

AGENTS.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -274,17 +274,21 @@ Release builds automatically extract debug symbols:
274274
## Development Workflow
275275

276276
### Running Single Tests
277-
Use Gradle's standard `--tests` flag across all platforms:
277+
Use the `-Ptests` property across all platforms:
278278
```bash
279-
./gradlew :ddprof-test:testdebug --tests=ClassName.methodName # Single method
280-
./gradlew :ddprof-test:testdebug --tests=ClassName # Entire class
281-
./gradlew :ddprof-test:testdebug --tests="*.ClassName" # Pattern matching
279+
./gradlew :ddprof-test:testdebug -Ptests=ClassName.methodName # Single method
280+
./gradlew :ddprof-test:testdebug -Ptests=ClassName # Entire class
281+
./gradlew :ddprof-test:testdebug -Ptests="*.ClassName" # Pattern matching
282282
```
283283

284284
**Platform Implementation Details:**
285-
- **glibc/macOS**: Test tasks use Gradle's native Test task type with direct `--tests` flag support
286-
- **musl (Alpine)**: Test tasks delegate to Exec tasks internally (workaround for Gradle 9 toolchain probe issues on musl)
287-
- **Result**: Unified `--tests` flag works identically across all platforms, no platform-specific syntax required
285+
- **glibc/macOS**: Test tasks use Gradle's native Test task type with JUnit Platform integration
286+
- **musl (Alpine)**: Exec tasks with custom ProfilerTestRunner (bypasses Gradle 9 toolchain probe issues)
287+
- **Custom Test Runner**: Uses JUnit Platform Launcher API directly (same API used by IDEs and Gradle internally)
288+
- **Result**: Unified `-Ptests` property works identically across all platforms, no platform-specific syntax required
289+
290+
**Why `-Ptests` instead of `--tests`?**
291+
The `-Ptests` property works consistently across both Test and Exec task types, while `--tests` only works with Test tasks. This ensures a truly unified interface across all platforms.
288292

289293
### Working with Native Code
290294
Native compilation is automatic during build. C++ code changes require:
@@ -678,11 +682,11 @@ The CI caches JDKs via `.github/workflows/cache_java.yml`. When adding a new JDK
678682
```
679683
- Instead of:
680684
```bash
681-
./gradlew :ddprof-test:testdebug --tests=MuslDetectionTest
685+
./gradlew :ddprof-test:testdebug -Ptests=MuslDetectionTest
682686
```
683687
use:
684688
```bash
685-
./.claude/commands/build-and-summarize :ddprof-test:testdebug --tests=MuslDetectionTest
689+
./.claude/commands/build-and-summarize :ddprof-test:testdebug -Ptests=MuslDetectionTest
686690
```
687691

688692
- This ensures the full build log is captured to a file and only a summary is shown in the main session.

build-logic/conventions/src/main/kotlin/com/datadoghq/profiler/ProfilerTestPlugin.kt

Lines changed: 129 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
6977
class 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()) {

ddprof-lib/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,9 @@ val javadocJar by tasks.registering(Jar::class) {
185185
archiveBaseName.set(libraryName)
186186
archiveClassifier.set("javadoc")
187187
archiveVersion.set(componentVersion)
188-
from(tasks.javadoc.map { it.destinationDir!! })
188+
from(tasks.javadoc.map {
189+
it.destinationDir ?: throw GradleException("Javadoc task destinationDir is null - task may not have been configured properly")
190+
})
189191
}
190192

191193
// Publishing configuration

ddprof-stresstest/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,10 @@ Use reduced iterations:
230230
### Profiler fails to start
231231
Verify profiler library loads:
232232
```bash
233-
./gradlew :ddprof-test:testdebug --tests=JavaProfilerTest.testGetInstance
233+
./gradlew :ddprof-test:testdebug -Ptests=JavaProfilerTest.testGetInstance
234234
```
235235

236-
**Note**: The `--tests` flag works uniformly across all platforms with config-specific test tasks.
236+
**Note**: The `-Ptests` property works uniformly across all platforms with config-specific test tasks.
237237

238238
### Out of memory errors
239239
- Reduce concurrent thread counts

0 commit comments

Comments
 (0)