Add tracing for testing tools (#44046)

This commit is contained in:
Jan Kaifer 2022-12-16 09:58:04 +01:00 committed by GitHub
parent daa14ab3e2
commit ab328c6c39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 509 additions and 353 deletions

View file

@ -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

View file

@ -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
}
throw err
}
return await rootSpan.traceAsyncFn(async () => {
const pkgPaths = new Map()
const pkgDatas = new Map()
let pkgs
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)
}
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 = []
try {
pkgs = await fs.readdir(path.join(repoDir, 'packages'))
} catch (err) {
if (err.code === 'ENOENT') {
console.log('no packages to link')
return pkgPaths
}
pkgData.files.push('native')
console.log(
'using swc binaries: ',
await exec(`ls ${path.join(path.dirname(pkgDataPath), 'native')}`)
)
throw err
}
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 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)
}
}
}
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
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')
}
}
}
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
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
})
},
}
}

1
.gitignore vendored
View file

@ -28,6 +28,7 @@ test/**/tsconfig.json
.DS_Store
/e2e-tests
test/tmp/**
test/.trace
# Editors
**/.idea

View file

@ -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.

View file

@ -93,17 +93,17 @@ export class Span {
this.attrs[key] = String(value)
}
traceFn<T>(fn: () => T): T {
traceFn<T>(fn: (span: Span) => T): T {
try {
return fn()
return fn(this)
} finally {
this.stop()
}
}
async traceAsyncFn<T>(fn: () => T | Promise<T>): Promise<T> {
async traceAsyncFn<T>(fn: (span: Span) => T | Promise<T>): Promise<T> {
try {
return await fn()
return await fn(this)
} finally {
this.stop()
}

View file

@ -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
}

View file

@ -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'),

View file

@ -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')
})
})

View file

@ -0,0 +1,3 @@
export default function Page() {
return <h1>Hello World</h1>
}

View file

@ -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
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
)
},
)
if (packageLockPath) {
await fs.copy(
packageLockPath,
path.join(installDir, path.basename(packageLockPath))
)
}
if (installCommand) {
const installString =
typeof installCommand === 'function'
? installCommand({ dependencies: combinedDependencies })
: installCommand
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,
}
)
})
}
await fs.remove(tmpRepoDir)
return installDir
})
}
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
}, {}),
}
}
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 (installCommand) {
const installString =
typeof installCommand === 'function'
? installCommand({ dependencies: combinedDependencies })
: installCommand
console.log('running install command', installString)
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
}
module.exports = {

View file

@ -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()
setupTracing()
return await trace('createNext').traceAsyncFn(async (rootSpan) => {
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,
})
} else {
// next build + next start
nextInstance = new NextStartInstance({
...opts,
turbo: false,
})
}
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
nextInstance.on('destroy', () => {
nextInstance = undefined
})
await nextInstance.setup(rootSpan)
if (!opts.skipStart) {
await rootSpan
.traceChild('start next instance')
.traceAsyncFn(async () => {
await nextInstance.start()
})
}
return nextInstance!
})
await nextInstance.setup()
if (!opts.skipStart) {
await nextInstance.start()
}
return nextInstance!
} catch (err) {
require('console').error('Failed to create next instance', err)
try {
nextInstance.destroy()
} catch (_) {}
process.exit(1)
} finally {
flushAllTraces()
}
}

View file

@ -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}`
)
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
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
}`
)
)
} 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')
}
await this.writeInitialFiles()
const reactVersion = process.env.NEXT_TEST_REACT_VERSION || 'latest'
const finalDependencies = {
react: reactVersion,
'react-dom': reactVersion,
...this.dependencies,
...this.packageJson?.dependencies,
}
let nextConfigFile = Object.keys(this.files).find((file) =>
file.startsWith('next.config.')
)
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')
}
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 (this.nextConfig || ((global as any).isNextDeploy && !nextConfigFile)) {
const functions = []
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()
await rootSpan
.traceChild('writeInitialFiles')
.traceAsyncFn(async () => {
await this.writeInitialFiles()
})
)
}
if ((global as any).isNextDeploy) {
const fileName = path.join(
this.testDir,
nextConfigFile || 'next.config.js'
)
const content = await fs.readFile(fileName, 'utf8')
let nextConfigFile = Object.keys(this.files).find((file) =>
file.startsWith('next.config.')
)
if (content.includes('basePath')) {
this.basePath =
content.match(/['"`]?basePath['"`]?:.*?['"`](.*?)['"`]/)?.[1] || ''
}
if (await fs.pathExists(path.join(this.testDir, 'next.config.js'))) {
nextConfigFile = 'next.config.js'
}
await fs.writeFile(
fileName,
`${content}\n` +
`
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 = []
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()
})
)
}
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] ||
''
}
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<void> {}
public async setup(parentSpan: Span): Promise<void> {}
public async start(useDirArg: boolean = false): Promise<void> {}
public async stop(): Promise<void> {
this.isStopping = true

View file

@ -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 {

View file

@ -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() {

View file

@ -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) => {