diff --git a/internal/testdrive/command_test.go b/internal/testdrive/command_test.go index 7ff9f3c7..63de64be 100644 --- a/internal/testdrive/command_test.go +++ b/internal/testdrive/command_test.go @@ -2,6 +2,7 @@ package testdrive import ( "os" + "path/filepath" "testing" "github.com/DataDog/ddtest/internal/framework" @@ -46,3 +47,13 @@ func TestTestdrivePreservesExplicitCommandArguments(t *testing.T) { require.Equal(t, "npm", command) require.Equal(t, []string{"run", "smoke", "--", "--config", "config with spaces.js"}, args) } + +func TestTestdriveMinitestRequiresExecutableRailsBinstub(t *testing.T) { + root := t.TempDir() + t.Chdir(root) + require.NoError(t, os.MkdirAll(filepath.Join(root, "bin"), 0755)) + require.NoError(t, os.WriteFile(filepath.Join(root, "bin", "rails"), []byte("#!/bin/sh\n"), 0644)) + command, args := framework.NewMinitest().Command() + require.Equal(t, "bundle", command) + require.Equal(t, []string{"exec", "rake", "test"}, args) +} diff --git a/internal/testdrive/frameworks_integration_test.go b/internal/testdrive/frameworks_integration_test.go index bedaf9f3..05b0f6f8 100644 --- a/internal/testdrive/frameworks_integration_test.go +++ b/internal/testdrive/frameworks_integration_test.go @@ -39,6 +39,8 @@ func TestPublicFrameworkTestdrives(t *testing.T) { {"cucumber", `{"scripts":{"test":"cucumber-js"},"devDependencies":{"@cucumber/cucumber":"12.2.0"}}`, "npm test", map[string]string{"features/one.feature": "Feature: Arithmetic\n Scenario: Add\n Given addition works\n", "features/step_definitions/one.js": `const {Given} = require('@cucumber/cucumber'); Given('addition works', () => require('node:assert').equal(1+1,2));`}}, {"cypress", `{"scripts":{"test":"cypress run"},"devDependencies":{"cypress":"15.1.0"}}`, "npm test", map[string]string{"cypress.config.js": `module.exports={e2e:{supportFile:false,setupNodeEvents(on,config){on('task',{answer:()=>42});on('after:run',()=>{require('node:fs').writeFileSync('original-hook.txt','ran');});return config;}}};`, "cypress/e2e/one.cy.js": `it('preserves hooks', () => { cy.task('answer').should('equal',42); });`}}, {"pytest", "", "python -m pytest", map[string]string{"requirements.txt": "pytest==8.4.2\n", "pytest.ini": "[pytest]\n", "test_one.py": "def test_adds():\n assert 1 + 1 == 2\n"}}, + {"rspec", "", "bundle exec rspec", map[string]string{"Gemfile": "source 'https://rubygems.org'\ngem 'rspec', '3.13.1'\n", "spec/one_spec.rb": "RSpec.describe 'addition' do\n it('adds') { expect(1 + 1).to eq(2) }\nend\n"}}, + {"minitest", "", "bundle exec rake test", map[string]string{"Gemfile": "source 'https://rubygems.org'\ngem 'rake', '13.3.0'\ngem 'minitest', '5.25.5'\n", "Rakefile": "require 'rake/testtask'\nRake::TestTask.new(:test) { |t| t.pattern = 'test/**/*_test.rb' }\n", "test/one_test.rb": "require 'minitest/autorun'\nclass AdditionTest < Minitest::Test\n def test_adds\n assert_equal 2, 1 + 1\n end\nend\n"}}, } for _, fixture := range fixtures { t.Run(fixture.name, func(t *testing.T) { diff --git a/internal/testdrive/multiframework_test.go b/internal/testdrive/multiframework_test.go index 9e484f32..041c6091 100644 --- a/internal/testdrive/multiframework_test.go +++ b/internal/testdrive/multiframework_test.go @@ -10,6 +10,7 @@ import ( "github.com/DataDog/ddtest/internal/settings" "os" "path/filepath" + "strings" "testing" "github.com/DataDog/ddtest/internal/testdrive/intake" @@ -17,7 +18,7 @@ import ( ) func TestPrepareAllSupportedFrameworks(t *testing.T) { - for _, name := range []string{"jest", "mocha", "vitest", "playwright", "cucumber", "cypress", "pytest"} { + for _, name := range []string{"jest", "mocha", "vitest", "playwright", "cucumber", "cypress", "pytest", "rspec", "minitest"} { t.Run(name, func(t *testing.T) { root := t.TempDir() switch name { @@ -43,6 +44,7 @@ func TestPrepareAllSupportedFrameworks(t *testing.T) { installer := &fakeTracer{preloadPath: filepath.Join(root, "isolated")} run.platform = installer run.projectTracer = "" + run.projectTracer = "" executor := &fakeTestdriveExecutor{} run.executor = executor run.startIntake = func(string) (localIntake, error) { @@ -65,7 +67,8 @@ func TestPrepareAllSupportedFrameworks(t *testing.T) { require.NotContains(t, executor.env, "NODE_OPTIONS") case "ruby": require.NotContains(t, executor.env, "BUNDLE_GEMFILE") - require.Contains(t, preview.String(), "Bundler updates the project Gemfile and lockfile") + require.Contains(t, preview.String(), "Bundler updates Gemfile and Gemfile.lock.") + require.NotContains(t, preview.String(), "It will not change") require.Contains(t, output.String(), "datadog-ci · installed in project") require.Contains(t, executor.env["RUBYOPT"], "datadog/ci/auto_instrument") require.NotContains(t, executor.env, "NODE_OPTIONS") @@ -125,7 +128,9 @@ func TestLanguageEnvironmentsPreserveCustomerOptions(t *testing.T) { python := (&Testdrive{language: "python"}).environment("/session/python", "http://127.0.0.1:1234", "session") require.Equal(t, "/session/python"+string(os.PathListSeparator)+"/customer/modules", python["PYTHONPATH"]) require.Equal(t, "-q --ddtrace", python["PYTEST_ADDOPTS"]) - + ruby := (&Testdrive{language: "ruby"}).environment("/session/Gemfile", "http://127.0.0.1:1234", "session") + require.True(t, strings.HasPrefix(ruby["RUBYOPT"], "-W0 ")) + require.NotContains(t, ruby, "BUNDLE_PATH") // Inherit project Bundler configuration. } func TestCypressWrapperUsesExplicitConfigWithoutEditingIt(t *testing.T) { @@ -187,3 +192,16 @@ func TestPythonProjectTracerPreservesImportEnvironment(t *testing.T) { t.Fatal(env) } } + +func TestRubyProjectTracerPreservesBundleEnvironment(t *testing.T) { + drive := &Testdrive{language: "ruby"} + env := drive.environment("", "http://127.0.0.1:1234", "session") + for _, key := range []string{"BUNDLE_GEMFILE", "BUNDLE_PATH", "BUNDLE_APP_CONFIG", "BUNDLE_FROZEN", "BUNDLE_WITHOUT"} { + if _, changed := env[key]; changed { + t.Fatal("project Bundler setting overridden", key) + } + } + if !strings.Contains(env["RUBYOPT"], "-rdatadog/ci/auto_instrument") { + t.Fatal(env) + } +} diff --git a/internal/testdrive/ruby.go b/internal/testdrive/ruby.go new file mode 100644 index 00000000..b2deb334 --- /dev/null +++ b/internal/testdrive/ruby.go @@ -0,0 +1,12 @@ +package testdrive + +import ( + "os" + "strings" +) + +func rubyEnvironment(path string) map[string]string { + env := map[string]string{} + env["RUBYOPT"] = strings.TrimSpace(os.Getenv("RUBYOPT") + " -rbundler/setup -rdatadog/ci/auto_instrument") + return env +} diff --git a/internal/testdrive/testdrive.go b/internal/testdrive/testdrive.go index 14583496..aab0773f 100644 --- a/internal/testdrive/testdrive.go +++ b/internal/testdrive/testdrive.go @@ -79,11 +79,6 @@ func Prepare(version string) (*Testdrive, error) { return nil, err } language := detectedPlatform.Name() - switch runner.Name() { - case "jest", "mocha", "vitest", "playwright", "cucumber", "cypress", "pytest": - default: - return nil, fmt.Errorf("testdrive does not yet support %s", runner.Name()) - } command, args := runner.Command() label := map[string]string{"javascript": "dd-trace", "python": "ddtrace", "ruby": "datadog-ci"}[language] + "@" + version @@ -140,6 +135,10 @@ func (t *Testdrive) Preview(output io.Writer) { } _, _ = fmt.Fprintf(output, " - run: %s\n", shellquote.Join(append([]string{command}, args...)...)) _, _ = fmt.Fprintln(output) + if t.language == "ruby" && t.projectTracer == "" { + _, _ = fmt.Fprintln(output, "Bundler updates Gemfile and Gemfile.lock.") + return + } patterns := map[string][]string{ "javascript": {"package.json", "package-lock.json", "npm-shrinkwrap.json", "yarn.lock", "pnpm-lock.yaml", "bun.lock", "bun.lockb"}, "python": {"pyproject.toml", "setup.py", "setup.cfg", "requirements*.txt", "requirements*.in", "Pipfile", "Pipfile.lock", "poetry.lock", "uv.lock", "pdm.lock", "pylock.toml"}, @@ -190,6 +189,10 @@ func (t *Testdrive) Run(ctx context.Context, output io.Writer) (runErr error) { } tracerLabel := t.tracerLabel + if t.language == "ruby" { + // Bundler owns the project dependency selection. + tracerLabel = "datadog-ci · installed in project" + } if installation.Project { tracerLabel = t.installedTracerLabel(t.projectTracer) + " · reused" } @@ -426,6 +429,8 @@ func (t *Testdrive) environment(path, intakeURL, sessionID string) map[string]st maps.Copy(env, t.javascriptEnvironment(path)) case "python": maps.Copy(env, pythonEnvironment(path)) + case "ruby": + maps.Copy(env, rubyEnvironment(path)) } return env }