Skip to content

Commit 53bd8bc

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 53bd8bc

2 files changed

Lines changed: 298 additions & 2 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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('consolidates the floor with the top major when the floor is the pinned latest', () => {
46+
// `>=<pinned latest>` floors at the newest version, so the bare top-major key resolves to that same version and is
47+
// dropped; the pinned package.json is what proves the two keys identical.
48+
assert.deepEqual(keys('mongodb', [`>=${latests.mongodb}`]), [latests.mongodb])
49+
})
50+
51+
it('adds the floor, the floor major and the top major for non-consecutive packages', () => {
52+
// The middle majors may be unpublished, but the floor major (1) and the top major (5) both exist and are tested.
53+
assert.deepEqual(keys('mongodb', ['>=1 <6'], new Set(['mongodb'])), ['1.0.0', '1', '5'])
54+
})
55+
56+
it('treats the built-in non-consecutive packages as floor + floor major + top major', () => {
57+
// 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.
58+
assert.deepEqual(keys('graphql', ['>=0.10']), ['0.10.0', '0', latestMajorKey('graphql')])
59+
})
60+
61+
it('throws on an unparseable range', () => {
62+
assert.throws(() => getVersionList('mongodb', ['not-a-version']), /Invalid version range/)
63+
})
64+
65+
it('throws on an empty entry', () => {
66+
assert.throws(() => getVersionList('mongodb', ['', '>=2 <3']), /Empty version entry/)
67+
})
68+
})
69+
70+
describe('resolvePluginVersions', () => {
71+
const versionKeys = result => result.versionList.map(({ versionKey }) => versionKey)
72+
73+
it('expands the declared versions and points the unversioned folder at the newest in-scope key', () => {
74+
const result = resolvePluginVersions({ name: 'mongodb', declaredVersions: ['>=2 <5'], env: {} })
75+
76+
assert.deepEqual(versionKeys(result), ['2.0.0', '2', '3', '4'])
77+
assert.equal(result.unversioned, '4')
78+
})
79+
80+
it('filters the installed keys by RANGE and follows the filtered tail', () => {
81+
const result = resolvePluginVersions({
82+
name: 'mongodb',
83+
declaredVersions: ['>=1 <6'],
84+
env: { RANGE: '>=2.0.0 <4.0.0' },
85+
})
86+
87+
assert.deepEqual(versionKeys(result), ['2', '3'])
88+
assert.equal(result.unversioned, '3')
89+
})
90+
91+
it('replaces the declared versions with PACKAGE_VERSION_RANGE when the module is honoured', () => {
92+
const result = resolvePluginVersions({
93+
name: 'mongodb',
94+
declaredVersions: ['>=2 <5'],
95+
env: { PACKAGE_VERSION_RANGE: '>=3 <4' },
96+
})
97+
98+
assert.deepEqual(versionKeys(result), ['3.0.0', '3'])
99+
assert.equal(result.unversioned, '>=3 <4')
100+
})
101+
102+
it('ignores PACKAGE_VERSION_RANGE for a sibling external that must not be sharded', () => {
103+
const result = resolvePluginVersions({
104+
name: 'mongodb',
105+
declaredVersions: ['>=2 <5'],
106+
honourEnvRange: false,
107+
env: { PACKAGE_VERSION_RANGE: '>=3 <4' },
108+
})
109+
110+
assert.deepEqual(versionKeys(result), ['2.0.0', '2', '3', '4'])
111+
assert.equal(result.unversioned, '4')
112+
})
113+
114+
it('keeps the unversioned folder on the raw shard while RANGE narrows the installed keys', () => {
115+
const result = resolvePluginVersions({
116+
name: 'mongodb',
117+
declaredVersions: ['>=1 <6'],
118+
env: { PACKAGE_VERSION_RANGE: '>=2 <5', RANGE: '>=3.0.0 <4.0.0' },
119+
})
120+
121+
assert.deepEqual(versionKeys(result), ['3'])
122+
assert.equal(result.unversioned, '>=2 <5')
123+
})
124+
125+
it('reports nothing in scope when no version is declared', () => {
126+
const result = resolvePluginVersions({ name: 'mongodb', declaredVersions: [], env: {} })
127+
128+
assert.deepEqual(result.versionList, [])
129+
assert.equal(result.unversioned, undefined)
130+
})
131+
132+
it('reports nothing in scope when RANGE excludes every declared key', () => {
133+
const result = resolvePluginVersions({
134+
name: 'mongodb',
135+
declaredVersions: ['>=2 <3'],
136+
env: { RANGE: '>=9.0.0 <10.0.0' },
137+
})
138+
139+
assert.deepEqual(result.versionList, [])
140+
assert.equal(result.unversioned, undefined)
141+
})
142+
})

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

Lines changed: 156 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,150 @@ 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. A floor that equals the pinned latest also drops the redundant top-major key,
68+
* since the pinned `package.json` proves they resolve to the same version.
69+
*
70+
* @param {string} name The module name, e.g. `mongodb`.
71+
* @param {string[]} versions The declared version entries, e.g. `['>=3.3 <5', '5', '>=6']`.
72+
* @param {Set<string>} [nonConsecutiveMajors] Module names whose majors are not contiguous; injectable for testing.
73+
* @returns {Array<{ versionKey: string, range: string }>} Ordered, de-duplicated entries. `versionKey` is the folder
74+
* suffix; `range` is the declaring entry it came from.
75+
*/
76+
function getVersionList (name, versions, nonConsecutiveMajors = nonConsecutiveMajorPackages) {
77+
/** @type {Map<string, { versionKey: string, range: string }>} */
78+
const entries = new Map()
79+
80+
const add = (versionKey, range) => {
81+
if (!entries.has(versionKey)) entries.set(versionKey, { versionKey, range })
82+
}
83+
84+
for (const range of versions) {
85+
// An empty entry is a setup mistake (a stray comma or an undefined slot); fail loudly rather than skip silently.
86+
if (!range) {
87+
throw new Error(`Empty version entry declared for '${name}'. Each declared version must be a non-empty range.`)
88+
}
89+
90+
if (range === '*') {
91+
add(latestMajor(name), range)
92+
continue
93+
}
94+
95+
// Exact-version notations collapse to one key so the same version is never installed twice.
96+
const exact = clean(range)
97+
if (exact) {
98+
add(exact, range)
99+
continue
100+
}
101+
102+
const floor = coerce(range)
103+
if (!floor) throw new Error(`Invalid version range for '${name}': ${range}`)
104+
105+
add(floor.version, range)
106+
107+
const topMajor = highestMajor(name, range, floor.major)
108+
if (nonConsecutiveMajors.has(name)) {
109+
// Only the in-between majors were never published. The floor major and the top major both exist, so add the
110+
// latest of each (skipping the uncertain middle, which is what would make the install fail).
111+
add(String(floor.major), range)
112+
if (topMajor > floor.major) add(String(topMajor), range)
113+
} else {
114+
for (let major = floor.major; major <= topMajor; major++) {
115+
if (intersects(`>=${major}.0.0 <${major + 1}.0.0`, range)) add(String(major), range)
116+
}
117+
}
118+
}
119+
120+
// The bare-major key for the pinned latest's major resolves to the pin itself, so when a declared floor already pins
121+
// that exact version the major key would install the same thing. Drop it. This is the only such redundancy the
122+
// pinned upper bound lets us prove; for lower majors the newest published version is unknown without the registry.
123+
const pinned = coerce(latests[name])
124+
if (pinned && entries.has(pinned.version)) entries.delete(String(pinned.major))
125+
126+
return [...entries.values()]
127+
}
128+
129+
/**
130+
* The latest major of `name` as a bare-major version key. `*` resolves here so it de-duplicates against an open-ended
131+
* range whose top resolves to the same newest version.
132+
*
133+
* @param {string} name
134+
* @returns {string}
135+
*/
136+
function latestMajor (name) {
137+
const latest = coerce(latests[name])
138+
if (!latest) {
139+
throw new Error(
140+
`Latest version for '${name}' needs to be defined in 'packages/dd-trace/test/plugins/versions/package.json'.`
141+
)
142+
}
143+
return String(latest.major)
144+
}
145+
146+
/**
147+
* Highest major still spanned by `range`, capped at the pinned latest. Iterates down from the latest so the first
148+
* intersecting major is the top; nothing above the pinned latest is installed.
149+
*
150+
* @param {string} name
151+
* @param {string} range
152+
* @param {number} floorMajor
153+
* @returns {number}
154+
*/
155+
function highestMajor (name, range, floorMajor) {
156+
const latest = coerce(latests[name])
157+
if (!latest) return floorMajor
158+
for (let major = latest.major; major > floorMajor; major--) {
159+
if (intersects(`>=${major}.0.0 <${major + 1}.0.0`, range)) return major
160+
}
161+
return floorMajor
162+
}
163+
164+
/**
165+
* Resolve which version keys to install and test for a module, plus which key the unversioned
166+
* `versions/<name>` folder points at. `scripts/install_plugin_modules.js` and `withVersions()` both call this so the
167+
* installed folder set and the tested folder set are derived from one place and cannot drift.
168+
*
169+
* @param {object} options
170+
* @param {string} options.name The module name, e.g. `fastify`.
171+
* @param {string[]} options.declaredVersions The declared version entries to expand.
172+
* @param {boolean} [options.honourEnvRange] Whether `PACKAGE_VERSION_RANGE` applies to this module. False for sibling
173+
* externals that must stay on their declared versions while the matrix shards a different package.
174+
* @param {NodeJS.ProcessEnv} [options.env] Injectable for testing.
175+
* @returns {{ versionList: Array<{ versionKey: string, range: string }>, unversioned: string|undefined }} The ordered,
176+
* `RANGE`-filtered key set, and the key the default `versions/<name>` folder resolves to (the newest in-scope entry,
177+
* or `undefined` when nothing is in scope).
178+
*/
179+
function resolvePluginVersions ({ name, declaredVersions, honourEnvRange = true, env = process.env }) {
180+
const useEnvRange = Boolean(env.PACKAGE_VERSION_RANGE) && honourEnvRange
181+
const versions = useEnvRange ? [env.PACKAGE_VERSION_RANGE] : declaredVersions
182+
183+
let versionList = getVersionList(name, versions)
184+
if (env.RANGE) {
185+
versionList = versionList.filter(({ versionKey }) => subset(versionKey, env.RANGE))
186+
}
187+
188+
// With `PACKAGE_VERSION_RANGE` the shard itself is the target, so the unversioned folder keeps the raw range even
189+
// when `RANGE` narrows the installed keys; otherwise it follows the newest in-scope key.
190+
const unversioned = useEnvRange ? env.PACKAGE_VERSION_RANGE : versionList.at(-1)?.versionKey
191+
192+
return { versionList, unversioned }
193+
}
194+
43195
module.exports = {
44-
getCappedRange
196+
getCappedRange,
197+
getVersionList,
198+
resolvePluginVersions,
45199
}

0 commit comments

Comments
 (0)