Files
cypress/system-tests/lib/fixtures.ts
Zach Bloomquist dfd70445b1 test(app): fix EBUSY/EPERM errors when running e2e tests on Windows (#19848)
Co-authored-by: Barthélémy Ledoux <bart@cypress.io>
Co-authored-by: ElevateBart <ledouxb@gmail.com>
2022-01-26 19:39:13 +00:00

370 lines
11 KiB
TypeScript

import fs from 'fs-extra'
import _path from 'path'
import chokidar from 'chokidar'
import cachedir from 'cachedir'
import execa from 'execa'
import tempDir from 'temp-dir'
const root = _path.join(__dirname, '..')
const serverRoot = _path.join(__dirname, '../../packages/server/')
const projects = _path.join(root, 'projects')
const cyTmpDir = _path.join(tempDir, 'cy-projects')
const safeRemove = (path) => {
try {
fs.removeSync(path)
} catch (err) {
// Windows does not like the en masse deleting of files, since the AV will hold
// a lock on files when they are written. This skips deleting if the lock is
// encountered.
if (err.code === 'EBUSY' && process.platform === 'win32') {
return console.error(`Remove failed for ${ path } due to EBUSY. Skipping on Windows.`)
}
throw err
}
}
// copy contents instead of deleting+creating new file, which can cause
// filewatchers to lose track of toFile.
const copyContents = (fromFile, toFile) => {
return Promise.all([
fs.open(toFile, 'w'),
fs.readFile(fromFile),
])
.then(([toFd, fromFileBuf]) => {
return fs.write(toFd, fromFileBuf)
.finally(() => {
return fs.close(toFd)
})
})
}
// copies all of the project fixtures
// to the cyTmpDir .projects in the root
export function scaffold () {
fs.copySync(projects, cyTmpDir)
}
/**
* Given a project name, copy the project's test files to the temp dir.
*/
export async function scaffoldProject (project: string): Promise<void> {
const to = _path.join(cyTmpDir, project)
const from = _path.join(projects, project)
fs.copySync(from, to)
}
/**
* Symlink the cached `node_modules` directory to the temp project directory's `node_modules`.
*/
async function symlinkNodeModulesFromCache (project: string, cacheDir: string): Promise<void> {
const from = _path.join(projectPath(project), 'node_modules')
try {
await fs.stat(cacheDir)
} catch (err) {
console.log(`📦 Creating a new node_modules cache dir at ${cacheDir}`)
await fs.mkdirp(cacheDir)
}
try {
await fs.symlink(cacheDir, from, 'junction')
} catch (err) {
if (err.code !== 'EEXIST') return
}
console.log(`📦 node_modules symlink created at ${from}`)
}
/**
* Given a package name, returns the path to the module directory on disk.
*/
function pathToPackage (pkg: string): string {
return _path.dirname(require.resolve(`${pkg}/package.json`))
}
/**
* Given a path to a `package.json`, convert any references to development
* versions of packages to absolute paths, so `yarn` will not reach out to
* the Internet to obtain these packages once it runs in the temp dir.
* @returns a list of dependency names that were updated
*/
async function makeWorkspacePackagesAbsolute (pathToPkgJson: string): Promise<string[]> {
const pkgJson = await fs.readJson(pathToPkgJson)
const updatedDeps: string[] = []
for (const deps of [pkgJson.dependencies, pkgJson.devDependencies, pkgJson.optionalDependencies]) {
for (const dep in deps) {
const version = deps[dep]
if (version.startsWith('file:')) {
const absPath = pathToPackage(dep)
console.log(`📦 Setting absolute path in package.json for ${dep}: ${absPath}.`)
deps[dep] = `file:${absPath}`
updatedDeps.push(dep)
}
}
}
await fs.writeJson(pathToPkgJson, pkgJson)
return updatedDeps
}
function getYarnCommand (opts: {
yarnV311: boolean
updateYarnLock: boolean
isCI: boolean
runScripts: boolean
}): string {
let cmd = `yarn install`
if (opts.yarnV311) {
// @see https://yarnpkg.com/cli/install
if (!opts.runScripts) cmd += ' --mode=skip-build'
if (!opts.updateYarnLock) cmd += ' --immutable'
return cmd
}
cmd += ' --prefer-offline'
if (!opts.runScripts) cmd += ' --ignore-scripts'
if (!opts.updateYarnLock) cmd += ' --frozen-lockfile'
// yarn v1 has a bug with integrity checking and local cache/dependencies
// @see https://github.com/yarnpkg/yarn/issues/6407
cmd += ' --update-checksums'
// in CircleCI, this offline cache can be used
if (opts.isCI) cmd += ` --cache-folder=~/.yarn-${process.platform} `
else cmd += ` --cache-folder=${_path.join(tempDir, 'cy-system-tests-yarn-cache', String(Date.now()))}`
return cmd
}
type Dependencies = Record<string, string>
/**
* Type for package.json files for system-tests example projects.
*/
type SystemTestPkgJson = {
/**
* By default, scaffolding will run `yarn install` if there is a `package.json`.
* This option, if set, disables that.
*/
_cySkipYarnInstall?: boolean
/**
* Run the yarn v2-style install command instead of yarn v1-style.
*/
_cyYarnV311?: boolean
/**
* By default, the automatic `yarn install` will not run postinstall scripts. This
* option, if set, will cause postinstall scripts to run for this project.
*/
_cyRunScripts?: boolean
dependencies?: Dependencies
devDependencies?: Dependencies
optionalDependencies?: Dependencies
}
/**
* Given a `system-tests` project name, detect and install the `node_modules`
* specified in the project's `package.json`. No-op if no `package.json` is found.
*/
export async function scaffoldProjectNodeModules (project: string, updateYarnLock: boolean = !!process.env.UPDATE_YARN_LOCK): Promise<void> {
const projectDir = projectPath(project)
const relativePathToMonorepoRoot = _path.relative(
_path.join(projects, project),
_path.join(root, '..'),
)
const projectPkgJsonPath = _path.join(projectDir, 'package.json')
const runCmd = async (cmd) => {
console.log(`📦 Running "${cmd}" in ${projectDir}`)
await execa(cmd, { cwd: projectDir, stdio: 'inherit', shell: true })
}
const cacheDir = _path.join(cachedir('cy-system-tests-node-modules'), project, 'node_modules')
async function removeWorkspacePackages (packages: string[]): Promise<void> {
for (const dep of packages) {
const depDir = _path.join(cacheDir, dep)
safeRemove(depDir)
}
}
try {
// this will throw and exit early if the package.json does not exist
const pkgJson: SystemTestPkgJson = require(projectPkgJsonPath)
console.log(`📦 Found package.json for project ${project}.`)
if (pkgJson._cySkipYarnInstall) {
return console.log(`📦 cySkipYarnInstall set in package.json, skipping yarn steps`)
}
if (!pkgJson.dependencies && !pkgJson.devDependencies && !pkgJson.optionalDependencies) {
return console.log(`📦 No dependencies found, skipping yarn steps`)
}
// 1. Ensure there is a cache directory set up for this test project's `node_modules`.
await symlinkNodeModulesFromCache(project, cacheDir)
// 2. Before running `yarn`, resolve workspace deps to absolute paths.
// This is required to fix `yarn install` for workspace-only packages.
const workspaceDeps = await makeWorkspacePackagesAbsolute(projectPkgJsonPath)
await removeWorkspacePackages(workspaceDeps)
// 3. Fix relative paths in temp dir's `yarn.lock`.
const relativePathToProjectDir = _path.relative(projectDir, _path.join(root, '..'))
const yarnLockPath = _path.join(projectDir, 'yarn.lock')
console.log('📦 Writing yarn.lock with fixed relative paths to temp dir')
try {
const yarnLock = (await fs.readFile(yarnLockPath, 'utf8'))
.replaceAll(relativePathToMonorepoRoot, relativePathToProjectDir)
await fs.writeFile(yarnLockPath, yarnLock)
} catch (err) {
if (err.code !== 'ENOENT' || !updateYarnLock) throw err
console.log('📦 No yarn.lock found, continuing')
}
// 4. Run `yarn install`.
const cmd = getYarnCommand({
updateYarnLock,
yarnV311: pkgJson._cyYarnV311,
isCI: !!process.env.CI,
runScripts: pkgJson._cyRunScripts,
})
await runCmd(cmd)
console.log(`📦 Copying yarn.lock and fixing relative paths for ${project}`)
// Replace workspace dependency paths in `yarn.lock` with tokens so it can be the same
// for all developers
const yarnLock = (await fs.readFile(yarnLockPath, 'utf8'))
.replaceAll(relativePathToProjectDir, relativePathToMonorepoRoot)
await fs.writeFile(_path.join(projects, project, 'yarn.lock'), yarnLock)
// 5. After `yarn install`, we must now symlink *over* all workspace dependencies, or else
// `require` calls from `yarn install`'d workspace deps to peer deps will fail.
await removeWorkspacePackages(workspaceDeps)
for (const dep of workspaceDeps) {
console.log(`📦 Symlinking workspace dependency: ${dep}`)
const depDir = _path.join(cacheDir, dep)
await fs.symlink(pathToPackage(dep), depDir, 'junction')
}
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') return
console.error(`⚠ An error occurred while installing the node_modules for ${project}.`)
console.error([err.message, err.stack].join('\n'))
throw err
}
}
export async function scaffoldCommonNodeModules () {
await Promise.all([
'@babel/preset-env',
'@babel/preset-react',
'babel-loader',
// Used for import { defineConfig } from 'cypress'
'cypress',
'@cypress/code-coverage',
'@cypress/react',
'@cypress/webpack-dev-server',
'@packages/socket',
'@packages/ts',
'@tooling/system-tests',
'bluebird',
'chai',
'dayjs',
'debug',
'execa',
'fs-extra',
'https-proxy-agent',
'jimp',
'lazy-ass',
'lodash',
'proxyquire',
'react',
'semver',
'systeminformation',
'tslib',
'typescript',
].map(symlinkNodeModule))
}
export async function symlinkNodeModule (pkg) {
const from = _path.join(cyTmpDir, 'node_modules', pkg)
const to = pathToPackage(pkg)
await fs.ensureDir(_path.dirname(from))
try {
await fs.symlink(to, from, 'junction')
} catch (err) {
if (err.code === 'EEXIST') return
throw err
}
}
export function scaffoldWatch () {
const watchdir = _path.resolve(__dirname, '../projects')
console.log('watching files due to --no-exit', watchdir)
chokidar.watch(watchdir, {
})
.on('change', (srcFilepath, stats) => {
const tmpFilepath = _path.join(cyTmpDir, _path.relative(watchdir, srcFilepath))
return copyContents(srcFilepath, tmpFilepath)
})
.on('error', console.error)
}
// removes all of the project fixtures
// from the cyTmpDir .projects in the root
export function remove () {
safeRemove(cyTmpDir)
}
export function removeProject (name) {
safeRemove(projectPath(name))
}
// returns the path to project fixture
// in the cyTmpDir
export function project (...args) {
return this.projectPath.apply(this, args)
}
export function projectPath (name) {
return _path.join(cyTmpDir, name)
}
export function get (fixture, encoding: BufferEncoding = 'utf8') {
return fs.readFileSync(_path.join(serverRoot, 'test', 'support', 'fixtures', fixture), { encoding })
}
export function path (fixture) {
return _path.join(serverRoot, 'test', 'support', 'fixtures', fixture)
}
export default module.exports