test-fixture-cleanup.ts 1.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
  1. /**
  2. * Junction-safe fixture cleanup for Windows. Test fixtures junction the REAL
  3. * `scripts/`, `node_modules`, and tsx package directories so installer probes
  4. * resolve through them; Windows recursive deletion — both Node's `rmSync` and
  5. * Git's `worktree remove` — follows MOUNT_POINT junctions into their targets
  6. * and would delete the repository's own directories. POSIX `unlink`/`rm`
  7. * already remove symlinks without following them, so the walk is a no-op
  8. * there.
  9. */
  10. import { lstatSync, readdirSync, rmSync, unlinkSync } from 'node:fs'
  11. import { join } from 'node:path'
  12. /**
  13. * Recursively unlink every symbolic link (junction) under `path`.
  14. * @param path - the fixture tree whose reparse points are unlinked.
  15. */
  16. export function unlinkFixtureLinks(path: string): void {
  17. const visit = (entry: string): void => {
  18. let stat: ReturnType<typeof lstatSync>
  19. try {
  20. stat = lstatSync(entry)
  21. } catch (error) {
  22. if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
  23. throw error
  24. }
  25. if (stat.isSymbolicLink() || !stat.isDirectory()) {
  26. if (stat.isSymbolicLink()) unlinkSync(entry)
  27. return
  28. }
  29. for (const child of readdirSync(entry)) visit(join(entry, child))
  30. }
  31. visit(path)
  32. }
  33. /**
  34. * Remove one fixture tree after its junctions are unlinked (see
  35. * {@link unlinkFixtureLinks}). Retries the removal: Windows releases child
  36. * process and antivirus file handles asynchronously, and an unretried
  37. * `rmSync` fails immediately with EPERM under load. A 10-second retry window
  38. * (50 attempts × 200 ms) covers the failover pool's slow handle release;
  39. * release is one-shot (a terminated child's handles drain, not reacquired),
  40. * so a bounded window suffices and never pins afterEach cleanup.
  41. * @param path - the fixture tree to remove.
  42. */
  43. export function removeFixtureSafely(path: string): void {
  44. unlinkFixtureLinks(path)
  45. rmSync(path, { recursive: true, force: true, maxRetries: 50, retryDelay: 200 })
  46. }