Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
142 changes: 142 additions & 0 deletions packages/dd-trace/test/plugins/versions.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
'use strict'

const assert = require('node:assert/strict')

const { describe, it } = require('mocha')
const { coerce, major } = require('semver')

const { getVersionList, resolvePluginVersions } = require('./versions')

const latests = require('./versions/package.json').dependencies

const keys = (name, versions, nonConsecutive) =>
getVersionList(name, versions, nonConsecutive).map(({ versionKey }) => versionKey)

const latestMajorKey = name => String(major(coerce(latests[name])))

describe('getVersionList', () => {
it('collapses the wildcard to the latest major', () => {
assert.deepEqual(keys('mongodb', ['*']), [latestMajorKey('mongodb')])
})

it('collapses equivalent exact-version notations to a single key', () => {
assert.deepEqual(keys('mongodb', ['1.2.3', '=1.2.3', 'v1.2.3']), ['1.2.3'])
})

it('pins the floor and the major for a single-major range', () => {
// `<3` caps the range to major 2, so only the pinned floor and the latest of major 2 are keys.
assert.deepEqual(keys('mongodb', ['>=2 <3']), ['2.0.0', '2'])
})

it('covers the floor major and every major up to the range top', () => {
// `<5` caps the top at major 4; the floor (2.0.0) is pinned and majors 2-4 each resolve to their latest.
assert.deepEqual(keys('mongodb', ['>=2 <5']), ['2.0.0', '2', '3', '4'])
})

it('covers every major from the floor to the capped top', () => {
// `<6` caps the top at major 5; the floor major's latest (1) is covered too, not only the newest of the range.
assert.deepEqual(keys('mongodb', ['>=1 <6']), ['1.0.0', '1', '2', '3', '4', '5'])
})

it('de-duplicates a shared floor across multiple ranges', () => {
assert.deepEqual(keys('mongodb', ['>=2 <3', '^2.0.0']), ['2.0.0', '2'])
})

it('consolidates the floor with the top major when the floor is the pinned latest', () => {
// `>=<pinned latest>` floors at the newest version, so the bare top-major key resolves to that same version and is
// dropped; the pinned package.json is what proves the two keys identical.
assert.deepEqual(keys('mongodb', [`>=${latests.mongodb}`]), [latests.mongodb])
})

it('adds the floor, the floor major and the top major for non-consecutive packages', () => {
// The middle majors may be unpublished, but the floor major (1) and the top major (5) both exist and are tested.
assert.deepEqual(keys('mongodb', ['>=1 <6'], new Set(['mongodb'])), ['1.0.0', '1', '5'])
})

it('treats the built-in non-consecutive packages as floor + floor major + top major', () => {
// graphql jumps from 0.x to 14.x; 1.x–13.x are skipped, but the latest 0.x and the newest major are still tested.
assert.deepEqual(keys('graphql', ['>=0.10']), ['0.10.0', '0', latestMajorKey('graphql')])
})

it('throws on an unparseable range', () => {
assert.throws(() => getVersionList('mongodb', ['not-a-version']), /Invalid version range/)
})

it('throws on an empty entry', () => {
assert.throws(() => getVersionList('mongodb', ['', '>=2 <3']), /Empty version entry/)
})
})

describe('resolvePluginVersions', () => {
const versionKeys = result => result.versionList.map(({ versionKey }) => versionKey)

it('expands the declared versions and points the unversioned folder at the newest in-scope key', () => {
const result = resolvePluginVersions({ name: 'mongodb', declaredVersions: ['>=2 <5'], env: {} })

assert.deepEqual(versionKeys(result), ['2.0.0', '2', '3', '4'])
assert.equal(result.unversioned, '4')
})

it('filters the installed keys by RANGE and follows the filtered tail', () => {
const result = resolvePluginVersions({
name: 'mongodb',
declaredVersions: ['>=1 <6'],
env: { RANGE: '>=2.0.0 <4.0.0' },
})

assert.deepEqual(versionKeys(result), ['2', '3'])
assert.equal(result.unversioned, '3')
})

it('replaces the declared versions with PACKAGE_VERSION_RANGE when the module is honoured', () => {
const result = resolvePluginVersions({
name: 'mongodb',
declaredVersions: ['>=2 <5'],
env: { PACKAGE_VERSION_RANGE: '>=3 <4' },
})

assert.deepEqual(versionKeys(result), ['3.0.0', '3'])
assert.equal(result.unversioned, '>=3 <4')
})

it('ignores PACKAGE_VERSION_RANGE for a sibling external that must not be sharded', () => {
const result = resolvePluginVersions({
name: 'mongodb',
declaredVersions: ['>=2 <5'],
honourEnvRange: false,
env: { PACKAGE_VERSION_RANGE: '>=3 <4' },
})

assert.deepEqual(versionKeys(result), ['2.0.0', '2', '3', '4'])
assert.equal(result.unversioned, '4')
})

it('keeps the unversioned folder on the raw shard while RANGE narrows the installed keys', () => {
const result = resolvePluginVersions({
name: 'mongodb',
declaredVersions: ['>=1 <6'],
env: { PACKAGE_VERSION_RANGE: '>=2 <5', RANGE: '>=3.0.0 <4.0.0' },
})

assert.deepEqual(versionKeys(result), ['3'])
assert.equal(result.unversioned, '>=2 <5')
})

it('reports nothing in scope when no version is declared', () => {
const result = resolvePluginVersions({ name: 'mongodb', declaredVersions: [], env: {} })

assert.deepEqual(result.versionList, [])
assert.equal(result.unversioned, undefined)
})

it('reports nothing in scope when RANGE excludes every declared key', () => {
const result = resolvePluginVersions({
name: 'mongodb',
declaredVersions: ['>=2 <3'],
env: { RANGE: '>=9.0.0 <10.0.0' },
})

assert.deepEqual(result.versionList, [])
assert.equal(result.unversioned, undefined)
})
})
158 changes: 156 additions & 2 deletions packages/dd-trace/test/plugins/versions/index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
'use strict'

const { subset } = require('semver')
const { clean, coerce, intersects, subset } = require('semver')

const latests = require('./package.json').dependencies

const exactVersionExp = /^=?\d+\.\d+\.\d+/

// Packages whose published majors are not contiguous. `getVersionList()` must not auto-fill their in-between majors:
// the missing majors were never published, so installing them fails. A failing install of a multi-major range is the
// signal to add a package here (the install script's error points back to this list). New entries must explain the gap
// so the next reader does not "fix" it by removing the entry.
const nonConsecutiveMajorPackages = new Set([
'graphql', // jumps from 0.x straight to 14.x (no 1.x–13.x); 14.x–17.x are covered via the apollo externals entries
'@redis/client', // jumps from 2.x to 5.x (no 3.x–4.x)
])

/**
* @param {string} name
* @param {string} range
Expand Down Expand Up @@ -40,6 +50,150 @@ function capSubrange (name, subrange) {
return `${subrange} <=${latests[name]}`
}

/**
* Expand a module's declared version entries into the de-duplicated set of version keys to install and test. Each key
* maps to a `versions/<name>@<key>` workspace folder; the install script and `withVersions()` share this so the set of
* installed folders and the set of tested folders never drift apart.
*
* Per declared range this yields the lowest supported version (pinned exactly) and the newest version of every major
* the range spans, keyed by the bare major so the key resolves to that major's latest. Covering each major explicitly
* (rather than emitting the raw range, which resolves only to the newest version of the whole range) makes sure the
* floor major's latest is tested too. The top major is derived from the pinned latest in `package.json` rather than a
* registry lookup, so a major that was never published makes the install fail loudly — the signal to add the package
* to `nonConsecutiveMajorPackages`.
*
* Notations that resolve to the same exact version (`1.2.3`, `=1.2.3`, `v1.2.3`) collapse to a single key, and `*`
* collapses to the latest major (the same version an open-ended range resolves to), so a version is never installed
* twice under different spellings. A floor that equals the pinned latest also drops the redundant top-major key,
* since the pinned `package.json` proves they resolve to the same version.
*
* @param {string} name The module name, e.g. `mongodb`.
* @param {string[]} versions The declared version entries, e.g. `['>=3.3 <5', '5', '>=6']`.
* @param {Set<string>} [nonConsecutiveMajors] Module names whose majors are not contiguous; injectable for testing.
* @returns {Array<{ versionKey: string, range: string }>} Ordered, de-duplicated entries. `versionKey` is the folder
* suffix; `range` is the declaring entry it came from.
*/
function getVersionList (name, versions, nonConsecutiveMajors = nonConsecutiveMajorPackages) {
/** @type {Map<string, { versionKey: string, range: string }>} */
const entries = new Map()

const add = (versionKey, range) => {
if (!entries.has(versionKey)) entries.set(versionKey, { versionKey, range })
}

for (const range of versions) {
// An empty entry is a setup mistake (a stray comma or an undefined slot); fail loudly rather than skip silently.
if (!range) {
throw new Error(`Empty version entry declared for '${name}'. Each declared version must be a non-empty range.`)
}

if (range === '*') {
add(latestMajor(name), range)
continue
}

// Exact-version notations collapse to one key so the same version is never installed twice.
const exact = clean(range)
if (exact) {
add(exact, range)
continue
}

const floor = coerce(range)
if (!floor) throw new Error(`Invalid version range for '${name}': ${range}`)

add(floor.version, range)

const topMajor = highestMajor(name, range, floor.major)
if (nonConsecutiveMajors.has(name)) {
// Only the in-between majors were never published. The floor major and the top major both exist, so add the
// latest of each (skipping the uncertain middle, which is what would make the install fail).
add(String(floor.major), range)
if (topMajor > floor.major) add(String(topMajor), range)
} else {
for (let major = floor.major; major <= topMajor; major++) {
if (intersects(`>=${major}.0.0 <${major + 1}.0.0`, range)) add(String(major), range)
Comment on lines +114 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep top-major keys inside bounded ranges

For declared ranges whose upper bound falls inside a major, this loop still emits the bare major key for that top major. For example, the existing pino hook range >=5 <6.8.0 in packages/datadog-instrumentations/src/pino.js would produce pino@6, which resolves to the latest 6.x and can be >=6.8.0, so the install/test set no longer covers the newest version below 6.8 and instead points at a version belonging to the next hook range. The same pattern exists for other sub-major ranges such as grpc and express, so the resolver needs to preserve a bounded range key (or otherwise cap the dependency) when the major is only partially included.

Useful? React with 👍 / 👎.

}
}
}

// The bare-major key for the pinned latest's major resolves to the pin itself, so when a declared floor already pins
// that exact version the major key would install the same thing. Drop it. This is the only such redundancy the
// pinned upper bound lets us prove; for lower majors the newest published version is unknown without the registry.
const pinned = coerce(latests[name])
if (pinned && entries.has(pinned.version)) entries.delete(String(pinned.major))

return [...entries.values()]
}

/**
* The latest major of `name` as a bare-major version key. `*` resolves here so it de-duplicates against an open-ended
* range whose top resolves to the same newest version.
*
* @param {string} name
* @returns {string}
*/
function latestMajor (name) {
const latest = coerce(latests[name])
if (!latest) {
throw new Error(
`Latest version for '${name}' needs to be defined in 'packages/dd-trace/test/plugins/versions/package.json'.`
)
}
return String(latest.major)
}

/**
* Highest major still spanned by `range`, capped at the pinned latest. Iterates down from the latest so the first
* intersecting major is the top; nothing above the pinned latest is installed.
*
* @param {string} name
* @param {string} range
* @param {number} floorMajor
* @returns {number}
*/
function highestMajor (name, range, floorMajor) {
const latest = coerce(latests[name])
if (!latest) return floorMajor
for (let major = latest.major; major > floorMajor; major--) {
if (intersects(`>=${major}.0.0 <${major + 1}.0.0`, range)) return major
}
return floorMajor
}

/**
* Resolve which version keys to install and test for a module, plus which key the unversioned
* `versions/<name>` folder points at. `scripts/install_plugin_modules.js` and `withVersions()` both call this so the
* installed folder set and the tested folder set are derived from one place and cannot drift.
*
* @param {object} options
* @param {string} options.name The module name, e.g. `fastify`.
* @param {string[]} options.declaredVersions The declared version entries to expand.
* @param {boolean} [options.honourEnvRange] Whether `PACKAGE_VERSION_RANGE` applies to this module. False for sibling
* externals that must stay on their declared versions while the matrix shards a different package.
* @param {NodeJS.ProcessEnv} [options.env] Injectable for testing.
* @returns {{ versionList: Array<{ versionKey: string, range: string }>, unversioned: string|undefined }} The ordered,
* `RANGE`-filtered key set, and the key the default `versions/<name>` folder resolves to (the newest in-scope entry,
* or `undefined` when nothing is in scope).
*/
function resolvePluginVersions ({ name, declaredVersions, honourEnvRange = true, env = process.env }) {
const useEnvRange = Boolean(env.PACKAGE_VERSION_RANGE) && honourEnvRange
const versions = useEnvRange ? [env.PACKAGE_VERSION_RANGE] : declaredVersions

let versionList = getVersionList(name, versions)
if (env.RANGE) {
versionList = versionList.filter(({ versionKey }) => subset(versionKey, env.RANGE))
}

// With `PACKAGE_VERSION_RANGE` the shard itself is the target, so the unversioned folder keeps the raw range even
// when `RANGE` narrows the installed keys; otherwise it follows the newest in-scope key.
const unversioned = useEnvRange ? env.PACKAGE_VERSION_RANGE : versionList.at(-1)?.versionKey

return { versionList, unversioned }
}

module.exports = {
getCappedRange
getCappedRange,
getVersionList,
resolvePluginVersions,
}
Loading