Skip to content

Commit

Permalink
perf(cypress): pack cypress runfiles into a single tar
Browse files Browse the repository at this point in the history
  • Loading branch information
Dan Muller authored and alexeagle committed Nov 20, 2020
1 parent 799abf5 commit 185c632
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 99 deletions.
2 changes: 1 addition & 1 deletion examples/cypress/package.json
Expand Up @@ -16,4 +16,4 @@
"scripts": {
"test": "bazel test //..."
}
}
}
1 change: 1 addition & 0 deletions packages/cypress/BUILD.bazel
Expand Up @@ -95,6 +95,7 @@ pkg_npm(
"@build_bazel_rules_nodejs//packages/cypress:internal/plugins/base.js": "TEMPLATED_node_modules_workspace_name//@bazel/cypress:internal/plugins/base.js",
"@build_bazel_rules_nodejs//packages/cypress:internal/plugins/index.template.js": "TEMPLATED_node_modules_workspace_name//@bazel/cypress:internal/plugins/index.template.js",
"@build_bazel_rules_nodejs//packages/cypress:internal/run-cypress.js": "TEMPLATED_node_modules_workspace_name//@bazel/cypress:internal/run-cypress.js",
"TEMPLATED_node_modules_workspace_name//tar": "TEMPLATED_node_modules_workspace_name//@bazel/cypress",
},
deps = [
":npm_version_check",
Expand Down
138 changes: 62 additions & 76 deletions packages/cypress/internal/install-cypress.js
Expand Up @@ -9,31 +9,42 @@
* repository rule while the file system remains read/write.
*/

const {spawnSync, spawn} = require('child_process');
const {readdirSync, statSync, writeFileSync, mkdirSync} = require('fs');
const {spawnSync} = require('child_process');
const {readdirSync, statSync, writeFileSync, mkdirSync, createWriteStream} = require('fs');
const {
join,
basename,
relative,
dirname,
} = require('path');

const nodePath = process.argv[1];
const cwd = process.cwd();
const cypressBin = process.argv[2];

// Sandboxing doesn't work on windows, so we can just use the global cypress cache.
if (process.platform === 'win32') {
installGlobalCypressCache();
process.exit(0);
}
const nodeModulesPath = join(cypressBin.split('node_modules')[0], 'node_modules');
const tar = require(require.resolve('tar', {
paths: [
join(nodeModulesPath, '@bazel', 'cypress', 'node_modules'),
nodeModulesPath,
]
}));

async function main() {
// Sandboxing doesn't work on windows, so we can just use the global cypress cache.
if (process.platform === 'win32') {
installGlobalCypressCache();
process.exit(0);
}

// Attempt to install the cypress cache within the bazel sandbox and fallback to a global cypress
// cache as a last resort.
try {
installSandboxedCypressCache()
} catch (e) {
console.error('ERROR', e);
installGlobalCypressCache();
// Attempt to install the cypress cache within the bazel sandbox and fallback to a global cypress
// cache as a last resort.
try {
await installSandboxedCypressCache()
} catch (e) {
console.error('ERROR', e);
installGlobalCypressCache();
}
}

function installGlobalCypressCache() {
Expand Down Expand Up @@ -70,11 +81,11 @@ cypress_web_test = _cypress_web_test`)
}


function installSandboxedCypressCache() {
mkdirSync(join(cwd, 'cypress-cache'))
async function installSandboxedCypressCache() {
mkdirSync(join(cwd, 'cypress-install'))

const env = {
CYPRESS_CACHE_FOLDER: join(cwd, 'cypress-cache'),
CYPRESS_CACHE_FOLDER: join(cwd, 'cypress-install'),
PATH: `${dirname(nodePath)}:${process.env.PATH}`,
DEBUG: 'cypress:*'
}
Expand Down Expand Up @@ -119,19 +130,24 @@ function installSandboxedCypressCache() {
throw new Error(`cypress verify failed`);
}

writeFileSync(join(env.CYPRESS_CACHE_FOLDER, 'bazel_cypress.json'), JSON.stringify({
cypressExecutable: relative(cwd, CYPRESS_RUN_BINARY),
}));

const cacheFiles = [];
walkDir(env.CYPRESS_CACHE_FOLDER, (filePath) => {
cacheFiles.push(filePath);
cacheFiles.push(relative(cwd, filePath));
});

const archiveName = 'cypress.archive';
await createCypressArchive(cacheFiles, join(cwd, archiveName));

writeFileSync('index.bzl', `load(
"//:packages/cypress/internal/cypress_web_test.bzl",
_cypress_web_test = "cypress_web_test",
)
cypress_web_test = _cypress_web_test`)
writeFileSync(
'BUILD.bazel',
`
writeFileSync('BUILD.bazel', `
package(default_visibility = ["//visibility:public"])
exports_files([
Expand All @@ -140,62 +156,32 @@ exports_files([
])
filegroup(
name = "cypress_cache",
srcs = ${
process.platform === 'darwin' ?
// On mac we are required to include cache files including spaces. These can only be
// included using a glob.
'glob(["cypress-cache/**/*"]),' :
// On unix the only no files containing spaces are required to run cypress.
` [
${
cacheFiles.filter(f => !f.includes(' '))
.map(f => `"${relative(cwd, f)}"`)
.join(',\n ')}
]`}
)
filegroup(
name = "cypress_executable",
srcs = ["${relative(cwd, CYPRESS_RUN_BINARY)}"]
name = "cypress_archive",
srcs = ["${relative(cwd, archiveName)}"],
)
`.trim())
}

function createCypressArchive(cypressFiles, archiveName) {
return new Promise((resolve, reject) => {
const writeStream = createWriteStream(archiveName);

tar.create(
{
gzip: false,
portable: true,
noMtime: true,
},
cypressFiles)
.pipe(writeStream)
.on('finish', (err) => {
if (err) {
return reject(err);
}

return resolve();
})
});
}

// On mac, the first run of cypress requires write access to the filesystem.
if (process.platform === 'darwin') {
const http = require('http');
const server = http.createServer((_request, response) => {
response.writeHead(200, {'Content-Type': 'text/html'});
response.write('<html><body>hello-world</body></html>\n');
response.end();
})
.listen(0, '127.0.0.1');
server.on('listening', () => {
const baseUrl = `http://127.0.0.1:${server.address().port}`;
writeFileSync(
'cypress.json', JSON.stringify({baseUrl, 'integrationFolder': cwd, video: false}))
writeFileSync('spec.js', `
describe('hello', () => {
it('should find hello', () => {
cy.visit('${baseUrl}');
cy.contains('hello');
});
});
`)


spawn(
`${cypressBin}`, ['run', '--config-file=cypress.json', '--headless', '--spec=spec.js'],
spawnOptions)
.on('exit', (code) => {
server.close();

if (code !== 0) {
throw new Error('Failed to perform a dry-run of cypress')
}
})
})
}
}
main()
84 changes: 68 additions & 16 deletions packages/cypress/internal/run-cypress.js
@@ -1,21 +1,22 @@
const runfiles = require(process.env['BAZEL_NODE_RUNFILES_HELPER']);
const init = require('cypress/lib/cli').init;
const {join} = require('path');
const {readFileSync} = require('fs');

const [node, entry, configFilePath, pluginsFilePath, cypressExecutable, ...args] = process.argv;

if (cypressExecutable) {
process.env.CYPRESS_RUN_BINARY =
join(process.cwd(), cypressExecutable.replace('external/', '../'));
process.env.CYPRESS_CACHE_FOLDER =
join(process.env.CYPRESS_RUN_BINARY.split('/cypress-cache/')[0], '/cypress-cache');
process.env.HOME = process.env['TEST_TMPDIR'];
}
const [node, entry, configFilePath, pluginsFilePath, cypressTarPath, cypressBin, ...args] =
process.argv;

const pluginsFile = runfiles.resolveWorkspaceRelative(pluginsFilePath).replace(process.cwd(), '.');
const configFile = runfiles.resolveWorkspaceRelative(configFilePath).replace(process.cwd(), '.');

function invokeCypressWithCommand(command) {
async function invokeCypressWithCommand(command) {
process.env.HOME = process.env['TEST_TMPDIR'];

if (cypressTarPath) {
const resolvedArchivePath = join(cypressTarPath.replace('external/', '../'));
await untarCypress(resolvedArchivePath, join(process.env['TEST_TMPDIR']))
}

init([
node,
entry,
Expand All @@ -28,10 +29,61 @@ function invokeCypressWithCommand(command) {
]);
}

// Detect that we are running as a test, by using well-known environment
// variables. See go/test-encyclopedia
if (!process.env.BUILD_WORKSPACE_DIRECTORY) {
invokeCypressWithCommand('run');
} else {
invokeCypressWithCommand('open');


function untarCypress(cypressTarPath, outputPath) {
return new Promise((resolve, reject) => {
const nodeModulesPath = join(
process.cwd(), cypressBin.replace('external/', '../').split('node_modules')[0],
'node_modules');

const tar = require(require.resolve('tar', {
paths: [
join(nodeModulesPath, '@bazel', 'cypress', 'node_modules'),
nodeModulesPath,
]
}));


tar.x(
{
cwd: outputPath,
file: cypressTarPath,
noMtime: true,
},
err => {
if (err) {
return reject(err);
}

try {
const {cypressExecutable} =
JSON.parse(readFileSync(join(outputPath, 'cypress-install', 'bazel_cypress.json')));

process.env.CYPRESS_RUN_BINARY = join(outputPath, cypressExecutable);
process.env.CYPRESS_CACHE_FOLDER = outputPath;
} catch (err) {
return reject(err)
}

return resolve();
})
});
}

async function main() {
try {
// Detect that we are running as a test, by using well-known environment
// variables. See go/test-encyclopedia
if (!process.env.BUILD_WORKSPACE_DIRECTORY) {
await invokeCypressWithCommand('run');
} else {
await invokeCypressWithCommand('open');
}
} catch (e) {
console.error(e);
process.exit(1)
}
}

main();
15 changes: 10 additions & 5 deletions packages/cypress/internal/template.cypress_web_test.bzl
Expand Up @@ -80,8 +80,10 @@ def cypress_web_test(
plugins_file = Label("//plugins/base.js"),
data = [],
templated_args = [],
cypress_cache = Label("//:cypress_cache"),
cypress_executable = Label("//:cypress_executable"),
cypress = Label("TEMPLATED_node_modules_workspace_name//cypress"),
cypress_archive = Label("//:cypress_archive"),
cypress_bin = Label("TEMPLATED_node_modules_workspace_name//:node_modules/cypress/bin/cypress"),
tar = Label("TEMPLATED_node_modules_workspace_name//tar"),
**kwargs):
cypress_plugin = "{name}_cypress_plugin".format(name = name)
tags = kwargs.pop("tags", []) + ["cypress"]
Expand All @@ -101,8 +103,10 @@ def cypress_web_test(
tags = tags,
data = data + [
plugins_file,
cypress_cache,
cypress_executable,
cypress_archive,
cypress_bin,
cypress,
tar,
"{cypress_plugin}".format(cypress_plugin = cypress_plugin),
"{config_file}".format(config_file = config_file),
] + srcs,
Expand All @@ -111,7 +115,8 @@ def cypress_web_test(
"--nobazel_patch_module_resolver",
"$(rootpath {config_file})".format(config_file = config_file),
"$(rootpath {cypress_plugin})".format(cypress_plugin = cypress_plugin),
"$(rootpath {cypress_executable})".format(cypress_executable = cypress_executable),
"$(rootpath {cypress_archive})".format(cypress_archive = cypress_archive),
"$(rootpath {cypress_bin})".format(cypress_bin = cypress_bin),
] + templated_args,
**kwargs
)
Expand Down
3 changes: 3 additions & 0 deletions packages/cypress/package.json
@@ -1,5 +1,8 @@
{
"name": "@bazel/cypress",
"dependencies": {
"tar": "6.0.5"
},
"peerDependencies": {
"cypress": ">=4.7.0"
},
Expand Down
3 changes: 2 additions & 1 deletion packages/cypress/test/package.json
Expand Up @@ -5,6 +5,7 @@
"express": "4.17.1",
"cypress": "^5.2.0",
"@types/node": "14.0.14",
"typescript": "3.9.5"
"typescript": "3.9.5",
"tar": "6.0.5"
}
}

0 comments on commit 185c632

Please sign in to comment.