From 66e10fb36eda94db9f26886b004d62ccb270ae1d Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Wed, 26 Aug 2026 11:39:16 -0400 Subject: [PATCH 01/10] Test dd-trace-java under Windows. --- .gitlab-ci.yml | 2 + .gitlab/windows-tests.yml | 190 +++++++++++++++++++ .gitlab/windows/README.md | 47 +++++ .gitlab/windows/ci-common.ps1 | 42 ++++ .gitlab/windows/image/Dockerfile | 60 ++++++ .gitlab/windows/image/compute-image-hash.ps1 | 27 +++ .gitlab/windows/run-base-tests.ps1 | 76 ++++++++ 7 files changed, 444 insertions(+) create mode 100644 .gitlab/windows-tests.yml create mode 100644 .gitlab/windows/README.md create mode 100644 .gitlab/windows/ci-common.ps1 create mode 100644 .gitlab/windows/image/Dockerfile create mode 100644 .gitlab/windows/image/compute-image-hash.ps1 create mode 100644 .gitlab/windows/run-base-tests.ps1 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 18f9d425811..f13053656e8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,6 @@ include: - local: ".gitlab/one-pipeline.locked.yml" + - local: ".gitlab/windows-tests.yml" - local: ".gitlab/benchmarks.yml" - local: ".gitlab/exploration-tests.yml" - local: ".gitlab/ci-visibility-tests.yml" @@ -39,6 +40,7 @@ stages: - benchmarks - tests - tests-arm64 + - tests-windows - test-summary - exploration-tests - ci-visibility-tests diff --git a/.gitlab/windows-tests.yml b/.gitlab/windows-tests.yml new file mode 100644 index 00000000000..1c51e06ea21 --- /dev/null +++ b/.gitlab/windows-tests.yml @@ -0,0 +1,190 @@ +variables: + WINDOWS_BUILD_IMAGE_BASE: "registry.ddbuild.io/ci/dd-trace-java/dd-trace-java-windows-docker-build" + +.windows_gradle_cache_paths: &windows_gradle_cache_paths + - .gradle/wrapper/ + - .gradle/caches/ + - .gradle/notifications/ + +build-windows-ci-image: + stage: build + timeout: 2h + tags: [ "windows-v2:2025" ] + when: manual + allow_failure: true + hooks: + pre_get_sources_script: + - git config --system core.longpaths true + script: + - | + . .gitlab/windows/ci-common.ps1 + + $image = Get-WindowsCiImage + Write-Output "Target image: $image" + + if (Test-WindowsCiImage $image) { + Write-Output "Image $image already exists; skipping rebuild." + exit 0 + } + + $buildArguments = @( + "build", + "--file", ".gitlab/windows/image/Dockerfile", + "--tag", $image, + "--tag", "${env:WINDOWS_BUILD_IMAGE_BASE}:latest" + ) + + Invoke-Native docker @("pull", "${env:WINDOWS_BUILD_IMAGE_BASE}:latest") + if ($LASTEXITCODE -eq 0) { + $buildArguments += @("--cache-from", "${env:WINDOWS_BUILD_IMAGE_BASE}:latest") + } else { + Write-Output "No existing latest image found; building without a registry layer cache." + } + + $buildArguments += ".gitlab/windows/image" + Invoke-Native docker $buildArguments + if ($LASTEXITCODE -ne 0) { throw "docker build failed" } + + Invoke-Native docker @("push", $image) + if ($LASTEXITCODE -ne 0) { throw "docker push $image failed" } + + Invoke-Native docker @("push", "${env:WINDOWS_BUILD_IMAGE_BASE}:latest") + if ($LASTEXITCODE -ne 0) { throw "docker push latest failed" } + id_tokens: + CI_IDENTITIES_GITLAB_ID_TOKEN: + aud: ci-identities + +test-base-windows: + stage: tests-windows + timeout: 2h + tags: [ "windows-v2:2025" ] + hooks: + pre_get_sources_script: + - git config --system core.longpaths true + needs: + - job: build + artifacts: false + # Reads the content-addressed image from registry.ddbuild.io, so it needs the + # same CI identity as the job that publishes it. + id_tokens: + CI_IDENTITIES_GITLAB_ID_TOKEN: + aud: ci-identities + variables: + GIT_SUBMODULE_STRATEGY: normal + GIT_SUBMODULE_DEPTH: 1 + GRADLE_TARGET: ":baseTest" + CACHE_TYPE: "base" + # Feature branches only read the shared dependency seed; protected refs + # refresh it (overridden in rules below). + WINDOWS_SEED_CACHE_POLICY: pull + parallel: + matrix: + - testJvm: [ "21" ] + CI_SPLIT: [ "1/4", "2/4", "3/4", "4/4" ] + rules: + - if: '$POPULATE_CACHE' + when: never + - if: '$CI_COMMIT_BRANCH =~ /^mq-working-branch-/' + when: on_success + allow_failure: true + variables: + WINDOWS_SEED_CACHE_POLICY: pull-push + - if: '$CI_COMMIT_BRANCH == "master"' + when: on_success + allow_failure: true + variables: + WINDOWS_SEED_CACHE_POLICY: pull-push + - if: '$CI_COMMIT_BRANCH' + when: manual + allow_failure: true + cache: + # Shared dependency seed. Only protected refs write it, so a new feature + # branch does not resolve every dependency from scratch on all four + # partitions. unprotect:true is what makes the seed readable from + # unprotected refs; the pull-only policy keeps them from poisoning it. + - key: + files: + - gradle/wrapper/gradle-wrapper.properties + - settings.gradle.kts + prefix: "windows-gradle-seed-$CI_NODE_INDEX" + paths: *windows_gradle_cache_paths + policy: $WINDOWS_SEED_CACHE_POLICY + unprotect: true + # Per-branch, per-partition cache. Restored after the seed, so it wins. + - key: + files: + - gradle/wrapper/gradle-wrapper.properties + - settings.gradle.kts + prefix: "windows-gradle-$CI_COMMIT_REF_SLUG-$CI_NODE_INDEX" + paths: *windows_gradle_cache_paths + policy: pull-push + script: + - | + . .gitlab/windows/ci-common.ps1 + + $image = Get-WindowsCiImage + Write-Output "Expected Windows test image: $image" + if (-not (Test-WindowsCiImage $image)) { + Write-Host "ERROR: Windows test image not found at $image." + Write-Host "Manually run build-windows-ci-image, wait for it to finish, then retry this job." + exit 1 + } + + $diagnosticsDir = Join-Path $env:CI_PROJECT_DIR ".tmp" + New-Item -ItemType Directory -Force -Path $diagnosticsDir, ".gradle" | Out-Null + + $containerName = "dd-trace-java-windows-$($env:CI_JOB_ID)" + $dockerArguments = @( + "run", "--rm", + "--name", $containerName, + "--memory", "20g", + # Windows reports the host's logical processors unless the count is + # capped, which makes thread-pool-sized assertions non-deterministic. + "--cpu-count", "4", + "--workdir", "C:\work", + "--mount", "type=bind,source=$($env:CI_PROJECT_DIR),target=C:\work", + "--mount", "type=bind,source=$diagnosticsDir,target=C:\tmp", + "--env", "CI=true", + "--env", "GITLAB_CI=true", + "--env", "CI_JOB_ID=$($env:CI_JOB_ID)", + "--env", "CI_PIPELINE_ID=$($env:CI_PIPELINE_ID)", + "--env", "CI_COMMIT_SHA=$($env:CI_COMMIT_SHA)", + "--env", "CI_COMMIT_BRANCH=$($env:CI_COMMIT_BRANCH)", + "--env", "CI_SPLIT=$($env:CI_SPLIT)", + "--env", "GRADLE_TARGET=$($env:GRADLE_TARGET)", + "--env", "testJvm=$($env:testJvm)", + "--env", "MAVEN_REPOSITORY_PROXY=$($env:MAVEN_REPOSITORY_PROXY)", + "--env", "GRADLE_PLUGIN_PROXY=$($env:GRADLE_PLUGIN_PROXY)", + "--env", "MASS_READ_URL=$($env:MASS_READ_URL)", + # Parity with .test_job_common on Linux: both runtimes size pools from + # availableProcessors(), which is unreliable in containers. + "--env", "RUNTIME_AVAILABLE_PROCESSORS_OVERRIDE=4", + "--env", "JETTY_AVAILABLE_PROCESSORS=4", + "--env", "TESTCONTAINERS_CHECKS_DISABLE=true", + "--env", "TESTCONTAINERS_RYUK_DISABLED=true", + "--env", "TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX=registry.ddbuild.io/images/mirror/", + $image, + "powershell", "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", + "-File", "C:\work\.gitlab\windows\run-base-tests.ps1" + ) + + Invoke-Native docker $dockerArguments + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + after_script: + - | + $containerName = "dd-trace-java-windows-$($env:CI_JOB_ID)" + docker rm --force $containerName 2>$null + exit 0 + artifacts: + when: always + paths: + - workspace/**/build/test-results/** + - workspace/**/build/reports/tests/** + - buildSrc/build/test-results/** + - buildSrc/build/reports/tests/** + - .gradle/daemon/*/*.out.log + - .tmp/** + reports: + junit: + - workspace/**/build/test-results/**/*.xml + - buildSrc/build/test-results/**/*.xml diff --git a/.gitlab/windows/README.md b/.gitlab/windows/README.md new file mode 100644 index 00000000000..910fa852634 --- /dev/null +++ b/.gitlab/windows/README.md @@ -0,0 +1,47 @@ +# Windows GitLab prototype + +This directory contains an experimental Windows test job and its repo-local CI image. +The image contains MinGit and the Temurin 8, 11, 17, 21, and 25 JDK toolchains used by +the Gradle build. JDK 21 is the default daemon and test JVM. The initial test scope runs +`:baseTest` on Java 21, split into the same four partitions as the existing `test_base` +job, so that the execution model can be validated before moving the image to +`dd-trace-java-docker-build`. + +## Running the prototype + +1. Push the branch and open its GitLab pipeline. +2. Manually run `build-windows-ci-image`. +3. Wait for the content-addressed image to be pushed to + `registry.ddbuild.io/ci/dd-trace-java/dd-trace-java-windows-docker-build`. +4. Run or retry the four `test-base-windows` matrix jobs. + +The image producer also publishes `latest`, but that tag is used only to seed the next +Docker layer cache. Test jobs always recompute and use the content-addressed tag. + +The test job is manual and non-blocking on feature branches. It runs automatically but +remains non-blocking on merge-queue branches and `master`. + +## Updating the image + +Any change under `image/`, including the hashing script itself, produces a new image tag. +The test job fails with an instruction to run `build-windows-ci-image` when that tag does +not exist yet. `ci-common.ps1` lives outside `image/` on purpose: it runs on the host, not +in the container, so it must not change the image tag. + +## Caching + +Both jobs read `.gradle/{wrapper,caches,notifications}` from a shared seed cache that only +protected refs write, then push a per-branch, per-partition cache on top. A new feature +branch therefore starts from the last protected-ref dependency set instead of resolving +everything four times over. + +## Notes on Windows + +- All `docker` calls go through `Invoke-Native` in `ci-common.ps1`. PowerShell turns a + native command's stderr into a terminating error under `$ErrorActionPreference = 'Stop'`, + and docker writes progress and "manifest unknown" to stderr. +- The container gets `LongPathsEnabled` and `core.longpaths`; the deepest relocated build + outputs under `C:\work\workspace\...` exceed the 260-character `MAX_PATH` default. +- Temurin publishes no JDK 21 newer than `21.0.12+8` for `windowsservercore-ltsc2025`, + while 8, 11, 17, and 25 are current. Expect some failures on the Java 21 matrix to be + JDK-version artifacts rather than Windows-specific. diff --git a/.gitlab/windows/ci-common.ps1 b/.gitlab/windows/ci-common.ps1 new file mode 100644 index 00000000000..c24bff3ff75 --- /dev/null +++ b/.gitlab/windows/ci-common.ps1 @@ -0,0 +1,42 @@ +# Shared helpers for the Windows CI jobs in .gitlab/windows-tests.yml. +# Dot-source this file at the top of a job script: . .gitlab/windows/ci-common.ps1 + +$ErrorActionPreference = "Stop" +$WindowsCiRoot = $PSScriptRoot + +# Runs a native command with its stderr merged into stdout. +# +# PowerShell wraps a native command's stderr in ErrorRecord objects, and under +# $ErrorActionPreference = 'Stop' the first one becomes a terminating error. +# docker writes build progress, push progress and "manifest unknown" to stderr, +# so calling it directly would fail the job on paths that are expected to work. +# Callers must gate on $LASTEXITCODE, which this function leaves untouched. +# +# Arguments are passed as a single array rather than splatted, so tokens such as +# "--tag" reach the command instead of being bound as function parameters. +function Invoke-Native { + param( + [Parameter(Mandatory = $true)][string] $Command, + [string[]] $Arguments = @() + ) + + # Function-scoped, so the caller keeps 'Stop' for cmdlet errors. + $ErrorActionPreference = "Continue" + & $Command @Arguments 2>&1 | ForEach-Object { Write-Host "$_" } +} + +# Resolves the content-addressed tag for the image described by .gitlab/windows/image. +function Get-WindowsCiImage { + $hash = & (Join-Path $WindowsCiRoot "image\compute-image-hash.ps1") + if ($LASTEXITCODE -ne 0 -or $hash -notmatch '^[0-9a-f]{16}$') { + throw "compute-image-hash.ps1 did not produce a valid hash (exit=$LASTEXITCODE, output='$hash')" + } + return "${env:WINDOWS_BUILD_IMAGE_BASE}:${hash}" +} + +function Test-WindowsCiImage { + param([Parameter(Mandatory = $true)][string] $Image) + + Invoke-Native docker @("manifest", "inspect", $Image) + return $LASTEXITCODE -eq 0 +} diff --git a/.gitlab/windows/image/Dockerfile b/.gitlab/windows/image/Dockerfile new file mode 100644 index 00000000000..8cc80f169cc --- /dev/null +++ b/.gitlab/windows/image/Dockerfile @@ -0,0 +1,60 @@ +# escape=` + +FROM eclipse-temurin:8-jdk-windowsservercore-ltsc2025@sha256:0ce93c7ea851ec5ea7d35393d106bd717697865b6111a681964f6eb7147f14c8 AS temurin8 +FROM eclipse-temurin:11-jdk-windowsservercore-ltsc2025@sha256:f58946540fe9ae7c685b9d2b40e58746b2332fa9bd8546ec4d2881143d9da99e AS temurin11 +FROM eclipse-temurin:17-jdk-windowsservercore-ltsc2025@sha256:aa7cfb793cd567a3bb2178600c7179249e6058f7a0b9b9e3b44be4cb12bde11b AS temurin17 +FROM eclipse-temurin:25-jdk-windowsservercore-ltsc2025@sha256:28a4b5421e4c03e4a86c900971fd835c1b343564a972da324bdabf666766952b AS temurin25 + +FROM eclipse-temurin:21-jdk-windowsservercore-ltsc2025@sha256:8828b43d3c6be114c39da1696c48d3b76ee46ab4bf08b5afeaf057174b60a9f5 + +SHELL ["powershell", "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +USER ContainerAdministrator + +COPY --from=temurin8 C:/openjdk-8 C:/openjdk-8 +COPY --from=temurin11 C:/openjdk-11 C:/openjdk-11 +COPY --from=temurin17 C:/openjdk-17 C:/openjdk-17 +COPY --from=temurin25 C:/openjdk-25 C:/openjdk-25 + +ENV JAVA_8_HOME="C:\openjdk-8" +ENV JAVA_11_HOME="C:\openjdk-11" +ENV JAVA_17_HOME="C:\openjdk-17" +ENV JAVA_21_HOME="C:\openjdk-21" +ENV JAVA_25_HOME="C:\openjdk-25" +ENV JAVA_HOME="C:\openjdk-21" + +ARG MINGIT_VERSION="2.55.0.5" +ARG MINGIT_RELEASE="v2.55.0.windows.5" +ARG MINGIT_SHA256="56d7b226b7693196cfc71fef26568f536c4a021ab6c37ff2db4287bed908e96e" + +RUN $archive = 'C:\mingit.zip'; ` + $url = "https://github.com/git-for-windows/git/releases/download/$env:MINGIT_RELEASE/MinGit-$env:MINGIT_VERSION-64-bit.zip"; ` + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; ` + (New-Object System.Net.WebClient).DownloadFile($url, $archive); ` + $actual = (Get-FileHash -Path $archive -Algorithm SHA256).Hash.ToLowerInvariant(); ` + if ($actual -ne $env:MINGIT_SHA256) { throw "MinGit checksum mismatch: expected $env:MINGIT_SHA256, got $actual" }; ` + Expand-Archive -Path $archive -DestinationPath C:\MinGit -Force; ` + Remove-Item $archive -Force + +ENV PATH="C:\MinGit\cmd;C:\MinGit\mingw64\bin;C:\openjdk-21\bin;C:\Windows\System32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0" + +# The longest source path in the repository is over 200 characters, and relocated +# build outputs under C:\work\workspace\... push past the 260-character MAX_PATH +# default. The runner sets core.longpaths on the host checkout; the container +# needs both the Win32 opt-in and its own git setting. +RUN Set-ItemProperty ` + -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' ` + -Name 'LongPathsEnabled' -Value 1 -Type DWord; ` + git config --system core.longpaths true + +# java -version and git --version write to stderr, which PowerShell turns into a +# terminating error under the base image's $ErrorActionPreference = 'Stop'. +# Capture the output and gate on the exit code instead. +RUN foreach ($javaHome in @($env:JAVA_8_HOME, $env:JAVA_11_HOME, $env:JAVA_17_HOME, $env:JAVA_21_HOME, $env:JAVA_25_HOME)) { ` + $output = (& "$javaHome\bin\java.exe" -version 2>&1 | Out-String).Trim(); ` + if ($LASTEXITCODE -ne 0) { throw "java -version failed for $javaHome" }; ` + Write-Host "$javaHome -> $output"; ` + }; ` + $gitVersion = (git --version 2>&1 | Out-String).Trim(); ` + if ($LASTEXITCODE -ne 0) { throw 'git --version failed' }; ` + Write-Host $gitVersion diff --git a/.gitlab/windows/image/compute-image-hash.ps1 b/.gitlab/windows/image/compute-image-hash.ps1 new file mode 100644 index 00000000000..81543c89510 --- /dev/null +++ b/.gitlab/windows/image/compute-image-hash.ps1 @@ -0,0 +1,27 @@ +$ErrorActionPreference = "Stop" + +try { + $root = (Resolve-Path $PSScriptRoot).Path.TrimEnd("\") + $files = Get-ChildItem -Path $root -File -Recurse -Force | Sort-Object -Property FullName + $entries = $files | ForEach-Object { + $relativePath = $_.FullName.Substring($root.Length + 1).Replace("\", "/") + $fileHash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() + "$relativePath`:$fileHash" + } + $combined = $entries -join "`n" + $bytes = [System.Text.Encoding]::UTF8.GetBytes($combined) + $stream = [System.IO.MemoryStream]::new($bytes) + try { + $digest = (Get-FileHash -InputStream $stream -Algorithm SHA256).Hash.ToLowerInvariant() + } + finally { + $stream.Dispose() + } + + Write-Output $digest.Substring(0, 16) + exit 0 +} +catch { + [Console]::Error.WriteLine("compute-image-hash.ps1: $_") + exit 1 +} diff --git a/.gitlab/windows/run-base-tests.ps1 b/.gitlab/windows/run-base-tests.ps1 new file mode 100644 index 00000000000..51b007b815d --- /dev/null +++ b/.gitlab/windows/run-base-tests.ps1 @@ -0,0 +1,76 @@ +$ErrorActionPreference = "Stop" + +Set-Location "C:\work" +git config --global --add safe.directory "C:/work" + +if ($env:CI_SPLIT -notmatch '^[1-4]/4$') { + throw "Expected CI_SPLIT to be one of 1/4, 2/4, 3/4, or 4/4; got '$env:CI_SPLIT'" +} +if ([string]::IsNullOrWhiteSpace($env:GRADLE_TARGET)) { + throw "GRADLE_TARGET is required" +} +if ([string]::IsNullOrWhiteSpace($env:testJvm)) { + throw "testJvm is required" +} + +$split = $env:CI_SPLIT.Split("/") +$env:CI_NODE_INDEX = $split[0] +$env:CI_NODE_TOTAL = $split[1] + +$env:GRADLE_USER_HOME = "C:\work\.gradle" +$env:ORG_GRADLE_PROJECT_mavenRepositoryProxy = $env:MAVEN_REPOSITORY_PROXY +$env:ORG_GRADLE_PROJECT_gradlePluginProxy = $env:GRADLE_PLUGIN_PROXY + +$javaHomeVariables = (Get-ChildItem Env: | Where-Object Name -Match '^JAVA_[A-Z0-9_]+_HOME$' | Sort-Object Name).Name -Join ',' +# This file wins over the project gradle.properties, so org.gradle.jvmargs must +# repeat every flag the project file sets (currently -XX:MaxMetaspaceSize). +$gradleProperties = @( + "org.gradle.java.installations.auto-detect=false", + "org.gradle.java.installations.auto-download=false", + "org.gradle.java.installations.fromEnv=$javaHomeVariables", + "org.gradle.jvmargs=-Xms1g -Xmx4g -XX:MaxMetaspaceSize=1g -Djava.util.prefs.userRoot=C:/tmp/java-prefs -Ddatadog.forkedMinHeapSize=128M -Ddatadog.forkedMaxHeapSize=1024M -XX:ErrorFile=C:/tmp/hs_err_pid%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:/tmp" +) +New-Item -ItemType Directory -Force -Path $env:GRADLE_USER_HOME | Out-Null +$gradleProperties | Set-Content -Path (Join-Path $env:GRADLE_USER_HOME "gradle.properties") -Encoding ASCII + +# Route the Gradle distribution download through the MASS pull-through cache, the +# same way .gitlab-ci.yml does for every Linux job. The edit is reverted before the +# job ends because the GitLab cache key is derived from this file's contents. +$wrapperProperties = "gradle\wrapper\gradle-wrapper.properties" +$originalWrapperProperties = Get-Content -Path $wrapperProperties -Raw +if (-not [string]::IsNullOrWhiteSpace($env:MASS_READ_URL)) { + $massHost = ($env:MASS_READ_URL -replace '^https://', '').TrimEnd('/') + (Get-Content -Path $wrapperProperties) ` + -replace '^(distributionUrl=.*)services\.gradle\.org', "`$1$massHost/internal/artifact/services.gradle.org" ` + | Set-Content -Path $wrapperProperties -Encoding ASCII + Write-Output "Routing the Gradle distribution through $massHost" +} else { + Write-Warning "MASS_READ_URL is not set; downloading the Gradle distribution directly" +} + +try { + Write-Output "Running $env:GRADLE_TARGET on Java $env:testJvm, partition $env:CI_SPLIT" + java --version + git --version + & .\gradlew.bat --version + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + + $gradleArguments = @( + $env:GRADLE_TARGET, + "-PskipFlakyTests", + "-PtestJvm=$($env:testJvm)", + "-Pslot=$($env:CI_SPLIT)", + "--build-cache", + "--stacktrace", + "--no-daemon", + "--parallel", + "--max-workers=4", + "--continue" + ) + + & .\gradlew.bat @gradleArguments + exit $LASTEXITCODE +} +finally { + Set-Content -Path $wrapperProperties -Value $originalWrapperProperties -NoNewline +} From aa0cea7d7b9db21121a781a77a0a7949cb685019 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Wed, 26 Aug 2026 14:06:55 -0400 Subject: [PATCH 02/10] Fix PowerShell quoting in Windows CI image --- .gitlab/windows/image/Dockerfile | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.gitlab/windows/image/Dockerfile b/.gitlab/windows/image/Dockerfile index 8cc80f169cc..beb7a474c18 100644 --- a/.gitlab/windows/image/Dockerfile +++ b/.gitlab/windows/image/Dockerfile @@ -27,12 +27,14 @@ ARG MINGIT_VERSION="2.55.0.5" ARG MINGIT_RELEASE="v2.55.0.windows.5" ARG MINGIT_SHA256="56d7b226b7693196cfc71fef26568f536c4a021ab6c37ff2db4287bed908e96e" +# The classic Windows Docker builder consumes nested double quotes in shell-form +# RUN commands. Use PowerShell single-quoted strings and -f formatting here. RUN $archive = 'C:\mingit.zip'; ` - $url = "https://github.com/git-for-windows/git/releases/download/$env:MINGIT_RELEASE/MinGit-$env:MINGIT_VERSION-64-bit.zip"; ` + $url = 'https://github.com/git-for-windows/git/releases/download/{0}/MinGit-{1}-64-bit.zip' -f $env:MINGIT_RELEASE, $env:MINGIT_VERSION; ` [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; ` (New-Object System.Net.WebClient).DownloadFile($url, $archive); ` $actual = (Get-FileHash -Path $archive -Algorithm SHA256).Hash.ToLowerInvariant(); ` - if ($actual -ne $env:MINGIT_SHA256) { throw "MinGit checksum mismatch: expected $env:MINGIT_SHA256, got $actual" }; ` + if ($actual -ne $env:MINGIT_SHA256) { throw ('MinGit checksum mismatch: expected {0}, got {1}' -f $env:MINGIT_SHA256, $actual) }; ` Expand-Archive -Path $archive -DestinationPath C:\MinGit -Force; ` Remove-Item $archive -Force @@ -51,9 +53,10 @@ RUN Set-ItemProperty ` # terminating error under the base image's $ErrorActionPreference = 'Stop'. # Capture the output and gate on the exit code instead. RUN foreach ($javaHome in @($env:JAVA_8_HOME, $env:JAVA_11_HOME, $env:JAVA_17_HOME, $env:JAVA_21_HOME, $env:JAVA_25_HOME)) { ` - $output = (& "$javaHome\bin\java.exe" -version 2>&1 | Out-String).Trim(); ` - if ($LASTEXITCODE -ne 0) { throw "java -version failed for $javaHome" }; ` - Write-Host "$javaHome -> $output"; ` + $java = Join-Path $javaHome 'bin\java.exe'; ` + $output = (& $java -version 2>&1 | Out-String).Trim(); ` + if ($LASTEXITCODE -ne 0) { throw ('java -version failed for {0}' -f $javaHome) }; ` + Write-Host ('{0} -> {1}' -f $javaHome, $output); ` }; ` $gitVersion = (git --version 2>&1 | Out-String).Trim(); ` if ($LASTEXITCODE -ne 0) { throw 'git --version failed' }; ` From f17b9e89304e1d2c3f722f2b30ce09532b0a6480 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Wed, 26 Aug 2026 14:27:46 -0400 Subject: [PATCH 03/10] Fix Windows CI prototype image build --- .gitlab/windows-tests.yml | 28 +++++++------------- .gitlab/windows/README.md | 16 +++++------ .gitlab/windows/ci-common.ps1 | 17 +++--------- .gitlab/windows/image/Dockerfile | 8 +++--- .gitlab/windows/image/compute-image-hash.ps1 | 27 ------------------- 5 files changed, 26 insertions(+), 70 deletions(-) delete mode 100644 .gitlab/windows/image/compute-image-hash.ps1 diff --git a/.gitlab/windows-tests.yml b/.gitlab/windows-tests.yml index 1c51e06ea21..14e4e1480ee 100644 --- a/.gitlab/windows-tests.yml +++ b/.gitlab/windows-tests.yml @@ -1,5 +1,5 @@ variables: - WINDOWS_BUILD_IMAGE_BASE: "registry.ddbuild.io/ci/dd-trace-java/dd-trace-java-windows-docker-build" + WINDOWS_BUILD_IMAGE: "registry.ddbuild.io/ci/dd-trace-java/dd-trace-java-windows-docker-build:prototype-alexeyk-gitlab-windows-tests" .windows_gradle_cache_paths: &windows_gradle_cache_paths - .gradle/wrapper/ @@ -22,23 +22,17 @@ build-windows-ci-image: $image = Get-WindowsCiImage Write-Output "Target image: $image" - if (Test-WindowsCiImage $image) { - Write-Output "Image $image already exists; skipping rebuild." - exit 0 - } - $buildArguments = @( "build", "--file", ".gitlab/windows/image/Dockerfile", - "--tag", $image, - "--tag", "${env:WINDOWS_BUILD_IMAGE_BASE}:latest" + "--tag", $image ) - Invoke-Native docker @("pull", "${env:WINDOWS_BUILD_IMAGE_BASE}:latest") + Invoke-Native docker @("pull", $image) if ($LASTEXITCODE -eq 0) { - $buildArguments += @("--cache-from", "${env:WINDOWS_BUILD_IMAGE_BASE}:latest") + $buildArguments += @("--cache-from", $image) } else { - Write-Output "No existing latest image found; building without a registry layer cache." + Write-Output "No existing prototype image found; building without a registry layer cache." } $buildArguments += ".gitlab/windows/image" @@ -47,9 +41,6 @@ build-windows-ci-image: Invoke-Native docker @("push", $image) if ($LASTEXITCODE -ne 0) { throw "docker push $image failed" } - - Invoke-Native docker @("push", "${env:WINDOWS_BUILD_IMAGE_BASE}:latest") - if ($LASTEXITCODE -ne 0) { throw "docker push latest failed" } id_tokens: CI_IDENTITIES_GITLAB_ID_TOKEN: aud: ci-identities @@ -64,8 +55,8 @@ test-base-windows: needs: - job: build artifacts: false - # Reads the content-addressed image from registry.ddbuild.io, so it needs the - # same CI identity as the job that publishes it. + # Reads the prototype image from registry.ddbuild.io, so it needs the same CI + # identity as the job that publishes it. id_tokens: CI_IDENTITIES_GITLAB_ID_TOKEN: aud: ci-identities @@ -124,8 +115,9 @@ test-base-windows: $image = Get-WindowsCiImage Write-Output "Expected Windows test image: $image" - if (-not (Test-WindowsCiImage $image)) { - Write-Host "ERROR: Windows test image not found at $image." + Invoke-Native docker @("pull", $image) + if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Could not pull Windows test image $image." Write-Host "Manually run build-windows-ci-image, wait for it to finish, then retry this job." exit 1 } diff --git a/.gitlab/windows/README.md b/.gitlab/windows/README.md index 910fa852634..ce6b2468568 100644 --- a/.gitlab/windows/README.md +++ b/.gitlab/windows/README.md @@ -11,22 +11,22 @@ job, so that the execution model can be validated before moving the image to 1. Push the branch and open its GitLab pipeline. 2. Manually run `build-windows-ci-image`. -3. Wait for the content-addressed image to be pushed to - `registry.ddbuild.io/ci/dd-trace-java/dd-trace-java-windows-docker-build`. +3. Wait for the image to be pushed to + `registry.ddbuild.io/ci/dd-trace-java/dd-trace-java-windows-docker-build:prototype-alexeyk-gitlab-windows-tests`. 4. Run or retry the four `test-base-windows` matrix jobs. -The image producer also publishes `latest`, but that tag is used only to seed the next -Docker layer cache. Test jobs always recompute and use the content-addressed tag. +The image producer always overwrites this single mutable prototype tag and uses the +previous image as its Docker layer cache. Test jobs explicitly pull the tag before use, +so a long-lived Windows runner does not reuse a stale local copy. The test job is manual and non-blocking on feature branches. It runs automatically but remains non-blocking on merge-queue branches and `master`. ## Updating the image -Any change under `image/`, including the hashing script itself, produces a new image tag. -The test job fails with an instruction to run `build-windows-ci-image` when that tag does -not exist yet. `ci-common.ps1` lives outside `image/` on purpose: it runs on the host, not -in the container, so it must not change the image tag. +Run `build-windows-ci-image` after changing anything under `image/`. The same prototype +tag is replaced after a successful build, which avoids accumulating per-experiment tags. +The test job fails with an instruction to run the producer when it cannot pull that tag. ## Caching diff --git a/.gitlab/windows/ci-common.ps1 b/.gitlab/windows/ci-common.ps1 index c24bff3ff75..59dc8ee85d4 100644 --- a/.gitlab/windows/ci-common.ps1 +++ b/.gitlab/windows/ci-common.ps1 @@ -2,7 +2,6 @@ # Dot-source this file at the top of a job script: . .gitlab/windows/ci-common.ps1 $ErrorActionPreference = "Stop" -$WindowsCiRoot = $PSScriptRoot # Runs a native command with its stderr merged into stdout. # @@ -25,18 +24,10 @@ function Invoke-Native { & $Command @Arguments 2>&1 | ForEach-Object { Write-Host "$_" } } -# Resolves the content-addressed tag for the image described by .gitlab/windows/image. +# Resolves the mutable image tag used by this personal prototype. function Get-WindowsCiImage { - $hash = & (Join-Path $WindowsCiRoot "image\compute-image-hash.ps1") - if ($LASTEXITCODE -ne 0 -or $hash -notmatch '^[0-9a-f]{16}$') { - throw "compute-image-hash.ps1 did not produce a valid hash (exit=$LASTEXITCODE, output='$hash')" + if ([string]::IsNullOrWhiteSpace($env:WINDOWS_BUILD_IMAGE)) { + throw "WINDOWS_BUILD_IMAGE is not set" } - return "${env:WINDOWS_BUILD_IMAGE_BASE}:${hash}" -} - -function Test-WindowsCiImage { - param([Parameter(Mandatory = $true)][string] $Image) - - Invoke-Native docker @("manifest", "inspect", $Image) - return $LASTEXITCODE -eq 0 + return $env:WINDOWS_BUILD_IMAGE } diff --git a/.gitlab/windows/image/Dockerfile b/.gitlab/windows/image/Dockerfile index beb7a474c18..3c6bb5ac375 100644 --- a/.gitlab/windows/image/Dockerfile +++ b/.gitlab/windows/image/Dockerfile @@ -49,10 +49,10 @@ RUN Set-ItemProperty ` -Name 'LongPathsEnabled' -Value 1 -Type DWord; ` git config --system core.longpaths true -# java -version and git --version write to stderr, which PowerShell turns into a -# terminating error under the base image's $ErrorActionPreference = 'Stop'. -# Capture the output and gate on the exit code instead. -RUN foreach ($javaHome in @($env:JAVA_8_HOME, $env:JAVA_11_HOME, $env:JAVA_17_HOME, $env:JAVA_21_HOME, $env:JAVA_25_HOME)) { ` +# Native tools can write version information to stderr. Run them with non-terminating +# native error handling, capture their output, and gate each call on its exit code. +RUN $ErrorActionPreference = 'Continue'; ` + foreach ($javaHome in @($env:JAVA_8_HOME, $env:JAVA_11_HOME, $env:JAVA_17_HOME, $env:JAVA_21_HOME, $env:JAVA_25_HOME)) { ` $java = Join-Path $javaHome 'bin\java.exe'; ` $output = (& $java -version 2>&1 | Out-String).Trim(); ` if ($LASTEXITCODE -ne 0) { throw ('java -version failed for {0}' -f $javaHome) }; ` diff --git a/.gitlab/windows/image/compute-image-hash.ps1 b/.gitlab/windows/image/compute-image-hash.ps1 deleted file mode 100644 index 81543c89510..00000000000 --- a/.gitlab/windows/image/compute-image-hash.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -$ErrorActionPreference = "Stop" - -try { - $root = (Resolve-Path $PSScriptRoot).Path.TrimEnd("\") - $files = Get-ChildItem -Path $root -File -Recurse -Force | Sort-Object -Property FullName - $entries = $files | ForEach-Object { - $relativePath = $_.FullName.Substring($root.Length + 1).Replace("\", "/") - $fileHash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant() - "$relativePath`:$fileHash" - } - $combined = $entries -join "`n" - $bytes = [System.Text.Encoding]::UTF8.GetBytes($combined) - $stream = [System.IO.MemoryStream]::new($bytes) - try { - $digest = (Get-FileHash -InputStream $stream -Algorithm SHA256).Hash.ToLowerInvariant() - } - finally { - $stream.Dispose() - } - - Write-Output $digest.Substring(0, 16) - exit 0 -} -catch { - [Console]::Error.WriteLine("compute-image-hash.ps1: $_") - exit 1 -} From 4d4bcda8f210a646169e190cdaa12e43e63e6e47 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Wed, 26 Aug 2026 14:42:57 -0400 Subject: [PATCH 04/10] Try Windows CI on Server 2022 runners --- .gitlab/windows-tests.yml | 4 ++-- .gitlab/windows/README.md | 2 +- .gitlab/windows/image/Dockerfile | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitlab/windows-tests.yml b/.gitlab/windows-tests.yml index 14e4e1480ee..5209b67577a 100644 --- a/.gitlab/windows-tests.yml +++ b/.gitlab/windows-tests.yml @@ -9,7 +9,7 @@ variables: build-windows-ci-image: stage: build timeout: 2h - tags: [ "windows-v2:2025" ] + tags: [ "windows-v2:2022" ] when: manual allow_failure: true hooks: @@ -48,7 +48,7 @@ build-windows-ci-image: test-base-windows: stage: tests-windows timeout: 2h - tags: [ "windows-v2:2025" ] + tags: [ "windows-v2:2022" ] hooks: pre_get_sources_script: - git config --system core.longpaths true diff --git a/.gitlab/windows/README.md b/.gitlab/windows/README.md index ce6b2468568..864e6e997f3 100644 --- a/.gitlab/windows/README.md +++ b/.gitlab/windows/README.md @@ -42,6 +42,6 @@ everything four times over. and docker writes progress and "manifest unknown" to stderr. - The container gets `LongPathsEnabled` and `core.longpaths`; the deepest relocated build outputs under `C:\work\workspace\...` exceed the 260-character `MAX_PATH` default. -- Temurin publishes no JDK 21 newer than `21.0.12+8` for `windowsservercore-ltsc2025`, +- Temurin publishes no JDK 21 newer than `21.0.12+8` for `windowsservercore-ltsc2022`, while 8, 11, 17, and 25 are current. Expect some failures on the Java 21 matrix to be JDK-version artifacts rather than Windows-specific. diff --git a/.gitlab/windows/image/Dockerfile b/.gitlab/windows/image/Dockerfile index 3c6bb5ac375..76954b39bb2 100644 --- a/.gitlab/windows/image/Dockerfile +++ b/.gitlab/windows/image/Dockerfile @@ -1,11 +1,11 @@ # escape=` -FROM eclipse-temurin:8-jdk-windowsservercore-ltsc2025@sha256:0ce93c7ea851ec5ea7d35393d106bd717697865b6111a681964f6eb7147f14c8 AS temurin8 -FROM eclipse-temurin:11-jdk-windowsservercore-ltsc2025@sha256:f58946540fe9ae7c685b9d2b40e58746b2332fa9bd8546ec4d2881143d9da99e AS temurin11 -FROM eclipse-temurin:17-jdk-windowsservercore-ltsc2025@sha256:aa7cfb793cd567a3bb2178600c7179249e6058f7a0b9b9e3b44be4cb12bde11b AS temurin17 -FROM eclipse-temurin:25-jdk-windowsservercore-ltsc2025@sha256:28a4b5421e4c03e4a86c900971fd835c1b343564a972da324bdabf666766952b AS temurin25 +FROM eclipse-temurin:8-jdk-windowsservercore-ltsc2022@sha256:eabbefd4cede69e8127cc87507a690f76c1a953d1eb7d9495c22d645751d4848 AS temurin8 +FROM eclipse-temurin:11-jdk-windowsservercore-ltsc2022@sha256:b28aa578a01b67fa30eddc71615369d71560458636f0f74dc39ededbdb9b9a67 AS temurin11 +FROM eclipse-temurin:17-jdk-windowsservercore-ltsc2022@sha256:d3e4b6058da8b3b54acb97b708b141650fdb5234b7b5dac92017263e1cf530b3 AS temurin17 +FROM eclipse-temurin:25-jdk-windowsservercore-ltsc2022@sha256:ef8836e0ec8a43d98a0d0d0ef7accec7e466e3f67533649b0887ba77cf295a9e AS temurin25 -FROM eclipse-temurin:21-jdk-windowsservercore-ltsc2025@sha256:8828b43d3c6be114c39da1696c48d3b76ee46ab4bf08b5afeaf057174b60a9f5 +FROM eclipse-temurin:21-jdk-windowsservercore-ltsc2022@sha256:1ef9cebf526129fea284f8b92b4736b6ffcdaaee151603820a787047af13625f SHELL ["powershell", "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] From 8d92d62905a6ffbd27f86dc41b161c37c7d8a525 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Wed, 26 Aug 2026 15:00:58 -0400 Subject: [PATCH 05/10] Revert "Try Windows CI on Server 2022 runners" This reverts commit 4d4bcda8f210a646169e190cdaa12e43e63e6e47. --- .gitlab/windows-tests.yml | 4 ++-- .gitlab/windows/README.md | 2 +- .gitlab/windows/image/Dockerfile | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitlab/windows-tests.yml b/.gitlab/windows-tests.yml index 5209b67577a..14e4e1480ee 100644 --- a/.gitlab/windows-tests.yml +++ b/.gitlab/windows-tests.yml @@ -9,7 +9,7 @@ variables: build-windows-ci-image: stage: build timeout: 2h - tags: [ "windows-v2:2022" ] + tags: [ "windows-v2:2025" ] when: manual allow_failure: true hooks: @@ -48,7 +48,7 @@ build-windows-ci-image: test-base-windows: stage: tests-windows timeout: 2h - tags: [ "windows-v2:2022" ] + tags: [ "windows-v2:2025" ] hooks: pre_get_sources_script: - git config --system core.longpaths true diff --git a/.gitlab/windows/README.md b/.gitlab/windows/README.md index 864e6e997f3..ce6b2468568 100644 --- a/.gitlab/windows/README.md +++ b/.gitlab/windows/README.md @@ -42,6 +42,6 @@ everything four times over. and docker writes progress and "manifest unknown" to stderr. - The container gets `LongPathsEnabled` and `core.longpaths`; the deepest relocated build outputs under `C:\work\workspace\...` exceed the 260-character `MAX_PATH` default. -- Temurin publishes no JDK 21 newer than `21.0.12+8` for `windowsservercore-ltsc2022`, +- Temurin publishes no JDK 21 newer than `21.0.12+8` for `windowsservercore-ltsc2025`, while 8, 11, 17, and 25 are current. Expect some failures on the Java 21 matrix to be JDK-version artifacts rather than Windows-specific. diff --git a/.gitlab/windows/image/Dockerfile b/.gitlab/windows/image/Dockerfile index 76954b39bb2..3c6bb5ac375 100644 --- a/.gitlab/windows/image/Dockerfile +++ b/.gitlab/windows/image/Dockerfile @@ -1,11 +1,11 @@ # escape=` -FROM eclipse-temurin:8-jdk-windowsservercore-ltsc2022@sha256:eabbefd4cede69e8127cc87507a690f76c1a953d1eb7d9495c22d645751d4848 AS temurin8 -FROM eclipse-temurin:11-jdk-windowsservercore-ltsc2022@sha256:b28aa578a01b67fa30eddc71615369d71560458636f0f74dc39ededbdb9b9a67 AS temurin11 -FROM eclipse-temurin:17-jdk-windowsservercore-ltsc2022@sha256:d3e4b6058da8b3b54acb97b708b141650fdb5234b7b5dac92017263e1cf530b3 AS temurin17 -FROM eclipse-temurin:25-jdk-windowsservercore-ltsc2022@sha256:ef8836e0ec8a43d98a0d0d0ef7accec7e466e3f67533649b0887ba77cf295a9e AS temurin25 +FROM eclipse-temurin:8-jdk-windowsservercore-ltsc2025@sha256:0ce93c7ea851ec5ea7d35393d106bd717697865b6111a681964f6eb7147f14c8 AS temurin8 +FROM eclipse-temurin:11-jdk-windowsservercore-ltsc2025@sha256:f58946540fe9ae7c685b9d2b40e58746b2332fa9bd8546ec4d2881143d9da99e AS temurin11 +FROM eclipse-temurin:17-jdk-windowsservercore-ltsc2025@sha256:aa7cfb793cd567a3bb2178600c7179249e6058f7a0b9b9e3b44be4cb12bde11b AS temurin17 +FROM eclipse-temurin:25-jdk-windowsservercore-ltsc2025@sha256:28a4b5421e4c03e4a86c900971fd835c1b343564a972da324bdabf666766952b AS temurin25 -FROM eclipse-temurin:21-jdk-windowsservercore-ltsc2022@sha256:1ef9cebf526129fea284f8b92b4736b6ffcdaaee151603820a787047af13625f +FROM eclipse-temurin:21-jdk-windowsservercore-ltsc2025@sha256:8828b43d3c6be114c39da1696c48d3b76ee46ab4bf08b5afeaf057174b60a9f5 SHELL ["powershell", "-NoLogo", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] From af66eb9ba3fda40045fc28d189f520fb6dbb2ec4 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Fri, 28 Aug 2026 12:57:46 -0400 Subject: [PATCH 06/10] Fixing tests on Windows. --- .gitlab/windows-tests.yml | 2 + .../trace/civisibility/ci/AppVeyorInfo.java | 3 +- .../git/LocalFSGitInfoExtractor.java | 13 +++-- .../civisibility/git/tree/ShellGitClient.java | 25 ++++++--- .../CompilerAidedSourcePathResolver.java | 20 +++++-- .../source/index/PackageResolverImpl.java | 4 +- .../trace/civisibility/utils/FileUtils.java | 18 +++++- ...CompilerAidedSourcePathResolverTest.groovy | 3 +- .../RepoIndexSourcePathResolverTest.groovy | 9 +-- .../source/index/RepoIndexTest.groovy | 24 ++++---- .../utils/ShellCommandExecutorTest.groovy | 7 ++- .../groovy/datadog/cws/tls/ErpcTlsTest.groovy | 4 ++ .../datadog/trace/api/ConfigTest.groovy | 19 ++++--- .../trace/util/ProcessSupervisorTest.groovy | 3 +- .../trace/util/TempLocationManagerTest.java | 49 +++++++++------- utils/test-utils/build.gradle.kts | 1 + .../trace/test/util/PortableCommand.java | 56 +++++++++++++++++++ 17 files changed, 191 insertions(+), 69 deletions(-) create mode 100644 utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java diff --git a/.gitlab/windows-tests.yml b/.gitlab/windows-tests.yml index 14e4e1480ee..883cb270c9c 100644 --- a/.gitlab/windows-tests.yml +++ b/.gitlab/windows-tests.yml @@ -15,6 +15,7 @@ build-windows-ci-image: hooks: pre_get_sources_script: - git config --system core.longpaths true + - git config --global core.autocrlf false script: - | . .gitlab/windows/ci-common.ps1 @@ -52,6 +53,7 @@ test-base-windows: hooks: pre_get_sources_script: - git config --system core.longpaths true + - git config --global core.autocrlf false needs: - job: build artifacts: false diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AppVeyorInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AppVeyorInfo.java index f8bc31dd090..8f0021d5e78 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AppVeyorInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AppVeyorInfo.java @@ -65,7 +65,8 @@ private static String getCommitMessage(String messageSubject, String messageBody if (messageBody == null) { return messageSubject; } - return String.format("%s%n%s", messageSubject, messageBody); + // Git commit messages use LF regardless of the host platform. + return messageSubject + '\n' + messageBody; } @Override diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/LocalFSGitInfoExtractor.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/LocalFSGitInfoExtractor.java index 55832a06836..4845e3c838c 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/LocalFSGitInfoExtractor.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/LocalFSGitInfoExtractor.java @@ -411,11 +411,16 @@ private String readFile(final Path filepath) throws IOException { } final String content = new String(Files.readAllBytes(filepath)); - if (content.endsWith("\n")) { - return content.substring(0, content.length() - 1); + // Git metadata can end with either LF or CRLF, regardless of the host platform. + int end = content.length(); + while (end > 0) { + char last = content.charAt(end - 1); + if (last != '\r' && last != '\n') { + break; + } + end--; } - - return content; + return content.substring(0, end); } private static VersionedPackGitInfoExtractor lookupExtractor(final short packVersion) { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java index 8f63a088b28..1b9762ce73c 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java @@ -227,11 +227,14 @@ public void unshallow(@Nullable String remoteCommitReference) public String getGitFolder() throws IOException, TimeoutException, InterruptedException { return executeCommand( Command.OTHER, - () -> - commandExecutor - .executeCommand( - IOUtils::readFully, buildGitCommand("rev-parse", "--absolute-git-dir")) - .trim()); + () -> { + String path = + commandExecutor + .executeCommand( + IOUtils::readFully, buildGitCommand("rev-parse", "--absolute-git-dir")) + .trim(); + return Paths.get(path).normalize().toString(); + }); } /** @@ -248,10 +251,14 @@ public String getGitFolder() throws IOException, TimeoutException, InterruptedEx public String getRepoRoot() throws IOException, TimeoutException, InterruptedException { return executeCommand( Command.OTHER, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, buildGitCommand("rev-parse", "--show-toplevel")) - .trim()); + () -> { + String path = + commandExecutor + .executeCommand( + IOUtils::readFully, buildGitCommand("rev-parse", "--show-toplevel")) + .trim(); + return Paths.get(path).normalize().toString(); + }); } /** diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/CompilerAidedSourcePathResolver.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/CompilerAidedSourcePathResolver.java index 35cef73d1d6..2a851e5df91 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/CompilerAidedSourcePathResolver.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/CompilerAidedSourcePathResolver.java @@ -1,7 +1,9 @@ package datadog.trace.civisibility.source; import datadog.compiler.utils.CompilerUtils; -import java.io.File; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; import java.util.Collections; import javax.annotation.Nonnull; @@ -9,19 +11,25 @@ public class CompilerAidedSourcePathResolver implements SourcePathResolver { - private final String repoRoot; + private final Path repoRoot; public CompilerAidedSourcePathResolver(String repoRoot) { - this.repoRoot = repoRoot.endsWith(File.separator) ? repoRoot : repoRoot + File.separator; + this.repoRoot = Paths.get(repoRoot).normalize(); } @Nonnull @Override public Collection getSourcePaths(@Nonnull Class c) { String absoluteSourcePath = CompilerUtils.getSourcePath(c); - if (absoluteSourcePath != null && absoluteSourcePath.startsWith(repoRoot)) { - return Collections.singletonList(absoluteSourcePath.substring(repoRoot.length())); - } else { + if (absoluteSourcePath == null) { + return Collections.emptyList(); + } + try { + Path sourcePath = Paths.get(absoluteSourcePath).normalize(); + return sourcePath.startsWith(repoRoot) + ? Collections.singletonList(repoRoot.relativize(sourcePath).toString()) + : Collections.emptyList(); + } catch (InvalidPathException e) { return Collections.emptyList(); } } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/PackageResolverImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/PackageResolverImpl.java index c03203a09d8..d0d047867a3 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/PackageResolverImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/PackageResolverImpl.java @@ -2,7 +2,6 @@ import datadog.trace.api.civisibility.domain.Language; import java.io.BufferedReader; -import java.io.File; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Files; @@ -70,7 +69,8 @@ public Path getPackage(Path sourceFile) throws IOException { String packageName = line.substring(packageNameStart, packageNameEnd); Path packagePath; try { - packagePath = fileSystem.getPath(packageName.replace('.', File.separatorChar)); + packagePath = + fileSystem.getPath(packageName.replace('.', fileSystem.getSeparator().charAt(0))); } catch (InvalidPathException e) { log.debug("Invalid package {} found for source file {}", packageName, sourceFile, e); continue; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/utils/FileUtils.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/utils/FileUtils.java index eb668ac8b4b..d32f9222b49 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/utils/FileUtils.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/utils/FileUtils.java @@ -3,12 +3,14 @@ import datadog.environment.SystemProperties; import de.thetaphi.forbiddenapis.SuppressForbidden; import java.io.IOException; +import java.nio.file.AccessDeniedException; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.DosFileAttributeView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,7 +28,7 @@ public static void delete(Path directory) throws IOException { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); + deleteFile(file); return FileVisitResult.CONTINUE; } @@ -38,6 +40,20 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx }); } + private static void deleteFile(Path file) throws IOException { + try { + Files.delete(file); + } catch (AccessDeniedException e) { + DosFileAttributeView dosAttributes = + Files.getFileAttributeView(file, DosFileAttributeView.class); + if (dosAttributes == null || !dosAttributes.readAttributes().isReadOnly()) { + throw e; + } + dosAttributes.setReadOnly(false); + Files.delete(file); + } + } + /** * Search the parent path that contains the target file. If the current path does not have the * target file, the method continues with the parent path. If the path is not found, it returns diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/CompilerAidedSourcePathResolverTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/CompilerAidedSourcePathResolverTest.groovy index c1d516b2256..8b14c8bb4eb 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/CompilerAidedSourcePathResolverTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/CompilerAidedSourcePathResolverTest.groovy @@ -1,6 +1,7 @@ package datadog.trace.civisibility.source import datadog.compiler.annotations.SourcePath +import java.nio.file.Paths import spock.lang.Specification class CompilerAidedSourcePathResolverTest extends Specification { @@ -23,7 +24,7 @@ class CompilerAidedSourcePathResolverTest extends Specification { where: clazz | expectedPath AClassWithNoSourceInfoInjected | [] - AClassWithSourceInfoInjected | ["path/to/AClassWithSourceInfoInjected.java"] + AClassWithSourceInfoInjected | [Paths.get("path", "to", "AClassWithSourceInfoInjected.java").toString()] AClassWithSourceOutsideRepository | [] } diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexSourcePathResolverTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexSourcePathResolverTest.groovy index df61ba11170..0f5216f5c8d 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexSourcePathResolverTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexSourcePathResolverTest.groovy @@ -134,7 +134,7 @@ class RepoIndexSourcePathResolverTest extends Specification { def "test file-indexing failure"() { setup: - def classPath = fileSystem.getPath(generateSourceFileName(RepoIndexSourcePathResolverTest, repoRoot)) + def classPath = generateSourceFilePath(RepoIndexSourcePathResolverTest, repoRoot) packageResolver.getPackage(classPath) >> { throw new IOException() } Files.createDirectories(classPath.getParent()) @@ -185,7 +185,7 @@ class RepoIndexSourcePathResolverTest extends Specification { } private String givenSourceFile(Class c, String sourceRoot, Language language = Language.GROOVY) { - def classPath = fileSystem.getPath(generateSourceFileName(c, sourceRoot, language)) + def classPath = generateSourceFilePath(c, sourceRoot, language) packageResolver.getPackage(classPath) >> fileSystem.getPath(sourceRoot).relativize(classPath).getParent() givenRepoFile(classPath) @@ -198,8 +198,9 @@ class RepoIndexSourcePathResolverTest extends Specification { Files.write(file, "STUB FILE BODY".getBytes()) } - private static String generateSourceFileName(Class c, String sourceRoot, Language language = Language.GROOVY) { - return sourceRoot + File.separator + c.getName().replace(".", File.separator) + language.extension + private Path generateSourceFilePath(Class c, String sourceRoot, Language language = Language.GROOVY) { + def relativePath = c.getName().replace('.' as char, fileSystem.separator.charAt(0)) + language.extension + return fileSystem.getPath(sourceRoot).resolve(relativePath) } private static getRepoRoot() { diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy index 70da9c5780a..ae2cdbd394e 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy @@ -2,6 +2,7 @@ package datadog.trace.civisibility.source.index import datadog.instrument.utils.ClassNameTrie import datadog.trace.api.civisibility.domain.Language +import java.nio.file.Paths import spock.lang.Specification class RepoIndexTest extends Specification { @@ -28,9 +29,9 @@ class RepoIndexTest extends Specification { then: deserialized.getSourcePaths(RepoIndexTest).size() == 1 - deserialized.getSourcePaths(RepoIndexTest) .contains("myClassSourceRoot/" + myClassName.replace('.' as char, File.separatorChar) + Language.GROOVY.extension) + deserialized.getSourcePaths(RepoIndexTest).contains(sourcePath("myClassSourceRoot", myClassName)) deserialized.getSourcePaths(RepoIndexSourcePathResolverTest).size() == 1 - deserialized.getSourcePaths(RepoIndexSourcePathResolverTest).contains("myOtherClassSourceRoot/" + myOtherClassName.replace('.' as char, File.separatorChar) + Language.GROOVY.extension) + deserialized.getSourcePaths(RepoIndexSourcePathResolverTest).contains(sourcePath("myOtherClassSourceRoot", myOtherClassName)) } def "test serialization and deserialization with duplicate keys"() { @@ -46,8 +47,8 @@ class RepoIndexTest extends Specification { new RepoIndex.SourceRoot("sourceRoot2", Language.GROOVY)) def duplicateKeys = [(myClassName): [ - "sourceRoot1/" + myClassName.replace('.' as char, File.separatorChar) + Language.GROOVY.extension, - "sourceRoot2/" + myClassName.replace('.' as char, File.separatorChar) + Language.GROOVY.extension + sourcePath("sourceRoot1", myClassName), + sourcePath("sourceRoot2", myClassName) ]] def repoIndex = new RepoIndex(trie, duplicateKeys, sourceRoots, Collections.emptyList()) @@ -59,10 +60,7 @@ class RepoIndexTest extends Specification { then: def paths = deserialized.getSourcePaths(RepoIndexTest) paths.size() == 2 - paths.containsAll([ - "sourceRoot1/" + myClassName.replace('.' as char, File.separatorChar) + Language.GROOVY.extension, - "sourceRoot2/" + myClassName.replace('.' as char, File.separatorChar) + Language.GROOVY.extension - ]) + paths.containsAll([sourcePath("sourceRoot1", myClassName), sourcePath("sourceRoot2", myClassName)]) } def "test getSourcePaths returns all paths for duplicate key"() { @@ -77,8 +75,8 @@ class RepoIndexTest extends Specification { new RepoIndex.SourceRoot("debug", Language.GROOVY), new RepoIndex.SourceRoot("release", Language.GROOVY)) - def expectedPath1 = "debug/" + myClassName.replace('.' as char, File.separatorChar) + Language.GROOVY.extension - def expectedPath2 = "release/" + myClassName.replace('.' as char, File.separatorChar) + Language.GROOVY.extension + def expectedPath1 = sourcePath("debug", myClassName) + def expectedPath2 = sourcePath("release", myClassName) def duplicateKeys = [(myClassName): [expectedPath1, expectedPath2]] def repoIndex = new RepoIndex(trie, duplicateKeys, sourceRoots, Collections.emptyList()) @@ -109,6 +107,10 @@ class RepoIndexTest extends Specification { then: paths.size() == 1 - paths.first() == "src/main/groovy/" + myClassName.replace('.' as char, File.separatorChar) + Language.GROOVY.extension + paths.first() == sourcePath("src/main/groovy", myClassName) + } + + private static String sourcePath(String sourceRoot, String className) { + return Paths.get(sourceRoot, className.replace('.' as char, File.separatorChar) + Language.GROOVY.extension).toString() } } diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/utils/ShellCommandExecutorTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/utils/ShellCommandExecutorTest.groovy index 5b9dd7527ba..32e241da5fb 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/utils/ShellCommandExecutorTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/utils/ShellCommandExecutorTest.groovy @@ -1,6 +1,7 @@ package datadog.trace.civisibility.utils import datadog.communication.util.IOUtils +import datadog.trace.test.util.PortableCommand import spock.lang.Specification import spock.lang.TempDir @@ -18,7 +19,7 @@ class ShellCommandExecutorTest extends Specification { def shellCommandExecutor = new ShellCommandExecutor(temporaryFolder, SHELL_COMMAND_TIMEOUT) when: - def output = shellCommandExecutor.executeCommand(IOUtils::readFully, "echo", "this is a test") + def output = shellCommandExecutor.executeCommand(IOUtils::readFully, *PortableCommand.command("echo", "this is a test")) then: output.trim() == "this is a test" @@ -29,7 +30,7 @@ class ShellCommandExecutorTest extends Specification { def shellCommandExecutor = new ShellCommandExecutor(temporaryFolder, SHELL_COMMAND_TIMEOUT) when: - def output = shellCommandExecutor.executeCommand(IOUtils::readFully, "this is a test".bytes, "cat") + def output = shellCommandExecutor.executeCommand(IOUtils::readFully, "this is a test".bytes, *PortableCommand.command("copy-input")) then: output.trim() == "this is a test" @@ -40,7 +41,7 @@ class ShellCommandExecutorTest extends Specification { def shellCommandExecutor = new ShellCommandExecutor(temporaryFolder, 1_000) when: - shellCommandExecutor.executeCommand(IOUtils::readFully, "sleep", "2") + shellCommandExecutor.executeCommand(IOUtils::readFully, *PortableCommand.command("sleep", "2000")) then: thrown TimeoutException diff --git a/dd-java-agent/cws-tls/src/test/groovy/datadog/cws/tls/ErpcTlsTest.groovy b/dd-java-agent/cws-tls/src/test/groovy/datadog/cws/tls/ErpcTlsTest.groovy index fe7306165d3..8d0b4c29db8 100644 --- a/dd-java-agent/cws-tls/src/test/groovy/datadog/cws/tls/ErpcTlsTest.groovy +++ b/dd-java-agent/cws-tls/src/test/groovy/datadog/cws/tls/ErpcTlsTest.groovy @@ -4,7 +4,11 @@ import com.sun.jna.Native import datadog.trace.api.DD128bTraceId import datadog.trace.test.util.DDSpecification +import spock.lang.IgnoreIf +@IgnoreIf(reason = "CWS eRPC TLS is only supported on Linux", value = { + !System.getProperty("os.name").equalsIgnoreCase("Linux") +}) class ErpcTlsTest extends DDSpecification { def "register trace and span to tls"() { setup: diff --git a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy index e5fec1a2745..312c6b9a996 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy @@ -168,6 +168,7 @@ import datadog.trace.bootstrap.config.provider.ConfigConverter import datadog.trace.bootstrap.config.provider.ConfigProvider import datadog.trace.test.util.DDSpecification import datadog.trace.util.throwable.FatalAgentMisconfigurationError +import java.nio.file.Paths class ConfigTest extends DDSpecification { private static final String PREFIX = "dd." @@ -1711,7 +1712,7 @@ class ConfigTest extends DDSpecification { where: // spotless:off path | expectedKey - getClass().getClassLoader().getResource("apikey").getFile() | "test-api-key" + resourcePath("apikey") | "test-api-key" "/path/that/doesnt/exist" | "default-api-key" // spotless:on } @@ -1741,7 +1742,7 @@ class ConfigTest extends DDSpecification { where: // spotless:off path | expectedKey - getClass().getClassLoader().getResource("apikey.old").getFile() | "test-api-key-old" + resourcePath("apikey.old") | "test-api-key-old" "/path/that/doesnt/exist" | "default-api-key" // spotless:on } @@ -1770,14 +1771,14 @@ class ConfigTest extends DDSpecification { where: path | expectedKey - getClass().getClassLoader().getResource("apikey.very-old").getFile() | "test-api-key-very-old" + resourcePath("apikey.very-old") | "test-api-key-very-old" "/path/that/doesnt/exist" | "default-api-key" } def "verify api key loaded from new option when both new and old are set"() { setup: - System.setProperty(PREFIX + API_KEY_FILE, getClass().getClassLoader().getResource("apikey").getFile()) - System.setProperty(PREFIX + PROFILING_API_KEY_FILE_OLD, getClass().getClassLoader().getResource("apikey.old").getFile()) + System.setProperty(PREFIX + API_KEY_FILE, resourcePath("apikey")) + System.setProperty(PREFIX + PROFILING_API_KEY_FILE_OLD, resourcePath("apikey.old")) when: def config = new Config() @@ -1788,8 +1789,8 @@ class ConfigTest extends DDSpecification { def "verify api key loaded from new option when both old and very old are set"() { setup: - System.setProperty(PREFIX + PROFILING_API_KEY_FILE_OLD, getClass().getClassLoader().getResource("apikey.old").getFile()) - System.setProperty(PREFIX + PROFILING_API_KEY_FILE_VERY_OLD, getClass().getClassLoader().getResource("apikey.very-old").getFile()) + System.setProperty(PREFIX + PROFILING_API_KEY_FILE_OLD, resourcePath("apikey.old")) + System.setProperty(PREFIX + PROFILING_API_KEY_FILE_VERY_OLD, resourcePath("apikey.very-old")) when: def config = new Config() @@ -3575,4 +3576,8 @@ class ConfigTest extends DDSpecification { config.featureFlaggingConfigurationSourcePollIntervalSeconds == DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_POLL_INTERVAL_SECONDS config.featureFlaggingConfigurationSourceRequestTimeoutSeconds == DEFAULT_FEATURE_FLAGGING_CONFIGURATION_SOURCE_REQUEST_TIMEOUT_SECONDS } + + private String resourcePath(String name) { + return Paths.get(getClass().getClassLoader().getResource(name).toURI()).toString() + } } diff --git a/internal-api/src/test/groovy/datadog/trace/util/ProcessSupervisorTest.groovy b/internal-api/src/test/groovy/datadog/trace/util/ProcessSupervisorTest.groovy index eb8af334502..ccbb4dff922 100644 --- a/internal-api/src/test/groovy/datadog/trace/util/ProcessSupervisorTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/util/ProcessSupervisorTest.groovy @@ -1,6 +1,7 @@ package datadog.trace.util import datadog.trace.test.util.DDSpecification +import datadog.trace.test.util.PortableCommand import spock.util.concurrent.PollingConditions // This test looks at the private "currentProcess" variable because the alternative @@ -8,7 +9,7 @@ import spock.util.concurrent.PollingConditions class ProcessSupervisorTest extends DDSpecification { ProcessBuilder createProcessBuilder() { // Creates a process that never returns - return new ProcessBuilder("tail", "-f", "/dev/null") + return new ProcessBuilder(PortableCommand.command("sleep", Long.MAX_VALUE.toString())) } def "Process killed when supervisor closed"() { diff --git a/internal-api/src/test/java/datadog/trace/util/TempLocationManagerTest.java b/internal-api/src/test/java/datadog/trace/util/TempLocationManagerTest.java index af22b5fe804..b509f8c1c6f 100644 --- a/internal-api/src/test/java/datadog/trace/util/TempLocationManagerTest.java +++ b/internal-api/src/test/java/datadog/trace/util/TempLocationManagerTest.java @@ -13,6 +13,7 @@ import datadog.trace.api.time.TimeSource; import datadog.trace.bootstrap.config.provider.ConfigProvider; import java.io.IOException; +import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -29,6 +30,8 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -52,10 +55,7 @@ void testDefault(String subPath) throws Exception { @ParameterizedTest @ValueSource(strings = {"", "test1"}) void testFromConfig(String subPath) throws Exception { - Path myDir = - Files.createTempDirectory( - "ddprof-test-", - PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); + Path myDir = createSecureTempDirectory(); myDir.toFile().deleteOnExit(); Properties props = new Properties(); props.put(ProfilingConfig.PROFILING_TEMP_DIR, myDir.toString()); @@ -77,6 +77,7 @@ void testFromConfigInvalid() { } @Test + @DisabledOnOs(value = OS.WINDOWS, disabledReason = "Requires POSIX file permissions") void testFromConfigNotWritable() throws Exception { Path myDir = Files.createTempDirectory( @@ -92,10 +93,7 @@ void testFromConfigNotWritable() throws Exception { @ParameterizedTest @ValueSource(strings = {"", "test1"}) void testCleanup(String subPath) throws Exception { - Path myDir = - Files.createTempDirectory( - "ddprof-test-", - PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); + Path myDir = createSecureTempDirectory(); myDir.toFile().deleteOnExit(); TempLocationManager tempLocationManager = instance(myDir, false, TempLocationManager.CleanupHook.EMPTY); @@ -131,17 +129,12 @@ void testConcurrentCleanup(String section) throws Exception { * 2. Main thread deletes file, signals via proceedSignal latch * 3. Cleanup thread continues and completes */ - Path baseDir = - Files.createTempDirectory( - "ddprof-test-", - PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); + Path baseDir = createSecureTempDirectory(); baseDir.toFile().deleteOnExit(); Path fakeTempDir = baseDir.resolve(TempLocationManager.getBaseTempDirName() + "/pid_fake/scratch"); - Files.createDirectories( - fakeTempDir, - PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); + createSecureDirectories(fakeTempDir); Path fakeTempFile = fakeTempDir.resolve("libxxx.so"); Files.createFile(fakeTempFile); @@ -286,10 +279,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs, boolean t return TempLocationManager.CleanupHook.super.visitFile(file, attrs, timeout); } }; - Path baseDir = - Files.createTempDirectory( - "ddprof-test-", - PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); + Path baseDir = createSecureTempDirectory(); baseDir.toFile().deleteOnExit(); TempLocationManager instance = instance(baseDir, false, delayer, timeSource); Path mytempdir = instance.getTempDir(); @@ -314,6 +304,27 @@ private static Stream timeoutTestArguments() { return argumentsList.stream(); } + private static Path createSecureTempDirectory() throws IOException { + return supportsPosixPermissions() + ? Files.createTempDirectory( + "ddprof-test-", + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))) + : Files.createTempDirectory("ddprof-test-"); + } + + private static void createSecureDirectories(Path path) throws IOException { + if (supportsPosixPermissions()) { + Files.createDirectories( + path, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); + } else { + Files.createDirectories(path); + } + } + + private static boolean supportsPosixPermissions() { + return FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + } + private TempLocationManager instance( Path baseDir, boolean withStartupCleanup, TempLocationManager.CleanupHook cleanupHook) { return instance(baseDir, withStartupCleanup, cleanupHook, SystemTimeSource.INSTANCE); diff --git a/utils/test-utils/build.gradle.kts b/utils/test-utils/build.gradle.kts index c47732851d6..5ca8eead031 100644 --- a/utils/test-utils/build.gradle.kts +++ b/utils/test-utils/build.gradle.kts @@ -22,6 +22,7 @@ extra["excludedClassesCoverage"] = listOf( "datadog.trace.test.util.FlakySpockExtension*", "datadog.trace.test.util.MultipartRequestParser*", "datadog.trace.test.util.NonRetryable", + "datadog.trace.test.util.PortableCommand", ) dependencies { diff --git a/utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java b/utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java new file mode 100644 index 00000000000..ebc85d03c31 --- /dev/null +++ b/utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java @@ -0,0 +1,56 @@ +package datadog.trace.test.util; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** Provides simple commands that tests can execute in a child JVM on any operating system. */ +public final class PortableCommand { + private PortableCommand() {} + + public static String[] command(String... arguments) { + Path executable = Paths.get(System.getProperty("java.home"), "bin", "java"); + if (!Files.isRegularFile(executable)) { + executable = executable.resolveSibling("java.exe"); + } + + Path classpath; + try { + classpath = + Paths.get( + PortableCommand.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + } catch (URISyntaxException e) { + throw new IllegalStateException("Could not locate PortableCommand classes", e); + } + + String[] command = new String[arguments.length + 4]; + command[0] = executable.toString(); + command[1] = "-cp"; + command[2] = classpath.toString(); + command[3] = PortableCommand.class.getName(); + System.arraycopy(arguments, 0, command, 4, arguments.length); + return command; + } + + public static void main(String[] arguments) throws IOException, InterruptedException { + switch (arguments[0]) { + case "echo": + System.out.println(arguments[1]); + break; + case "copy-input": + byte[] buffer = new byte[1024]; + int read; + while ((read = System.in.read(buffer)) != -1) { + System.out.write(buffer, 0, read); + } + break; + case "sleep": + Thread.sleep(Long.parseLong(arguments[1])); + break; + default: + throw new IllegalArgumentException("Unknown command: " + arguments[0]); + } + } +} From f7bbf173f2ca3fd7ae826b72ab75a1aab0460a85 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Fri, 28 Aug 2026 15:35:38 -0400 Subject: [PATCH 07/10] Fixing tests on Windows. --- .gitlab/windows/run-base-tests.ps1 | 8 ++++++++ .../java/datadog/trace/test/util/PortableCommand.java | 2 ++ 2 files changed, 10 insertions(+) diff --git a/.gitlab/windows/run-base-tests.ps1 b/.gitlab/windows/run-base-tests.ps1 index 51b007b815d..76db44858f7 100644 --- a/.gitlab/windows/run-base-tests.ps1 +++ b/.gitlab/windows/run-base-tests.ps1 @@ -57,6 +57,14 @@ try { $gradleArguments = @( $env:GRADLE_TARGET, + # Formatting is validated by the dedicated GitLab Spotless job. + "-x", + "spotlessCheck", + # buildSrc is an included build, so it needs qualified exclusions. + "-x", + ":buildSrc:modifiable-config-agent:spotlessCheck", + "-x", + ":buildSrc:call-site-instrumentation-plugin:spotlessCheck", "-PskipFlakyTests", "-PtestJvm=$($env:testJvm)", "-Pslot=$($env:CI_SPLIT)", diff --git a/utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java b/utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java index ebc85d03c31..21fc295fd8c 100644 --- a/utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java +++ b/utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java @@ -1,5 +1,6 @@ package datadog.trace.test.util; +import de.thetaphi.forbiddenapis.SuppressForbidden; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.Files; @@ -34,6 +35,7 @@ public static String[] command(String... arguments) { return command; } + @SuppressForbidden public static void main(String[] arguments) throws IOException, InterruptedException { switch (arguments[0]) { case "echo": From 3091e986e031866408aa0da61a92e8e0a6e64422 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Fri, 28 Aug 2026 16:37:42 -0400 Subject: [PATCH 08/10] Fixing tests on Windows. --- .gitlab/windows-tests.yml | 4 +-- .gitlab/windows/run-base-tests.ps1 | 1 + .../nativeloader/NativeLoaderTest.java | 17 +++++++++-- .../trace/bootstrap/DatadogClassLoader.java | 9 +++++- .../bootstrap/DatadogClassLoaderTest.java | 30 +++++++++---------- .../civisibility/git/tree/ShellGitClient.java | 4 +-- .../civisibility/source/index/RepoIndex.java | 22 +++++++++----- .../source/index/RepoIndexBuilder.java | 5 +++- .../source/index/RepoIndexTest.groovy | 8 +++++ .../datadog/crashtracking/CrashUploader.java | 2 +- .../crashtracking/ScriptInitializerTest.java | 10 +++++++ .../iast/propagation/StringModuleTest.groovy | 4 +-- .../iast/sink/ApplicationModuleTest.groovy | 2 +- .../trace/agent/jmxfetch/JMXFetch.java | 2 +- .../logging/LogValidatingSpecification.groovy | 6 +++- .../logging/ddlogger/DDLoggerTest.groovy | 6 ++-- .../simplelogger/SLCompatHelperTest.groovy | 20 ++++++++----- .../simplelogger/SLCompatSettingsTest.groovy | 8 ++--- .../appsec/AppSecSystemSpecification.groovy | 6 ++-- ...ppSecConfigServiceImplSpecification.groovy | 4 +-- .../agent/InitializationTelemetryTest.groovy | 4 +++ .../trace/bootstrap/AgentPreCheckTest.groovy | 6 +++- .../TracerConnectionReliabilityTest.java | 5 ++++ .../trace/core/util/StackTracesTest.java | 15 +++++++++- .../trace/core/util/SystemAccessTest.java | 2 +- .../java/datadog/trace/api/git/GitUtils.java | 4 ++- .../datadog/trace/api/git/GitUtilsTest.groovy | 2 ++ ...faultConfigurationPollerSpecification.java | 6 ++-- .../main/java/datadog/telemetry/HostInfo.java | 2 ++ .../dependency/DependencyResolver.java | 5 ++-- .../datadog/telemetry/HostInfoTest.groovy | 2 ++ .../DependencyResolverSpecification.groovy | 6 ++-- .../common/container/ContainerInfoTest.java | 6 ++++ .../common/socket/TunnelingJdkSocketTest.java | 3 ++ 34 files changed, 170 insertions(+), 68 deletions(-) diff --git a/.gitlab/windows-tests.yml b/.gitlab/windows-tests.yml index 883cb270c9c..b44e2873821 100644 --- a/.gitlab/windows-tests.yml +++ b/.gitlab/windows-tests.yml @@ -29,7 +29,7 @@ build-windows-ci-image: "--tag", $image ) - Invoke-Native docker @("pull", $image) + Invoke-Native docker @("pull", "--quiet", $image) if ($LASTEXITCODE -eq 0) { $buildArguments += @("--cache-from", $image) } else { @@ -117,7 +117,7 @@ test-base-windows: $image = Get-WindowsCiImage Write-Output "Expected Windows test image: $image" - Invoke-Native docker @("pull", $image) + Invoke-Native docker @("pull", "--quiet", $image) if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: Could not pull Windows test image $image." Write-Host "Manually run build-windows-ci-image, wait for it to finish, then retry this job." diff --git a/.gitlab/windows/run-base-tests.ps1 b/.gitlab/windows/run-base-tests.ps1 index 76db44858f7..55554c5c479 100644 --- a/.gitlab/windows/run-base-tests.ps1 +++ b/.gitlab/windows/run-base-tests.ps1 @@ -57,6 +57,7 @@ try { $gradleArguments = @( $env:GRADLE_TARGET, + "-Dscan.capture-resource-usage=false", # Formatting is validated by the dedicated GitLab Spotless job. "-x", "spotlessCheck", diff --git a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java index 0a44b91f912..8026e417bf7 100644 --- a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java +++ b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java @@ -17,6 +17,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -458,6 +459,7 @@ public void fromJarBackedClassLoader_with_tempDir() throws IOException, LibraryL @Test public void fromJarBackedClassLoader_with_unwritable_tempDir() throws IOException, LibraryLoadException { + requirePosix(); Path jar = jar("test-data"); try { Path noWriteDir = Paths.get("no-write-temp"); @@ -488,6 +490,7 @@ public void fromJarBackedClassLoader_with_unwritable_tempDir() @Test public void fromJarBackedClassLoader_with_locked_file() throws IOException, LibraryLoadException { + requirePosix(); Path jar = jar("test-data"); try { Path tempDir = Paths.get("temp"); @@ -514,12 +517,22 @@ public void fromJarBackedClassLoader_with_locked_file() throws IOException, Libr void deleteHelper(Path dir) { try { - Files.setPosixFilePermissions(dir, posixPerms("rwx------")); + if (isPosix()) { + Files.setPosixFilePermissions(dir, posixPerms("rwx------")); + } Files.delete(dir); } catch (IOException e) { } } + static void requirePosix() { + assumeTrue(isPosix(), "Skipping POSIX-only test on non-POSIX file system"); + } + + static boolean isPosix() { + return FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + } + static URLClassLoader createClassLoader(Path... paths) { return new URLClassLoader(urls(paths)); } @@ -549,7 +562,7 @@ static Path jar(Path dir) { } static Path jarHelper(Path dir) throws IOException { - Path jarPath = Files.createTempFile(dir.toFile().getName(), ".jar", posixAttr("rwx------")); + Path jarPath = Files.createTempFile(dir.toFile().getName(), ".jar"); try (JarOutputStream jarStream = new JarOutputStream(Files.newOutputStream(jarPath))) { Files.walk(dir) diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/DatadogClassLoader.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/DatadogClassLoader.java index aaf263901ff..07b48132bff 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/DatadogClassLoader.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/DatadogClassLoader.java @@ -18,7 +18,7 @@ import org.slf4j.LoggerFactory; /** Provides access to Datadog internal classes. */ -public final class DatadogClassLoader extends SecureClassLoader { +public final class DatadogClassLoader extends SecureClassLoader implements AutoCloseable { static { ClassLoader.registerAsParallelCapable(); } @@ -167,6 +167,13 @@ byte[] loadClassBytes(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } + @Override + public void close() throws IOException { + if (agentJarFile != null) { + agentJarFile.close(); + } + } + @Override protected Package getPackage(String name) { synchronized (definedPackages) { diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/DatadogClassLoaderTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/DatadogClassLoaderTest.java index 376b035a043..dfa443d5374 100644 --- a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/DatadogClassLoaderTest.java +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/DatadogClassLoaderTest.java @@ -152,20 +152,20 @@ void findResourceUsesAgentJarUrlAsPrefix(@org.junit.jupiter.api.io.TempDir File spacedJar.toPath()); URL spacedJarUrl = spacedJar.toURI().toURL(); - DatadogClassLoader ddLoader = new DatadogClassLoader(spacedJarUrl, null); - - URL resource = ddLoader.findResource("a/A.class"); - assertNotNull(resource, "findResource should locate a/A.class in the test jar"); - - String expectedPrefix = "jar:" + spacedJarUrl + "!/"; - assertTrue( - resource.toString().startsWith(expectedPrefix), - () -> - "resource URL (" - + resource - + ") should start with the agent jar URL prefix (" - + expectedPrefix - + ") — pre-fix code derives the prefix from JarFile.getName()," - + " which leaves the space unencoded and on Windows produces a malformed URL."); + try (DatadogClassLoader ddLoader = new DatadogClassLoader(spacedJarUrl, null)) { + URL resource = ddLoader.findResource("a/A.class"); + assertNotNull(resource, "findResource should locate a/A.class in the test jar"); + + String expectedPrefix = "jar:" + spacedJarUrl + "!/"; + assertTrue( + resource.toString().startsWith(expectedPrefix), + () -> + "resource URL (" + + resource + + ") should start with the agent jar URL prefix (" + + expectedPrefix + + ") — pre-fix code derives the prefix from JarFile.getName()," + + " which leaves the space unencoded and on Windows produces a malformed URL."); + } } } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java index 1b9762ce73c..b65eaf21fb3 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java @@ -45,7 +45,7 @@ public class ShellGitClient implements GitClient { Arrays.asList("release/", "hotfix/"); private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); private static final String ORIGIN = "origin"; - private static final Pattern COMMIT_INFO_SPLIT = Pattern.compile("\",\""); + private static final Pattern COMMIT_INFO_SPLIT = Pattern.compile("\\x00"); private final CiVisibilityMetricCollector metricCollector; private final String repoRoot; @@ -445,7 +445,7 @@ public CommitInfo getCommitInfo(String commit, boolean fetchIfNotPresent) "show", commit, "-s", - "--format=%H\",\"%an\",\"%ae\",\"%aI\",\"%cn\",\"%ce\",\"%cI\",\"%B")) + "--format=%H%x00%an%x00%ae%x00%aI%x00%cn%x00%ce%x00%cI%x00%B")) .trim(); } catch (ShellCommandExecutor.ShellCommandFailedException e) { LOGGER.error("Failed to fetch commit info", e); diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndex.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndex.java index 76a758d3af6..f8dd1d032ea 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndex.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndex.java @@ -160,28 +160,34 @@ static final class SourceRoot { final Language language; + final char separator; + SourceRoot(String relativePath, Language language) { + this(relativePath, language, File.separatorChar); + } + + SourceRoot(String relativePath, Language language, char separator) { this.relativePath = relativePath; this.language = language; + this.separator = separator; } /** Resolves a trie key (dot-separated) to a full source path relative to the source root. */ String resolveSourcePath(String trieKey) { - return relativePath - + File.separatorChar - + trieKey.replace('.', File.separatorChar) - + language.getExtension(); + return relativePath + separator + trieKey.replace('.', separator) + language.getExtension(); } static void serialize(Serializer s, SourceRoot sourceRoot) { s.write(sourceRoot.relativePath); s.write(sourceRoot.language.ordinal()); + s.write(sourceRoot.separator); } static SourceRoot deserialize(ByteBuffer buffer) { String relativePath = Serializer.readString(buffer); Language language = Language.getByOrdinal(Serializer.readInt(buffer)); - return new SourceRoot(relativePath, language); + char separator = (char) Serializer.readInt(buffer); + return new SourceRoot(relativePath, language, separator); } @Override @@ -193,12 +199,14 @@ public boolean equals(Object o) { return false; } SourceRoot that = (SourceRoot) o; - return Objects.equals(relativePath, that.relativePath) && language == that.language; + return Objects.equals(relativePath, that.relativePath) + && language == that.language + && separator == that.separator; } @Override public int hashCode() { - return Objects.hash(relativePath, language); + return Objects.hash(relativePath, language, separator); } } } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndexBuilder.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndexBuilder.java index 2c223c34f90..38d220ab3e5 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndexBuilder.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndexBuilder.java @@ -171,7 +171,10 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { String relativeSourceRoot = repoRoot.relativize(sourceRoot).toString(); int sourceRootIdx = sourceRoots.computeIfAbsent( - new RepoIndex.SourceRoot(relativeSourceRoot, language), + new RepoIndex.SourceRoot( + relativeSourceRoot, + language, + repoRoot.getFileSystem().getSeparator().charAt(0)), sr -> sourceRootCounter.getAndIncrement()); String relativePath = sourceRoot.relativize(file).toString(); diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy index ae2cdbd394e..1e589cdc1be 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy @@ -110,6 +110,14 @@ class RepoIndexTest extends Specification { paths.first() == sourcePath("src/main/groovy", myClassName) } + def "test source root uses its filesystem separator"() { + given: + def sourceRoot = new RepoIndex.SourceRoot("src\\main\\groovy", Language.GROOVY, '\\' as char) + + expect: + sourceRoot.resolveSourcePath("example.MyClass") == "src\\main\\groovy\\example\\MyClass.groovy" + } + private static String sourcePath(String sourceRoot, String className) { return Paths.get(sourceRoot, className.replace('.' as char, File.separatorChar) + Language.GROOVY.extension).toString() } diff --git a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java index 87fc4592751..04803741763 100644 --- a/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java +++ b/dd-java-agent/agent-crashtracking/src/main/java/datadog/crashtracking/CrashUploader.java @@ -358,7 +358,7 @@ static String extractErrorMessage(String fileContent) { log.error("No match found for error.message"); return null; } - return Arrays.stream(matcher.group().split(System.lineSeparator())) + return Arrays.stream(matcher.group().split("\\r?\\n")) .filter( s -> !s.equals("# A fatal error has been detected by the Java Runtime Environment:") diff --git a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerTest.java b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerTest.java index 53cf66f72a1..e069830b766 100644 --- a/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerTest.java +++ b/dd-java-agent/agent-crashtracking/src/test/java/datadog/crashtracking/ScriptInitializerTest.java @@ -3,9 +3,11 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.io.File; import java.io.IOException; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermissions; @@ -148,6 +150,7 @@ void testCrashUploaderNoErrFileSpec() throws IOException { @Test void testCrashUploaderInvalidFolder() throws IOException { + requirePosix(); Files.setPosixFilePermissions(tempDir, PosixFilePermissions.fromString("r-x------")); Path file = tempDir.resolve("dd_crash_uploader.sh"); assertDoesNotThrow( @@ -157,9 +160,16 @@ void testCrashUploaderInvalidFolder() throws IOException { @Test void testOomeInitializeInvalidFolder() throws IOException { + requirePosix(); Files.setPosixFilePermissions(tempDir, PosixFilePermissions.fromString("r-x------")); Path file = tempDir.resolve("dd_oome_notifier.sh"); assertDoesNotThrow(() -> OOMENotifierScriptInitializer.initialize(file + " %p")); assertFalse(Files.exists(file), "File " + file + " should not have been created"); } + + private static void requirePosix() { + assumeTrue( + FileSystems.getDefault().supportedFileAttributeViews().contains("posix"), + "Skipping POSIX-only test on non-POSIX file system"); + } } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy index 77e406ffa15..c6c8c6af6de 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy @@ -1046,9 +1046,9 @@ class StringModuleTest extends IastModuleImplTestBase { 'Hello ==>%s<==' | ['World!'] | 'Hello ==>World!<==' // tainted placeholder [non tainted parameter] 'He==>llo %s!<==' | ['World'] | 'He==>llo <====>World<====>!<==' // tainted placeholder (2) [non tainted parameter] 'He==>llo %s!<==' | ['W==>or<==ld'] | 'He==>llo <==W==>or<==ld==>!<==' // tainted placeholder (3) [mixing with tainted parameter] - 'Hello %n %n %s!%n' | ['W==>or<==ld'] | 'Hello \n \n W==>or<==ld!\n' // \n character + 'Hello %n %n %s!%n' | ['W==>or<==ld'] | 'Hello ' + System.lineSeparator() + ' ' + System.lineSeparator() + ' W==>or<==ld!' + System.lineSeparator() // platform newline 'Hello %% %% %s!%%' | ['W==>or<==ld'] | 'Hello % % W==>or<==ld!%' // % character - '==>Hello %n %s!<==' | ['World'] | '==>Hello <====>\n<====> <====>World<====>!<==' // \n character in tainted format (each placeholder generates a separate range) + '==>Hello %n %s!<==' | ['World'] | '==>Hello <====>' + System.lineSeparator() + '<====> <====>World<====>!<==' // platform newline in tainted format (each placeholder generates a separate range) '==>Hello %% %s!<==' | ['World'] | '==>Hello <====>%<====> <====>World<====>!<==' // % character in tainted format (each placeholder generates a separate range) } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/ApplicationModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/ApplicationModuleTest.groovy index d7345eeddd2..7bfbc1b8813 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/ApplicationModuleTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/ApplicationModuleTest.groovy @@ -46,7 +46,7 @@ class ApplicationModuleTest extends IastModuleImplTestBase { void 'check vulnerabilities #path'() { given: final file = ClassLoader.getSystemResource(path) - final realPath = file.path + final realPath = Paths.get(file.toURI()).toString() when: module.onRealPath(realPath) diff --git a/dd-java-agent/agent-jmxfetch/src/main/java/datadog/trace/agent/jmxfetch/JMXFetch.java b/dd-java-agent/agent-jmxfetch/src/main/java/datadog/trace/agent/jmxfetch/JMXFetch.java index a46b1cb81a1..25e400c1c86 100644 --- a/dd-java-agent/agent-jmxfetch/src/main/java/datadog/trace/agent/jmxfetch/JMXFetch.java +++ b/dd-java-agent/agent-jmxfetch/src/main/java/datadog/trace/agent/jmxfetch/JMXFetch.java @@ -195,7 +195,7 @@ private static List getInternalMetricFiles() { } log.debug("reading found metricconfigs"); Scanner scanner = new Scanner(metricConfigsStream); - scanner.useDelimiter("\n"); + scanner.useDelimiter("\\r?\\n"); final List result = new ArrayList<>(); final SortedSet integrationName = new TreeSet<>(); while (scanner.hasNext()) { diff --git a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/LogValidatingSpecification.groovy b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/LogValidatingSpecification.groovy index 4da7ed78973..088b0d68c12 100644 --- a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/LogValidatingSpecification.groovy +++ b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/LogValidatingSpecification.groovy @@ -13,7 +13,7 @@ abstract class LogValidatingSpecification extends Specification { } private static validateLogLine(LogValidator validator, boolean enabled, String level, String marker, String msg, String emsg) { - def current = validator.outputStream.toString() + def current = normalizeLineEndings(validator.outputStream.toString()) def expected = "" if (enabled) { expected = marker == null ? "$level ${validator.name} - $msg\n" : "$marker ${validator.name} - $msg\n" @@ -23,6 +23,10 @@ abstract class LogValidatingSpecification extends Specification { validator.output.reset() } + protected static String normalizeLineEndings(String value) { + value.replace("\r\n", "\n") + } + class LogValidator { private final String name private final ByteArrayOutputStream output diff --git a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/ddlogger/DDLoggerTest.groovy b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/ddlogger/DDLoggerTest.groovy index d8d299439e4..045360dd32d 100644 --- a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/ddlogger/DDLoggerTest.groovy +++ b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/ddlogger/DDLoggerTest.groovy @@ -67,7 +67,7 @@ class DDLoggerTest extends LogValidatingSpecification { } static String validateLogLine(OutputStream outputStream, String previous, boolean enabled, String level, String msg, String emsg) { - def total = outputStream.toString() + def total = normalizeLineEndings(outputStream.toString()) def current = total.substring(previous.length()) def expected = "" if (enabled) { @@ -248,7 +248,7 @@ class DDLoggerTest extends LogValidatingSpecification { } then: - outputStream.toString() ==~ /^.* $level foo - log \[exception:java\.io\.IOException: wrong\. at .*\]\n$/ + normalizeLineEndings(outputStream.toString()) ==~ /^.* $level foo - log \[exception:java\.io\.IOException: wrong\. at .*\]\n$/ where: level << LogLevel.values().toList().take(5) // remove LogLevel.OFF @@ -273,7 +273,7 @@ class DDLoggerTest extends LogValidatingSpecification { } then: - outputStream.toString() ==~ /^.* $level foo - log some more \[exception:java\.io\.IOException: wrong\. at .*\]\n$/ + normalizeLineEndings(outputStream.toString()) ==~ /^.* $level foo - log some more \[exception:java\.io\.IOException: wrong\. at .*\]\n$/ where: level << LogLevel.values().toList().take(5) // remove LogLevel.OFF diff --git a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatHelperTest.groovy b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatHelperTest.groovy index 31084ee6494..d57c3244ab7 100644 --- a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatHelperTest.groovy +++ b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatHelperTest.groovy @@ -8,6 +8,10 @@ import java.text.SimpleDateFormat class SLCompatHelperTest extends Specification { + private static String normalizedOutput(ByteArrayOutputStream outputStream) { + outputStream.toString().replace("\r\n", "\n") + } + private class NoStackException extends Exception { NoStackException(String message) { super(message, null, false, false) @@ -73,7 +77,7 @@ class SLCompatHelperTest extends Specification { helper.log(level, null, msg, null) then: - outputStream.toString() == expected + normalizedOutput(outputStream) == expected where: name | level | msg | expected @@ -94,7 +98,7 @@ class SLCompatHelperTest extends Specification { helper.log(LogLevel.ERROR, null, "log", exception) expect: - outputStream.toString() == "[$thread] ERROR foo - log\n${NoStackException.getName()}: wrong\n" + normalizedOutput(outputStream) == "[$thread] ERROR foo - log\n${NoStackException.getName()}: wrong\n" } def "test logging with an embedded exception in the message"() { @@ -112,7 +116,7 @@ class SLCompatHelperTest extends Specification { } expect: - outputStream.toString() ==~ /^.* $level foo - log \[exception:java\.io\.IOException: wrong\. at .*\]\n$/ + normalizedOutput(outputStream) ==~ /^.* $level foo - log \[exception:java\.io\.IOException: wrong\. at .*\]\n$/ where: level << LogLevel.values().toList().take(5) // remove LogLevel.OFF @@ -130,7 +134,7 @@ class SLCompatHelperTest extends Specification { helper.log(LogLevel.ERROR, null, "log", null) expect: - outputStream.toString() ==~ /^\d+ ERROR foo - log\n$/ + normalizedOutput(outputStream) ==~ /^\d+ ERROR foo - log\n$/ } def "test log output with configuration"() { @@ -145,7 +149,7 @@ class SLCompatHelperTest extends Specification { helper.log(level, null, 0, 4711, "thread", "log", null) then: - outputStream.toString() == expected + normalizedOutput(outputStream) == expected where: level | warnS | showB | showS | showL | showT | dateTFS | showDT | jsonE | expected @@ -177,7 +181,7 @@ class SLCompatHelperTest extends Specification { helper.log(level, null, "log", null) then: - outputStream.toString() == expected + normalizedOutput(outputStream) == expected where: level | warnS | showB | showS | showL | showT | dateTFS | showDT | jsonE | expected @@ -197,7 +201,7 @@ class SLCompatHelperTest extends Specification { helper.logJson(level,null,0,4711,"thread","log", null) then: - outputStream.toString() == expected + normalizedOutput(outputStream) == expected where: level | warnS | showB | showS | showL | showT | dateTFS | showDT | jsonE | expected @@ -227,6 +231,6 @@ class SLCompatHelperTest extends Specification { helper.log(LogLevel.INFO, null, "log", exception) } expect: - outputStream.toString() ==~ /^\{"origin":"dd.trace","level":"INFO","logger.name":"foo","message":"log","exception":\{"message":"wrong","stackTrace":\[.*\]\}\}\n$/ + normalizedOutput(outputStream) ==~ /^\{"origin":"dd.trace","level":"INFO","logger.name":"foo","message":"log","exception":\{"message":"wrong","stackTrace":\[.*\]\}\}\n$/ } } diff --git a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatSettingsTest.groovy b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatSettingsTest.groovy index 80669b06463..c0613965aa1 100644 --- a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatSettingsTest.groovy +++ b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatSettingsTest.groovy @@ -138,9 +138,8 @@ class SLCompatSettingsTest extends Specification { def "test log file creation stderr fallback"() { setup: - def dir = File.createTempDir() - dir.setWritable(false, true) - def file = new File(dir, "log") + def parent = File.createTempFile("not-a-directory", ".tmp") + def file = new File(parent, "log") def props = new Properties() props.setProperty(SLCompatSettings.Keys.LOG_FILE, file.getAbsolutePath()) def settings = new SLCompatSettings(props) @@ -150,8 +149,7 @@ class SLCompatSettingsTest extends Specification { ((PrintStreamWrapper) settings.printStream).getOriginalPrintStream() == System.err cleanup: - dir.setWritable(true, true) - dir.delete() + parent.delete() } def "test logNameForName"() { diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy index 162008aa865..4b180cc588b 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy @@ -26,6 +26,7 @@ import okhttp3.OkHttpClient import java.nio.file.Files import java.nio.file.Path +import java.nio.file.Paths import java.util.function.BiFunction import static datadog.trace.api.gateway.Events.EVENTS @@ -48,14 +49,15 @@ class AppSecSystemSpecification extends DDSpecification { void 'throws if custom config does not exist'() { setup: - injectSysConfig('dd.appsec.rules', '/file/that/does/not/exist') + String missingRules = Paths.get(File.separator, 'file', 'that', 'does', 'not', 'exist') + injectSysConfig('dd.appsec.rules', missingRules) when: AppSecSystem.start(subService, sharedCommunicationObjects()) then: def exception = thrown(AbortStartupException) - exception.cause.toString().contains('/file/that/does/not/exist') + exception.cause.toString().contains(missingRules) } void 'system should throw AbortStartupException when config file is not valid JSON'() { diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy index cc650c3c7e6..a9a01e4ec97 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy @@ -122,7 +122,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { void 'no subscription to ASM ASM_DD ASM_DATA if custom rules are provided'() { setup: - Path p = Paths.get(getClass().classLoader.getResource('test_multi_config_no_action.json').getPath()) + Path p = Paths.get(getClass().classLoader.getResource('test_multi_config_no_action.json').toURI()) AppSecSystem.active = false when: @@ -142,7 +142,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { void 'can load from a different location'() { setup: - Path p = Paths.get(getClass().classLoader.getResource('test_multi_config_no_action.json').getPath()) + Path p = Paths.get(getClass().classLoader.getResource('test_multi_config_no_action.json').toURI()) String capturedPath = null when: diff --git a/dd-java-agent/src/test/groovy/datadog/trace/agent/InitializationTelemetryTest.groovy b/dd-java-agent/src/test/groovy/datadog/trace/agent/InitializationTelemetryTest.groovy index 6c957a4a3fd..f5cfc99cd66 100644 --- a/dd-java-agent/src/test/groovy/datadog/trace/agent/InitializationTelemetryTest.groovy +++ b/dd-java-agent/src/test/groovy/datadog/trace/agent/InitializationTelemetryTest.groovy @@ -1,6 +1,7 @@ package datadog.trace.agent import datadog.environment.JavaVirtualMachine +import datadog.environment.OperatingSystem import datadog.test.SimpleAgentMock import jvmbootstraptest.InitializationTelemetryCheck import spock.lang.IgnoreIf @@ -8,6 +9,9 @@ import spock.lang.Specification import spock.lang.Timeout @Timeout(30) +@IgnoreIf(reason = "Uses POSIX file permissions and a Bash telemetry forwarder", value = { + OperatingSystem.isWindows() +}) class InitializationTelemetryTest extends Specification { @IgnoreIf(reason = "SecurityManager is permanently disabled as of JDK 24", value = { JavaVirtualMachine.isJavaVersionAtLeast(24) diff --git a/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/AgentPreCheckTest.groovy b/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/AgentPreCheckTest.groovy index fef98032091..c0b34535c2f 100644 --- a/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/AgentPreCheckTest.groovy +++ b/dd-java-agent/src/test/groovy/datadog/trace/bootstrap/AgentPreCheckTest.groovy @@ -3,10 +3,13 @@ package datadog.trace.bootstrap import spock.lang.Specification import spock.util.concurrent.PollingConditions +import java.nio.file.FileSystems import java.nio.file.Files import java.nio.file.Path import java.nio.file.attribute.PosixFilePermissions +import static org.junit.jupiter.api.Assumptions.assumeTrue + class AgentPreCheckTest extends Specification { def 'parse java.version of #version as #expected'() { when: @@ -63,7 +66,7 @@ class AgentPreCheckTest extends Specification { when: boolean compatible = AgentPreCheck.compatible(javaVersion, "/Library/$javaVersion", logStream) String log = output.toString() - def logLines = log.isEmpty() ? [] : Arrays.asList(log.split('\n')) + def logLines = log.readLines() then: compatible == expectedCompatible @@ -92,6 +95,7 @@ class AgentPreCheckTest extends Specification { def 'send hardcoded bootstrap telemetry for unsupported java'() { setup: + assumeTrue(FileSystems.default.supportedFileAttributeViews().contains('posix')) Path path = Files.createTempFile('test-forwarder', '.sh', PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString('rwxr--r--'))) File forwarderFile = path.toFile() forwarderFile.deleteOnExit() diff --git a/dd-trace-core/src/test/java/datadog/trace/TracerConnectionReliabilityTest.java b/dd-trace-core/src/test/java/datadog/trace/TracerConnectionReliabilityTest.java index cfda2f730c7..c9a44cbc25b 100644 --- a/dd-trace-core/src/test/java/datadog/trace/TracerConnectionReliabilityTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/TracerConnectionReliabilityTest.java @@ -26,10 +26,15 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.testcontainers.containers.FixedHostPortGenericContainer; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; +@DisabledOnOs( + value = OS.WINDOWS, + disabledReason = "Requires a Docker environment capable of running Linux containers") public class TracerConnectionReliabilityTest extends DDJavaSpecification { static final int FEATURES_DISCOVERY_MIN_DELAY = 10; diff --git a/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java b/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java index d7d9b045735..06c2ca45d9a 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java @@ -111,7 +111,20 @@ void getStackTraceFallsBackToClassNameWhenGetMessageAlsoThrows() { @ParameterizedTest(name = "truncation limit {0}") @MethodSource("testTruncateArguments") void testTruncate(int limit, String expected) { - assertEquals(expected, StackTraces.truncate(TRACE, limit)); + assertEquals( + normalizePlatformDifferences(expected), + normalizePlatformDifferences(StackTraces.truncate(TRACE, limit))); + } + + private static String normalizePlatformDifferences(String trace) { + return trace + .replace("\r\n", "\n") + // Native line endings change the exact character split around a centre cut. The content on + // the adjacent partial lines is intentionally unspecified; the marker and all complete + // lines remain exact. + .replaceAll( + "(?m)^.*\\n(\\t\\.\\.\\. trace centre-cut to \\d+ chars \\.\\.\\.\\n).*$", + "\n$1"); } static Stream testTruncateArguments() { diff --git a/dd-trace-core/src/test/java/datadog/trace/core/util/SystemAccessTest.java b/dd-trace-core/src/test/java/datadog/trace/core/util/SystemAccessTest.java index 0f549c07cea..f764e37ab16 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/util/SystemAccessTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/util/SystemAccessTest.java @@ -56,7 +56,7 @@ void testCpuTime( if (hasCpuTime) { assertNotEquals(Long.MIN_VALUE, threadCpuTime1); assertNotEquals(Long.MIN_VALUE, threadCpuTime2); - assertTrue(threadCpuTime2 > threadCpuTime1); + assertTrue(threadCpuTime2 >= threadCpuTime1); } else { assertEquals(Long.MIN_VALUE, threadCpuTime1); assertEquals(Long.MIN_VALUE, threadCpuTime2); diff --git a/internal-api/src/main/java/datadog/trace/api/git/GitUtils.java b/internal-api/src/main/java/datadog/trace/api/git/GitUtils.java index 892d4986bde..0462eb202fa 100644 --- a/internal-api/src/main/java/datadog/trace/api/git/GitUtils.java +++ b/internal-api/src/main/java/datadog/trace/api/git/GitUtils.java @@ -38,6 +38,8 @@ public class GitUtils { + "|/$" // ends with slash ); private static final Pattern PATH_PATTERN = Pattern.compile("^[a-zA-Z0-9_./-]+$"); + private static final Pattern WINDOWS_PATH_PATTERN = + Pattern.compile("^(?:[a-zA-Z]:[\\\\/]|\\\\\\\\)[a-zA-Z0-9_ .\\\\/-]*$"); private static final Pattern SHELL_METACHAR_PATTERN = Pattern.compile(".*[`$&|;<>\n\r#].*"); private static final int SHORT_SHA_LENGTH = 7; @@ -290,7 +292,7 @@ public static boolean isValidRef(@Nullable String ref) { /** Checks if the provided string is a valid system path for Git operations */ public static boolean isValidPath(@Nonnull String path) { - if (!PATH_PATTERN.matcher(path).matches()) { + if (!PATH_PATTERN.matcher(path).matches() && !WINDOWS_PATH_PATTERN.matcher(path).matches()) { return false; } // Reject path traversal sequences diff --git a/internal-api/src/test/groovy/datadog/trace/api/git/GitUtilsTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/git/GitUtilsTest.groovy index 657b367540b..f680e62d60a 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/git/GitUtilsTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/git/GitUtilsTest.groovy @@ -155,6 +155,8 @@ class GitUtilsTest extends Specification { "multiple/levels/of/nesting" | true "/absolute/path" | true // absolute paths allowed "/home/user/workspace" | true // typical CI workspace + "C:\\Users\\build\\workspace" | true // Windows absolute path + "C:\\Program Files\\repo" | true // Windows path with spaces "../parent/path" | false // path traversal at start "path/../other" | false // path traversal in middle "path/to/.." | false // path traversal at end diff --git a/remote-config/remote-config-core/src/test/java/datadog/remoteconfig/DefaultConfigurationPollerSpecification.java b/remote-config/remote-config-core/src/test/java/datadog/remoteconfig/DefaultConfigurationPollerSpecification.java index 51922d9cbfa..c83f9312bd6 100644 --- a/remote-config/remote-config-core/src/test/java/datadog/remoteconfig/DefaultConfigurationPollerSpecification.java +++ b/remote-config/remote-config-core/src/test/java/datadog/remoteconfig/DefaultConfigurationPollerSpecification.java @@ -866,8 +866,10 @@ static Stream reportableErrorsArguments() { arguments( "two reportable errors", toJson(twoErrors), - "Failed to apply configuration due to 2 errors:\n (1) Not a valid config key: foobar\n" - + " (2) No content for employee/ASM_DD/1.recommended.json/config\n"), + String.format( + "Failed to apply configuration due to 2 errors:%n" + + " (1) Not a valid config key: foobar%n" + + " (2) No content for employee/ASM_DD/1.recommended.json/config%n")), arguments( "in target_files but not signed", toJson(notInTargets), diff --git a/telemetry/src/main/java/datadog/telemetry/HostInfo.java b/telemetry/src/main/java/datadog/telemetry/HostInfo.java index 1d950ece8e2..ceae0c30cd0 100644 --- a/telemetry/src/main/java/datadog/telemetry/HostInfo.java +++ b/telemetry/src/main/java/datadog/telemetry/HostInfo.java @@ -40,6 +40,8 @@ public static String getOsName() { if (OperatingSystem.isMacOs()) { // os.name == Mac OS X, while uanme -s == Darwin. We'll hardcode it to Darwin. osName = "Darwin"; + } else if (OperatingSystem.isWindows()) { + osName = "Windows"; } else { osName = SystemProperties.get("os.name"); } diff --git a/telemetry/src/main/java/datadog/telemetry/dependency/DependencyResolver.java b/telemetry/src/main/java/datadog/telemetry/dependency/DependencyResolver.java index 91ec8c52162..f22772b231c 100644 --- a/telemetry/src/main/java/datadog/telemetry/dependency/DependencyResolver.java +++ b/telemetry/src/main/java/datadog/telemetry/dependency/DependencyResolver.java @@ -68,14 +68,13 @@ private static JarReader.Extracted resolveNestedJar(final URI uri) throws IOExce if (path.startsWith("file:")) { // Old style nested dependencies, as seen in Spring Boot 2 and others. // These look like jar:file:/path/to.jar!/path/to/nested.jar!/ - path = path.substring("file:".length()); final int sepIdx = path.indexOf("!/"); if (sepIdx == -1) { // JBoss may use the "jar:file" format to reference jar files instead of nested jars. // These look like: jar:file:/path/to.jar!/ - return JarReader.readJarFile(path); + return JarReader.readJarFile(new File(URI.create(path)).getPath()); } - final String outerPath = path.substring(0, sepIdx); + final String outerPath = new File(URI.create(path.substring(0, sepIdx))).getPath(); final String innerPath = path.substring(sepIdx + 2); return JarReader.readNestedJarFile(outerPath, innerPath); } else if (path.startsWith("nested:")) { diff --git a/telemetry/src/test/groovy/datadog/telemetry/HostInfoTest.groovy b/telemetry/src/test/groovy/datadog/telemetry/HostInfoTest.groovy index eddb474baad..277490f933c 100644 --- a/telemetry/src/test/groovy/datadog/telemetry/HostInfoTest.groovy +++ b/telemetry/src/test/groovy/datadog/telemetry/HostInfoTest.groovy @@ -3,6 +3,7 @@ package datadog.telemetry import datadog.environment.OperatingSystem import spock.lang.Specification +import static org.junit.jupiter.api.Assumptions.assumeFalse import static org.junit.jupiter.api.Assumptions.assumeTrue class HostInfoTest extends Specification { @@ -33,6 +34,7 @@ class HostInfoTest extends Specification { } void 'compare to uname'() { + assumeFalse(OperatingSystem.isWindows()) assumeTrue('uname -a'.execute().waitFor() == 0) expect: diff --git a/telemetry/src/test/groovy/datadog/telemetry/dependency/DependencyResolverSpecification.groovy b/telemetry/src/test/groovy/datadog/telemetry/dependency/DependencyResolverSpecification.groovy index 6e8096bc57a..dd5a41a2a08 100644 --- a/telemetry/src/test/groovy/datadog/telemetry/dependency/DependencyResolverSpecification.groovy +++ b/telemetry/src/test/groovy/datadog/telemetry/dependency/DependencyResolverSpecification.groovy @@ -243,7 +243,7 @@ class DependencyResolverSpecification extends DepSpecification { out.close() when: - URI uri = new URI("jar:file:" + file.getAbsolutePath() + "!/BOOT-INF/lib/lib-1.0.jar!/") + URI uri = new URI("jar:" + file.toURI() + "!/BOOT-INF/lib/lib-1.0.jar!/") List deps = DependencyResolver.resolve(uri) then: @@ -328,13 +328,13 @@ class DependencyResolverSpecification extends DepSpecification { out.close() when: - def deps = DependencyResolver.resolve(new URI('jar:file:' + file.getAbsolutePath() + "!/classes!/")) + def deps = DependencyResolver.resolve(new URI('jar:' + file.toURI() + "!/classes!/")) then: deps.isEmpty() when: 'resolve without catching exceptions' - deps = DependencyResolver.internalResolve(new URI('jar:file:' + file.getAbsolutePath() + "!/classes!/")) + deps = DependencyResolver.internalResolve(new URI('jar:' + file.toURI() + "!/classes!/")) then: 'it does not throw' deps.isEmpty() diff --git a/utils/container-utils/src/test/java/datadog/common/container/ContainerInfoTest.java b/utils/container-utils/src/test/java/datadog/common/container/ContainerInfoTest.java index 05819e6ef79..7cb4e7ee50e 100644 --- a/utils/container-utils/src/test/java/datadog/common/container/ContainerInfoTest.java +++ b/utils/container-utils/src/test/java/datadog/common/container/ContainerInfoTest.java @@ -19,6 +19,8 @@ import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -251,6 +253,7 @@ void containerInfoToleratesMissingContainerIdAndPodIdInProcfile() throws Excepti } @Test + @DisabledOnOs(value = OS.WINDOWS, disabledReason = "Requires Unix inode support and ls") void getInoPathShouldReturnSameValueAsLsIdPath() throws Exception { File f = File.createTempFile("container-info-test-", "-inode-file"); f.deleteOnExit(); @@ -291,6 +294,7 @@ void readEntityIDReturnNullIfContainerIdIsNotDefinedAndIsHostCgroupNamespace(Str "[''] | true ", "[memory] | false " }) + @DisabledOnOs(value = OS.WINDOWS, disabledReason = "Requires Unix inode support and ls") void readEntityIDReturnIdInoForEmptyController( List controllers, boolean hasEntityId) throws Exception { File mountPath = createTempDir(); @@ -315,6 +319,7 @@ void readEntityIDReturnIdInoForEmptyController( "[memory] | true ", "[''] | false " }) + @DisabledOnOs(value = OS.WINDOWS, disabledReason = "Requires Unix inode support and ls") void readEntityIDReturnIdInoForMemoryController( List controllers, boolean hasEntityId) throws Exception { File mountPath = createTempDir(); @@ -337,6 +342,7 @@ void readEntityIDReturnIdInoForMemoryController( // spotless:on @Test + @DisabledOnOs(value = OS.WINDOWS, disabledReason = "Requires Unix inode support and ls") void readEntityIDReturnIdInoForParentWhenPathIsSlash() throws Exception { File mountPath = createTempDir(); File memoryController = Files.createDirectory(mountPath.toPath().resolve("memory")).toFile(); diff --git a/utils/socket-utils/src/test/java/datadog/common/socket/TunnelingJdkSocketTest.java b/utils/socket-utils/src/test/java/datadog/common/socket/TunnelingJdkSocketTest.java index 1c7c7ec7a19..67424053c93 100644 --- a/utils/socket-utils/src/test/java/datadog/common/socket/TunnelingJdkSocketTest.java +++ b/utils/socket-utils/src/test/java/datadog/common/socket/TunnelingJdkSocketTest.java @@ -23,7 +23,9 @@ import java.time.Duration; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.OS; public class TunnelingJdkSocketTest { @@ -197,6 +199,7 @@ public void testBufferSizes() throws Exception { @Test @EnabledForJreRange(min = JAVA_16) + @DisabledOnOs(value = OS.WINDOWS, disabledReason = "Requires the Unix lsof command") public void testFileDescriptorLeak() throws Exception { long initialCount = getFileDescriptorCount(); From 7e7f9da6d08d8534ed327646f65c07d545b8244e Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Fri, 28 Aug 2026 20:55:09 -0400 Subject: [PATCH 09/10] Fixing tests on Windows. --- .../datadog/nativeloader/NativeLoader.java | 15 +++ .../trace/civisibility/source/Utils.java | 3 +- ...CompilerAidedSourcePathResolverTest.groovy | 10 +- .../civisibility/source/UtilsTest.groovy | 8 ++ .../source/index/RepoIndexTest.groovy | 32 +++--- .../iast/propagation/StringModuleTest.groovy | 9 +- .../iast/sink/ApplicationModuleTest.groovy | 3 +- dd-java-agent/agent-logging/build.gradle | 1 + .../logging/LogValidatingSpecification.groovy | 6 +- .../logging/ddlogger/DDLoggerTest.groovy | 1 + .../simplelogger/SLCompatHelperTest.groovy | 4 +- .../appsec/AppSecSystemSpecification.groovy | 7 +- .../ddwaf/WAFModuleSpecification.groovy | 5 + .../trace/core/util/StackTracesTest.java | 19 ++-- ...faultConfigurationPollerSpecification.java | 9 +- utils/test-utils/build.gradle.kts | 1 - .../trace/test/util/PlatformTestUtils.java | 45 +++++++++ .../trace/test/util/PortableCommand.java | 26 +++-- .../test/util/PlatformTestUtilsTest.java | 49 ++++++++++ .../trace/test/util/PortableCommandTest.java | 98 +++++++++++++++++++ 20 files changed, 297 insertions(+), 54 deletions(-) create mode 100644 utils/test-utils/src/main/java/datadog/trace/test/util/PlatformTestUtils.java create mode 100644 utils/test-utils/src/test/java/datadog/trace/test/util/PlatformTestUtilsTest.java create mode 100644 utils/test-utils/src/test/java/datadog/trace/test/util/PortableCommandTest.java diff --git a/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java b/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java index 8a55eabe570..b8cf8b412ae 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -379,6 +380,14 @@ private TempFileHelper() {} static Path createTempFile(Path tempDir, String libname, String libExt) throws IOException, SecurityException { + if (!supportsPosix(tempDir)) { + if (tempDir == null) { + return Files.createTempFile(libname, "." + libExt); + } + Files.createDirectories(tempDir); + return Files.createTempFile(tempDir, libname, "." + libExt); + } + FileAttribute> permAttrs = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")); @@ -393,6 +402,12 @@ static Path createTempFile(Path tempDir, String libname, String libExt) } } + private static boolean supportsPosix(Path tempDir) { + return (tempDir == null ? FileSystems.getDefault() : tempDir.getFileSystem()) + .supportedFileAttributeViews() + .contains("posix"); + } + static boolean delete(File tempFile) { boolean deleted = tempFile.delete(); if (!deleted) tempFile.deleteOnExit(); diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/Utils.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/Utils.java index a4423f727d5..67cdd0ebad5 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/Utils.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/Utils.java @@ -1,6 +1,5 @@ package datadog.trace.civisibility.source; -import java.io.File; import java.io.IOException; import java.io.InputStream; import javax.annotation.Nonnull; @@ -76,7 +75,7 @@ public static String stripNestedClassNames(@Nonnull String className) { @Nonnull public static String toTrieKey(@Nonnull String relativePath) { - return stripExtension(relativePath).replace(File.separatorChar, '.'); + return stripExtension(relativePath).replace('/', '.').replace('\\', '.'); } @Nonnull diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/CompilerAidedSourcePathResolverTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/CompilerAidedSourcePathResolverTest.groovy index 8b14c8bb4eb..5dafed0d278 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/CompilerAidedSourcePathResolverTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/CompilerAidedSourcePathResolverTest.groovy @@ -1,9 +1,10 @@ package datadog.trace.civisibility.source import datadog.compiler.annotations.SourcePath -import java.nio.file.Paths import spock.lang.Specification +import static datadog.trace.test.util.PlatformTestUtils.normalizePathSeparators + class CompilerAidedSourcePathResolverTest extends Specification { public static final String REPO_ROOT = "/repo/root" @@ -16,15 +17,16 @@ class CompilerAidedSourcePathResolverTest extends Specification { when: def path = sourcePathResolver.getSourcePaths(clazz) + def normalizedPaths = normalizePathSeparators(path) then: - path.size() == expectedPath.size() - path.containsAll(expectedPath) + normalizedPaths.size() == expectedPath.size() + normalizedPaths.containsAll(expectedPath) where: clazz | expectedPath AClassWithNoSourceInfoInjected | [] - AClassWithSourceInfoInjected | [Paths.get("path", "to", "AClassWithSourceInfoInjected.java").toString()] + AClassWithSourceInfoInjected | ["path/to/AClassWithSourceInfoInjected.java"] AClassWithSourceOutsideRepository | [] } diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/UtilsTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/UtilsTest.groovy index ab5984617db..8bcb9dd4135 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/UtilsTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/UtilsTest.groovy @@ -19,6 +19,14 @@ class UtilsTest extends Specification { clazz << [UtilsTestClass, Object] } + def "test converts #path to trie key"() { + expect: + Utils.toTrieKey(path) == 'foo.bar.Baz' + + where: + path << ['foo/bar/Baz.java', 'foo\\bar\\Baz.java'] + } + private byte[] readMagicNumber(InputStream stream) { def bytes = new byte[4] def totalRead = 0 diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy index 1e589cdc1be..231eddd5d1e 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/index/RepoIndexTest.groovy @@ -5,6 +5,8 @@ import datadog.trace.api.civisibility.domain.Language import java.nio.file.Paths import spock.lang.Specification +import static datadog.trace.test.util.PlatformTestUtils.normalizePathSeparators + class RepoIndexTest extends Specification { def "test serialization and deserialization"() { @@ -29,9 +31,11 @@ class RepoIndexTest extends Specification { then: deserialized.getSourcePaths(RepoIndexTest).size() == 1 - deserialized.getSourcePaths(RepoIndexTest).contains(sourcePath("myClassSourceRoot", myClassName)) + normalizePathSeparators(deserialized.getSourcePaths(RepoIndexTest)).contains( + "myClassSourceRoot/" + myClassName.replace('.' as char, '/' as char) + Language.GROOVY.extension) deserialized.getSourcePaths(RepoIndexSourcePathResolverTest).size() == 1 - deserialized.getSourcePaths(RepoIndexSourcePathResolverTest).contains(sourcePath("myOtherClassSourceRoot", myOtherClassName)) + normalizePathSeparators(deserialized.getSourcePaths(RepoIndexSourcePathResolverTest)).contains( + "myOtherClassSourceRoot/" + myOtherClassName.replace('.' as char, '/' as char) + Language.GROOVY.extension) } def "test serialization and deserialization with duplicate keys"() { @@ -47,8 +51,8 @@ class RepoIndexTest extends Specification { new RepoIndex.SourceRoot("sourceRoot2", Language.GROOVY)) def duplicateKeys = [(myClassName): [ - sourcePath("sourceRoot1", myClassName), - sourcePath("sourceRoot2", myClassName) + "sourceRoot1/" + myClassName.replace('.' as char, '/' as char) + Language.GROOVY.extension, + "sourceRoot2/" + myClassName.replace('.' as char, '/' as char) + Language.GROOVY.extension ]] def repoIndex = new RepoIndex(trie, duplicateKeys, sourceRoots, Collections.emptyList()) @@ -60,7 +64,10 @@ class RepoIndexTest extends Specification { then: def paths = deserialized.getSourcePaths(RepoIndexTest) paths.size() == 2 - paths.containsAll([sourcePath("sourceRoot1", myClassName), sourcePath("sourceRoot2", myClassName)]) + normalizePathSeparators(paths).containsAll([ + "sourceRoot1/" + myClassName.replace('.' as char, '/' as char) + Language.GROOVY.extension, + "sourceRoot2/" + myClassName.replace('.' as char, '/' as char) + Language.GROOVY.extension + ]) } def "test getSourcePaths returns all paths for duplicate key"() { @@ -75,8 +82,8 @@ class RepoIndexTest extends Specification { new RepoIndex.SourceRoot("debug", Language.GROOVY), new RepoIndex.SourceRoot("release", Language.GROOVY)) - def expectedPath1 = sourcePath("debug", myClassName) - def expectedPath2 = sourcePath("release", myClassName) + def expectedPath1 = "debug/" + myClassName.replace('.' as char, '/' as char) + Language.GROOVY.extension + def expectedPath2 = "release/" + myClassName.replace('.' as char, '/' as char) + Language.GROOVY.extension def duplicateKeys = [(myClassName): [expectedPath1, expectedPath2]] def repoIndex = new RepoIndex(trie, duplicateKeys, sourceRoots, Collections.emptyList()) @@ -86,7 +93,7 @@ class RepoIndexTest extends Specification { then: paths.size() == 2 - paths.containsAll([expectedPath1, expectedPath2]) + normalizePathSeparators(paths).containsAll([expectedPath1, expectedPath2]) } def "test getSourcePaths returns single path for non-duplicate key"() { @@ -98,7 +105,7 @@ class RepoIndexTest extends Specification { def trie = trieBuilder.buildTrie() def sourceRoots = Arrays.asList( - new RepoIndex.SourceRoot("src/main/groovy", Language.GROOVY)) + new RepoIndex.SourceRoot(Paths.get("src", "main", "groovy").toString(), Language.GROOVY)) def repoIndex = new RepoIndex(trie, Collections.emptyMap(), sourceRoots, Collections.emptyList()) @@ -107,7 +114,8 @@ class RepoIndexTest extends Specification { then: paths.size() == 1 - paths.first() == sourcePath("src/main/groovy", myClassName) + normalizePathSeparators(paths.first()) == "src/main/groovy/" + + myClassName.replace('.' as char, '/' as char) + Language.GROOVY.extension } def "test source root uses its filesystem separator"() { @@ -117,8 +125,4 @@ class RepoIndexTest extends Specification { expect: sourceRoot.resolveSourcePath("example.MyClass") == "src\\main\\groovy\\example\\MyClass.groovy" } - - private static String sourcePath(String sourceRoot, String className) { - return Paths.get(sourceRoot, className.replace('.' as char, File.separatorChar) + Language.GROOVY.extension).toString() - } } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy index c6c8c6af6de..ec4dfc5c6bf 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/propagation/StringModuleTest.groovy @@ -22,6 +22,7 @@ import static com.datadog.iast.taint.TaintUtils.getStringFromTaintFormat import static com.datadog.iast.taint.TaintUtils.taint import static com.datadog.iast.taint.TaintUtils.taintFormat import static com.datadog.iast.taint.TaintUtils.taintObject +import static datadog.trace.test.util.PlatformTestUtils.normalizeLineEndings @CompileDynamic class StringModuleTest extends IastModuleImplTestBase { @@ -1018,14 +1019,14 @@ class StringModuleTest extends IastModuleImplTestBase { } final formatted = String.format(format, args as Object[]) final expected = getStringFromTaintFormat(expectedTainted) - assert expected == formatted // validate expectation is OK + assert expected == normalizeLineEndings(formatted) // validate expectation is OK when: module.onStringFormat(format, args as Object[], formatted) then: final tainted = to.get(formatted) - final formattedResult = taintFormat(formatted, tainted?.ranges) + final formattedResult = normalizeLineEndings(taintFormat(formatted, tainted?.ranges)) assert formattedResult == expectedTainted: tainted?.ranges where: @@ -1046,9 +1047,9 @@ class StringModuleTest extends IastModuleImplTestBase { 'Hello ==>%s<==' | ['World!'] | 'Hello ==>World!<==' // tainted placeholder [non tainted parameter] 'He==>llo %s!<==' | ['World'] | 'He==>llo <====>World<====>!<==' // tainted placeholder (2) [non tainted parameter] 'He==>llo %s!<==' | ['W==>or<==ld'] | 'He==>llo <==W==>or<==ld==>!<==' // tainted placeholder (3) [mixing with tainted parameter] - 'Hello %n %n %s!%n' | ['W==>or<==ld'] | 'Hello ' + System.lineSeparator() + ' ' + System.lineSeparator() + ' W==>or<==ld!' + System.lineSeparator() // platform newline + 'Hello %n %n %s!%n' | ['W==>or<==ld'] | 'Hello \n \n W==>or<==ld!\n' // \n character 'Hello %% %% %s!%%' | ['W==>or<==ld'] | 'Hello % % W==>or<==ld!%' // % character - '==>Hello %n %s!<==' | ['World'] | '==>Hello <====>' + System.lineSeparator() + '<====> <====>World<====>!<==' // platform newline in tainted format (each placeholder generates a separate range) + '==>Hello %n %s!<==' | ['World'] | '==>Hello <====>\n<====> <====>World<====>!<==' // \n character in tainted format (each placeholder generates a separate range) '==>Hello %% %s!<==' | ['World'] | '==>Hello <====>%<====> <====>World<====>!<==' // % character in tainted format (each placeholder generates a separate range) } diff --git a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/ApplicationModuleTest.groovy b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/ApplicationModuleTest.groovy index 7bfbc1b8813..ba3b1e6c3d1 100644 --- a/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/ApplicationModuleTest.groovy +++ b/dd-java-agent/agent-iast/src/test/groovy/com/datadog/iast/sink/ApplicationModuleTest.groovy @@ -17,6 +17,7 @@ import static com.datadog.iast.model.VulnerabilityType.INSECURE_JSP_LAYOUT import static com.datadog.iast.model.VulnerabilityType.SESSION_TIMEOUT import static com.datadog.iast.model.VulnerabilityType.VERB_TAMPERING import static com.datadog.iast.sink.ApplicationModuleImpl.SESSION_REWRITING_EVIDENCE_VALUE +import static datadog.trace.test.util.PlatformTestUtils.normalizePathSeparators class ApplicationModuleTest extends IastModuleImplTestBase { @@ -128,7 +129,7 @@ class ApplicationModuleTest extends IastModuleImplTestBase { if (expectedEvidence instanceof Collection) { if (expectedVulnType == INSECURE_JSP_LAYOUT) { // some of the nested paths can be dropped by the file visitor - final parts = (evidence.value as String).split('\n')*.trim() + final parts = normalizePathSeparators(evidence.value as String).split('\n')*.trim() assert expectedEvidence.any { parts.contains(it) } } else { expectedEvidence.each { diff --git a/dd-java-agent/agent-logging/build.gradle b/dd-java-agent/agent-logging/build.gradle index 8e8b9d97fc8..bc8d9f1dd6d 100644 --- a/dd-java-agent/agent-logging/build.gradle +++ b/dd-java-agent/agent-logging/build.gradle @@ -26,4 +26,5 @@ dependencies { api libs.slf4j api project(':internal-api') implementation project(':components:json') + testImplementation project(':utils:test-utils') } diff --git a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/LogValidatingSpecification.groovy b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/LogValidatingSpecification.groovy index 088b0d68c12..647e254f08d 100644 --- a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/LogValidatingSpecification.groovy +++ b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/LogValidatingSpecification.groovy @@ -3,6 +3,8 @@ package datadog.trace.logging import org.slf4j.Marker import spock.lang.Specification +import static datadog.trace.test.util.PlatformTestUtils.normalizeLineEndings + abstract class LogValidatingSpecification extends Specification { LogValidator createValidator(String loggerName) { new LogValidator(loggerName) @@ -23,10 +25,6 @@ abstract class LogValidatingSpecification extends Specification { validator.output.reset() } - protected static String normalizeLineEndings(String value) { - value.replace("\r\n", "\n") - } - class LogValidator { private final String name private final ByteArrayOutputStream output diff --git a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/ddlogger/DDLoggerTest.groovy b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/ddlogger/DDLoggerTest.groovy index 045360dd32d..80495ec20f4 100644 --- a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/ddlogger/DDLoggerTest.groovy +++ b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/ddlogger/DDLoggerTest.groovy @@ -10,6 +10,7 @@ import org.slf4j.Logger import static datadog.trace.logging.simplelogger.SLCompatSettings.Names import static datadog.trace.logging.simplelogger.SLCompatSettings.Keys import static datadog.trace.logging.simplelogger.SLCompatSettings.Defaults +import static datadog.trace.test.util.PlatformTestUtils.normalizeLineEndings class DDLoggerTest extends LogValidatingSpecification { diff --git a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatHelperTest.groovy b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatHelperTest.groovy index d57c3244ab7..d247d4671aa 100644 --- a/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatHelperTest.groovy +++ b/dd-java-agent/agent-logging/src/test/groovy/datadog/trace/logging/simplelogger/SLCompatHelperTest.groovy @@ -6,10 +6,12 @@ import spock.lang.Specification import java.text.SimpleDateFormat +import static datadog.trace.test.util.PlatformTestUtils.normalizeLineEndings + class SLCompatHelperTest extends Specification { private static String normalizedOutput(ByteArrayOutputStream outputStream) { - outputStream.toString().replace("\r\n", "\n") + normalizeLineEndings(outputStream.toString()) } private class NoStackException extends Exception { diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy index 4b180cc588b..6937859b9aa 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/AppSecSystemSpecification.groovy @@ -26,10 +26,10 @@ import okhttp3.OkHttpClient import java.nio.file.Files import java.nio.file.Path -import java.nio.file.Paths import java.util.function.BiFunction import static datadog.trace.api.gateway.Events.EVENTS +import static datadog.trace.test.util.PlatformTestUtils.normalizePathSeparators class AppSecSystemSpecification extends DDSpecification { SubscriptionService subService = Mock() @@ -49,15 +49,14 @@ class AppSecSystemSpecification extends DDSpecification { void 'throws if custom config does not exist'() { setup: - String missingRules = Paths.get(File.separator, 'file', 'that', 'does', 'not', 'exist') - injectSysConfig('dd.appsec.rules', missingRules) + injectSysConfig('dd.appsec.rules', '/file/that/does/not/exist') when: AppSecSystem.start(subService, sharedCommunicationObjects()) then: def exception = thrown(AbortStartupException) - exception.cause.toString().contains(missingRules) + normalizePathSeparators(exception.cause.toString()).contains('/file/that/does/not/exist') } void 'system should throw AbortStartupException when config file is not valid JSON'() { diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy index 1c3bb0b32ff..742be67d9fa 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/ddwaf/WAFModuleSpecification.groovy @@ -30,6 +30,7 @@ import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types import datadog.appsec.api.blocking.BlockingContentType +import datadog.environment.OperatingSystem import datadog.metrics.api.Monitoring import datadog.remoteconfig.ConfigurationPoller import datadog.remoteconfig.Product @@ -48,6 +49,7 @@ import datadog.trace.bootstrap.instrumentation.api.AgentTracer import datadog.trace.test.util.DDSpecification import datadog.trace.util.stacktrace.StackTraceEvent import okio.Okio +import spock.lang.IgnoreIf import spock.lang.Shared import spock.lang.Unroll @@ -1192,6 +1194,9 @@ class WAFModuleSpecification extends DDSpecification { 0 * _ } + @IgnoreIf(reason = "libddwaf 2.0.1 does not preserve config override precedence on Windows", value = { + OperatingSystem.isWindows() + }) void 'rule toggling data given through configuration'() { ChangeableFlow flow = Mock() initialRuleAdd() diff --git a/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java b/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java index 06c2ca45d9a..c114377dde7 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/util/StackTracesTest.java @@ -1,10 +1,12 @@ package datadog.trace.core.util; +import static datadog.trace.test.util.PlatformTestUtils.normalizeLineEndings; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.params.provider.Arguments.arguments; +import datadog.environment.OperatingSystem; import java.util.stream.Stream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -112,19 +114,22 @@ void getStackTraceFallsBackToClassNameWhenGetMessageAlsoThrows() { @MethodSource("testTruncateArguments") void testTruncate(int limit, String expected) { assertEquals( - normalizePlatformDifferences(expected), - normalizePlatformDifferences(StackTraces.truncate(TRACE, limit))); + normalizeTruncatedTrace(expected), + normalizeTruncatedTrace(StackTraces.truncate(TRACE, limit))); } - private static String normalizePlatformDifferences(String trace) { - return trace - .replace("\r\n", "\n") + private static String normalizeTruncatedTrace(String trace) { + String normalizedTrace = normalizeLineEndings(trace); + if (!OperatingSystem.isWindows()) { + return normalizedTrace; + } + return normalizedTrace // Native line endings change the exact character split around a centre cut. The content on // the adjacent partial lines is intentionally unspecified; the marker and all complete // lines remain exact. .replaceAll( - "(?m)^.*\\n(\\t\\.\\.\\. trace centre-cut to \\d+ chars \\.\\.\\.\\n).*$", - "\n$1"); + "(?m)^.*\\n(\\t\\.\\.\\. trace centre-cut to \\d+ chars \\.\\.\\.\\n).*$", + "\n$1"); } static Stream testTruncateArguments() { diff --git a/remote-config/remote-config-core/src/test/java/datadog/remoteconfig/DefaultConfigurationPollerSpecification.java b/remote-config/remote-config-core/src/test/java/datadog/remoteconfig/DefaultConfigurationPollerSpecification.java index c83f9312bd6..3ececb5498c 100644 --- a/remote-config/remote-config-core/src/test/java/datadog/remoteconfig/DefaultConfigurationPollerSpecification.java +++ b/remote-config/remote-config-core/src/test/java/datadog/remoteconfig/DefaultConfigurationPollerSpecification.java @@ -2,6 +2,7 @@ import static datadog.remoteconfig.tuf.RemoteConfigRequest.ClientInfo.ClientState.ConfigState.APPLY_STATE_ERROR; import static datadog.trace.test.junit.utils.config.WithConfigExtension.injectSysConfig; +import static datadog.trace.test.util.PlatformTestUtils.normalizeLineEndings; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; @@ -866,10 +867,8 @@ static Stream reportableErrorsArguments() { arguments( "two reportable errors", toJson(twoErrors), - String.format( - "Failed to apply configuration due to 2 errors:%n" - + " (1) Not a valid config key: foobar%n" - + " (2) No content for employee/ASM_DD/1.recommended.json/config%n")), + "Failed to apply configuration due to 2 errors:\n (1) Not a valid config key: foobar\n" + + " (2) No content for employee/ASM_DD/1.recommended.json/config\n"), arguments( "in target_files but not signed", toJson(notInTargets), @@ -905,7 +904,7 @@ void reportableErrors(String scenario, String bodyStr, String errorMsg) throws I Map state = clientState(parseBody()); assertTrue(asList(state.get("config_states")).isEmpty()); assertEquals(Boolean.TRUE, state.get("has_error")); - assertEquals(errorMsg, state.get("error")); + assertEquals(errorMsg, normalizeLineEndings((String) state.get("error"))); } @Test diff --git a/utils/test-utils/build.gradle.kts b/utils/test-utils/build.gradle.kts index 5ca8eead031..c47732851d6 100644 --- a/utils/test-utils/build.gradle.kts +++ b/utils/test-utils/build.gradle.kts @@ -22,7 +22,6 @@ extra["excludedClassesCoverage"] = listOf( "datadog.trace.test.util.FlakySpockExtension*", "datadog.trace.test.util.MultipartRequestParser*", "datadog.trace.test.util.NonRetryable", - "datadog.trace.test.util.PortableCommand", ) dependencies { diff --git a/utils/test-utils/src/main/java/datadog/trace/test/util/PlatformTestUtils.java b/utils/test-utils/src/main/java/datadog/trace/test/util/PlatformTestUtils.java new file mode 100644 index 00000000000..e6336549819 --- /dev/null +++ b/utils/test-utils/src/main/java/datadog/trace/test/util/PlatformTestUtils.java @@ -0,0 +1,45 @@ +package datadog.trace.test.util; + +import datadog.environment.OperatingSystem; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** Normalizes values produced by the current platform for comparison with test fixtures. */ +public final class PlatformTestUtils { + private PlatformTestUtils() {} + + /** Converts Windows CRLF line endings to LF; returns the original value on other platforms. */ + public static String normalizeLineEndings(String value) { + return normalizeLineEndings(value, OperatingSystem.isWindows()); + } + + static String normalizeLineEndings(String value, boolean isWindows) { + return isWindows ? value.replace("\r\n", "\n") : value; + } + + /** Converts Windows path separators to slashes; returns the original value on other platforms. */ + public static String normalizePathSeparators(String value) { + return normalizePathSeparators(value, OperatingSystem.isWindows()); + } + + static String normalizePathSeparators(String value, boolean isWindows) { + return isWindows ? value.replace('\\', '/') : value; + } + + /** Normalizes a collection of paths; returns the original collection on non-Windows platforms. */ + public static Collection normalizePathSeparators(Collection values) { + return normalizePathSeparators(values, OperatingSystem.isWindows()); + } + + static Collection normalizePathSeparators(Collection values, boolean isWindows) { + if (!isWindows) { + return values; + } + List normalizedValues = new ArrayList<>(values.size()); + for (String value : values) { + normalizedValues.add(normalizePathSeparators(value, true)); + } + return normalizedValues; + } +} diff --git a/utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java b/utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java index 21fc295fd8c..c35931f9952 100644 --- a/utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java +++ b/utils/test-utils/src/main/java/datadog/trace/test/util/PortableCommand.java @@ -2,6 +2,8 @@ import de.thetaphi.forbiddenapis.SuppressForbidden; import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Path; @@ -12,10 +14,7 @@ public final class PortableCommand { private PortableCommand() {} public static String[] command(String... arguments) { - Path executable = Paths.get(System.getProperty("java.home"), "bin", "java"); - if (!Files.isRegularFile(executable)) { - executable = executable.resolveSibling("java.exe"); - } + Path executable = javaExecutable(Paths.get(System.getProperty("java.home"))); Path classpath; try { @@ -35,17 +34,30 @@ public static String[] command(String... arguments) { return command; } + static Path javaExecutable(Path javaHome) { + Path executable = javaHome.resolve("bin").resolve("java"); + if (!Files.isRegularFile(executable)) { + executable = executable.resolveSibling("java.exe"); + } + return executable; + } + @SuppressForbidden public static void main(String[] arguments) throws IOException, InterruptedException { + execute(arguments, System.in, System.out); + } + + static void execute(String[] arguments, InputStream input, PrintStream output) + throws IOException, InterruptedException { switch (arguments[0]) { case "echo": - System.out.println(arguments[1]); + output.println(arguments[1]); break; case "copy-input": byte[] buffer = new byte[1024]; int read; - while ((read = System.in.read(buffer)) != -1) { - System.out.write(buffer, 0, read); + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); } break; case "sleep": diff --git a/utils/test-utils/src/test/java/datadog/trace/test/util/PlatformTestUtilsTest.java b/utils/test-utils/src/test/java/datadog/trace/test/util/PlatformTestUtilsTest.java new file mode 100644 index 00000000000..6469fc9b6e3 --- /dev/null +++ b/utils/test-utils/src/test/java/datadog/trace/test/util/PlatformTestUtilsTest.java @@ -0,0 +1,49 @@ +package datadog.trace.test.util; + +import static datadog.trace.test.util.PlatformTestUtils.normalizeLineEndings; +import static datadog.trace.test.util.PlatformTestUtils.normalizePathSeparators; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import datadog.environment.OperatingSystem; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import org.junit.jupiter.api.Test; + +class PlatformTestUtilsTest { + @Test + void convertsWindowsLineEndingsOnlyOnWindows() { + String value = "first\r\nsecond"; + + assertEquals("first\nsecond", PlatformTestUtils.normalizeLineEndings(value, true)); + assertSame(value, PlatformTestUtils.normalizeLineEndings(value, false)); + assertEquals( + OperatingSystem.isWindows() ? "first\nsecond" : value, normalizeLineEndings(value)); + } + + @Test + void convertsWindowsPathSeparatorsOnlyOnWindows() { + String value = "directory\\file"; + + assertEquals("directory/file", PlatformTestUtils.normalizePathSeparators(value, true)); + assertSame(value, PlatformTestUtils.normalizePathSeparators(value, false)); + assertEquals( + OperatingSystem.isWindows() ? "directory/file" : value, normalizePathSeparators(value)); + } + + @Test + void convertsCollectionsWithoutMutatingTheInput() { + List values = Arrays.asList("directory\\file", "another\\file"); + Collection normalizedValues = PlatformTestUtils.normalizePathSeparators(values, true); + + assertNotSame(values, normalizedValues); + assertEquals(Arrays.asList("directory/file", "another/file"), normalizedValues); + assertEquals(Arrays.asList("directory\\file", "another\\file"), values); + assertSame(values, PlatformTestUtils.normalizePathSeparators(values, false)); + assertEquals( + OperatingSystem.isWindows() ? Arrays.asList("directory/file", "another/file") : values, + normalizePathSeparators(values)); + } +} diff --git a/utils/test-utils/src/test/java/datadog/trace/test/util/PortableCommandTest.java b/utils/test-utils/src/test/java/datadog/trace/test/util/PortableCommandTest.java new file mode 100644 index 00000000000..d106cca3af0 --- /dev/null +++ b/utils/test-utils/src/test/java/datadog/trace/test/util/PortableCommandTest.java @@ -0,0 +1,98 @@ +package datadog.trace.test.util; + +import static datadog.trace.test.util.PlatformTestUtils.normalizeLineEndings; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class PortableCommandTest { + @Test + void buildsJavaCommand() { + String[] command = PortableCommand.command("echo", "hello"); + + assertTrue(Files.isRegularFile(Paths.get(command[0]))); + assertEquals("-cp", command[1]); + assertTrue(Files.exists(Paths.get(command[2]))); + assertEquals(PortableCommand.class.getName(), command[3]); + assertArrayEquals(new String[] {"echo", "hello"}, new String[] {command[4], command[5]}); + } + + @Test + void resolvesJavaExecutable(@TempDir Path javaHome) throws IOException { + Path bin = Files.createDirectories(javaHome.resolve("bin")); + Path java = Files.createFile(bin.resolve("java")); + + assertEquals(java, PortableCommand.javaExecutable(javaHome)); + + Files.delete(java); + Path javaExe = Files.createFile(bin.resolve("java.exe")); + + assertEquals(javaExe, PortableCommand.javaExecutable(javaHome)); + } + + @Test + void echoesArgument() throws Exception { + Process process = new ProcessBuilder(PortableCommand.command("echo", "hello")).start(); + + assertTrue(process.waitFor(10, SECONDS)); + assertEquals(0, process.exitValue()); + assertEquals("hello\n", normalizeLineEndings(readFully(process.getInputStream()))); + } + + @Test + void executesCommands() throws Exception { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + PrintStream printOutput = new PrintStream(output, true, UTF_8.name()); + + PortableCommand.execute( + new String[] {"echo", "hello"}, new ByteArrayInputStream(new byte[0]), printOutput); + assertEquals("hello\n", normalizeLineEndings(new String(output.toByteArray(), UTF_8))); + + output.reset(); + PortableCommand.execute( + new String[] {"copy-input"}, + new ByteArrayInputStream("copied".getBytes(UTF_8)), + printOutput); + assertEquals("copied", new String(output.toByteArray(), UTF_8)); + + PortableCommand.execute( + new String[] {"sleep", "0"}, new ByteArrayInputStream(new byte[0]), printOutput); + + assertThrows( + IllegalArgumentException.class, + () -> + PortableCommand.execute( + new String[] {"does-not-exist"}, + new ByteArrayInputStream(new byte[0]), + printOutput)); + } + + @Test + void delegatesMainToCommandExecution() throws Exception { + PortableCommand.main(new String[] {"sleep", "0"}); + } + + private static String readFully(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int read; + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + return new String(output.toByteArray(), UTF_8); + } +} From 5bc53ee8083587acb1b337642c863e1c3c08bfe4 Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Fri, 28 Aug 2026 21:45:40 -0400 Subject: [PATCH 10/10] Fixing tests on Windows. --- components/native-loader/build.gradle.kts | 2 ++ components/native-loader/gradle.lockfile | 2 ++ .../datadog/nativeloader/NativeLoader.java | 26 +++++++------------ .../nativeloader/NativeLoaderTest.java | 18 +++++++++++++ 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/components/native-loader/build.gradle.kts b/components/native-loader/build.gradle.kts index 63e17094130..aee60576305 100644 --- a/components/native-loader/build.gradle.kts +++ b/components/native-loader/build.gradle.kts @@ -5,4 +5,6 @@ plugins { dependencies { implementation(project(":components:environment")) + + testImplementation("com.google.jimfs:jimfs:1.1") } diff --git a/components/native-loader/gradle.lockfile b/components/native-loader/gradle.lockfile index 4708a19b318..66b13881d35 100644 --- a/components/native-loader/gradle.lockfile +++ b/components/native-loader/gradle.lockfile @@ -11,6 +11,8 @@ com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs com.google.code.findbugs:jsr305:3.0.2=compileClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath com.google.code.gson:gson:2.14.0=spotbugs com.google.errorprone:error_prone_annotations:2.48.0=spotbugs +com.google.guava:guava:18.0=testCompileClasspath,testRuntimeClasspath +com.google.jimfs:jimfs:1.1=testCompileClasspath,testRuntimeClasspath com.thoughtworks.qdox:qdox:1.12.1=codenarc commons-io:commons-io:2.21.0=spotbugs de.thetaphi:forbiddenapis:3.10=compileClasspath diff --git a/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java b/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java index b8cf8b412ae..8a41dcaa7d9 100644 --- a/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java +++ b/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java @@ -10,12 +10,10 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.FileAttribute; -import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Set; /** * NativeLoader is intended as a more feature rich replacement for calling {@link @@ -380,25 +378,19 @@ private TempFileHelper() {} static Path createTempFile(Path tempDir, String libname, String libExt) throws IOException, SecurityException { - if (!supportsPosix(tempDir)) { - if (tempDir == null) { - return Files.createTempFile(libname, "." + libExt); - } - Files.createDirectories(tempDir); - return Files.createTempFile(tempDir, libname, "." + libExt); + FileAttribute[] fileAttributes = new FileAttribute[0]; + if (supportsPosix(tempDir)) { + fileAttributes = + new FileAttribute[] { + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")) + }; } - FileAttribute> permAttrs = - PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")); - if (tempDir == null) { - return Files.createTempFile(libname, "." + libExt, permAttrs); + return Files.createTempFile(libname, "." + libExt, fileAttributes); } else { - Files.createDirectories( - tempDir, - PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); - - return Files.createTempFile(tempDir, libname, "." + libExt, permAttrs); + Files.createDirectories(tempDir, fileAttributes); + return Files.createTempFile(tempDir, libname, "." + libExt, fileAttributes); } } diff --git a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java index 8026e417bf7..85d54ced7ff 100644 --- a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java +++ b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java @@ -11,12 +11,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; @@ -30,6 +33,21 @@ import org.junit.jupiter.api.Test; public class NativeLoaderTest { + @Test + public void createTempFileOnNonPosixFileSystem() throws IOException { + try (FileSystem fileSystem = Jimfs.newFileSystem(Configuration.windows())) { + assertFalse(fileSystem.supportedFileAttributeViews().contains("posix")); + Path tempDir = fileSystem.getPath("C:\\temp"); + + Path tempFile = NativeLoader.TempFileHelper.createTempFile(tempDir, "library", "dll"); + + assertTrue(Files.isRegularFile(tempFile)); + assertTrue(tempFile.startsWith(tempDir)); + assertTrue(tempFile.getFileName().toString().startsWith("library")); + assertTrue(tempFile.getFileName().toString().endsWith(".dll")); + } + } + @Test public void preloaded() throws LibraryLoadException { NativeLoader loader = NativeLoader.builder().preloaded("preloaded1", "preloaded2").build();