From daa14ab3e2b2ae8d78826872e691b2a375685e47 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Fri, 16 Dec 2022 00:42:42 -0800 Subject: [PATCH 1/2] Remove extra turbo test (#44073) These tests are run via an env variable now instead so we can remove the hard coded handling ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) --- test/integration/css-modules/test/index.test.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/integration/css-modules/test/index.test.js b/test/integration/css-modules/test/index.test.js index dca5629b8dd271f..2750db1535f9009 100644 --- a/test/integration/css-modules/test/index.test.js +++ b/test/integration/css-modules/test/index.test.js @@ -130,10 +130,7 @@ describe('3rd Party CSS Module Support', () => { }) }) -describe.each([ - ['dev', false], - ['turbo', true], -])('Has CSS Module in computed styles in Development %s', (turbo) => { +describe('Has CSS Module in computed styles in Development', () => { const appDir = join(fixturesDir, 'dev-module') let appPort @@ -141,7 +138,7 @@ describe.each([ beforeAll(async () => { await remove(join(appDir, '.next')) appPort = await findPort() - app = await launchApp(appDir, appPort, { turbo }) + app = await launchApp(appDir, appPort) }) afterAll(async () => { await killApp(app) From ab328c6c39106c1a8a87faa5917dff70811ecdf0 Mon Sep 17 00:00:00 2001 From: Jan Kaifer Date: Fri, 16 Dec 2022 09:58:04 +0100 Subject: [PATCH 2/2] Add tracing for testing tools (#44046) --- .../actions/next-stats-action/src/index.js | 8 +- .../src/prepare/repo-setup.js | 185 ++++++------ .gitignore | 1 + contributing/core/testing.md | 4 + packages/next/trace/trace.ts | 8 +- run-tests.js | 12 +- scripts/trace-next-server.js | 2 +- test/e2e/test-utils-tests/basic/basic.test.ts | 24 ++ .../test-utils-tests/basic/pages/index.tsx | 3 + test/lib/create-next-install.js | 224 ++++++++------- test/lib/e2e-utils.ts | 87 ++++-- test/lib/next-modes/base.ts | 263 ++++++++++-------- test/lib/next-modes/next-deploy.ts | 5 +- test/lib/next-modes/next-dev.ts | 5 +- test/lib/next-modes/next-start.ts | 5 +- 15 files changed, 496 insertions(+), 340 deletions(-) create mode 100644 test/e2e/test-utils-tests/basic/basic.test.ts create mode 100644 test/e2e/test-utils-tests/basic/pages/index.tsx diff --git a/.github/actions/next-stats-action/src/index.js b/.github/actions/next-stats-action/src/index.js index be9284e0c273725..f66ad587c69f27c 100644 --- a/.github/actions/next-stats-action/src/index.js +++ b/.github/actions/next-stats-action/src/index.js @@ -135,10 +135,10 @@ if (!allowedActions.has(actionInfo.actionName) && !actionInfo.isRelease) { logger(`Linking packages in ${dir}`) const isMainRepo = dir === mainRepoDir - const pkgPaths = await linkPackages( - dir, - isMainRepo ? mainNextSwcVersion : undefined - ) + const pkgPaths = await linkPackages({ + repoDir: dir, + nextSwcPkg: isMainRepo ? mainNextSwcVersion : undefined, + }) if (isMainRepo) mainRepoPkgPaths = pkgPaths else diffRepoPkgPaths = pkgPaths diff --git a/.github/actions/next-stats-action/src/prepare/repo-setup.js b/.github/actions/next-stats-action/src/prepare/repo-setup.js index 493644727d6ec98..3034d47fc1747ac 100644 --- a/.github/actions/next-stats-action/src/prepare/repo-setup.js +++ b/.github/actions/next-stats-action/src/prepare/repo-setup.js @@ -5,6 +5,11 @@ const { remove } = require('fs-extra') const logger = require('../util/logger') const semver = require('semver') +const mockTrace = () => ({ + traceAsyncFn: (fn) => fn(mockTrace()), + traceChild: () => mockTrace(), +}) + module.exports = (actionInfo) => { return { async cloneRepo(repoPath = '', dest = '') { @@ -53,93 +58,115 @@ module.exports = (actionInfo) => { } } }, - async linkPackages(repoDir = '', nextSwcPkg) { - const pkgPaths = new Map() - const pkgDatas = new Map() - let pkgs + async linkPackages({ repoDir = '', nextSwcPkg, parentSpan }) { + const rootSpan = parentSpan + ? parentSpan.traceChild('linkPackages') + : mockTrace() - try { - pkgs = await fs.readdir(path.join(repoDir, 'packages')) - } catch (err) { - if (err.code === 'ENOENT') { - console.log('no packages to link') - return pkgPaths + return await rootSpan.traceAsyncFn(async () => { + const pkgPaths = new Map() + const pkgDatas = new Map() + let pkgs + + try { + pkgs = await fs.readdir(path.join(repoDir, 'packages')) + } catch (err) { + if (err.code === 'ENOENT') { + console.log('no packages to link') + return pkgPaths + } + throw err } - throw err - } - for (const pkg of pkgs) { - const pkgPath = path.join(repoDir, 'packages', pkg) - const packedPkgPath = path.join(pkgPath, `${pkg}-packed.tgz`) + await rootSpan + .traceChild('prepare packages for packing') + .traceAsyncFn(async () => { + for (const pkg of pkgs) { + const pkgPath = path.join(repoDir, 'packages', pkg) + const packedPkgPath = path.join(pkgPath, `${pkg}-packed.tgz`) - const pkgDataPath = path.join(pkgPath, 'package.json') - if (!fs.existsSync(pkgDataPath)) { - console.log(`Skipping ${pkgDataPath}`) - continue - } - const pkgData = require(pkgDataPath) - const { name } = pkgData - pkgDatas.set(name, { - pkgDataPath, - pkg, - pkgPath, - pkgData, - packedPkgPath, - }) - pkgPaths.set(name, packedPkgPath) - } + const pkgDataPath = path.join(pkgPath, 'package.json') + if (!fs.existsSync(pkgDataPath)) { + console.log(`Skipping ${pkgDataPath}`) + continue + } + const pkgData = require(pkgDataPath) + const { name } = pkgData + pkgDatas.set(name, { + pkgDataPath, + pkg, + pkgPath, + pkgData, + packedPkgPath, + }) + pkgPaths.set(name, packedPkgPath) + } - for (const pkg of pkgDatas.keys()) { - const { pkgDataPath, pkgData } = pkgDatas.get(pkg) + for (const pkg of pkgDatas.keys()) { + const { pkgDataPath, pkgData } = pkgDatas.get(pkg) - for (const pkg of pkgDatas.keys()) { - const { packedPkgPath } = pkgDatas.get(pkg) - if (!pkgData.dependencies || !pkgData.dependencies[pkg]) continue - pkgData.dependencies[pkg] = packedPkgPath - } - // make sure native binaries are included in local linking - if (pkg === '@next/swc') { - if (!pkgData.files) { - pkgData.files = [] - } - pkgData.files.push('native') - console.log( - 'using swc binaries: ', - await exec(`ls ${path.join(path.dirname(pkgDataPath), 'native')}`) - ) - } - if (pkg === 'next') { - if (nextSwcPkg) { - Object.assign(pkgData.dependencies, nextSwcPkg) - } else { - if (pkgDatas.get('@next/swc')) { - pkgData.dependencies['@next/swc'] = - pkgDatas.get('@next/swc').packedPkgPath - } else { - pkgData.files.push('native') + for (const pkg of pkgDatas.keys()) { + const { packedPkgPath } = pkgDatas.get(pkg) + if (!pkgData.dependencies || !pkgData.dependencies[pkg]) + continue + pkgData.dependencies[pkg] = packedPkgPath + } + + // make sure native binaries are included in local linking + if (pkg === '@next/swc') { + if (!pkgData.files) { + pkgData.files = [] + } + pkgData.files.push('native') + console.log( + 'using swc binaries: ', + await exec( + `ls ${path.join(path.dirname(pkgDataPath), 'native')}` + ) + ) + } + + if (pkg === 'next') { + if (nextSwcPkg) { + Object.assign(pkgData.dependencies, nextSwcPkg) + } else { + if (pkgDatas.get('@next/swc')) { + pkgData.dependencies['@next/swc'] = + pkgDatas.get('@next/swc').packedPkgPath + } else { + pkgData.files.push('native') + } + } + } + + await fs.writeFile( + pkgDataPath, + JSON.stringify(pkgData, null, 2), + 'utf8' + ) } - } - } - await fs.writeFile( - pkgDataPath, - JSON.stringify(pkgData, null, 2), - 'utf8' - ) - } + }) - // wait to pack packages until after dependency paths have been updated - // to the correct versions - for (const pkgName of pkgDatas.keys()) { - const { pkg, pkgPath } = pkgDatas.get(pkgName) - await exec(`cd ${pkgPath} && yarn pack -f ${pkg}-packed.tgz`, true, { - env: { - // Yarn installed through corepack will not run in pnpm project without this env var set - // This var works for corepack >=0.15.0 - COREPACK_ENABLE_STRICT: '0', - }, - }) - } - return pkgPaths + // wait to pack packages until after dependency paths have been updated + // to the correct versions + await rootSpan + .traceChild('packing packages') + .traceAsyncFn(async (packingSpan) => { + for (const pkgName of pkgDatas.keys()) { + await packingSpan + .traceChild(`pack ${pkgName}`) + .traceAsyncFn(async () => { + const { pkg, pkgPath } = pkgDatas.get(pkgName) + await exec( + `cd ${pkgPath} && yarn pack -f '${pkg}-packed.tgz'`, + true + ) + }) + } + }) + + return pkgPaths + }) }, } } diff --git a/.gitignore b/.gitignore index 0474f7c1883a746..6bbbba1173fd1c3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ test/**/tsconfig.json .DS_Store /e2e-tests test/tmp/** +test/.trace # Editors **/.idea diff --git a/contributing/core/testing.md b/contributing/core/testing.md index 70eca377e49b7ac..6025437f3cc4af6 100644 --- a/contributing/core/testing.md +++ b/contributing/core/testing.md @@ -76,3 +76,7 @@ Some test-specific environment variables can be used to help debug isolated test ### Debugging When tests are run in CI and a test failure occurs we attempt to capture traces of the playwright run to make debugging the failure easier. A test-trace artifact should be uploaded after the workflow completes which can be downloaded, unzipped, and then inspected with `pnpm playwright show-trace ./path/to/trace` + +### Profiling tests + +Add `NEXT_TEST_TRACE=1` to enable test profiling. It's useful for improving our testing infrastructure. diff --git a/packages/next/trace/trace.ts b/packages/next/trace/trace.ts index b14b99ce2671300..67bcb1ee1bb7848 100644 --- a/packages/next/trace/trace.ts +++ b/packages/next/trace/trace.ts @@ -93,17 +93,17 @@ export class Span { this.attrs[key] = String(value) } - traceFn(fn: () => T): T { + traceFn(fn: (span: Span) => T): T { try { - return fn() + return fn(this) } finally { this.stop() } } - async traceAsyncFn(fn: () => T | Promise): Promise { + async traceAsyncFn(fn: (span: Span) => T | Promise): Promise { try { - return await fn() + return await fn(this) } finally { this.stop() } diff --git a/run-tests.js b/run-tests.js index f82c2eed606651b..687c9702e72f407 100644 --- a/run-tests.js +++ b/run-tests.js @@ -29,6 +29,11 @@ const testFilters = { development: 'development/', } +const mockTrace = () => ({ + traceAsyncFn: (fn) => fn(mockTrace()), + traceChild: () => mockTrace(), +}) + // which types we have configured to run separate const configuredTestTypes = Object.values(testFilters) @@ -223,8 +228,11 @@ async function main() { console.log('Creating Next.js install for isolated tests') const reactVersion = process.env.NEXT_TEST_REACT_VERSION || 'latest' const testStarter = await createNextInstall({ - react: reactVersion, - 'react-dom': reactVersion, + parentSpan: mockTrace(), + dependencies: { + react: reactVersion, + 'react-dom': reactVersion, + }, }) process.env.NEXT_TEST_STARTER = testStarter } diff --git a/scripts/trace-next-server.js b/scripts/trace-next-server.js index bf49c6ade8f59a6..e2d8659e9a07245 100644 --- a/scripts/trace-next-server.js +++ b/scripts/trace-next-server.js @@ -38,7 +38,7 @@ async function main() { console.log('using repodir', repoDir) await fs.ensureDir(workDir) - const pkgPaths = await linkPackages(repoDir) + const pkgPaths = await linkPackages({ repoDir }) await fs.writeFile( path.join(workDir, 'package.json'), diff --git a/test/e2e/test-utils-tests/basic/basic.test.ts b/test/e2e/test-utils-tests/basic/basic.test.ts new file mode 100644 index 000000000000000..e37b2145639ceaf --- /dev/null +++ b/test/e2e/test-utils-tests/basic/basic.test.ts @@ -0,0 +1,24 @@ +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { fetchViaHTTP } from 'next-test-utils' + +describe('createNext', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(__dirname), + dependencies: { + typescript: 'latest', + '@types/react': 'latest', + '@types/node': 'latest', + }, + }) + }) + afterAll(() => next.destroy()) + + it('should work', async () => { + const res = await fetchViaHTTP(next.url, '/') + expect(await res.text()).toContain('Hello World') + }) +}) diff --git a/test/e2e/test-utils-tests/basic/pages/index.tsx b/test/e2e/test-utils-tests/basic/pages/index.tsx new file mode 100644 index 000000000000000..15a9b6e44f614ce --- /dev/null +++ b/test/e2e/test-utils-tests/basic/pages/index.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

Hello World

+} diff --git a/test/lib/create-next-install.js b/test/lib/create-next-install.js index 2f1e222d79330a6..1291b5db1e12028 100644 --- a/test/lib/create-next-install.js +++ b/test/lib/create-next-install.js @@ -7,115 +7,151 @@ const { randomBytes } = require('crypto') const { linkPackages } = require('../../.github/actions/next-stats-action/src/prepare/repo-setup')() -async function createNextInstall( +async function createNextInstall({ + parentSpan, dependencies, installCommand, packageJson = {}, packageLockPath = '', - dirSuffix = '' -) { - const tmpDir = await fs.realpath(process.env.NEXT_TEST_DIR || os.tmpdir()) - const origRepoDir = path.join(__dirname, '../../') - const installDir = path.join( - tmpDir, - `next-install-${randomBytes(32).toString('hex')}${dirSuffix}` - ) - const tmpRepoDir = path.join( - tmpDir, - `next-repo-${randomBytes(32).toString('hex')}${dirSuffix}` - ) + dirSuffix = '', +}) { + return await parentSpan + .traceChild('createNextInstall') + .traceAsyncFn(async (rootSpan) => { + const tmpDir = await fs.realpath(process.env.NEXT_TEST_DIR || os.tmpdir()) + const origRepoDir = path.join(__dirname, '../../') + const installDir = path.join( + tmpDir, + `next-install-${randomBytes(32).toString('hex')}${dirSuffix}` + ) + const tmpRepoDir = path.join( + tmpDir, + `next-repo-${randomBytes(32).toString('hex')}${dirSuffix}` + ) - // ensure swc binary is present in the native folder if - // not already built - for (const folder of await fs.readdir( - path.join(origRepoDir, 'node_modules/@next') - )) { - if (folder.startsWith('swc-')) { - const swcPkgPath = path.join(origRepoDir, 'node_modules/@next', folder) - const outputPath = path.join(origRepoDir, 'packages/next-swc/native') - await fs.copy(swcPkgPath, outputPath, { - filter: (item) => { - return ( - item === swcPkgPath || - (item.endsWith('.node') && - !fs.pathExistsSync(path.join(outputPath, path.basename(item)))) - ) - }, + await rootSpan.traceChild(' enruse swc binary').traceAsyncFn(async () => { + // ensure swc binary is present in the native folder if + // not already built + for (const folder of await fs.readdir( + path.join(origRepoDir, 'node_modules/@next') + )) { + if (folder.startsWith('swc-')) { + const swcPkgPath = path.join( + origRepoDir, + 'node_modules/@next', + folder + ) + const outputPath = path.join( + origRepoDir, + 'packages/next-swc/native' + ) + await fs.copy(swcPkgPath, outputPath, { + filter: (item) => { + return ( + item === swcPkgPath || + (item.endsWith('.node') && + !fs.pathExistsSync( + path.join(outputPath, path.basename(item)) + )) + ) + }, + }) + } + } }) - } - } - for (const item of ['package.json', 'packages']) { - await fs.copy(path.join(origRepoDir, item), path.join(tmpRepoDir, item), { - filter: (item) => { - return ( - !item.includes('node_modules') && - !item.includes('.DS_Store') && - // Exclude Rust compilation files - !/next[\\/]build[\\/]swc[\\/]target/.test(item) && - !/next-swc[\\/]target/.test(item) - ) - }, - }) - } + for (const item of ['package.json', 'packages']) { + await rootSpan + .traceChild(`copy ${item} to temp dir`) + .traceAsyncFn(async () => { + await fs.copy( + path.join(origRepoDir, item), + path.join(tmpRepoDir, item), + { + filter: (item) => { + return ( + !item.includes('node_modules') && + !item.includes('.DS_Store') && + // Exclude Rust compilation files + !/next[\\/]build[\\/]swc[\\/]target/.test(item) && + !/next-swc[\\/]target/.test(item) + ) + }, + } + ) + }) + } - let combinedDependencies = dependencies + let combinedDependencies = dependencies - if (!(packageJson && packageJson.nextPrivateSkipLocalDeps)) { - const pkgPaths = await linkPackages(tmpRepoDir) - combinedDependencies = { - next: pkgPaths.get('next'), - ...Object.keys(dependencies).reduce((prev, pkg) => { - const pkgPath = pkgPaths.get(pkg) - prev[pkg] = pkgPath || dependencies[pkg] - return prev - }, {}), - } - } + if (!(packageJson && packageJson.nextPrivateSkipLocalDeps)) { + const pkgPaths = await linkPackages({ + repoDir: tmpRepoDir, + parentSpan: rootSpan, + }) + combinedDependencies = { + next: pkgPaths.get('next'), + ...Object.keys(dependencies).reduce((prev, pkg) => { + const pkgPath = pkgPaths.get(pkg) + prev[pkg] = pkgPath || dependencies[pkg] + return prev + }, {}), + } + } - await fs.ensureDir(installDir) - await fs.writeFile( - path.join(installDir, 'package.json'), - JSON.stringify( - { - ...packageJson, - dependencies: combinedDependencies, - private: true, - }, - null, - 2 - ) - ) + await fs.ensureDir(installDir) + await fs.writeFile( + path.join(installDir, 'package.json'), + JSON.stringify( + { + ...packageJson, + dependencies: combinedDependencies, + private: true, + }, + null, + 2 + ) + ) - if (packageLockPath) { - await fs.copy( - packageLockPath, - path.join(installDir, path.basename(packageLockPath)) - ) - } + if (packageLockPath) { + await fs.copy( + packageLockPath, + path.join(installDir, path.basename(packageLockPath)) + ) + } - if (installCommand) { - const installString = - typeof installCommand === 'function' - ? installCommand({ dependencies: combinedDependencies }) - : installCommand + if (installCommand) { + const installString = + typeof installCommand === 'function' + ? installCommand({ dependencies: combinedDependencies }) + : installCommand - console.log('running install command', installString) + console.log('running install command', installString) + rootSpan.traceChild('run custom install').traceFn(() => { + childProcess.execSync(installString, { + cwd: installDir, + stdio: ['ignore', 'inherit', 'inherit'], + }) + }) + } else { + await rootSpan + .traceChild('run generic install command') + .traceAsyncFn(async () => { + await execa( + 'pnpm', + ['install', '--strict-peer-dependencies=false'], + { + cwd: installDir, + stdio: ['ignore', 'inherit', 'inherit'], + env: process.env, + } + ) + }) + } - childProcess.execSync(installString, { - cwd: installDir, - stdio: ['ignore', 'inherit', 'inherit'], - }) - } else { - await execa('pnpm', ['install', '--strict-peer-dependencies=false'], { - cwd: installDir, - stdio: ['ignore', 'inherit', 'inherit'], - env: process.env, + await fs.remove(tmpRepoDir) + return installDir }) - } - - await fs.remove(tmpRepoDir) - return installDir } module.exports = { diff --git a/test/lib/e2e-utils.ts b/test/lib/e2e-utils.ts index 934e4333b611fc5..16b2b4de923ff98 100644 --- a/test/lib/e2e-utils.ts +++ b/test/lib/e2e-utils.ts @@ -1,5 +1,7 @@ import path from 'path' import assert from 'assert' +import { flushAllTraces, setGlobal, trace } from 'next/trace' +import { PHASE_DEVELOPMENT_SERVER } from 'next/constants' import { NextInstance, NextInstanceOpts } from './next-modes/base' import { NextDevInstance } from './next-modes/next-dev' import { NextStartInstance } from './next-modes/next-start' @@ -105,6 +107,15 @@ if (typeof afterAll === 'function') { }) } +const setupTracing = () => { + if (!process.env.NEXT_TEST_TRACE) return + + setGlobal('distDir', './test/.trace') + // This is a hacky way to use tracing utils even for tracing test utils. + // We want the same treatment as DEVELOPMENT_SERVER - adds a reasonable treshold for logs size. + setGlobal('phase', PHASE_DEVELOPMENT_SERVER) +} + /** * Sets up and manages a Next.js instance in the configured * test mode. The next instance will be isolated from the monorepo @@ -118,45 +129,61 @@ export async function createNext( throw new Error(`createNext called without destroying previous instance`) } - const useTurbo = !!process.env.TEST_WASM - ? false - : opts?.turbo ?? shouldRunTurboDevTest() - - if (testMode === 'dev') { - // next dev - nextInstance = new NextDevInstance({ - ...opts, - turbo: useTurbo, - }) - } else if (testMode === 'deploy') { - // Vercel - nextInstance = new NextDeployInstance({ - ...opts, - turbo: false, + setupTracing() + return await trace('createNext').traceAsyncFn(async (rootSpan) => { + const useTurbo = !!process.env.TEST_WASM + ? false + : opts?.turbo ?? shouldRunTurboDevTest() + + if (testMode === 'dev') { + // next dev + rootSpan.traceChild('init next dev instance').traceFn(() => { + nextInstance = new NextDevInstance({ + ...opts, + turbo: useTurbo, + }) + }) + } else if (testMode === 'deploy') { + // Vercel + rootSpan.traceChild('init next deploy instance').traceFn(() => { + nextInstance = new NextDeployInstance({ + ...opts, + turbo: false, + }) + }) + } else { + // next build + next start + rootSpan.traceChild('init next start instance').traceFn(() => { + nextInstance = new NextStartInstance({ + ...opts, + turbo: false, + }) + }) + } + + nextInstance.on('destroy', () => { + nextInstance = undefined }) - } else { - // next build + next start - nextInstance = new NextStartInstance({ - ...opts, - turbo: false, - }) - } - nextInstance.on('destroy', () => { - nextInstance = undefined - }) + await nextInstance.setup(rootSpan) - await nextInstance.setup() + if (!opts.skipStart) { + await rootSpan + .traceChild('start next instance') + .traceAsyncFn(async () => { + await nextInstance.start() + }) + } - if (!opts.skipStart) { - await nextInstance.start() - } - return nextInstance! + return nextInstance! + }) } catch (err) { require('console').error('Failed to create next instance', err) try { nextInstance.destroy() } catch (_) {} process.exit(1) + } finally { + flushAllTraces() } } diff --git a/test/lib/next-modes/base.ts b/test/lib/next-modes/base.ts index 458e6693846e413..d7398b068f45153 100644 --- a/test/lib/next-modes/base.ts +++ b/test/lib/next-modes/base.ts @@ -6,6 +6,7 @@ import { NextConfig } from 'next' import { FileRef } from '../e2e-utils' import { ChildProcess } from 'child_process' import { createNextInstall } from '../create-next-install' +import { Span } from 'next/trace' type Event = 'stdout' | 'stderr' | 'error' | 'destroy' export type InstallCommand = @@ -84,148 +85,170 @@ export class NextInstance { protected async createTestDir({ skipInstall = false, - }: { skipInstall?: boolean } = {}) { + parentSpan, + }: { + skipInstall?: boolean + parentSpan: Span + }) { if (this.isDestroyed) { throw new Error('next instance already destroyed') } require('console').log(`Creating test directory with isolated next...`) - const skipIsolatedNext = !!process.env.NEXT_SKIP_ISOLATE - const tmpDir = skipIsolatedNext - ? path.join(__dirname, '../../tmp') - : process.env.NEXT_TEST_DIR || (await fs.realpath(os.tmpdir())) - this.testDir = path.join( - tmpDir, - `next-test-${Date.now()}-${(Math.random() * 1000) | 0}${this.dirSuffix}` - ) + await parentSpan + .traceChild('createTestDir') + .traceAsyncFn(async (rootSpan) => { + const skipIsolatedNext = !!process.env.NEXT_SKIP_ISOLATE + const tmpDir = skipIsolatedNext + ? path.join(__dirname, '../../tmp') + : process.env.NEXT_TEST_DIR || (await fs.realpath(os.tmpdir())) + this.testDir = path.join( + tmpDir, + `next-test-${Date.now()}-${(Math.random() * 1000) | 0}${ + this.dirSuffix + }` + ) - const reactVersion = process.env.NEXT_TEST_REACT_VERSION || 'latest' - const finalDependencies = { - react: reactVersion, - 'react-dom': reactVersion, - ...this.dependencies, - ...this.packageJson?.dependencies, - } + const reactVersion = process.env.NEXT_TEST_REACT_VERSION || 'latest' + const finalDependencies = { + react: reactVersion, + 'react-dom': reactVersion, + ...this.dependencies, + ...this.packageJson?.dependencies, + } - if (skipInstall) { - const pkgScripts = (this.packageJson['scripts'] as {}) || {} - await fs.ensureDir(this.testDir) - await fs.writeFile( - path.join(this.testDir, 'package.json'), - JSON.stringify( - { - ...this.packageJson, - dependencies: { - ...finalDependencies, - next: - process.env.NEXT_TEST_VERSION || - require('next/package.json').version, - }, - scripts: { - ...pkgScripts, - build: - (pkgScripts['build'] || this.buildCommand || 'next build') + - ' && yarn post-build', - // since we can't get the build id as a build artifact, make it - // available under the static files - 'post-build': 'cp .next/BUILD_ID .next/static/__BUILD_ID', - }, - }, - null, - 2 - ) - ) - } else { - if ( - process.env.NEXT_TEST_STARTER && - !this.dependencies && - !this.installCommand && - !this.packageJson && - !(global as any).isNextDeploy - ) { - await fs.copy(process.env.NEXT_TEST_STARTER, this.testDir) - } else if (!skipIsolatedNext) { - this.testDir = await createNextInstall( - finalDependencies, - this.installCommand, - this.packageJson, - this.packageLockPath, - this.dirSuffix - ) - } - require('console').log('created next.js install, writing test files') - } + if (skipInstall) { + const pkgScripts = (this.packageJson['scripts'] as {}) || {} + await fs.ensureDir(this.testDir) + await fs.writeFile( + path.join(this.testDir, 'package.json'), + JSON.stringify( + { + ...this.packageJson, + dependencies: { + ...finalDependencies, + next: + process.env.NEXT_TEST_VERSION || + require('next/package.json').version, + }, + scripts: { + ...pkgScripts, + build: + (pkgScripts['build'] || this.buildCommand || 'next build') + + ' && yarn post-build', + // since we can't get the build id as a build artifact, make it + // available under the static files + 'post-build': 'cp .next/BUILD_ID .next/static/__BUILD_ID', + }, + }, + null, + 2 + ) + ) + } else { + if ( + process.env.NEXT_TEST_STARTER && + !this.dependencies && + !this.installCommand && + !this.packageJson && + !(global as any).isNextDeploy + ) { + await fs.copy(process.env.NEXT_TEST_STARTER, this.testDir) + } else if (!skipIsolatedNext) { + this.testDir = await createNextInstall({ + parentSpan: rootSpan, + dependencies: finalDependencies, + installCommand: this.installCommand, + packageJson: this.packageJson, + packageLockPath: this.packageLockPath, + dirSuffix: this.dirSuffix, + }) + } + require('console').log('created next.js install, writing test files') + } - await this.writeInitialFiles() + await rootSpan + .traceChild('writeInitialFiles') + .traceAsyncFn(async () => { + await this.writeInitialFiles() + }) - let nextConfigFile = Object.keys(this.files).find((file) => - file.startsWith('next.config.') - ) + let nextConfigFile = Object.keys(this.files).find((file) => + file.startsWith('next.config.') + ) - if (await fs.pathExists(path.join(this.testDir, 'next.config.js'))) { - nextConfigFile = 'next.config.js' - } + if (await fs.pathExists(path.join(this.testDir, 'next.config.js'))) { + nextConfigFile = 'next.config.js' + } - if (nextConfigFile && this.nextConfig) { - throw new Error( - `nextConfig provided on "createNext()" and as a file "${nextConfigFile}", use one or the other to continue` - ) - } + if (nextConfigFile && this.nextConfig) { + throw new Error( + `nextConfig provided on "createNext()" and as a file "${nextConfigFile}", use one or the other to continue` + ) + } - if (this.nextConfig || ((global as any).isNextDeploy && !nextConfigFile)) { - const functions = [] + if ( + this.nextConfig || + ((global as any).isNextDeploy && !nextConfigFile) + ) { + const functions = [] - await fs.writeFile( - path.join(this.testDir, 'next.config.js'), - ` + await fs.writeFile( + path.join(this.testDir, 'next.config.js'), + ` module.exports = ` + - JSON.stringify( - { - ...this.nextConfig, - } as NextConfig, - (key, val) => { - if (typeof val === 'function') { - functions.push( - val - .toString() - .replace(new RegExp(`${val.name}[\\s]{0,}\\(`), 'function(') - ) - return `__func_${functions.length - 1}` - } - return val - }, - 2 - ).replace(/"__func_[\d]{1,}"/g, function (str) { - return functions.shift() - }) - ) - } + JSON.stringify( + { + ...this.nextConfig, + } as NextConfig, + (key, val) => { + if (typeof val === 'function') { + functions.push( + val + .toString() + .replace( + new RegExp(`${val.name}[\\s]{0,}\\(`), + 'function(' + ) + ) + return `__func_${functions.length - 1}` + } + return val + }, + 2 + ).replace(/"__func_[\d]{1,}"/g, function (str) { + return functions.shift() + }) + ) + } - if ((global as any).isNextDeploy) { - const fileName = path.join( - this.testDir, - nextConfigFile || 'next.config.js' - ) - const content = await fs.readFile(fileName, 'utf8') + if ((global as any).isNextDeploy) { + const fileName = path.join( + this.testDir, + nextConfigFile || 'next.config.js' + ) + const content = await fs.readFile(fileName, 'utf8') - if (content.includes('basePath')) { - this.basePath = - content.match(/['"`]?basePath['"`]?:.*?['"`](.*?)['"`]/)?.[1] || '' - } + if (content.includes('basePath')) { + this.basePath = + content.match(/['"`]?basePath['"`]?:.*?['"`](.*?)['"`]/)?.[1] || + '' + } - await fs.writeFile( - fileName, - `${content}\n` + - ` + await fs.writeFile( + fileName, + `${content}\n` + + ` // alias __NEXT_TEST_MODE for next-deploy as "_" is not a valid // env variable during deploy if (process.env.NEXT_PRIVATE_TEST_MODE) { process.env.__NEXT_TEST_MODE = process.env.NEXT_PRIVATE_TEST_MODE } ` - ) - } - require('console').log(`Test directory created at ${this.testDir}`) + ) + } + require('console').log(`Test directory created at ${this.testDir}`) + }) } public async clean() { @@ -245,7 +268,7 @@ export class NextInstance { public async export(): Promise<{ exitCode?: number; cliOutput?: string }> { return {} } - public async setup(): Promise {} + public async setup(parentSpan: Span): Promise {} public async start(useDirArg: boolean = false): Promise {} public async stop(): Promise { this.isStopping = true diff --git a/test/lib/next-modes/next-deploy.ts b/test/lib/next-modes/next-deploy.ts index 8cb141fa4dcdcd1..4e9732ac1eb097d 100644 --- a/test/lib/next-modes/next-deploy.ts +++ b/test/lib/next-modes/next-deploy.ts @@ -9,6 +9,7 @@ import { TEST_TOKEN, } from '../../../scripts/reset-vercel-project.mjs' import fetch from 'node-fetch' +import { Span } from 'next/trace' export class NextDeployInstance extends NextInstance { private _cliOutput: string @@ -20,8 +21,8 @@ export class NextDeployInstance extends NextInstance { return this._buildId } - public async setup() { - await super.createTestDir({ skipInstall: true }) + public async setup(parentSpan: Span) { + await super.createTestDir({ parentSpan, skipInstall: true }) // ensure Vercel CLI is installed try { diff --git a/test/lib/next-modes/next-dev.ts b/test/lib/next-modes/next-dev.ts index 58909699c7ceb94..b2699bcd9b91fc9 100644 --- a/test/lib/next-modes/next-dev.ts +++ b/test/lib/next-modes/next-dev.ts @@ -1,4 +1,5 @@ import { spawn } from 'child_process' +import { Span } from 'next/trace' import { NextInstance } from './base' export class NextDevInstance extends NextInstance { @@ -8,8 +9,8 @@ export class NextDevInstance extends NextInstance { return 'development' } - public async setup() { - await super.createTestDir() + public async setup(parentSpan: Span) { + await super.createTestDir({ parentSpan }) } public get cliOutput() { diff --git a/test/lib/next-modes/next-start.ts b/test/lib/next-modes/next-start.ts index 51672583a40e431..f0b1484c0413e2b 100644 --- a/test/lib/next-modes/next-start.ts +++ b/test/lib/next-modes/next-start.ts @@ -2,6 +2,7 @@ import path from 'path' import fs from 'fs-extra' import { NextInstance } from './base' import { spawn, SpawnOptions } from 'child_process' +import { Span } from 'next/trace' export class NextStartInstance extends NextInstance { private _buildId: string @@ -16,8 +17,8 @@ export class NextStartInstance extends NextInstance { return this._cliOutput } - public async setup() { - await super.createTestDir() + public async setup(parentSpan: Span) { + await super.createTestDir({ parentSpan }) } private handleStdio = (childProcess) => {