Skip to content

Commit

Permalink
[ci-visibility] Add get known tests request (#4015)
Browse files Browse the repository at this point in the history
  • Loading branch information
juan-fernandez authored and tlhunter committed Feb 12, 2024
1 parent 165b340 commit d28f33e
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 5 deletions.
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 @@ -116,6 +124,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 @@ -145,7 +177,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 @@ -156,8 +188,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 @@ -167,6 +199,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 }) => {
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 @@ -170,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 @@ -644,4 +645,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 incompatible', (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

0 comments on commit d28f33e

Please sign in to comment.