mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
The `watch-distdir-deletion` dev test deletes `.next` to verify that the dev server notices its distDir (`.next/dev`) being removed and restarts. The deletion frequently failed in CI with `ENOTEMPTY: directory not empty, rmdir '.../.next/dev'`. The cause is a race between the test's recursive delete and the dev server's own writes. After the warmup request returns, Turbopack keeps flushing manifests into `.next/dev` through its background update subscription, and `writeFileAtomic` does so by creating a transient temporary file and renaming it over the target. `fs.rm` removes a directory by reading its entries, unlinking them, and then calling `rmdir`; when the server writes a new file into `.next/dev` between the unlink pass and the `rmdir`, the `rmdir` throws `ENOTEMPTY`. The test only passed `force: true`, which suppresses `ENOENT` but not `ENOTEMPTY`, so the first such collision failed the test outright. Resource contention on CI widens the window, which is why it failed so often. This change passes `maxRetries` and `retryDelay` to the same `fs.rm` call so the failed `rmdir` is re-attempted across that brief window until the flush settles, instead of failing on the first collision. The deletion stays a plain recursive remove. Run twenty times concurrently on an oversubscribed machine, the test went from a 45% failure rate to passing every time, with the distDir-deletion watcher still firing in every run.
56 lines
1.7 KiB
TypeScript
56 lines
1.7 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
import { retry } from 'next-test-utils'
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
|
|
describe('app-dir watch-distdir-deletion', () => {
|
|
const { next } = nextTestSetup({
|
|
files: path.join(__dirname, 'fixture'),
|
|
})
|
|
|
|
it('should restart the server when distDir is deleted', async () => {
|
|
// Wait for the dev server to be ready
|
|
await retry(async () => {
|
|
expect(next.cliOutput).toMatch(/ready/i)
|
|
})
|
|
|
|
// Make a request first to ensure the dev server has compiled
|
|
const warmupRes = await next.fetch('/')
|
|
expect(warmupRes.status).toBe(200)
|
|
|
|
// Delete .next (which also removes .next/dev, the watched distDir). The dev
|
|
// server may still be flushing manifests into .next/dev from the warmup
|
|
// compilation, and those writes race the recursive delete: fs.rm unlinks
|
|
// the directory's contents and then rmdir's it, but a file written
|
|
// concurrently makes the rmdir throw ENOTEMPTY. maxRetries makes this
|
|
// single delete resilient to that brief window by re-attempting the failed
|
|
// rmdir until the flush settles. force ignores entries an earlier attempt
|
|
// already removed.
|
|
await fs.promises.rm(path.join(next.testDir, '.next'), {
|
|
recursive: true,
|
|
force: true,
|
|
maxRetries: 10,
|
|
retryDelay: 200,
|
|
})
|
|
|
|
// Wait for restart message and server to come back up
|
|
await retry(
|
|
async () => {
|
|
expect(next.cliOutput).toMatch(/The directory at .* was deleted/)
|
|
},
|
|
30_000,
|
|
1000
|
|
)
|
|
|
|
// Server should come back up and serve pages
|
|
await retry(
|
|
async () => {
|
|
const res = await next.fetch('/')
|
|
expect(res.status).toBe(200)
|
|
},
|
|
30_000,
|
|
1000
|
|
)
|
|
})
|
|
})
|