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
+90
View File
@@ -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
View File
@@ -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)