diff --git a/pkg/lint/detect_project_test.go b/pkg/lint/detect_project_test.go index cbd3d54..eb4a9ed 100644 --- a/pkg/lint/detect_project_test.go +++ b/pkg/lint/detect_project_test.go @@ -29,6 +29,34 @@ func TestDetect_Good_ProjectMarkersAndFiles(t *testing.T) { ) } +func TestDetect_Good_MarkerCoverage(t *testing.T) { + dir := t.TempDir() + + files := map[string]string{ + "go.mod": "module example.com/test\n", + "composer.json": "{}\n", + "package.json": "{}\n", + "tsconfig.json": "{}\n", + "requirements.txt": "ruff\n", + "pyproject.toml": "[tool.ruff]\n", + "Cargo.toml": "[package]\nname = \"test\"\n", + "Dockerfile.dev": "FROM scratch\n", + "run.sh": "#!/bin/sh\n", + "main.cpp": "int main() { return 0; }\n", + "config.yaml": "kind: Config\n", + "config.yml": "kind: Config\n", + } + + for name, content := range files { + require.NoError(t, os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644)) + } + + assert.Equal(t, + []string{"cpp", "dockerfile", "go", "js", "php", "python", "rust", "shell", "ts", "yaml"}, + Detect(dir), + ) +} + func TestDetectFromFiles_Good(t *testing.T) { files := []string{ "main.go", @@ -45,7 +73,7 @@ func TestDetectFromFiles_Good(t *testing.T) { ) } -func TestDetect_MissingPathReturnsEmptySlice(t *testing.T) { +func TestDetect_Bad_MissingPathReturnsEmptySlice(t *testing.T) { assert.Equal(t, []string{}, Detect(filepath.Join(t.TempDir(), "missing"))) } @@ -57,3 +85,17 @@ func TestDetect_Good_SkipsHiddenRootDirectory(t *testing.T) { assert.Equal(t, []string{}, Detect(hiddenDir)) } + +func TestDetect_Ugly_SkipsNestedHiddenAndExcludedDirectories(t *testing.T) { + dir := t.TempDir() + + require.NoError(t, os.WriteFile(filepath.Join(dir, "root.go"), []byte("package main\n"), 0o644)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "vendor"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "vendor", "ignored.go"), []byte("package ignored\n"), 0o644)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "ignored.go"), []byte("package ignored\n"), 0o644)) + require.NoError(t, os.MkdirAll(filepath.Join(dir, "services", ".generated"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, "services", ".generated", "ignored.go"), []byte("package ignored\n"), 0o644)) + + assert.Equal(t, []string{"go"}, Detect(dir)) +} diff --git a/pkg/lint/output_test.go b/pkg/lint/output_test.go index 3293b39..76d2e6b 100644 --- a/pkg/lint/output_test.go +++ b/pkg/lint/output_test.go @@ -62,3 +62,26 @@ func TestResolveRunOutputFormat_Good_ExplicitOutputBypassesConfigLoading(t *test require.NoError(t, err) assert.Equal(t, "sarif", format) } + +func TestResolveRunOutputFormat_Bad_BrokenConfig(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "lint.yaml"), []byte("{not: yaml"), 0o644)) + + _, err := ResolveRunOutputFormat(RunInput{ + Path: dir, + }) + assert.Error(t, err) +} + +func TestResolveRunOutputFormat_Ugly_MissingSchedule(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "lint.yaml"), []byte("output: text\n"), 0o644)) + + _, err := ResolveRunOutputFormat(RunInput{ + Path: dir, + Schedule: "nightly", + }) + assert.Error(t, err) +} diff --git a/pkg/lint/report_test.go b/pkg/lint/report_test.go index a2f8ab6..4259c82 100644 --- a/pkg/lint/report_test.go +++ b/pkg/lint/report_test.go @@ -58,6 +58,19 @@ func TestSummarise_Good_Empty(t *testing.T) { assert.Empty(t, summary.BySeverity) } +func TestSummarise_Bad_BlankSeverityDefaultsToWarning(t *testing.T) { + summary := Summarise([]Finding{ + {Severity: ""}, + {Severity: "info"}, + }) + + assert.Equal(t, 2, summary.Total) + assert.Equal(t, 1, summary.Warnings) + assert.Equal(t, 1, summary.Info) + assert.Equal(t, 0, summary.Errors) + assert.True(t, summary.Passed) +} + func TestWriteJSON_Good_Roundtrip(t *testing.T) { findings := sampleFindings() var buf bytes.Buffer @@ -116,6 +129,11 @@ func TestWriteJSONL_Good_Empty(t *testing.T) { assert.Empty(t, buf.String()) } +func TestWriteJSONL_Bad_PropagatesWriterErrors(t *testing.T) { + err := WriteJSONL(failingWriter{}, sampleFindings()) + require.Error(t, err) +} + func TestWriteText_Good(t *testing.T) { findings := sampleFindings() var buf bytes.Buffer diff --git a/pkg/lint/scanner_test.go b/pkg/lint/scanner_test.go index 1911dcf..d428cac 100644 --- a/pkg/lint/scanner_test.go +++ b/pkg/lint/scanner_test.go @@ -29,6 +29,9 @@ func TestDetectLanguage_Good(t *testing.T) { {"noextension", ""}, {"file.py", "python"}, {"Dockerfile", "dockerfile"}, + {"services/Dockerfile.prod", "dockerfile"}, + {"configs/settings.yaml", "yaml"}, + {"configs/settings.yml", "yaml"}, } for _, tt := range tests { @@ -39,6 +42,15 @@ func TestDetectLanguage_Good(t *testing.T) { } } +func TestDetectLanguage_Bad_UnknownExtension(t *testing.T) { + assert.Equal(t, "", DetectLanguage("notes.txt")) + assert.Equal(t, "", DetectLanguage("README")) +} + +func TestDetectLanguage_Ugly_DockerfileVariant(t *testing.T) { + assert.Equal(t, "dockerfile", DetectLanguage("nested/Dockerfile.test")) +} + func TestScanDir_Good_FindsMatches(t *testing.T) { dir := t.TempDir() @@ -209,6 +221,58 @@ func TestScanFile_Good_Python(t *testing.T) { assert.Equal(t, "python", DetectLanguage(file)) } +func TestScanFile_Bad_NoMatchingLanguageRules(t *testing.T) { + dir := t.TempDir() + file := filepath.Join(dir, "app.go") + err := os.WriteFile(file, []byte("package main\n"), 0o644) + require.NoError(t, err) + + rules := []Rule{ + { + ID: "php-only", + Title: "PHP TODO", + Severity: "low", + Languages: []string{"php"}, + Pattern: `TODO`, + Fix: "Remove TODO", + Detection: "regex", + }, + } + + s, err := NewScanner(rules) + require.NoError(t, err) + + findings, err := s.ScanFile(file) + require.NoError(t, err) + assert.Empty(t, findings) +} + +func TestScanFile_Ugly_UnsupportedExtension(t *testing.T) { + dir := t.TempDir() + file := filepath.Join(dir, "notes.txt") + err := os.WriteFile(file, []byte("TODO: this is not a recognised source file\n"), 0o644) + require.NoError(t, err) + + rules := []Rule{ + { + ID: "go-only", + Title: "Go TODO", + Severity: "low", + Languages: []string{"go"}, + Pattern: `TODO`, + Fix: "Remove TODO", + Detection: "regex", + }, + } + + s, err := NewScanner(rules) + require.NoError(t, err) + + findings, err := s.ScanFile(file) + require.NoError(t, err) + assert.Nil(t, findings) +} + func TestScanDir_Good_Subdirectories(t *testing.T) { dir := t.TempDir()