v0.1.4 #15
@@ -0,0 +1,21 @@
|
||||
version: "3"
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build dirmd with injected semantic version (task build VERSION=v0.1.4)
|
||||
deps: [vet]
|
||||
preconditions:
|
||||
- sh: printf '%s' '{{.VERSION}}' | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
msg: "VERSION is required and must be a semantic tag like v0.1.4 — task build VERSION=v0.1.4"
|
||||
cmds:
|
||||
- go build -trimpath -ldflags "-s -w -X git.flo.fo/FLO/dirmd/internal/config.Version={{.VERSION}}" -o bin/dirmd .
|
||||
|
||||
vet:
|
||||
desc: Run go vet
|
||||
cmds:
|
||||
- go vet ./...
|
||||
|
||||
test:
|
||||
desc: Run all tests
|
||||
cmds:
|
||||
- go test ./...
|
||||
+3
-2
@@ -18,6 +18,7 @@ var (
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "dirmd [flags] <directory>",
|
||||
Short: "Generate markdown documentation from directory structure",
|
||||
Version: config.Version,
|
||||
Long: `dirmd walks a directory and generates markdown files with:
|
||||
|
||||
- Directory tree structure
|
||||
@@ -46,7 +47,7 @@ Examples:
|
||||
dirmd -o ./docs ~/repos/myproject --proton-drive
|
||||
|
||||
# Interactive TUI mode
|
||||
dirmd --tui
|
||||
dirmd -i
|
||||
|
||||
# Pick specific files in vertical slices mode
|
||||
dirmd -o ./docs ~/repos/myproject --vertical-slices --pick common/src/lib.rs --pick mssql-extractor/src/processor.rs`,
|
||||
@@ -106,7 +107,7 @@ func run(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
if outputPath == "" {
|
||||
return fmt.Errorf("-o is required (or use --tui for interactive mode)")
|
||||
return fmt.Errorf("-o is required (or use --interactive for interactive mode)")
|
||||
}
|
||||
|
||||
if drivePath != "" && !protonDrive {
|
||||
|
||||
+2
-143
@@ -1,19 +1,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultMaxSize = 512 * 1024
|
||||
DefaultMaxDepth = 20
|
||||
Version = "0.1.4"
|
||||
)
|
||||
|
||||
var Version = "dev"
|
||||
|
||||
var DefaultIgnores = []string{
|
||||
".git", "node_modules", "vendor", "bin", "go.mod", "go.sum",
|
||||
"target", "build", "dist", "Cargo.lock", ".idea", ".vscode",
|
||||
@@ -41,11 +34,6 @@ var FrontendExts = []string{
|
||||
".vue", ".svelte",
|
||||
}
|
||||
|
||||
type stringSlice []string
|
||||
|
||||
func (s *stringSlice) String() string { return strings.Join(*s, ", ") }
|
||||
func (s *stringSlice) Set(v string) error { *s = append(*s, v); return nil }
|
||||
|
||||
type Config struct {
|
||||
InputPath string
|
||||
OutputPath string
|
||||
@@ -67,132 +55,3 @@ type Config struct {
|
||||
IncludeInstructions bool
|
||||
PickPaths []string
|
||||
}
|
||||
|
||||
func PrintUsage() {
|
||||
fmt.Fprintf(os.Stderr, "dirmd v%s\n\n", Version)
|
||||
fmt.Fprintf(os.Stderr, "Usage: dirmd [flags] <directory>\n")
|
||||
fmt.Fprintf(os.Stderr, " dirmd -a -i <file> -o <output>\n\n")
|
||||
fmt.Fprintf(os.Stderr, "Flags:\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func Parse() (*Config, error) {
|
||||
var (
|
||||
input string
|
||||
output string
|
||||
appendMd bool
|
||||
force bool
|
||||
maxSize int64
|
||||
maxDepth int
|
||||
ignores stringSlice
|
||||
exts stringSlice
|
||||
showVer bool
|
||||
protonDrive bool
|
||||
drivePath string
|
||||
verticalSlices bool
|
||||
skipFrontend bool
|
||||
frontendOnly bool
|
||||
omitTest bool
|
||||
omitMd bool
|
||||
includeInstructions bool
|
||||
pickPaths stringSlice
|
||||
)
|
||||
|
||||
flag.StringVar(&input, "i", "", "single file input (append mode only)")
|
||||
flag.StringVar(&output, "o", "", "output markdown file path (required)")
|
||||
flag.BoolVar(&appendMd, "a", false, "append to existing output file")
|
||||
flag.BoolVar(&force, "f", false, "overwrite existing output file (non-append mode)")
|
||||
flag.Int64Var(&maxSize, "max-size", DefaultMaxSize, "max file size in bytes")
|
||||
flag.IntVar(&maxDepth, "max-depth", DefaultMaxDepth, "max directory recursion depth")
|
||||
flag.Var(&ignores, "ignore", "additional ignore patterns (glob, repeatable)")
|
||||
flag.Var(&exts, "extensions", "additional file extensions to skip (repeatable)")
|
||||
flag.BoolVar(&showVer, "version", false, "print version and exit")
|
||||
flag.BoolVar(&protonDrive, "proton-drive", false, "upload output to Proton Drive after writing locally")
|
||||
flag.StringVar(&drivePath, "drive-path", "", "full remote path on Proton Drive (requires --proton-drive")
|
||||
flag.BoolVar(&verticalSlices, "vertical-slices", false, "split output into root file + one markdown per top-level directory")
|
||||
flag.BoolVar(&skipFrontend, "skip-frontend", false, "omit frontend file types")
|
||||
flag.BoolVar(&frontendOnly, "frontend-only", false, "include only frontend file types and README.md")
|
||||
flag.BoolVar(&omitTest, "omit-test", false, "exclude test files and strip #[cfg(test)] blocks")
|
||||
flag.BoolVar(&omitMd, "omit-md", false, "omit all .md files except README.md")
|
||||
flag.BoolVar(&includeInstructions, "instructions", false, "include .dirmd instructions file if present in root")
|
||||
flag.Var(&pickPaths, "pick", "repeatable: relative path to file to include as pick (requires --vertical-slices)")
|
||||
|
||||
flag.Usage = PrintUsage
|
||||
flag.Parse()
|
||||
|
||||
if showVer {
|
||||
fmt.Printf("dirmd v%s\n", Version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if output == "" {
|
||||
return nil, fmt.Errorf("-o is required")
|
||||
}
|
||||
if drivePath != "" && !protonDrive {
|
||||
return nil, fmt.Errorf("--drive-path requires --proton-drive")
|
||||
}
|
||||
if verticalSlices && appendMd {
|
||||
return nil, fmt.Errorf("--vertical-slices cannot be used with --append")
|
||||
}
|
||||
if verticalSlices && input != "" {
|
||||
return nil, fmt.Errorf("--vertical-slices cannot be used with --input-file")
|
||||
}
|
||||
if skipFrontend && frontendOnly {
|
||||
return nil, fmt.Errorf("--skip-frontend and --frontend-only are mutually exclusive")
|
||||
}
|
||||
if len(pickPaths) > 0 && !verticalSlices {
|
||||
return nil, fmt.Errorf("--pick requires --vertical-slices")
|
||||
}
|
||||
|
||||
excludedExts := append(DefaultExts, exts...)
|
||||
if skipFrontend {
|
||||
excludedExts = append(excludedExts, FrontendExts...)
|
||||
}
|
||||
|
||||
cfg := &Config{
|
||||
OutputPath: output,
|
||||
AppendMode: appendMd,
|
||||
Force: force,
|
||||
MaxSize: maxSize,
|
||||
MaxDepth: maxDepth,
|
||||
Ignores: append(DefaultIgnores, ignores...),
|
||||
Extensions: excludedExts,
|
||||
ProtonDrive: protonDrive,
|
||||
DrivePath: drivePath,
|
||||
VerticalSlices: verticalSlices,
|
||||
SkipFrontend: skipFrontend,
|
||||
FrontendOnly: frontendOnly,
|
||||
OmitTest: omitTest,
|
||||
OmitMd: omitMd,
|
||||
IncludeInstructions: includeInstructions,
|
||||
PickPaths: pickPaths,
|
||||
}
|
||||
|
||||
if input != "" {
|
||||
cfg.SingleFile = true
|
||||
cfg.InputPath = input
|
||||
if !appendMd {
|
||||
return nil, fmt.Errorf("single file input (-i) requires append mode (-a)")
|
||||
}
|
||||
} else {
|
||||
args := flag.Args()
|
||||
if len(args) < 1 {
|
||||
return nil, fmt.Errorf("directory argument is required when -i is not used")
|
||||
}
|
||||
cfg.InputPath = args[0]
|
||||
}
|
||||
|
||||
abs, err := filepath.Abs(cfg.InputPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve absolute path: %w", err)
|
||||
}
|
||||
cfg.AbsRoot = abs
|
||||
|
||||
if !appendMd {
|
||||
if _, err := os.Stat(output); err == nil && !force {
|
||||
return nil, fmt.Errorf("output file %s already exists (use -f to overwrite or -a to append)", output)
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
if Version == "" {
|
||||
t.Error("Version is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultIgnores(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"git", ".git"},
|
||||
{"node_modules", "node_modules"},
|
||||
{"vendor", "vendor"},
|
||||
{"target", "target"},
|
||||
{"pycache", "__pycache__"},
|
||||
{"lock_file", "uv.lock"},
|
||||
{"cargo_lock", "Cargo.lock"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !slices.Contains(DefaultIgnores, tt.want) {
|
||||
t.Errorf("DefaultIgnores does not contain %q", tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultExts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"jpg", ".jpg"},
|
||||
{"png", ".png"},
|
||||
{"mp4", ".mp4"},
|
||||
{"zip", ".zip"},
|
||||
{"exe", ".exe"},
|
||||
{"pdf", ".pdf"},
|
||||
{"pem", ".pem"},
|
||||
{"db", ".db"},
|
||||
{"jar", ".jar"},
|
||||
{"pyc", ".pyc"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !slices.Contains(DefaultExts, tt.want) {
|
||||
t.Errorf("DefaultExts does not contain %q", tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontendExts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want string
|
||||
}{
|
||||
{"html", ".html"},
|
||||
{"css", ".css"},
|
||||
{"js", ".js"},
|
||||
{"ts", ".ts"},
|
||||
{"vue", ".vue"},
|
||||
{"svelte", ".svelte"},
|
||||
{"gohtml", ".gohtml"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !slices.Contains(FrontendExts, tt.want) {
|
||||
t.Errorf("FrontendExts does not contain %q", tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package drive
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestBinaryName_constant(t *testing.T) {
|
||||
if binaryName != "proton-drive" {
|
||||
t.Errorf("binaryName = %q; want \"proton-drive\"", binaryName)
|
||||
}
|
||||
}
|
||||
+21
-10
@@ -46,15 +46,15 @@ func (f *Filter) ShouldSkipFile(path, name string, info os.FileInfo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
if f.OmitMd && isNonReadmeMarkdown(path, name) {
|
||||
if f.OmitMd && IsNonReadmeMarkdown(path, name) {
|
||||
return true
|
||||
}
|
||||
|
||||
if f.FrontendOnly && !isFrontendOrReadme(path, name) {
|
||||
if f.FrontendOnly && !IsFrontendOrReadme(path, name) {
|
||||
return true
|
||||
}
|
||||
|
||||
if f.OmitTest && isTestFile(path, name) {
|
||||
if f.OmitTest && IsTestFile(path, name) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -68,25 +68,37 @@ func (f *Filter) ShouldSkipFile(path, name string, info os.FileInfo) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isNonReadmeMarkdown(path, name string) bool {
|
||||
func IsNonReadmeMarkdown(path, name string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext != ".md" && ext != ".markdown" {
|
||||
return false
|
||||
}
|
||||
return !strings.EqualFold(name, "README.md")
|
||||
return !strings.EqualFold(name, "README.md") && !strings.EqualFold(name, "README.markdown")
|
||||
}
|
||||
|
||||
func isFrontendOrReadme(path, name string) bool {
|
||||
func IsFrontendOrReadme(path, name string) bool {
|
||||
if IsFrontendExt(path) {
|
||||
return true
|
||||
}
|
||||
return strings.EqualFold(name, "README.md")
|
||||
}
|
||||
|
||||
func IsFrontendExt(path string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
for _, fe := range config.FrontendExts {
|
||||
if strings.EqualFold(ext, fe) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return strings.EqualFold(name, "README.md")
|
||||
return false
|
||||
}
|
||||
|
||||
func isTestFile(path, name string) bool {
|
||||
func IsTestDir(path string) bool {
|
||||
slashPath := filepath.ToSlash(path)
|
||||
return strings.Contains(slashPath, "/tests/") || strings.HasPrefix(slashPath, "tests/")
|
||||
}
|
||||
|
||||
func IsTestFile(path, name string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
|
||||
switch ext {
|
||||
@@ -107,8 +119,7 @@ func isTestFile(path, name string) bool {
|
||||
if strings.EqualFold(name, "conftest.py") {
|
||||
return true
|
||||
}
|
||||
slashPath := filepath.ToSlash(path)
|
||||
return strings.Contains(slashPath, "/tests/")
|
||||
return IsTestDir(path)
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.flo.fo/FLO/dirmd/internal/config"
|
||||
)
|
||||
|
||||
func TestIsNonReadmeMarkdown(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{"readme_md_false", "README.md", false},
|
||||
{"readme_markdown_false", "README.markdown", false},
|
||||
{"readme_lower_false", "readme.md", false},
|
||||
{"other_md_true", "CHANGELOG.md", true},
|
||||
{"other_markdown_true", "LICENSE.markdown", true},
|
||||
{"txt_false", "README.txt", false},
|
||||
{"go_false", "main.go", false},
|
||||
{"empty_false", "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsNonReadmeMarkdown(tt.path, filepath.Base(tt.path)); got != tt.want {
|
||||
t.Errorf("IsNonReadmeMarkdown(%q) = %v; want %v", tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsFrontendExt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{"html_true", "page.html", true},
|
||||
{"htm_true", "page.htm", true},
|
||||
{"css_true", "style.css", true},
|
||||
{"js_true", "app.js", true},
|
||||
{"ts_true", "app.ts", true},
|
||||
{"vue_true", "component.vue", true},
|
||||
{"svelte_true", "app.svelte", true},
|
||||
{"gohtml_true", "template.gohtml", true},
|
||||
{"go_false", "main.go", false},
|
||||
{"md_false", "README.md", false},
|
||||
{"json_false", "config.json", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsFrontendExt(tt.path); got != tt.want {
|
||||
t.Errorf("IsFrontendExt(%q) = %v; want %v", tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsFrontendOrReadme(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{"html_true", "page.html", true},
|
||||
{"readme_true", "README.md", true},
|
||||
{"readme_lower_true", "readme.md", true},
|
||||
{"css_true", "style.css", true},
|
||||
{"go_false", "main.go", false},
|
||||
{"changelog_false", "CHANGELOG.md", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsFrontendOrReadme(tt.path, filepath.Base(tt.path)); got != tt.want {
|
||||
t.Errorf("IsFrontendOrReadme(%q) = %v; want %v", tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTestDir(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{"tests_top_level_true", "tests/", true},
|
||||
{"tests_nested_true", "pkg/tests/unit/", true},
|
||||
{"test_single_letter_false", "test/", false},
|
||||
{"src_test_false_current_behavior", "src/test/java/com/example/", false},
|
||||
{"normal_false", "src/", false},
|
||||
{"tests_file_false", "testfile.go", false},
|
||||
{"empty_false", "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsTestDir(tt.path); got != tt.want {
|
||||
t.Errorf("IsTestDir(%q) = %v; want %v", tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTestFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{"go_test_true", "path/to/file_test.go", true},
|
||||
{"go_test_uppercase_true", "path/to/file_TEST.go", true},
|
||||
{"go_normal_false", "path/to/main.go", false},
|
||||
{"go_integration_test_true", "path/to/integration_test.go", true},
|
||||
{"java_test_suffix_true", "MyClassTest.java", true},
|
||||
{"java_tests_suffix_true", "MyClassTests.java", true},
|
||||
{"java_normal_false", "MyClass.java", false},
|
||||
{"java_src_test_true", "src/test/java/com/example/MyTest.java", true},
|
||||
{"py_test_prefix_true", "test_main.py", true},
|
||||
{"py_test_suffix_true", "main_test.py", true},
|
||||
{"py_conftest_true", "conftest.py", true},
|
||||
{"py_normal_false", "main.py", false},
|
||||
{"py_tests_dir_true", "tests/conftest.py", true},
|
||||
{"rust_cfg_false", "main.rs", false},
|
||||
{"c_false", "main.c", false},
|
||||
{"unknown_false", "README.md", false},
|
||||
{"empty_false", "", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsTestFile(tt.path, filepath.Base(tt.path)); got != tt.want {
|
||||
t.Errorf("IsTestFile(%q, %q) = %v; want %v", tt.path, filepath.Base(tt.path), got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkipFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filter *Filter
|
||||
path string
|
||||
size int64
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "large_file_skipped",
|
||||
filter: &Filter{MaxSize: 1000, Extensions: []string{}, Ignores: []string{}},
|
||||
path: "big.bin",
|
||||
size: 2000,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "small_file_not_skipped",
|
||||
filter: &Filter{MaxSize: 1000, Extensions: []string{}, Ignores: []string{}},
|
||||
path: "tiny.txt",
|
||||
size: 500,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "excluded_extension_skipped",
|
||||
filter: &Filter{MaxSize: 1000000, Extensions: []string{".bin"}, Ignores: []string{}},
|
||||
path: "file.bin",
|
||||
size: 500,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "hidden_file_skipped",
|
||||
filter: &Filter{MaxSize: 1000000, Extensions: []string{}, Ignores: []string{}},
|
||||
path: ".gitignore",
|
||||
size: 500,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "omit_test_go_test",
|
||||
filter: &Filter{MaxSize: 1000000, Extensions: []string{}, Ignores: []string{}, OmitTest: true},
|
||||
path: "main_test.go",
|
||||
size: 500,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "omit_test_normal_go",
|
||||
filter: &Filter{MaxSize: 1000000, Extensions: []string{}, Ignores: []string{}, OmitTest: true},
|
||||
path: "main.go",
|
||||
size: 500,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "omit_md_changelog",
|
||||
filter: &Filter{MaxSize: 1000000, Extensions: []string{}, Ignores: []string{}, OmitMd: true},
|
||||
path: "CHANGELOG.md",
|
||||
size: 500,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "omit_md_readme_not",
|
||||
filter: &Filter{MaxSize: 1000000, Extensions: []string{}, Ignores: []string{}, OmitMd: true},
|
||||
path: "README.md",
|
||||
size: 500,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "omit_md_readme_markdown_not",
|
||||
filter: &Filter{MaxSize: 1000000, Extensions: []string{}, Ignores: []string{}, OmitMd: true},
|
||||
path: "README.markdown",
|
||||
size: 500,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "frontend_only_non_frontend",
|
||||
filter: &Filter{MaxSize: 1000000, Extensions: []string{}, Ignores: []string{}, FrontendOnly: true},
|
||||
path: "main.go",
|
||||
size: 500,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "frontend_only_readme",
|
||||
filter: &Filter{MaxSize: 1000000, Extensions: []string{}, Ignores: []string{}, FrontendOnly: true},
|
||||
path: "README.md",
|
||||
size: 500,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
fi := fakeFileInfo{size: tt.size}
|
||||
if got := tt.filter.ShouldSkipFile(tt.path, filepath.Base(tt.path), fi); got != tt.want {
|
||||
t.Errorf("ShouldSkipFile() = %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkipDir(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filter *Filter
|
||||
dir string
|
||||
want bool
|
||||
}{
|
||||
{"hidden_dir", &Filter{Ignores: []string{}}, ".git", true},
|
||||
{"node_modules_with_defaults", &Filter{Ignores: config.DefaultIgnores}, "node_modules", true},
|
||||
{"node_modules_without_defaults", &Filter{Ignores: []string{}}, "node_modules", false},
|
||||
{"custom_ignore", &Filter{Ignores: []string{"vendor"}}, "vendor", true},
|
||||
{"normal_dir", &Filter{Ignores: config.DefaultIgnores}, "src", false},
|
||||
{"empty_ignores_no_skip", &Filter{Ignores: []string{}}, "src", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.filter.ShouldSkipDir(tt.dir); got != tt.want {
|
||||
t.Errorf("ShouldSkipDir(%q) = %v; want %v", tt.dir, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeFileInfo struct {
|
||||
size int64
|
||||
}
|
||||
|
||||
func (f fakeFileInfo) Name() string { return "" }
|
||||
func (f fakeFileInfo) Size() int64 { return f.size }
|
||||
func (f fakeFileInfo) Mode() os.FileMode { return 0 }
|
||||
func (f fakeFileInfo) ModTime() time.Time { return time.Time{} }
|
||||
func (f fakeFileInfo) IsDir() bool { return false }
|
||||
func (f fakeFileInfo) Sys() any { return nil }
|
||||
@@ -0,0 +1,48 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatCommitLine_nil(t *testing.T) {
|
||||
if got := FormatCommitLine(nil); got != "" {
|
||||
t.Errorf("FormatCommitLine(nil) = %q; want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatCommitLine_valid(t *testing.T) {
|
||||
commit := &CommitInfo{
|
||||
ShortSHA: "abc1234",
|
||||
Message: "Initial commit",
|
||||
}
|
||||
expected := "> Commit: abc1234 (Initial commit)\n"
|
||||
if got := FormatCommitLine(commit); got != expected {
|
||||
t.Errorf("FormatCommitLine() = %q; want %q", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCommitInfo_no_git(t *testing.T) {
|
||||
cmd := exec.Command("git", "-C", "/nonexistent", "log", "-1", "--format=%h %s")
|
||||
output, err := cmd.Output()
|
||||
if err == nil {
|
||||
t.Fatalf("expected error from nonexistent path, got %s", string(output))
|
||||
}
|
||||
|
||||
result := GetCommitInfo("/nonexistent")
|
||||
if result != nil {
|
||||
t.Errorf("GetCommitInfo(/nonexistent) = %v; want nil", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCommitInfo_no_commits(t *testing.T) {
|
||||
out, err := exec.Command("sh", "-c", "echo 'abc123'").Output()
|
||||
if err != nil {
|
||||
t.Skip("shell command failed")
|
||||
}
|
||||
parts := strings.SplitN(strings.TrimSpace(string(out)), " ", 2)
|
||||
if len(parts) < 2 {
|
||||
t.Skip("cannot verify single-part output")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package instructions
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormat_empty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"empty_string", "", ""},
|
||||
{"whitespace_only", " ", ""},
|
||||
{"newline_only", "\n\n", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Format(tt.input); got != tt.want {
|
||||
t.Errorf("Format(%q) = %q; want %q", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat_valid(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{"single_line", "hello world"},
|
||||
{"multi_line", "line 1\nline 2\nline 3"},
|
||||
{"trailing_newlines", "content\n\n\n"},
|
||||
{"leading_whitespace", "\n\ncontent"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := Format(tt.input)
|
||||
if !strings.HasPrefix(got, "> Instructions:\n") {
|
||||
t.Errorf("missing instructions header")
|
||||
}
|
||||
if !strings.HasSuffix(got, "\n") {
|
||||
t.Errorf("does not end with newline")
|
||||
}
|
||||
lines := strings.Split(got, "\n")
|
||||
for _, line := range lines {
|
||||
if line != "" && !strings.HasPrefix(line, ">") {
|
||||
t.Errorf("line does not start with '>': %q", line)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileName_constant(t *testing.T) {
|
||||
if FileName != ".dirmd" {
|
||||
t.Errorf("FileName = %q; want \".dirmd\"", FileName)
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func Write(result *walker.Result, outputPath string, appendMode bool, singleFile
|
||||
language := extToLang(filepath.Ext(entry.RelPath))
|
||||
lines := countLines(content)
|
||||
|
||||
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, entry.Size, lines)
|
||||
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, len(content), lines)
|
||||
fmt.Fprintf(&sb, "%s", fenceChars)
|
||||
if language != "" {
|
||||
fmt.Fprintf(&sb, "%s", language)
|
||||
@@ -161,7 +161,7 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
|
||||
generated = append(generated, pickInfoPaths(pickInfos)...)
|
||||
}
|
||||
|
||||
if err := writeRootIndex(rootPath, absRoot, rootName, repoName, topLevelDirs, result.Commit, result.Instructions, treeRoot, result.Cfg, pickInfos); err != nil {
|
||||
if err := writeRootIndex(rootPath, absRoot, rootName, repoName, topLevelDirs, rootFiles, result.Commit, result.Instructions, treeRoot, result.Cfg, pickInfos); err != nil {
|
||||
return nil, fmt.Errorf("failed to write root index: %w", err)
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ func WriteSlices(result *walker.Result, outputDir, repoName string) ([]string, e
|
||||
}
|
||||
|
||||
for _, dirName := range dirNames {
|
||||
sliceName := strings.TrimSuffix(dirName, "/") + "_" + repoName + ".md"
|
||||
sliceName := dirName + "_" + repoName + ".md"
|
||||
sliceDir := filepath.Join(outputDir, dirName)
|
||||
if err := os.MkdirAll(sliceDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("cannot create slice directory %s: %w", sliceDir, err)
|
||||
@@ -283,7 +283,7 @@ func writePickFile(path, absRoot, relPath string, omitTest bool, commit *git.Com
|
||||
|
||||
fmt.Fprintf(&sb, "Tree: %s\n\n", tree.RenderBreadcrumb(absRoot, relPath))
|
||||
|
||||
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", relPath, infoSize(fullPath), lines)
|
||||
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", relPath, len(content), lines)
|
||||
sb.WriteString(fenceChars)
|
||||
if language != "" {
|
||||
sb.WriteString(language)
|
||||
@@ -299,15 +299,7 @@ func writePickFile(path, absRoot, relPath string, omitTest bool, commit *git.Com
|
||||
return err
|
||||
}
|
||||
|
||||
func infoSize(path string) int64 {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return info.Size()
|
||||
}
|
||||
|
||||
func writeRootIndex(path, absRoot, rootName, repoName string, topLevelDirs map[string][]walker.Entry, commit *git.CommitInfo, instContent string, treeRoot *tree.Node, cfg *config.Config, picks []PickInfo) error {
|
||||
func writeRootIndex(path, absRoot, rootName, repoName string, topLevelDirs map[string][]walker.Entry, rootFiles []walker.Entry, commit *git.CommitInfo, instContent string, treeRoot *tree.Node, cfg *config.Config, picks []PickInfo) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create root index: %w", err)
|
||||
@@ -337,8 +329,11 @@ func writeRootIndex(path, absRoot, rootName, repoName string, topLevelDirs map[s
|
||||
sb.WriteString("|------------|-----------|-------|-------------|\n")
|
||||
|
||||
rootSliceFile := "root_" + repoName + ".md"
|
||||
rootFileCount := len(topLevelDirs)
|
||||
rootFileCount := len(rootFiles)
|
||||
rootTotalBytes := int64(0)
|
||||
for _, e := range rootFiles {
|
||||
rootTotalBytes += e.Size
|
||||
}
|
||||
fmt.Fprintf(&sb, "| %s | (root) | %d | %d |\n", rootSliceFile, rootFileCount, rootTotalBytes)
|
||||
|
||||
for _, dirName := range sortedKeys(topLevelDirs) {
|
||||
@@ -348,7 +343,7 @@ func writeRootIndex(path, absRoot, rootName, repoName string, topLevelDirs map[s
|
||||
for _, e := range entries {
|
||||
totalBytes += e.Size
|
||||
}
|
||||
sliceFile := filepath.Join(dirName, strings.TrimSuffix(dirName, "/")+"_"+repoName+".md")
|
||||
sliceFile := filepath.Join(dirName, dirName+"_"+repoName+".md")
|
||||
fmt.Fprintf(&sb, "| %s | %s/ | %d | %d |\n", sliceFile, dirName, fileCount, totalBytes)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
@@ -398,7 +393,7 @@ func writeRootSliceFile(path, absRoot string, rootFiles []walker.Entry, omitTest
|
||||
}
|
||||
language := extToLang(filepath.Ext(entry.RelPath))
|
||||
lines := countLines(content)
|
||||
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, entry.Size, lines)
|
||||
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, len(content), lines)
|
||||
fmt.Fprintf(&sb, "%s", fenceChars)
|
||||
if language != "" {
|
||||
fmt.Fprintf(&sb, "%s", language)
|
||||
@@ -442,7 +437,7 @@ func writeDirSlice(path, absDir, absRoot, dirName string, fileEntries []walker.E
|
||||
}
|
||||
language := extToLang(filepath.Ext(entry.RelPath))
|
||||
lines := countLines(content)
|
||||
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, entry.Size, lines)
|
||||
fmt.Fprintf(&sb, "--- FILE: ./%s (%d bytes, %d lines) ---\n", entry.RelPath, len(content), lines)
|
||||
fmt.Fprintf(&sb, "%s", fenceChars)
|
||||
if language != "" {
|
||||
fmt.Fprintf(&sb, "%s", language)
|
||||
@@ -582,20 +577,13 @@ func stripTestBlocks(content string) string {
|
||||
|
||||
for i < len(lines) {
|
||||
line := lines[i]
|
||||
|
||||
if !foundBrace {
|
||||
if strings.Contains(line, "{") {
|
||||
braceDepth += strings.Count(line, "{") - strings.Count(line, "}")
|
||||
if strings.Contains(line, "{") {
|
||||
foundBrace = true
|
||||
}
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
braceDepth += strings.Count(line, "{") - strings.Count(line, "}")
|
||||
i++
|
||||
|
||||
if braceDepth <= 0 {
|
||||
if foundBrace && braceDepth <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -639,6 +627,7 @@ func extToLang(ext string) string {
|
||||
".cpp": "cpp",
|
||||
".hpp": "cpp",
|
||||
".txt": "text",
|
||||
".toml": "toml",
|
||||
}
|
||||
|
||||
if lang, ok := mapping[strings.ToLower(ext)]; ok {
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package renderer
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.flo.fo/FLO/dirmd/internal/config"
|
||||
"git.flo.fo/FLO/dirmd/internal/tree"
|
||||
)
|
||||
|
||||
func TestCountLines_empty(t *testing.T) {
|
||||
if got := countLines(""); got != 0 {
|
||||
t.Errorf("countLines(\"\") = %d; want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCountLines_basic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want int
|
||||
}{
|
||||
{"one_line_no_newline", "hello", 1},
|
||||
{"one_line_with_newline", "hello\n", 1},
|
||||
{"two_lines", "hello\nworld", 2},
|
||||
{"two_lines_trailing", "hello\nworld\n", 2},
|
||||
{"three_lines", "a\nb\nc", 3},
|
||||
{"empty_lines", "a\n\nb", 3},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := countLines(tt.input); got != tt.want {
|
||||
t.Errorf("countLines(%q) = %d; want %d", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripTestBlocks_rust(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{"no_test_block", "fn main() {}\nfn helper() {}", "fn main() {}\nfn helper() {}"},
|
||||
{"cfg_test_block", "#[cfg(test)]\nmod tests {\n #[test]\n fn test_one() {}\n}", ""},
|
||||
{"mixed_content", "pub fn main() {}\n#[cfg(test)]\nmod tests {\n #[test]\n fn t() {}\n}\npub fn end() {}", "pub fn main() {}\npub fn end() {}"},
|
||||
{"nested_braces", "#[cfg(test)]\nmod tests {\n mod inner {\n #[test]\n fn t() {}\n }\n}", ""},
|
||||
{"multiple_blocks", "#[cfg(test)]\nmod t1 {}\n#[cfg(test)]\nmod t2 {}", ""},
|
||||
{"cfg_all_test", "#[cfg(all(test, unix))]\nmod tests {}", ""},
|
||||
{"cfg_any_test", "#[cfg(any(test, feature = \"foo\"))]\nmod tests {}", ""},
|
||||
{"empty_after_strip", "#[cfg(test)]\nmod tests {}", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := stripTestBlocks(tt.input); got != tt.want {
|
||||
t.Errorf("stripTestBlocks(%q)\ngot:\n%s\nwant:\n%s", tt.input, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtToLang(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ext string
|
||||
want string
|
||||
}{
|
||||
{"go", ".go", "go"},
|
||||
{"markdown", ".md", "markdown"},
|
||||
{"yaml_upper", ".YAML", "yaml"},
|
||||
{"yaml_yml", ".yml", "yaml"},
|
||||
{"javascript", ".js", "javascript"},
|
||||
{"typescript", ".ts", "typescript"},
|
||||
{"css", ".css", "css"},
|
||||
{"html", ".html", "html"},
|
||||
{"json", ".json", "json"},
|
||||
{"xml", ".xml", "xml"},
|
||||
{"sql", ".sql", "sql"},
|
||||
{"bash", ".sh", "bash"},
|
||||
{"python", ".py", "python"},
|
||||
{"rust", ".rs", "rust"},
|
||||
{"c", ".c", "c"},
|
||||
{"cpp", ".cpp", "cpp"},
|
||||
{"txt", ".txt", "text"},
|
||||
{"toml", ".toml", "toml"},
|
||||
{"unknown", ".xyz", ""},
|
||||
{"empty", "", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := extToLang(tt.ext); got != tt.want {
|
||||
t.Errorf("extToLang(%q) = %q; want %q", tt.ext, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasAnyAnnotation_none(t *testing.T) {
|
||||
root := &tree.Node{Name: "root", IsDir: true, Children: map[string]*tree.Node{
|
||||
"file.go": {Name: "file.go", IsDir: false},
|
||||
}}
|
||||
if hasAnyAnnotation(root) {
|
||||
t.Error("hasAnyAnnotation should be false for clean tree")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasAnyAnnotation_present(t *testing.T) {
|
||||
root := &tree.Node{Name: "root", IsDir: true, Children: map[string]*tree.Node{
|
||||
".git": {Name: ".git", IsDir: true, Annotation: "ignored (default)"},
|
||||
}}
|
||||
if !hasAnyAnnotation(root) {
|
||||
t.Error("hasAnyAnnotation should be true with annotation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasAnyAnnotation_deep(t *testing.T) {
|
||||
root := &tree.Node{Name: "root", IsDir: true, Children: map[string]*tree.Node{
|
||||
"internal": {Name: "internal", IsDir: true, Children: map[string]*tree.Node{
|
||||
"deep": {Name: "deep", IsDir: true, Annotation: "tests omitted (--omit-test)"},
|
||||
}},
|
||||
}}
|
||||
if !hasAnyAnnotation(root) {
|
||||
t.Error("hasAnyAnnotation should be true for deep annotation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatFlags_nil(t *testing.T) {
|
||||
if FormatFlags(nil) != "" {
|
||||
t.Error("FormatFlags(nil) should be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatFlags_empty(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
MaxDepth: config.DefaultMaxDepth,
|
||||
MaxSize: config.DefaultMaxSize,
|
||||
}
|
||||
if out := FormatFlags(cfg); out != "" {
|
||||
t.Errorf("FormatFlags(default config) = %q; want empty", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatFlags_set(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
OmitTest: true,
|
||||
OmitMd: true,
|
||||
MaxDepth: 10,
|
||||
}
|
||||
out := FormatFlags(cfg)
|
||||
if !strings.Contains(out, "--omit-test") {
|
||||
t.Error("missing --omit-test")
|
||||
}
|
||||
if !strings.Contains(out, "--omit-md") {
|
||||
t.Error("missing --omit-md")
|
||||
}
|
||||
if !strings.Contains(out, "--max-depth 10") {
|
||||
t.Error("missing --max-depth 10")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatFlags_extra_ignores(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Ignores: append(config.DefaultIgnores, "mycustomdir"),
|
||||
}
|
||||
out := FormatFlags(cfg)
|
||||
if !strings.Contains(out, "mycustomdir") {
|
||||
t.Error("missing custom ignore in output")
|
||||
}
|
||||
if strings.Contains(out, "node_modules") {
|
||||
t.Error("default ignore leaked into Ignored line")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMinus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
haystack []string
|
||||
defaults []string
|
||||
want []string
|
||||
}{
|
||||
{"empty", nil, []string{"a"}, nil},
|
||||
{"all_default", []string{"a", "b"}, []string{"a", "b"}, nil},
|
||||
{"one_extra", []string{"a", "c"}, []string{"a"}, []string{"c"}},
|
||||
{"duplicates_deduped", []string{"c", "c", "a"}, []string{"a"}, []string{"c"}},
|
||||
{"no_defaults", []string{"x"}, nil, []string{"x"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := setMinus(tt.haystack, tt.defaults)
|
||||
if len(got) != len(tt.want) {
|
||||
t.Errorf("setMinus() length = %d; want %d", len(got), len(tt.want))
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.want[i] {
|
||||
t.Errorf("setMinus()[%d] = %q; want %q", i, got[i], tt.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package tree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddPath_single(t *testing.T) {
|
||||
root := &Node{IsDir: true, Children: make(map[string]*Node)}
|
||||
addPath(root, "README.md", false)
|
||||
node, ok := root.Children["README.md"]
|
||||
if !ok {
|
||||
t.Fatal("missing README.md child")
|
||||
}
|
||||
if node.IsDir {
|
||||
t.Error("README.md should not be a directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddPath_nested(t *testing.T) {
|
||||
root := &Node{IsDir: true, Children: make(map[string]*Node)}
|
||||
addPath(root, "cmd/main.go", false)
|
||||
cmdNode, ok := root.Children["cmd"]
|
||||
if !ok {
|
||||
t.Fatal("missing cmd child")
|
||||
}
|
||||
if !cmdNode.IsDir {
|
||||
t.Error("cmd should be a directory")
|
||||
}
|
||||
if _, ok := cmdNode.Children["main.go"]; !ok {
|
||||
t.Fatal("missing main.go child under cmd")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddPath_overwrite_annotation(t *testing.T) {
|
||||
root := &Node{IsDir: true, Children: make(map[string]*Node)}
|
||||
node := addPath(root, "cmd", true)
|
||||
node.Annotation = "ignored (default)"
|
||||
|
||||
node2 := addPath(root, "cmd", true)
|
||||
if node2 != node {
|
||||
t.Error("addPath should return the same node for repeated path")
|
||||
}
|
||||
if node2.Annotation != "ignored (default)" {
|
||||
t.Errorf("annotation lost: %q", node2.Annotation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expect []string
|
||||
}{
|
||||
{"root_file", "README.md", []string{"README.md"}},
|
||||
{"two_parts", "cmd/main.go", []string{"cmd", "main.go"}},
|
||||
{"three_parts", "internal/tree/node.go", []string{"internal", "tree", "node.go"}},
|
||||
{"empty", "", nil},
|
||||
{"only_slashes", "///", nil},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := splitPath(tt.input)
|
||||
if len(got) != len(tt.expect) {
|
||||
t.Errorf("splitPath(%q) = %v; want %v", tt.input, got, tt.expect)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tt.expect[i] {
|
||||
t.Errorf("splitPath(%q)[%d] = %q; want %q", tt.input, i, got[i], tt.expect[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBreadcrumb_root(t *testing.T) {
|
||||
breadcrumb := RenderBreadcrumb("/home/user/project", "README.md")
|
||||
expected := "project/README.md"
|
||||
if breadcrumb != expected {
|
||||
t.Errorf("RenderBreadcrumb(...) = %q; want %q", breadcrumb, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderBreadcrumb_nested(t *testing.T) {
|
||||
breadcrumb := RenderBreadcrumb("/home/user/project", "internal/tree/tree.go")
|
||||
expected := "project / internal / tree / tree.go"
|
||||
if breadcrumb != expected {
|
||||
t.Errorf("RenderBreadcrumb(...) = %q; want %q", breadcrumb, expected)
|
||||
}
|
||||
}
|
||||
+21
-80
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.flo.fo/FLO/dirmd/internal/config"
|
||||
@@ -40,12 +41,17 @@ func BuildFullTree(absRoot string, cfg *config.Config) (*Node, error) {
|
||||
name := filepath.Base(path)
|
||||
|
||||
if d.IsDir() {
|
||||
if isIgnoredName(name, cfg.Ignores) {
|
||||
if strings.HasPrefix(name, ".") {
|
||||
node := addPath(root, rel, true)
|
||||
node.Annotation = "ignored (--ignore)"
|
||||
node.Annotation = "ignored (default)"
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if cfg.OmitTest && isTestDir(rel) {
|
||||
if annotation, ok := ignoreReason(name, cfg.Ignores); ok {
|
||||
node := addPath(root, rel, true)
|
||||
node.Annotation = annotation
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if cfg.OmitTest && filter.IsTestDir(rel) {
|
||||
node := addPath(root, rel, true)
|
||||
node.Annotation = "tests omitted (--omit-test)"
|
||||
return filepath.SkipDir
|
||||
@@ -70,105 +76,40 @@ func BuildFullTree(absRoot string, cfg *config.Config) (*Node, error) {
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func isIgnoredName(name string, ignores []string) bool {
|
||||
if strings.HasPrefix(name, ".") {
|
||||
return true
|
||||
}
|
||||
func ignoreReason(name string, ignores []string) (string, bool) {
|
||||
for _, pattern := range ignores {
|
||||
if matched, _ := filepath.Match(pattern, name); matched {
|
||||
return true
|
||||
if slices.Contains(config.DefaultIgnores, pattern) {
|
||||
return "ignored (default)", true
|
||||
}
|
||||
return "ignored (--ignore)", true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isTestDir(rel string) bool {
|
||||
slashPath := filepath.ToSlash(rel)
|
||||
return strings.Contains(slashPath, "/tests/") || strings.HasPrefix(slashPath, "tests/")
|
||||
return "", false
|
||||
}
|
||||
|
||||
func fileAnnotation(rel, name string, cfg *config.Config) (string, bool) {
|
||||
if strings.HasPrefix(name, ".") {
|
||||
return "", false
|
||||
}
|
||||
for _, pattern := range cfg.Ignores {
|
||||
if matched, _ := filepath.Match(pattern, name); matched {
|
||||
return "ignored (--ignore)", true
|
||||
if annotation, ok := ignoreReason(name, cfg.Ignores); ok {
|
||||
return annotation, true
|
||||
}
|
||||
}
|
||||
if cfg.OmitTest && isTestFileName(rel, name) {
|
||||
if cfg.OmitTest && filter.IsTestFile(rel, name) {
|
||||
return "tests omitted (--omit-test)", true
|
||||
}
|
||||
if cfg.OmitMd && isNonReadmeMarkdown(name) {
|
||||
if cfg.OmitMd && filter.IsNonReadmeMarkdown(rel, name) {
|
||||
return "omitted (--omit-md)", true
|
||||
}
|
||||
if cfg.FrontendOnly && !isFrontendOrReadme(rel, name) {
|
||||
if cfg.FrontendOnly && !filter.IsFrontendOrReadme(rel, name) {
|
||||
return "omitted (--frontend-only)", true
|
||||
}
|
||||
if cfg.SkipFrontend && isFrontendExt(rel) {
|
||||
if cfg.SkipFrontend && filter.IsFrontendExt(rel) {
|
||||
return "omitted (--skip-frontend)", true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isTestFileName(rel, name string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
switch ext {
|
||||
case ".go":
|
||||
return strings.HasSuffix(strings.ToLower(name), "_test.go")
|
||||
case ".java":
|
||||
base := strings.TrimSuffix(name, filepath.Ext(name))
|
||||
if strings.HasSuffix(base, "Test") || strings.HasSuffix(base, "Tests") {
|
||||
return true
|
||||
}
|
||||
slashPath := filepath.ToSlash(rel)
|
||||
return strings.Contains(slashPath, "/src/test/")
|
||||
case ".py":
|
||||
lower := strings.ToLower(name)
|
||||
if strings.HasPrefix(lower, "test_") || strings.HasSuffix(lower, "_test.py") {
|
||||
return true
|
||||
}
|
||||
if strings.EqualFold(name, "conftest.py") {
|
||||
return true
|
||||
}
|
||||
return isTestDir(rel)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isNonReadmeMarkdown(name string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
if ext != ".md" && ext != ".markdown" {
|
||||
return false
|
||||
}
|
||||
return !strings.EqualFold(name, "README.md")
|
||||
}
|
||||
|
||||
func isFrontendOrReadme(rel, name string) bool {
|
||||
if isFrontendExt(rel) {
|
||||
return true
|
||||
}
|
||||
return strings.EqualFold(name, "README.md")
|
||||
}
|
||||
|
||||
func isFrontendExt(rel string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(rel))
|
||||
for _, fe := range frontendExts {
|
||||
if strings.EqualFold(ext, fe) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var frontendExts = []string{
|
||||
".html", ".htm", ".gohtml", ".tmpl",
|
||||
".css", ".scss", ".sass", ".less",
|
||||
".js", ".jsx", ".mjs", ".cjs",
|
||||
".ts", ".tsx",
|
||||
".vue", ".svelte",
|
||||
}
|
||||
|
||||
func RenderBreadcrumb(absRoot, relPath string) string {
|
||||
var sb strings.Builder
|
||||
rootName := filepath.Base(absRoot)
|
||||
|
||||
+2
-2
@@ -56,8 +56,8 @@ func Run() error {
|
||||
|
||||
km.MultiSelect.Next = key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "next field"))
|
||||
km.MultiSelect.Prev = key.NewBinding(key.WithKeys("shift+tab"), key.WithHelp("shift+tab", "prev field"))
|
||||
km.MultiSelect.Up = key.NewBinding(key.WithKeys("up", "k"), key.WithHelp("↑/k", "up"))
|
||||
km.MultiSelect.Down = key.NewBinding(key.WithKeys("down", "j"), key.WithHelp("↓/j", "down"))
|
||||
km.MultiSelect.Up = key.NewBinding(key.WithKeys("up"), key.WithHelp("↑", "up"))
|
||||
km.MultiSelect.Down = key.NewBinding(key.WithKeys("down"), key.WithHelp("↓", "down"))
|
||||
km.MultiSelect.Toggle = key.NewBinding(key.WithKeys("enter", " "), key.WithHelp("enter/space", "toggle"))
|
||||
km.MultiSelect.Submit = key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "next"))
|
||||
km.MultiSelect.Filter = key.NewBinding(key.WithKeys("/"), key.WithHelp("/", "filter"))
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContainsOption_present(t *testing.T) {
|
||||
opts := []string{"force", "vertical-slices", "omit-test"}
|
||||
if !containsOption(opts, "vertical-slices") {
|
||||
t.Error("containsOption failed to find present option")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainsOption_absent(t *testing.T) {
|
||||
opts := []string{"force", "vertical-slices"}
|
||||
if containsOption(opts, "omit-test") {
|
||||
t.Error("containsOption found absent option")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultOutputDir_stable(t *testing.T) {
|
||||
out1 := defaultOutputDir()
|
||||
out2 := defaultOutputDir()
|
||||
if out1 != out2 {
|
||||
t.Error("defaultOutputDir should be deterministic for same cwd")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
package walker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"git.flo.fo/FLO/dirmd/internal/config"
|
||||
)
|
||||
|
||||
func writeFile(t *testing.T, dir, name, content string) {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
t.Fatalf("mkdir for %s: %v", name, err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("write %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func baseCfg(absRoot string) *config.Config {
|
||||
return &config.Config{
|
||||
AbsRoot: absRoot,
|
||||
InputPath: absRoot,
|
||||
MaxSize: 1024 * 1024,
|
||||
MaxDepth: 20,
|
||||
Ignores: config.DefaultIgnores,
|
||||
Extensions: config.DefaultExts,
|
||||
}
|
||||
}
|
||||
|
||||
func relPaths(entries []Entry) []string {
|
||||
paths := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
paths = append(paths, e.RelPath)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func containsPath(paths []string, target string) bool {
|
||||
for _, p := range paths {
|
||||
if p == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestWalkDirectory_happy(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "main.go", "package main\n")
|
||||
writeFile(t, root, "cmd/tool/main.go", "package main\n")
|
||||
writeFile(t, root, "cmd/tool/util.go", "package tool\n")
|
||||
writeFile(t, root, "internal/x/y/deep.go", "package y\n")
|
||||
|
||||
result, err := Run(baseCfg(root))
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
|
||||
got := relPaths(result.Entries)
|
||||
want := []string{"main.go", "cmd/tool/main.go", "cmd/tool/util.go", "internal/x/y/deep.go"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("entries = %v; want %v", got, want)
|
||||
}
|
||||
for _, w := range want {
|
||||
if !containsPath(got, w) {
|
||||
t.Errorf("missing entry %q in %v", w, got)
|
||||
}
|
||||
}
|
||||
if result.AbsRoot != root {
|
||||
t.Errorf("AbsRoot = %q; want %q", result.AbsRoot, root)
|
||||
}
|
||||
if result.Cfg == nil {
|
||||
t.Error("Cfg not attached to result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDirectory_respects_filters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(t *testing.T) string
|
||||
must []string
|
||||
musts []string
|
||||
}{
|
||||
{
|
||||
name: "default_ignores_skip_dirs_and_files",
|
||||
setup: func(t *testing.T) string {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "main.go", "x\n")
|
||||
writeFile(t, root, ".git/config", "x\n")
|
||||
writeFile(t, root, "node_modules/pkg/index.js", "x\n")
|
||||
writeFile(t, root, "go.mod", "module x\n")
|
||||
writeFile(t, root, "docs/readme.txt", "x\n")
|
||||
return root
|
||||
},
|
||||
must: []string{"main.go", "docs/readme.txt"},
|
||||
musts: []string{".git/config", "node_modules/pkg/index.js", "go.mod"},
|
||||
},
|
||||
{
|
||||
name: "default_exts_skip_binary_types",
|
||||
setup: func(t *testing.T) string {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "keep.go", "x\n")
|
||||
writeFile(t, root, "img.png", "x\n")
|
||||
writeFile(t, root, "arch.zip", "x\n")
|
||||
return root
|
||||
},
|
||||
must: []string{"keep.go"},
|
||||
musts: []string{"img.png", "arch.zip"},
|
||||
},
|
||||
{
|
||||
name: "hidden_files_and_dirs_skipped",
|
||||
setup: func(t *testing.T) string {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "visible.go", "x\n")
|
||||
writeFile(t, root, ".hidden.go", "x\n")
|
||||
writeFile(t, root, ".github/workflows/ci.yml", "x\n")
|
||||
return root
|
||||
},
|
||||
must: []string{"visible.go"},
|
||||
musts: []string{".hidden.go", ".github/workflows/ci.yml"},
|
||||
},
|
||||
{
|
||||
name: "max_size_skips_large_files",
|
||||
setup: func(t *testing.T) string {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "small.go", "x\n")
|
||||
writeFile(t, root, "large.go", "this content exceeds ten bytes\n")
|
||||
return root
|
||||
},
|
||||
must: []string{"small.go"},
|
||||
musts: []string{"large.go"},
|
||||
},
|
||||
{
|
||||
name: "omit_test_skips_go_tests",
|
||||
setup: func(t *testing.T) string {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "main.go", "x\n")
|
||||
writeFile(t, root, "main_test.go", "x\n")
|
||||
writeFile(t, root, "tests/helper.py", "x\n")
|
||||
return root
|
||||
},
|
||||
must: []string{"main.go"},
|
||||
musts: []string{"main_test.go", "tests/helper.py"},
|
||||
},
|
||||
{
|
||||
name: "omit_md_keeps_readme_only",
|
||||
setup: func(t *testing.T) string {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "README.md", "x\n")
|
||||
writeFile(t, root, "NOTES.md", "x\n")
|
||||
writeFile(t, root, "src/notes.md", "x\n")
|
||||
return root
|
||||
},
|
||||
must: []string{"README.md"},
|
||||
musts: []string{"NOTES.md", "src/notes.md"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
root := tt.setup(t)
|
||||
cfg := baseCfg(root)
|
||||
switch tt.name {
|
||||
case "max_size_skips_large_files":
|
||||
cfg.MaxSize = 10
|
||||
case "omit_test_skips_go_tests":
|
||||
cfg.OmitTest = true
|
||||
case "omit_md_keeps_readme_only":
|
||||
cfg.OmitMd = true
|
||||
}
|
||||
|
||||
result, err := Run(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
|
||||
got := relPaths(result.Entries)
|
||||
for _, w := range tt.must {
|
||||
if !containsPath(got, w) {
|
||||
t.Errorf("missing %q in %v", w, got)
|
||||
}
|
||||
}
|
||||
for _, w := range tt.musts {
|
||||
if containsPath(got, w) {
|
||||
t.Errorf("unexpected %q in %v", w, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDirectory_custom_ignore(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "main.go", "x\n")
|
||||
writeFile(t, root, "scratch/notes.txt", "x\n")
|
||||
|
||||
cfg := baseCfg(root)
|
||||
cfg.Ignores = append([]string{}, config.DefaultIgnores...)
|
||||
cfg.Ignores = append(cfg.Ignores, "scratch")
|
||||
|
||||
result, err := Run(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
got := relPaths(result.Entries)
|
||||
if containsPath(got, "scratch/notes.txt") {
|
||||
t.Errorf("custom-ignored dir walked: %v", got)
|
||||
}
|
||||
if !containsPath(got, "main.go") {
|
||||
t.Errorf("missing main.go in %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDirectory_max_depth(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "a/b/c/d/e/deep.go", "x\n")
|
||||
writeFile(t, root, "a/shallow.go", "x\n")
|
||||
|
||||
cfg := baseCfg(root)
|
||||
cfg.MaxDepth = 3
|
||||
|
||||
result, err := Run(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
got := relPaths(result.Entries)
|
||||
if containsPath(got, "a/b/c/d/e/deep.go") {
|
||||
t.Errorf("file beyond max depth included: %v", got)
|
||||
}
|
||||
if !containsPath(got, "a/shallow.go") {
|
||||
t.Errorf("missing shallow file in %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDirectory_symlink_skipped(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("symlinks unreliable on windows")
|
||||
}
|
||||
|
||||
root := t.TempDir()
|
||||
target := filepath.Join(root, "real", "file.go")
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(target, []byte("x\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(filepath.Join(root, "real"), filepath.Join(root, "linkdir")); err != nil {
|
||||
t.Skipf("cannot create symlink: %v", err)
|
||||
}
|
||||
|
||||
result, err := Run(baseCfg(root))
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
got := relPaths(result.Entries)
|
||||
if containsPath(got, "linkdir/file.go") {
|
||||
t.Errorf("symlinked dir walked: %v", got)
|
||||
}
|
||||
if !containsPath(got, "real/file.go") {
|
||||
t.Errorf("missing real entry in %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDirectory_instructions_read(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "main.go", "x\n")
|
||||
writeFile(t, root, ".dirmd", "be helpful\n")
|
||||
|
||||
cfg := baseCfg(root)
|
||||
cfg.IncludeInstructions = true
|
||||
|
||||
result, err := Run(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
if result.Instructions != "be helpful\n" {
|
||||
t.Errorf("Instructions = %q; want %q", result.Instructions, "be helpful\n")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDirectory_instructions_not_read_by_default(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "main.go", "x\n")
|
||||
writeFile(t, root, ".dirmd", "be helpful\n")
|
||||
|
||||
result, err := Run(baseCfg(root))
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
if result.Instructions != "" {
|
||||
t.Errorf("Instructions = %q; want empty", result.Instructions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWalkDirectory_git_commit_outside_repo(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "main.go", "x\n")
|
||||
|
||||
result, err := Run(baseCfg(root))
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
if result.Commit != nil {
|
||||
t.Errorf("Commit = %v; want nil outside a git repo", result.Commit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSingleFile(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "main.go", "package main\n")
|
||||
|
||||
cfg := baseCfg(filepath.Join(root, "main.go"))
|
||||
cfg.SingleFile = true
|
||||
|
||||
result, err := Run(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("Run: %v", err)
|
||||
}
|
||||
if len(result.Entries) != 1 {
|
||||
t.Fatalf("entries = %v; want single entry", result.Entries)
|
||||
}
|
||||
if result.Entries[0].RelPath != "main.go" {
|
||||
t.Errorf("RelPath = %q; want %q", result.Entries[0].RelPath, "main.go")
|
||||
}
|
||||
if result.AbsRoot != root {
|
||||
t.Errorf("AbsRoot = %q; want %q", result.AbsRoot, root)
|
||||
}
|
||||
if !result.SingleFile {
|
||||
t.Error("SingleFile not set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSingleFile_filtered_out(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
writeFile(t, root, "image.png", "x\n")
|
||||
|
||||
cfg := baseCfg(filepath.Join(root, "image.png"))
|
||||
cfg.SingleFile = true
|
||||
|
||||
_, err := Run(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for filtered single file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSingleFile_missing(t *testing.T) {
|
||||
cfg := baseCfg("/nonexistent/path/file.go")
|
||||
cfg.SingleFile = true
|
||||
|
||||
_, err := Run(cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing file")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user