Skip to content

Commit ec5e4eb

Browse files
committed
Increase test coverage above 70%
1 parent 7a45c5d commit ec5e4eb

File tree

9 files changed

+1964
-298
lines changed

9 files changed

+1964
-298
lines changed

CONTRIBUTING.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,22 @@ Thank you for considering contributing to Codeowners Plus! We welcome contributi
77
### How to Contribute
88

99
#### Reporting Issues
10+
1011
* Search existing issues to see if your concern has already been raised.
1112
* Open a new issue if you find a bug, have a suggestion, or encounter a problem.
1213
* Provide as much detail as possible, including steps to reproduce the issue and any relevant context.
1314

15+
#### Running the Project Locally
16+
17+
> [!Note]
18+
> Running locally still requires real Github PRs to exist that you are testing against.
19+
20+
```bash
21+
go run main.go -token <your_gh_token> -dir ../chaturbate -pr <pr_num> -repo multimediallc/chaturbate -v true
22+
```
23+
1424
#### Submitting Changes
25+
1526
* Fork the repository
1627
* Create a new branch for your changes
1728
* After making code changes, run `./scripts/covbadge.sh` to update the code coverage badge (this will be enforced in GHA checks)
@@ -20,15 +31,18 @@ Thank you for considering contributing to Codeowners Plus! We welcome contributi
2031
* Open a pull request against the `main` branch of this repository
2132

2233
### Code Style and Standards
34+
2335
* Follow [Go best practices](https://go.dev/doc/effective_go).
2436
* Write clear, concise, and well-documented code.
2537
* Include unit tests for any new functionality.
2638

2739
### Reviewing and Merging Changes
40+
2841
* All pull requests will be reviewed by maintainers.
2942
* Address feedback promptly and communicate if additional time is needed.
3043
* Once approved, your changes will be merged by a maintainer.
3144

3245
### Community
46+
3347
* Join discussions in the issues section to help troubleshoot or brainstorm solutions.
3448
* Respectfully engage with others to maintain a friendly and constructive environment.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Code Ownership &amp; Review Assignment Tool - GitHub CODEOWNERS but better
44

55
[![Go Report Card](https://goreportcard.com/badge/github.com/multimediallc/codeowners-plus)](https://goreportcard.com/report/github.com/multimediallc/codeowners-plus?kill_cache=1)
66
[![Tests](https://github.com/multimediallc/codeowners-plus/actions/workflows/go.yml/badge.svg)](https://github.com/multimediallc/codeowners-plus/actions/workflows/go.yml)
7-
![Coverage](https://img.shields.io/badge/Coverage-60.1%25-yellow)
7+
![Coverage](https://img.shields.io/badge/Coverage-74.3%25-brightgreen)
88
[![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
99
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)
1010

internal/config/config_test.go

Lines changed: 181 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,194 @@
11
package owners
22

33
import (
4+
"os"
5+
"path/filepath"
46
"testing"
57
)
68

7-
func TestReadCodeownersConfig(t *testing.T) {
8-
conf, err := ReadConfig("../../test_project/")
9-
if err != nil {
10-
t.Errorf("Error reading codeowners config: %v", err)
11-
return
12-
}
13-
if conf == nil {
14-
t.Errorf("Error reading codeowners config")
15-
return
16-
}
17-
// if *conf.MaxReviews != 2 {
18-
// t.Errorf("Expected max reviews 2, got %d", *conf.MaxReviews)
19-
// }
20-
if conf.MaxReviews != nil {
21-
t.Errorf("Expected max reviews to be nil, got %d", *conf.MaxReviews)
22-
}
23-
if len(conf.UnskippableReviewers) != 2 {
24-
t.Errorf("Expected 2 unskippable reviewers, got %d", len(conf.UnskippableReviewers))
25-
}
26-
if conf.UnskippableReviewers[0] != "@user1" {
27-
t.Errorf("Expected unskippable reviewer @user1, got %s", conf.UnskippableReviewers[0])
28-
}
29-
if conf.UnskippableReviewers[1] != "@user2" {
30-
t.Errorf("Expected unskippable reviewer @user2, got %s", conf.UnskippableReviewers[0])
9+
func TestReadConfig(t *testing.T) {
10+
tt := []struct {
11+
name string
12+
configContent string
13+
path string
14+
expected *Config
15+
expectedErr bool
16+
}{
17+
{
18+
name: "default config when no file exists",
19+
path: "nonexistent/",
20+
expected: &Config{
21+
MaxReviews: nil,
22+
MinReviews: nil,
23+
UnskippableReviewers: []string{},
24+
Ignore: []string{},
25+
Enforcement: &Enforcement{Approval: false, FailCheck: true},
26+
},
27+
expectedErr: false,
28+
},
29+
{
30+
name: "valid config with all fields",
31+
configContent: `
32+
max_reviews = 2
33+
min_reviews = 1
34+
unskippable_reviewers = ["@user1", "@user2"]
35+
ignore = ["ignored/"]
36+
[enforcement]
37+
approval = true
38+
fail_check = false
39+
`,
40+
path: "testdata/",
41+
expected: &Config{
42+
MaxReviews: intPtr(2),
43+
MinReviews: intPtr(1),
44+
UnskippableReviewers: []string{"@user1", "@user2"},
45+
Ignore: []string{"ignored/"},
46+
Enforcement: &Enforcement{Approval: true, FailCheck: false},
47+
},
48+
expectedErr: false,
49+
},
50+
{
51+
name: "partial config with defaults",
52+
configContent: `
53+
max_reviews = 3
54+
unskippable_reviewers = ["@user1"]
55+
`,
56+
path: "testdata/",
57+
expected: &Config{
58+
MaxReviews: intPtr(3),
59+
MinReviews: nil,
60+
UnskippableReviewers: []string{"@user1"},
61+
Ignore: []string{},
62+
Enforcement: &Enforcement{Approval: false, FailCheck: true},
63+
},
64+
expectedErr: false,
65+
},
66+
{
67+
name: "invalid toml",
68+
configContent: `
69+
max_reviews = invalid
70+
`,
71+
path: "testdata/",
72+
expectedErr: true,
73+
},
3174
}
32-
if len(conf.Ignore) != 1 {
33-
t.Errorf("Expected 1 ignore dir, got %d", len(conf.Ignore))
75+
76+
for _, tc := range tt {
77+
t.Run(tc.name, func(t *testing.T) {
78+
// Create test directory
79+
testDir := t.TempDir()
80+
configPath := filepath.Join(testDir, tc.path)
81+
82+
// Create config file if content is provided
83+
if tc.configContent != "" {
84+
err := os.MkdirAll(configPath, 0755)
85+
if err != nil {
86+
t.Fatalf("failed to create test directory: %v", err)
87+
}
88+
err = os.WriteFile(filepath.Join(configPath, "codeowners.toml"), []byte(tc.configContent), 0644)
89+
if err != nil {
90+
t.Fatalf("failed to write test config: %v", err)
91+
}
92+
}
93+
94+
// Test with and without trailing slash
95+
paths := []string{configPath, configPath + "/"}
96+
for _, path := range paths {
97+
got, err := ReadConfig(path)
98+
if tc.expectedErr {
99+
if err == nil {
100+
t.Error("expected error but got none")
101+
}
102+
continue
103+
}
104+
105+
if err != nil {
106+
t.Errorf("unexpected error: %v", err)
107+
continue
108+
}
109+
110+
if got == nil {
111+
t.Error("got nil config")
112+
continue
113+
}
114+
115+
// Compare fields
116+
if tc.expected.MaxReviews != nil {
117+
if got.MaxReviews == nil {
118+
t.Error("expected MaxReviews to be set")
119+
} else if *got.MaxReviews != *tc.expected.MaxReviews {
120+
t.Errorf("MaxReviews: expected %d, got %d", *tc.expected.MaxReviews, *got.MaxReviews)
121+
}
122+
} else if got.MaxReviews != nil {
123+
t.Errorf("MaxReviews: expected nil, got %d", *got.MaxReviews)
124+
}
125+
126+
if tc.expected.MinReviews != nil {
127+
if got.MinReviews == nil {
128+
t.Error("expected MinReviews to be set")
129+
} else if *got.MinReviews != *tc.expected.MinReviews {
130+
t.Errorf("MinReviews: expected %d, got %d", *tc.expected.MinReviews, *got.MinReviews)
131+
}
132+
} else if got.MinReviews != nil {
133+
t.Errorf("MinReviews: expected nil, got %d", *got.MinReviews)
134+
}
135+
136+
if !sliceEqual(got.UnskippableReviewers, tc.expected.UnskippableReviewers) {
137+
t.Errorf("UnskippableReviewers: expected %v, got %v", tc.expected.UnskippableReviewers, got.UnskippableReviewers)
138+
}
139+
140+
if !sliceEqual(got.Ignore, tc.expected.Ignore) {
141+
t.Errorf("Ignore: expected %v, got %v", tc.expected.Ignore, got.Ignore)
142+
}
143+
144+
if tc.expected.Enforcement != nil {
145+
if got.Enforcement == nil {
146+
t.Error("expected Enforcement to be set")
147+
} else {
148+
if got.Enforcement.Approval != tc.expected.Enforcement.Approval {
149+
t.Errorf("Enforcement.Approval: expected %v, got %v", tc.expected.Enforcement.Approval, got.Enforcement.Approval)
150+
}
151+
if got.Enforcement.FailCheck != tc.expected.Enforcement.FailCheck {
152+
t.Errorf("Enforcement.FailCheck: expected %v, got %v", tc.expected.Enforcement.FailCheck, got.Enforcement.FailCheck)
153+
}
154+
}
155+
} else if got.Enforcement != nil {
156+
t.Error("Enforcement: expected nil, got non-nil")
157+
}
158+
}
159+
})
34160
}
35-
if conf.Ignore[0] != "ignored" {
36-
t.Errorf("Expected ignore dir ignored, got %s", conf.Ignore[0])
161+
}
162+
163+
func TestReadConfigFileError(t *testing.T) {
164+
// Create a directory with no read permissions
165+
testDir := t.TempDir()
166+
configPath := filepath.Join(testDir, "test/")
167+
err := os.MkdirAll(configPath, 0000)
168+
if err != nil {
169+
t.Fatalf("failed to create test directory: %v", err)
37170
}
38-
if conf.Enforcement == nil {
39-
t.Errorf("Expected enforcement to be set")
171+
172+
// Try to read config from directory with no permissions
173+
_, err = ReadConfig(configPath)
174+
if err == nil {
175+
t.Error("expected error when reading from directory with no permissions")
40176
}
41-
if conf.Enforcement.Approval != false {
42-
t.Errorf("Expected Approval to be false")
177+
}
178+
179+
// Helper functions
180+
func intPtr(i int) *int {
181+
return &i
182+
}
183+
184+
func sliceEqual(a, b []string) bool {
185+
if len(a) != len(b) {
186+
return false
43187
}
44-
if conf.Enforcement.FailCheck != true {
45-
t.Errorf("Expected FailCheck to be true")
188+
for i := range a {
189+
if a[i] != b[i] {
190+
return false
191+
}
46192
}
193+
return true
47194
}

internal/git/diff.go

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,46 @@ import (
1313
"github.com/sourcegraph/go-diff/diff"
1414
)
1515

16+
// gitCommandExecutor defines the interface for executing git commands
17+
type gitCommandExecutor interface {
18+
execute(command string, args ...string) ([]byte, error)
19+
}
20+
21+
// realGitExecutor implements GitCommandExecutor using os/exec
22+
type realGitExecutor struct {
23+
dir string
24+
}
25+
26+
func newRealGitExecutor(dir string) *realGitExecutor {
27+
return &realGitExecutor{dir: dir}
28+
}
29+
30+
func (e *realGitExecutor) execute(command string, args ...string) ([]byte, error) {
31+
cmd := exec.Command(command, args...)
32+
cmd.Dir = e.dir
33+
return cmd.CombinedOutput()
34+
}
35+
1636
type Diff interface {
1737
AllChanges() []codeowners.DiffFile
1838
ChangesSince(ref string) ([]codeowners.DiffFile, error)
1939
Context() DiffContext
2040
}
2141

2242
type GitDiff struct {
23-
context DiffContext
24-
diff []*diff.FileDiff
25-
files []codeowners.DiffFile
43+
context DiffContext
44+
diff []*diff.FileDiff
45+
files []codeowners.DiffFile
46+
executor gitCommandExecutor
2647
}
2748

2849
func NewDiff(context DiffContext) (Diff, error) {
29-
gitDiff, err := getGitDiff(context)
50+
executor := newRealGitExecutor(context.Dir)
51+
return NewDiffWithExecutor(context, executor)
52+
}
53+
54+
func NewDiffWithExecutor(context DiffContext, executor gitCommandExecutor) (Diff, error) {
55+
gitDiff, err := getGitDiff(context, executor)
3056
if err != nil {
3157
return nil, err
3258
}
@@ -36,9 +62,10 @@ func NewDiff(context DiffContext) (Diff, error) {
3662
}
3763

3864
return &GitDiff{
39-
context: context,
40-
diff: gitDiff,
41-
files: diffFiles,
65+
context: context,
66+
diff: gitDiff,
67+
files: diffFiles,
68+
executor: executor,
4269
}, nil
4370
}
4471

@@ -53,17 +80,17 @@ func (gd *GitDiff) ChangesSince(ref string) ([]codeowners.DiffFile, error) {
5380
Dir: gd.context.Dir,
5481
IgnoreDirs: gd.context.IgnoreDirs,
5582
}
56-
olderDiff, err := getGitDiff(olderDiffContext)
83+
olderDiff, err := getGitDiff(olderDiffContext, gd.executor)
5784
if err != nil {
58-
return nil, err
85+
return nil, fmt.Errorf("failed to get older diff: %w", err)
5986
}
6087
changesContext := changesSinceContext{
6188
newerDiff: gd.diff,
6289
olderDiff: olderDiff,
6390
}
6491
diffFiles, err := changesSince(changesContext)
6592
if err != nil {
66-
return nil, err
93+
return nil, fmt.Errorf("failed to compute changes since: %w", err)
6794
}
6895
return diffFiles, nil
6996
}
@@ -140,10 +167,8 @@ func changesSince(context changesSinceContext) ([]codeowners.DiffFile, error) {
140167
return diffFiles, nil
141168
}
142169

143-
func getGitDiff(data DiffContext) ([]*diff.FileDiff, error) {
144-
cmd := exec.Command("git", "diff", "-U0", fmt.Sprintf("%s...%s", data.Base, data.Head))
145-
cmd.Dir = data.Dir
146-
cmdOutput, err := cmd.CombinedOutput()
170+
func getGitDiff(data DiffContext, executor gitCommandExecutor) ([]*diff.FileDiff, error) {
171+
cmdOutput, err := executor.execute("git", "diff", "-U0", fmt.Sprintf("%s...%s", data.Base, data.Head))
147172
if err != nil {
148173
return nil, fmt.Errorf("Diff Error: %s\n%s\n", err, cmdOutput)
149174
}
@@ -167,6 +192,10 @@ func hunkHash(hunk *diff.Hunk) [32]byte {
167192
var lines []byte
168193
data := hunk.Body
169194

195+
if len(data) == 0 {
196+
return sha256.Sum256(nil)
197+
}
198+
170199
scanner := bufio.NewScanner(bytes.NewReader(data))
171200

172201
for scanner.Scan() {

0 commit comments

Comments
 (0)