Skip to content

Commit 72ba2fd

Browse files
authored
Add checking capabilities (#9)
1 parent c94a6b9 commit 72ba2fd

5 files changed

Lines changed: 238 additions & 0 deletions

File tree

checker.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package codeowners
2+
3+
import (
4+
"fmt"
5+
"io"
6+
)
7+
8+
var availableCheckers map[string]Checker
9+
10+
func init() {
11+
availableCheckers = make(map[string]Checker)
12+
}
13+
14+
// AvailableCheckers returns list of registered checkers
15+
func AvailableCheckers() []string {
16+
names := make([]string, len(availableCheckers))
17+
i := 0
18+
for checkerName := range availableCheckers {
19+
names[i] = checkerName
20+
i++
21+
}
22+
return names
23+
}
24+
25+
// RegisterChecker adds checker to be used later when checking CODEOWNERS files
26+
func RegisterChecker(name string, checker Checker) error {
27+
_, found := availableCheckers[name]
28+
if found {
29+
return fmt.Errorf("Checker %s already exists", name)
30+
}
31+
availableCheckers[name] = checker
32+
return nil
33+
}
34+
35+
// Checker provides tools for validating CODEOWNER file contents
36+
type Checker interface {
37+
CheckLine(lineNo int, filePattern string, owners ...string) []CheckResult
38+
}
39+
40+
// SeverityLevel exposes all possible levels of severity check results
41+
type SeverityLevel int
42+
43+
// All possible severiy levels
44+
const (
45+
Error SeverityLevel = iota // Error serverity level
46+
Warning // Warning serverity level
47+
)
48+
49+
// String returns the string representation of this severity level
50+
func (l SeverityLevel) String() string {
51+
return [...]string{"Error", "Warning"}[l]
52+
}
53+
54+
// CheckResult provides structured way to evaluate results of a CODEOWNERS validation check
55+
type CheckResult struct {
56+
LineNo int
57+
Message string
58+
Severity SeverityLevel
59+
CheckName string
60+
}
61+
62+
// Check evaluates the file contents against the checkers and return the results back.
63+
func Check(r io.Reader, checkers ...string) ([]CheckResult, error) {
64+
results := []CheckResult{}
65+
decoder := NewDecoder(r)
66+
for decoder.More() {
67+
token, lineNo := decoder.Token()
68+
for _, checker := range checkers {
69+
c, ok := availableCheckers[checker]
70+
if !ok {
71+
return nil, fmt.Errorf("'%s' not found", checker)
72+
}
73+
lineResults := c.CheckLine(lineNo, token.Path(), token.Owners()...)
74+
results = append(results, lineResults...)
75+
}
76+
}
77+
78+
return results, nil
79+
}

checker_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package codeowners_test
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"strings"
7+
"testing"
8+
9+
"github.com/fmenezes/codeowners"
10+
_ "github.com/fmenezes/codeowners/checkers"
11+
)
12+
13+
const dummyCheckerName string = "dummy"
14+
15+
type dummyChecker struct {
16+
}
17+
18+
func (c dummyChecker) CheckLine(lineNo int, filePath string, owners ...string) []codeowners.CheckResult {
19+
return []codeowners.CheckResult{
20+
{
21+
LineNo: 1,
22+
Message: "Dummy Error",
23+
Severity: codeowners.Error,
24+
CheckName: dummyCheckerName,
25+
},
26+
}
27+
}
28+
29+
func TestRegisterChecker(t *testing.T) {
30+
err := codeowners.RegisterChecker(dummyCheckerName, dummyChecker{})
31+
if err != nil {
32+
t.Error(err)
33+
}
34+
found := false
35+
for _, checker := range codeowners.AvailableCheckers() {
36+
if checker == dummyCheckerName {
37+
found = true
38+
}
39+
}
40+
if !found {
41+
t.Errorf("%s not properly registered", dummyCheckerName)
42+
}
43+
}
44+
45+
func TestRegisterCheckerAgain(t *testing.T) {
46+
codeowners.RegisterChecker(dummyCheckerName, dummyChecker{})
47+
err := codeowners.RegisterChecker(dummyCheckerName, dummyChecker{})
48+
if err == nil {
49+
t.Errorf("%s should be already registered, expecting an error", dummyCheckerName)
50+
}
51+
}
52+
53+
func TestSeverityLevelLabels(t *testing.T) {
54+
if codeowners.Error.String() != "Error" {
55+
t.Errorf("codeowners.Error.String() should evaluate to 'Error'")
56+
}
57+
if codeowners.Warning.String() != "Warning" {
58+
t.Errorf("codeowners.Warning.String() should evaluate to 'Warning'")
59+
}
60+
}
61+
62+
func TestSimpleCheck(t *testing.T) {
63+
input := `filepattern @owner`
64+
want := []codeowners.CheckResult{
65+
{
66+
LineNo: 1,
67+
Message: "Dummy Error",
68+
Severity: codeowners.Error,
69+
CheckName: dummyCheckerName,
70+
},
71+
}
72+
73+
codeowners.RegisterChecker(dummyCheckerName, dummyChecker{})
74+
got, err := codeowners.Check(strings.NewReader(input), dummyCheckerName)
75+
if err != nil {
76+
t.Errorf("Input %s, Error %v", input, err)
77+
}
78+
if !reflect.DeepEqual(want, got) {
79+
t.Errorf("Input %s, Want %v, Got %v", input, want, got)
80+
}
81+
}
82+
83+
func ExampleCheck() {
84+
contents := strings.NewReader(`filepattern`)
85+
checks, err := codeowners.Check(contents, "NoOwner")
86+
if err != nil {
87+
88+
}
89+
for _, check := range checks {
90+
fmt.Printf("%d ::%s:: %s [%s]\n", check.LineNo, check.Severity, check.Message, check.CheckName)
91+
}
92+
//Output:
93+
//1 ::Error:: No owners specified [NoOwner]
94+
}

checkers/checkers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Package checkers contain pre built checkers to validate CODEOWNER files
2+
package checkers

checkers/noowner.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package checkers
2+
3+
import "github.com/fmenezes/codeowners"
4+
5+
const noOwnerCheckerName string = "NoOwner"
6+
7+
func init() {
8+
codeowners.RegisterChecker(noOwnerCheckerName, NoOwner{})
9+
}
10+
11+
// NoOwner represents checker to decide validate presence of owners in each of CODEOWNERS lines
12+
type NoOwner struct{}
13+
14+
// CheckLine runs this NoOwner's check against each line
15+
func (c NoOwner) CheckLine(lineNo int, pattern string, owners ...string) []codeowners.CheckResult {
16+
results := []codeowners.CheckResult{}
17+
18+
if len(owners) == 0 {
19+
results = append(results, codeowners.CheckResult{
20+
LineNo: lineNo,
21+
Message: "No owners specified",
22+
Severity: codeowners.Error,
23+
CheckName: noOwnerCheckerName,
24+
})
25+
26+
}
27+
return results
28+
}

checkers/noowner_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package checkers_test
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"github.com/fmenezes/codeowners"
8+
"github.com/fmenezes/codeowners/checkers"
9+
)
10+
11+
func TestNoOwnerCheck(t *testing.T) {
12+
input := struct {
13+
lineNo int
14+
pattern string
15+
owners []string
16+
}{
17+
lineNo: 1,
18+
pattern: "filepattern",
19+
owners: []string{},
20+
}
21+
want := []codeowners.CheckResult{
22+
{
23+
LineNo: 1,
24+
Message: "No owners specified",
25+
Severity: codeowners.Error,
26+
CheckName: "NoOwner",
27+
},
28+
}
29+
30+
checker := checkers.NoOwner{}
31+
got := checker.CheckLine(input.lineNo, input.pattern, input.owners...)
32+
if !reflect.DeepEqual(got, want) {
33+
t.Errorf("Input: %v, Want: %v, Got: %v", input, want, got)
34+
}
35+
}

0 commit comments

Comments
 (0)