diff --git a/integration-tests/CODEOWNERS b/integration-tests/CODEOWNERS new file mode 100644 index 00000000000..538c14b1a96 --- /dev/null +++ b/integration-tests/CODEOWNERS @@ -0,0 +1,2 @@ +# CODEOWNERS for testing purposes +ci-visibility/subproject @datadog-dd-trace-js diff --git a/integration-tests/ci-visibility/subproject/cypress-config.json b/integration-tests/ci-visibility/subproject/cypress-config.json new file mode 100644 index 00000000000..3ad19f9f90a --- /dev/null +++ b/integration-tests/ci-visibility/subproject/cypress-config.json @@ -0,0 +1,9 @@ +{ + "video": false, + "screenshotOnRunFailure": false, + "pluginsFile": "cypress/plugins-old/index.js", + "supportFile": "cypress/support/e2e.js", + "integrationFolder": "cypress/e2e", + "defaultCommandTimeout": 100, + "nodeVersion": "system" +} diff --git a/integration-tests/ci-visibility/subproject/cypress.config.js b/integration-tests/ci-visibility/subproject/cypress.config.js new file mode 100644 index 00000000000..9a786e4ef75 --- /dev/null +++ b/integration-tests/ci-visibility/subproject/cypress.config.js @@ -0,0 +1,11 @@ +module.exports = { + defaultCommandTimeout: 100, + e2e: { + setupNodeEvents (on, config) { + return require('dd-trace/ci/cypress/plugin')(on, config) + }, + specPattern: process.env.SPEC_PATTERN || 'cypress/e2e/**/*.cy.js' + }, + video: false, + screenshotOnRunFailure: false +} diff --git a/integration-tests/ci-visibility/subproject/cypress/e2e/spec.cy.js b/integration-tests/ci-visibility/subproject/cypress/e2e/spec.cy.js new file mode 100644 index 00000000000..c03d23b677c --- /dev/null +++ b/integration-tests/ci-visibility/subproject/cypress/e2e/spec.cy.js @@ -0,0 +1,8 @@ +/* eslint-disable */ +describe('context', () => { + it('passes', () => { + cy.visit('/') + .get('.hello-world') + .should('have.text', 'Hello World') + }) +}) diff --git a/integration-tests/ci-visibility/subproject/cypress/plugins-old/index.js b/integration-tests/ci-visibility/subproject/cypress/plugins-old/index.js new file mode 100644 index 00000000000..f80695694a9 --- /dev/null +++ b/integration-tests/ci-visibility/subproject/cypress/plugins-old/index.js @@ -0,0 +1 @@ +module.exports = require('dd-trace/ci/cypress/plugin') diff --git a/integration-tests/ci-visibility/subproject/cypress/support/e2e.js b/integration-tests/ci-visibility/subproject/cypress/support/e2e.js new file mode 100644 index 00000000000..c169d8b1bb2 --- /dev/null +++ b/integration-tests/ci-visibility/subproject/cypress/support/e2e.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line +require('dd-trace/ci/cypress/support') diff --git a/integration-tests/ci-visibility/subproject/features/greetings.feature b/integration-tests/ci-visibility/subproject/features/greetings.feature new file mode 100644 index 00000000000..7f8097b87d5 --- /dev/null +++ b/integration-tests/ci-visibility/subproject/features/greetings.feature @@ -0,0 +1,4 @@ +Feature: Greetings + Scenario: Say greetings + When the greeter says greetings + Then I should have heard "greetings" diff --git a/integration-tests/ci-visibility/subproject/features/support/steps.js b/integration-tests/ci-visibility/subproject/features/support/steps.js new file mode 100644 index 00000000000..6a946067ca9 --- /dev/null +++ b/integration-tests/ci-visibility/subproject/features/support/steps.js @@ -0,0 +1,15 @@ +const assert = require('assert') +const { When, Then } = require('@cucumber/cucumber') +class Greeter { + sayGreetings () { + return 'greetings' + } +} + +When('the greeter says greetings', function () { + this.whatIHeard = new Greeter().sayGreetings() +}) + +Then('I should have heard {string}', function (expectedResponse) { + assert.equal(this.whatIHeard, expectedResponse) +}) diff --git a/integration-tests/ci-visibility/subproject/package.json b/integration-tests/ci-visibility/subproject/package.json new file mode 100644 index 00000000000..dc1d9050f8e --- /dev/null +++ b/integration-tests/ci-visibility/subproject/package.json @@ -0,0 +1,6 @@ +{ + "name": "subproject", + "private": true, + "version": "1.0.0", + "description": "app within repo" +} diff --git a/integration-tests/ci-visibility/subproject/playwright-tests/landing-page-test.js b/integration-tests/ci-visibility/subproject/playwright-tests/landing-page-test.js new file mode 100644 index 00000000000..34e6eb2c3aa --- /dev/null +++ b/integration-tests/ci-visibility/subproject/playwright-tests/landing-page-test.js @@ -0,0 +1,13 @@ +const { test, expect } = require('@playwright/test') + +test.beforeEach(async ({ page }) => { + await page.goto(process.env.PW_BASE_URL) +}) + +test.describe('playwright', () => { + test('should work with passing tests', async ({ page }) => { + await expect(page.locator('.hello-world')).toHaveText([ + 'Hello World' + ]) + }) +}) diff --git a/integration-tests/ci-visibility/subproject/playwright.config.js b/integration-tests/ci-visibility/subproject/playwright.config.js new file mode 100644 index 00000000000..3be77049e3b --- /dev/null +++ b/integration-tests/ci-visibility/subproject/playwright.config.js @@ -0,0 +1,18 @@ +// Playwright config file for integration tests +const { devices } = require('@playwright/test') + +const config = { + baseURL: process.env.PW_BASE_URL, + testDir: './playwright-tests', + timeout: 30000, + reporter: 'line', + projects: [ + { + name: 'chromium', + use: devices['Desktop Chrome'] + } + ], + testMatch: '**/*-test.js' +} + +module.exports = config diff --git a/integration-tests/ci-visibility/subproject/subproject-test.js b/integration-tests/ci-visibility/subproject/subproject-test.js new file mode 100644 index 00000000000..5300f1926d6 --- /dev/null +++ b/integration-tests/ci-visibility/subproject/subproject-test.js @@ -0,0 +1,9 @@ +// eslint-disable-next-line +const { expect } = require('chai') + +describe('subproject-test', () => { + it('can run', () => { + // eslint-disable-next-line + expect(1).to.equal(1) + }) +}) diff --git a/integration-tests/ci-visibility/subproject/vitest-test.mjs b/integration-tests/ci-visibility/subproject/vitest-test.mjs new file mode 100644 index 00000000000..d495a14a98c --- /dev/null +++ b/integration-tests/ci-visibility/subproject/vitest-test.mjs @@ -0,0 +1,7 @@ +import { describe, test, expect } from 'vitest' + +describe('context', () => { + test('can report passed test', () => { + expect(1 + 2).to.equal(3) + }) +}) diff --git a/integration-tests/config-jest.js b/integration-tests/config-jest.js index 29366c94d53..f30aec0ad35 100644 --- a/integration-tests/config-jest.js +++ b/integration-tests/config-jest.js @@ -1,5 +1,5 @@ module.exports = { - projects: [__dirname], + projects: process.env.PROJECTS ? JSON.parse(process.env.PROJECTS) : [__dirname], testPathIgnorePatterns: ['/node_modules/'], cache: false, testMatch: [ diff --git a/integration-tests/cucumber/cucumber.spec.js b/integration-tests/cucumber/cucumber.spec.js index 72b753a4227..ca338d12a39 100644 --- a/integration-tests/cucumber/cucumber.spec.js +++ b/integration-tests/cucumber/cucumber.spec.js @@ -31,7 +31,9 @@ const { TEST_IS_NEW, TEST_IS_RETRY, TEST_NAME, - CUCUMBER_IS_PARALLEL + CUCUMBER_IS_PARALLEL, + TEST_SUITE, + TEST_CODE_OWNERS } = require('../../packages/dd-trace/src/plugins/util/test') const isOldNode = semver.satisfies(process.version, '<=16') @@ -1087,6 +1089,35 @@ versions.forEach(version => { } }) }) + + it('correctly calculates test code owners when working directory is not repository root', (done) => { + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const test = events.find(event => event.type === 'test').content + // The test is in a subproject + assert.notEqual(test.meta[TEST_SOURCE_FILE], test.meta[TEST_SUITE]) + assert.equal(test.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) + }) + + childProcess = exec( + 'node ../../node_modules/.bin/cucumber-js features/*.feature', + { + cwd: `${cwd}/ci-visibility/subproject`, + env: { + ...getCiVisAgentlessConfig(receiver.port) + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + eventsPromise.then(() => { + done() + }).catch(done) + }) + }) }) }) }) diff --git a/integration-tests/cypress/cypress.spec.js b/integration-tests/cypress/cypress.spec.js index 4953423587e..8a5a67722fb 100644 --- a/integration-tests/cypress/cypress.spec.js +++ b/integration-tests/cypress/cypress.spec.js @@ -30,7 +30,9 @@ const { TEST_SOURCE_FILE, TEST_IS_NEW, TEST_IS_RETRY, - TEST_EARLY_FLAKE_ENABLED + TEST_EARLY_FLAKE_ENABLED, + TEST_SUITE, + TEST_CODE_OWNERS } = require('../../packages/dd-trace/src/plugins/util/test') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') const { NODE_MAJOR } = require('../../version') @@ -1271,5 +1273,51 @@ moduleTypes.forEach(({ }) }) }) + + it('correctly calculates test code owners when working directory is not repository root', (done) => { + let command + + if (type === 'commonJS') { + const commandSuffix = version === '6.7.0' + ? '--config-file cypress-config.json --spec "cypress/e2e/*.cy.js"' + : '' + command = `../../node_modules/.bin/cypress run ${commandSuffix}` + } else { + command = `node --loader=${hookFile} ../../cypress-esm-config.mjs` + } + + const { + NODE_OPTIONS, // NODE_OPTIONS dd-trace config does not work with cypress + ...restEnvVars + } = getCiVisAgentlessConfig(receiver.port) + + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const test = events.find(event => event.type === 'test').content + // The test is in a subproject + assert.notEqual(test.meta[TEST_SOURCE_FILE], test.meta[TEST_SUITE]) + assert.equal(test.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) + }, 25000) + + childProcess = exec( + command, + { + cwd: `${cwd}/ci-visibility/subproject`, + env: { + ...restEnvVars, + CYPRESS_BASE_URL: `http://localhost:${webAppPort}` + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + eventsPromise.then(() => { + done() + }).catch(done) + }) + }) }) }) diff --git a/integration-tests/jest/jest.spec.js b/integration-tests/jest/jest.spec.js index 9a67d7a666d..c3d2410c48c 100644 --- a/integration-tests/jest/jest.spec.js +++ b/integration-tests/jest/jest.spec.js @@ -30,7 +30,8 @@ const { TEST_NAME, JEST_DISPLAY_NAME, TEST_EARLY_FLAKE_ABORT_REASON, - TEST_SOURCE_START + TEST_SOURCE_START, + TEST_CODE_OWNERS } = require('../../packages/dd-trace/src/plugins/util/test') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -240,6 +241,38 @@ describe('jest CommonJS', () => { }) }) + it('correctly calculates test code owners when working directory is not repository root', (done) => { + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const test = events.find(event => event.type === 'test').content + // The test is in a subproject + assert.notEqual(test.meta[TEST_SOURCE_FILE], test.meta[TEST_SUITE]) + assert.equal(test.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) + }) + + childProcess = exec( + 'node ./node_modules/jest/bin/jest --config config-jest.js --rootDir ci-visibility/subproject', + { + cwd, + env: { + ...getCiVisAgentlessConfig(receiver.port), + PROJECTS: JSON.stringify([{ + testMatch: ['**/subproject-test*'] + }]) + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + eventsPromise.then(() => { + done() + }).catch(done) + }) + }) + it('works when sharding', (done) => { receiver.payloadReceived(({ url }) => url === '/api/v2/citestcycle').then(events => { const testSuiteEvents = events.payload.events.filter(event => event.type === 'test_suite_end') diff --git a/integration-tests/mocha/mocha.spec.js b/integration-tests/mocha/mocha.spec.js index 16ebeaff4b3..1c22eafcb28 100644 --- a/integration-tests/mocha/mocha.spec.js +++ b/integration-tests/mocha/mocha.spec.js @@ -31,7 +31,8 @@ const { TEST_COMMAND, TEST_MODULE, MOCHA_IS_PARALLEL, - TEST_SOURCE_START + TEST_SOURCE_START, + TEST_CODE_OWNERS } = require('../../packages/dd-trace/src/plugins/util/test') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -240,6 +241,35 @@ describe('mocha CommonJS', function () { }) }) + it('correctly calculates test code owners when working directory is not repository root', (done) => { + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const test = events.find(event => event.type === 'test').content + // The test is in a subproject + assert.notEqual(test.meta[TEST_SOURCE_FILE], test.meta[TEST_SUITE]) + assert.equal(test.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) + }) + + childProcess = exec( + 'node ../../node_modules/mocha/bin/mocha subproject-test.js', + { + cwd: `${cwd}/ci-visibility/subproject`, + env: { + ...getCiVisAgentlessConfig(receiver.port) + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + eventsPromise.then(() => { + done() + }).catch(done) + }) + }) + it('does not change mocha config if CI Visibility fails to init', (done) => { receiver.assertPayloadReceived(() => { const error = new Error('it should not report tests') diff --git a/integration-tests/playwright/playwright.spec.js b/integration-tests/playwright/playwright.spec.js index 04e6b1bb65a..ea8d4475ccd 100644 --- a/integration-tests/playwright/playwright.spec.js +++ b/integration-tests/playwright/playwright.spec.js @@ -20,7 +20,9 @@ const { TEST_CONFIGURATION_BROWSER_NAME, TEST_IS_NEW, TEST_IS_RETRY, - TEST_EARLY_FLAKE_ENABLED + TEST_EARLY_FLAKE_ENABLED, + TEST_SUITE, + TEST_CODE_OWNERS } = require('../../packages/dd-trace/src/plugins/util/test') const { ERROR_MESSAGE } = require('../../packages/dd-trace/src/constants') @@ -570,5 +572,37 @@ versions.forEach((version) => { .catch(done) }) }) + + it('correctly calculates test code owners when working directory is not repository root', (done) => { + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const test = events.find(event => event.type === 'test').content + // The test is in a subproject + assert.notEqual(test.meta[TEST_SOURCE_FILE], test.meta[TEST_SUITE]) + assert.equal(test.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) + }) + + childProcess = exec( + '../../node_modules/.bin/playwright test', + { + cwd: `${cwd}/ci-visibility/subproject`, + env: { + ...getCiVisAgentlessConfig(receiver.port), + PW_BASE_URL: `http://localhost:${webAppPort}`, + PW_RUNNER_DEBUG: '1', + TEST_DIR: '.' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + eventsPromise.then(() => { + done() + }).catch(done) + }) + }) }) }) diff --git a/integration-tests/vitest/vitest.spec.js b/integration-tests/vitest/vitest.spec.js index e543608d43e..1e8dd8b99ea 100644 --- a/integration-tests/vitest/vitest.spec.js +++ b/integration-tests/vitest/vitest.spec.js @@ -12,7 +12,8 @@ const { FakeCiVisIntake } = require('../ci-visibility-intake') const { TEST_STATUS, TEST_TYPE, - TEST_IS_RETRY + TEST_IS_RETRY, + TEST_CODE_OWNERS } = require('../../packages/dd-trace/src/plugins/util/test') // tested with 1.6.0 @@ -203,5 +204,34 @@ versions.forEach((version) => { ) }) }) + + it('correctly calculates test code owners when working directory is not repository root', (done) => { + const eventsPromise = receiver + .gatherPayloadsMaxTimeout(({ url }) => url.endsWith('/api/v2/citestcycle'), (payloads) => { + const events = payloads.flatMap(({ payload }) => payload.events) + + const test = events.find(event => event.type === 'test').content + assert.equal(test.meta[TEST_CODE_OWNERS], JSON.stringify(['@datadog-dd-trace-js'])) + }) + + childProcess = exec( + '../../node_modules/.bin/vitest run', + { + cwd: `${cwd}/ci-visibility/subproject`, + env: { + ...getCiVisAgentlessConfig(receiver.port), + NODE_OPTIONS: '--import dd-trace/register.js -r dd-trace/ci/init', // ESM requires more flags + TEST_DIR: './vitest-test.mjs' + }, + stdio: 'inherit' + } + ) + + childProcess.on('exit', () => { + eventsPromise.then(() => { + done() + }).catch(done) + }) + }) }) }) diff --git a/packages/datadog-plugin-cypress/src/cypress-plugin.js b/packages/datadog-plugin-cypress/src/cypress-plugin.js index aba25cf6611..a918344f6a9 100644 --- a/packages/datadog-plugin-cypress/src/cypress-plugin.js +++ b/packages/datadog-plugin-cypress/src/cypress-plugin.js @@ -258,7 +258,7 @@ class CypressPlugin { }) } - getTestSpan (testName, testSuite, isUnskippable, isForcedToRun) { + getTestSpan ({ testName, testSuite, isUnskippable, isForcedToRun, testSourceFile }) { const testSuiteTags = { [TEST_COMMAND]: this.command, [TEST_COMMAND]: this.command, @@ -282,8 +282,11 @@ class CypressPlugin { ...testSpanMetadata } = getTestCommonTags(testName, testSuite, this.cypressConfig.version, TEST_FRAMEWORK_NAME) - const codeOwners = getCodeOwnersForFilename(testSuite, this.codeOwnersEntries) + if (testSourceFile) { + testSpanMetadata[TEST_SOURCE_FILE] = testSourceFile + } + const codeOwners = this.getTestCodeOwners({ testSuite, testSourceFile }) if (codeOwners) { testSpanMetadata[TEST_CODE_OWNERS] = codeOwners } @@ -480,12 +483,16 @@ class CypressPlugin { const isSkippedByItr = this.testsToSkip.find(test => cypressTestName === test.name && spec.relative === test.suite ) - const skippedTestSpan = this.getTestSpan(cypressTestName, spec.relative) + let testSourceFile + if (spec.absolute && this.repositoryRoot) { - skippedTestSpan.setTag(TEST_SOURCE_FILE, getTestSuitePath(spec.absolute, this.repositoryRoot)) + testSourceFile = getTestSuitePath(spec.absolute, this.repositoryRoot) } else { - skippedTestSpan.setTag(TEST_SOURCE_FILE, spec.relative) + testSourceFile = spec.relative } + + const skippedTestSpan = this.getTestSpan({ testName: cypressTestName, testSuite: spec.relative, testSourceFile }) + skippedTestSpan.setTag(TEST_STATUS, 'skip') if (isSkippedByItr) { skippedTestSpan.setTag(TEST_SKIPPED_BY_ITR, 'true') @@ -538,11 +545,21 @@ class CypressPlugin { if (this.itrCorrelationId) { finishedTest.testSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId) } + let testSourceFile if (spec.absolute && this.repositoryRoot) { - finishedTest.testSpan.setTag(TEST_SOURCE_FILE, getTestSuitePath(spec.absolute, this.repositoryRoot)) + testSourceFile = getTestSuitePath(spec.absolute, this.repositoryRoot) } else { - finishedTest.testSpan.setTag(TEST_SOURCE_FILE, spec.relative) + testSourceFile = spec.relative + } + if (testSourceFile) { + finishedTest.testSpan.setTag(TEST_SOURCE_FILE, testSourceFile) + } + const codeOwners = this.getTestCodeOwners({ testSuite: spec.relative, testSourceFile }) + + if (codeOwners) { + finishedTest.testSpan.setTag(TEST_CODE_OWNERS, codeOwners) } + finishedTest.testSpan.finish(finishedTest.finishTime) }) }) @@ -591,7 +608,12 @@ class CypressPlugin { } if (!this.activeTestSpan) { - this.activeTestSpan = this.getTestSpan(testName, testSuite, isUnskippable, isForcedToRun) + this.activeTestSpan = this.getTestSpan({ + testName, + testSuite, + isUnskippable, + isForcedToRun + }) } return this.activeTestSpan ? { traceId: this.activeTestSpan.context().toTraceId() } : {} @@ -658,6 +680,13 @@ class CypressPlugin { } } } + + getTestCodeOwners ({ testSuite, testSourceFile }) { + if (testSourceFile) { + return getCodeOwnersForFilename(testSourceFile, this.codeOwnersEntries) + } + return getCodeOwnersForFilename(testSuite, this.codeOwnersEntries) + } } module.exports = new CypressPlugin() diff --git a/packages/dd-trace/src/plugins/ci_plugin.js b/packages/dd-trace/src/plugins/ci_plugin.js index 4daeb02a4bf..e09af4bed82 100644 --- a/packages/dd-trace/src/plugins/ci_plugin.js +++ b/packages/dd-trace/src/plugins/ci_plugin.js @@ -16,7 +16,8 @@ const { getTestSuiteCommonTags, TEST_STATUS, TEST_SKIPPED_BY_ITR, - ITR_CORRELATION_ID + ITR_CORRELATION_ID, + TEST_SOURCE_FILE } = require('./util/test') const Plugin = require('./plugin') const { COMPONENT } = require('../constants') @@ -207,7 +208,13 @@ module.exports = class CiPlugin extends Plugin { ...extraTags } - const codeOwners = getCodeOwnersForFilename(testSuite, this.codeOwnersEntries) + const { [TEST_SOURCE_FILE]: testSourceFile } = extraTags + // We'll try with the test source file if available (it could be different from the test suite) + let codeOwners = getCodeOwnersForFilename(testSourceFile, this.codeOwnersEntries) + if (!codeOwners) { + codeOwners = getCodeOwnersForFilename(testSuite, this.codeOwnersEntries) + } + if (codeOwners) { testTags[TEST_CODE_OWNERS] = codeOwners }