snapshot.ts 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. // Shared normalization helpers for cross-OS-stable snapshot tests.
  2. //
  3. // Every snapshot test that captures subprocess output, file paths, or other
  4. // OS-flavored strings hits the same two issues:
  5. // 1. Bun emits CRLF line endings on Windows stderr; LF elsewhere.
  6. // 2. Path separators differ (\ on Windows, / on POSIX), and macOS's
  7. // /var/folders symlink resolves to /private/var/folders.
  8. //
  9. // These helpers exist so each test doesn't reinvent the same regexes.
  10. //
  11. // Use individually for fine-grained control, or compose them via
  12. // `normalizeForSnapshot` for the common "snapshot subprocess output" path.
  13. import fs from "node:fs"
  14. import os from "node:os"
  15. const TMP = os.tmpdir()
  16. const REAL_TMP = fs.realpathSync(TMP)
  17. /**
  18. * Collapses CRLF to LF. Bun's subprocess pipes emit native line endings —
  19. * snapshots captured on macOS/Linux contain LF, so a Windows run without
  20. * this step always diffs.
  21. */
  22. export function stripCrlf(text: string): string {
  23. return text.replaceAll("\r\n", "\n")
  24. }
  25. /**
  26. * Converts Windows-style `\` separators to POSIX `/` so paths render
  27. * identically across OSes. Use for path strings you want stable in a
  28. * snapshot, not for filesystem operations.
  29. */
  30. export function toPosixPath(p: string): string {
  31. return p.replaceAll("\\", "/")
  32. }
  33. /**
  34. * Strips both the OS-level `os.tmpdir()` and its realpath form (macOS
  35. * `/var/folders` → `/private/var/folders`) from text, replacing each
  36. * occurrence with `marker` (default `<TMPDIR>`).
  37. */
  38. export function withTmpdirStripped(text: string, marker = "<TMPDIR>"): string {
  39. return text.replaceAll(REAL_TMP, marker).replaceAll(TMP, marker)
  40. }
  41. /**
  42. * Separator-agnostic match class for path-style strings. Use inside a
  43. * larger regex when you want to match both `/` (POSIX) and `\` (Windows)
  44. * boundaries — e.g. `<TMPDIR>${PATH_SEP}oc-cli-[a-z0-9]+`.
  45. */
  46. export const PATH_SEP = "[/\\\\]"
  47. /**
  48. * One-shot normalization for the common case: strip CRLF, strip tmpdir,
  49. * then apply any caller-supplied path regex substitutions. Does NOT
  50. * blanket-replace `\` with `/` — that would mangle non-path backslash
  51. * content (regex literals in help text, etc.). Use `toPosixPath` or
  52. * `PATH_SEP` in your own regex when you need separator agnosticism.
  53. */
  54. export function normalizeForSnapshot(
  55. text: string,
  56. options?: {
  57. readonly tmpdirMarker?: string
  58. readonly pathReplacements?: ReadonlyArray<readonly [RegExp, string]>
  59. },
  60. ): string {
  61. let out = stripCrlf(text)
  62. out = withTmpdirStripped(out, options?.tmpdirMarker)
  63. for (const [pattern, replacement] of options?.pathReplacements ?? []) {
  64. out = out.replace(pattern, replacement)
  65. }
  66. return out
  67. }