Skip to content

Commit 55f6aa7

Browse files
committed
test(plugins): select latest unversioned dependency range (#9352)
Runtime-gated declarations could leave the first active range as the unversioned target even when a newer compatible range was also active.
1 parent 98ebe70 commit 55f6aa7

5 files changed

Lines changed: 159 additions & 88 deletions

File tree

packages/dd-trace/test/plugins/externals.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,12 @@ module.exports = {
163163
'express-mongo-sanitize': [
164164
{
165165
name: 'mongodb',
166-
versions: ['>=3.3 <5', '5', '6', '>=7'],
167-
node: '>=20.19.0',
166+
versions: ['>=3.3 <5', '5', '6'],
168167
},
169168
{
170169
name: 'mongodb',
171-
versions: ['>=3.3 <5', '5', '6'],
172-
node: '<20.19.0',
170+
versions: ['>=7'],
171+
node: '>=20.19.0',
173172
},
174173
{
175174
name: 'mongodb-core',
@@ -201,13 +200,12 @@ module.exports = {
201200
},
202201
{
203202
name: 'mongodb',
204-
versions: ['5', '6', '>=7'],
205-
node: '>=20.19.0',
203+
versions: ['5', '6'],
206204
},
207205
{
208206
name: 'mongodb',
209-
versions: ['5', '6'],
210-
node: '<20.19.0',
207+
versions: ['>=7'],
208+
node: '>=20.19.0',
211209
},
212210
],
213211
mysql2: [

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

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@
33
const assert = require('node:assert/strict')
44

55
const { describe, it } = require('mocha')
6-
const { coerce, major } = require('semver')
6+
const { coerce, major, maxSatisfying } = require('semver')
77

8-
const { getVersionList, resolvePluginVersions, brokenVersionReason } = require('./versions')
8+
const {
9+
brokenVersionReason,
10+
getVersionList,
11+
resolvePluginVersions,
12+
} = require('./versions')
913

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

@@ -80,32 +84,43 @@ describe('getVersionList', () => {
8084

8185
describe('resolvePluginVersions', () => {
8286
const versionKeys = result => result.versionList.map(({ versionKey }) => versionKey)
83-
84-
it('expands the declared versions and points the unversioned folder at the newest in-scope key', () => {
85-
const result = resolvePluginVersions({ name: 'mongodb', declaredVersions: ['>=2 <5'], env: {} })
87+
const declarations = [
88+
{
89+
versions: ['>=7'],
90+
node: '>=20.19.0',
91+
},
92+
{
93+
versions: ['5', '6'],
94+
},
95+
]
96+
97+
it('expands the declared versions and uses every active range for the unversioned folder', () => {
98+
const result = resolvePluginVersions({
99+
name: 'mongodb',
100+
declarations: [{ versions: ['>=2 <5'] }],
101+
env: {},
102+
})
86103

87104
assert.deepEqual(versionKeys(result), ['2.0.0', '2', '3', '4'])
88-
assert.equal(result.unversioned, '4')
105+
assert.equal(result.unversioned, '>=2 <5')
89106
})
90107

91108
it('includes declarations at the Node.js range boundary', () => {
92109
const result = resolvePluginVersions({
93110
name: 'mongodb',
94-
declaredVersions: ['>=2 <5'],
95-
nodeRange: '>=22',
111+
declarations: [{ versions: ['>=2 <5'], node: '>=22' }],
96112
nodeVersion: '22.0.0',
97113
env: {},
98114
})
99115

100116
assert.deepEqual(versionKeys(result), ['2.0.0', '2', '3', '4'])
101-
assert.equal(result.unversioned, '4')
117+
assert.equal(result.unversioned, '>=2 <5')
102118
})
103119

104120
it('excludes declarations outside the Node.js range before applying package range overrides', () => {
105121
const result = resolvePluginVersions({
106122
name: 'mongodb',
107-
declaredVersions: ['>=2 <5'],
108-
nodeRange: '>=22',
123+
declarations: [{ versions: ['>=2 <5'], node: '>=22' }],
109124
nodeVersion: '21.999.999',
110125
env: { PACKAGE_VERSION_RANGE: '>=3 <4' },
111126
})
@@ -118,8 +133,7 @@ describe('resolvePluginVersions', () => {
118133
assert.throws(
119134
() => resolvePluginVersions({
120135
name: 'mongodb',
121-
declaredVersions: ['>=2 <5'],
122-
nodeRange: 'not-a-version',
136+
declarations: [{ versions: ['>=2 <5'], node: 'not-a-version' }],
123137
nodeVersion: '22.0.0',
124138
env: {},
125139
}),
@@ -130,18 +144,19 @@ describe('resolvePluginVersions', () => {
130144
it('filters the installed keys by RANGE and follows the filtered tail', () => {
131145
const result = resolvePluginVersions({
132146
name: 'mongodb',
133-
declaredVersions: ['>=1 <6'],
147+
declarations: [{ versions: ['>=1 <6'] }],
134148
env: { RANGE: '>=2.0.0 <4.0.0' },
135149
})
136150

137151
assert.deepEqual(versionKeys(result), ['2', '3'])
138-
assert.equal(result.unversioned, '3')
152+
assert.equal(result.unversioned, '2 || 3')
153+
assert.equal(maxSatisfying(['2.12.0', '3.9.0'], result.unversioned), '3.9.0')
139154
})
140155

141156
it('replaces the declared versions with PACKAGE_VERSION_RANGE when the module is honoured', () => {
142157
const result = resolvePluginVersions({
143158
name: 'mongodb',
144-
declaredVersions: ['>=2 <5'],
159+
declarations: [{ versions: ['>=2 <5'] }],
145160
env: { PACKAGE_VERSION_RANGE: '>=3 <4' },
146161
})
147162

@@ -152,19 +167,19 @@ describe('resolvePluginVersions', () => {
152167
it('ignores PACKAGE_VERSION_RANGE for a sibling external that must not be sharded', () => {
153168
const result = resolvePluginVersions({
154169
name: 'mongodb',
155-
declaredVersions: ['>=2 <5'],
170+
declarations: [{ versions: ['>=2 <5'] }],
156171
honourEnvRange: false,
157172
env: { PACKAGE_VERSION_RANGE: '>=3 <4' },
158173
})
159174

160175
assert.deepEqual(versionKeys(result), ['2.0.0', '2', '3', '4'])
161-
assert.equal(result.unversioned, '4')
176+
assert.equal(result.unversioned, '>=2 <5')
162177
})
163178

164179
it('keeps the unversioned folder on the raw shard while RANGE narrows the installed keys', () => {
165180
const result = resolvePluginVersions({
166181
name: 'mongodb',
167-
declaredVersions: ['>=1 <6'],
182+
declarations: [{ versions: ['>=1 <6'] }],
168183
env: { PACKAGE_VERSION_RANGE: '>=2 <5', RANGE: '>=3.0.0 <4.0.0' },
169184
})
170185

@@ -173,7 +188,7 @@ describe('resolvePluginVersions', () => {
173188
})
174189

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

178193
assert.deepEqual(result.versionList, [])
179194
assert.equal(result.unversioned, undefined)
@@ -182,13 +197,58 @@ describe('resolvePluginVersions', () => {
182197
it('reports nothing in scope when RANGE excludes every declared key', () => {
183198
const result = resolvePluginVersions({
184199
name: 'mongodb',
185-
declaredVersions: ['>=2 <3'],
200+
declarations: [{ versions: ['>=2 <3'] }],
186201
env: { RANGE: '>=9.0.0 <10.0.0' },
187202
})
188203

189204
assert.deepEqual(result.versionList, [])
190205
assert.equal(result.unversioned, undefined)
191206
})
207+
208+
it('points the unversioned folder at the latest active declaration regardless of order', () => {
209+
const result = resolvePluginVersions({
210+
name: 'mongodb',
211+
declarations,
212+
nodeVersion: '20.19.0',
213+
env: {},
214+
})
215+
216+
assert.deepEqual(
217+
versionKeys(result),
218+
['7.0.0', '7', '5.0.0', '5', '6.0.0', '6']
219+
)
220+
assert.equal(maxSatisfying(['6.21.0', '7.2.0'], result.unversioned), '7.2.0')
221+
})
222+
223+
it('excludes declarations unsupported by the current Node.js version', () => {
224+
const result = resolvePluginVersions({
225+
name: 'mongodb',
226+
declarations,
227+
nodeVersion: '20.18.0',
228+
env: {},
229+
})
230+
231+
assert.deepEqual(
232+
versionKeys(result),
233+
['5.0.0', '5', '6.0.0', '6']
234+
)
235+
assert.equal(result.unversioned, '5 || 6')
236+
})
237+
238+
it('applies a package version override once across active declarations', () => {
239+
const result = resolvePluginVersions({
240+
name: 'mongodb',
241+
declarations,
242+
nodeVersion: '20.19.0',
243+
env: { PACKAGE_VERSION_RANGE: '>=6 <7' },
244+
})
245+
246+
assert.deepEqual(
247+
versionKeys(result),
248+
['6.0.0', '6']
249+
)
250+
assert.equal(result.unversioned, '>=6 <7')
251+
})
192252
})
193253

194254
describe('brokenVersionReason', () => {

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

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -218,40 +218,55 @@ function highestMajor (name, range, floorMajor) {
218218
*
219219
* @param {object} options
220220
* @param {string} options.name The module name, e.g. `fastify`.
221-
* @param {string[]} options.declaredVersions The declared version entries to expand.
222-
* @param {string} [options.nodeRange] The Node.js versions where these declarations apply.
221+
* @param {Array<{ versions?: string[], node?: string }>} options.declarations
223222
* @param {string} [options.nodeVersion] The current Node.js version; injectable for testing.
224223
* @param {boolean} [options.honourEnvRange] Whether `PACKAGE_VERSION_RANGE` applies to this module. False for sibling
225224
* externals that must stay on their declared versions while the matrix shards a different package.
226225
* @param {NodeJS.ProcessEnv} [options.env] Injectable for testing.
227226
* @returns {{ versionList: Array<{ versionKey: string, range: string }>, unversioned: string|undefined }} The ordered,
228-
* `RANGE`-filtered key set, and the key the default `versions/<name>` folder resolves to (the newest in-scope entry,
229-
* or `undefined` when nothing is in scope).
227+
* `RANGE`-filtered key set, and the range the default `versions/<name>` folder resolves from.
230228
*/
231229
function resolvePluginVersions ({
232230
name,
233-
declaredVersions,
234-
nodeRange,
231+
declarations,
235232
nodeVersion = process.versions.node,
236233
honourEnvRange = true,
237234
env = process.env,
238235
}) {
239-
if (nodeRange !== undefined) {
240-
if (!validRange(nodeRange)) throw new Error(`Invalid Node.js version range for '${name}': ${nodeRange}`)
241-
if (!satisfies(nodeVersion, nodeRange)) return { versionList: [], unversioned: undefined }
236+
const useEnvRange = Boolean(env.PACKAGE_VERSION_RANGE) && honourEnvRange
237+
const versions = []
238+
let hasActiveDeclaration = false
239+
240+
for (const declaration of declarations) {
241+
if (declaration.node !== undefined) {
242+
if (!validRange(declaration.node)) {
243+
throw new Error(`Invalid Node.js version range for '${name}': ${declaration.node}`)
244+
}
245+
if (!satisfies(nodeVersion, declaration.node)) continue
246+
}
247+
248+
hasActiveDeclaration = true
249+
if (!useEnvRange) versions.push(...(declaration.versions ?? []))
242250
}
243251

244-
const useEnvRange = Boolean(env.PACKAGE_VERSION_RANGE) && honourEnvRange
245-
const versions = useEnvRange ? [env.PACKAGE_VERSION_RANGE] : declaredVersions
252+
if (!hasActiveDeclaration) return { versionList: [], unversioned: undefined }
253+
if (useEnvRange) versions.push(env.PACKAGE_VERSION_RANGE)
246254

247255
let versionList = getVersionList(name, versions)
248256
if (env.RANGE) {
249257
versionList = versionList.filter(({ versionKey }) => subset(versionKey, env.RANGE))
258+
if (!useEnvRange) {
259+
versions.length = 0
260+
for (const { versionKey } of versionList) versions.push(versionKey)
261+
}
250262
}
251263

252264
// With `PACKAGE_VERSION_RANGE` the shard itself is the target, so the unversioned folder keeps the raw range even
253-
// when `RANGE` narrows the installed keys; otherwise it follows the newest in-scope key.
254-
const unversioned = useEnvRange ? env.PACKAGE_VERSION_RANGE : versionList.at(-1)?.versionKey
265+
// when `RANGE` narrows the installed keys. Otherwise the package manager selects the newest version from every
266+
// active range, independent of declaration order.
267+
const unversioned = useEnvRange
268+
? env.PACKAGE_VERSION_RANGE
269+
: versions.length > 0 ? versions.join(' || ') : undefined
255270

256271
return { versionList, unversioned }
257272
}

packages/dd-trace/test/setup/mocha.js

Lines changed: 15 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -291,34 +291,29 @@ function withVersions (plugin, modules, range, cb) {
291291
/** @type {Map<string, {versionRange: string, versionKey: string, resolvedVersion: string}>} */
292292
const testVersions = new Map()
293293

294-
let moduleMatched = false
294+
const declarations = []
295295
for (const instrumentation of instrumentations) {
296-
if (instrumentation.name !== moduleName) continue
297-
moduleMatched = true
298-
299-
// Some entries coming from `externals.js` are dependency-only (e.g. `dep: true`) and don't have `versions`.
300-
// Treat those as "not a test target" instead of crashing.
301-
// Share the install script's resolution so the tested folders exactly match the installed ones (lowest supported
302-
// version, the latest of every major in between, and the newest supported version), de-duplicated by version.
303-
const { versionList } = resolvePluginVersions({
304-
name: moduleName,
305-
declaredVersions: normalizeVersions(instrumentation.versions),
306-
nodeRange: instrumentation.node,
307-
})
308-
309-
for (const { versionKey, range: declaredRange } of versionList) {
310-
// Exact keys resolve to themselves; range keys (`*`, `>=2`, `>=3.0.0 <4.0.0`) resolve to what was installed.
311-
const resolvedVersion = semver.valid(versionKey) ?? require(getModulePath(moduleName, versionKey)).version()
312-
testVersions.set(resolvedVersion, { versionRange: declaredRange, versionKey, resolvedVersion })
313-
}
296+
if (instrumentation.name === moduleName) declarations.push(instrumentation)
314297
}
315298

316299
// A module no instrumentation declares would silently run zero tests instead of failing.
317-
if (!moduleMatched) {
300+
if (declarations.length === 0) {
318301
throw new Error(`withVersions: no instrumentation declares the module "${moduleName}". Pass the integration ` +
319302
`name as the first argument (e.g. 'express'), or register "${moduleName}" in test/plugins/externals.js.`)
320303
}
321304

305+
// Some entries coming from `externals.js` are dependency-only (e.g. `dep: true`) and don't have `versions`.
306+
// Treat those as "not a test target" instead of crashing.
307+
// Share the install script's resolution so the tested folders exactly match the installed ones (lowest supported
308+
// version, the latest of every major in between, and the newest supported version), de-duplicated by version.
309+
const { versionList } = resolvePluginVersions({ name: moduleName, declarations })
310+
311+
for (const { versionKey, range: declaredRange } of versionList) {
312+
// Exact keys resolve to themselves; range keys (`*`, `>=2`, `>=3.0.0 <4.0.0`) resolve to what was installed.
313+
const resolvedVersion = semver.valid(versionKey) ?? require(getModulePath(moduleName, versionKey)).version()
314+
testVersions.set(resolvedVersion, { versionRange: declaredRange, versionKey, resolvedVersion })
315+
}
316+
322317
const testCases = Array.from(testVersions.values())
323318
.filter(({ resolvedVersion }) => !range || semver.satisfies(resolvedVersion, range))
324319
.sort(({ resolvedVersion }) => resolvedVersion.localeCompare(resolvedVersion))
@@ -370,11 +365,6 @@ function withVersions (plugin, modules, range, cb) {
370365
}
371366
}
372367

373-
function normalizeVersions (versions) {
374-
if (!versions) return []
375-
return Array.isArray(versions) ? versions : [versions]
376-
}
377-
378368
/**
379369
* @callback withExportsCallback
380370
* @param {() => import('module').Module} getExport - A function that returns the module export to test

0 commit comments

Comments
 (0)