mirror of
https://github.com/LukeHagar/vercel.git
synced 2025-12-09 21:07:46 +00:00
[cli] Migrate Dev tests from ava to jest (#7822)
* Migrate ava cli dev tests to jest * remove outdated test * update tests * Update utils * update stdio to fix stalling * test stdio tests * add debug logs * more logs * use strip ansi * update * add exit flag and fix next dev test * update timeout for mac ci * update cancel * update cancel tests.yml -> test.yml * update other ids -> names * remove outdated test * de-dupe scripts * remove redwood dev test due to size
This commit is contained in:
495
packages/cli/test/dev/utils.js
Normal file
495
packages/cli/test/dev/utils.js
Normal file
@@ -0,0 +1,495 @@
|
||||
const fs = require('fs-extra');
|
||||
const { join, resolve } = require('path');
|
||||
const _execa = require('execa');
|
||||
const fetch = require('node-fetch');
|
||||
const retry = require('async-retry');
|
||||
const { satisfies } = require('semver');
|
||||
const stripAnsi = require('strip-ansi');
|
||||
const { getDistTag } = require('../../src/util/get-dist-tag');
|
||||
const { version: cliVersion } = require('../../package.json');
|
||||
const {
|
||||
fetchCachedToken,
|
||||
} = require('../../../../test/lib/deployment/now-deploy');
|
||||
|
||||
jest.setTimeout(6 * 60 * 1000);
|
||||
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
const isCanary = () => getDistTag(cliVersion) === 'canary';
|
||||
|
||||
let port = 3000;
|
||||
|
||||
const binaryPath = resolve(__dirname, `../../scripts/start.js`);
|
||||
const fixture = name => join('test', 'dev', 'fixtures', name);
|
||||
const fixtureAbsolute = name => join(__dirname, 'fixtures', name);
|
||||
const exampleAbsolute = name =>
|
||||
join(__dirname, '..', '..', '..', '..', 'examples', name);
|
||||
|
||||
let processCounter = 0;
|
||||
const processList = new Map();
|
||||
|
||||
function execa(...args) {
|
||||
const procId = ++processCounter;
|
||||
const child = _execa(...args);
|
||||
|
||||
processList.set(procId, child);
|
||||
child.on('exit', () => processList.delete(procId));
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
function fetchWithRetry(url, opts = {}) {
|
||||
return retry(
|
||||
async () => {
|
||||
const res = await fetch(url, opts);
|
||||
|
||||
if (res.status !== opts.status) {
|
||||
const text = await res.text();
|
||||
throw new Error(
|
||||
`Failed to fetch ${url} with status ${res.status} (expected ${opts.status}):\n\n${text}\n\n`
|
||||
);
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
{
|
||||
retries: opts.retries || 3,
|
||||
factor: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createResolver() {
|
||||
let resolver;
|
||||
const p = new Promise(res => (resolver = res));
|
||||
p.resolve = resolver;
|
||||
return p;
|
||||
}
|
||||
|
||||
function formatOutput({ stderr, stdout }) {
|
||||
return `Received:\n"${stderr}"\n"${stdout}"`;
|
||||
}
|
||||
|
||||
function printOutput(fixture, stdout, stderr) {
|
||||
const lines = (
|
||||
`\nOutput for "${fixture}"\n` +
|
||||
`\n----- stdout -----\n` +
|
||||
stdout +
|
||||
`\n----- stderr -----\n` +
|
||||
stderr
|
||||
).split('\n');
|
||||
|
||||
const getPrefix = nr => {
|
||||
return nr === 0 ? '╭' : nr === lines.length - 1 ? '╰' : '│';
|
||||
};
|
||||
|
||||
console.log(
|
||||
lines.map((line, index) => ` ${getPrefix(index)} ${line}`).join('\n')
|
||||
);
|
||||
}
|
||||
|
||||
function shouldSkip(name, versions) {
|
||||
if (!satisfies(process.version, versions)) {
|
||||
console.log(`Skipping "${name}" because it requires "${versions}".`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function validateResponseHeaders(res) {
|
||||
if (res.status < 500) {
|
||||
expect(res.headers.get('x-vercel-id')).toBeTruthy();
|
||||
expect(res.headers.get('cache-control').length > 0).toBeTruthy;
|
||||
}
|
||||
}
|
||||
|
||||
async function exec(directory, args = []) {
|
||||
const token = await fetchCachedToken();
|
||||
return execa(
|
||||
binaryPath,
|
||||
[
|
||||
'dev',
|
||||
directory,
|
||||
'-t',
|
||||
token,
|
||||
...(process.env.VERCEL_TEAM_ID
|
||||
? ['--scope', process.env.VERCEL_TEAM_ID]
|
||||
: []),
|
||||
...args,
|
||||
],
|
||||
{
|
||||
reject: false,
|
||||
shell: true,
|
||||
env: { __VERCEL_SKIP_DEV_CMD: 1 },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function runNpmInstall(fixturePath) {
|
||||
if (await fs.pathExists(join(fixturePath, 'package.json'))) {
|
||||
await execa('yarn', ['install'], {
|
||||
cwd: fixturePath,
|
||||
shell: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function testPath(
|
||||
isDev,
|
||||
origin,
|
||||
status,
|
||||
path,
|
||||
expectedText,
|
||||
expectedHeaders = {},
|
||||
fetchOpts = {}
|
||||
) {
|
||||
const opts = {
|
||||
...fetchOpts,
|
||||
redirect: 'manual-dont-change',
|
||||
retries: 5,
|
||||
status,
|
||||
};
|
||||
const url = `${origin}${path}`;
|
||||
const res = await fetchWithRetry(url, opts);
|
||||
const msg = `Testing response from ${fetchOpts.method || 'GET'} ${url}`;
|
||||
console.log(msg);
|
||||
expect(res.status).toBe(status);
|
||||
validateResponseHeaders(res);
|
||||
if (typeof expectedText === 'string') {
|
||||
const actualText = await res.text();
|
||||
expect(actualText.trim()).toBe(expectedText.trim());
|
||||
} else if (typeof expectedText === 'function') {
|
||||
const actualText = await res.text();
|
||||
await expectedText(actualText, res, isDev);
|
||||
} else if (expectedText instanceof RegExp) {
|
||||
const actualText = await res.text();
|
||||
expectedText.lastIndex = 0; // reset since we test twice
|
||||
expect(actualText).toMatch(expectedText);
|
||||
}
|
||||
if (expectedHeaders) {
|
||||
Object.entries(expectedHeaders).forEach(([key, expectedValue]) => {
|
||||
let actualValue = res.headers.get(key);
|
||||
if (key.toLowerCase() === 'location' && actualValue === '//') {
|
||||
// HACK: `node-fetch` has strange behavior for location header so fix it
|
||||
// with `manual-dont-change` opt and convert double slash to single.
|
||||
// See https://github.com/node-fetch/node-fetch/issues/417#issuecomment-587233352
|
||||
actualValue = '/';
|
||||
}
|
||||
expect(actualValue).toBe(expectedValue);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function testFixture(directory, opts = {}, args = []) {
|
||||
await runNpmInstall(directory);
|
||||
|
||||
const token = await fetchCachedToken();
|
||||
const dev = execa(
|
||||
binaryPath,
|
||||
[
|
||||
'dev',
|
||||
directory,
|
||||
'-t',
|
||||
token,
|
||||
...(process.env.VERCEL_TEAM_ID
|
||||
? ['--scope', process.env.VERCEL_TEAM_ID]
|
||||
: []),
|
||||
'-l',
|
||||
String(port),
|
||||
...args,
|
||||
],
|
||||
{
|
||||
reject: false,
|
||||
detached: true,
|
||||
shell: true,
|
||||
stdio: 'pipe',
|
||||
...opts,
|
||||
env: { ...opts.env, __VERCEL_SKIP_DEV_CMD: 1 },
|
||||
}
|
||||
);
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
const readyResolver = createResolver();
|
||||
const exitResolver = createResolver();
|
||||
|
||||
dev.stdout.setEncoding('utf8');
|
||||
dev.stderr.setEncoding('utf8');
|
||||
|
||||
dev.stdout.on('data', data => {
|
||||
stdout += data;
|
||||
});
|
||||
dev.stderr.on('data', data => {
|
||||
stderr += data;
|
||||
|
||||
if (stripAnsi(stderr).includes('Ready! Available at')) {
|
||||
readyResolver.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
let printedOutput = false;
|
||||
|
||||
dev.on('exit', () => {
|
||||
if (!printedOutput) {
|
||||
printOutput(directory, stdout, stderr);
|
||||
printedOutput = true;
|
||||
}
|
||||
exitResolver.resolve();
|
||||
readyResolver.resolve();
|
||||
});
|
||||
|
||||
dev.on('error', () => {
|
||||
if (!printedOutput) {
|
||||
printOutput(directory, stdout, stderr);
|
||||
printedOutput = true;
|
||||
}
|
||||
exitResolver.resolve();
|
||||
readyResolver.resolve();
|
||||
});
|
||||
|
||||
dev._kill = dev.kill;
|
||||
dev.kill = async (...args) => {
|
||||
dev._kill(...args);
|
||||
await exitResolver;
|
||||
};
|
||||
|
||||
return {
|
||||
dev,
|
||||
port,
|
||||
readyResolver,
|
||||
};
|
||||
}
|
||||
|
||||
function testFixtureStdio(
|
||||
directory,
|
||||
fn,
|
||||
{ expectedCode = 0, skipDeploy, isExample, projectSettings } = {}
|
||||
) {
|
||||
return async () => {
|
||||
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
||||
if (isExample && nodeMajor < 12) {
|
||||
console.log(`Skipping ${directory} on Node ${process.version}`);
|
||||
return;
|
||||
}
|
||||
const cwd = isExample
|
||||
? exampleAbsolute(directory)
|
||||
: fixtureAbsolute(directory);
|
||||
const token = await fetchCachedToken();
|
||||
let deploymentUrl;
|
||||
|
||||
// Deploy fixture and link project
|
||||
if (!skipDeploy) {
|
||||
const projectJsonPath = join(cwd, '.vercel', 'project.json');
|
||||
await fs.remove(projectJsonPath);
|
||||
const gitignore = join(cwd, '.gitignore');
|
||||
const hasGitignore = await fs.pathExists(gitignore);
|
||||
|
||||
try {
|
||||
// Run `vc link`
|
||||
const linkResult = await execa(
|
||||
binaryPath,
|
||||
[
|
||||
'-t',
|
||||
token,
|
||||
...(process.env.VERCEL_TEAM_ID
|
||||
? ['--scope', process.env.VERCEL_TEAM_ID]
|
||||
: []),
|
||||
'link',
|
||||
'--confirm',
|
||||
],
|
||||
{ cwd, stdio: 'pipe', reject: false }
|
||||
);
|
||||
console.log({
|
||||
stderr: linkResult.stderr,
|
||||
stdout: linkResult.stdout,
|
||||
});
|
||||
expect(linkResult.exitCode).toBe(0);
|
||||
|
||||
// Patch the project with any non-default properties
|
||||
if (projectSettings) {
|
||||
const { projectId } = await fs.readJson(projectJsonPath);
|
||||
const res = await fetchWithRetry(
|
||||
`https://api.vercel.com/v2/projects/${projectId}${
|
||||
process.env.VERCEL_TEAM_ID
|
||||
? `?teamId=${process.env.VERCEL_TEAM_ID}`
|
||||
: ''
|
||||
}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(projectSettings),
|
||||
retries: 3,
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
}
|
||||
|
||||
// Run `vc deploy`
|
||||
let deployResult = await execa(
|
||||
binaryPath,
|
||||
[
|
||||
'-t',
|
||||
token,
|
||||
...(process.env.VERCEL_TEAM_ID
|
||||
? ['--scope', process.env.VERCEL_TEAM_ID]
|
||||
: []),
|
||||
'deploy',
|
||||
'--public',
|
||||
'--no-clipboard',
|
||||
'--debug',
|
||||
],
|
||||
{ cwd, stdio: 'pipe', reject: false }
|
||||
);
|
||||
console.log({
|
||||
exitCode: deployResult.exitCode,
|
||||
stdout: deployResult.stdout,
|
||||
stderr: deployResult.stderr,
|
||||
});
|
||||
expect(deployResult.exitCode).toBe(expectedCode);
|
||||
if (expectedCode === 0) {
|
||||
deploymentUrl = new URL(deployResult.stdout).host;
|
||||
}
|
||||
} finally {
|
||||
if (!hasGitignore) {
|
||||
await fs.remove(gitignore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start dev
|
||||
let dev;
|
||||
|
||||
await runNpmInstall(cwd);
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
const readyResolver = createResolver();
|
||||
const exitResolver = createResolver();
|
||||
|
||||
try {
|
||||
let printedOutput = false;
|
||||
|
||||
const env = skipDeploy
|
||||
? { ...process.env, __VERCEL_SKIP_DEV_CMD: 1 }
|
||||
: process.env;
|
||||
dev = execa(
|
||||
binaryPath,
|
||||
[
|
||||
'dev',
|
||||
'-l',
|
||||
port,
|
||||
'-t',
|
||||
token,
|
||||
...(process.env.VERCEL_TEAM_ID
|
||||
? ['--scope', process.env.VERCEL_TEAM_ID]
|
||||
: []),
|
||||
'--debug',
|
||||
],
|
||||
{
|
||||
cwd,
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
||||
dev.stdout.setEncoding('utf8');
|
||||
dev.stderr.setEncoding('utf8');
|
||||
|
||||
dev.stdout.pipe(process.stdout);
|
||||
dev.stderr.pipe(process.stderr);
|
||||
|
||||
dev.stdout.on('data', data => {
|
||||
stdout += data;
|
||||
});
|
||||
|
||||
dev.stderr.on('data', data => {
|
||||
stderr += data;
|
||||
|
||||
if (stripAnsi(data).includes('Ready! Available at')) {
|
||||
readyResolver.resolve();
|
||||
}
|
||||
|
||||
if (stderr.includes(`Requested port ${port} is already in use`)) {
|
||||
dev.kill('SIGTERM');
|
||||
throw new Error(
|
||||
`Failed for "${directory}" with port ${port} with stderr "${stderr}".`
|
||||
);
|
||||
}
|
||||
|
||||
if (stderr.includes('Command failed') || stderr.includes('Error!')) {
|
||||
dev.kill('SIGTERM');
|
||||
throw new Error(`Failed for "${directory}" with stderr "${stderr}".`);
|
||||
}
|
||||
});
|
||||
|
||||
dev.on('exit', () => {
|
||||
if (!printedOutput) {
|
||||
printOutput(directory, stdout, stderr);
|
||||
printedOutput = true;
|
||||
}
|
||||
exitResolver.resolve();
|
||||
});
|
||||
|
||||
dev.on('error', () => {
|
||||
if (!printedOutput) {
|
||||
printOutput(directory, stdout, stderr);
|
||||
printedOutput = true;
|
||||
}
|
||||
exitResolver.resolve();
|
||||
});
|
||||
|
||||
await readyResolver;
|
||||
|
||||
const helperTestPath = async (...args) => {
|
||||
if (!skipDeploy) {
|
||||
await testPath(false, `https://${deploymentUrl}`, ...args);
|
||||
}
|
||||
await testPath(true, `http://localhost:${port}`, ...args);
|
||||
};
|
||||
await fn(helperTestPath, port);
|
||||
} finally {
|
||||
dev.kill('SIGTERM');
|
||||
await exitResolver;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
port = ++port;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
Array.from(processList).map(([_procId, proc]) => {
|
||||
if (proc.killed === false) {
|
||||
console.log(
|
||||
`killing process ${proc.pid} "${proc.spawnargs.join(' ')}"`
|
||||
);
|
||||
|
||||
try {
|
||||
process.kill(proc.pid, 'SIGTERM');
|
||||
} catch (err) {
|
||||
// Was already killed
|
||||
console.error(`Failed to kill process`, proc.pid, err);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
sleep,
|
||||
isCanary,
|
||||
testPath,
|
||||
testFixture,
|
||||
testFixtureStdio,
|
||||
exec,
|
||||
formatOutput,
|
||||
shouldSkip,
|
||||
fixture,
|
||||
fetch,
|
||||
validateResponseHeaders,
|
||||
};
|
||||
Reference in New Issue
Block a user