From 5292b08028d98cc8b8adee5705921d539777bed5 Mon Sep 17 00:00:00 2001 From: Drew Powers <1369770+drwpow@users.noreply.github.com> Date: Thu, 27 May 2021 14:06:54 -0600 Subject: [PATCH] Make UVU tests time out (#265) --- packages/astro/test/config-path.test.js | 13 +++ packages/astro/test/config-port.test.js | 20 +++- packages/astro/test/helpers.js | 72 ++++++++----- .../astro/test/snowpack-integration.test.js | 102 +++++------------- 4 files changed, 100 insertions(+), 107 deletions(-) diff --git a/packages/astro/test/config-path.test.js b/packages/astro/test/config-path.test.js index 5d78e7027..14cbef4d9 100644 --- a/packages/astro/test/config-path.test.js +++ b/packages/astro/test/config-path.test.js @@ -3,8 +3,17 @@ import * as assert from 'uvu/assert'; import { runDevServer } from './helpers.js'; const ConfigPath = suite('Config path'); +const MAX_TEST_TIME = 10000; // max time this test suite may take const root = new URL('./fixtures/config-path/', import.meta.url); +const timers = {}; + +ConfigPath.before.each(({ __test__ }) => { + timers[__test__] = setTimeout(() => { + throw new Error(`Test "${__test__}" did not finish within allowed time`); + }, MAX_TEST_TIME); +}); + ConfigPath('can be passed via --config', async (context) => { const configPath = new URL('./config/my-config.mjs', root).pathname; const args = ['--config', configPath]; @@ -21,4 +30,8 @@ ConfigPath('can be passed via --config', async (context) => { assert.ok(true, 'Server started'); }); +ConfigPath.after.each(({ __test__ }) => { + clearTimeout(timers[__test__]); +}); + ConfigPath.run(); diff --git a/packages/astro/test/config-port.test.js b/packages/astro/test/config-port.test.js index b8e2d3ee9..f87951fee 100644 --- a/packages/astro/test/config-port.test.js +++ b/packages/astro/test/config-port.test.js @@ -5,8 +5,16 @@ import { runDevServer } from './helpers.js'; import { loadConfig } from '#astro/config'; const ConfigPort = suite('Config path'); +const MAX_TEST_TIME = 10000; // max time this test suite may take const root = new URL('./fixtures/config-port/', import.meta.url); +const timers = {}; + +ConfigPort.before.each(({ __test__ }) => { + timers[__test__] = setTimeout(() => { + throw new Error(`Test "${__test__}" did not finish within allowed time`); + }, MAX_TEST_TIME); +}); ConfigPort('can be specified in the astro config', async (context) => { const astroConfig = await loadConfig(fileURLToPath(root)); @@ -15,17 +23,21 @@ ConfigPort('can be specified in the astro config', async (context) => { ConfigPort('can be specified via --port flag', async (context) => { const args = ['--port', '3002']; - const process = runDevServer(root, args); + const proc = runDevServer(root, args); - process.stdout.setEncoding('utf8'); - for await (const chunk of process.stdout) { + proc.stdout.setEncoding('utf8'); + for await (const chunk of proc.stdout) { if (/Local:/.test(chunk)) { assert.ok(/:3002/.test(chunk), 'Using the right port'); break; } } - process.kill(); + proc.kill(); +}); + +ConfigPort.after.each(({ __test__ }) => { + clearTimeout(timers[__test__]); }); ConfigPort.run(); diff --git a/packages/astro/test/helpers.js b/packages/astro/test/helpers.js index f3bb0a7e0..904cb0345 100644 --- a/packages/astro/test/helpers.js +++ b/packages/astro/test/helpers.js @@ -3,65 +3,87 @@ import { build as astroBuild } from '#astro/build'; import { readFile } from 'fs/promises'; import { createRuntime } from '#astro/runtime'; import { loadConfig } from '#astro/config'; -import * as assert from 'uvu/assert'; import execa from 'execa'; +const MAX_STARTUP_TIME = 7000; // max time startup may take +const MAX_TEST_TIME = 10000; // max time an individual test may take +const MAX_SHUTDOWN_TIME = 3000; // max time shutdown() may take + /** setup fixtures for tests */ export function setup(Suite, fixturePath) { - let runtime, setupError; + let runtime; + const timers = {}; Suite.before(async (context) => { + let timeout = setTimeout(() => { + throw new Error('Startup did not complete within allowed time'); + }, MAX_STARTUP_TIME); + const astroConfig = await loadConfig(fileURLToPath(new URL(fixturePath, import.meta.url))); - const logging = { - level: 'error', - dest: process.stderr, - }; - - try { - runtime = await createRuntime(astroConfig, { logging }); - } catch (err) { - console.error(err); - setupError = err; - } + runtime = await createRuntime(astroConfig, { + logging: { level: 'error', dest: process.stderr }, + }); context.runtime = runtime; + + clearTimeout(timeout); + }); + + Suite.before.each(({ __test__ }) => { + if (timers[__test__]) throw new Error(`Test "${__test__}" already declared`); + timers[__test__] = setTimeout(() => { + throw new Error(`"${__test__}" did not finish within allowed time`); + }, MAX_TEST_TIME); }); Suite.after(async () => { + let timeout = setTimeout(() => { + throw new Error('Shutdown did not complete within allowed time'); + }, MAX_SHUTDOWN_TIME); + (await runtime) && runtime.shutdown(); + + clearTimeout(timeout); }); - Suite('No errors creating a runtime', () => { - assert.equal(setupError, undefined); + Suite.after.each(({ __test__ }) => { + clearTimeout(timers[__test__]); }); } export function setupBuild(Suite, fixturePath) { - let build, setupError; + const timers = {}; Suite.before(async (context) => { + let timeout = setTimeout(() => { + throw new Error('Startup did not complete within allowed time'); + }, MAX_STARTUP_TIME); + const astroConfig = await loadConfig(fileURLToPath(new URL(fixturePath, import.meta.url))); - const logging = { - level: 'error', - dest: process.stderr, - }; - - build = () => astroBuild(astroConfig, logging); - context.build = build; + context.build = () => astroBuild(astroConfig, { level: 'error', dest: process.stderr }); context.readFile = async (path) => { const resolved = fileURLToPath(new URL(`${fixturePath}/${astroConfig.dist}${path}`, import.meta.url)); return readFile(resolved).then((r) => r.toString('utf8')); }; + + clearTimeout(timeout); + }); + + Suite.before.each(({ __test__ }) => { + if (timers[__test__]) throw new Error(`Test "${__test__}" already declared`); + timers[__test__] = setTimeout(() => { + throw new Error(`"${__test__}" did not finish within allowed time`); + }, MAX_TEST_TIME); }); Suite.after(async () => { // Shutdown i guess. }); - Suite('No errors creating a runtime', () => { - assert.equal(setupError, undefined); + Suite.after.each(({ __test__ }) => { + clearTimeout(timers[__test__]); }); } diff --git a/packages/astro/test/snowpack-integration.test.js b/packages/astro/test/snowpack-integration.test.js index abb06517f..24388c49b 100644 --- a/packages/astro/test/snowpack-integration.test.js +++ b/packages/astro/test/snowpack-integration.test.js @@ -1,91 +1,37 @@ +import path from 'path'; +import glob from 'tiny-glob/sync.js'; import { fileURLToPath } from 'url'; import { suite } from 'uvu'; import * as assert from 'uvu/assert'; -import { createRuntime } from '#astro/runtime'; -import { loadConfig } from '#astro/config'; -import { promises as fsPromises } from 'fs'; -import { relative as pathRelative } from 'path'; - -const { readdir, stat } = fsPromises; +import { setup } from './helpers.js'; const SnowpackDev = suite('snowpack.dev'); +setup(SnowpackDev, '../../../examples/snowpack'); -const snowpackDir = new URL('../../../examples/snowpack/', import.meta.url); - -let runtime, cwd, setupError; - -SnowpackDev.before(async () => { - // Bug: Snowpack config is still loaded relative to the current working directory. - cwd = process.cwd(); - process.chdir(fileURLToPath(snowpackDir)); - - const astroConfig = await loadConfig(fileURLToPath(snowpackDir)); - - const logging = { - level: 'error', - dest: process.stderr, - }; - - try { - runtime = await createRuntime(astroConfig, { logging }); - } catch (err) { - console.error(err); - setupError = err; - } -}); - -SnowpackDev.after(async () => { - process.chdir(cwd); - (await runtime) && runtime.shutdown(); -}); -/** create an iterator for all page files */ -async function* allPageFiles(root) { - for (const filename of await readdir(root)) { - const fullpath = new URL(filename, root); - const info = await stat(fullpath); - - if (info.isDirectory()) { - yield* allPageFiles(new URL(fullpath + '/')); - } else { - yield fullpath; - } - } -} -/** create an iterator for all pages and yield the relative paths */ -async function* allPages(root) { - for await (let fileURL of allPageFiles(root)) { - let bare = fileURLToPath(fileURL) - .replace(/\.(astro|md)$/, '') - .replace(/index$/, ''); - - yield '/' + pathRelative(fileURLToPath(root), bare); - } +// convert file path to its final url +function formatURL(filepath) { + return filepath + .replace(/^\/?/, '/') // add / to beginning, if missing + .replace(/(index)?\.(astro|md)$/, '') // remove .astro and .md extensions + .replace(/\/$/, ''); // remove trailing slash, if any } -SnowpackDev('No error creating the runtime', () => { - assert.equal(setupError, undefined); +// declaring routes individually helps us run many quick tests rather than one giant slow test +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../../examples/snowpack/src/pages'); +let pages = glob('**/*.{astro,md}', { cwd: root, onlyFiles: true }) + .filter((page) => !page.includes('proof-of-concept-dynamic')) + .map(formatURL); + +SnowpackDev('Pages successfully scanned', () => { + assert.ok(pages.length > 0); }); -SnowpackDev('Can load every page', async () => { - const failed = []; - - const pageRoot = new URL('./src/pages/', snowpackDir); - for await (let pathname of allPages(pageRoot)) { - if (pathname.includes('proof-of-concept-dynamic')) { - continue; - } +for (const pathname of pages) { + SnowpackDev(`Loads "${pathname}"`, async ({ runtime }) => { const result = await runtime.load(pathname); - if (result.statusCode === 500) { - failed.push({ ...result, pathname }); - continue; - } - assert.equal(result.statusCode, 200, `Loading ${pathname}`); - } - - if (failed.length > 0) { - console.error(failed); - } - assert.equal(failed.length, 0, 'Failed pages'); -}); + assert.equal(result.statusCode, 200); + return; + }); +} SnowpackDev.run();