test(lib): extract snapshot normalizer utility for cross-OS stability (#28356)
This commit is contained in:
@@ -13,36 +13,26 @@
|
|||||||
// version (changes per release), so we'd snapshot a moving target.
|
// version (changes per release), so we'd snapshot a moving target.
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import fs from "node:fs"
|
|
||||||
import os from "node:os"
|
|
||||||
import { cliIt } from "../../lib/cli-process"
|
import { cliIt } from "../../lib/cli-process"
|
||||||
|
import { normalizeForSnapshot, PATH_SEP } from "../../lib/snapshot"
|
||||||
|
|
||||||
// Strips dynamic content that varies per run so snapshots are stable.
|
// Composes `normalizeForSnapshot` (CRLF + tmpdir) with two help-specific
|
||||||
// Currently only the tmpdir prefix bleeds in (via `--cwd` defaults that
|
// rules:
|
||||||
// resolve to `process.cwd()`). Add new patterns here as they surface.
|
|
||||||
//
|
//
|
||||||
// On macOS `os.tmpdir()` returns `/var/folders/...` but `process.cwd()`
|
// 1. The harness's `oc-cli-XXX` subdir under TMPDIR collapses to `<HOME>`.
|
||||||
// inside the child returns the realpath `/private/var/folders/...` — so
|
// `PATH_SEP` matches `/` and `\\` so the rule works on POSIX + Windows.
|
||||||
// we strip both forms.
|
//
|
||||||
const TMP = os.tmpdir()
|
// 2. yargs wraps the `[string] [default: "..."]` clause based on the
|
||||||
const REAL_TMP = fs.realpathSync(TMP)
|
// pre-normalized default's character length, so different random home
|
||||||
|
// path widths produce different leading-whitespace counts (or even
|
||||||
|
// line-wraps onto a fresh line on Windows). `\s+` matches both forms.
|
||||||
function normalize(text: string): string {
|
function normalize(text: string): string {
|
||||||
return (
|
return normalizeForSnapshot(text, {
|
||||||
text
|
pathReplacements: [
|
||||||
// Windows emits CRLF on stderr; collapse first so the rest of the
|
[new RegExp(`<TMPDIR>${PATH_SEP}oc-cli-[a-z0-9]+`, "g"), "<HOME>"],
|
||||||
// pipeline doesn't need separate Windows-vs-POSIX branches.
|
[/\s+\[string\] \[default: "<HOME>"\]/g, ' [string] [default: "<HOME>"]'],
|
||||||
.replaceAll("\r\n", "\n")
|
],
|
||||||
.replaceAll(REAL_TMP, "<TMPDIR>")
|
})
|
||||||
.replaceAll(TMP, "<TMPDIR>")
|
|
||||||
// The harness writes the random home dir at `<TMPDIR>/oc-cli-XXX` on
|
|
||||||
// POSIX, `<TMPDIR>\oc-cli-XXX` on Windows. Strip either form.
|
|
||||||
.replace(/<TMPDIR>[/\\]oc-cli-[a-z0-9]+/g, "<HOME>")
|
|
||||||
// yargs wraps the `[string] [default: "..."]` clause based on the
|
|
||||||
// pre-normalized default's character length, so different random home
|
|
||||||
// path widths produce different leading-whitespace counts (or even
|
|
||||||
// line-wraps onto a fresh line on Windows). `\s+` matches both forms.
|
|
||||||
.replace(/\s+\[string\] \[default: "<HOME>"\]/g, ' [string] [default: "<HOME>"]')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top-level commands. Order matches what `opencode --help` prints today;
|
// Top-level commands. Order matches what `opencode --help` prints today;
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// Shared normalization helpers for cross-OS-stable snapshot tests.
|
||||||
|
//
|
||||||
|
// Every snapshot test that captures subprocess output, file paths, or other
|
||||||
|
// OS-flavored strings hits the same two issues:
|
||||||
|
// 1. Bun emits CRLF line endings on Windows stderr; LF elsewhere.
|
||||||
|
// 2. Path separators differ (\ on Windows, / on POSIX), and macOS's
|
||||||
|
// /var/folders symlink resolves to /private/var/folders.
|
||||||
|
//
|
||||||
|
// These helpers exist so each test doesn't reinvent the same regexes.
|
||||||
|
//
|
||||||
|
// Use individually for fine-grained control, or compose them via
|
||||||
|
// `normalizeForSnapshot` for the common "snapshot subprocess output" path.
|
||||||
|
import fs from "node:fs"
|
||||||
|
import os from "node:os"
|
||||||
|
|
||||||
|
const TMP = os.tmpdir()
|
||||||
|
const REAL_TMP = fs.realpathSync(TMP)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collapses CRLF to LF. Bun's subprocess pipes emit native line endings —
|
||||||
|
* snapshots captured on macOS/Linux contain LF, so a Windows run without
|
||||||
|
* this step always diffs.
|
||||||
|
*/
|
||||||
|
export function stripCrlf(text: string): string {
|
||||||
|
return text.replaceAll("\r\n", "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Windows-style `\` separators to POSIX `/` so paths render
|
||||||
|
* identically across OSes. Use for path strings you want stable in a
|
||||||
|
* snapshot, not for filesystem operations.
|
||||||
|
*/
|
||||||
|
export function toPosixPath(p: string): string {
|
||||||
|
return p.replaceAll("\\", "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips both the OS-level `os.tmpdir()` and its realpath form (macOS
|
||||||
|
* `/var/folders` → `/private/var/folders`) from text, replacing each
|
||||||
|
* occurrence with `marker` (default `<TMPDIR>`).
|
||||||
|
*/
|
||||||
|
export function withTmpdirStripped(text: string, marker = "<TMPDIR>"): string {
|
||||||
|
return text.replaceAll(REAL_TMP, marker).replaceAll(TMP, marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Separator-agnostic match class for path-style strings. Use inside a
|
||||||
|
* larger regex when you want to match both `/` (POSIX) and `\` (Windows)
|
||||||
|
* boundaries — e.g. `<TMPDIR>${PATH_SEP}oc-cli-[a-z0-9]+`.
|
||||||
|
*/
|
||||||
|
export const PATH_SEP = "[/\\\\]"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-shot normalization for the common case: strip CRLF, strip tmpdir,
|
||||||
|
* then apply any caller-supplied path regex substitutions. Does NOT
|
||||||
|
* blanket-replace `\` with `/` — that would mangle non-path backslash
|
||||||
|
* content (regex literals in help text, etc.). Use `toPosixPath` or
|
||||||
|
* `PATH_SEP` in your own regex when you need separator agnosticism.
|
||||||
|
*/
|
||||||
|
export function normalizeForSnapshot(
|
||||||
|
text: string,
|
||||||
|
options?: {
|
||||||
|
readonly tmpdirMarker?: string
|
||||||
|
readonly pathReplacements?: ReadonlyArray<readonly [RegExp, string]>
|
||||||
|
},
|
||||||
|
): string {
|
||||||
|
let out = stripCrlf(text)
|
||||||
|
out = withTmpdirStripped(out, options?.tmpdirMarker)
|
||||||
|
for (const [pattern, replacement] of options?.pathReplacements ?? []) {
|
||||||
|
out = out.replace(pattern, replacement)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user