mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 03:03:32 +01:00
feat: walk up directory tree to find .cheat directory (#602)
Previously cheat only checked the current working directory for a .cheat subdirectory. Now it walks upward through ancestor directories, stopping at the first .cheat directory found. This mirrors how git discovers .git directories, so users can place .cheat at their project root and have it work from any subdirectory. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,8 +27,9 @@
|
||||
// # Directory-Scoped Cheatpaths
|
||||
//
|
||||
// The package supports directory-scoped cheatpaths via `.cheat` directories.
|
||||
// When running cheat from a directory containing a `.cheat` subdirectory,
|
||||
// that directory is temporarily added to the available cheatpaths.
|
||||
// When running cheat, the tool walks upward from the current working directory
|
||||
// to the filesystem root, stopping at the first `.cheat` directory found. That
|
||||
// directory is temporarily added to the available cheatpaths.
|
||||
//
|
||||
// # Precedence and Overrides
|
||||
//
|
||||
|
||||
@@ -45,21 +45,20 @@ func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error
|
||||
return Config{}, fmt.Errorf("could not unmarshal yaml: %v", err)
|
||||
}
|
||||
|
||||
// if a .cheat directory exists locally, append it to the cheatpaths
|
||||
// if a .cheat directory exists in the current directory or any ancestor,
|
||||
// append it to the cheatpaths
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("failed to get cwd: %v", err)
|
||||
}
|
||||
|
||||
local := filepath.Join(cwd, ".cheat")
|
||||
if _, err := os.Stat(local); err == nil {
|
||||
if local := findLocalCheatpath(cwd); local != "" {
|
||||
path := cp.Cheatpath{
|
||||
Name: "cwd",
|
||||
Path: local,
|
||||
ReadOnly: false,
|
||||
Tags: []string{},
|
||||
}
|
||||
|
||||
conf.Cheatpaths = append(conf.Cheatpaths, path)
|
||||
}
|
||||
|
||||
@@ -140,3 +139,21 @@ func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// findLocalCheatpath walks upward from dir looking for a .cheat directory.
|
||||
// It returns the path to the first .cheat directory found, or an empty string
|
||||
// if none exists. This mirrors the discovery pattern used by git for .git
|
||||
// directories.
|
||||
func findLocalCheatpath(dir string) string {
|
||||
for {
|
||||
candidate := filepath.Join(dir, ".cheat")
|
||||
if info, err := os.Stat(candidate); err == nil && info.IsDir() {
|
||||
return candidate
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return ""
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,187 @@ func TestConfigLocalCheatpath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigLocalCheatpathInParent tests that .cheat in a parent directory is found
|
||||
func TestConfigLocalCheatpathInParent(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Resolve symlinks in temp dir path (macOS /var -> /private/var)
|
||||
tempDir, err = filepath.EvalSymlinks(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to resolve temp dir symlinks: %v", err)
|
||||
}
|
||||
|
||||
oldCwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get cwd: %v", err)
|
||||
}
|
||||
defer os.Chdir(oldCwd)
|
||||
|
||||
// Create .cheat in the root of the temp dir
|
||||
localCheat := filepath.Join(tempDir, ".cheat")
|
||||
if err := os.Mkdir(localCheat, 0755); err != nil {
|
||||
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||
}
|
||||
|
||||
// Create a subdirectory and cd into it
|
||||
subDir := filepath.Join(tempDir, "sub")
|
||||
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create sub dir: %v", err)
|
||||
}
|
||||
if err := os.Chdir(subDir); err != nil {
|
||||
t.Fatalf("failed to chdir: %v", err)
|
||||
}
|
||||
|
||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, cp := range conf.Cheatpaths {
|
||||
if cp.Name == "cwd" && cp.Path == localCheat {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("parent .cheat directory was not added to cheatpaths")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigLocalCheatpathNearestWins tests that the nearest .cheat wins
|
||||
func TestConfigLocalCheatpathNearestWins(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Resolve symlinks in temp dir path (macOS /var -> /private/var)
|
||||
tempDir, err = filepath.EvalSymlinks(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to resolve temp dir symlinks: %v", err)
|
||||
}
|
||||
|
||||
oldCwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get cwd: %v", err)
|
||||
}
|
||||
defer os.Chdir(oldCwd)
|
||||
|
||||
// Create .cheat at root
|
||||
if err := os.Mkdir(filepath.Join(tempDir, ".cheat"), 0755); err != nil {
|
||||
t.Fatalf("failed to create root .cheat dir: %v", err)
|
||||
}
|
||||
|
||||
// Create sub/.cheat (the nearer one)
|
||||
subDir := filepath.Join(tempDir, "sub")
|
||||
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create sub dir: %v", err)
|
||||
}
|
||||
nearCheat := filepath.Join(subDir, ".cheat")
|
||||
if err := os.Mkdir(nearCheat, 0755); err != nil {
|
||||
t.Fatalf("failed to create near .cheat dir: %v", err)
|
||||
}
|
||||
|
||||
// cd into sub/deep/
|
||||
deepDir := filepath.Join(subDir, "deep")
|
||||
if err := os.Mkdir(deepDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create deep dir: %v", err)
|
||||
}
|
||||
if err := os.Chdir(deepDir); err != nil {
|
||||
t.Fatalf("failed to chdir: %v", err)
|
||||
}
|
||||
|
||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, cp := range conf.Cheatpaths {
|
||||
if cp.Name == "cwd" {
|
||||
if cp.Path != nearCheat {
|
||||
t.Errorf("expected nearest .cheat %s, got %s", nearCheat, cp.Path)
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("no cwd cheatpath found")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigNoLocalCheatpath tests that no cwd cheatpath is added when no .cheat exists
|
||||
func TestConfigNoLocalCheatpath(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
oldCwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get cwd: %v", err)
|
||||
}
|
||||
defer os.Chdir(oldCwd)
|
||||
|
||||
if err := os.Chdir(tempDir); err != nil {
|
||||
t.Fatalf("failed to chdir: %v", err)
|
||||
}
|
||||
|
||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
for _, cp := range conf.Cheatpaths {
|
||||
if cp.Name == "cwd" {
|
||||
t.Error("cwd cheatpath should not be added when no .cheat exists")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigLocalCheatpathFileSkipped tests that a .cheat file (not dir) is skipped
|
||||
func TestConfigLocalCheatpathFileSkipped(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
oldCwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get cwd: %v", err)
|
||||
}
|
||||
defer os.Chdir(oldCwd)
|
||||
|
||||
// Create .cheat as a file, not a directory
|
||||
if err := os.WriteFile(filepath.Join(tempDir, ".cheat"), []byte("not a dir"), 0644); err != nil {
|
||||
t.Fatalf("failed to create .cheat file: %v", err)
|
||||
}
|
||||
|
||||
if err := os.Chdir(tempDir); err != nil {
|
||||
t.Fatalf("failed to chdir: %v", err)
|
||||
}
|
||||
|
||||
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
for _, cp := range conf.Cheatpaths {
|
||||
if cp.Name == "cwd" {
|
||||
t.Error("cwd cheatpath should not be added for a .cheat file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigDefaults tests default values
|
||||
func TestConfigDefaults(t *testing.T) {
|
||||
// Load empty config
|
||||
|
||||
122
internal/config/config_fuzz_test.go
Normal file
122
internal/config/config_fuzz_test.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzFindLocalCheatpath exercises findLocalCheatpath with randomised
|
||||
// directory depths and .cheat placements. For each fuzz input it builds a
|
||||
// temporary directory hierarchy, places a single .cheat directory at a
|
||||
// computed level, and asserts that the function always returns it.
|
||||
func FuzzFindLocalCheatpath(f *testing.F) {
|
||||
// Seed corpus: (totalDepth, cheatPlacement)
|
||||
f.Add(uint8(1), uint8(0)) // depth 1, .cheat at root
|
||||
f.Add(uint8(3), uint8(0)) // depth 3, .cheat at root
|
||||
f.Add(uint8(5), uint8(3)) // depth 5, .cheat at level 3
|
||||
f.Add(uint8(1), uint8(1)) // depth 1, .cheat at same level as search dir
|
||||
f.Add(uint8(10), uint8(5)) // deep hierarchy
|
||||
|
||||
f.Fuzz(func(t *testing.T, totalDepth uint8, cheatPlacement uint8) {
|
||||
// Clamp to reasonable values to keep I/O bounded
|
||||
depth := int(totalDepth%15) + 1 // 1..15
|
||||
cheatAt := int(cheatPlacement) % (depth + 1) // 0..depth (0 = tempDir itself)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Build chain: tempDir/d0/d1/…/d{depth-1}
|
||||
dirs := make([]string, 0, depth+1)
|
||||
dirs = append(dirs, tempDir)
|
||||
current := tempDir
|
||||
for i := 0; i < depth; i++ {
|
||||
current = filepath.Join(current, fmt.Sprintf("d%d", i))
|
||||
if err := os.Mkdir(current, 0755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
dirs = append(dirs, current)
|
||||
}
|
||||
|
||||
// Place .cheat at dirs[cheatAt]
|
||||
cheatDir := filepath.Join(dirs[cheatAt], ".cheat")
|
||||
if err := os.Mkdir(cheatDir, 0755); err != nil {
|
||||
t.Fatalf("mkdir .cheat: %v", err)
|
||||
}
|
||||
|
||||
// Search from the deepest directory
|
||||
result := findLocalCheatpath(current)
|
||||
|
||||
// Invariant 1: must find the .cheat we placed
|
||||
if result != cheatDir {
|
||||
t.Errorf("depth=%d cheatAt=%d: expected %s, got %s",
|
||||
depth, cheatAt, cheatDir, result)
|
||||
}
|
||||
|
||||
// Invariant 2: result must end with /.cheat
|
||||
if !strings.HasSuffix(result, string(filepath.Separator)+".cheat") {
|
||||
t.Errorf("result %q does not end with /.cheat", result)
|
||||
}
|
||||
|
||||
// Invariant 3: result must be under tempDir
|
||||
if !strings.HasPrefix(result, tempDir) {
|
||||
t.Errorf("result %q is not under tempDir %s", result, tempDir)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzFindLocalCheatpathNearestWins verifies that when two .cheat directories
|
||||
// exist at different levels of the ancestor chain, the nearest one is returned.
|
||||
func FuzzFindLocalCheatpathNearestWins(f *testing.F) {
|
||||
f.Add(uint8(5), uint8(1), uint8(3))
|
||||
f.Add(uint8(8), uint8(0), uint8(7))
|
||||
f.Add(uint8(3), uint8(0), uint8(2))
|
||||
f.Add(uint8(10), uint8(2), uint8(8))
|
||||
|
||||
f.Fuzz(func(t *testing.T, totalDepth, shallowRaw, deepRaw uint8) {
|
||||
depth := int(totalDepth%12) + 2 // 2..13 (need room for two placements)
|
||||
s := int(shallowRaw) % depth
|
||||
d := int(deepRaw) % depth
|
||||
|
||||
// Need two distinct levels
|
||||
if s == d {
|
||||
d = (d + 1) % depth
|
||||
}
|
||||
// Ensure s < d (shallow is higher in tree, deep is closer to search dir)
|
||||
if s > d {
|
||||
s, d = d, s
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Build chain
|
||||
dirs := make([]string, 0, depth+1)
|
||||
dirs = append(dirs, tempDir)
|
||||
current := tempDir
|
||||
for i := 0; i < depth; i++ {
|
||||
current = filepath.Join(current, fmt.Sprintf("d%d", i))
|
||||
if err := os.Mkdir(current, 0755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
dirs = append(dirs, current)
|
||||
}
|
||||
|
||||
// Place .cheat at both levels
|
||||
shallowCheat := filepath.Join(dirs[s], ".cheat")
|
||||
deepCheat := filepath.Join(dirs[d], ".cheat")
|
||||
if err := os.Mkdir(shallowCheat, 0755); err != nil {
|
||||
t.Fatalf("mkdir shallow .cheat: %v", err)
|
||||
}
|
||||
if err := os.Mkdir(deepCheat, 0755); err != nil {
|
||||
t.Fatalf("mkdir deep .cheat: %v", err)
|
||||
}
|
||||
|
||||
// Search from the deepest directory — should find the deeper (nearer) .cheat
|
||||
result := findLocalCheatpath(current)
|
||||
if result != deepCheat {
|
||||
t.Errorf("depth=%d shallow=%d deep=%d: expected nearest %s, got %s",
|
||||
depth, s, d, deepCheat, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
@@ -13,9 +14,267 @@ import (
|
||||
"github.com/cheat/cheat/internal/mock"
|
||||
)
|
||||
|
||||
// TestFindLocalCheatpathInCurrentDir tests that .cheat in the given dir is found
|
||||
func TestFindLocalCheatpathInCurrentDir(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
cheatDir := filepath.Join(tempDir, ".cheat")
|
||||
if err := os.Mkdir(cheatDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||
}
|
||||
|
||||
result := findLocalCheatpath(tempDir)
|
||||
if result != cheatDir {
|
||||
t.Errorf("expected %s, got %s", cheatDir, result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindLocalCheatpathInParent tests walking up to a parent directory
|
||||
func TestFindLocalCheatpathInParent(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
cheatDir := filepath.Join(tempDir, ".cheat")
|
||||
if err := os.Mkdir(cheatDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||
}
|
||||
|
||||
subDir := filepath.Join(tempDir, "sub")
|
||||
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create sub dir: %v", err)
|
||||
}
|
||||
|
||||
result := findLocalCheatpath(subDir)
|
||||
if result != cheatDir {
|
||||
t.Errorf("expected %s, got %s", cheatDir, result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindLocalCheatpathInGrandparent tests walking up multiple levels
|
||||
func TestFindLocalCheatpathInGrandparent(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
cheatDir := filepath.Join(tempDir, ".cheat")
|
||||
if err := os.Mkdir(cheatDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||
}
|
||||
|
||||
deepDir := filepath.Join(tempDir, "a", "b", "c")
|
||||
if err := os.MkdirAll(deepDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create deep dir: %v", err)
|
||||
}
|
||||
|
||||
result := findLocalCheatpath(deepDir)
|
||||
if result != cheatDir {
|
||||
t.Errorf("expected %s, got %s", cheatDir, result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindLocalCheatpathNearestWins tests that the closest .cheat is returned
|
||||
func TestFindLocalCheatpathNearestWins(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create .cheat at root level
|
||||
if err := os.Mkdir(filepath.Join(tempDir, ".cheat"), 0755); err != nil {
|
||||
t.Fatalf("failed to create root .cheat dir: %v", err)
|
||||
}
|
||||
|
||||
// Create sub/.cheat (the nearer one)
|
||||
subDir := filepath.Join(tempDir, "sub")
|
||||
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create sub dir: %v", err)
|
||||
}
|
||||
nearCheatDir := filepath.Join(subDir, ".cheat")
|
||||
if err := os.Mkdir(nearCheatDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create sub .cheat dir: %v", err)
|
||||
}
|
||||
|
||||
// Search from sub/deep/
|
||||
deepDir := filepath.Join(subDir, "deep")
|
||||
if err := os.Mkdir(deepDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create deep dir: %v", err)
|
||||
}
|
||||
|
||||
result := findLocalCheatpath(deepDir)
|
||||
if result != nearCheatDir {
|
||||
t.Errorf("expected nearest %s, got %s", nearCheatDir, result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindLocalCheatpathNotFound tests that empty string is returned when no .cheat exists
|
||||
func TestFindLocalCheatpathNotFound(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
result := findLocalCheatpath(tempDir)
|
||||
if result != "" {
|
||||
t.Errorf("expected empty string, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindLocalCheatpathSkipsFile tests that a file named .cheat is not matched
|
||||
func TestFindLocalCheatpathSkipsFile(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "cheat-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create .cheat as a file, not a directory
|
||||
cheatFile := filepath.Join(tempDir, ".cheat")
|
||||
if err := os.WriteFile(cheatFile, []byte("not a directory"), 0644); err != nil {
|
||||
t.Fatalf("failed to create .cheat file: %v", err)
|
||||
}
|
||||
|
||||
result := findLocalCheatpath(tempDir)
|
||||
if result != "" {
|
||||
t.Errorf("expected empty string for .cheat file, got %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindLocalCheatpathSymlink tests that a .cheat symlink to a directory is found
|
||||
func TestFindLocalCheatpathSymlink(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create the real directory
|
||||
realDir := filepath.Join(tempDir, "real-cheat")
|
||||
if err := os.Mkdir(realDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create real dir: %v", err)
|
||||
}
|
||||
|
||||
// Symlink .cheat -> real-cheat
|
||||
cheatLink := filepath.Join(tempDir, ".cheat")
|
||||
if err := os.Symlink(realDir, cheatLink); err != nil {
|
||||
t.Fatalf("failed to create symlink: %v", err)
|
||||
}
|
||||
|
||||
result := findLocalCheatpath(tempDir)
|
||||
if result != cheatLink {
|
||||
t.Errorf("expected %s, got %s", cheatLink, result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindLocalCheatpathSymlinkInAncestor tests discovery through a symlinked
|
||||
// ancestor directory. When the cwd is reached via a symlink, filepath.Dir
|
||||
// walks the symlinked path (not the real path), so .cheat must be findable
|
||||
// through that chain.
|
||||
func TestFindLocalCheatpathSymlinkInAncestor(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Create real/project/.cheat
|
||||
realProject := filepath.Join(tempDir, "real", "project")
|
||||
if err := os.MkdirAll(realProject, 0755); err != nil {
|
||||
t.Fatalf("failed to create real project dir: %v", err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(realProject, ".cheat"), 0755); err != nil {
|
||||
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||
}
|
||||
|
||||
// Create symlink: linked -> real/project
|
||||
linkedProject := filepath.Join(tempDir, "linked")
|
||||
if err := os.Symlink(realProject, linkedProject); err != nil {
|
||||
t.Fatalf("failed to create symlink: %v", err)
|
||||
}
|
||||
|
||||
// Create sub inside the symlinked path
|
||||
subDir := filepath.Join(linkedProject, "sub")
|
||||
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create sub dir: %v", err)
|
||||
}
|
||||
|
||||
// Search from linked/sub — should find linked/.cheat
|
||||
// (os.Stat follows symlinks, so linked/.cheat resolves to real/project/.cheat)
|
||||
result := findLocalCheatpath(subDir)
|
||||
expected := filepath.Join(linkedProject, ".cheat")
|
||||
if result != expected {
|
||||
t.Errorf("expected %s, got %s", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindLocalCheatpathPermissionDenied tests that unreadable ancestor
|
||||
// directories are skipped and the walk continues upward.
|
||||
func TestFindLocalCheatpathPermissionDenied(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("Unix permissions do not apply on Windows")
|
||||
}
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("test requires non-root user")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Resolve symlinks (macOS /var -> /private/var)
|
||||
tempDir, err := filepath.EvalSymlinks(tempDir)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to resolve symlinks: %v", err)
|
||||
}
|
||||
|
||||
// Create tempDir/.cheat (the target we want found)
|
||||
cheatDir := filepath.Join(tempDir, ".cheat")
|
||||
if err := os.Mkdir(cheatDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create .cheat dir: %v", err)
|
||||
}
|
||||
|
||||
// Create tempDir/restricted/ with its own .cheat and sub/
|
||||
restricted := filepath.Join(tempDir, "restricted")
|
||||
if err := os.Mkdir(restricted, 0755); err != nil {
|
||||
t.Fatalf("failed to create restricted dir: %v", err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(restricted, ".cheat"), 0755); err != nil {
|
||||
t.Fatalf("failed to create restricted .cheat dir: %v", err)
|
||||
}
|
||||
subDir := filepath.Join(restricted, "sub")
|
||||
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||
t.Fatalf("failed to create sub dir: %v", err)
|
||||
}
|
||||
|
||||
// Make restricted/ unreadable — blocks stat of children
|
||||
if err := os.Chmod(restricted, 0000); err != nil {
|
||||
t.Fatalf("failed to chmod: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { os.Chmod(restricted, 0755) })
|
||||
|
||||
// Walk from restricted/sub: stat("restricted/sub/.cheat") fails (EACCES),
|
||||
// stat("restricted/.cheat") fails (EACCES), walk continues to tempDir/.cheat
|
||||
result := findLocalCheatpath(subDir)
|
||||
if result != cheatDir {
|
||||
t.Errorf("expected %s (walked past restricted dir), got %s", cheatDir, result)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfig asserts that the configs are loaded correctly
|
||||
func TestConfigSuccessful(t *testing.T) {
|
||||
|
||||
// Chdir into a temp directory so no ancestor .cheat directory can
|
||||
// leak into the cheatpaths (findLocalCheatpath walks the full
|
||||
// ancestor chain).
|
||||
oldCwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get cwd: %v", err)
|
||||
}
|
||||
defer os.Chdir(oldCwd)
|
||||
if err := os.Chdir(t.TempDir()); err != nil {
|
||||
t.Fatalf("failed to chdir: %v", err)
|
||||
}
|
||||
|
||||
// clear env vars so they don't override the config file value
|
||||
oldVisual := os.Getenv("VISUAL")
|
||||
oldEditor := os.Getenv("EDITOR")
|
||||
|
||||
Reference in New Issue
Block a user