Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ci-visibility] Add get known tests request #4015

Merged
merged 4 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const request = require('../../exporters/common/request')
const id = require('../../id')
const log = require('../../log')

function getKnownTests ({
url,
isEvpProxy,
evpProxyPrefix,
isGzipCompatible,
env,
service,
repositoryUrl,
sha,
osVersion,
osPlatform,
osArchitecture,
runtimeName,
runtimeVersion,
custom
}, done) {
const options = {
path: '/api/v2/ci/libraries/tests',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
timeout: 20000,
url
}

if (isGzipCompatible) {
options.headers['accept-encoding'] = 'gzip'
}

if (isEvpProxy) {
options.path = `${evpProxyPrefix}/api/v2/ci/libraries/tests`
options.headers['X-Datadog-EVP-Subdomain'] = 'api'
} else {
const apiKey = process.env.DATADOG_API_KEY || process.env.DD_API_KEY
if (!apiKey) {
return done(new Error('Skippable suites were not fetched because Datadog API key is not defined.'))
}

options.headers['dd-api-key'] = apiKey
}

const data = JSON.stringify({
data: {
id: id().toString(10),
type: 'ci_app_libraries_tests_request',
attributes: {
configurations: {
'os.platform': osPlatform,
'os.version': osVersion,
'os.architecture': osArchitecture,
'runtime.name': runtimeName,
'runtime.version': runtimeVersion,
custom
},
service,
env,
repository_url: repositoryUrl,
sha
}
}
})

request(data, options, (err, res) => {
if (err) {
done(err)
} else {
try {
const { data: { attributes: { test_full_names: knownTests } } } = JSON.parse(res)
log.debug(() => `Number of received known tests: ${Object.keys(knownTests).length}`)
done(null, knownTests)
} catch (err) {
done(err)
}
}
})
}

module.exports = { getKnownTests }
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const URL = require('url').URL
const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata')
const { getLibraryConfiguration: getLibraryConfigurationRequest } = require('../requests/get-library-configuration')
const { getSkippableSuites: getSkippableSuitesRequest } = require('../intelligent-test-runner/get-skippable-suites')
const { getKnownTests: getKnownTestsRequest } = require('../early-flake-detection/get-known-tests')
const log = require('../../log')
const AgentInfoExporter = require('../../exporters/common/agent-info-exporter')

Expand Down Expand Up @@ -79,6 +80,13 @@ class CiVisibilityExporter extends AgentInfoExporter {
this._libraryConfig?.isSuitesSkippingEnabled)
}

shouldRequestKnownTests () {
return !!(
this._config.isEarlyFlakeDetectionEnabled &&
this._libraryConfig?.isEarlyFlakeDetectionEnabled
)
}

shouldRequestLibraryConfiguration () {
return this._config.isIntelligentTestRunnerEnabled
}
Expand Down Expand Up @@ -115,6 +123,30 @@ class CiVisibilityExporter extends AgentInfoExporter {
})
}

getKnownTests (testConfiguration, callback) {
if (!this.shouldRequestKnownTests()) {
return callback(null)
}
this._canUseCiVisProtocolPromise.then((canUseCiVisProtocol) => {
if (!canUseCiVisProtocol) {
return callback(
new Error('Known tests can not be requested because CI Visibility protocol can not be used')
)
}
const configuration = {
url: this._getApiUrl(),
env: this._config.env,
service: this._config.service,
isEvpProxy: !!this._isUsingEvpProxy,
evpProxyPrefix: this.evpProxyPrefix,
custom: getTestConfigurationTags(this._config.tags),
isGzipCompatible: this._isGzipCompatible,
...testConfiguration
}
getKnownTestsRequest(configuration, callback)
})
}

/**
* We can't request library configuration until we know whether we can use the
* CI Visibility Protocol, hence the this._canUseCiVisProtocol promise.
Expand Down Expand Up @@ -144,7 +176,7 @@ class CiVisibilityExporter extends AgentInfoExporter {
* where the tests run in a subprocess, like Jest,
* because `getLibraryConfiguration` is called only once in the main process.
*/
this._libraryConfig = libraryConfig
this._libraryConfig = this.getConfiguration(libraryConfig)

if (err) {
callback(err, {})
Expand All @@ -155,8 +187,8 @@ class CiVisibilityExporter extends AgentInfoExporter {
return callback(gitUploadError, {})
}
getLibraryConfigurationRequest(configuration, (err, finalLibraryConfig) => {
this._libraryConfig = finalLibraryConfig
callback(err, finalLibraryConfig)
this._libraryConfig = this.getConfiguration(finalLibraryConfig)
callback(err, this._libraryConfig)
})
})
} else {
Expand All @@ -166,6 +198,27 @@ class CiVisibilityExporter extends AgentInfoExporter {
})
}

// Takes into account potential kill switches
getConfiguration (remoteConfiguration) {
if (!remoteConfiguration) {
return {}
}
const {
isCodeCoverageEnabled,
isSuitesSkippingEnabled,
isItrEnabled,
requireGit,
isEarlyFlakeDetectionEnabled
} = remoteConfiguration
return {
isCodeCoverageEnabled,
isSuitesSkippingEnabled,
isItrEnabled,
requireGit,
isEarlyFlakeDetectionEnabled: isEarlyFlakeDetectionEnabled && this._config.isEarlyFlakeDetectionEnabled
}
}

sendGitMetadata (repositoryUrl) {
if (!this._config.isGitUploadEnabled) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,14 @@ function getLibraryConfiguration ({
}
} = JSON.parse(res)

const settings = { isCodeCoverageEnabled, isSuitesSkippingEnabled, isItrEnabled, requireGit }
const settings = {
isCodeCoverageEnabled,
isSuitesSkippingEnabled,
isItrEnabled,
requireGit,
// TODO: change to backend response
isEarlyFlakeDetectionEnabled: false
}

log.debug(() => `Remote settings: ${JSON.stringify(settings)}`)

Expand Down
6 changes: 6 additions & 0 deletions packages/dd-trace/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ class Config {
false
)

const DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED = coalesce(
process.env.DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED,
true
)

const DD_TRACE_MEMCACHED_COMMAND_ENABLED = coalesce(
process.env.DD_TRACE_MEMCACHED_COMMAND_ENABLED,
false
Expand Down Expand Up @@ -666,6 +671,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)

this.gitMetadataEnabled = isTrue(DD_TRACE_GIT_METADATA_ENABLED)
this.isManualApiEnabled = this.isCiVisibility && isTrue(DD_CIVISIBILITY_MANUAL_API_ENABLED)
this.isEarlyFlakeDetectionEnabled = this.isCiVisibility && isTrue(DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED)

this.openaiSpanCharLimit = DD_OPENAI_SPAN_CHAR_LIMIT

Expand Down
12 changes: 12 additions & 0 deletions packages/dd-trace/src/plugins/ci_plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ module.exports = class CiPlugin extends Plugin {
})
this.telemetry.count(TELEMETRY_ITR_SKIPPED, { testLevel: 'suite' }, skippedSuites.length)
})

this.addSub(`ci:${this.constructor.id}:known-tests`, ({ onDone }) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocking, but it's a bit difficult to know why this is needed without any publisher. Generally speaking it's a pattern that should be avoided, so just pointing it out to make sure other options were evaluated.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a good point. The changes are considerable so I split the PRs, leaving this is as a noop, which isn't great. The usage will become clear very soon though, as I have the changes ready :)

if (!this.tracer._exporter?.getKnownTests) {
return onDone({ err: new Error('CI Visibility was not initialized correctly') })
}
this.tracer._exporter.getKnownTests(this.testConfiguration, (err, knownTests) => {
if (err) {
log.error(`Known tests could not be fetched. ${err.message}`)
}
onDone({ err, knownTests })
})
})
}

get telemetry () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ require('../../../../dd-trace/test/setup/tap')

const cp = require('child_process')
const fs = require('fs')
const zlib = require('zlib')

const CiVisibilityExporter = require('../../../src/ci-visibility/exporters/ci-visibility-exporter')
const nock = require('nock')
Expand Down Expand Up @@ -169,7 +170,8 @@ describe('CI Visibility Exporter', () => {
requireGit: false,
isCodeCoverageEnabled: true,
isItrEnabled: true,
isSuitesSkippingEnabled: true
isSuitesSkippingEnabled: true,
isEarlyFlakeDetectionEnabled: false
})
expect(err).not.to.exist
expect(scope.isDone()).to.be.true
Expand Down Expand Up @@ -545,4 +547,136 @@ describe('CI Visibility Exporter', () => {
})
})
})

describe('getKnownTests', () => {
context('if early flake detection is disabled', () => {
it('should resolve immediately to undefined', (done) => {
const scope = nock(`http://localhost:${port}`)
.post('/api/v2/ci/libraries/tests')
.reply(200)

const ciVisibilityExporter = new CiVisibilityExporter({ port, isEarlyFlakeDetectionEnabled: false })

ciVisibilityExporter._resolveCanUseCiVisProtocol(true)

ciVisibilityExporter.getKnownTests({}, (err, knownTests) => {
expect(err).to.be.null
expect(knownTests).to.eql(undefined)
expect(scope.isDone()).not.to.be.true
done()
})
})
})
context('if early flake detection is enabled but can not use CI Visibility protocol', () => {
it('should raise an error', (done) => {
const scope = nock(`http://localhost:${port}`)
.post('/api/v2/ci/libraries/tests')
.reply(200)

const ciVisibilityExporter = new CiVisibilityExporter({ port, isEarlyFlakeDetectionEnabled: true })

ciVisibilityExporter._resolveCanUseCiVisProtocol(false)
ciVisibilityExporter._libraryConfig = { isEarlyFlakeDetectionEnabled: true }
ciVisibilityExporter.getKnownTests({}, (err) => {
expect(err.message).to.include(
'Known tests can not be requested because CI Visibility protocol can not be used'
)
expect(scope.isDone()).not.to.be.true
done()
})
})
})
context('if early flake detection is enabled and can use CI Vis Protocol', () => {
it('should request known tests', (done) => {
const scope = nock(`http://localhost:${port}`)
.post('/api/v2/ci/libraries/tests')
.reply(200, JSON.stringify({
data: {
attributes: {
test_full_names: ['suite1.test1', 'suite2.test2']
}
}
}))

const ciVisibilityExporter = new CiVisibilityExporter({ port, isEarlyFlakeDetectionEnabled: true })

ciVisibilityExporter._resolveCanUseCiVisProtocol(true)
ciVisibilityExporter._libraryConfig = { isEarlyFlakeDetectionEnabled: true }
ciVisibilityExporter.getKnownTests({}, (err, knownTests) => {
expect(err).to.be.null
expect(knownTests).to.eql(['suite1.test1', 'suite2.test2'])
expect(scope.isDone()).to.be.true
done()
})
})
it('should return an error if the request fails', (done) => {
const scope = nock(`http://localhost:${port}`)
.post('/api/v2/ci/libraries/tests')
.reply(500)
const ciVisibilityExporter = new CiVisibilityExporter({ port, isEarlyFlakeDetectionEnabled: true })

ciVisibilityExporter._resolveCanUseCiVisProtocol(true)
ciVisibilityExporter._libraryConfig = { isEarlyFlakeDetectionEnabled: true }
ciVisibilityExporter.getKnownTests({}, (err) => {
expect(err).not.to.be.null
expect(scope.isDone()).to.be.true
done()
})
})
it('should accept gzip if the exporter is gzip compatible', (done) => {
let requestHeaders = {}
const scope = nock(`http://localhost:${port}`)
.post('/api/v2/ci/libraries/tests')
.reply(200, function () {
requestHeaders = this.req.headers

return zlib.gzipSync(JSON.stringify({
data: { attributes: { test_full_names: ['suite1.test1', 'suite2.test2'] } }
}))
}, {
'content-encoding': 'gzip'
})

const ciVisibilityExporter = new CiVisibilityExporter({ port, isEarlyFlakeDetectionEnabled: true })

ciVisibilityExporter._resolveCanUseCiVisProtocol(true)
ciVisibilityExporter._libraryConfig = { isEarlyFlakeDetectionEnabled: true }
ciVisibilityExporter._isGzipCompatible = true
ciVisibilityExporter.getKnownTests({}, (err, knownTests) => {
expect(err).to.be.null
expect(knownTests).to.eql(['suite1.test1', 'suite2.test2'])
expect(scope.isDone()).to.be.true
expect(requestHeaders['accept-encoding']).to.equal('gzip')
done()
})
})
it('should not accept gzip if the exporter is gzip compatible', (done) => {
let requestHeaders = {}
const scope = nock(`http://localhost:${port}`)
.post('/api/v2/ci/libraries/tests')
.reply(200, function () {
requestHeaders = this.req.headers

return JSON.stringify({
data: { attributes: { test_full_names: ['suite1.test1', 'suite2.test2'] } }
})
})

const ciVisibilityExporter = new CiVisibilityExporter({ port, isEarlyFlakeDetectionEnabled: true })

ciVisibilityExporter._resolveCanUseCiVisProtocol(true)
ciVisibilityExporter._libraryConfig = { isEarlyFlakeDetectionEnabled: true }

ciVisibilityExporter._isGzipCompatible = false

ciVisibilityExporter.getKnownTests({}, (err, knownTests) => {
expect(err).to.be.null
expect(knownTests).to.eql(['suite1.test1', 'suite2.test2'])
expect(scope.isDone()).to.be.true
expect(requestHeaders['accept-encoding']).not.to.equal('gzip')
done()
})
})
})
})
})
Loading
Loading