From 53bd8bc1cdde8c33dd08e1d09fa4501d4e83101b Mon Sep 17 00:00:00 2001 From: Ruben Bridgewater Date: Mon, 22 Jun 2026 21:08:41 +0200 Subject: [PATCH] test: add shared plugin version resolver withVersions() and the install script each expand a module's declared versions into the set of folders to test/install, and the two computations drifted, so a folder could be installed but never tested (or the reverse). This adds a single resolvePluginVersions/getVersionList that both consume: per declared range it pins the lowest supported version exactly and adds the newest of every major the range spans, keyed by the bare major. Covering each major (not the raw range, which only resolves to the newest of the whole range) makes sure the floor major's latest is tested too. `*` collapses to the latest major, and an empty or unparseable entry throws instead of being skipped. --- .../dd-trace/test/plugins/versions.spec.js | 142 ++++++++++++++++ .../dd-trace/test/plugins/versions/index.js | 158 +++++++++++++++++- 2 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 packages/dd-trace/test/plugins/versions.spec.js diff --git a/packages/dd-trace/test/plugins/versions.spec.js b/packages/dd-trace/test/plugins/versions.spec.js new file mode 100644 index 0000000000..ea35ea1da4 --- /dev/null +++ b/packages/dd-trace/test/plugins/versions.spec.js @@ -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', () => { + // `>=` 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) + }) +}) diff --git a/packages/dd-trace/test/plugins/versions/index.js b/packages/dd-trace/test/plugins/versions/index.js index 5a249c76ed..b856fd5c41 100644 --- a/packages/dd-trace/test/plugins/versions/index.js +++ b/packages/dd-trace/test/plugins/versions/index.js @@ -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 @@ -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/@` 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} [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} */ + 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) + } + } + } + + // 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/` 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/` 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, }