// gitignore_test.go -- .gitignore 解析器的单元测试. // // 覆盖场景: // - parseIgnoreLine 解析各种规则格式 // - 否定规则(! 前缀) // - 目录标记(/ 后缀) // - 锚定规则(包含 /) // - 注释和空行跳过 // - IsIgnored 综合路径匹配 // - defaultIgnorePatterns 默认排除模式 // - ** 双星通配符匹配 // - globMatchIgnore 各种模式 // - matchDoublestar 处理 package builtin import ( "os" "path/filepath" "testing" ) // TestParseIgnoreLine 测试单行 .gitignore 规则解析 func TestParseIgnoreLine(t *testing.T) { tests := []struct { name string line string baseDir string wantNil bool wantNegate bool wantDir bool wantAnchored bool wantPattern string }{ {"空行", "", "", true, false, false, false, ""}, {"注释行", "# comment", "", true, false, false, false, ""}, {"简单模式", "*.log", "", false, false, false, false, "*.log"}, {"否定规则", "!important.log", "", false, true, false, false, "important.log"}, {"目录标记", "build/", "", false, false, true, false, "build"}, {"锚定规则", "src/temp", "", false, false, false, true, "src/temp"}, {"前导斜杠", "/build", "", false, false, false, true, "build"}, {"否定目录", "!dist/", "", false, true, true, false, "dist"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := parseIgnoreLine(tt.line, tt.baseDir) if tt.wantNil { if p != nil { t.Errorf("期望返回 nil, 实际: %+v", p) } return } if p == nil { t.Fatal("不应返回 nil") } if p.negate != tt.wantNegate { t.Errorf("negate: %v, 期望 %v", p.negate, tt.wantNegate) } if p.dirOnly != tt.wantDir { t.Errorf("dirOnly: %v, 期望 %v", p.dirOnly, tt.wantDir) } if p.anchored != tt.wantAnchored { t.Errorf("anchored: %v, 期望 %v", p.anchored, tt.wantAnchored) } if p.pattern != tt.wantPattern { t.Errorf("pattern: %q, 期望 %q", p.pattern, tt.wantPattern) } }) } } // TestIsIgnored 测试路径忽略判断 func TestIsIgnored(t *testing.T) { patterns := []IgnorePattern{ {pattern: "*.log", dirOnly: false}, {pattern: "build", dirOnly: true}, {pattern: "node_modules", dirOnly: true}, {pattern: "important.log", negate: true}, } tests := []struct { name string relPath string isDir bool want bool }{ {"匹配 *.log", "error.log", false, true}, {"否定规则", "important.log", false, false}, {"目录匹配", "build", true, true}, {"node_modules 目录", "node_modules", true, true}, {"普通文件不匹配", "main.go", false, false}, {"build 文件(非目录)", "build", false, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := IsIgnored(tt.relPath, patterns, tt.isDir) if got != tt.want { t.Errorf("IsIgnored(%q, ..., %v) = %v, 期望 %v", tt.relPath, tt.isDir, got, tt.want) } }) } } // TestDefaultIgnorePatterns 测试默认排除模式 func TestDefaultIgnorePatterns(t *testing.T) { patterns := defaultIgnorePatterns() // 验证包含常见的默认排除模式 expectedDirs := []string{".git", "node_modules", "__pycache__", "vendor", "dist", "build"} for _, dirName := range expectedDirs { found := false for _, p := range patterns { if p.pattern == dirName && p.dirOnly { found = true break } } if !found { t.Errorf("默认模式应包含目录排除: %s/", dirName) } } } // TestGlobMatchIgnore 测试 .gitignore 风格的 glob 匹配 func TestGlobMatchIgnore(t *testing.T) { tests := []struct { name string pattern string name_ string want bool }{ {"精确匹配", "main.go", "main.go", true}, {"通配符匹配", "*.go", "main.go", true}, {"通配符不匹配", "*.go", "main.ts", false}, {"问号匹配", "?.txt", "a.txt", true}, {"路径匹配", "src/main.go", "src/main.go", true}, {"路径不匹配长度", "src/main.go", "pkg/main.go", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := globMatchIgnore(tt.pattern, tt.name_) if got != tt.want { t.Errorf("globMatchIgnore(%q, %q) = %v, 期望 %v", tt.pattern, tt.name_, got, tt.want) } }) } } // TestMatchDoublestar 测试 ** 双星匹配 func TestMatchDoublestar(t *testing.T) { tests := []struct { name string pattern string name_ string want bool }{ {"** 在开头", "**/*.go", "src/main.go", true}, {"** 在开头深层", "**/*.go", "a/b/c/main.go", true}, {"** 在结尾", "src/**", "src/a/b/c", true}, {"** 在中间", "src/**/main.go", "src/pkg/main.go", true}, {"纯 **", "**", "anything", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := matchDoublestar(tt.pattern, tt.name_) if got != tt.want { t.Errorf("matchDoublestar(%q, %q) = %v, 期望 %v", tt.pattern, tt.name_, got, tt.want) } }) } } // TestParseGitignore 测试从文件解析 .gitignore 规则 func TestParseGitignore(t *testing.T) { dir := t.TempDir() gitignorePath := filepath.Join(dir, ".gitignore") content := "# comment\n*.log\nbuild/\n!important.log\n\n/dist\n" os.WriteFile(gitignorePath, []byte(content), 0644) patterns := ParseGitignore(gitignorePath, "") if len(patterns) != 4 { t.Fatalf("期望 4 条规则, 实际 %d 条", len(patterns)) } // 验证第一条规则 if patterns[0].pattern != "*.log" { t.Errorf("第一条规则应为 '*.log', 实际 %q", patterns[0].pattern) } // 验证否定规则 if !patterns[2].negate { t.Error("第三条规则应为否定规则") } } // TestIsIgnored_WithBaseDir 测试带 baseDir 的路径匹配 func TestIsIgnored_WithBaseDir(t *testing.T) { patterns := []IgnorePattern{ {pattern: "*.tmp", baseDir: "src"}, } // 在 src/ 下的 .tmp 文件应被匹配 if !IsIgnored("src/test.tmp", patterns, false) { t.Error("src/test.tmp 应被忽略") } // 不在 src/ 下的 .tmp 文件不应被匹配 if IsIgnored("pkg/test.tmp", patterns, false) { t.Error("pkg/test.tmp 不应被忽略") } } // TestCollectIgnorePatterns 测试递归收集忽略规则 func TestCollectIgnorePatterns(t *testing.T) { dir := t.TempDir() // 创建根目录 .gitignore os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.log\n"), 0644) // 创建子目录和子 .gitignore subDir := filepath.Join(dir, "src") os.MkdirAll(subDir, 0755) os.WriteFile(filepath.Join(subDir, ".gitignore"), []byte("*.tmp\n"), 0644) patterns := CollectIgnorePatterns(dir) // 应包含默认模式 + 根 .gitignore + 子 .gitignore if len(patterns) < 3 { t.Errorf("应收集到至少 3 条规则(含默认), 实际 %d 条", len(patterns)) } }