Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 107 additions & 9 deletions scripts/__tests__/lint-no-hardcoded-runners.test.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execFileSync } from 'node:child_process'
import { spawnSync } from 'node:child_process'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
Expand All @@ -11,21 +11,82 @@ const SCRIPT = resolve(
)

function runScript(script, ...targets) {
try {
execFileSync(process.execPath, [script, ...targets], { encoding: 'utf8' })
return { exitCode: 0, output: '' }
} catch (err) {
return {
exitCode: err.status,
output: String(err.stdout) + String(err.stderr),
}
const result = spawnSync(process.execPath, [script, ...targets], {
encoding: 'utf8',
})
return {
exitCode: result.status,
output: String(result.stdout) + String(result.stderr),
}
}

function run(target) {
return runScript(SCRIPT, target)
}

function runWithReaddirError(code, { afterParent = false } = {}) {
const probe = resolve(
fileURLToPath(import.meta.url),
'../../walk-error-probe.mjs',
)
const dir = mkdtempSync(join(tmpdir(), 'lint-hardcoded-runners-walk-'))
const replacement = afterParent
? `let readdirCalls = 0
async function readdir() {
readdirCalls += 1
if (readdirCalls === 1) {
return [{ name: 'vanished', isDirectory: () => true }]
}
const err = new Error('readdir failed')
err.code = '${code}'
throw err
}`
: `async function readdir() { const err = new Error('readdir failed'); err.code = '${code}'; throw err }`
const src = readFileSync(SCRIPT, 'utf8').replace(
"import { readdir } from 'node:fs/promises'",
replacement,
)
try {
writeFileSync(probe, src)
return runScript(probe, dir)
} finally {
rmSync(probe, { force: true })
rmSync(dir, { recursive: true, force: true })
}
}

function runWithReadFileError(code) {
const probe = resolve(
fileURLToPath(import.meta.url),
'../../read-error-probe.mjs',
)
const dir = mkdtempSync(join(tmpdir(), 'lint-hardcoded-runners-read-'))
const src = readFileSync(SCRIPT, 'utf8')
.replace(
"import { readFileSync, statSync } from 'node:fs'",
`import { readFileSync as realReadFileSync, statSync } from 'node:fs'
function readFileSync(path, ...args) {
if (String(path).endsWith('vanished.ts')) {
const err = new Error('read failed')
err.code = '${code}'
throw err
}
return realReadFileSync(path, ...args)
}`,
)
.replace(
"import { readdir } from 'node:fs/promises'",
"async function readdir() { return [{ name: 'vanished.ts', isDirectory: () => false }] }",
)
try {
writeFileSync(probe, src)
return runScript(probe, dir)
} finally {
rmSync(probe, { force: true })
rmSync(dir, { recursive: true, force: true })
}
}

describe('lint-no-hardcoded-runners', () => {
const fx = (name) =>
resolve(fileURLToPath(import.meta.url), `../fixtures/${name}`)
Expand All @@ -34,6 +95,19 @@ describe('lint-no-hardcoded-runners', () => {
expect(run(fx('clean.ts')).exitCode).toBe(0)
})

it('captures stderr from a successful script', () => {
const dir = mkdtempSync(join(tmpdir(), 'lint-hardcoded-runners-stderr-'))
const probe = join(dir, 'stderr-probe.mjs')
try {
writeFileSync(probe, "process.stderr.write('successful warning\\n')\n")
const r = runScript(probe)
expect(r.exitCode).toBe(0)
expect(r.output).toContain('successful warning')
} finally {
rmSync(dir, { recursive: true, force: true })
}
})

it('fails on a hardcoded `npx ...` string literal', () => {
const r = run(fx('offender.ts'))
expect(r.exitCode).toBe(1)
Expand All @@ -51,6 +125,30 @@ describe('lint-no-hardcoded-runners', () => {
expect(r.output).not.toMatch(/at ModuleJob/)
})

it('skips a directory that vanished after its parent was enumerated', () => {
const r = runWithReaddirError('ENOENT', { afterParent: true })
expect(r.exitCode).toBe(0)
expect(r.output).toContain('OK')
})

it('rethrows unexpected errors encountered while walking', () => {
const r = runWithReaddirError('EACCES')
expect(r.exitCode).not.toBe(0)
expect(r.output).toContain('EACCES')
})

it('skips a file that vanished after its directory was enumerated', () => {
const r = runWithReadFileError('ENOENT')
expect(r.exitCode).toBe(0)
expect(r.output).toContain('OK')
})

it('rethrows unexpected errors reading an enumerated file', () => {
const r = runWithReadFileError('EACCES')
expect(r.exitCode).not.toBe(0)
expect(r.output).toContain('EACCES')
})

// Matches the sibling linter: a target outside the repo rendered as a
// `../../../../..` chain out of the root instead of naming the file.
it('renders an absolute path for an offender outside the repo root', () => {
Expand Down
8 changes: 8 additions & 0 deletions scripts/__tests__/vitest-config.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { describe, expect, it } from 'vitest'
import scriptsVitestConfig from '../vitest.config.mjs'

describe('scripts vitest config', () => {
it('keeps script test files serialized', () => {
expect(scriptsVitestConfig.test.fileParallelism).toBe(false)
})
})
12 changes: 11 additions & 1 deletion scripts/lint-no-hardcoded-runners.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,17 @@ for (const target of TARGETS) {
const shown = rel.startsWith('..') ? file : rel
if (ALLOWLISTED_PATHS.has(rel)) continue
if (/\.(test|spec)\.(ts|tsx|mts|cts)$/.test(file)) continue
const lines = readFileSync(file, 'utf8').split('\n')
let source
try {
source = readFileSync(file, 'utf8')
} catch (err) {
// A file can vanish after its directory was enumerated, just as a
// directory can vanish before the recursive readdir above. There is
// nothing left to lint; unexpected read failures must still surface.
if (err?.code === 'ENOENT') continue
throw err
}
const lines = source.split('\n')
lines.forEach((line, idx) => {
const matches = NPX_TOKEN.test(line)
if (!matches) return
Expand Down