dd-trace
Version:
Datadog APM tracing client for JavaScript
284 lines (263 loc) • 7.94 kB
JavaScript
const getConfig = require('../../config')
const request = require('../requests/request')
const log = require('../../log')
const {
incrementCountMetric,
distributionMetric,
TELEMETRY_ITR_SKIPPABLE_TESTS,
TELEMETRY_ITR_SKIPPABLE_TESTS_MS,
TELEMETRY_ITR_SKIPPABLE_TESTS_ERRORS,
TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_SUITES,
TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS,
TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES,
} = require('../../ci-visibility/telemetry')
const { buildCacheKey, writeToCache, withCache } = require('../requests/fs-cache')
const { validateSkippableTestsResponse } = require('../test-optimization-http-cache-schema')
function parseJsonResponse (rawJson) {
return typeof rawJson === 'string' ? JSON.parse(rawJson) : rawJson
}
function parseSkippableSuitesResponse (
rawJson,
{
testLevel = 'suite',
isCoverageReportUploadEnabled = false,
validateRequiredFields = false,
validationMode = false,
} = {}
) {
const parsedResponse = parseJsonResponse(rawJson)
if (validateRequiredFields) {
validateSkippableTestsResponse(parsedResponse, { validationMode })
}
const coverage = {}
for (const [filename, bitmap] of Object.entries(parsedResponse.meta?.coverage || {})) {
coverage[filename.replaceAll('\\', '/')] = bitmap
}
const skippableItems = parsedResponse
.data
.filter(({ type }) => type === testLevel)
const skippableSuites = []
let numExcludedByMissingLineCoverage = 0
for (const {
attributes: {
suite,
name,
_is_missing_line_code_coverage: isMissingLineCodeCoverage,
},
} of skippableItems) {
// Only reject candidates without backend line coverage when we need that coverage to backfill reports.
if (isCoverageReportUploadEnabled && isMissingLineCodeCoverage) {
numExcludedByMissingLineCoverage++
continue
}
skippableSuites.push(testLevel === 'suite' ? suite : { suite, name })
}
const correlationId = parsedResponse.meta?.correlation_id
return {
skippableSuites,
correlationId,
coverage,
numReceivedSkippableItems: skippableItems.length,
numExcludedByMissingLineCoverage,
}
}
/**
* Logs how missing line coverage affected the skippable candidates without logging their contents.
*
* @param {{
* skippableSuites: Array<string|{suite: string, name: string}>,
* numReceivedSkippableItems?: number,
* numExcludedByMissingLineCoverage?: number
* }} result - Parsed skippable response.
* @param {'suite'|'test'} testLevel - Test optimization skipping level.
* @returns {void}
*/
function logSkippableSuitesResponse (result, testLevel) {
if (result.numReceivedSkippableItems === undefined) {
log.debug('Number of received skippable %ss: %d', testLevel, result.skippableSuites.length)
return
}
log.debug(
'Received %d skippable %s candidates; excluded %d because line coverage is missing; %d remain.',
result.numReceivedSkippableItems,
testLevel,
result.numExcludedByMissingLineCoverage,
result.skippableSuites.length
)
if (
result.numReceivedSkippableItems > 0 &&
result.numExcludedByMissingLineCoverage === result.numReceivedSkippableItems
) {
log.warn(
'All %d skippable %s candidates were excluded: coverage upload is enabled but line coverage is missing.',
result.numReceivedSkippableItems,
testLevel
)
}
}
function getSkippableSuites ({
url,
isEvpProxy,
evpProxyPrefix,
isGzipCompatible,
env,
service,
repositoryUrl,
sha,
osVersion,
osPlatform,
osArchitecture,
runtimeName,
runtimeVersion,
custom,
testLevel = 'suite',
isCoverageReportUploadEnabled = false,
}, done) {
const cacheKey = buildCacheKey('skippable', [
sha, service, env, repositoryUrl, osPlatform, osVersion, osArchitecture,
runtimeName, runtimeVersion, testLevel, custom, isCoverageReportUploadEnabled,
])
withCache(cacheKey, (activeCacheKey, cb) => {
fetchFromApi({
url,
isEvpProxy,
evpProxyPrefix,
isGzipCompatible,
env,
service,
repositoryUrl,
sha,
osVersion,
osPlatform,
osArchitecture,
runtimeName,
runtimeVersion,
custom,
testLevel,
isCoverageReportUploadEnabled,
cacheKey: activeCacheKey,
}, cb)
}, (err, data) => {
if (err) return done(err)
logSkippableSuitesResponse(data, testLevel)
done(null, data.skippableSuites, data.correlationId, data.coverage)
})
}
/**
* Fetches skippable suites from the API and writes the result to cache on success.
*
* @param {object} params
* @param {string} params.url
* @param {boolean} params.isEvpProxy
* @param {string} params.evpProxyPrefix
* @param {boolean} params.isGzipCompatible
* @param {string} params.env
* @param {string} params.service
* @param {string} params.repositoryUrl
* @param {string} params.sha
* @param {string} params.osVersion
* @param {string} params.osPlatform
* @param {string} params.osArchitecture
* @param {string} params.runtimeName
* @param {string} params.runtimeVersion
* @param {object} [params.custom]
* @param {string} [params.testLevel]
* @param {boolean} [params.isCoverageReportUploadEnabled]
* @param {string | null} params.cacheKey
* @param {Function} done
*/
function fetchFromApi ({
url,
isEvpProxy,
evpProxyPrefix,
isGzipCompatible,
env,
service,
repositoryUrl,
sha,
osVersion,
osPlatform,
osArchitecture,
runtimeName,
runtimeVersion,
custom,
testLevel,
isCoverageReportUploadEnabled,
cacheKey,
}, done) {
const options = {
path: '/api/v2/ci/tests/skippable',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
timeout: 20_000,
url,
}
if (isGzipCompatible) {
options.headers['accept-encoding'] = 'gzip'
}
if (isEvpProxy) {
options.path = `${evpProxyPrefix}/api/v2/ci/tests/skippable`
options.headers['X-Datadog-EVP-Subdomain'] = 'api'
} else {
const { DD_API_KEY } = getConfig()
if (!DD_API_KEY) {
return done(new Error('Skippable suites were not fetched because Datadog API key is not defined.'))
}
options.headers['dd-api-key'] = DD_API_KEY
}
const data = JSON.stringify({
data: {
type: 'test_params',
attributes: {
test_level: testLevel,
configurations: {
'os.platform': osPlatform,
'os.version': osVersion,
'os.architecture': osArchitecture,
'runtime.name': runtimeName,
'runtime.version': runtimeVersion,
custom,
},
service,
env,
repository_url: repositoryUrl,
sha,
},
},
})
incrementCountMetric(TELEMETRY_ITR_SKIPPABLE_TESTS)
const startTime = Date.now()
request(data, options, (err, res, statusCode) => {
distributionMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_MS, {}, Date.now() - startTime)
if (err) {
incrementCountMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_ERRORS, { statusCode })
done(err)
} else {
try {
const parsedResponse = parseJsonResponse(res)
const result = parseSkippableSuitesResponse(parsedResponse, {
testLevel,
isCoverageReportUploadEnabled,
validateRequiredFields: true,
})
const skippableItems = parsedResponse.data.filter(({ type }) => type === testLevel)
incrementCountMetric(
testLevel === 'test'
? TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_TESTS
: TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_SUITES,
{},
skippableItems.length
)
distributionMetric(TELEMETRY_ITR_SKIPPABLE_TESTS_RESPONSE_BYTES, {}, res.length)
writeToCache(cacheKey, result)
done(null, result)
} catch (err) {
done(err)
}
}
})
}
module.exports = { getSkippableSuites, logSkippableSuitesResponse, parseSkippableSuitesResponse }