Rever concurrent changes as there are still race conditions
This commit is contained in:
parent
19c3ef65a2
commit
629b8b9efb
2 changed files with 260 additions and 257 deletions
|
|
@ -64,290 +64,293 @@ export function test(
|
|||
testCallback: TestCallback,
|
||||
{ only = false, debug = false }: TestFlags = {},
|
||||
) {
|
||||
return (
|
||||
only || (!process.env.CI && debug) ? defaultTest.only.concurrent : defaultTest.concurrent
|
||||
)(name, { timeout: TEST_TIMEOUT }, async (options) => {
|
||||
let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT
|
||||
await fs.mkdir(rootDir, { recursive: true })
|
||||
return (only || (!process.env.CI && debug) ? defaultTest.only : defaultTest)(
|
||||
name,
|
||||
{ timeout: TEST_TIMEOUT },
|
||||
async (options) => {
|
||||
let rootDir = debug ? path.join(REPO_ROOT, '.debug') : TMP_ROOT
|
||||
await fs.mkdir(rootDir, { recursive: true })
|
||||
|
||||
let root = await fs.mkdtemp(path.join(rootDir, 'tailwind-integrations'))
|
||||
let root = await fs.mkdtemp(path.join(rootDir, 'tailwind-integrations'))
|
||||
|
||||
if (debug) {
|
||||
console.log('Running test in debug mode. File system will be written to:')
|
||||
console.log(root)
|
||||
console.log()
|
||||
}
|
||||
if (debug) {
|
||||
console.log('Running test in debug mode. File system will be written to:')
|
||||
console.log(root)
|
||||
console.log()
|
||||
}
|
||||
|
||||
let context = {
|
||||
root,
|
||||
async exec(command: string, childProcessOptions: ChildProcessOptions = {}) {
|
||||
let cwd = childProcessOptions.cwd ?? root
|
||||
if (debug && cwd !== root) {
|
||||
let relative = path.relative(root, cwd)
|
||||
if (relative[0] !== '.') relative = `./${relative}`
|
||||
console.log(`> cd ${relative}`)
|
||||
}
|
||||
if (debug) console.log(`> ${command}`)
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
command,
|
||||
{
|
||||
cwd,
|
||||
...childProcessOptions,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(stderr)
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(stdout.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
},
|
||||
async spawn(command: string, childProcessOptions: ChildProcessOptions = {}) {
|
||||
let resolveDisposal: (() => void) | undefined
|
||||
let rejectDisposal: ((error: Error) => void) | undefined
|
||||
let disposePromise = new Promise<void>((resolve, reject) => {
|
||||
resolveDisposal = resolve
|
||||
rejectDisposal = reject
|
||||
})
|
||||
|
||||
let cwd = childProcessOptions.cwd ?? root
|
||||
if (debug && cwd !== root) {
|
||||
let relative = path.relative(root, cwd)
|
||||
if (relative[0] !== '.') relative = `./${relative}`
|
||||
console.log(`> cd ${relative}`)
|
||||
}
|
||||
if (debug) console.log(`>& ${command}`)
|
||||
let child = spawn(command, {
|
||||
cwd,
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
},
|
||||
...childProcessOptions,
|
||||
})
|
||||
|
||||
function dispose() {
|
||||
child.kill()
|
||||
|
||||
let timer = setTimeout(
|
||||
() => rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)),
|
||||
ASSERTION_TIMEOUT,
|
||||
)
|
||||
disposePromise.finally(() => {
|
||||
clearTimeout(timer)
|
||||
let context = {
|
||||
root,
|
||||
async exec(command: string, childProcessOptions: ChildProcessOptions = {}) {
|
||||
let cwd = childProcessOptions.cwd ?? root
|
||||
if (debug && cwd !== root) {
|
||||
let relative = path.relative(root, cwd)
|
||||
if (relative[0] !== '.') relative = `./${relative}`
|
||||
console.log(`> cd ${relative}`)
|
||||
}
|
||||
if (debug) console.log(`> ${command}`)
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(
|
||||
command,
|
||||
{
|
||||
cwd,
|
||||
...childProcessOptions,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(stderr)
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(stdout.toString())
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
},
|
||||
async spawn(command: string, childProcessOptions: ChildProcessOptions = {}) {
|
||||
let resolveDisposal: (() => void) | undefined
|
||||
let rejectDisposal: ((error: Error) => void) | undefined
|
||||
let disposePromise = new Promise<void>((resolve, reject) => {
|
||||
resolveDisposal = resolve
|
||||
rejectDisposal = reject
|
||||
})
|
||||
return disposePromise
|
||||
}
|
||||
disposables.push(dispose)
|
||||
function onExit() {
|
||||
resolveDisposal?.()
|
||||
}
|
||||
|
||||
let stdoutMessages: string[] = []
|
||||
let stderrMessages: string[] = []
|
||||
let cwd = childProcessOptions.cwd ?? root
|
||||
if (debug && cwd !== root) {
|
||||
let relative = path.relative(root, cwd)
|
||||
if (relative[0] !== '.') relative = `./${relative}`
|
||||
console.log(`> cd ${relative}`)
|
||||
}
|
||||
if (debug) console.log(`>& ${command}`)
|
||||
let child = spawn(command, {
|
||||
cwd,
|
||||
shell: true,
|
||||
env: {
|
||||
...process.env,
|
||||
},
|
||||
...childProcessOptions,
|
||||
})
|
||||
|
||||
let stdoutActors: SpawnActor[] = []
|
||||
let stderrActors: SpawnActor[] = []
|
||||
function dispose() {
|
||||
child.kill()
|
||||
|
||||
function notifyNext(actors: SpawnActor[], messages: string[]) {
|
||||
if (actors.length <= 0) return
|
||||
let [next] = actors
|
||||
let timer = setTimeout(
|
||||
() =>
|
||||
rejectDisposal?.(new Error(`spawned process (${command}) did not exit in time`)),
|
||||
ASSERTION_TIMEOUT,
|
||||
)
|
||||
disposePromise.finally(() => {
|
||||
clearTimeout(timer)
|
||||
})
|
||||
return disposePromise
|
||||
}
|
||||
disposables.push(dispose)
|
||||
function onExit() {
|
||||
resolveDisposal?.()
|
||||
}
|
||||
|
||||
for (let [idx, message] of messages.entries()) {
|
||||
if (next.predicate(message)) {
|
||||
messages.splice(0, idx + 1)
|
||||
let actorIdx = actors.indexOf(next)
|
||||
actors.splice(actorIdx, 1)
|
||||
next.resolve()
|
||||
break
|
||||
let stdoutMessages: string[] = []
|
||||
let stderrMessages: string[] = []
|
||||
|
||||
let stdoutActors: SpawnActor[] = []
|
||||
let stderrActors: SpawnActor[] = []
|
||||
|
||||
function notifyNext(actors: SpawnActor[], messages: string[]) {
|
||||
if (actors.length <= 0) return
|
||||
let [next] = actors
|
||||
|
||||
for (let [idx, message] of messages.entries()) {
|
||||
if (next.predicate(message)) {
|
||||
messages.splice(0, idx + 1)
|
||||
let actorIdx = actors.indexOf(next)
|
||||
actors.splice(actorIdx, 1)
|
||||
next.resolve()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let combined: ['stdout' | 'stderr', string][] = []
|
||||
let combined: ['stdout' | 'stderr', string][] = []
|
||||
|
||||
child.stdout.on('data', (result) => {
|
||||
let content = result.toString()
|
||||
if (debug) console.log(content)
|
||||
combined.push(['stdout', content])
|
||||
stdoutMessages.push(content)
|
||||
notifyNext(stdoutActors, stdoutMessages)
|
||||
})
|
||||
child.stderr.on('data', (result) => {
|
||||
let content = result.toString()
|
||||
if (debug) console.error(content)
|
||||
combined.push(['stderr', content])
|
||||
stderrMessages.push(content)
|
||||
notifyNext(stderrActors, stderrMessages)
|
||||
})
|
||||
child.on('exit', onExit)
|
||||
child.on('error', (error) => {
|
||||
if (error.name !== 'AbortError') {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
options.onTestFailed(() => {
|
||||
// In debug mode, messages are logged to the console immediately
|
||||
if (debug) return
|
||||
|
||||
for (let [type, message] of combined) {
|
||||
if (type === 'stdout') {
|
||||
console.log(message)
|
||||
} else {
|
||||
console.error(message)
|
||||
child.stdout.on('data', (result) => {
|
||||
let content = result.toString()
|
||||
if (debug) console.log(content)
|
||||
combined.push(['stdout', content])
|
||||
stdoutMessages.push(content)
|
||||
notifyNext(stdoutActors, stdoutMessages)
|
||||
})
|
||||
child.stderr.on('data', (result) => {
|
||||
let content = result.toString()
|
||||
if (debug) console.error(content)
|
||||
combined.push(['stderr', content])
|
||||
stderrMessages.push(content)
|
||||
notifyNext(stderrActors, stderrMessages)
|
||||
})
|
||||
child.on('exit', onExit)
|
||||
child.on('error', (error) => {
|
||||
if (error.name !== 'AbortError') {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
dispose,
|
||||
onStdout(predicate: (message: string) => boolean) {
|
||||
return new Promise<void>((resolve) => {
|
||||
stdoutActors.push({ predicate, resolve })
|
||||
notifyNext(stdoutActors, stdoutMessages)
|
||||
})
|
||||
},
|
||||
onStderr(predicate: (message: string) => boolean) {
|
||||
return new Promise<void>((resolve) => {
|
||||
stderrActors.push({ predicate, resolve })
|
||||
notifyNext(stderrActors, stderrMessages)
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
async getFreePort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let server = net.createServer()
|
||||
server.listen(0, () => {
|
||||
let address = server.address()
|
||||
let port = address === null || typeof address === 'string' ? null : address.port
|
||||
options.onTestFailed(() => {
|
||||
// In debug mode, messages are logged to the console immediately
|
||||
if (debug) return
|
||||
|
||||
server.close(() => {
|
||||
if (port === null) {
|
||||
reject(new Error(`Failed to get a free port: address is ${address}`))
|
||||
for (let [type, message] of combined) {
|
||||
if (type === 'stdout') {
|
||||
console.log(message)
|
||||
} else {
|
||||
disposables.push(async () => {
|
||||
// Wait for 10ms in case the process was just killed
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
console.error(message)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// kill-port uses `lsof` on macOS which is expensive and can
|
||||
// block for multiple seconds. In order to avoid that for a
|
||||
// server that is no longer running, we check if the port is
|
||||
// still in use first.
|
||||
let isPortTaken = await testIfPortTaken(port)
|
||||
if (!isPortTaken) {
|
||||
return
|
||||
}
|
||||
return {
|
||||
dispose,
|
||||
onStdout(predicate: (message: string) => boolean) {
|
||||
return new Promise<void>((resolve) => {
|
||||
stdoutActors.push({ predicate, resolve })
|
||||
notifyNext(stdoutActors, stdoutMessages)
|
||||
})
|
||||
},
|
||||
onStderr(predicate: (message: string) => boolean) {
|
||||
return new Promise<void>((resolve) => {
|
||||
stderrActors.push({ predicate, resolve })
|
||||
notifyNext(stderrActors, stderrMessages)
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
async getFreePort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let server = net.createServer()
|
||||
server.listen(0, () => {
|
||||
let address = server.address()
|
||||
let port = address === null || typeof address === 'string' ? null : address.port
|
||||
|
||||
try {
|
||||
await killPort(port)
|
||||
} catch {
|
||||
// If the process can not be killed, we can't do anything
|
||||
}
|
||||
})
|
||||
resolve(port)
|
||||
server.close(() => {
|
||||
if (port === null) {
|
||||
reject(new Error(`Failed to get a free port: address is ${address}`))
|
||||
} else {
|
||||
disposables.push(async () => {
|
||||
// Wait for 10ms in case the process was just killed
|
||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
||||
|
||||
// kill-port uses `lsof` on macOS which is expensive and can
|
||||
// block for multiple seconds. In order to avoid that for a
|
||||
// server that is no longer running, we check if the port is
|
||||
// still in use first.
|
||||
let isPortTaken = await testIfPortTaken(port)
|
||||
if (!isPortTaken) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await killPort(port)
|
||||
} catch {
|
||||
// If the process can not be killed, we can't do anything
|
||||
}
|
||||
})
|
||||
resolve(port)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
fs: {
|
||||
async write(filename: string, content: string): Promise<void> {
|
||||
let full = path.join(root, filename)
|
||||
|
||||
if (filename.endsWith('package.json')) {
|
||||
content = await overwriteVersionsInPackageJson(content)
|
||||
}
|
||||
|
||||
// Ensure that files written on Windows use \r\n line ending
|
||||
if (IS_WINDOWS) {
|
||||
content = content.replace(/\n/g, '\r\n')
|
||||
}
|
||||
|
||||
let dir = path.dirname(full)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await fs.writeFile(full, content)
|
||||
},
|
||||
async read(filePath: string) {
|
||||
let content = await fs.readFile(path.resolve(root, filePath), 'utf8')
|
||||
|
||||
// Ensure that files read on Windows have \r\n line endings removed
|
||||
if (IS_WINDOWS) {
|
||||
content = content.replace(/\r\n/g, '\n')
|
||||
}
|
||||
|
||||
return content
|
||||
},
|
||||
async glob(pattern: string) {
|
||||
let files = await fastGlob(pattern, { cwd: root })
|
||||
return Promise.all(
|
||||
files.map(async (file) => {
|
||||
let content = await fs.readFile(path.join(root, file), 'utf8')
|
||||
return [file, content]
|
||||
}),
|
||||
)
|
||||
},
|
||||
async expectFileToContain(filePath, contents) {
|
||||
return retryAssertion(async () => {
|
||||
let fileContent = await this.read(filePath)
|
||||
for (let content of contents) {
|
||||
expect(fileContent).toContain(content)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
fs: {
|
||||
async write(filename: string, content: string): Promise<void> {
|
||||
let full = path.join(root, filename)
|
||||
|
||||
if (filename.endsWith('package.json')) {
|
||||
content = await overwriteVersionsInPackageJson(content)
|
||||
}
|
||||
|
||||
// Ensure that files written on Windows use \r\n line ending
|
||||
if (IS_WINDOWS) {
|
||||
content = content.replace(/\n/g, '\r\n')
|
||||
}
|
||||
|
||||
let dir = path.dirname(full)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await fs.writeFile(full, content)
|
||||
},
|
||||
async expectFileNotToContain(filePath, contents) {
|
||||
return retryAssertion(async () => {
|
||||
let fileContent = await this.read(filePath)
|
||||
for (let content of contents) {
|
||||
expect(fileContent).not.toContain(content)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
async read(filePath: string) {
|
||||
let content = await fs.readFile(path.resolve(root, filePath), 'utf8')
|
||||
} satisfies TestContext
|
||||
|
||||
// Ensure that files read on Windows have \r\n line endings removed
|
||||
if (IS_WINDOWS) {
|
||||
content = content.replace(/\r\n/g, '\n')
|
||||
}
|
||||
|
||||
return content
|
||||
},
|
||||
async glob(pattern: string) {
|
||||
let files = await fastGlob(pattern, { cwd: root })
|
||||
return Promise.all(
|
||||
files.map(async (file) => {
|
||||
let content = await fs.readFile(path.join(root, file), 'utf8')
|
||||
return [file, content]
|
||||
}),
|
||||
)
|
||||
},
|
||||
async expectFileToContain(filePath, contents) {
|
||||
return retryAssertion(async () => {
|
||||
let fileContent = await this.read(filePath)
|
||||
for (let content of contents) {
|
||||
expect(fileContent).toContain(content)
|
||||
}
|
||||
})
|
||||
},
|
||||
async expectFileNotToContain(filePath, contents) {
|
||||
return retryAssertion(async () => {
|
||||
let fileContent = await this.read(filePath)
|
||||
for (let content of contents) {
|
||||
expect(fileContent).not.toContain(content)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
} satisfies TestContext
|
||||
|
||||
config.fs['.gitignore'] ??= txt`
|
||||
config.fs['.gitignore'] ??= txt`
|
||||
node_modules/
|
||||
`
|
||||
|
||||
for (let [filename, content] of Object.entries(config.fs)) {
|
||||
await context.fs.write(filename, content)
|
||||
}
|
||||
|
||||
try {
|
||||
// In debug mode, the directory is going to be inside the pnpm workspace
|
||||
// of the tailwindcss package. This means that `pnpm install` will run
|
||||
// pnpm install on the workspace instead (expect if the root dir defines
|
||||
// a separate workspace). We work around this by using the
|
||||
// `--ignore-workspace` flag.
|
||||
let ignoreWorkspace = debug && !config.fs['pnpm-workspace.yaml']
|
||||
await context.exec(`pnpm install${ignoreWorkspace ? ' --ignore-workspace' : ''}`)
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
console.error(error.stdout?.toString())
|
||||
console.error(error.stderr?.toString())
|
||||
throw error
|
||||
}
|
||||
|
||||
let disposables: (() => Promise<void>)[] = []
|
||||
|
||||
async function dispose() {
|
||||
await Promise.all(disposables.map((dispose) => dispose()))
|
||||
|
||||
if (!debug) {
|
||||
await gracefullyRemove(root)
|
||||
for (let [filename, content] of Object.entries(config.fs)) {
|
||||
await context.fs.write(filename, content)
|
||||
}
|
||||
}
|
||||
|
||||
options.onTestFinished(dispose)
|
||||
try {
|
||||
// In debug mode, the directory is going to be inside the pnpm workspace
|
||||
// of the tailwindcss package. This means that `pnpm install` will run
|
||||
// pnpm install on the workspace instead (expect if the root dir defines
|
||||
// a separate workspace). We work around this by using the
|
||||
// `--ignore-workspace` flag.
|
||||
let ignoreWorkspace = debug && !config.fs['pnpm-workspace.yaml']
|
||||
await context.exec(`pnpm install${ignoreWorkspace ? ' --ignore-workspace' : ''}`)
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
console.error(error.stdout?.toString())
|
||||
console.error(error.stderr?.toString())
|
||||
throw error
|
||||
}
|
||||
|
||||
return await testCallback(context)
|
||||
})
|
||||
let disposables: (() => Promise<void>)[] = []
|
||||
|
||||
async function dispose() {
|
||||
await Promise.all(disposables.map((dispose) => dispose()))
|
||||
|
||||
if (!debug) {
|
||||
await gracefullyRemove(root)
|
||||
}
|
||||
}
|
||||
|
||||
options.onTestFinished(dispose)
|
||||
|
||||
return await testCallback(context)
|
||||
},
|
||||
)
|
||||
}
|
||||
test.only = (name: string, config: TestConfig, testCallback: TestCallback) => {
|
||||
return test(name, config, testCallback, { only: true })
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
yaml,
|
||||
} from '../utils'
|
||||
;['postcss', 'lightningcss'].forEach((transformer) => {
|
||||
describe.concurrent(transformer, () => {
|
||||
describe(transformer, () => {
|
||||
test(
|
||||
`production build`,
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue