Skip to content

Commit 20798c8

Browse files
authored
test: extract pure functions to better testing (#71)
* test: extract pure functions to better testing * cleanup * update
1 parent c24bca6 commit 20798c8

8 files changed

Lines changed: 208 additions & 215 deletions

File tree

src/providers/diagnostics/rules/engine-mismatch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import intersects from 'semver/ranges/intersects'
77
import subset from 'semver/ranges/subset'
88
import { DiagnosticSeverity, Uri } from 'vscode'
99

10-
interface EngineMismatch {
10+
export interface EngineMismatch {
1111
engine: string
1212
packageRange: string
1313
dependencyRange: string
1414
hasIntersection: boolean
1515
}
1616

17-
function resolveEngineMismatches(
17+
export function resolveEngineMismatches(
1818
packageEngines: Engines,
1919
dependencyEngines: Engines,
2020
) {
Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ValidNode } from '#types/extractor'
2+
import type { ParsedVersion } from '#utils/version'
23
import type { DiagnosticRule, NodeDiagnosticInfo } from '..'
34
import { config } from '#state'
45
import { checkIgnored } from '#utils/ignore'
@@ -9,48 +10,78 @@ import lte from 'semver/functions/lte'
910
import prerelease from 'semver/functions/prerelease'
1011
import { DiagnosticSeverity, Uri } from 'vscode'
1112

12-
function createUpgradeDiagnostic(node: ValidNode, name: string, targetVersion: string): NodeDiagnosticInfo {
13-
return {
14-
node,
15-
severity: DiagnosticSeverity.Hint,
16-
message: `"${name}" can be upgraded to ${targetVersion}.`,
17-
code: {
18-
value: 'upgrade',
19-
target: Uri.parse(npmxPackageUrl(name, targetVersion)),
20-
},
21-
}
13+
export interface ResolveUpgradeOptions {
14+
name: string
15+
version: string
16+
parsed: ParsedVersion
17+
exactVersion: string
18+
distTags: Record<string, string>
19+
ignoreList: string[]
2220
}
2321

24-
export const checkUpgrade: DiagnosticRule = ({ dep, name, pkg, parsed, exactVersion }) => {
25-
if (!parsed || !exactVersion)
26-
return
22+
export interface UpgradeResult {
23+
name: string
24+
targetVersion: string
25+
}
26+
27+
export function resolveUpgrade(options: ResolveUpgradeOptions): UpgradeResult | undefined {
28+
const { name, version, parsed, exactVersion, distTags, ignoreList } = options
2729

28-
if (Object.hasOwn(pkg.distTags, dep.version))
30+
if (Object.hasOwn(distTags, version))
2931
return
3032

31-
const { latest } = pkg.distTags
33+
const { latest } = distTags
3234
if (gt(latest, exactVersion)) {
3335
const targetVersion = formatUpgradeVersion(parsed, latest)
34-
if (checkIgnored({ ignoreList: config.ignore.upgrade, name, version: targetVersion }))
36+
if (checkIgnored({ ignoreList, name, version: targetVersion }))
3537
return
36-
return createUpgradeDiagnostic(dep.versionNode, name, targetVersion)
38+
return { name, targetVersion }
3739
}
3840

3941
const currentPreId = prerelease(exactVersion)?.[0]
4042
if (currentPreId == null)
4143
return
4244

43-
for (const [tag, tagVersion] of Object.entries(pkg.distTags)) {
45+
for (const [tag, tagVersion] of Object.entries(distTags)) {
4446
if (tag === 'latest')
4547
continue
4648
if (prerelease(tagVersion)?.[0] !== currentPreId)
4749
continue
4850
if (lte(tagVersion, exactVersion))
4951
continue
5052
const targetVersion = formatUpgradeVersion(parsed, tagVersion)
51-
if (checkIgnored({ ignoreList: config.ignore.upgrade, name, version: targetVersion }))
53+
if (checkIgnored({ ignoreList, name, version: targetVersion }))
5254
continue
5355

54-
return createUpgradeDiagnostic(dep.versionNode, name, targetVersion)
56+
return { name, targetVersion }
57+
}
58+
}
59+
60+
function createUpgradeDiagnostic(node: ValidNode, name: string, targetVersion: string): NodeDiagnosticInfo {
61+
return {
62+
node,
63+
severity: DiagnosticSeverity.Hint,
64+
message: `"${name}" can be upgraded to ${targetVersion}.`,
65+
code: {
66+
value: 'upgrade',
67+
target: Uri.parse(npmxPackageUrl(name, targetVersion)),
68+
},
5569
}
5670
}
71+
72+
export const checkUpgrade: DiagnosticRule = ({ dep, name, pkg, parsed, exactVersion }) => {
73+
if (!parsed || !exactVersion)
74+
return
75+
76+
const result = resolveUpgrade({
77+
name,
78+
version: dep.version,
79+
parsed,
80+
exactVersion,
81+
distTags: pkg.distTags,
82+
ignoreList: config.ignore.upgrade,
83+
})
84+
85+
if (result)
86+
return createUpgradeDiagnostic(dep.versionNode, result.name, result.targetVersion)
87+
}

tests/diagnostics/deprecation.test.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,24 @@ function createDeprecationContext(version: string) {
2121

2222
describe('checkDeprecation', () => {
2323
it('should flag deprecated version', async () => {
24-
const ctx = createDeprecationContext('1.0.0')
25-
const result = await checkDeprecation(ctx)
24+
const result = await checkDeprecation(createDeprecationContext('1.0.0'))
2625

27-
expect(result).toBeDefined()
26+
expect(result).toMatchObject({
27+
code: { value: 'deprecation' },
28+
})
2829
expect(result!.message).toMatchInlineSnapshot('""lodash@1.0.0" has been deprecated: old notice"')
29-
expect(result!.code).toMatchObject({ value: 'deprecation' })
3030
})
3131

3232
it('resolve range to the highest matching deprecated version', async () => {
33-
const ctx = createDeprecationContext('^1.0.0')
34-
const result = await checkDeprecation(ctx)
33+
const result = await checkDeprecation(createDeprecationContext('^1.0.0'))
3534

36-
expect(result).toBeDefined()
35+
expect(result).toMatchObject({
36+
code: { value: 'deprecation' },
37+
})
3738
expect(result!.message).toMatchInlineSnapshot('""lodash@1.2.0" has been deprecated: new notice"')
38-
expect(result!.code).toMatchObject({ value: 'deprecation' })
3939
})
4040

4141
it('should not flag non-deprecated version', async () => {
42-
const ctx = createDeprecationContext('^2.0.0')
43-
const result = await checkDeprecation(ctx)
44-
45-
expect(result).toBeUndefined()
42+
expect(await checkDeprecation(createDeprecationContext('^2.0.0'))).toBeUndefined()
4643
})
4744
})

tests/diagnostics/dist-tag.test.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@ import { createContext } from './context'
44

55
describe('checkDistTag', () => {
66
it('should flag when version matches a dist tag', async () => {
7-
const ctx = createContext({ name: 'lodash', version: 'latest', distTags: { latest: '2.0.0' } })
8-
const result = await checkDistTag(ctx)
9-
10-
expect(result).toBeDefined()
11-
expect(result!.code).toMatchObject({ value: 'dist-tag' })
7+
expect(await checkDistTag(
8+
createContext({ name: 'lodash', version: 'latest', distTags: { latest: '2.0.0' } }),
9+
)).toMatchObject({
10+
code: { value: 'dist-tag' },
11+
})
1212
})
1313

1414
it('should not flag when version does not match any dist tag', async () => {
15-
const ctx = createContext({ name: 'lodash', version: 'next', distTags: { latest: '2.0.0' } })
16-
const result = await checkDistTag(ctx)
17-
18-
expect(result).toBeUndefined()
15+
expect(await checkDistTag(
16+
createContext({ name: 'lodash', version: 'next', distTags: { latest: '2.0.0' } }),
17+
)).toBeUndefined()
1918
})
2019
})
Lines changed: 72 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,101 @@
1-
import type { Engines } from 'fast-npm-meta'
21
import { describe, expect, it } from 'vitest'
3-
import { checkEngineMismatch } from '../../src/providers/diagnostics/rules/engine-mismatch'
2+
import { checkEngineMismatch, resolveEngineMismatches } from '../../src/providers/diagnostics/rules/engine-mismatch'
43
import { createContext } from './context'
54

6-
function createEngineMismatchContext(
7-
engines: Engines | undefined,
8-
dependencyEngines: Engines | undefined,
9-
) {
10-
return createContext({
11-
name: 'foo',
12-
version: '^1.0.0',
13-
distTags: { latest: '1.0.0' },
14-
versionsMeta: {
15-
'1.0.0': dependencyEngines ? { engines: dependencyEngines } : {},
16-
},
17-
engines,
18-
})
19-
}
20-
21-
describe('checkEngineMismatch', () => {
22-
it('should flag when engine ranges do not overlap', async () => {
23-
const ctx = createEngineMismatchContext(
5+
describe('resolveEngineMismatches', () => {
6+
it('should flag when engine ranges do not overlap', () => {
7+
expect(resolveEngineMismatches(
248
{ node: '^18.0.0' },
259
{ node: '>=20' },
26-
)
27-
const result = await checkEngineMismatch(ctx)
28-
29-
expect(result).toBeDefined()
30-
expect(result!.code).toMatchObject({ value: 'engine-mismatch' })
31-
expect(result!.message).toContain('requires ">=20", but package supports "^18.0.0"')
10+
)).toMatchObject([
11+
{
12+
engine: 'node',
13+
packageRange: '^18.0.0',
14+
dependencyRange: '>=20',
15+
hasIntersection: false,
16+
},
17+
])
3218
})
3319

34-
it('should flag when engine ranges overlap but are not fully compatible', async () => {
35-
const ctx = createEngineMismatchContext(
20+
it('should flag when engine ranges overlap but are not fully compatible', () => {
21+
expect(resolveEngineMismatches(
3622
{ node: '>=20', npm: '>=8 <11' },
3723
{ node: '>=18', npm: '>=10 <12' },
38-
)
39-
const result = await checkEngineMismatch(ctx)
40-
41-
expect(result).toBeDefined()
42-
expect(result!.message).toContain('npm')
43-
expect(result!.message).toContain('partial overlap')
24+
)).toMatchObject([{
25+
engine: 'npm',
26+
hasIntersection: true,
27+
}])
4428
})
4529

46-
it('should include multiple engine mismatches in one diagnostic', async () => {
47-
const ctx = createEngineMismatchContext(
30+
it('should include multiple engine mismatches', () => {
31+
expect(resolveEngineMismatches(
4832
{ node: '^18.0.0', npm: '^9.0.0' },
4933
{ node: '>=20', npm: '>=10' },
50-
)
51-
const result = await checkEngineMismatch(ctx)
52-
53-
expect(result).toBeDefined()
54-
expect(result!.message).toContain('node')
55-
expect(result!.message).toContain('npm')
34+
)).toMatchObject([
35+
{ engine: 'node' },
36+
{ engine: 'npm' },
37+
])
5638
})
5739

58-
it('should not flag when package ranges are compatible', async () => {
59-
const ctx = createEngineMismatchContext(
40+
it('should not flag when package ranges are compatible', () => {
41+
expect(resolveEngineMismatches(
6042
{ node: '>=20', npm: '>=10' },
6143
{ node: '>=18', npm: '>=10' },
62-
)
63-
64-
expect(await checkEngineMismatch(ctx)).toBeUndefined()
44+
)).toEqual([])
6545
})
6646

67-
it('should not flag when package does not declare the dependency engine', async () => {
68-
const ctx = createEngineMismatchContext(
47+
it('should not flag when package does not declare the dependency engine', () => {
48+
expect(resolveEngineMismatches(
6949
{ node: '>=20' },
7050
{ npm: '>=10' },
71-
)
51+
)).toEqual([])
52+
})
7253

73-
expect(await checkEngineMismatch(ctx)).toBeUndefined()
54+
it('should skip engines with non-standard semver values', () => {
55+
expect(resolveEngineMismatches(
56+
{ node: '>=18' },
57+
{ node: 'lts' },
58+
)).toEqual([])
7459
})
60+
})
7561

76-
it('should not flag when either engines is missing', async () => {
77-
expect(await checkEngineMismatch(createEngineMismatchContext(undefined, { node: '>=18' }))).toBeUndefined()
78-
expect(await checkEngineMismatch(createEngineMismatchContext({ node: '>=18' }, undefined))).toBeUndefined()
62+
describe('checkEngineMismatch', () => {
63+
it('should format a diagnostic when mismatches exist', async () => {
64+
const result = await checkEngineMismatch(createContext({
65+
name: 'foo',
66+
version: '^1.0.0',
67+
distTags: { latest: '1.0.0' },
68+
versionsMeta: {
69+
'1.0.0': {
70+
engines: { node: '>=20' },
71+
},
72+
},
73+
engines: { node: '^18.0.0' },
74+
}))
75+
76+
expect(result).toBeDefined()
77+
expect(result!.code).toMatchObject({ value: 'engine-mismatch' })
78+
expect(result!.message).toContain('requires ">=20", but package supports "^18.0.0"')
7979
})
8080

81-
it('should skip engines with non-standard semver values', async () => {
82-
const ctx = createEngineMismatchContext(
83-
{ node: '>=18' },
84-
{ node: 'lts' },
85-
)
81+
it('should not flag when either engines is missing', async () => {
82+
expect(await checkEngineMismatch(createContext({
83+
name: 'foo',
84+
version: '^1.0.0',
85+
distTags: { latest: '1.0.0' },
86+
versionsMeta: {
87+
'1.0.0': { engines: { node: '>=18' } },
88+
},
89+
}))).toBeUndefined()
8690

87-
expect(await checkEngineMismatch(ctx)).toBeUndefined()
91+
expect(await checkEngineMismatch(createContext({
92+
name: 'foo',
93+
version: '^1.0.0',
94+
distTags: { latest: '1.0.0' },
95+
versionsMeta: {
96+
'1.0.0': {},
97+
},
98+
engines: { node: '>=18' },
99+
}))).toBeUndefined()
88100
})
89101
})

tests/diagnostics/replacement.test.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,17 @@ import { checkReplacement } from '../../src/providers/diagnostics/rules/replacem
33
import { createContext } from './context'
44

55
function createReplacementContext(name: string) {
6-
return createContext({
7-
name,
8-
version: '^1.0.0',
9-
distTags: { latest: '1.0.0' },
10-
versionsMeta: { '1.0.0': {} },
11-
})
6+
return createContext({ name, version: '^1.0.0' })
127
}
138

149
describe('checkReplacement', () => {
1510
it('should flag when replacement found', async () => {
16-
const result = await checkReplacement(createReplacementContext('left-pad'))
17-
18-
expect(result).toBeDefined()
19-
expect(result!.message).toBeDefined()
20-
expect(result!.code).toMatchObject({ value: 'replacement' })
11+
expect(await checkReplacement(createReplacementContext('left-pad'))).toMatchObject({
12+
code: { value: 'replacement' },
13+
})
2114
})
2215

2316
it('should not flag when no replacement found', async () => {
24-
const result = await checkReplacement(createReplacementContext('vitest'))
25-
26-
expect(result).toBeUndefined()
17+
expect(await checkReplacement(createReplacementContext('vitest'))).toBeUndefined()
2718
})
2819
})

0 commit comments

Comments
 (0)