Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

269 lines
8.3 KiB
Go

package pathkey
import (
"os"
"path/filepath"
"testing"
)
// withCaseInsensitive flips the process-global CaseInsensitivePaths for
// the duration of a test and restores it on cleanup. Tests that call it
// must not run in parallel.
func withCaseInsensitive(t *testing.T, v bool) {
t.Helper()
prev := CaseInsensitivePaths
CaseInsensitivePaths = v
t.Cleanup(func() { CaseInsensitivePaths = prev })
}
func TestFoldPath_CaseInsensitiveLowersRemainder(t *testing.T) {
// With ci=true, two casings of the same path fold to one key.
a := foldPath("/Users/me/Documents/Project", true)
b := foldPath("/Users/me/documents/project", true)
if a != b {
t.Fatalf("foldPath ci=true did not converge: %q vs %q", a, b)
}
}
func TestFoldPath_CaseSensitiveKeepsRemainder(t *testing.T) {
// With ci=false, differing casings stay distinct.
a := foldPath("/Users/me/Documents", false)
b := foldPath("/Users/me/documents", false)
if a == b {
t.Fatalf("foldPath ci=false collapsed distinct casings: both %q", a)
}
}
func TestFoldPath_UppercasesDriveLetter(t *testing.T) {
// Windows drive letters fold identically regardless of case, even
// when the rest of the path is compared case-sensitively.
for _, ci := range []bool{false, true} {
a := foldPath(`c:\work\git\myrepo`, ci)
b := foldPath(`C:\work\git\myrepo`, ci)
if a != b {
t.Fatalf("foldPath ci=%v did not fold drive-letter case: %q vs %q", ci, a, b)
}
}
}
func TestFoldPath_CleansRedundantSegments(t *testing.T) {
got := foldPath("/Users/me/./project/../project", false)
want := "/Users/me/project"
if got != want {
t.Fatalf("foldPath did not clean: got %q want %q", got, want)
}
}
func TestFoldPath_FoldsNFDBeforeCasing(t *testing.T) {
// The NFC fold runs before case folding, so an NFD macOS spelling
// and an NFC git spelling of the same accented path converge.
a := foldPath("/x/"+cafeNFD, true)
b := foldPath("/x/"+cafeNFC, true)
if a != b {
t.Fatalf("foldPath did not reconcile NFD vs NFC: %q vs %q", a, b)
}
}
func TestEqualPaths_CaseVariants(t *testing.T) {
withCaseInsensitive(t, true)
if !EqualPaths("/Users/me/Documents/Project", "/Users/me/documents/project") {
t.Fatal("EqualPaths should treat case variants as equal when case-insensitive")
}
}
func TestEqualPaths_CaseSensitiveRejects(t *testing.T) {
withCaseInsensitive(t, false)
if EqualPaths("/Users/me/Documents", "/Users/me/documents") {
t.Fatal("EqualPaths must keep case variants distinct when case-sensitive")
}
}
func TestEqualPaths_ByteEqualFastPath(t *testing.T) {
withCaseInsensitive(t, false)
if !EqualPaths("/a/b/c", "/a/b/c") {
t.Fatal("EqualPaths byte-equal fast path failed")
}
}
func TestEqualPaths_DistinctPaths(t *testing.T) {
withCaseInsensitive(t, true)
if EqualPaths("/Users/me/alpha", "/Users/me/beta") {
t.Fatal("EqualPaths reported genuinely distinct paths as equal")
}
}
func TestHasPathPrefix_RootItself(t *testing.T) {
withCaseInsensitive(t, true)
if !HasPathPrefix("/Users/me/Repo", "/Users/me/repo") {
t.Fatal("a path is a prefix of itself (case-insensitive)")
}
}
func TestHasPathPrefix_ChildUnderRoot(t *testing.T) {
withCaseInsensitive(t, true)
if !HasPathPrefix("/Users/me/Repo/pkg/file.go", "/Users/me/repo") {
t.Fatal("child path should be under root regardless of case")
}
}
func TestHasPathPrefix_ComponentBoundary(t *testing.T) {
withCaseInsensitive(t, false)
// A shared textual prefix that is not a path-component prefix must
// not match: "/Users/me/Doc" is NOT under "/Users/me/Documents".
if HasPathPrefix("/Users/me/Documents", "/Users/me/Doc") {
t.Fatal("prefix-but-not-component must not match")
}
if HasPathPrefix("/Users/me/Doc", "/Users/me/Documents") {
t.Fatal("shorter sibling must not be under longer sibling")
}
}
func TestHasPathPrefix_PosixRoot(t *testing.T) {
withCaseInsensitive(t, false)
if !HasPathPrefix("/anything/here", "/") {
t.Fatal("everything is under the POSIX root")
}
if !HasPathPrefix("/", "/") {
t.Fatal("root is a prefix of itself")
}
}
func TestHasPathPrefix_TrailingSeparatorRoot(t *testing.T) {
withCaseInsensitive(t, true)
// A root passed with a trailing separator is cleaned; the child
// still matches.
if !HasPathPrefix("/Users/me/Repo/x", "/Users/me/repo/") {
t.Fatal("trailing-separator root should still match its children")
}
}
func TestHasPathPrefix_WindowsDriveRoot(t *testing.T) {
withCaseInsensitive(t, true)
// The drive-letter fold — the #277 fix — is exercised by the
// root-equals-cwd path (VS Code passes "c:\..." for a config-stored
// "C:\..."). This holds with either separator on any host OS.
if !HasPathPrefix(`C:\work\git\myrepo`, `c:\work\git\myrepo`) {
t.Fatal("Windows drive-letter case must not defeat the coverage check")
}
// The child-under-root branch relies on filepath.Separator, so use a
// forward-slash Windows path (also accepted by Windows) so the
// component boundary is verifiable on a POSIX CI runner too.
if !HasPathPrefix(`c:/work/git/myrepo/pkg`, `C:/work/git/myrepo`) {
t.Fatal("child under a drive-letter root must match despite case")
}
}
func TestNormalizeVolume(t *testing.T) {
cases := []struct {
in string
want string
}{
{`c:\x`, `C:\x`},
{`C:\x`, `C:\x`},
{`\\srv\share`, `\\SRV\SHARE`},
{`\\Srv\Share\dir`, `\\SRV\SHARE\dir`},
{"/Users/me/project", "/Users/me/project"}, // no volume: no-op
{"relative/path", "relative/path"}, // no volume: no-op
{"", ""},
}
for _, c := range cases {
if got := NormalizeVolume(c.in); got != c.want {
t.Errorf("NormalizeVolume(%q) = %q, want %q", c.in, got, c.want)
}
}
}
// TestNormalizeVolume_Idempotent proves store-time volume normalization is
// stable: applying it in the CLI (runTrack / install) and again in the
// daemon's TrackRepoCtx / ReconcileRepoCtx never drifts the stored path.
func TestNormalizeVolume_Idempotent(t *testing.T) {
for _, in := range []string{`c:\x`, `C:\x`, `\\srv\share\dir`, "/Users/me/p", "relative/p", ""} {
once := NormalizeVolume(in)
if twice := NormalizeVolume(once); twice != once {
t.Errorf("NormalizeVolume not idempotent for %q: once=%q twice=%q", in, once, twice)
}
}
}
func TestVolumeNameLen(t *testing.T) {
cases := []struct {
in string
want int
}{
{"/Users/me", 0},
{"relative", 0},
{`c:\x`, 2},
{`C:`, 2},
{`\\srv\share`, len(`\\srv\share`)},
{`\\srv\share\dir`, len(`\\srv\share`)},
{`\\`, 0},
{"", 0},
{"a", 0},
}
for _, c := range cases {
if got := volumeNameLen(c.in); got != c.want {
t.Errorf("volumeNameLen(%q) = %d, want %d", c.in, got, c.want)
}
}
}
func TestSamePathIdentity_DistinctDirsNotMerged(t *testing.T) {
// Two genuinely distinct directories must never be merged, even
// when the fold is forced case-insensitive.
withCaseInsensitive(t, true)
a := t.TempDir()
b := t.TempDir()
if SamePathIdentity(a, b) {
t.Fatalf("distinct dirs %q and %q must not be identified", a, b)
}
}
func TestSamePathIdentity_CaseVariantOfSameDir(t *testing.T) {
// On this host's default FS, a case variant of a real directory
// resolves to the same inode. Only assert the merge when the host
// filesystem is actually case-insensitive (so the variant exists).
withCaseInsensitive(t, true)
dir := t.TempDir()
base := filepath.Base(dir)
variant := filepath.Join(filepath.Dir(dir), swapCase(base))
if _, err := os.Stat(variant); err != nil {
t.Skipf("host filesystem is case-sensitive; %q does not resolve", variant)
}
if !SamePathIdentity(dir, variant) {
t.Fatalf("case variant %q of %q should identify as the same dir", variant, dir)
}
}
func TestSamePathIdentity_MissingPathTrustsFold(t *testing.T) {
// When a path cannot be stat'd, the fold is trusted rather than
// treating the pair as distinct.
withCaseInsensitive(t, true)
a := "/nonexistent/gortex/Repo"
b := "/nonexistent/gortex/repo"
if !SamePathIdentity(a, b) {
t.Fatal("fold-equal missing paths should be identified")
}
}
func TestSamePathIdentity_ByteEqualFastPath(t *testing.T) {
withCaseInsensitive(t, false)
if !SamePathIdentity("/x/y", "/x/y") {
t.Fatal("byte-equal fast path failed")
}
}
// swapCase toggles the case of ASCII letters so a case-variant directory
// name can be constructed deterministically.
func swapCase(s string) string {
b := []byte(s)
for i := range b {
switch {
case b[i] >= 'a' && b[i] <= 'z':
b[i] -= 32
case b[i] >= 'A' && b[i] <= 'Z':
b[i] += 32
}
}
return string(b)
}