feat: introduce v8 snapshots to improve startup performance (#24295)

Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com>
Co-authored-by: Zach Bloomquist <git@chary.us>
Co-authored-by: Tyler Biethman <tbiethman@users.noreply.github.com>
Co-authored-by: Matt Henkes <mjhenkes@gmail.com>
Co-authored-by: Chris Breiding <chrisbreiding@users.noreply.github.com>
Co-authored-by: Matt Schile <mschile@cypress.io>
Co-authored-by: Mark Noonan <mark@cypress.io>
Co-authored-by: Zachary Williams <ZachJW34@gmail.com>
Co-authored-by: Ben M <benm@cypress.io>
Co-authored-by: Zachary Williams <zachjw34@gmail.com>
Co-authored-by: astone123 <adams@cypress.io>
Co-authored-by: Bill Glesias <bglesias@gmail.com>
Co-authored-by: Emily Rohrbough <emilyrohrbough@yahoo.com>
Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com>
Co-authored-by: semantic-release-bot <semantic-release-bot@martynus.net>
Co-authored-by: Adam Stone <adams@cypress.io>
Co-authored-by: Blue F <blue@cypress.io>
Co-authored-by: GitStart <1501599+gitstart@users.noreply.github.com>
Co-authored-by: Mike Plummer <mike-plummer@users.noreply.github.com>
Co-authored-by: Jordan <jordan@jpdesigning.com>
Co-authored-by: Sam Goodger <turbo@tailz.dev>
Co-authored-by: Colum Ferry <cferry09@gmail.com>
Co-authored-by: Stokes Player <stokes@cypress.io>
Co-authored-by: Vilhelm Melkstam <vilhelm.melkstam@gmail.com>
Co-authored-by: amehta265 <65267668+amehta265@users.noreply.github.com>
This commit is contained in:
Ryan Manuel
2022-10-31 20:20:27 -05:00
committed by GitHub
parent 440a08bb2a
commit b0c0eaa508
335 changed files with 37377 additions and 629 deletions
+22
View File
@@ -4,6 +4,9 @@ const { join } = require('path')
const glob = require('glob')
const os = require('os')
const path = require('path')
const { setupV8Snapshots } = require('@tooling/v8-snapshot')
const { flipFuses, FuseVersion, FuseV1Options } = require('@electron/fuses')
const { cleanup } = require('./binary/binary-cleanup')
module.exports = async function (params) {
console.log('****************************')
@@ -44,4 +47,23 @@ module.exports = async function (params) {
await fs.copy(distNodeModules, appNodeModules)
console.log('all node_modules subfolders copied to', outputFolder)
const exePathPerPlatform = {
darwin: join(params.appOutDir, 'Cypress.app', 'Contents', 'MacOS', 'Cypress'),
linux: join(params.appOutDir, 'Cypress'),
win32: join(params.appOutDir, 'Cypress.exe'),
}
if (!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE)) {
await flipFuses(
exePathPerPlatform[os.platform()],
{
version: FuseVersion.V1,
[FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: true,
},
)
await setupV8Snapshots(params.appOutDir)
await cleanup(outputFolder)
}
}
+191
View File
@@ -0,0 +1,191 @@
const fs = require('fs-extra')
const path = require('path')
const { consolidateDeps } = require('@tooling/v8-snapshot')
const del = require('del')
const esbuild = require('esbuild')
const snapshotMetadata = require('@tooling/v8-snapshot/cache/prod-darwin/snapshot-meta.cache.json')
const tempDir = require('temp-dir')
const workingDir = path.join(tempDir, 'binary-cleanup-workdir')
fs.ensureDirSync(workingDir)
async function removeEmptyDirectories (directory) {
// lstat does not follow symlinks (in contrast to stat)
const fileStats = await fs.lstat(directory)
if (!fileStats.isDirectory()) {
return
}
let fileNames = await fs.readdir(directory)
if (fileNames.length > 0) {
const recursiveRemovalPromises = fileNames.map(
(fileName) => removeEmptyDirectories(path.join(directory, fileName)),
)
await Promise.all(recursiveRemovalPromises)
// re-evaluate fileNames; after deleting subdirectory
// we may have parent directory empty now
fileNames = await fs.readdir(directory)
}
if (fileNames.length === 0) {
await fs.rmdir(directory)
}
}
const getDependencyPathsToKeep = async () => {
let entryPoints = new Set([
// This is the entry point for the server bundle. It will not have access to the snapshot yet. It needs to be kept in the binary
require.resolve('@packages/server/index.js'),
// This is a dynamic import that is used to load the snapshot require logic. It will not have access to the snapshot yet. It needs to be kept in the binary
require.resolve('@packages/server/hook-require.js'),
// These dependencies are started in a new process or thread and will not have access to the snapshot. They need to be kept in the binary
require.resolve('@packages/server/lib/plugins/child/require_async_child.js'),
require.resolve('@packages/server/lib/plugins/child/register_ts_node.js'),
require.resolve('@packages/rewriter/lib/threads/worker.ts'),
// These dependencies use the `require.resolve(<dependency>, { paths: [<path>] })` pattern where <path> is a path within the cypress monorepo. These will not be
// pulled in by esbuild but still need to be kept in the binary.
require.resolve('webpack'),
require.resolve('webpack-dev-server', { paths: [path.join(__dirname, '..', '..', 'npm', 'webpack-dev-server')] }),
require.resolve('html-webpack-plugin-4', { paths: [path.join(__dirname, '..', '..', 'npm', 'webpack-dev-server')] }),
require.resolve('html-webpack-plugin-5', { paths: [path.join(__dirname, '..', '..', 'npm', 'webpack-dev-server')] }),
// These dependencies are completely dynamic using the pattern `require(`./${name}`)` and will not be pulled in by esbuild but still need to be kept in the binary.
...['ibmi',
'sunos',
'android',
'darwin',
'freebsd',
'linux',
'openbsd',
'sunos',
'win32'].map((platform) => require.resolve(`default-gateway/${platform}`)),
])
let esbuildResult
let newEntryPointsFound = true
// The general idea here is to run esbuild on entry points that are used outside of the snapshot. If, during the process,
// we find places where we do a require.resolve on a module, that should be treated as an additional entry point and we run
// esbuild again. We do this until we no longer find any new entry points. The resulting metafile inputs are
// the dependency paths that we need to ensure stay in the snapshot.
while (newEntryPointsFound) {
esbuildResult = await esbuild.build({
entryPoints: [...entryPoints],
bundle: true,
outdir: workingDir,
platform: 'node',
metafile: true,
external: [
'./packages/server/server-entry',
'fsevents',
'pnpapi',
'@swc/core',
'emitter',
],
})
newEntryPointsFound = false
esbuildResult.warnings.forEach((warning) => {
const matches = warning.text.match(/"(.*)" should be marked as external for use with "require.resolve"/)
const warningSubject = matches && matches[1]
if (warningSubject) {
let entryPoint
if (warningSubject.startsWith('.')) {
entryPoint = path.join(__dirname, '..', '..', path.dirname(warning.location.file), warningSubject)
} else {
entryPoint = require.resolve(warningSubject)
}
if (path.extname(entryPoint) !== '' && !entryPoints.has(entryPoint)) {
newEntryPointsFound = true
entryPoints.add(entryPoint)
}
}
})
}
return [...Object.keys(esbuildResult.metafile.inputs), ...entryPoints]
}
const cleanup = async (buildAppDir) => {
// 1. Retrieve all dependencies that still need to be kept in the binary. In theory, we could use the bundles generated here as single files within the binary,
// but for now, we just track on the dependencies that get pulled in
const keptDependencies = [...await getDependencyPathsToKeep(), 'package.json', 'packages/server/server-entry.js']
// 2. Gather the dependencies that could potentially be removed from the binary due to being in the snapshot
const potentiallyRemovedDependencies = [...snapshotMetadata.healthy, ...snapshotMetadata.deferred, ...snapshotMetadata.norewrite]
// 3. Remove all dependencies that are in the snapshot but not in the list of kept dependencies from the binary
await Promise.all(potentiallyRemovedDependencies.map(async (dependency) => {
// marionette-client requires all of its dependencies in a very non-standard dynamic way. We will keep anything in marionette-client
if (!keptDependencies.includes(dependency.slice(2)) && !dependency.includes('marionette-client')) {
await fs.remove(path.join(buildAppDir, dependency.replace(/.ts$/, '.js')))
}
}))
// 4. Consolidate dependencies that are safe to consolidate (`lodash` and `bluebird`)
await consolidateDeps({ projectBaseDir: buildAppDir })
// 5. Remove various unnecessary files from the binary to further clean things up. Likely, there is additional work that can be done here
await del([
// Remove test files
path.join(buildAppDir, '**', 'test'),
path.join(buildAppDir, '**', 'tests'),
// What we need of prettier is entirely encapsulated within the v8 snapshot, but has a few leftover large files
path.join(buildAppDir, '**', 'prettier', 'esm'),
path.join(buildAppDir, '**', 'prettier', 'standalone.js'),
path.join(buildAppDir, '**', 'prettier', 'bin-prettier.js'),
// ESM files are mostly not needed currently
path.join(buildAppDir, '**', '@babel', '**', 'esm'),
path.join(buildAppDir, '**', 'ramda', 'es'),
path.join(buildAppDir, '**', 'jimp', 'es'),
path.join(buildAppDir, '**', '@jimp', '**', 'es'),
path.join(buildAppDir, '**', 'nexus', 'dist-esm'),
path.join(buildAppDir, '**', '@graphql-tools', '**', '*.mjs'),
path.join(buildAppDir, '**', 'graphql', '**', '*.mjs'),
// We currently do not use any map files
path.join(buildAppDir, '**', '*js.map'),
// License files need to be kept
path.join(buildAppDir, '**', '!(LICENSE|license|License).md'),
// These are type related files that are not used within the binary
path.join(buildAppDir, '**', '*.d.ts'),
path.join(buildAppDir, '**', 'ajv', 'lib', '**', '*.ts'),
path.join(buildAppDir, '**', '*.flow'),
// Example files are not needed
path.join(buildAppDir, '**', 'jimp', 'browser', 'examples'),
// Documentation files are not needed
path.join(buildAppDir, '**', 'JSV', 'jsdoc-toolkit'),
path.join(buildAppDir, '**', 'JSV', 'docs'),
path.join(buildAppDir, '**', 'fluent-ffmpeg', 'doc'),
// Files used as part of prebuilding are not necessary
path.join(buildAppDir, '**', 'registry-js', 'prebuilds'),
path.join(buildAppDir, '**', '*.cc'),
path.join(buildAppDir, '**', '*.o'),
path.join(buildAppDir, '**', '*.c'),
path.join(buildAppDir, '**', '*.h'),
// Remove distributions that are not needed in the binary
path.join(buildAppDir, '**', 'ramda', 'dist'),
path.join(buildAppDir, '**', 'jimp', 'browser'),
path.join(buildAppDir, '**', '@jimp', '**', 'src'),
path.join(buildAppDir, '**', 'nexus', 'src'),
path.join(buildAppDir, '**', 'source-map', 'dist'),
path.join(buildAppDir, '**', 'source-map-js', 'dist'),
path.join(buildAppDir, '**', 'pako', 'dist'),
path.join(buildAppDir, '**', 'node-forge', 'dist'),
path.join(buildAppDir, '**', 'pngjs', 'browser.js'),
path.join(buildAppDir, '**', 'plist', 'dist'),
// Remove yarn locks
path.join(buildAppDir, '**', 'yarn.lock'),
], { force: true })
// 6. Remove any empty directories as a result of the rest of the cleanup
await removeEmptyDirectories(buildAppDir)
}
module.exports = {
cleanup,
}
+50 -10
View File
@@ -18,6 +18,7 @@ import { transformRequires } from './util/transform-requires'
import execa from 'execa'
import { testStaticAssets } from './util/testStaticAssets'
import performanceTracking from '../../system-tests/lib/performance'
import verify from '../../cli/lib/tasks/verify'
const globAsync = promisify(glob)
@@ -173,6 +174,10 @@ export async function buildCypressApp (options: BuildCypressAppOpts) {
}, { spaces: 2 })
fs.writeFileSync(meta.distDir('index.js'), `\
${!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE) ?
`if (!global.snapshotResult && process.versions?.electron) {
throw new Error('global.snapshotResult is not defined. This binary has been built incorrectly.')
}` : ''}
process.env.CYPRESS_INTERNAL_ENV = process.env.CYPRESS_INTERNAL_ENV || 'production'
require('./packages/server')\
`)
@@ -196,8 +201,8 @@ require('./packages/server')\
await transformRequires(meta.distDir())
log(`#testVersion ${meta.distDir()}`)
await testVersion(meta.distDir(), version)
log(`#testDistVersion ${meta.distDir()}`)
await testDistVersion(meta.distDir(), version)
log('#testStaticAssets')
await testStaticAssets(meta.distDir())
@@ -249,9 +254,18 @@ require('./packages/server')\
console.log('electron-builder arguments:')
console.log(args.join(' '))
// Update the root package.json with the next app version so that it is snapshot properly
fs.writeJSONSync(path.join(CY_ROOT_DIR, 'package.json'), {
...jsonRoot,
version,
}, { spaces: 2 })
try {
await execa('electron-builder', args, {
stdio: 'inherit',
env: {
NODE_OPTIONS: '--max_old_space_size=8192',
},
})
} catch (e) {
if (!skipSigning) {
@@ -259,6 +273,9 @@ require('./packages/server')\
}
}
// Revert the root package.json so that subsequent steps will work properly
fs.writeJSONSync(path.join(CY_ROOT_DIR, 'package.json'), jsonRoot, { spaces: 2 })
await checkMaxPathLength()
// lsDistFolder
@@ -268,9 +285,6 @@ require('./packages/server')\
console.log(stdout)
// testVersion(buildAppDir)
await testVersion(meta.buildAppDir(), version)
// runSmokeTests
let usingXvfb = xvfb.isNeeded()
@@ -279,6 +293,9 @@ require('./packages/server')\
await xvfb.start()
}
log(`#testExecutableVersion ${meta.buildAppExecutable()}`)
await testExecutableVersion(meta.buildAppExecutable(), version)
const executablePath = meta.buildAppExecutable()
await smoke.test(executablePath)
@@ -358,23 +375,46 @@ function getIconFilename () {
return iconFilename
}
async function testVersion (dir: string, version: string) {
async function testDistVersion (distDir: string, version: string) {
log('#testVersion')
console.log('testing dist package version')
console.log('by calling: node index.js --version')
console.log('in the folder %s', dir)
console.log('in the folder %s', distDir)
const result = await execa('node', ['index.js', '--version'], {
cwd: dir,
cwd: distDir,
})
la(result.stdout, 'missing output when getting built version', result)
console.log('app in %s', dir)
console.log('app in %s', distDir)
console.log('built app version', result.stdout)
la(result.stdout === version, 'different version reported',
la(result.stdout.trim() === version.trim(), 'different version reported',
result.stdout, 'from input version to build', version)
console.log('✅ using node --version works')
}
async function testExecutableVersion (buildAppExecutable: string, version: string) {
log('#testVersion')
console.log('testing built app executable version')
console.log(`by calling: ${buildAppExecutable} --version`)
const args = ['--version']
if (verify.needsSandbox()) {
args.push('--no-sandbox')
}
const result = await execa(buildAppExecutable, args)
la(result.stdout, 'missing output when getting built version', result)
console.log('built app version', result.stdout)
la(result.stdout.trim() === version.trim(), 'different version reported',
result.stdout, 'from input version to build', version)
console.log('✅ using --version on the Cypress binary works')
}
-22
View File
@@ -1,22 +0,0 @@
/**
* Safer konfig load for test code. The original konfig changes the
* current working directory, thus the tests might be affected
* unexpectedly. This function loads the konfig, but then
* restores the current working directory.
*
* The tests should use this function to get `konfig` function like
*
* @example
* const konfig = require('../binary/get-config')()
*/
const getConfig = () => {
const cwd = process.cwd()
const konfig = require('../../packages/server/lib/konfig')
// restore previous cwd in case it was changed by loading "konfig"
process.chdir(cwd)
return konfig
}
module.exports = getConfig
+45
View File
@@ -160,6 +160,47 @@ const runFailingProjectTest = function (buildAppExecutable, e2e) {
.then(verifyScreenshots)
}
const runV8SnapshotProjectTest = function (buildAppExecutable, e2e) {
if (shouldSkipProjectTest()) {
console.log('skipping failing project test')
return Promise.resolve()
}
console.log('running v8 snapshot project test')
const spawn = () => {
return new Promise((resolve, reject) => {
const env = _.omit(process.env, 'CYPRESS_INTERNAL_ENV')
const args = [
`--run-project=${e2e}`,
`--spec=${e2e}/cypress/e2e/simple_v8_snapshot.cy.js`,
]
if (verify.needsSandbox()) {
args.push('--no-sandbox')
}
const options = {
stdio: 'inherit',
env,
}
return cp.spawn(buildAppExecutable, args, options)
.on('exit', (code) => {
if (code === 0) {
return resolve()
}
return reject(new Error(`running project tests failed with: '${code}' errors.`))
})
})
}
return spawn()
}
const test = async function (buildAppExecutable) {
await scaffoldCommonNodeModules()
await Fixtures.scaffoldProject('e2e')
@@ -168,6 +209,10 @@ const test = async function (buildAppExecutable) {
await runSmokeTest(buildAppExecutable)
await runProjectTest(buildAppExecutable, e2e)
await runFailingProjectTest(buildAppExecutable, e2e)
if (!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE)) {
await runV8SnapshotProjectTest(buildAppExecutable, e2e)
}
Fixtures.remove()
}
+3 -2
View File
@@ -7,11 +7,12 @@ const os = require('os')
const Promise = require('bluebird')
const { fromSSO, fromEnv } = require('@aws-sdk/credential-providers')
const konfig = require('../get-config')()
const { purgeCloudflareCache } = require('./purge-cloudflare-cache')
const CDN_URL = 'https://cdn.cypress.io'
const getUploadUrl = function () {
const url = konfig('cdn_url')
const url = CDN_URL
la(check.url(url), 'could not get CDN url', url)
+4 -4
View File
@@ -11,10 +11,10 @@ declare global {
/**
* Gulp is only used for running the application during development. At this point of starting the app,
* process.env.CYPRESS_INTERNAL_ENV has not been set yet unless explicitly set on the command line. If not
* set on the command line, it is set to 'development' [here](https://github.com/cypress-io/cypress/blob/a5ec234005fead97f6cfdf611abf8d9f4ad0565d/packages/server/lib/environment.js#L22)
* set on the command line, it is set to 'development' [here](https://github.com/cypress-io/cypress/blob/develop/packages/server/lib/environment.js#L22)
*
* When running in a production build, a file is written out to set CYPRESS_INTERNAL_ENV to 'production'
* [here](https://github.com/cypress-io/cypress/blob/a5ec234005fead97f6cfdf611abf8d9f4ad0565d/scripts/binary/build.ts#L176).
* [here](https://github.com/cypress-io/cypress/blob/develop/scripts/binary/build.ts#L176).
* However, running in production will not use the code in this file.
*/
@@ -34,14 +34,14 @@ export const ENV_VARS = {
// Uses the "built" vite assets, not the served ones
DEV_OPEN: {
CYPRESS_KONFIG_ENV: DEFAULT_INTERNAL_CLOUD_ENV, // TODO: Change this / remove konfig
CYPRESS_CONFIG_ENV: DEFAULT_INTERNAL_CLOUD_ENV, // TODO: Change this / remove config
CYPRESS_INTERNAL_CLOUD_ENV: DEFAULT_INTERNAL_CLOUD_ENV,
CYPRESS_INTERNAL_EVENT_COLLECTOR_ENV: DEFAULT_INTERNAL_EVENT_COLLECTOR_ENV,
},
// Used when we're running Cypress in true "development" mode
DEV: {
CYPRESS_KONFIG_ENV: DEFAULT_INTERNAL_CLOUD_ENV, // TODO: Change this / remove konfig
CYPRESS_CONFIG_ENV: DEFAULT_INTERNAL_CLOUD_ENV, // TODO: Change this / remove config
CYPRESS_INTERNAL_CLOUD_ENV: DEFAULT_INTERNAL_CLOUD_ENV,
CYPRESS_INTERNAL_EVENT_COLLECTOR_ENV: DEFAULT_INTERNAL_EVENT_COLLECTOR_ENV,
},
+2
View File
@@ -21,6 +21,7 @@ export const monorepoPaths = {
pkgLaunchpad: path.join(__dirname, '../../packages/launchpad'),
pkgNetStubbing: path.join(__dirname, '../../packages/net-stubbing'),
pkgNetwork: path.join(__dirname, '../../packages/network'),
pkgPackherdRequire: path.join(__dirname, '../../packages/packherd-require'),
pkgProxy: path.join(__dirname, '../../packages/proxy'),
pkgReporter: path.join(__dirname, '../../packages/reporter'),
pkgResolveDist: path.join(__dirname, '../../packages/resolve-dist'),
@@ -32,5 +33,6 @@ export const monorepoPaths = {
pkgSocket: path.join(__dirname, '../../packages/socket'),
pkgTs: path.join(__dirname, '../../packages/ts'),
pkgTypes: path.join(__dirname, '../../packages/types'),
pkgV8SnapshotRequire: path.join(__dirname, '../../packages/v8-snapshot-require'),
pkgWebConfig: path.join(__dirname, '../../packages/web-config')
} as const
+1 -1
View File
@@ -3,7 +3,7 @@ const { execSync } = require('child_process')
const executionEnv = process.env.CI ? 'ci' : 'local'
const postInstallCommands = {
local: 'patch-package && yarn-deduplicate --strategy=highest && yarn clean && gulp postinstall && yarn build',
local: 'patch-package && yarn-deduplicate --strategy=highest && yarn clean && gulp postinstall && yarn build && yarn build-v8-snapshot-dev',
ci: 'patch-package && yarn clean && gulp postinstall',
}
-46
View File
@@ -1,46 +0,0 @@
const la = require('lazy-ass')
const is = require('check-more-types')
const { join } = require('path')
/* eslint-env mocha */
describe('konfig check', () => {
/*
script tests should NOT suddenly change the current working directory to
packages/server - otherwise the local path filenames might be all wrong
and unexpected. The current working directory changes when we
require `packages/server/lib/konfig` which in turn requires
`lib/cwd` which changes CWD.
From the scripts unit tests we should not use `lib/konfig` directly,
instead we should use `binary/get-config` script to get the konfig function.
*/
let cwd
before(() => {
cwd = process.cwd()
la(
!cwd.includes(join('packages', 'server')),
'process CWD is set to',
cwd,
'for some reason',
)
// if the above assertion breaks, it means some script in binary scripts
// loads "lib/konfig" directly, which unexpectedly changes the CWD.
})
it('does not change CWD on load', () => {
const konfig = require('../binary/get-config')()
const cwdAfter = process.cwd()
la(
cwd === cwdAfter,
'previous cwd',
cwd,
'differs after loading konfig',
cwdAfter,
)
la(is.fn(konfig), 'expected konfig to be a function', konfig)
})
})
+66 -76
View File
@@ -3,89 +3,79 @@ const sinon = require('sinon')
const { expect } = require('chai')
const { verifyMochaResults } = require('../verify-mocha-results')
describe('verify-mocha-results', () => {
let cachedEnv = { ...process.env }
if (process.platform !== 'win32') {
describe('verify-mocha-results', () => {
let cachedEnv = { ...process.env }
if (process.platform === 'win32') {
// skip the rest of the tests
return it('fails on windows', async () => {
try {
await verifyMochaResults()
throw new Error('should not reach')
} catch (err) {
expect(err.message).to.equal('verifyMochaResults not supported on Windows')
}
afterEach(() => {
sinon.restore()
Object.assign(process.env, cachedEnv)
})
}
afterEach(() => {
sinon.restore()
Object.assign(process.env, cachedEnv)
})
beforeEach(() => {
process.env.CIRCLE_INTERNAL_CONFIG = '/foo.json'
sinon.stub(fs, 'readFile')
.withArgs('/foo.json').resolves(JSON.stringify({
Dispatched: { TaskInfo: { Environment: { somekey: 'someval' } } },
}))
beforeEach(() => {
process.env.CIRCLE_INTERNAL_CONFIG = '/foo.json'
sinon.stub(fs, 'readFile')
.withArgs('/foo.json').resolves(JSON.stringify({
Dispatched: { TaskInfo: { Environment: { somekey: 'someval' } } },
}))
sinon.stub(fs, 'readdir').withArgs('/tmp/cypress/junit').resolves([
'report.xml',
])
})
it('does not fail with normal report', async () => {
fs.readFile
.withArgs('/tmp/cypress/junit/report.xml')
.resolves('<testsuites name="foo" time="1" tests="10" failures="0">')
await verifyMochaResults()
})
context('env checking', () => {
it('checks for protected env and fails and removes results when found', async () => {
const spy = sinon.stub(fs, 'rm').withArgs('/tmp/cypress/junit', { recursive: true, force: true })
sinon.stub(fs, 'readdir').withArgs('/tmp/cypress/junit').resolves([
'report.xml',
])
})
it('does not fail with normal report', async () => {
fs.readFile
.withArgs('/tmp/cypress/junit/report.xml')
.resolves('<testsuites name="foo" time="1" tests="10" failures="0">someval')
.resolves('<testsuites name="foo" time="1" tests="10" failures="0">')
try {
await verifyMochaResults()
throw new Error('should not reach')
} catch (err) {
expect(err.message).to.include('somekey').and.not.include('someval')
expect(spy.getCalls().length).to.equal(1)
}
await verifyMochaResults()
})
context('env checking', () => {
it('checks for protected env and fails and removes results when found', async () => {
const spy = sinon.stub(fs, 'rm').withArgs('/tmp/cypress/junit', { recursive: true, force: true })
fs.readFile
.withArgs('/tmp/cypress/junit/report.xml')
.resolves('<testsuites name="foo" time="1" tests="10" failures="0">someval')
try {
await verifyMochaResults()
throw new Error('should not reach')
} catch (err) {
expect(err.message).to.include('somekey').and.not.include('someval')
expect(spy.getCalls().length).to.equal(1)
}
})
})
context('test result checking', () => {
it('checks for non-passing tests and fails when found', async () => {
fs.readFile
.withArgs('/tmp/cypress/junit/report.xml')
.resolves('<testsuites name="foo" time="1" tests="10" failures="3">')
try {
await verifyMochaResults()
throw new Error('should not reach')
} catch (err) {
expect(err.message).to.include('Expected the number of failures to be equal to 0')
}
})
it('checks for 0 tests run and fails when found', async () => {
fs.readFile
.withArgs('/tmp/cypress/junit/report.xml')
.resolves('<testsuites name="foo" time="1" tests="0" failures="0">')
try {
await verifyMochaResults()
throw new Error('should not reach')
} catch (err) {
expect(err.message).to.include('Expected the total number of tests to be >0')
}
})
})
})
context('test result checking', () => {
it('checks for non-passing tests and fails when found', async () => {
fs.readFile
.withArgs('/tmp/cypress/junit/report.xml')
.resolves('<testsuites name="foo" time="1" tests="10" failures="3">')
try {
await verifyMochaResults()
throw new Error('should not reach')
} catch (err) {
expect(err.message).to.include('Expected the number of failures to be equal to 0')
}
})
it('checks for 0 tests run and fails when found', async () => {
fs.readFile
.withArgs('/tmp/cypress/junit/report.xml')
.resolves('<testsuites name="foo" time="1" tests="0" failures="0">')
try {
await verifyMochaResults()
throw new Error('should not reach')
} catch (err) {
expect(err.message).to.include('Expected the total number of tests to be >0')
}
})
})
})
}
-2
View File
@@ -95,8 +95,6 @@ async function checkReportFiles (filenames) {
}
async function verifyMochaResults () {
if (process.platform === 'win32') throw new Error('verifyMochaResults not supported on Windows')
try {
const filenames = await fs.readdir(REPORTS_PATH)