Compare commits

...

6 Commits

Author SHA1 Message Date
Christopher Allen Lane
ecfb83a3b0 docs: clean up project documentation
- Convert all headings from underline style to ATX style
- Fix heading hierarchy in INSTALLING.md (H3→H2, H4→H3)
- Move Installing section before Usage in README
- Bump install snippet version from 4.5.1 to 4.7.0
- Add feature request guidance to CONTRIBUTING.md
- Add installer package and .cheat discovery to CLAUDE.md
- Remove unused reference links and dead HTML comments
- Trim trailing whitespace

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:56:58 -05:00
Christopher Allen Lane
9440b4f816 test: remove redundant tests
Remove integration tests in config_extended_test.go that duplicate
unit tests in config_test.go for findLocalCheatpath. Remove redundant
fuzz functions (FuzzFindLocalCheatpathNearestWins, FuzzTagsStress),
a duplicate parse test (TestParseInvalidYAML), and a permanently-skipped
interactive test (TestPromptIntegration).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:46:17 -05:00
Christopher Allen Lane
971be88150 test: audit and clean up test suite
Delete tautological, no-assertion, and permanently-skipped tests.
Rewrite false-confidence tests that couldn't detect the bugs they
claimed to test. Decouple brittle assertions from error message
strings and third-party library output. Fix misleading test names
and error messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:42:11 -05:00
Christopher Allen Lane
d4a8a79628 chore: bump version to 4.7.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:24:54 -05:00
Christopher Allen Lane
007c9f9efe Merge branch 'brief-list-flag' 2026-02-15 12:24:42 -05:00
Christopher Allen Lane
f61203ac1b feat: add -b/--brief flag for compact list output (#505)
Add a --brief flag that lists cheatsheets with only title and tags,
omitting the file path column. Can be used standalone (-b) or combined
with --list (-lb), and composes with existing filters (-t, -p).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 12:21:35 -05:00
26 changed files with 258 additions and 745 deletions

View File

@@ -85,7 +85,11 @@ The `cheat` command-line tool is organized into several key packages:
- Writes to stdout or pager - Writes to stdout or pager
- Handles text formatting and indentation - Handles text formatting and indentation
6. **`internal/repo`**: Git repository management 6. **`internal/installer`**: First-run installer
- Prompts user for initial configuration choices
- Generates default `conf.yml` and downloads community cheatsheets
7. **`internal/repo`**: Git repository management
- Clones community cheatsheet repositories - Clones community cheatsheet repositories
- Updates existing repositories - Updates existing repositories
@@ -95,6 +99,7 @@ The `cheat` command-line tool is organized into several key packages:
- **Override mechanism**: Local sheets override community sheets with same name - **Override mechanism**: Local sheets override community sheets with same name
- **Tag system**: Sheets can be categorized with tags in frontmatter - **Tag system**: Sheets can be categorized with tags in frontmatter
- **Multiple cheatpaths**: Supports personal, community, and directory-scoped sheets - **Multiple cheatpaths**: Supports personal, community, and directory-scoped sheets
- **Directory-scoped discovery**: Walks up from cwd to find the nearest `.cheat` directory (like `.git` discovery)
### Sheet Format ### Sheet Format

View File

@@ -1,5 +1,4 @@
Contributing # Contributing
============
Thank you for your interest in `cheat`. Thank you for your interest in `cheat`.
@@ -11,4 +10,8 @@ Bug reports are still welcome. If you've found a bug, please open an issue in
the [issue tracker][issues]. Before doing so, please search through the the [issue tracker][issues]. Before doing so, please search through the
existing open issues to make sure it hasn't already been reported. existing open issues to make sure it hasn't already been reported.
Feature requests may be filed, but are unlikely to be implemented. The project
is now mature and the maintainer considers its feature set to be essentially
complete.
[issues]: https://github.com/cheat/cheat/issues [issues]: https://github.com/cheat/cheat/issues

View File

@@ -1,30 +1,29 @@
Installing # Installing
==========
`cheat` has no runtime dependencies. As such, installing it is generally `cheat` has no runtime dependencies. As such, installing it is generally
straightforward. There are a few methods available: straightforward. There are a few methods available:
### Install manually ## Install manually
#### Unix-like ### Unix-like
On Unix-like systems, you may simply paste the following snippet into your terminal: On Unix-like systems, you may simply paste the following snippet into your terminal:
```sh ```sh
cd /tmp \ cd /tmp \
&& wget https://github.com/cheat/cheat/releases/download/4.5.1/cheat-linux-amd64.gz \ && wget https://github.com/cheat/cheat/releases/download/4.7.0/cheat-linux-amd64.gz \
&& gunzip cheat-linux-amd64.gz \ && gunzip cheat-linux-amd64.gz \
&& chmod +x cheat-linux-amd64 \ && chmod +x cheat-linux-amd64 \
&& sudo mv cheat-linux-amd64 /usr/local/bin/cheat && sudo mv cheat-linux-amd64 /usr/local/bin/cheat
``` ```
You may need to need to change the version number (`4.5.1`) and the archive You may need to need to change the version number (`4.7.0`) and the archive
(`cheat-linux-amd64.gz`) depending on your platform. (`cheat-linux-amd64.gz`) depending on your platform.
See the [releases page][releases] for a list of supported platforms. See the [releases page][releases] for a list of supported platforms.
#### Windows ### Windows
On Windows, download the appropriate binary from the [releases page][releases], On Windows, download the appropriate binary from the [releases page][releases],
unzip the archive, and place the `cheat.exe` executable on your `PATH`. unzip the archive, and place the `cheat.exe` executable on your `PATH`.
### Install via `go install` ## Install via `go install`
If you have `go` version `>=1.17` available on your `PATH`, you can install If you have `go` version `>=1.17` available on your `PATH`, you can install
`cheat` via `go install`: `cheat` via `go install`:
@@ -32,7 +31,7 @@ If you have `go` version `>=1.17` available on your `PATH`, you can install
go install github.com/cheat/cheat/cmd/cheat@latest go install github.com/cheat/cheat/cmd/cheat@latest
``` ```
### Install via package manager ## Install via package manager
Several community-maintained packages are also available: Several community-maintained packages are also available:
Package manager | Package(s) Package manager | Package(s)
@@ -43,8 +42,6 @@ docker | [docker-cheat][pkg-docker]
nix | [nixos.cheat][pkg-nix] nix | [nixos.cheat][pkg-nix]
snap | [cheat][pkg-snap] snap | [cheat][pkg-snap]
<!--[pacman][] |-->
## Configuring ## Configuring
Three things must be done before you can use `cheat`: Three things must be done before you can use `cheat`:
1. A config file must be generated 1. A config file must be generated
@@ -56,7 +53,7 @@ automatically. After the installer is complete, it is strongly advised that you
view the configuration file that was generated, as you may want to change some view the configuration file that was generated, as you may want to change some
of its default values (to enable colorization, change the paginator, etc). of its default values (to enable colorization, change the paginator, etc).
### conf.yml ### ### conf.yml
`cheat` is configured by a YAML file that will be auto-generated on first run. `cheat` is configured by a YAML file that will be auto-generated on first run.
By default, the config file is assumed to exist on an XDG-compliant By default, the config file is assumed to exist on an XDG-compliant
@@ -72,8 +69,8 @@ export CHEAT_CONFIG_PATH="~/.dotfiles/cheat/conf.yml"
[community]: https://github.com/cheat/cheatsheets/ [community]: https://github.com/cheat/cheatsheets/
[pkg-aur-cheat-bin]: https://aur.archlinux.org/packages/cheat-bin [pkg-aur-cheat-bin]: https://aur.archlinux.org/packages/cheat-bin
[pkg-aur-cheat]: https://aur.archlinux.org/packages/cheat [pkg-aur-cheat]: https://aur.archlinux.org/packages/cheat
[pkg-brew]: https://formulae.brew.sh/formula/cheat [pkg-brew]: https://formulae.brew.sh/formula/cheat
[pkg-docker]: https://github.com/bannmann/docker-cheat [pkg-docker]: https://github.com/bannmann/docker-cheat
[pkg-nix]: https://search.nixos.org/packages?channel=unstable&show=cheat&from=0&size=50&sort=relevance&type=packages&query=cheat [pkg-nix]: https://search.nixos.org/packages?channel=unstable&show=cheat&from=0&size=50&sort=relevance&type=packages&query=cheat
[pkg-snap]: https://snapcraft.io/cheat [pkg-snap]: https://snapcraft.io/cheat
[releases]: https://github.com/cheat/cheat/releases [releases]: https://github.com/cheat/cheat/releases

View File

@@ -1,8 +1,6 @@
![Workflow status](https://github.com/cheat/cheat/actions/workflows/build.yml/badge.svg) ![Workflow status](https://github.com/cheat/cheat/actions/workflows/build.yml/badge.svg)
# cheat
cheat
=====
`cheat` allows you to create and view interactive cheatsheets on the `cheat` allows you to create and view interactive cheatsheets on the
command-line. It was designed to help remind \*nix system administrators of command-line. It was designed to help remind \*nix system administrators of
@@ -13,9 +11,7 @@ remember.
Use `cheat` with [cheatsheets][]. Use `cheat` with [cheatsheets][].
## Example
Example
-------
The next time you're forced to disarm a nuclear weapon without consulting The next time you're forced to disarm a nuclear weapon without consulting
Google, you may run: Google, you may run:
@@ -42,8 +38,10 @@ tar -xjvf '/path/to/foo.tgz'
tar -cjvf '/path/to/foo.tgz' '/path/to/foo/' tar -cjvf '/path/to/foo.tgz' '/path/to/foo/'
``` ```
Usage ## Installing
----- For installation and configuration instructions, see [INSTALLING.md][].
## Usage
To view a cheatsheet: To view a cheatsheet:
```sh ```sh
@@ -70,6 +68,12 @@ To list all available cheatsheets:
cheat -l cheat -l
``` ```
To briefly list all cheatsheets (names and tags only):
```sh
cheat -b
```
To list all cheatsheets that are tagged with "networking": To list all cheatsheets that are tagged with "networking":
```sh ```sh
@@ -101,14 +105,7 @@ Flags may be combined in intuitive ways. Example: to search sheets on the
cheat -p personal -t networking --regex -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}' cheat -p personal -t networking --regex -s '(?:[0-9]{1,3}\.){3}[0-9]{1,3}'
``` ```
## Cheatsheets
Installing
----------
For installation and configuration instructions, see [INSTALLING.md][].
Cheatsheets
-----------
Cheatsheets are plain-text files with no file extension, and are named Cheatsheets are plain-text files with no file extension, and are named
according to the command used to view them: according to the command used to view them:
@@ -137,8 +134,7 @@ 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`.
Cheatpaths ## Cheatpaths
----------
Cheatsheets are stored on "cheatpaths", which are directories that contain Cheatsheets are stored on "cheatpaths", which are directories that contain
cheatsheets. Cheatpaths are specified in the `conf.yml` file. cheatsheets. Cheatpaths are specified in the `conf.yml` file.
@@ -170,7 +166,7 @@ If a user attempts to edit a cheatsheet on a read-only cheatpath, `cheat` will
transparently copy that sheet to a writeable directory before opening it for transparently copy that sheet to a writeable directory before opening it for
editing. 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` directory your filesystem. `cheat` facilitates this by searching for a `.cheat` directory
in the current working directory and its ancestors (similar to how `git` locates in the current working directory and its ancestors (similar to how `git` locates
@@ -178,8 +174,7 @@ in the current working directory and its ancestors (similar to how `git` locates
added to the cheatpaths. This means you can place a `.cheat` directory at your 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. project root and it will be available from any subdirectory within that project.
Autocompletion ## Autocompletion
--------------
Shell autocompletion is currently available for `bash`, `fish`, and `zsh`. Copy Shell autocompletion is currently available for `bash`, `fish`, and `zsh`. Copy
the relevant [completion script][completions] into the appropriate directory on the relevant [completion script][completions] into the appropriate directory on
your filesystem to enable autocompletion. (This directory will vary depending your filesystem to enable autocompletion. (This directory will vary depending
@@ -198,4 +193,3 @@ Additionally, `cheat` supports enhanced autocompletion via integration with
[Chroma]: https://github.com/alecthomas/chroma [Chroma]: https://github.com/alecthomas/chroma
[supported languages]: https://github.com/alecthomas/chroma#supported-languages [supported languages]: https://github.com/alecthomas/chroma#supported-languages
[fzf]: https://github.com/junegunn/fzf [fzf]: https://github.com/junegunn/fzf
[go]: https://golang.org

View File

@@ -0,0 +1,128 @@
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)
// TestBriefFlagIntegration exercises the -b/--brief flag end-to-end.
func TestBriefFlagIntegration(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)
}
// Set up a temp environment with some cheatsheets.
root := t.TempDir()
sheetsDir := filepath.Join(root, "sheets")
os.MkdirAll(sheetsDir, 0755)
os.WriteFile(
filepath.Join(sheetsDir, "tar"),
[]byte("---\nsyntax: bash\ntags: [ compression ]\n---\ntar xf archive.tar\n"),
0644,
)
os.WriteFile(
filepath.Join(sheetsDir, "curl"),
[]byte("---\nsyntax: bash\ntags: [ networking, http ]\n---\ncurl https://example.com\n"),
0644,
)
confPath := filepath.Join(root, "conf.yml")
conf := fmt.Sprintf("---\neditor: vi\ncolorize: false\ncheatpaths:\n - name: test\n path: %s\n readonly: true\n", sheetsDir)
os.WriteFile(confPath, []byte(conf), 0644)
env := []string{
"CHEAT_CONFIG_PATH=" + confPath,
"HOME=" + root,
"PATH=" + os.Getenv("PATH"),
"EDITOR=vi",
}
run := func(t *testing.T, args ...string) string {
t.Helper()
cmd := exec.Command(binPath, args...)
cmd.Dir = root
cmd.Env = env
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("cheat %v failed: %v\nOutput: %s", args, err, output)
}
return string(output)
}
t.Run("brief output omits file path column", func(t *testing.T) {
output := run(t, "-b")
lines := strings.Split(strings.TrimSpace(output), "\n")
// Header should have title and tags but not file
if !strings.Contains(lines[0], "title:") {
t.Errorf("expected title: in header, got: %s", lines[0])
}
if !strings.Contains(lines[0], "tags:") {
t.Errorf("expected tags: in header, got: %s", lines[0])
}
if strings.Contains(lines[0], "file:") {
t.Errorf("brief output should not contain file: column, got: %s", lines[0])
}
// Data lines should not contain the sheets directory path
for _, line := range lines[1:] {
if strings.Contains(line, sheetsDir) {
t.Errorf("brief output should not contain file paths, got: %s", line)
}
}
})
t.Run("list output still includes file path column", func(t *testing.T) {
output := run(t, "-l")
lines := strings.Split(strings.TrimSpace(output), "\n")
if !strings.Contains(lines[0], "file:") {
t.Errorf("list output should contain file: column, got: %s", lines[0])
}
})
t.Run("brief with filter works", func(t *testing.T) {
output := run(t, "-b", "tar")
if !strings.Contains(output, "tar") {
t.Errorf("expected tar in output, got: %s", output)
}
if strings.Contains(output, "curl") {
t.Errorf("filter should exclude curl, got: %s", output)
}
})
t.Run("combined -lb works identically to -b", func(t *testing.T) {
briefOnly := run(t, "-b", "tar")
combined := run(t, "-lb", "tar")
if briefOnly != combined {
t.Errorf("-b and -lb should produce identical output\n-b:\n%s\n-lb:\n%s", briefOnly, combined)
}
})
t.Run("brief with tag filter works", func(t *testing.T) {
output := run(t, "-b", "-t", "networking")
if !strings.Contains(output, "curl") {
t.Errorf("expected curl in tag-filtered output, got: %s", output)
}
if strings.Contains(output, "tar") {
// tar is tagged "compression", not "networking"
t.Errorf("tag filter should exclude tar, got: %s", output)
}
if strings.Contains(output, "file:") {
t.Errorf("brief output should not contain file: column, got: %s", output)
}
})
}

View File

@@ -87,12 +87,17 @@ func cmdList(opts map[string]interface{}, conf config.Config) {
var out bytes.Buffer var out bytes.Buffer
w := tabwriter.NewWriter(&out, 0, 0, 1, ' ', 0) w := tabwriter.NewWriter(&out, 0, 0, 1, ' ', 0)
// write a header row
fmt.Fprintln(w, "title:\tfile:\ttags:")
// generate sorted, columnized output // generate sorted, columnized output
for _, sheet := range flattened { if opts["--brief"].(bool) {
fmt.Fprintf(w, "%s\t%s\t%s\n", sheet.Title, sheet.Path, strings.Join(sheet.Tags, ",")) fmt.Fprintln(w, "title:\ttags:")
for _, sheet := range flattened {
fmt.Fprintf(w, "%s\t%s\n", sheet.Title, strings.Join(sheet.Tags, ","))
}
} else {
fmt.Fprintln(w, "title:\tfile:\ttags:")
for _, sheet := range flattened {
fmt.Fprintf(w, "%s\t%s\t%s\n", sheet.Title, sheet.Path, strings.Join(sheet.Tags, ","))
}
} }
// write columnized output to stdout // write columnized output to stdout

View File

@@ -15,7 +15,7 @@ import (
"github.com/cheat/cheat/internal/installer" "github.com/cheat/cheat/internal/installer"
) )
const version = "4.6.0" const version = "4.7.0"
func main() { func main() {
@@ -129,7 +129,7 @@ func main() {
case opts["--edit"] != nil: case opts["--edit"] != nil:
cmd = cmdEdit cmd = cmdEdit
case opts["--list"].(bool): case opts["--list"].(bool), opts["--brief"].(bool):
cmd = cmdList cmd = cmdList
case opts["--tags"].(bool): case opts["--tags"].(bool):

View File

@@ -9,7 +9,6 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
@@ -108,23 +107,15 @@ cheatpaths:
cmd := exec.Command(cheatBin, tc.args...) cmd := exec.Command(cheatBin, tc.args...)
cmd.Env = env cmd.Env = env
// Capture output to prevent spamming
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout cmd.Stdout = &stdout
cmd.Stderr = &stderr cmd.Stderr = &stderr
start := time.Now()
err := cmd.Run() err := cmd.Run()
elapsed := time.Since(start)
if err != nil { if err != nil {
b.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String()) b.Fatalf("Command failed: %v\nStderr: %s", err, stderr.String())
} }
// Report custom metric
b.ReportMetric(float64(elapsed.Nanoseconds())/1e6, "ms/op")
// Ensure we got some results
if stdout.Len() == 0 { if stdout.Len() == 0 {
b.Fatal("No output from search") b.Fatal("No output from search")
} }

View File

@@ -8,6 +8,7 @@ func usage() string {
Options: Options:
--init Write a default config file to stdout --init Write a default config file to stdout
-a --all Search among all cheatpaths -a --all Search among all cheatpaths
-b --brief List cheatsheets without file paths
-c --colorize Colorize output -c --colorize Colorize output
-d --directories List cheatsheet directories -d --directories List cheatsheet directories
-e --edit=<cheatsheet> Edit <cheatsheet> -e --edit=<cheatsheet> Edit <cheatsheet>
@@ -41,8 +42,8 @@ Examples:
To list all available cheatsheets: To list all available cheatsheets:
cheat -l cheat -l
To list all cheatsheets whose titles match "apt": To briefly list all cheatsheets whose titles match "apt":
cheat -l apt cheat -b apt
To list all tags in use: To list all tags in use:
cheat -T cheat -T

View File

@@ -23,6 +23,9 @@ Display the config file path.
\-a, \[en]all \-a, \[en]all
Search among all cheatpaths. Search among all cheatpaths.
.TP .TP
\-b, \[en]brief
List cheatsheets without file paths.
.TP
\-c, \[en]colorize \-c, \[en]colorize
Colorize output. Colorize output.
.TP .TP
@@ -72,8 +75,8 @@ cheat \-d
To list all available cheatsheets: To list all available cheatsheets:
cheat \-l cheat \-l
.TP .TP
To list all cheatsheets whose titles match `apt': To briefly list all cheatsheets whose titles match `apt':
cheat \-l \f[I]apt\f[R] cheat \-b \f[I]apt\f[R]
.TP .TP
To list all tags in use: To list all tags in use:
cheat \-T cheat \-T

View File

@@ -29,6 +29,9 @@ OPTIONS
-a, --all -a, --all
: Search among all cheatpaths. : Search among all cheatpaths.
-b, --brief
: List cheatsheets without file paths.
-c, --colorize -c, --colorize
: Colorize output. : Colorize output.
@@ -81,8 +84,8 @@ To view all cheatsheet directories:
To list all available cheatsheets: To list all available cheatsheets:
: cheat -l : cheat -l
To list all cheatsheets whose titles match 'apt': To briefly list all cheatsheets whose titles match 'apt':
: cheat -l _apt_ : cheat -b _apt_
To list all tags in use: To list all tags in use:
: cheat -T : cheat -T

View File

@@ -88,26 +88,3 @@ func TestCheatpathValidate(t *testing.T) {
}) })
} }
} }
func TestCheatpathStruct(t *testing.T) {
// Test that the struct fields work as expected
cp := Cheatpath{
Name: "test",
Path: "/test/path",
ReadOnly: true,
Tags: []string{"tag1", "tag2"},
}
if cp.Name != "test" {
t.Errorf("expected Name to be 'test', got %q", cp.Name)
}
if cp.Path != "/test/path" {
t.Errorf("expected Path to be '/test/path', got %q", cp.Path)
}
if !cp.ReadOnly {
t.Error("expected ReadOnly to be true")
}
if len(cp.Tags) != 2 || cp.Tags[0] != "tag1" || cp.Tags[1] != "tag2" {
t.Errorf("expected Tags to be [tag1 tag2], got %v", cp.Tags)
}
}

View File

@@ -3,7 +3,6 @@ package config
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"testing" "testing"
"github.com/cheat/cheat/internal/mock" "github.com/cheat/cheat/internal/mock"
@@ -19,7 +18,7 @@ func TestConfigYAMLErrors(t *testing.T) {
defer os.RemoveAll(tempDir) defer os.RemoveAll(tempDir)
invalidYAML := filepath.Join(tempDir, "invalid.yml") invalidYAML := filepath.Join(tempDir, "invalid.yml")
err = os.WriteFile(invalidYAML, []byte("invalid: yaml: content:\n - no closing"), 0644) err = os.WriteFile(invalidYAML, []byte("cheatpaths: [{unclosed\n"), 0644)
if err != nil { if err != nil {
t.Fatalf("failed to write invalid yaml: %v", err) t.Fatalf("failed to write invalid yaml: %v", err)
} }
@@ -31,242 +30,6 @@ func TestConfigYAMLErrors(t *testing.T) {
} }
} }
// TestConfigLocalCheatpath tests local .cheat directory detection
func TestConfigLocalCheatpath(t *testing.T) {
// Create a temporary directory to act as working directory
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)
}
// Save current working directory
oldCwd, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get cwd: %v", err)
}
defer os.Chdir(oldCwd)
// Change to temp directory
err = os.Chdir(tempDir)
if err != nil {
t.Fatalf("failed to change dir: %v", err)
}
// Create .cheat directory
localCheat := filepath.Join(tempDir, ".cheat")
err = os.Mkdir(localCheat, 0755)
if err != nil {
t.Fatalf("failed to create .cheat dir: %v", err)
}
// Load config
conf, err := New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
if err != nil {
t.Errorf("failed to load config: %v", err)
}
// Check that local cheatpath was added
found := false
for _, cp := range conf.Cheatpaths {
if cp.Name == "cwd" && cp.Path == localCheat {
found = true
break
}
}
if !found {
t.Error("local .cheat directory was not added to cheatpaths")
}
}
// 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
@@ -335,7 +98,10 @@ cheatpaths:
} }
// Verify symlink was resolved // Verify symlink was resolved
if len(conf.Cheatpaths) > 0 && conf.Cheatpaths[0].Path != targetDir { if len(conf.Cheatpaths) == 0 {
t.Fatal("expected at least one cheatpath, got none")
}
if conf.Cheatpaths[0].Path != targetDir {
t.Errorf("expected symlink to be resolved to %s, got %s", targetDir, conf.Cheatpaths[0].Path) t.Errorf("expected symlink to be resolved to %s, got %s", targetDir, conf.Cheatpaths[0].Path)
} }
} }
@@ -380,70 +146,3 @@ cheatpaths:
t.Errorf("expected broken cheatpath to be filtered out, got %d cheatpaths", len(conf.Cheatpaths)) t.Errorf("expected broken cheatpath to be filtered out, got %d cheatpaths", len(conf.Cheatpaths))
} }
} }
// TestConfigTildeExpansionError tests tilde expansion error handling
func TestConfigTildeExpansionError(t *testing.T) {
// This is tricky to test without mocking homedir.Expand
// We'll create a config with an invalid home reference
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
// Create config with user that likely doesn't exist
configContent := `---
editor: vim
cheatpaths:
- name: test
path: ~nonexistentuser12345/cheat
readonly: true
`
configFile := filepath.Join(tempDir, "config.yml")
err = os.WriteFile(configFile, []byte(configContent), 0644)
if err != nil {
t.Fatalf("failed to write config: %v", err)
}
// Load config - this may or may not fail depending on the system
// but we're testing that it doesn't panic
_, _ = New(map[string]interface{}{}, configFile, false)
}
// TestConfigGetCwdError tests error handling when os.Getwd fails
func TestConfigGetCwdError(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows does not allow removing the current directory")
}
// This is difficult to test without being able to break os.Getwd
// We'll create a scenario where the current directory is removed
// Create and enter a temp directory
tempDir, err := os.MkdirTemp("", "cheat-config-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
oldCwd, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get cwd: %v", err)
}
defer os.Chdir(oldCwd)
err = os.Chdir(tempDir)
if err != nil {
t.Fatalf("failed to change dir: %v", err)
}
// Remove the directory we're in
err = os.RemoveAll(tempDir)
if err != nil {
t.Fatalf("failed to remove temp dir: %v", err)
}
// Now os.Getwd should fail
_, err = New(map[string]interface{}{}, mock.Path("conf/empty.yml"), false)
// This might not fail on all systems, so we just ensure no panic
_ = err
}

View File

@@ -65,58 +65,3 @@ func FuzzFindLocalCheatpath(f *testing.F) {
} }
}) })
} }
// 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)
}
})
}

View File

@@ -4,7 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"testing" "testing"
) )
@@ -90,9 +89,6 @@ func TestInitWriteError(t *testing.T) {
if err == nil { if err == nil {
t.Error("expected error when writing to invalid path, got nil") t.Error("expected error when writing to invalid path, got nil")
} }
if err != nil && !strings.Contains(err.Error(), "failed to create") {
t.Errorf("expected 'failed to create' error, got: %v", err)
}
} }
// TestInitExistingFile tests that Init overwrites existing files // TestInitExistingFile tests that Init overwrites existing files

View File

@@ -2,6 +2,7 @@ package config
import ( import (
"os" "os"
"path/filepath"
"runtime" "runtime"
"testing" "testing"
) )
@@ -44,29 +45,20 @@ func TestPager(t *testing.T) {
os.Setenv("PAGER", "") os.Setenv("PAGER", "")
pager := Pager() pager := Pager()
// Should find one of the fallback pagers or return empty string if pager == "" {
return // no pager found is acceptable
}
// Should find one of the known fallback pagers
validPagers := map[string]bool{ validPagers := map[string]bool{
"": true, // no pager found
"pager": true, "pager": true,
"less": true, "less": true,
"more": true, "more": true,
} }
// Check if it's a path to one of these base := filepath.Base(pager)
found := false if !validPagers[base] {
for p := range validPagers { t.Errorf("unexpected pager value: %s (base: %s)", pager, base)
if p == "" && pager == "" {
found = true
break
}
if p != "" && (pager == p || len(pager) >= len(p) && pager[len(pager)-len(p):] == p) {
found = true
break
}
}
if !found {
t.Errorf("unexpected pager value: %s", pager)
} }
}) })

View File

@@ -71,19 +71,28 @@ func TestInvalidateMissingCheatpaths(t *testing.T) {
} }
} }
// TestMissingInvalidFormatters asserts that configs which contain invalid // TestInvalidateInvalidFormatter asserts that configs which contain invalid
// formatters are invalidated // formatters are invalidated
func TestMissingInvalidFormatters(t *testing.T) { func TestInvalidateInvalidFormatter(t *testing.T) {
// mock a config // mock a config with a valid editor and cheatpaths but invalid formatter
conf := Config{ conf := Config{
Colorize: true, Colorize: true,
Editor: "vim", Editor: "vim",
Formatter: "html",
Cheatpaths: []cheatpath.Cheatpath{
cheatpath.Cheatpath{
Name: "foo",
Path: "/foo",
ReadOnly: false,
Tags: []string{},
},
},
} }
// assert that no errors are returned // assert that the config is invalidated due to the formatter
if err := conf.Validate(); err == nil { if err := conf.Validate(); err == nil {
t.Errorf("failed to invalidate config without formatter") t.Errorf("failed to invalidate config with invalid formatter")
} }
} }

View File

@@ -2,7 +2,6 @@ package installer
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"os" "os"
"strings" "strings"
@@ -158,23 +157,3 @@ func TestPromptError(t *testing.T) {
t.Errorf("expected 'failed to prompt' error, got: %v", err) t.Errorf("expected 'failed to prompt' error, got: %v", err)
} }
} }
// TestPromptIntegration provides a simple integration test
func TestPromptIntegration(t *testing.T) {
// This demonstrates how the prompt would be used in practice
// It's skipped by default since it requires actual user input
if os.Getenv("TEST_INTERACTIVE") != "1" {
t.Skip("Skipping interactive test - set TEST_INTERACTIVE=1 to run")
}
fmt.Println("\n=== Interactive Prompt Test ===")
fmt.Println("You will be prompted to answer a question.")
fmt.Println("Try different inputs: y, n, Y, N, empty (just press Enter)")
result, err := Prompt("Would you like to continue? [Y/n]", true)
if err != nil {
t.Fatalf("Prompt failed: %v", err)
}
fmt.Printf("You answered: %v\n", result)
}

View File

@@ -1,7 +1,6 @@
package installer package installer
import ( import (
"fmt"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@@ -245,10 +244,10 @@ cheatpaths:
if strings.Contains(contentStr, "PERSONAL_PATH") { if strings.Contains(contentStr, "PERSONAL_PATH") {
t.Error("PERSONAL_PATH was not replaced") t.Error("PERSONAL_PATH was not replaced")
} }
if strings.Contains(contentStr, "EDITOR_PATH") && !strings.Contains(contentStr, fmt.Sprintf("editor: %s", "")) { if strings.Contains(contentStr, "EDITOR_PATH") {
t.Error("EDITOR_PATH was not replaced") t.Error("EDITOR_PATH was not replaced")
} }
if strings.Contains(contentStr, "PAGER_PATH") && !strings.Contains(contentStr, fmt.Sprintf("pager: %s", "")) { if strings.Contains(contentStr, "PAGER_PATH") {
t.Error("PAGER_PATH was not replaced") t.Error("PAGER_PATH was not replaced")
} }
if strings.Contains(contentStr, "WORK_PATH") { if strings.Contains(contentStr, "WORK_PATH") {

View File

@@ -1,6 +1,7 @@
package sheet package sheet
import ( import (
"strings"
"testing" "testing"
"github.com/cheat/cheat/internal/config" "github.com/cheat/cheat/internal/config"
@@ -16,45 +17,26 @@ func TestColorize(t *testing.T) {
} }
// mock a sheet // mock a sheet
original := "echo 'foo'"
s := Sheet{ s := Sheet{
Text: "echo 'foo'", Text: original,
} }
// colorize the sheet text // colorize the sheet text
s.Colorize(conf) s.Colorize(conf)
// initialize expectations // assert that the text was modified (colorization applied)
want := "echo" if s.Text == original {
want += " 'foo'" t.Error("Colorize did not modify sheet text")
}
// assert // assert that ANSI escape codes are present
if s.Text != want { if !strings.Contains(s.Text, "\x1b[") && !strings.Contains(s.Text, "[0m") {
t.Errorf("failed to colorize sheet: want: %s, got: %s", want, s.Text) t.Errorf("colorized text does not contain ANSI escape codes: %q", s.Text)
}
// assert that the original content is still present within the colorized output
if !strings.Contains(s.Text, "echo") || !strings.Contains(s.Text, "foo") {
t.Errorf("colorized text lost original content: %q", s.Text)
} }
} }
// TestColorizeError tests the error handling in Colorize
func TestColorizeError(_ *testing.T) {
// Create a sheet with content
sheet := Sheet{
Text: "some text",
Syntax: "invalidlexer12345", // Use an invalid lexer that might cause issues
}
// Create a config with invalid formatter/style
conf := config.Config{
Formatter: "invalidformatter",
Style: "invalidstyle",
}
// Store original text
originalText := sheet.Text
// Colorize should not panic even with invalid settings
sheet.Colorize(conf)
// The text might be unchanged if there was an error, or it might be colorized
// We're mainly testing that it doesn't panic
_ = sheet.Text
_ = originalText
}

View File

@@ -10,15 +10,12 @@ import (
// TestCopyErrors tests error cases for the Copy method // TestCopyErrors tests error cases for the Copy method
func TestCopyErrors(t *testing.T) { func TestCopyErrors(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
setup func() (*Sheet, string, func()) setup func() (*Sheet, string, func())
wantErr bool
errMsg string
}{ }{
{ {
name: "source file does not exist", name: "source file does not exist",
setup: func() (*Sheet, string, func()) { setup: func() (*Sheet, string, func()) {
// Create a sheet with non-existent path
sheet := &Sheet{ sheet := &Sheet{
Title: "test", Title: "test",
Path: "/non/existent/file.txt", Path: "/non/existent/file.txt",
@@ -30,13 +27,10 @@ func TestCopyErrors(t *testing.T) {
} }
return sheet, dest, cleanup return sheet, dest, cleanup
}, },
wantErr: true,
errMsg: "failed to open cheatsheet",
}, },
{ {
name: "destination directory creation fails", name: "destination directory creation fails",
setup: func() (*Sheet, string, func()) { setup: func() (*Sheet, string, func()) {
// Create a source file
src, err := os.CreateTemp("", "copy-test-src-*") src, err := os.CreateTemp("", "copy-test-src-*")
if err != nil { if err != nil {
t.Fatalf("failed to create temp file: %v", err) t.Fatalf("failed to create temp file: %v", err)
@@ -50,13 +44,11 @@ func TestCopyErrors(t *testing.T) {
CheatPath: "test", CheatPath: "test",
} }
// Create a file where we want a directory
blockerFile := filepath.Join(os.TempDir(), "copy-blocker-file") blockerFile := filepath.Join(os.TempDir(), "copy-blocker-file")
if err := os.WriteFile(blockerFile, []byte("blocker"), 0644); err != nil { if err := os.WriteFile(blockerFile, []byte("blocker"), 0644); err != nil {
t.Fatalf("failed to create blocker file: %v", err) t.Fatalf("failed to create blocker file: %v", err)
} }
// Try to create dest under the blocker file (will fail)
dest := filepath.Join(blockerFile, "subdir", "dest.txt") dest := filepath.Join(blockerFile, "subdir", "dest.txt")
cleanup := func() { cleanup := func() {
@@ -65,13 +57,10 @@ func TestCopyErrors(t *testing.T) {
} }
return sheet, dest, cleanup return sheet, dest, cleanup
}, },
wantErr: true,
errMsg: "failed to create directory",
}, },
{ {
name: "destination file creation fails", name: "destination file creation fails",
setup: func() (*Sheet, string, func()) { setup: func() (*Sheet, string, func()) {
// Create a source file
src, err := os.CreateTemp("", "copy-test-src-*") src, err := os.CreateTemp("", "copy-test-src-*")
if err != nil { if err != nil {
t.Fatalf("failed to create temp file: %v", err) t.Fatalf("failed to create temp file: %v", err)
@@ -85,7 +74,6 @@ func TestCopyErrors(t *testing.T) {
CheatPath: "test", CheatPath: "test",
} }
// Create a directory where we want the file
destDir := filepath.Join(os.TempDir(), "copy-test-dir") destDir := filepath.Join(os.TempDir(), "copy-test-dir")
if err := os.Mkdir(destDir, 0755); err != nil && !os.IsExist(err) { if err := os.Mkdir(destDir, 0755); err != nil && !os.IsExist(err) {
t.Fatalf("failed to create dest dir: %v", err) t.Fatalf("failed to create dest dir: %v", err)
@@ -97,8 +85,6 @@ func TestCopyErrors(t *testing.T) {
} }
return sheet, destDir, cleanup return sheet, destDir, cleanup
}, },
wantErr: true,
errMsg: "failed to create outfile",
}, },
} }
@@ -108,43 +94,27 @@ func TestCopyErrors(t *testing.T) {
defer cleanup() defer cleanup()
err := sheet.Copy(dest) err := sheet.Copy(dest)
if (err != nil) != tt.wantErr { if err == nil {
t.Errorf("Copy() error = %v, wantErr %v", err, tt.wantErr) t.Error("Copy() expected error, got nil")
return
}
if err != nil && tt.errMsg != "" {
if !contains(err.Error(), tt.errMsg) {
t.Errorf("Copy() error = %v, want error containing %q", err, tt.errMsg)
}
} }
}) })
} }
} }
// TestCopyIOError tests the io.Copy error case // TestCopyUnreadableSource verifies that Copy returns an error when the source
func TestCopyIOError(t *testing.T) { // file cannot be opened (e.g., permission denied).
// This is difficult to test without mocking io.Copy func TestCopyUnreadableSource(t *testing.T) {
// The error case would occur if the source file is modified
// or removed after opening but before copying
t.Skip("Skipping io.Copy error test - requires file system race condition")
}
// TestCopyCleanupOnError verifies that partially written files are cleaned up on error
func TestCopyCleanupOnError(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.Skip("chmod does not restrict reads on Windows") t.Skip("chmod does not restrict reads on Windows")
} }
// Create a source file that we'll make unreadable after opening src, err := os.CreateTemp("", "copy-test-unreadable-*")
src, err := os.CreateTemp("", "copy-test-cleanup-*")
if err != nil { if err != nil {
t.Fatalf("failed to create temp file: %v", err) t.Fatalf("failed to create temp file: %v", err)
} }
defer os.Remove(src.Name()) defer os.Remove(src.Name())
// Write some content if _, err := src.WriteString("test content"); err != nil {
content := "test content for cleanup"
if _, err := src.WriteString(content); err != nil {
t.Fatalf("failed to write content: %v", err) t.Fatalf("failed to write content: %v", err)
} }
src.Close() src.Close()
@@ -155,38 +125,21 @@ func TestCopyCleanupOnError(t *testing.T) {
CheatPath: "test", CheatPath: "test",
} }
// Destination path dest := filepath.Join(os.TempDir(), "copy-unreadable-test.txt")
dest := filepath.Join(os.TempDir(), "copy-cleanup-test.txt") defer os.Remove(dest)
defer os.Remove(dest) // Clean up if test fails
// Make the source file unreadable (simulating a read error during copy)
// This is platform-specific, but should work on Unix-like systems
if err := os.Chmod(src.Name(), 0000); err != nil { if err := os.Chmod(src.Name(), 0000); err != nil {
t.Skip("Cannot change file permissions on this platform") t.Skip("Cannot change file permissions on this platform")
} }
defer os.Chmod(src.Name(), 0644) // Restore permissions for cleanup defer os.Chmod(src.Name(), 0644)
// Attempt to copy - this should fail during io.Copy
err = sheet.Copy(dest) err = sheet.Copy(dest)
if err == nil { if err == nil {
t.Error("Expected Copy to fail with permission error") t.Error("expected Copy to fail with permission error")
} }
// Verify the destination file was cleaned up // Destination should not exist since the error occurs before it is created
if _, err := os.Stat(dest); !os.IsNotExist(err) { if _, err := os.Stat(dest); !os.IsNotExist(err) {
t.Error("Destination file should have been removed after copy failure") t.Error("destination file should not exist after open failure")
} }
} }
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
}
func containsHelper(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}

View File

@@ -27,22 +27,3 @@ func TestParseWindowsLineEndings(t *testing.T) {
t.Errorf("failed to parse syntax: want: %s, got: %s", want, fm.Syntax) t.Errorf("failed to parse syntax: want: %s, got: %s", want, fm.Syntax)
} }
} }
// TestParseInvalidYAML tests parsing with invalid YAML in frontmatter
func TestParseInvalidYAML(t *testing.T) {
// stub our cheatsheet content with invalid YAML
markdown := `---
syntax: go
tags: [ test
unclosed bracket
---
To foo the bar: baz`
// parse the frontmatter
_, _, err := parse(markdown)
// assert that an error was returned for invalid YAML
if err == nil {
t.Error("expected error for invalid YAML, got nil")
}
}

View File

@@ -38,7 +38,7 @@ To foo the bar: baz`
t.Errorf("failed to parse tags: want: %s, got: %s", want, fm.Tags[0]) t.Errorf("failed to parse tags: want: %s, got: %s", want, fm.Tags[0])
} }
if len(fm.Tags) != 1 { if len(fm.Tags) != 1 {
t.Errorf("failed to parse tags: want: len 0, got: len %d", len(fm.Tags)) t.Errorf("failed to parse tags: want: len 1, got: len %d", len(fm.Tags))
} }
} }

View File

@@ -122,69 +122,3 @@ func FuzzSearchRegex(f *testing.F) {
} }
}) })
} }
// FuzzSearchCatastrophicBacktracking specifically tests for regex patterns
// that could cause performance issues
func FuzzSearchCatastrophicBacktracking(f *testing.F) {
// Seed with patterns known to potentially cause issues
f.Add("a", 10, 5)
f.Add("x", 20, 3)
f.Fuzz(func(t *testing.T, char string, repeats int, groups int) {
// Limit the size to avoid memory issues in the test
if repeats > 30 || repeats < 0 || groups > 10 || groups < 0 || len(char) > 5 {
t.Skip("Skipping invalid or overly large test case")
}
// Construct patterns that might cause backtracking
patterns := []string{
strings.Repeat(char, repeats),
"(" + char + "+)+",
"(" + char + "*)*",
"(" + char + "|" + char + ")*",
}
// Add nested groups
if groups > 0 && groups < 10 {
nested := char
for i := 0; i < groups; i++ {
nested = "(" + nested + ")+"
}
patterns = append(patterns, nested)
}
// Test text that might trigger backtracking
testText := strings.Repeat(char, repeats) + "x"
for _, pattern := range patterns {
// Try to compile the pattern
reg, err := regexp.Compile(pattern)
if err != nil {
// Invalid pattern, skip
continue
}
// Test with timeout
done := make(chan bool, 1)
go func() {
defer func() {
if r := recover(); r != nil {
t.Errorf("Search panicked with backtracking pattern %q: %v", pattern, r)
}
done <- true
}()
sheet := Sheet{Text: testText}
_ = sheet.Search(reg)
}()
select {
case <-done:
// Completed successfully
case <-time.After(50 * time.Millisecond):
t.Logf("Warning: potential backtracking issue with pattern %q (completed slowly)", pattern)
}
}
})
}

View File

@@ -18,28 +18,26 @@ func TestFilterSingleTag(t *testing.T) {
map[string]sheet.Sheet{ map[string]sheet.Sheet{
"foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}}, "foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}},
"bar": sheet.Sheet{Title: "bar", Tags: []string{"bravo", "charlie"}}, "bar": sheet.Sheet{Title: "bar", Tags: []string{"charlie"}},
}, },
map[string]sheet.Sheet{ map[string]sheet.Sheet{
"baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha", "bravo"}}, "baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha"}},
"bat": sheet.Sheet{Title: "bat", Tags: []string{"bravo", "charlie"}}, "bat": sheet.Sheet{Title: "bat", Tags: []string{"bravo", "charlie"}},
}, },
} }
// filter the cheatsheets // filter the cheatsheets
filtered := Filter(cheatpaths, []string{"bravo"}) filtered := Filter(cheatpaths, []string{"alpha"})
// assert that the expect results were returned // assert that the expect results were returned
want := []map[string]sheet.Sheet{ want := []map[string]sheet.Sheet{
map[string]sheet.Sheet{ map[string]sheet.Sheet{
"foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}}, "foo": sheet.Sheet{Title: "foo", Tags: []string{"alpha", "bravo"}},
"bar": sheet.Sheet{Title: "bar", Tags: []string{"bravo", "charlie"}},
}, },
map[string]sheet.Sheet{ map[string]sheet.Sheet{
"baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha", "bravo"}}, "baz": sheet.Sheet{Title: "baz", Tags: []string{"alpha"}},
"bat": sheet.Sheet{Title: "bat", Tags: []string{"bravo", "charlie"}},
}, },
} }

View File

@@ -127,64 +127,3 @@ func FuzzTags(f *testing.F) {
}() }()
}) })
} }
// FuzzTagsStress tests Tags function with large numbers of tags
func FuzzTagsStress(f *testing.F) {
// Seed: number of unique tags, number of sheets, tags per sheet
f.Add(10, 10, 5)
f.Add(100, 50, 10)
f.Add(1000, 100, 20)
f.Fuzz(func(t *testing.T, numUniqueTags int, numSheets int, tagsPerSheet int) {
// Limit to reasonable values
if numUniqueTags > 1000 || numUniqueTags < 0 ||
numSheets > 1000 || numSheets < 0 ||
tagsPerSheet > 100 || tagsPerSheet < 0 {
t.Skip("Skipping unreasonable test case")
}
// Generate unique tags
uniqueTags := make([]string, numUniqueTags)
for i := 0; i < numUniqueTags; i++ {
uniqueTags[i] = "tag" + string(rune(i))
}
// Create sheets with random tags
cheatpaths := []map[string]sheet.Sheet{
make(map[string]sheet.Sheet),
}
for i := 0; i < numSheets; i++ {
// Select random tags for this sheet
sheetTags := make([]string, 0, tagsPerSheet)
for j := 0; j < tagsPerSheet && j < numUniqueTags; j++ {
// Distribute tags across sheets
tagIndex := (i*tagsPerSheet + j) % numUniqueTags
sheetTags = append(sheetTags, uniqueTags[tagIndex])
}
cheatpaths[0]["sheet"+string(rune(i))] = sheet.Sheet{
Title: "sheet" + string(rune(i)),
Tags: sheetTags,
}
}
// Should handle large numbers efficiently
func() {
defer func() {
if r := recover(); r != nil {
t.Errorf("Tags panicked with %d unique tags, %d sheets, %d tags/sheet: %v",
numUniqueTags, numSheets, tagsPerSheet, r)
}
}()
result := Tags(cheatpaths)
// Should have at most numUniqueTags in result
if len(result) > numUniqueTags {
t.Errorf("More tags in result (%d) than unique tags created (%d)",
len(result), numUniqueTags)
}
}()
})
}