mirror of
https://github.com/cheat/cheat.git
synced 2026-03-07 11:13:33 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1db4ee378 | ||
|
|
366d63afdc | ||
|
|
c1551683a3 | ||
|
|
09aad6f8ea |
18
README.md
18
README.md
@@ -129,6 +129,10 @@ tags: [ array, map ]
|
|||||||
const squares = [1, 2, 3, 4].map(x => x * x);
|
const squares = [1, 2, 3, 4].map(x => x * x);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Syntax highlighting is provided by [Chroma][], and the `syntax` value may be
|
||||||
|
set to any lexer name that Chroma supports. See Chroma's [supported
|
||||||
|
languages][] for a complete list.
|
||||||
|
|
||||||
The `cheat` executable includes no cheatsheets, but [community-sourced
|
The `cheat` executable includes no cheatsheets, but [community-sourced
|
||||||
cheatsheets are available][cheatsheets]. You will be asked if you would like to
|
cheatsheets are available][cheatsheets]. You will be asked if you would like to
|
||||||
install the community-sourced cheatsheets the first time you run `cheat`.
|
install the community-sourced cheatsheets the first time you run `cheat`.
|
||||||
@@ -168,9 +172,11 @@ editing.
|
|||||||
|
|
||||||
### Directory-scoped Cheatpaths ###
|
### Directory-scoped Cheatpaths ###
|
||||||
At times, it can be useful to closely associate cheatsheets with a directory on
|
At times, it can be useful to closely associate cheatsheets with a directory on
|
||||||
your filesystem. `cheat` facilitates this by searching for a `.cheat` folder in
|
your filesystem. `cheat` facilitates this by searching for a `.cheat` directory
|
||||||
the current working directory. If found, the `.cheat` directory will
|
in the current working directory and its ancestors (similar to how `git` locates
|
||||||
(temporarily) be added to the cheatpaths.
|
`.git` directories). The nearest `.cheat` directory found will (temporarily) be
|
||||||
|
added to the cheatpaths. This means you can place a `.cheat` directory at your
|
||||||
|
project root and it will be available from any subdirectory within that project.
|
||||||
|
|
||||||
Autocompletion
|
Autocompletion
|
||||||
--------------
|
--------------
|
||||||
@@ -189,5 +195,7 @@ Additionally, `cheat` supports enhanced autocompletion via integration with
|
|||||||
[Releases]: https://github.com/cheat/cheat/releases
|
[Releases]: https://github.com/cheat/cheat/releases
|
||||||
[cheatsheets]: https://github.com/cheat/cheatsheets
|
[cheatsheets]: https://github.com/cheat/cheatsheets
|
||||||
[completions]: https://github.com/cheat/cheat/tree/master/scripts
|
[completions]: https://github.com/cheat/cheat/tree/master/scripts
|
||||||
[fzf]: https://github.com/junegunn/fzf
|
[Chroma]: https://github.com/alecthomas/chroma
|
||||||
[go]: https://golang.org
|
[supported languages]: https://github.com/alecthomas/chroma#supported-languages
|
||||||
|
[fzf]: https://github.com/junegunn/fzf
|
||||||
|
[go]: https://golang.org
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ TESTS=(
|
|||||||
"FuzzTagged:./internal/sheet:tag matching with malicious input"
|
"FuzzTagged:./internal/sheet:tag matching with malicious input"
|
||||||
"FuzzFilter:./internal/sheets:tag filtering operations"
|
"FuzzFilter:./internal/sheets:tag filtering operations"
|
||||||
"FuzzTags:./internal/sheets:tag aggregation and sorting"
|
"FuzzTags:./internal/sheets:tag aggregation and sorting"
|
||||||
|
"FuzzFindLocalCheatpath:./internal/config:recursive .cheat directory discovery"
|
||||||
|
"FuzzFindLocalCheatpathNearestWins:./internal/config:nearest .cheat wins invariant"
|
||||||
)
|
)
|
||||||
|
|
||||||
echo "Running fuzz tests ($DURATION each)..."
|
echo "Running fuzz tests ($DURATION each)..."
|
||||||
|
|||||||
245
cmd/cheat/cheatpath_integration_test.go
Normal file
245
cmd/cheat/cheatpath_integration_test.go
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// hasCwdCheatpath checks whether the --directories output contains a
|
||||||
|
// cheatpath named "cwd". The output format is "name: path\n" per line
|
||||||
|
// (tabwriter-aligned), so we look for a line beginning with "cwd".
|
||||||
|
func hasCwdCheatpath(output string) bool {
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
if strings.HasPrefix(line, "cwd") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocalCheatpathIntegration exercises the recursive .cheat directory
|
||||||
|
// discovery end-to-end: it builds the real cheat binary, sets up filesystem
|
||||||
|
// layouts, and verifies behaviour from the user's perspective.
|
||||||
|
func TestLocalCheatpathIntegration(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("integration test uses Unix-specific env vars")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the cheat binary once for all sub-tests.
|
||||||
|
binPath := filepath.Join(t.TempDir(), "cheat_test")
|
||||||
|
build := exec.Command("go", "build", "-o", binPath, ".")
|
||||||
|
if output, err := build.CombinedOutput(); err != nil {
|
||||||
|
t.Fatalf("failed to build cheat: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cheatEnv returns a minimal environment for the cheat binary.
|
||||||
|
cheatEnv := func(confPath, home string) []string {
|
||||||
|
return []string{
|
||||||
|
"CHEAT_CONFIG_PATH=" + confPath,
|
||||||
|
"HOME=" + home,
|
||||||
|
"PATH=" + os.Getenv("PATH"),
|
||||||
|
"EDITOR=vi",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeConfig writes a minimal valid config file referencing sheetsDir.
|
||||||
|
writeConfig := func(t *testing.T, dir, sheetsDir string) string {
|
||||||
|
t.Helper()
|
||||||
|
conf := fmt.Sprintf("---\neditor: vi\ncolorize: false\ncheatpaths:\n - name: base\n path: %s\n readonly: true\n", sheetsDir)
|
||||||
|
confPath := filepath.Join(dir, "conf.yml")
|
||||||
|
if err := os.WriteFile(confPath, []byte(conf), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
return confPath
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("parent .cheat is discovered from subdirectory", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
// Configured cheatpath (empty but must exist for validation)
|
||||||
|
sheetsDir := filepath.Join(root, "sheets")
|
||||||
|
os.MkdirAll(sheetsDir, 0755)
|
||||||
|
|
||||||
|
// .cheat at root with a cheatsheet
|
||||||
|
dotCheat := filepath.Join(root, ".cheat")
|
||||||
|
os.Mkdir(dotCheat, 0755)
|
||||||
|
os.WriteFile(
|
||||||
|
filepath.Join(dotCheat, "localsheet"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\necho hello from local\n"),
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
|
||||||
|
confPath := writeConfig(t, root, sheetsDir)
|
||||||
|
|
||||||
|
// Work from a subdirectory
|
||||||
|
workDir := filepath.Join(root, "src", "pkg")
|
||||||
|
os.MkdirAll(workDir, 0755)
|
||||||
|
env := cheatEnv(confPath, root)
|
||||||
|
|
||||||
|
// --directories should list "cwd" cheatpath
|
||||||
|
cmd := exec.Command(binPath, "--directories")
|
||||||
|
cmd.Dir = workDir
|
||||||
|
cmd.Env = env
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat --directories failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
if !hasCwdCheatpath(string(output)) {
|
||||||
|
t.Errorf("expected 'cwd' cheatpath in --directories output:\n%s", output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viewing the cheatsheet should show its content
|
||||||
|
cmd2 := exec.Command(binPath, "localsheet")
|
||||||
|
cmd2.Dir = workDir
|
||||||
|
cmd2.Env = env
|
||||||
|
output2, err := cmd2.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat localsheet failed: %v\nOutput: %s", err, output2)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output2), "echo hello from local") {
|
||||||
|
t.Errorf("expected cheatsheet content, got:\n%s", output2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("grandparent .cheat is discovered from deep subdirectory", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
sheetsDir := filepath.Join(root, "sheets")
|
||||||
|
os.MkdirAll(sheetsDir, 0755)
|
||||||
|
|
||||||
|
dotCheat := filepath.Join(root, ".cheat")
|
||||||
|
os.Mkdir(dotCheat, 0755)
|
||||||
|
os.WriteFile(
|
||||||
|
filepath.Join(dotCheat, "deepsheet"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\ndeep discovery works\n"),
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
|
||||||
|
confPath := writeConfig(t, root, sheetsDir)
|
||||||
|
|
||||||
|
deepDir := filepath.Join(root, "a", "b", "c", "d", "e")
|
||||||
|
os.MkdirAll(deepDir, 0755)
|
||||||
|
|
||||||
|
cmd := exec.Command(binPath, "deepsheet")
|
||||||
|
cmd.Dir = deepDir
|
||||||
|
cmd.Env = cheatEnv(confPath, root)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat deepsheet failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output), "deep discovery works") {
|
||||||
|
t.Errorf("expected cheatsheet content, got:\n%s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nearest .cheat wins over ancestor .cheat", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
sheetsDir := filepath.Join(root, "sheets")
|
||||||
|
os.MkdirAll(sheetsDir, 0755)
|
||||||
|
|
||||||
|
// .cheat at root
|
||||||
|
rootCheat := filepath.Join(root, ".cheat")
|
||||||
|
os.Mkdir(rootCheat, 0755)
|
||||||
|
os.WriteFile(
|
||||||
|
filepath.Join(rootCheat, "shared"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\nfrom root\n"),
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
|
||||||
|
// .cheat at project/ (nearer)
|
||||||
|
projectDir := filepath.Join(root, "project")
|
||||||
|
os.MkdirAll(projectDir, 0755)
|
||||||
|
projectCheat := filepath.Join(projectDir, ".cheat")
|
||||||
|
os.Mkdir(projectCheat, 0755)
|
||||||
|
os.WriteFile(
|
||||||
|
filepath.Join(projectCheat, "shared"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\nfrom project nearest\n"),
|
||||||
|
0644,
|
||||||
|
)
|
||||||
|
|
||||||
|
confPath := writeConfig(t, root, sheetsDir)
|
||||||
|
|
||||||
|
workDir := filepath.Join(projectDir, "src")
|
||||||
|
os.MkdirAll(workDir, 0755)
|
||||||
|
env := cheatEnv(confPath, root)
|
||||||
|
|
||||||
|
// --directories should list the nearer cheatpath
|
||||||
|
cmd := exec.Command(binPath, "--directories")
|
||||||
|
cmd.Dir = workDir
|
||||||
|
cmd.Env = env
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat --directories failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output), projectCheat) {
|
||||||
|
t.Errorf("expected project .cheat path in output, got:\n%s", output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// "shared" sheet should come from the nearer .cheat
|
||||||
|
cmd2 := exec.Command(binPath, "shared")
|
||||||
|
cmd2.Dir = workDir
|
||||||
|
cmd2.Env = env
|
||||||
|
output2, err := cmd2.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat shared failed: %v\nOutput: %s", err, output2)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(output2), "from project nearest") {
|
||||||
|
t.Errorf("expected nearest .cheat content, got:\n%s", output2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no .cheat directory means no cwd cheatpath", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
sheetsDir := filepath.Join(root, "sheets")
|
||||||
|
os.MkdirAll(sheetsDir, 0755)
|
||||||
|
// Need at least one sheet for --directories to work without error
|
||||||
|
os.WriteFile(filepath.Join(sheetsDir, "placeholder"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\nplaceholder\n"), 0644)
|
||||||
|
|
||||||
|
confPath := writeConfig(t, root, sheetsDir)
|
||||||
|
|
||||||
|
// No .cheat anywhere under root
|
||||||
|
cmd := exec.Command(binPath, "--directories")
|
||||||
|
cmd.Dir = root
|
||||||
|
cmd.Env = cheatEnv(confPath, root)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat --directories failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
if hasCwdCheatpath(string(output)) {
|
||||||
|
t.Errorf("'cwd' cheatpath should not appear when no .cheat exists:\n%s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run(".cheat file (not directory) is ignored", func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
|
||||||
|
sheetsDir := filepath.Join(root, "sheets")
|
||||||
|
os.MkdirAll(sheetsDir, 0755)
|
||||||
|
os.WriteFile(filepath.Join(sheetsDir, "placeholder"),
|
||||||
|
[]byte("---\nsyntax: bash\n---\nplaceholder\n"), 0644)
|
||||||
|
|
||||||
|
// Create .cheat as a regular file
|
||||||
|
os.WriteFile(filepath.Join(root, ".cheat"), []byte("not a dir"), 0644)
|
||||||
|
|
||||||
|
confPath := writeConfig(t, root, sheetsDir)
|
||||||
|
|
||||||
|
cmd := exec.Command(binPath, "--directories")
|
||||||
|
cmd.Dir = root
|
||||||
|
cmd.Env = cheatEnv(confPath, root)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("cheat --directories failed: %v\nOutput: %s", err, output)
|
||||||
|
}
|
||||||
|
if hasCwdCheatpath(string(output)) {
|
||||||
|
t.Errorf("'cwd' should not appear for a .cheat file:\n%s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/cheat/cheat/internal/installer"
|
"github.com/cheat/cheat/internal/installer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "4.5.2"
|
const version = "4.6.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
|
|||||||
80
doc/adr/004-recursive-cheat-directory-search.md
Normal file
80
doc/adr/004-recursive-cheat-directory-search.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# ADR-004: Recursive `.cheat` Directory Search
|
||||||
|
|
||||||
|
Date: 2026-02-15
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Previously, `cheat` only checked the current working directory for a `.cheat`
|
||||||
|
subdirectory to use as a directory-scoped cheatpath. If a user was in
|
||||||
|
`~/projects/myapp/src/handlers/` but the `.cheat` directory lived at
|
||||||
|
`~/projects/myapp/.cheat`, it would not be found. Users requested (#602) that
|
||||||
|
`cheat` walk up the directory hierarchy to find the nearest `.cheat`
|
||||||
|
directory, mirroring the discovery pattern used by `git` for `.git`
|
||||||
|
directories.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Walk upward from the current working directory to the filesystem root, and
|
||||||
|
stop at the first `.cheat` directory found. Only directories are matched (a
|
||||||
|
file named `.cheat` is ignored).
|
||||||
|
|
||||||
|
### Stop at first `.cheat` found
|
||||||
|
|
||||||
|
Rather than collecting multiple `.cheat` directories from ancestor directories:
|
||||||
|
|
||||||
|
- Matches `.git` discovery semantics, which users already understand
|
||||||
|
- Fits the existing single-cheatpath-named-`"cwd"` code without structural
|
||||||
|
changes
|
||||||
|
- Avoids precedence and naming complexity when multiple `.cheat` directories
|
||||||
|
exist in the ancestor chain
|
||||||
|
- `cheat` already supports multiple cheatpaths via `conf.yml` for users who
|
||||||
|
want that; directory-scoped `.cheat` serves the project-context use case
|
||||||
|
|
||||||
|
### Walk to filesystem root (not `$HOME`)
|
||||||
|
|
||||||
|
Rather than stopping the search at `$HOME`:
|
||||||
|
|
||||||
|
- Simpler implementation with no platform-specific home-directory detection
|
||||||
|
- Supports sysadmins working in `/etc`, `/srv`, `/var`, or other paths
|
||||||
|
outside `$HOME`
|
||||||
|
- The boundary only matters on the failure path (no `.cheat` found anywhere),
|
||||||
|
where the cost is a few extra `stat` calls
|
||||||
|
- Security is not a concern since cheatsheets are display-only text, not
|
||||||
|
executable code
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### Positive
|
||||||
|
- Users can place `.cheat` at their project root and it works from any
|
||||||
|
subdirectory, matching their mental model
|
||||||
|
- No configuration changes needed; existing `.cheat` directories continue to
|
||||||
|
work identically
|
||||||
|
- Minimal code change (one small helper function)
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- A `.cheat` directory in an unexpected ancestor could be picked up
|
||||||
|
unintentionally, though this is unlikely in practice and matches how `.git`
|
||||||
|
works
|
||||||
|
|
||||||
|
### Neutral
|
||||||
|
- The cheatpath name remains `"cwd"` regardless of which ancestor the `.cheat`
|
||||||
|
was found in
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### 1. Stop at `$HOME`
|
||||||
|
**Rejected**: Adds platform-specific complexity for minimal benefit. The only
|
||||||
|
downside of walking to root is a few extra `stat` calls on the failure path.
|
||||||
|
|
||||||
|
### 2. Collect multiple `.cheat` directories
|
||||||
|
**Rejected**: Introduces precedence and naming complexity. Users who want
|
||||||
|
multiple cheatpaths can configure them in `conf.yml`.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- GitHub issue: #602
|
||||||
|
- Implementation: `findLocalCheatpath()` in `internal/config/config.go`
|
||||||
@@ -27,8 +27,9 @@
|
|||||||
// # Directory-Scoped Cheatpaths
|
// # Directory-Scoped Cheatpaths
|
||||||
//
|
//
|
||||||
// The package supports directory-scoped cheatpaths via `.cheat` directories.
|
// The package supports directory-scoped cheatpaths via `.cheat` directories.
|
||||||
// When running cheat from a directory containing a `.cheat` subdirectory,
|
// When running cheat, the tool walks upward from the current working directory
|
||||||
// that directory is temporarily added to the available cheatpaths.
|
// to the filesystem root, stopping at the first `.cheat` directory found. That
|
||||||
|
// directory is temporarily added to the available cheatpaths.
|
||||||
//
|
//
|
||||||
// # Precedence and Overrides
|
// # 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)
|
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()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Config{}, fmt.Errorf("failed to get cwd: %v", err)
|
return Config{}, fmt.Errorf("failed to get cwd: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
local := filepath.Join(cwd, ".cheat")
|
if local := findLocalCheatpath(cwd); local != "" {
|
||||||
if _, err := os.Stat(local); err == nil {
|
|
||||||
path := cp.Cheatpath{
|
path := cp.Cheatpath{
|
||||||
Name: "cwd",
|
Name: "cwd",
|
||||||
Path: local,
|
Path: local,
|
||||||
ReadOnly: false,
|
ReadOnly: false,
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
conf.Cheatpaths = append(conf.Cheatpaths, path)
|
conf.Cheatpaths = append(conf.Cheatpaths, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,3 +139,21 @@ func New(_ map[string]interface{}, confPath string, resolve bool) (Config, error
|
|||||||
|
|
||||||
return conf, nil
|
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
|
// TestConfigDefaults tests default values
|
||||||
func TestConfigDefaults(t *testing.T) {
|
func TestConfigDefaults(t *testing.T) {
|
||||||
// Load empty config
|
// 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"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
@@ -13,9 +14,267 @@ import (
|
|||||||
"github.com/cheat/cheat/internal/mock"
|
"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
|
// TestConfig asserts that the configs are loaded correctly
|
||||||
func TestConfigSuccessful(t *testing.T) {
|
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
|
// clear env vars so they don't override the config file value
|
||||||
oldVisual := os.Getenv("VISUAL")
|
oldVisual := os.Getenv("VISUAL")
|
||||||
oldEditor := os.Getenv("EDITOR")
|
oldEditor := os.Getenv("EDITOR")
|
||||||
|
|||||||
Reference in New Issue
Block a user