diff --git a/test/integration/graceful-shutdown/test/index.test.ts b/test/production/graceful-shutdown/index.test.ts similarity index 63% rename from test/integration/graceful-shutdown/test/index.test.ts rename to test/production/graceful-shutdown/index.test.ts index cdea34843359b..7e4aa03e58bdf 100644 --- a/test/integration/graceful-shutdown/test/index.test.ts +++ b/test/production/graceful-shutdown/index.test.ts @@ -1,9 +1,9 @@ -/* eslint-env jest */ - import { join } from 'path' +import { NextInstance, createNext, FileRef } from 'e2e-utils' import { fetchViaHTTP, findPort, + initNextServerScript, isAppRunning, killApp, launchApp, @@ -11,10 +11,11 @@ import { nextStart, waitFor, } from 'next-test-utils' -import { LONG_RUNNING_MS } from '../pages/api/long-running' +import fs from 'fs-extra' +import glob from 'glob' +import { LONG_RUNNING_MS } from './pages/api/long-running' import { once } from 'events' -const appDir = join(__dirname, '../') let appPort let app @@ -22,6 +23,98 @@ function assertDefined(value: T | void): asserts value is T { expect(value).toBeDefined() } +describe('Graceful Shutdown', () => { + describe('development (next dev)', () => { + beforeEach(async () => { + appPort = await findPort() + app = await launchApp(__dirname, appPort) + }) + afterEach(() => killApp(app)) + + runTests(true) + }) + ;(process.env.TURBOPACK ? describe.skip : describe)( + 'production (next start)', + () => { + beforeAll(async () => { + await nextBuild(__dirname) + }) + beforeEach(async () => { + appPort = await findPort() + app = await nextStart(__dirname, appPort) + }) + afterEach(() => killApp(app)) + + runTests() + } + ) + ;(process.env.TURBOPACK ? describe.skip : describe)( + 'production (standalone mode)', + () => { + let next: NextInstance + let serverFile + + const projectFiles = { + 'next.config.mjs': `export default { output: 'standalone' }`, + } + + for (const file of glob.sync('*', { cwd: __dirname, dot: false })) { + projectFiles[file] = new FileRef(join(__dirname, file)) + } + + beforeAll(async () => { + next = await createNext({ + files: projectFiles, + dependencies: { + swr: 'latest', + }, + }) + + await next.stop() + + await fs.move( + join(next.testDir, '.next/standalone'), + join(next.testDir, 'standalone') + ) + + for (const file of await fs.readdir(next.testDir)) { + if (file !== 'standalone') { + await fs.remove(join(next.testDir, file)) + } + } + const files = glob.sync('**/*', { + cwd: join(next.testDir, 'standalone/.next/server/pages'), + dot: true, + }) + + for (const file of files) { + if (file.endsWith('.json') || file.endsWith('.html')) { + await fs.remove(join(next.testDir, '.next/server', file)) + } + } + + serverFile = join(next.testDir, 'standalone/server.js') + }) + + beforeEach(async () => { + appPort = await findPort() + app = await initNextServerScript( + serverFile, + /- Local:/, + { ...process.env, PORT: appPort.toString() }, + undefined, + { cwd: next.testDir } + ) + }) + afterEach(() => killApp(app)) + + afterAll(() => next.destroy()) + + runTests() + } + ) +}) + function runTests(dev = false) { if (dev) { it('should shut down child immediately', async () => { @@ -128,13 +221,12 @@ function runTests(dev = false) { process.kill(app.pid, 'SIGTERM') expect(isAppRunning(app)).toBe(true) + // yield event loop to allow server to start the shutdown process + await waitFor(20) await expect( fetchViaHTTP(appPort, '/api/long-running') ).rejects.toThrow() - // App is still running briefly while server is closing - expect(isAppRunning(app)).toBe(true) - // App finally shuts down await appKilledPromise expect(isAppRunning(app)).toBe(false) @@ -142,27 +234,3 @@ function runTests(dev = false) { }) } } - -describe('API routes', () => { - describe('dev support', () => { - beforeEach(async () => { - appPort = await findPort() - app = await launchApp(appDir, appPort) - }) - afterEach(() => killApp(app)) - - runTests(true) - }) - ;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => { - beforeAll(async () => { - await nextBuild(appDir) - }) - beforeEach(async () => { - appPort = await findPort() - app = await nextStart(appDir, appPort) - }) - afterEach(() => killApp(app)) - - runTests() - }) -}) diff --git a/test/integration/graceful-shutdown/pages/api/long-running.ts b/test/production/graceful-shutdown/pages/api/long-running.ts similarity index 100% rename from test/integration/graceful-shutdown/pages/api/long-running.ts rename to test/production/graceful-shutdown/pages/api/long-running.ts diff --git a/test/production/graceful-shutdown/tsconfig.json b/test/production/graceful-shutdown/tsconfig.json new file mode 100644 index 0000000000000..1c4f693a991e7 --- /dev/null +++ b/test/production/graceful-shutdown/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve" + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/test/production/standalone-mode/graceful-shutdown/index.test.ts b/test/production/standalone-mode/graceful-shutdown/index.test.ts deleted file mode 100644 index 826faa109b913..0000000000000 --- a/test/production/standalone-mode/graceful-shutdown/index.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { NextInstance, createNext } from 'e2e-utils' -import { - fetchViaHTTP, - findPort, - initNextServerScript, - isAppRunning, - killApp, - waitFor, -} from 'next-test-utils' -import glob from 'glob' -import { join } from 'path' -import fs from 'fs-extra' -import { once } from 'events' - -import { LONG_RUNNING_MS } from './pages/api/long-running' - -function assertDefined(value: T | void): asserts value is T { - expect(value).toBeDefined() -} - -describe('standalone mode - graceful shutdown', () => { - let next: NextInstance - let appPort - let serverFile - let app - - beforeAll(async () => { - next = await createNext({ - files: __dirname, - dependencies: { - swr: 'latest', - }, - }) - - await next.stop() - - await fs.move( - join(next.testDir, '.next/standalone'), - join(next.testDir, 'standalone') - ) - - for (const file of await fs.readdir(next.testDir)) { - if (file !== 'standalone') { - await fs.remove(join(next.testDir, file)) - console.log('removed', file) - } - } - const files = glob.sync('**/*', { - cwd: join(next.testDir, 'standalone/.next/server/pages'), - dot: true, - }) - - for (const file of files) { - if (file.endsWith('.json') || file.endsWith('.html')) { - await fs.remove(join(next.testDir, '.next/server', file)) - } - } - - serverFile = join(next.testDir, 'standalone/server.js') - }) - - beforeEach(async () => { - appPort = await findPort() - app = await initNextServerScript( - serverFile, - /- Local:/, - { ...process.env, PORT: appPort.toString() }, - undefined, - { cwd: next.testDir } - ) - }) - afterEach(() => killApp(app)) - - afterAll(() => next.destroy()) - - it('should wait for requests to complete before exiting', async () => { - const appKilledPromise = once(app, 'exit') - - let responseResolved = false - const resPromise = fetchViaHTTP(appPort, '/api/long-running') - .then((res) => { - responseResolved = true - return res - }) - .catch(() => {}) - - // yield event loop to kick off request before killing the app - await waitFor(20) - process.kill(app.pid, 'SIGTERM') - expect(isAppRunning(app)).toBe(true) - - // Long running response should still be running after a bit - await waitFor(LONG_RUNNING_MS / 2) - expect(isAppRunning(app)).toBe(true) - expect(responseResolved).toBe(false) - - // App responds as expected without being interrupted - const res = await resPromise - assertDefined(res) - expect(res.status).toBe(200) - expect(await res.json()).toStrictEqual({ hello: 'world' }) - - // App is still running briefly after response returns - expect(isAppRunning(app)).toBe(true) - expect(responseResolved).toBe(true) - - // App finally shuts down - await appKilledPromise - expect(isAppRunning(app)).toBe(false) - }) - - describe('should not accept new requests during shutdown cleanup', () => { - it('when request is made before shutdown', async () => { - const appKilledPromise = once(app, 'exit') - - const resPromise = fetchViaHTTP(appPort, '/api/long-running') - - // yield event loop to kick off request before killing the app - await waitFor(20) - process.kill(app.pid, 'SIGTERM') - expect(isAppRunning(app)).toBe(true) - - // Long running response should still be running after a bit - await waitFor(LONG_RUNNING_MS / 2) - expect(isAppRunning(app)).toBe(true) - - // Second request should be rejected - await expect(fetchViaHTTP(appPort, '/api/long-running')).rejects.toThrow() - - // Original request responds as expected without being interrupted - await expect(resPromise).resolves.toBeDefined() - const res = await resPromise - expect(res.status).toBe(200) - expect(await res.json()).toStrictEqual({ hello: 'world' }) - - // App is still running briefly after response returns - expect(isAppRunning(app)).toBe(true) - - // App finally shuts down - await appKilledPromise - expect(isAppRunning(app)).toBe(false) - }) - - it('when there is no activity', async () => { - const appKilledPromise = once(app, 'exit') - - process.kill(app.pid, 'SIGTERM') - expect(isAppRunning(app)).toBe(true) - - // yield event loop to allow server to start the shutdown process - await waitFor(20) - await expect(fetchViaHTTP(appPort, '/api/long-running')).rejects.toThrow() - - // App finally shuts down - await appKilledPromise - expect(isAppRunning(app)).toBe(false) - }) - }) -}) diff --git a/test/production/standalone-mode/graceful-shutdown/next.config.js b/test/production/standalone-mode/graceful-shutdown/next.config.js deleted file mode 100644 index e97173b4b3799..0000000000000 --- a/test/production/standalone-mode/graceful-shutdown/next.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - output: 'standalone', -} diff --git a/test/production/standalone-mode/graceful-shutdown/pages/api/long-running.ts b/test/production/standalone-mode/graceful-shutdown/pages/api/long-running.ts deleted file mode 100644 index 749713e990036..0000000000000 --- a/test/production/standalone-mode/graceful-shutdown/pages/api/long-running.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const LONG_RUNNING_MS = 400 - -export default async (req, res) => { - await new Promise((resolve) => setTimeout(resolve, LONG_RUNNING_MS)) - res.json({ hello: 'world' }) -}