a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
146 lines
4.5 KiB
Go
146 lines
4.5 KiB
Go
package pathkey
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"golang.org/x/text/unicode/norm"
|
|
)
|
|
|
|
// All non-ASCII fixtures are spelled with explicit \u / \U escapes so
|
|
// this source file stays pure-ASCII and the byte form of each fixture
|
|
// is deterministic regardless of the editor encoding used to save it.
|
|
//
|
|
// - cafeNFC / cafeNFD: "cafe.go" with an accented e. NFC precomposes
|
|
// it to U+00E9; NFD spells it as base "e" (U+0065) + U+0301
|
|
// COMBINING ACUTE ACCENT. Same filename, different bytes — the
|
|
// exact macOS-NFD vs git/Linux-NFC split this package reconciles.
|
|
// - cjkName / cyrName: CJK ideographs and Cyrillic letters have no
|
|
// canonical decomposition, so they are identical in every normal
|
|
// form; included to prove non-Latin scripts pass through cleanly.
|
|
// - emoji: an astral-plane code point, to prove rune-correctness.
|
|
const (
|
|
cafeNFC = "café.go" // precomposed e-acute
|
|
cafeNFD = "café.go" // "e" + combining acute accent
|
|
cjkName = "日本語.go" // CJK: "Japanese language"
|
|
cyrName = "кириллица.go" // Cyrillic
|
|
emoji = "\U0001f600.go" // grinning face
|
|
)
|
|
|
|
func TestNormalize_FoldsNFDToNFC(t *testing.T) {
|
|
// The decomposed input must come back as the precomposed form, so
|
|
// a path observed in NFD (macOS) keys identically to the same
|
|
// path observed in NFC (git / Linux).
|
|
if cafeNFD == cafeNFC {
|
|
t.Fatal("test fixture invalid: NFD and NFC forms are byte-identical")
|
|
}
|
|
got := Normalize(cafeNFD)
|
|
if got != cafeNFC {
|
|
t.Fatalf("Normalize(NFD) = %q, want NFC %q", got, cafeNFC)
|
|
}
|
|
}
|
|
|
|
func TestNormalize_NFCIsStable(t *testing.T) {
|
|
// An already-NFC path is returned unchanged — normalisation is
|
|
// idempotent, so re-keying an NFC path never drifts.
|
|
if got := Normalize(cafeNFC); got != cafeNFC {
|
|
t.Fatalf("Normalize(NFC) = %q, want unchanged %q", got, cafeNFC)
|
|
}
|
|
}
|
|
|
|
func TestNormalize_Idempotent(t *testing.T) {
|
|
for _, in := range []string{cafeNFD, cafeNFC, cjkName, cyrName, emoji, "plain/ascii/path.go"} {
|
|
once := Normalize(in)
|
|
twice := Normalize(once)
|
|
if once != twice {
|
|
t.Errorf("Normalize not idempotent for %q: once=%q twice=%q", in, once, twice)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalize_ASCIIUnchanged(t *testing.T) {
|
|
// Pure-ASCII paths — the overwhelmingly common case — must pass
|
|
// through byte-identical (and via the isASCII fast path).
|
|
for _, in := range []string{
|
|
"main.go",
|
|
"internal/indexer/watcher.go",
|
|
"./pkg/util.go",
|
|
"a/b/c/d/e.go",
|
|
"",
|
|
} {
|
|
if got := Normalize(in); got != in {
|
|
t.Errorf("Normalize(%q) = %q, want unchanged", in, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalize_AlwaysReturnsNFC(t *testing.T) {
|
|
// Whatever Normalize returns must itself be in NFC — that is the
|
|
// contract every keying call site relies on.
|
|
for _, in := range []string{cafeNFD, cafeNFC, cjkName, cyrName, emoji} {
|
|
got := Normalize(in)
|
|
if !norm.NFC.IsNormalString(got) {
|
|
t.Errorf("Normalize(%q) = %q is not NFC-normalised", in, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNormalize_PreservesSeparatorsAndContent(t *testing.T) {
|
|
// Normalisation must only touch Unicode composition — it must not
|
|
// drop, reorder, or rewrite path separators or other content.
|
|
cjkDir := "src/" + cjkName // already NFC
|
|
if got := Normalize(cjkDir); got != cjkDir {
|
|
t.Fatalf("Normalize altered an already-NFC CJK path: got %q want %q", got, cjkDir)
|
|
}
|
|
// A decomposed component nested in a multi-segment path folds in
|
|
// place without disturbing the surrounding segments.
|
|
nested := "a/" + cafeNFD + "/b.go"
|
|
want := "a/" + cafeNFC + "/b.go"
|
|
if got := Normalize(nested); got != want {
|
|
t.Fatalf("Normalize(%q) = %q, want %q", nested, got, want)
|
|
}
|
|
}
|
|
|
|
func TestEqual_NFCvsNFD(t *testing.T) {
|
|
// The whole point: the two byte forms of one filename compare
|
|
// equal once folded.
|
|
if !Equal(cafeNFC, cafeNFD) {
|
|
t.Fatalf("Equal(%q, %q) = false, want true", cafeNFC, cafeNFD)
|
|
}
|
|
}
|
|
|
|
func TestEqual_IdenticalShortCircuit(t *testing.T) {
|
|
if !Equal(cjkName, cjkName) {
|
|
t.Fatalf("Equal of identical strings returned false")
|
|
}
|
|
}
|
|
|
|
func TestEqual_DistinctPathsNotEqual(t *testing.T) {
|
|
if Equal(cjkName, cyrName) {
|
|
t.Fatalf("Equal reported two genuinely different paths as equal")
|
|
}
|
|
if Equal("a.go", "b.go") {
|
|
t.Fatalf("Equal reported a.go == b.go")
|
|
}
|
|
}
|
|
|
|
func TestIsASCII(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want bool
|
|
}{
|
|
{"", true},
|
|
{"plain.go", true},
|
|
{"internal/indexer/watcher.go", true},
|
|
{cafeNFC, false},
|
|
{cafeNFD, false},
|
|
{cjkName, false},
|
|
{cyrName, false},
|
|
{emoji, false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := isASCII(c.in); got != c.want {
|
|
t.Errorf("isASCII(%q) = %v, want %v", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|