Skip to content

Commit b4b4b6f

Browse files
committed
fixup: add tests and prefix for ps1 scripts
1 parent aa4bf21 commit b4b4b6f

File tree

5 files changed

+210
-97
lines changed

5 files changed

+210
-97
lines changed

.github/workflows/ci.yml

+33
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,36 @@ jobs:
150150
run: node . test -w smoke-tests --ignore-scripts
151151
- name: Check Git Status
152152
run: node scripts/git-dirty.js
153+
154+
windows-shims:
155+
name: Windows Shims Tests
156+
runs-on: windows-latest
157+
defaults:
158+
run:
159+
shell: cmd
160+
steps:
161+
- name: Checkout
162+
uses: actions/checkout@v3
163+
- name: Setup Git User
164+
run: |
165+
git config --global user.email "npm-cli+bot@github.com"
166+
git config --global user.name "npm CLI robot"
167+
- name: Setup Node
168+
uses: actions/setup-node@v3
169+
with:
170+
node-version: 18.x
171+
cache: npm
172+
- name: Reset Deps
173+
run: node . run resetdeps
174+
- name: Setup WSL
175+
uses: Vampire/setup-wsl@v2.0.1
176+
- name: Set up Cygwin
177+
uses: egor-tensin/setup-cygwin@v4.0.1
178+
with:
179+
install-dir: C:\Windows\cygwin64
180+
- name: Run Windows Shims Tests
181+
run: node . test --ignore-scripts -- test/bin/windows-shims.js --no-coverage
182+
env:
183+
WINDOWS_SHIMS_TEST: fail
184+
- name: Check Git Status
185+
run: node scripts/git-dirty.js

bin/npm.ps1

+13-4
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,27 @@ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
99
}
1010
$ret=0
1111

12-
$nodebin = $(Get-Command "node$exe" -ErrorAction SilentlyContinue -ErrorVariable F).Source
12+
$nodeexe = "node$exe"
13+
$nodebin = $(Get-Command $nodeexe -ErrorAction SilentlyContinue -ErrorVariable F).Source
1314
if ($nodebin -eq $null) {
14-
Write-Host "node$exe not found."
15+
Write-Host "$nodeexe not found."
1516
exit 1
1617
}
1718
$nodedir = $(New-Object -ComObject Scripting.FileSystemObject).GetFile("$nodebin").ParentFolder.Path
1819

20+
$npmclijs="$nodedir/node_modules/npm/bin/npm-cli.js"
21+
$npmprefix=(& $nodeexe $npmclijs prefix -g)
22+
if ($LASTEXITCODE -ne 0) {
23+
Write-Host "Could not determine Node.js install directory"
24+
exit 1
25+
}
26+
$npmprefixclijs="$npmprefix/node_modules/npm/bin/npm-cli.js"
27+
1928
# Support pipeline input
2029
if ($MyInvocation.ExpectingInput) {
21-
$input | & "node$exe" "$nodedir/node_modules/npm/bin/npm-cli.js" $args
30+
$input | & $nodeexe $npmprefixclijs $args
2231
} else {
23-
& "node$exe" "$nodedir/node_modules/npm/bin/npm-cli.js" $args
32+
& $nodeexe $npmprefixclijs $args
2433
}
2534
$ret=$LASTEXITCODE
2635
exit $ret

bin/npx.ps1

+13-4
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,27 @@ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
99
}
1010
$ret=0
1111

12-
$nodebin = $(Get-Command "node$exe" -ErrorAction SilentlyContinue -ErrorVariable F).Source
12+
$nodeexe = "node$exe"
13+
$nodebin = $(Get-Command $nodeexe -ErrorAction SilentlyContinue -ErrorVariable F).Source
1314
if ($nodebin -eq $null) {
14-
Write-Host "node$exe not found."
15+
Write-Host "$nodeexe not found."
1516
exit 1
1617
}
1718
$nodedir = $(New-Object -ComObject Scripting.FileSystemObject).GetFile("$nodebin").ParentFolder.Path
1819

20+
$npmclijs="$nodedir/node_modules/npm/bin/npm-cli.js"
21+
$npmprefix=(& $nodeexe $npmclijs prefix -g)
22+
if ($LASTEXITCODE -ne 0) {
23+
Write-Host "Could not determine Node.js install directory"
24+
exit 1
25+
}
26+
$npmprefixclijs="$npmprefix/node_modules/npm/bin/npx-cli.js"
27+
1928
# Support pipeline input
2029
if ($MyInvocation.ExpectingInput) {
21-
$input | & "node$exe" "$nodedir/node_modules/npm/bin/npx-cli.js" $args
30+
$input | & $nodeexe $npmprefixclijs $args
2231
} else {
23-
& "node$exe" "$nodedir/node_modules/npm/bin/npx-cli.js" $args
32+
& $nodeexe $npmprefixclijs $args
2433
}
2534
$ret=$LASTEXITCODE
2635
exit $ret

scripts/template-oss/ci.yml

+21
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,24 @@
1111
run: {{rootNpmPath}} test -w smoke-tests --ignore-scripts
1212
- name: Check Git Status
1313
run: node scripts/git-dirty.js
14+
15+
windows-shims:
16+
name: Windows Shims Tests
17+
runs-on: windows-latest
18+
defaults:
19+
run:
20+
shell: cmd
21+
steps:
22+
{{> stepsSetup }}
23+
- name: Setup WSL
24+
uses: Vampire/setup-wsl@v2.0.1
25+
- name: Set up Cygwin
26+
uses: egor-tensin/setup-cygwin@v4.0.1
27+
with:
28+
install-dir: C:\Windows\cygwin64
29+
- name: Run Windows Shims Tests
30+
run: {{rootNpmPath}} test --ignore-scripts -- test/bin/windows-shims.js --no-coverage
31+
env:
32+
WINDOWS_SHIMS_TEST: fail
33+
- name: Check Git Status
34+
run: node scripts/git-dirty.js

test/bin/windows-shims.js

+130-89
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,83 @@
11
const t = require('tap')
2-
const spawn = require('@npmcli/promise-spawn')
32
const { spawnSync } = require('child_process')
4-
const { resolve, join } = require('path')
5-
const { readFileSync, chmodSync } = require('fs')
3+
const { resolve, join, extname, sep } = require('path')
4+
const { readFileSync, chmodSync, readdirSync } = require('fs')
65
const Diff = require('diff')
6+
const { sync: which } = require('which')
77
const { version } = require('../../package.json')
88

9-
const root = resolve(__dirname, '../..')
10-
const npmShim = join(root, 'bin/npm')
11-
const npxShim = join(root, 'bin/npx')
9+
const ROOT = resolve(__dirname, '../..')
10+
const BIN = join(ROOT, 'bin')
11+
const SHIMS = readdirSync(BIN).reduce((acc, shim) => {
12+
if (extname(shim) !== '.js') {
13+
acc[shim] = readFileSync(join(BIN, shim), 'utf-8')
14+
}
15+
return acc
16+
}, {})
17+
18+
// windows requires each segment of a command path to be quoted when using shell: true
19+
const quoteWhich = (cmd) => which(cmd)
20+
.split(sep)
21+
.map(p => p.includes(' ') ? `"${p}"` : p)
22+
.join(sep)
1223

13-
t.test('npm vs npx', t => {
24+
t.test('shim contents', t => {
1425
// these scripts should be kept in sync so this tests the contents of each
1526
// and does a diff to ensure the only differences between them are necessary
16-
const diffFiles = (ext = '') => Diff.diffChars(
17-
readFileSync(`${npmShim}${ext}`, 'utf8'),
18-
readFileSync(`${npxShim}${ext}`, 'utf8')
19-
).filter(v => v.added || v.removed).map((v, i) => i === 0 ? v.value : v.value.toUpperCase())
27+
const diffFiles = (npm, npx) => Diff.diffChars(npm, npx)
28+
.filter(v => v.added || v.removed)
29+
.reduce((acc, v) => {
30+
if (v.value.length === 1) {
31+
acc.letters.add(v.value.toUpperCase())
32+
} else {
33+
acc.diff.push(v.value)
34+
}
35+
return acc
36+
}, { diff: [], letters: new Set() })
37+
38+
t.plan(3)
2039

2140
t.test('bash', t => {
22-
const [npxCli, ...changes] = diffFiles()
23-
const npxCliLine = npxCli.split('\n').reverse().join('')
24-
t.match(npxCliLine, /^NPX_CLI_JS=/, 'has NPX_CLI')
25-
t.equal(changes.length, 20)
26-
t.strictSame([...new Set(changes)], ['M', 'X'], 'all other changes are m->x')
41+
const { diff, letters } = diffFiles(SHIMS.npm, SHIMS.npx)
42+
t.match(diff[0].split('\n').reverse().join(''), /^NPX_CLI_JS=/, 'has NPX_CLI')
43+
t.equal(diff.length, 1)
44+
t.strictSame([...letters], ['M', 'X'], 'all other changes are m->x')
2745
t.end()
2846
})
2947

3048
t.test('cmd', t => {
31-
const [npxCli, ...changes] = diffFiles('.cmd')
32-
t.match(npxCli, /^SET "NPX_CLI_JS=/, 'has NPX_CLI')
33-
t.equal(changes.length, 12)
34-
t.strictSame([...new Set(changes)], ['M', 'X'], 'all other changes are m->x')
49+
const { diff, letters } = diffFiles(SHIMS['npm.cmd'], SHIMS['npx.cmd'])
50+
t.match(diff[0], /^SET "NPX_CLI_JS=/, 'has NPX_CLI')
51+
t.equal(diff.length, 1)
52+
t.strictSame([...letters], ['M', 'X'], 'all other changes are m->x')
3553
t.end()
3654
})
3755

38-
t.end()
56+
t.test('pwsh', t => {
57+
const { diff, letters } = diffFiles(SHIMS['npm.ps1'], SHIMS['npx.ps1'])
58+
t.equal(diff.length, 0)
59+
t.strictSame([...letters], ['M', 'X'], 'all other changes are m->x')
60+
t.end()
61+
})
3962
})
4063

41-
t.test('basic', async t => {
42-
if (process.platform !== 'win32') {
43-
t.comment('test only relevant on windows')
44-
return
45-
}
46-
64+
t.test('run shims', t => {
4765
const path = t.testdir({
66+
...SHIMS,
4867
'node.exe': readFileSync(process.execPath),
49-
npm: readFileSync(npmShim),
50-
npx: readFileSync(npxShim),
5168
// simulate the state where one version of npm is installed
5269
// with node, but we should load the globally installed one
5370
'global-prefix': {
5471
node_modules: {
55-
npm: t.fixture('symlink', root),
72+
npm: t.fixture('symlink', ROOT),
5673
},
5774
},
5875
// put in a shim that ONLY prints the intended global prefix,
5976
// and should not be used for anything else.
6077
node_modules: {
6178
npm: {
6279
bin: {
63-
'npx-cli.js': `
64-
throw new Error('this should not be called')
65-
`,
80+
'npx-cli.js': `throw new Error('this should not be called')`,
6681
'npm-cli.js': `
6782
const assert = require('assert')
6883
const args = process.argv.slice(2)
@@ -76,70 +91,96 @@ t.test('basic', async t => {
7691
},
7792
})
7893

79-
chmodSync(join(path, 'npm'), 0o755)
80-
chmodSync(join(path, 'npx'), 0o755)
81-
82-
const { ProgramFiles, SystemRoot, NYC_CONFIG } = process.env
83-
const gitBash = join(ProgramFiles, 'Git', 'bin', 'bash.exe')
84-
const gitUsrBinBash = join(ProgramFiles, 'Git', 'usr', 'bin', 'bash.exe')
85-
const wslBash = join(SystemRoot, 'System32', 'bash.exe')
86-
const cygwinBash = join(SystemRoot, '/', 'cygwin64', 'bin', 'bash.exe')
87-
88-
const bashes = Object.entries({
89-
'wsl bash': wslBash,
90-
'git bash': gitBash,
91-
'git internal bash': gitUsrBinBash,
92-
'cygwin bash': cygwinBash,
93-
}).map(([name, bash]) => {
94-
let skip
95-
if (bash === cygwinBash && NYC_CONFIG) {
96-
skip = 'does not play nicely with NYC, run without coverage'
97-
} else {
94+
const spawn = (cmd, args, opts) => {
95+
const result = spawnSync(cmd, args, {
96+
// don't hit the registry for the update check
97+
env: { PATH: path, npm_config_update_notifier: 'false' },
98+
cwd: path,
99+
windowsHide: true,
100+
...opts,
101+
})
102+
result.stdout = result.stdout.toString().trim()
103+
result.stderr = result.stderr.toString().trim()
104+
return result
105+
}
106+
107+
for (const shim of Object.keys(SHIMS)) {
108+
chmodSync(join(path, shim), 0o755)
109+
}
110+
111+
const { ProgramFiles = '', SystemRoot = '', NYC_CONFIG, WINDOWS_SHIMS_TEST } = process.env
112+
const failOnMissing = WINDOWS_SHIMS_TEST === 'fail'
113+
const defaultSkip = process.platform === 'win32' ? null : 'test on relevant on windows'
114+
115+
const matchSpawn = (t, cmd, bin = '', { skip = defaultSkip, name } = {}) => {
116+
const testName = `${name || cmd} ${bin}`.trim()
117+
if (skip) {
118+
if (failOnMissing) {
119+
t.fail(testName)
120+
} else {
121+
t.skip(`${testName} - ${skip}`)
122+
}
123+
return
124+
}
125+
t.test(testName, t => {
126+
t.plan(1)
127+
const isNpm = testName.includes('npm')
128+
const binArg = isNpm ? 'help' : '--version'
129+
const args = []
130+
const opts = {}
131+
if (cmd.endsWith('.cmd')) {
132+
args.push(binArg)
133+
} else if (cmd === 'pwsh') {
134+
cmd = quoteWhich(cmd)
135+
args.push(`${bin}.ps1`, binArg)
136+
opts.shell = true
137+
} else if (cmd.endsWith('bash.exe')) {
138+
// only cygwin *requires* the -l, but the others are ok with it
139+
args.push('-l', bin, binArg)
140+
}
141+
t.match(spawn(cmd, args, opts), {
142+
status: 0,
143+
signal: null,
144+
stderr: '',
145+
stdout: isNpm ? `npm@${version} ${ROOT}` : version,
146+
}, 'command output is correct')
147+
})
148+
}
149+
150+
// ensure that all tests are either run or skipped
151+
t.plan(12)
152+
153+
matchSpawn(t, 'npm.cmd')
154+
matchSpawn(t, 'npx.cmd')
155+
matchSpawn(t, 'pwsh', 'npm')
156+
matchSpawn(t, 'pwsh', 'npx')
157+
158+
const bashes = [
159+
{ name: 'git', cmd: join(ProgramFiles, 'Git', 'bin', 'bash.exe') },
160+
{ name: 'user git', cmd: join(ProgramFiles, 'Git', 'usr', 'bin', 'bash.exe') },
161+
{ name: 'wsl', cmd: join(SystemRoot, 'System32', 'bash.exe') },
162+
{
163+
name: 'cygwin',
164+
cmd: join(SystemRoot, '/', 'cygwin64', 'bin', 'bash.exe'),
165+
skip: NYC_CONFIG ? 'does not play nicely with nyc' : undefined,
166+
},
167+
].map(({ name, cmd, skip = defaultSkip }) => {
168+
if (!skip) {
98169
try {
99170
// If WSL is installed, it *has* a bash.exe, but it fails if
100171
// there is no distro installed, so we need to detect that.
101-
if (spawnSync(bash, ['-l', '-c', 'exit 0']).status !== 0) {
172+
if (spawnSync(cmd, ['-l', '-c', 'exit 0']).status !== 0) {
102173
throw new Error('not installed')
103174
}
104-
} catch {
105-
skip = 'not installed'
175+
} catch (err) {
176+
skip = err.message
106177
}
107178
}
108-
return { name, bash, skip }
179+
return { cmd, skip, name: `${name} bash` }
109180
})
110181

111-
for (const { name, bash, skip } of bashes) {
112-
if (skip) {
113-
t.skip(name, { diagnostic: true, bin: bash, reason: skip })
114-
continue
115-
}
116-
117-
await t.test(name, async t => {
118-
const bins = Object.entries({
119-
// should have loaded this instance of npm we symlinked in
120-
npm: [['help'], `npm@${version} ${root}`],
121-
npx: [['--version'], version],
122-
})
123-
124-
for (const [binName, [cmdArgs, stdout]] of bins) {
125-
await t.test(binName, async t => {
126-
// only cygwin *requires* the -l, but the others are ok with it
127-
const args = ['-l', binName, ...cmdArgs]
128-
const result = await spawn(bash, args, {
129-
// don't hit the registry for the update check
130-
env: { PATH: path, npm_config_update_notifier: 'false' },
131-
cwd: path,
132-
})
133-
t.match(result, {
134-
cmd: bash,
135-
args: args,
136-
code: 0,
137-
signal: null,
138-
stderr: String,
139-
stdout,
140-
})
141-
})
142-
}
143-
})
182+
for (const { cmd, skip, name } of bashes) {
183+
matchSpawn(t, cmd, 'npm', { name, skip })
184+
matchSpawn(t, cmd, 'npx', { name, skip })
144185
}
145186
})

0 commit comments

Comments
 (0)