From 629b8b9efbf71501ea2ddd8f95c4b3645df7239c Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Wed, 28 Aug 2024 19:02:54 +0200 Subject: [PATCH] Rever concurrent changes as there are still race conditions --- integrations/utils.ts | 515 ++++++++++++++++---------------- integrations/vite/index.test.ts | 2 +- 2 files changed, 260 insertions(+), 257 deletions(-) diff --git a/integrations/utils.ts b/integrations/utils.ts index fbbe9a2dc..246f49375 100644 --- a/integrations/utils.ts +++ b/integrations/utils.ts @@ -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((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((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((resolve) => { - stdoutActors.push({ predicate, resolve }) - notifyNext(stdoutActors, stdoutMessages) - }) - }, - onStderr(predicate: (message: string) => boolean) { - return new Promise((resolve) => { - stderrActors.push({ predicate, resolve }) - notifyNext(stderrActors, stderrMessages) - }) - }, - } - }, - async getFreePort(): Promise { - 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((resolve) => { + stdoutActors.push({ predicate, resolve }) + notifyNext(stdoutActors, stdoutMessages) + }) + }, + onStderr(predicate: (message: string) => boolean) { + return new Promise((resolve) => { + stderrActors.push({ predicate, resolve }) + notifyNext(stderrActors, stderrMessages) + }) + }, + } + }, + async getFreePort(): Promise { + 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 { + 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 { - 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)[] = [] - - 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)[] = [] + + 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 }) diff --git a/integrations/vite/index.test.ts b/integrations/vite/index.test.ts index ba9440421..1852c6823 100644 --- a/integrations/vite/index.test.ts +++ b/integrations/vite/index.test.ts @@ -13,7 +13,7 @@ import { yaml, } from '../utils' ;['postcss', 'lightningcss'].forEach((transformer) => { - describe.concurrent(transformer, () => { + describe(transformer, () => { test( `production build`, {