add test suite

This commit is contained in:
2026-09-15 13:11:20 +01:00
parent 9fa410e3a1
commit 9562be5e34
16 changed files with 1233 additions and 267 deletions
+2 -143
View File
@@ -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
}
+81
View File
@@ -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)
}
})
}
}