-
Notifications
You must be signed in to change notification settings - Fork 1
feat: ANSI colored terminal output, test coverage boost, CI cleanup #4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| package reviewer | ||
|
|
||
| import ( | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/OpticDiff/code-reviewer/internal/model" | ||
| ) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // ColorTerminalOutput tests (new colored output) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| func TestColorTerminalOutput_NoColor_FallsBackToPlain(t *testing.T) { | ||
| result := &model.ReviewResult{ | ||
| Summary: "test", | ||
| Findings: []model.Finding{ | ||
| {File: "main.go", Line: 1, Severity: "HIGH", Title: "Bug", Body: "desc"}, | ||
| }, | ||
| } | ||
| // useColor=false should produce output identical to TerminalOutput. | ||
| out := ColorTerminalOutput(result, false) | ||
| plain := TerminalOutput(result) | ||
| if out != plain { | ||
| t.Error("ColorTerminalOutput(false) should equal TerminalOutput") | ||
| } | ||
| } | ||
|
|
||
| func TestColorTerminalOutput_WithColor_HasANSI(t *testing.T) { | ||
| result := &model.ReviewResult{ | ||
| Summary: "Found issues.", | ||
| Findings: []model.Finding{ | ||
| {File: "main.go", Line: 42, Severity: "CRITICAL", Title: "SQL Injection", Body: "User input in query.", Suggestion: "use parameterized queries"}, | ||
| {File: "main.go", Line: 55, Severity: "HIGH", Title: "Resource leak", Body: "Unclosed body."}, | ||
| {File: "config.go", Line: 10, Severity: "MEDIUM", Title: "Missing validation", Body: "No input check."}, | ||
| {File: "util.go", Line: 5, Severity: "LOW", Title: "Naming", Body: "Use better name."}, | ||
| }, | ||
| } | ||
| out := ColorTerminalOutput(result, true) | ||
|
|
||
| // Verify ANSI codes are present. | ||
| if !strings.Contains(out, "\033[") { | ||
| t.Error("expected ANSI escape codes in colored output") | ||
| } | ||
|
|
||
| // Verify box drawing. | ||
| if !strings.Contains(out, "β") { | ||
| t.Error("expected box-drawing top border") | ||
| } | ||
| if !strings.Contains(out, "β") { | ||
| t.Error("expected box-drawing bottom border") | ||
| } | ||
|
|
||
| // Verify severity badges. | ||
| if !strings.Contains(out, "CRITICAL") { | ||
| t.Error("expected CRITICAL severity in output") | ||
| } | ||
| if !strings.Contains(out, "π΄") { | ||
| t.Error("expected π΄ in severity counts") | ||
| } | ||
|
|
||
| // Verify file separators. | ||
| if !strings.Contains(out, "ββ main.go") { | ||
| t.Error("expected file separator for main.go") | ||
| } | ||
|
|
||
| // Verify suggestion block. | ||
| if !strings.Contains(out, "Suggestion:") { | ||
| t.Error("expected 'Suggestion:' label") | ||
| } | ||
|
|
||
| // Verify summary counts. | ||
| if !strings.Contains(out, "1 critical") { | ||
| t.Error("expected '1 critical' in summary") | ||
| } | ||
| if !strings.Contains(out, "1 high") { | ||
| t.Error("expected '1 high' in summary") | ||
| } | ||
| } | ||
|
|
||
| func TestColorTerminalOutput_NoFindings_ShowsClean(t *testing.T) { | ||
| result := &model.ReviewResult{ | ||
| Summary: "Clean code.", | ||
| Findings: nil, | ||
| } | ||
| out := ColorTerminalOutput(result, true) | ||
| if !strings.Contains(out, "No issues found") { | ||
| t.Error("expected 'No issues found' in colored empty output") | ||
| } | ||
| if !strings.Contains(out, "β") { | ||
| t.Error("expected box drawing even with no findings") | ||
| } | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // formatSummaryNote tests (GitLab posting) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| func TestFormatSummaryNote_WithFindings(t *testing.T) { | ||
| result := &model.ReviewResult{ | ||
| Summary: "Mixed issues.", | ||
| Findings: []model.Finding{ | ||
| {File: "a.go", Line: 1, Severity: "CRITICAL", Title: "critical", Body: "c"}, | ||
| {File: "a.go", Line: 2, Severity: "HIGH", Title: "high", Body: "h"}, | ||
| {File: "a.go", Line: 3, Severity: "HIGH", Title: "high2", Body: "h2"}, | ||
| {File: "a.go", Line: 4, Severity: "LOW", Title: "low", Body: "l"}, | ||
| }, | ||
| } | ||
| out := formatSummaryNote(result) | ||
|
|
||
| if !strings.Contains(out, "π Code Review Summary") { | ||
| t.Error("expected summary header") | ||
| } | ||
| if !strings.Contains(out, "CRITICAL") { | ||
| t.Error("expected CRITICAL in table") | ||
| } | ||
| // HIGH should show count 2. | ||
| if !strings.Contains(out, "2") { | ||
| t.Error("expected count of 2 for HIGH") | ||
| } | ||
| } | ||
|
|
||
| func TestFormatSummaryNote_NoFindings(t *testing.T) { | ||
| result := &model.ReviewResult{ | ||
| Summary: "All clean.", | ||
| Findings: nil, | ||
| } | ||
| out := formatSummaryNote(result) | ||
| if !strings.Contains(out, "No issues found") { | ||
| t.Error("expected 'No issues found' in summary note") | ||
| } | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Helper function tests | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| func TestSeverityEmoji_AllCases(t *testing.T) { | ||
| tests := []struct { | ||
| severity string | ||
| emoji string | ||
| }{ | ||
| {"CRITICAL", "π΄"}, | ||
| {"critical", "π΄"}, | ||
| {"HIGH", "π "}, | ||
| {"high", "π "}, | ||
| {"MEDIUM", "π‘"}, | ||
| {"LOW", "π΅"}, | ||
| {"UNKNOWN", "βͺ"}, | ||
| {"", "βͺ"}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.severity, func(t *testing.T) { | ||
| got := severityEmoji(tt.severity) | ||
| if got != tt.emoji { | ||
| t.Errorf("severityEmoji(%q) = %q, want %q", tt.severity, got, tt.emoji) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestSeverityColor(t *testing.T) { | ||
| tests := []struct { | ||
| severity string | ||
| color string | ||
| }{ | ||
| {"CRITICAL", ansiRed}, | ||
| {"HIGH", ansiOrange}, | ||
| {"MEDIUM", ansiYellow}, | ||
| {"LOW", ansiBlue}, | ||
| {"UNKNOWN", ansiWhite}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.severity, func(t *testing.T) { | ||
| got := severityColor(tt.severity) | ||
| if got != tt.color { | ||
| t.Errorf("severityColor(%q) = %q, want %q", tt.severity, got, tt.color) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestFormatInlineComment_WithoutSuggestion(t *testing.T) { | ||
| f := model.Finding{ | ||
| Severity: "HIGH", | ||
| Title: "Resource leak", | ||
| Body: "File handle not closed.", | ||
| } | ||
| out := formatInlineComment(f) | ||
| if !strings.Contains(out, "π ") { | ||
| t.Error("expected HIGH emoji") | ||
| } | ||
| if !strings.Contains(out, "Resource leak") { | ||
| t.Error("expected title") | ||
| } | ||
| if strings.Contains(out, "```suggestion") { | ||
| t.Error("should not have suggestion block when suggestion is empty") | ||
| } | ||
| } | ||
|
|
||
| func TestFormatInlineComment_WithSuggestion(t *testing.T) { | ||
| f := model.Finding{ | ||
| Severity: "CRITICAL", | ||
| Title: "SQL injection", | ||
| Body: "Raw concat.", | ||
| Suggestion: "db.Query(\"SELECT * FROM t WHERE id = ?\", id)", | ||
| } | ||
| out := formatInlineComment(f) | ||
| if !strings.Contains(out, "```suggestion") { | ||
| t.Error("expected suggestion code block") | ||
| } | ||
| if !strings.Contains(out, "db.Query") { | ||
| t.Error("expected suggestion content") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π Security & Privacy | π‘ Minor | β‘ Quick win
Add
persist-credentials: falseto checkout steps that don't need to push.The
actions/checkoutstep here (and similarly in thebuild/lintjobs) leavespersist-credentialsat its default oftrue, so theGITHUB_TOKENis written to.git/configfor the remainder of the job. This job also uploads acoverage.outartifact β if the artifact path ever broadens (e.g. workspace archiving) or a subsequent step is compromised, the persisted token could leak. Since none of these jobs push commits, credentials aren't needed post-checkout.π Proposed fix
Apply the same change to the checkout steps in the
build(line 20) andlint(line 58) jobs.π Committable suggestion
π§° Tools
πͺ zizmor (1.26.1)
[warning] 36-36: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
π€ Prompt for AI Agents
Source: Linters/SAST tools