Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,19 @@ jobs:
with:
python-version: "3.12"

- name: Set up Node.js for project environment regression tests
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "22"

- name: Install Yarn for the Plug'n'Play regression fixture
run: npm install --global yarn@1.22.22

- name: Set up Ruby for project environment regression tests
uses: ruby/setup-ruby@762794c140bbeda0f1224786aa33b4b46783a6c1 # v1
with:
ruby-version: "3.4"

- name: Cache Go modules
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
Expand Down Expand Up @@ -67,6 +80,7 @@ jobs:
run: go test -race -coverpkg=./... -coverprofile=coverage.out -covermode=atomic ./...
env:
DD_ENV: ci
DDTEST_REQUIRE_PROJECT_RUNTIMES: "1"

- name: Upload coverage report to Datadog
if: ${{ env.DATADOG_API_KEY_CONFIGURED == 'true' }}
Expand Down Expand Up @@ -227,9 +241,9 @@ jobs:
- name: Test Jest adapter
run: |
jest_dir="${RUNNER_TEMP}/jest-${{ matrix.jest }}"
npm install --prefix "${jest_dir}" "jest@${{ matrix.jest }}"
npm install --prefix "${jest_dir}" "jest@${{ matrix.jest }}" dd-trace@5.86.0
DDTEST_JEST_NODE_MODULES="${jest_dir}/node_modules" \
go test -v ./internal/compatibility -run '^TestJestAdapterIntegration$'
go test -v ./internal/compatibility -run '^TestJest(Adapter|Testdrive)Integration$'

vitest-compatibility:
runs-on: ubuntu-latest
Expand Down
13 changes: 7 additions & 6 deletions docs/running.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,9 @@ When `--tests-location` or `--tests-exclude-pattern` is set, DDTest filters the
file list returned by Jest after discovery; it does not pass `--tests-location`
as Jest's `--testMatch`.

DDTest prepends `-r dd-trace/ci/init` to `NODE_OPTIONS` for worker processes
unless `NODE_OPTIONS` already loads `dd-trace/ci/init`.
DDTest appends `-r dd-trace/ci/init` to `NODE_OPTIONS` for worker processes
unless `NODE_OPTIONS` already loads `dd-trace/ci/init`. Existing project loaders,
such as Yarn Plug'n'Play, run before the tracer.

## Cucumber Discovery And Instrumentation

Expand Down Expand Up @@ -336,10 +337,10 @@ projects, include and exclude patterns, and CLI filters without executing tests.
If that API is unavailable, DDTest falls back to its own filesystem glob using
`--tests-location` or the default Vitest test-file pattern.

DDTest prepends both `--import dd-trace/register.js` and
`-r dd-trace/ci/init` to `NODE_OPTIONS` for Vitest worker processes unless they
are already present. Discovery removes these options to avoid instrumenting the
file-listing process.
DDTest adds `--import dd-trace/register.js` and `-r dd-trace/ci/init` to
`NODE_OPTIONS` for Vitest worker processes unless they are already present.
The tracer's `--require` option follows existing project loaders. Discovery
removes these options to avoid instrumenting the file-listing process.

## Cypress Discovery And Instrumentation

Expand Down
38 changes: 38 additions & 0 deletions internal/compatibility/jest_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package compatibility

import (
"bytes"
"context"
"os"
"path/filepath"
"strconv"
"testing"
"time"

"github.com/DataDog/ddtest/internal/discovery"
"github.com/DataDog/ddtest/internal/framework"
"github.com/DataDog/ddtest/internal/testdrive"
"github.com/stretchr/testify/require"
)

func TestJestAdapterIntegration(t *testing.T) {
Expand All @@ -33,6 +37,14 @@ func TestJestAdapterIntegration(t *testing.T) {
throw new Error('unselected file ran')
})
`)
writeFixture(t, root, "noisy.cjs", `process.stdout.write('startup without newline');
process.stderr.write('startup stderr');
process.on('exit', () => {
process.stdout.write('shutdown without newline');
process.stderr.write('shutdown stderr');
});
`)
t.Setenv("NODE_OPTIONS", "--require "+strconv.Quote(filepath.Join(root, "noisy.cjs")))
t.Chdir(root)

jest := framework.NewJest()
Expand All @@ -51,3 +63,29 @@ func TestJestAdapterIntegration(t *testing.T) {
t.Fatalf("selected-file run failed: %v", err)
}
}

// A real tracer must emit events, not merely exit successfully. In particular,
// dd-trace 5.86 waits forever for disabled git upload if local settings advertise
// test skipping, allowing Node to exit before Jest starts.
func TestJestTestdriveIntegration(t *testing.T) {
nodeModules := requireEnv(t, "DDTEST_JEST_NODE_MODULES")
root := t.TempDir()
require.NoError(t, os.Symlink(nodeModules, filepath.Join(root, "node_modules")))
writeFixture(t, root, "package.json", `{"name":"testdrive-regression","private":true,"scripts":{"test":"jest"}}`)
writeFixture(t, root, "example.test.js", `test('runs the test body', () => { expect(1 + 1).toBe(2); });`)
writeFixture(t, root, "noisy.cjs", `process.stdout.write('startup'); process.on('exit', () => process.stdout.write('shutdown'));`)
t.Chdir(root)
t.Setenv("NODE_OPTIONS", "--require "+strconv.Quote(filepath.Join(root, "noisy.cjs")))
resetSettingsAfterTest(t)
configureFramework(shellCommand("node", filepath.Join(root, "node_modules", "jest", "bin", "jest.js"), "--runInBand"), "")
drive, err := testdrive.Prepare("git:must-not-install")
require.NoError(t, err)
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
var output bytes.Buffer
require.NoError(t, drive.Run(ctx, &output), output.String())
// Local EFD settings execute this one new test twice.
require.Contains(t, output.String(), "Test events: 2")
require.Contains(t, output.String(), "Jest: Passed")
require.Contains(t, output.String(), "reused")
}
183 changes: 183 additions & 0 deletions internal/compatibility/project_environment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package compatibility

import (
"bytes"
"context"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"testing"
"time"

"github.com/DataDog/ddtest/internal/discovery"
"github.com/DataDog/ddtest/internal/platform"
"github.com/DataDog/ddtest/internal/settings"
"github.com/DataDog/ddtest/internal/testdrive"
"github.com/stretchr/testify/require"
)

// These fixtures use real runtimes and package resolution, with no downloaded
// tracer or mocked command output. CI installs all four prerequisite commands.
func requireRuntime(t *testing.T, name string) {
t.Helper()
if _, err := exec.LookPath(name); err != nil {
if os.Getenv("DDTEST_REQUIRE_PROJECT_RUNTIMES") == "1" {
t.Fatalf("missing required regression-test runtime %s: %v", name, err)
}
t.Skipf("%s is required for the project environment regression test: %v", name, err)
}
}

func runFixtureCommand(t *testing.T, ctx context.Context, name string, args ...string) {
t.Helper()
output, err := exec.CommandContext(ctx, name, args...).CombinedOutput()
require.NoError(t, err, "%s: %s", name, output)
}

func TestJavaScriptProjectEnvironment(t *testing.T) {
requireRuntime(t, "node")
requireRuntime(t, "yarn")
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
root := filepath.Join(t.TempDir(), "project with spaces")
writeFixture(t, root, "package.json", `{"name":"pnp-regression","private":true,"scripts":{"test":"jest"},"dependencies":{"dd-trace":"file:./tracer"}}`)
writeFixture(t, root, "tracer/package.json", `{"name":"dd-trace","version":"1.0.0","dependencies":{"pnp-tracer-helper":"file:../helper"}}`)
writeFixture(t, root, "helper/package.json", `{"name":"pnp-tracer-helper","version":"1.0.0","main":"index.js"}`)
writeFixture(t, root, "helper/index.js", "module.exports = 'loaded through PnP';\n")
writeFixture(t, root, "tracer/ci/init.js", "global.ddtestTracer = require('pnp-tracer-helper');\n")
t.Chdir(root)
t.Setenv("NODE_OPTIONS", "")
t.Setenv("NODE_PATH", "")
runFixtureCommand(t, ctx, "yarn", "install", "--enable-pnp", "--offline", "--ignore-scripts", "--cache-folder", filepath.Join(root, "cache"))
require.NoDirExists(t, filepath.Join(root, "node_modules"))
loader := filepath.Join(root, ".pnp.js") // Yarn Classic's PnP loader.
require.FileExists(t, loader)
writeFixture(t, root, "noisy-preload.cjs", `process.stdout.write('startup log without newline');
console.error('startup stderr');
process.on('exit', () => {
process.stdout.write('shutdown log without newline');
console.error('shutdown stderr');
});
`)
javascript := platform.NewJavaScript()
// Prove the fixture cannot pass through ordinary node_modules resolution.
_, err := javascript.DetectTracer(ctx, platform.TracerOptions{})
require.ErrorContains(t, err, "Cannot find module 'dd-trace/ci/init'")
t.Setenv("NODE_OPTIONS", "--require "+strconv.Quote(loader)+" --require "+strconv.Quote(filepath.Join(root, "noisy-preload.cjs"))+" --max-old-space-size=256")
require.NoError(t, javascript.SanityCheck(ctx))
path, err := javascript.DetectTracer(ctx, platform.TracerOptions{})
require.NoError(t, err)
require.FileExists(t, path)
// Reusing the project must not fall back to a network installation.
installation, err := javascript.InstallTestdriveTracer(ctx, platform.TracerOptions{Directory: t.TempDir(), Version: "git:must-not-install"})
require.NoError(t, err)
require.True(t, installation.Project)
require.Equal(t, path, installation.Path)
// Use the detected result as an actual preload; log-contaminated paths fail.
runFixtureCommand(t, ctx, "node", "--require", path, "-e", "require('dd-trace/ci/init')")

// Exercise the actual discovery/run adapters and testdrive worker startup.
// The tracer's dependency also needs PnP when the tracer path is absolute.
resetSettingsAfterTest(t)
writeFixture(t, root, "example.test.js", "// Worker-startup fixture.\n")
writeFixture(t, root, "worker.cjs", `const assert = require('assert');
const fs = require('fs');
if (process.argv.includes('--listTests')) {
assert.strictEqual(global.ddtestTracer, undefined);
assert(process.argv.includes('--json'));
console.log(JSON.stringify([require('path').resolve('example.test.js')]));
} else {
assert.strictEqual(global.ddtestTracer, 'loaded through PnP');
fs.writeFileSync('worker-ran', 'instrumented');
}
`)
configureFramework(shellCommand("node", filepath.Join(root, "worker.cjs")), "")
t.Run("discovery and execution", func(t *testing.T) {
fw, err := javascript.DetectFramework()
require.NoError(t, err)
files, err := fw.DiscoverTestFiles(ctx, discovery.TestFileSet{Pattern: "**/*.test.js"})
require.NoError(t, err)
require.Len(t, files, 1)
require.NoError(t, fw.RunTests(ctx, files, nil))
require.FileExists(t, filepath.Join(root, "worker-ran"))
require.NoError(t, os.Remove(filepath.Join(root, "worker-ran")))
})
t.Run("testdrive", func(t *testing.T) {
drive, err := testdrive.Prepare("git:must-not-install")
require.NoError(t, err)
var output bytes.Buffer
err = drive.Run(ctx, &output)
// This fixture checks startup, not telemetry. A successful command with
// no events is distinguishable from a failed Node preload.
require.ErrorContains(t, err, "command exited successfully, but Test Optimization sent no test events", output.String())
require.FileExists(t, filepath.Join(root, "worker-ran"))
})
}

func TestRubyProjectEnvironment(t *testing.T) {
requireRuntime(t, "bundle")
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
root := t.TempDir()
writeFixture(t, root, "tracer/datadog-ci.gemspec", `Gem::Specification.new do |s|
s.name = "datadog-ci"
s.version = "1.31.0"
s.summary = "Regression fixture"
s.authors = ["DDTest"]
s.files = []
end
`)
writeFixture(t, root, "support/project_setup.rb", "puts 'startup log'\nwarn 'startup stderr'\nat_exit { puts 'shutdown log'; warn 'shutdown stderr' }\nPROJECT_TRACER_PATH = '../tracer'\n")
writeFixture(t, root, "config/Gemfile.test", "gem 'datadog-ci', path: PROJECT_TRACER_PATH\n")
t.Chdir(root)
t.Setenv("BUNDLE_GEMFILE", filepath.Join(root, "config", "Gemfile.test"))
t.Setenv("BUNDLE_USER_HOME", filepath.Join(root, "bundle-home"))
t.Setenv("RUBYOPT", "-I./support -rproject_setup")
runFixtureCommand(t, ctx, "bundle", "lock", "--local")
ruby := platform.NewRuby(settings.TestSkippingLevelTest)
t.Setenv("RUBYOPT", "")
_, err := ruby.DetectTracer(ctx, platform.TracerOptions{})
require.ErrorContains(t, err, "PROJECT_TRACER_PATH")
t.Setenv("RUBYOPT", "-I./support -rproject_setup")
require.NoError(t, ruby.SanityCheck(ctx))
version, err := ruby.DetectTracer(ctx, platform.TracerOptions{})
require.NoError(t, err)
require.Equal(t, " * datadog-ci (1.31.0)", version)
installation, err := ruby.InstallTestdriveTracer(ctx, platform.TracerOptions{Directory: t.TempDir(), Version: "git:must-not-install"})
require.NoError(t, err)
require.True(t, installation.Project)
}

func TestPythonProjectEnvironment(t *testing.T) {
requireRuntime(t, "python")
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
root := t.TempDir()
// A private venv prevents a globally installed ddtrace from hiding a failure
// to preserve PYTHONPATH. Metadata alone suffices for the version probe.
runFixtureCommand(t, ctx, "python", "-m", "venv", "--without-pip", filepath.Join(root, "venv"))
writeFixture(t, root, "packages/ddtrace-4.11.0.dist-info/METADATA", "Metadata-Version: 2.1\nName: ddtrace\nVersion: 4.11.0\n")
writeFixture(t, root, "packages/sitecustomize.py", `import atexit, sys
print('startup log', end='')
print('startup stderr', file=sys.stderr)
atexit.register(lambda: print('shutdown log', end=''))
atexit.register(lambda: print('shutdown stderr', file=sys.stderr))
`)
t.Chdir(root)
bin := "bin"
if runtime.GOOS == "windows" {
bin = "Scripts"
}
t.Setenv("PATH", filepath.Join(root, "venv", bin)+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("PYTHONPATH", "")
python := platform.NewPython()
_, err := python.DetectTracer(ctx, platform.TracerOptions{Command: "python"})
require.ErrorContains(t, err, "PackageNotFoundError")
t.Setenv("PYTHONPATH", filepath.Join(root, "packages"))
require.NoError(t, python.SanityCheck(ctx))
version, err := python.DetectTracer(ctx, platform.TracerOptions{Command: "python"})
require.NoError(t, err)
require.Equal(t, "4.11.0", version)
}
Loading
Loading