Skip to content

Commit 2395811

Browse files
committed
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.
1 parent 54c1fd2 commit 2395811

2 files changed

Lines changed: 282 additions & 2 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
'use strict'
2+
3+
const assert = require('node:assert/strict')
4+
5+
const { describe, it } = require('mocha')
6+
const { coerce, major } = require('semver')
7+
8+
const { getVersionList, resolvePluginVersions } = require('./versions')
9+
10+
const latests = require('./versions/package.json').dependencies
11+
12+
const keys = (name, versions, nonConsecutive) =>
13+
getVersionList(name, versions, nonConsecutive).map(({ versionKey }) => versionKey)
14+
15+
const latestMajorKey = name => String(major(coerce(latests[name])))
16+
17+
describe('getVersionList', () => {
18+
it('collapses the wildcard to the latest major', () => {
19+
assert.deepEqual(keys('mongodb', ['*']), [latestMajorKey('mongodb')])
20+
})
21+
22+
it('collapses equivalent exact-version notations to a single key', () => {
23+
assert.deepEqual(keys('mongodb', ['1.2.3', '=1.2.3', 'v1.2.3']), ['1.2.3'])
24+
})
25+
26+
it('pins the floor and the major for a single-major range', () => {
27+
// `<3` caps the range to major 2, so only the pinned floor and the latest of major 2 are keys.
28+
assert.deepEqual(keys('mongodb', ['>=2 <3']), ['2.0.0', '2'])
29+
})
30+
31+
it('covers the floor major and every major up to the range top', () => {
32+
// `<5` caps the top at major 4; the floor (2.0.0) is pinned and majors 2-4 each resolve to their latest.
33+
assert.deepEqual(keys('mongodb', ['>=2 <5']), ['2.0.0', '2', '3', '4'])
34+
})
35+
36+
it('covers every major from the floor to the capped top', () => {
37+
// `<6` caps the top at major 5; the floor major's latest (1) is covered too, not only the newest of the range.
38+
assert.deepEqual(keys('mongodb', ['>=1 <6']), ['1.0.0', '1', '2', '3', '4', '5'])
39+
})
40+
41+
it('de-duplicates a shared floor across multiple ranges', () => {
42+
assert.deepEqual(keys('mongodb', ['>=2 <3', '^2.0.0']), ['2.0.0', '2'])
43+
})
44+
45+
it('adds only the floor and the top major for non-consecutive packages', () => {
46+
assert.deepEqual(keys('mongodb', ['>=1 <6'], new Set(['mongodb'])), ['1.0.0', '5'])
47+
})
48+
49+
it('treats the built-in non-consecutive packages as floor + top major', () => {
50+
// graphql jumps from 0.x to 14.x; auto-filling 1.x–13.x would fail the install, so only the newest major is added.
51+
assert.deepEqual(keys('graphql', ['>=0.10']), ['0.10.0', latestMajorKey('graphql')])
52+
})
53+
54+
it('throws on an unparseable range', () => {
55+
assert.throws(() => getVersionList('mongodb', ['not-a-version']), /Invalid version range/)
56+
})
57+
58+
it('throws on an empty entry', () => {
59+
assert.throws(() => getVersionList('mongodb', ['', '>=2 <3']), /Empty version entry/)
60+
})
61+
})
62+
63+
describe('resolvePluginVersions', () => {
64+
const versionKeys = result => result.versionList.map(({ versionKey }) => versionKey)
65+
66+
it('expands the declared versions and points the unversioned folder at the newest in-scope key', () => {
67+
const result = resolvePluginVersions({ name: 'mongodb', declaredVersions: ['>=2 <5'], env: {} })
68+
69+
assert.deepEqual(versionKeys(result), ['2.0.0', '2', '3', '4'])
70+
assert.equal(result.unversioned, '4')
71+
})
72+
73+
it('filters the installed keys by RANGE and follows the filtered tail', () => {
74+
const result = resolvePluginVersions({
75+
name: 'mongodb',
76+
declaredVersions: ['>=1 <6'],
77+
env: { RANGE: '>=2.0.0 <4.0.0' },
78+
})
79+
80+
assert.deepEqual(versionKeys(result), ['2', '3'])
81+
assert.equal(result.unversioned, '3')
82+
})
83+
84+
it('replaces the declared versions with PACKAGE_VERSION_RANGE when the module is honoured', () => {
85+
const result = resolvePluginVersions({
86+
name: 'mongodb',
87+
declaredVersions: ['>=2 <5'],
88+
env: { PACKAGE_VERSION_RANGE: '>=3 <4' },
89+
})
90+
91+
assert.deepEqual(versionKeys(result), ['3.0.0', '3'])
92+
assert.equal(result.unversioned, '>=3 <4')
93+
})
94+
95+
it('ignores PACKAGE_VERSION_RANGE for a sibling external that must not be sharded', () => {
96+
const result = resolvePluginVersions({
97+
name: 'mongodb',
98+
declaredVersions: ['>=2 <5'],
99+
honourEnvRange: false,
100+
env: { PACKAGE_VERSION_RANGE: '>=3 <4' },
101+
})
102+
103+
assert.deepEqual(versionKeys(result), ['2.0.0', '2', '3', '4'])
104+
assert.equal(result.unversioned, '4')
105+
})
106+
107+
it('keeps the unversioned folder on the raw shard while RANGE narrows the installed keys', () => {
108+
const result = resolvePluginVersions({
109+
name: 'mongodb',
110+
declaredVersions: ['>=1 <6'],
111+
env: { PACKAGE_VERSION_RANGE: '>=2 <5', RANGE: '>=3.0.0 <4.0.0' },
112+
})
113+
114+
assert.deepEqual(versionKeys(result), ['3'])
115+
assert.equal(result.unversioned, '>=2 <5')
116+
})
117+
118+
it('reports nothing in scope when no version is declared', () => {
119+
const result = resolvePluginVersions({ name: 'mongodb', declaredVersions: [], env: {} })
120+
121+
assert.deepEqual(result.versionList, [])
122+
assert.equal(result.unversioned, undefined)
123+
})
124+
125+
it('reports nothing in scope when RANGE excludes every declared key', () => {
126+
const result = resolvePluginVersions({
127+
name: 'mongodb',
128+
declaredVersions: ['>=2 <3'],
129+
env: { RANGE: '>=9.0.0 <10.0.0' },
130+
})
131+
132+
assert.deepEqual(result.versionList, [])
133+
assert.equal(result.unversioned, undefined)
134+
})
135+
})

packages/dd-trace/test/plugins/versions/index.js

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
'use strict'
22

3-
const { subset } = require('semver')
3+
const { clean, coerce, intersects, subset } = require('semver')
4+
45
const latests = require('./package.json').dependencies
56

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

9+
// Packages whose published majors are not contiguous. `getVersionList()` must not auto-fill their in-between majors:
10+
// the missing majors were never published, so installing them fails. A failing install of a multi-major range is the
11+
// signal to add a package here (the install script's error points back to this list). New entries must explain the gap
12+
// so the next reader does not "fix" it by removing the entry.
13+
const nonConsecutiveMajorPackages = new Set([
14+
'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
15+
'@redis/client', // jumps from 2.x to 5.x (no 3.x–4.x)
16+
])
17+
818
/**
919
* @param {string} name
1020
* @param {string} range
@@ -40,6 +50,141 @@ function capSubrange (name, subrange) {
4050
return `${subrange} <=${latests[name]}`
4151
}
4252

53+
/**
54+
* Expand a module's declared version entries into the de-duplicated set of version keys to install and test. Each key
55+
* maps to a `versions/<name>@<key>` workspace folder; the install script and `withVersions()` share this so the set of
56+
* installed folders and the set of tested folders never drift apart.
57+
*
58+
* Per declared range this yields the lowest supported version (pinned exactly) and the newest version of every major
59+
* the range spans, keyed by the bare major so the key resolves to that major's latest. Covering each major explicitly
60+
* (rather than emitting the raw range, which resolves only to the newest version of the whole range) makes sure the
61+
* floor major's latest is tested too. The top major is derived from the pinned latest in `package.json` rather than a
62+
* registry lookup, so a major that was never published makes the install fail loudly — the signal to add the package
63+
* to `nonConsecutiveMajorPackages`.
64+
*
65+
* Notations that resolve to the same exact version (`1.2.3`, `=1.2.3`, `v1.2.3`) collapse to a single key, and `*`
66+
* collapses to the latest major (the same version an open-ended range resolves to), so a version is never installed
67+
* twice under different spellings.
68+
*
69+
* @param {string} name The module name, e.g. `mongodb`.
70+
* @param {string[]} versions The declared version entries, e.g. `['>=3.3 <5', '5', '>=6']`.
71+
* @param {Set<string>} [nonConsecutiveMajors] Module names whose majors are not contiguous; injectable for testing.
72+
* @returns {Array<{ versionKey: string, range: string }>} Ordered, de-duplicated entries. `versionKey` is the folder
73+
* suffix; `range` is the declaring entry it came from.
74+
*/
75+
function getVersionList (name, versions, nonConsecutiveMajors = nonConsecutiveMajorPackages) {
76+
/** @type {Map<string, { versionKey: string, range: string }>} */
77+
const entries = new Map()
78+
79+
const add = (versionKey, range) => {
80+
if (!entries.has(versionKey)) entries.set(versionKey, { versionKey, range })
81+
}
82+
83+
for (const range of versions) {
84+
// An empty entry is a setup mistake (a stray comma or an undefined slot); fail loudly rather than skip silently.
85+
if (!range) {
86+
throw new Error(`Empty version entry declared for '${name}'. Each declared version must be a non-empty range.`)
87+
}
88+
89+
if (range === '*') {
90+
add(latestMajor(name), range)
91+
continue
92+
}
93+
94+
// Exact-version notations collapse to one key so the same version is never installed twice.
95+
const exact = clean(range)
96+
if (exact) {
97+
add(exact, range)
98+
continue
99+
}
100+
101+
const floor = coerce(range)
102+
if (!floor) throw new Error(`Invalid version range for '${name}': ${range}`)
103+
104+
add(floor.version, range)
105+
106+
const topMajor = highestMajor(name, range, floor.major)
107+
if (nonConsecutiveMajors.has(name)) {
108+
// The in-between majors were never published; only add the newest in-range major so the install does not fail.
109+
if (topMajor > floor.major) add(String(topMajor), range)
110+
} else {
111+
for (let major = floor.major; major <= topMajor; major++) {
112+
if (intersects(`>=${major}.0.0 <${major + 1}.0.0`, range)) add(String(major), range)
113+
}
114+
}
115+
}
116+
117+
return [...entries.values()]
118+
}
119+
120+
/**
121+
* The latest major of `name` as a bare-major version key. `*` resolves here so it de-duplicates against an open-ended
122+
* range whose top resolves to the same newest version.
123+
*
124+
* @param {string} name
125+
* @returns {string}
126+
*/
127+
function latestMajor (name) {
128+
const latest = coerce(latests[name])
129+
if (!latest) {
130+
throw new Error(
131+
`Latest version for '${name}' needs to be defined in 'packages/dd-trace/test/plugins/versions/package.json'.`
132+
)
133+
}
134+
return String(latest.major)
135+
}
136+
137+
/**
138+
* Highest major still spanned by `range`, capped at the pinned latest. Iterates down from the latest so the first
139+
* intersecting major is the top; nothing above the pinned latest is installed.
140+
*
141+
* @param {string} name
142+
* @param {string} range
143+
* @param {number} floorMajor
144+
* @returns {number}
145+
*/
146+
function highestMajor (name, range, floorMajor) {
147+
const latest = coerce(latests[name])
148+
if (!latest) return floorMajor
149+
for (let major = latest.major; major > floorMajor; major--) {
150+
if (intersects(`>=${major}.0.0 <${major + 1}.0.0`, range)) return major
151+
}
152+
return floorMajor
153+
}
154+
155+
/**
156+
* Resolve which version keys to install and test for a module, plus which key the unversioned
157+
* `versions/<name>` folder points at. `scripts/install_plugin_modules.js` and `withVersions()` both call this so the
158+
* installed folder set and the tested folder set are derived from one place and cannot drift.
159+
*
160+
* @param {object} options
161+
* @param {string} options.name The module name, e.g. `fastify`.
162+
* @param {string[]} options.declaredVersions The declared version entries to expand.
163+
* @param {boolean} [options.honourEnvRange] Whether `PACKAGE_VERSION_RANGE` applies to this module. False for sibling
164+
* externals that must stay on their declared versions while the matrix shards a different package.
165+
* @param {NodeJS.ProcessEnv} [options.env] Injectable for testing.
166+
* @returns {{ versionList: Array<{ versionKey: string, range: string }>, unversioned: string|undefined }} The ordered,
167+
* `RANGE`-filtered key set, and the key the default `versions/<name>` folder resolves to (the newest in-scope entry,
168+
* or `undefined` when nothing is in scope).
169+
*/
170+
function resolvePluginVersions ({ name, declaredVersions, honourEnvRange = true, env = process.env }) {
171+
const useEnvRange = Boolean(env.PACKAGE_VERSION_RANGE) && honourEnvRange
172+
const versions = useEnvRange ? [env.PACKAGE_VERSION_RANGE] : declaredVersions
173+
174+
let versionList = getVersionList(name, versions)
175+
if (env.RANGE) {
176+
versionList = versionList.filter(({ versionKey }) => subset(versionKey, env.RANGE))
177+
}
178+
179+
// With `PACKAGE_VERSION_RANGE` the shard itself is the target, so the unversioned folder keeps the raw range even
180+
// when `RANGE` narrows the installed keys; otherwise it follows the newest in-scope key.
181+
const unversioned = useEnvRange ? env.PACKAGE_VERSION_RANGE : versionList.at(-1)?.versionKey
182+
183+
return { versionList, unversioned }
184+
}
185+
43186
module.exports = {
44-
getCappedRange
187+
getCappedRange,
188+
getVersionList,
189+
resolvePluginVersions,
45190
}

0 commit comments

Comments
 (0)