-
Notifications
You must be signed in to change notification settings - Fork 11
Add testProcess{Config} task to accept user options #581
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
42d4d54
6f5c7ba
a13c32a
0a80e41
710ac93
9b9d36e
dff76a3
9e9d6a0
1d5a380
15dbbf7
799203b
50547f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -399,20 +399,16 @@ class ProfilerTestPlugin : Plugin<Project> { | |
| } | ||
|
|
||
| // Environment variables | ||
| testConfig.environmentVariables.forEach { (key, value) -> | ||
| execTask.environment(key, value) | ||
| } | ||
|
|
||
| // CRITICAL FIX: Remove LD_LIBRARY_PATH to let RPATH work correctly | ||
| // CRITICAL FIX: Filter out LD_LIBRARY_PATH to let RPATH work correctly | ||
| // The test JDK's launcher has RPATH set to find its own libraries ($ORIGIN/../lib/jli) | ||
| // But LD_LIBRARY_PATH overrides RPATH and causes it to load the wrong libjli.so | ||
| // Solution: Unset LD_LIBRARY_PATH entirely to let RPATH take precedence | ||
| execTask.doFirst { | ||
| val currentLdLibPath = (execTask.environment["LD_LIBRARY_PATH"] as? String) ?: System.getenv("LD_LIBRARY_PATH") | ||
| if (!currentLdLibPath.isNullOrEmpty()) { | ||
| project.logger.info("Removing LD_LIBRARY_PATH to prevent cross-JDK library conflicts (was: $currentLdLibPath)") | ||
| execTask.environment.remove("LD_LIBRARY_PATH") | ||
| // Solution: Never propagate LD_LIBRARY_PATH so RPATH takes precedence | ||
| testConfig.environmentVariables.forEach { (key, value) -> | ||
| if (key == "LD_LIBRARY_PATH") { | ||
| project.logger.info("Removing LD_LIBRARY_PATH to prevent cross-JDK library conflicts (was: $value)") | ||
| return@forEach | ||
| } | ||
| execTask.environment(key, value) | ||
| } | ||
|
|
||
| // Sanitizer conditions | ||
|
|
@@ -429,6 +425,116 @@ class ProfilerTestPlugin : Plugin<Project> { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Create Exec-based test task that always runs in a separate process. | ||
| * Available on all platforms. Supports -Pprofiler.options for overriding profiler settings. | ||
| * Task name: testProcess<Config> (e.g., testProcessDebug, testProcessRelease) | ||
| */ | ||
| private fun createProcessTestTask( | ||
| project: Project, | ||
| extension: ProfilerTestExtension, | ||
| testConfig: TestTaskConfiguration, | ||
| testCfg: Configuration, | ||
| sourceSets: SourceSetContainer | ||
| ) { | ||
| val taskName = "testProcess${testConfig.configName.replaceFirstChar { it.uppercase() }}" | ||
| project.tasks.register(taskName, Exec::class.java) { | ||
| val execTask = this | ||
| execTask.description = "Runs tests in separate process with ${testConfig.configName} library (supports -Pprofiler.options)" | ||
| execTask.group = "verification" | ||
| execTask.onlyIf { testConfig.isActive && !project.hasProperty("skip-tests") } | ||
|
|
||
| // Dependencies | ||
| execTask.dependsOn(project.tasks.named("compileTestJava")) | ||
| execTask.dependsOn(testCfg) | ||
| execTask.dependsOn(sourceSets.getByName("test").output) | ||
|
|
||
| // Configure at execution time to capture properties | ||
| execTask.doFirst { | ||
| execTask.executable = PlatformUtils.testJavaExecutable() | ||
|
|
||
| val allArgs = mutableListOf<String>() | ||
|
|
||
| // JVM args | ||
| allArgs.addAll(testConfig.standardJvmArgs) | ||
| // Version-gated at execution time, when the real test JVM (JAVA_TEST_HOME) is resolvable. | ||
| allArgs.addAll(carrierExportJvmArgs(project)) | ||
| if (extension.nativeLibDir.isPresent) { | ||
| allArgs.add("-Djava.library.path=${extension.nativeLibDir.get().asFile.absolutePath}") | ||
| } | ||
| allArgs.addAll(testConfig.extraJvmArgs) | ||
|
zhengyu123 marked this conversation as resolved.
|
||
| allArgs.addAll(testConfig.configJvmArgs) | ||
|
|
||
| // System properties | ||
| testConfig.systemProperties.forEach { (key, value) -> | ||
| allArgs.add("-D$key=$value") | ||
| } | ||
|
|
||
| // Test filter from -Ptests property | ||
| val testsFilter = project.findProperty("tests") as String? | ||
| if (testsFilter != null) { | ||
| allArgs.add("-Dtest.filter=$testsFilter") | ||
| } | ||
|
|
||
| // Profiler options from -Pprofiler.options property | ||
| val profilerOptions = project.findProperty("profiler.options") as String? | ||
| if (profilerOptions != null) { | ||
| allArgs.add("-Dddprof.test.options=$profilerOptions") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This only adds Useful? React with 👍 / 👎.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AFAICT, that is a true concern which should be addressed? |
||
| } | ||
|
|
||
| // Classpath | ||
| allArgs.add("-cp") | ||
| allArgs.add(testConfig.testClasspath.asPath) | ||
|
|
||
| // Use custom test runner | ||
| allArgs.add("com.datadoghq.profiler.test.ProfilerTestRunner") | ||
|
|
||
| execTask.args = allArgs | ||
| } | ||
|
|
||
| // Environment variables | ||
| testConfig.environmentVariables.forEach { (key, value) -> | ||
| execTask.environment(key, value) | ||
| } | ||
|
|
||
| // Remove LD_LIBRARY_PATH to let RPATH work correctly | ||
| execTask.doFirst { | ||
|
zhengyu123 marked this conversation as resolved.
|
||
| val currentLdLibPath = (execTask.environment["LD_LIBRARY_PATH"] as? String) ?: System.getenv("LD_LIBRARY_PATH") | ||
| if (!currentLdLibPath.isNullOrEmpty()) { | ||
| project.logger.info("Removing LD_LIBRARY_PATH to prevent cross-JDK library conflicts (was: $currentLdLibPath)") | ||
| execTask.environment.remove("LD_LIBRARY_PATH") | ||
| } | ||
| } | ||
|
|
||
| // Sanitizer conditions | ||
| when (testConfig.configName) { | ||
| "asan" -> { | ||
| execTask.onlyIf { | ||
| PlatformUtils.locateLibasan() != null && | ||
| !PlatformUtils.isTestJvmJ9() | ||
| } | ||
| // Unlike the Test task's asan variant (setForkEvery(25) above), this task | ||
| // is a single un-forked Exec process running the whole suite in one JVM — | ||
| // there is no forkEvery equivalent for Exec. An unfiltered run would | ||
| // reintroduce the late-suite OOM under ASan's -Xmx512m cap that forkEvery | ||
| // was added to avoid, so require -Ptests to bound the run instead. | ||
| execTask.doFirst { | ||
| if (project.findProperty("tests") == null) { | ||
| throw GradleException( | ||
| "$taskName requires -Ptests=<ClassName|ClassName.method> for the " + | ||
| "asan config: it runs in a single process with no forkEvery " + | ||
| "equivalent, and JFR/JMC allocations accumulate under ASan's " + | ||
| "-Xmx512m heap cap until an unfiltered run OOMs late in the suite. " + | ||
| "For full-suite ASan coverage, use the forked testAsan task instead." | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| "tsan" -> execTask.onlyIf { false } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private fun generateMultiConfigTasks(project: Project, extension: ProfilerTestExtension) { | ||
| val nativeBuildExt = project.rootProject.extensions.findByType(NativeBuildExtension::class.java) | ||
| ?: return // No native build extension, nothing to generate | ||
|
|
@@ -496,6 +602,10 @@ class ProfilerTestPlugin : Plugin<Project> { | |
| createTestTask(project, extension, testConfig, testCfg, sourceSets) | ||
| } | ||
|
|
||
| // Create process-based test task (always uses Exec, available on all platforms) | ||
| // Supports -Pprofiler.options for overriding profiler settings | ||
| createProcessTestTask(project, extension, testConfig, testCfg, sourceSets) | ||
|
|
||
| // Create application tasks for specified configs | ||
| if (configName in applicationConfigs && appMainClass.isNotEmpty()) { | ||
| // Create main configuration | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -376,10 +376,81 @@ public final void registerCurrentThreadForWallClockProfiling() { | |
| profiler.addThread(); | ||
| } | ||
|
|
||
| /** | ||
| * Merges two profiler command strings. Options in overrides take precedence | ||
| * over options in base for matching keys. | ||
| * | ||
| * @param base The base profiler command (from test's getProfilerCommand()) | ||
| * @param overrides The override options (from -Pprofiler.options) | ||
| * @return Merged command string with overrides taking precedence | ||
| */ | ||
| private static String mergeProfilerOptions(String base, String overrides) { | ||
|
zhengyu123 marked this conversation as resolved.
|
||
| if (overrides == null || overrides.isEmpty()) { | ||
| return base; | ||
| } | ||
|
|
||
| // Parse base options into ordered map | ||
| Map<String, String> options = new java.util.LinkedHashMap<>(); | ||
| for (String part : base.split(",")) { | ||
| int eq = part.indexOf('='); | ||
| if (eq > 0) { | ||
| options.put(part.substring(0, eq), part.substring(eq + 1)); | ||
| } else if (!part.isEmpty()) { | ||
| options.put(part, ""); | ||
| } | ||
| } | ||
|
|
||
| // Apply overrides | ||
| for (String part : overrides.split(",")) { | ||
|
zhengyu123 marked this conversation as resolved.
|
||
| int eq = part.indexOf('='); | ||
| if (eq > 0) { | ||
| options.put(part.substring(0, eq), part.substring(eq + 1)); | ||
| } else if (!part.isEmpty()) { | ||
| options.put(part, ""); | ||
| } | ||
| } | ||
|
|
||
| // Rebuild command, preserving keys with empty values (e.g. "filter=") | ||
| // so untouched base options round-trip exactly. | ||
| StringBuilder result = new StringBuilder(); | ||
| for (Map.Entry<String, String> entry : options.entrySet()) { | ||
| if (result.length() > 0) { | ||
| result.append(","); | ||
| } | ||
|
zhengyu123 marked this conversation as resolved.
|
||
| result.append(entry.getKey()); | ||
| result.append("=").append(entry.getValue()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When either the base command or Useful? React with 👍 / 👎.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And this also sounds like a valid concern that's not addressed? |
||
| } | ||
| return result.toString(); | ||
| } | ||
|
|
||
| /** | ||
| * Applies user-provided options from -Pprofiler.options (via the | ||
| * "ddprof.test.options" system property) to a profiler command string, | ||
| * overriding any matching keys already present in it. | ||
| * | ||
| * <p>Tests that build their profiler command directly (rather than through | ||
| * {@link #getProfilerCommand()}/{@link #getAmendedProfilerCommand()}) must | ||
| * call this before {@code JavaProfiler.execute(...)} so that | ||
|
zhengyu123 marked this conversation as resolved.
|
||
| * -Pprofiler.options reliably applies across the whole test set, not just | ||
| * to subclasses of this class. | ||
| * | ||
| * @param profilerCommand the command to apply overrides to | ||
| * @return the command with overrides applied, or unchanged if none were requested | ||
| */ | ||
| public static String applyProfilerOptionOverrides(String profilerCommand) { | ||
| String userOptions = System.getProperty("ddprof.test.options"); | ||
| if (userOptions != null && !userOptions.isEmpty()) { | ||
| profilerCommand = mergeProfilerOptions(profilerCommand, userOptions); | ||
|
zhengyu123 marked this conversation as resolved.
|
||
| System.out.println("[TEST] Applied profiler.options: " + userOptions); | ||
| } | ||
| return profilerCommand; | ||
| } | ||
|
|
||
| private String getAmendedProfilerCommand() { | ||
| String profilerCommand = getProfilerCommand(); | ||
| String profilerCommand = applyProfilerOptionOverrides(getProfilerCommand()); | ||
|
|
||
| String testCstack = (String)testParams.get("cstack"); | ||
| if (testCstack != null) { | ||
| if (testCstack != null && !profilerCommand.contains("cstack=")) { | ||
| profilerCommand += ",cstack=" + testCstack; | ||
|
zhengyu123 marked this conversation as resolved.
|
||
| } else if(!(ALLOW_NATIVE_CSTACKS || profilerCommand.contains("cstack="))) { | ||
| profilerCommand += ",cstack=fp"; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the normal musl Exec test path runs with
LD_LIBRARY_PATHalready set in the CI shell, this loop only filters values coming fromtestConfig.environmentVariables; Gradle Exec tasks inherit the current process environment by default, so the ambientLD_LIBRARY_PATHremains inexecTask.environment. The previousdoFirstremoved the inherited value as well, and without that removal$JAVA_TEST_HOME/bin/javacan again load the wronglibjli.so, which is the cross-JDK failure this block is meant to prevent.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, that has been introduced by the change that I have requested. I think the previous version was actually more-correct. Sorry for that noise.