Skip to content

Commit

Permalink
Add CLI (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
fmenezes authored Jul 28, 2020
1 parent 8a95900 commit 98509d0
Show file tree
Hide file tree
Showing 17 changed files with 318 additions and 24 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
run: go build -v .

- name: Test
run: go test -v -race -covermode atomic -coverprofile=profile.cov ./...
run: go test -v -race -covermode atomic -coverprofile=profile.cov -tags=unit ./...

- name: Send coverage
env:
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/fmenezes/codeowners)](https://goreportcard.com/report/github.com/fmenezes/codeowners)
[![Coverage](https://coveralls.io/repos/github/fmenezes/codeowners/badge.svg?branch=master)](https://coveralls.io/github/fmenezes/codeowners?branch=master)

# CODEOWNERS Decoder
# CODEOWNERS

CodeOwners decoder provides funcionality to evaluate CODEOWNERS file in Go.
CodeOwners coder provides funcionality to evaluate CODEOWNERS file in Go. Also provices linter CLI.

## Documentation

To find documentation follow https://godoc.org/github.com/fmenezes/codeowners
To find package documentation follow https://godoc.org/github.com/fmenezes/codeowners

## Compatibility

Expand Down
60 changes: 55 additions & 5 deletions checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package codeowners

import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
)

var availableCheckers map[string]Checker
Expand Down Expand Up @@ -60,9 +62,21 @@ type CheckResult struct {
}

// Check evaluates the file contents against the checkers and return the results back.
func Check(r io.Reader, checkers ...string) ([]CheckResult, error) {
func Check(directory string, checkers ...string) ([]CheckResult, error) {

fileLocation, result := findCodeownersFile(directory)
if result != nil {
return []CheckResult{*result}, nil
}

file, err := os.Open(fileLocation)
if err != nil {
return nil, err
}
defer file.Close()

results := []CheckResult{}
decoder := NewDecoder(r)
decoder := NewDecoder(file)
for decoder.More() {
token, lineNo := decoder.Token()
for _, checker := range checkers {
Expand All @@ -71,9 +85,45 @@ func Check(r io.Reader, checkers ...string) ([]CheckResult, error) {
return nil, fmt.Errorf("'%s' not found", checker)
}
lineResults := c.CheckLine(lineNo, token.Path(), token.Owners()...)
results = append(results, lineResults...)
if lineResults != nil {
results = append(results, lineResults...)
}
}
}

return results, nil
if len(results) > 0 {
return results, nil
}

return nil, nil
}

func fileExists(file string) bool {
info, err := os.Stat(file)
return !os.IsNotExist(err) && !info.IsDir()
}

func findCodeownersFile(dir string) (string, *CheckResult) {
codeownersLocation := ""

filesFound := []string{}
for _, fileLocation := range DefaultLocations {
currentFile := filepath.Join(dir, fileLocation)
if fileExists(currentFile) {
filesFound = append(filesFound, fileLocation)
if len(codeownersLocation) == 0 {
codeownersLocation = currentFile
}
}
}

if len(filesFound) == 0 {
return "", &CheckResult{Message: "No CODEOWNERS file found", Severity: Error, CheckName: "NoCodeowners"}
}

if len(filesFound) > 1 {
return "", &CheckResult{Message: fmt.Sprintf("Multiple CODEOWNERS files found (%s)", strings.Join(filesFound, ", ")), Severity: Warning, CheckName: "MultipleCodeowners"}
}

return codeownersLocation, nil
}
71 changes: 64 additions & 7 deletions checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package codeowners_test
import (
"fmt"
"reflect"
"strings"
"testing"

"github.com/fmenezes/codeowners"
Expand Down Expand Up @@ -60,7 +59,7 @@ func TestSeverityLevelLabels(t *testing.T) {
}

func TestSimpleCheck(t *testing.T) {
input := `filepattern @owner`
input := "./test/data/pass"
want := []codeowners.CheckResult{
{
LineNo: 1,
Expand All @@ -71,7 +70,7 @@ func TestSimpleCheck(t *testing.T) {
}

codeowners.RegisterChecker(dummyCheckerName, dummyChecker{})
got, err := codeowners.Check(strings.NewReader(input), dummyCheckerName)
got, err := codeowners.Check(input, dummyCheckerName)
if err != nil {
t.Errorf("Input %s, Error %v", input, err)
}
Expand All @@ -80,15 +79,73 @@ func TestSimpleCheck(t *testing.T) {
}
}

func ExampleCheck() {
contents := strings.NewReader(`filepattern`)
checks, err := codeowners.Check(contents, "NoOwner")
func TestNoProblemsFound(t *testing.T) {
input := "./test/data/pass"
got, err := codeowners.Check(input)
if err != nil {
t.Errorf("Input %s, Error %v", input, err)
}
if got != nil {
t.Errorf("Input %s, Want %v, Got %v", input, nil, got)
}
}

func TestCheckerNotFound(t *testing.T) {
input := "./test/data/pass"
_, err := codeowners.Check(input, "NonExistentChecker")
if err == nil {
t.Error("Should have errored")
}
}

func TestNoCodeownersCheck(t *testing.T) {
input := "./test/data"
want := []codeowners.CheckResult{
{
LineNo: 0,
Message: "No CODEOWNERS file found",
Severity: codeowners.Error,
CheckName: "NoCodeowners",
},
}

got, err := codeowners.Check(input, dummyCheckerName)
if err != nil {
t.Errorf("Input %s, Error %v", input, err)
}
if !reflect.DeepEqual(want, got) {
t.Errorf("Input %s, Want %v, Got %v", input, want, got)
}
}

func TestMultipleCodeownersCheck(t *testing.T) {
input := "./test/data/multiple_codeowners"
want := []codeowners.CheckResult{
{
LineNo: 0,
Message: "Multiple CODEOWNERS files found (CODEOWNERS, docs/CODEOWNERS)",
Severity: codeowners.Warning,
CheckName: "MultipleCodeowners",
},
}

got, err := codeowners.Check(input, dummyCheckerName)
if err != nil {
t.Errorf("Input %s, Error %v", input, err)
}
if !reflect.DeepEqual(want, got) {
t.Errorf("Input %s, Want %v, Got %v", input, want, got)
}
}

func ExampleCheck() {
checks, err := codeowners.Check(".", codeowners.AvailableCheckers()...)
if err != nil {
panic(err)
}
for _, check := range checks {
fmt.Printf("%d ::%s:: %s [%s]\n", check.LineNo, check.Severity, check.Message, check.CheckName)
}
//Output:
//1 ::Error:: No owners specified [NoOwner]
//0 ::Error:: No CODEOWNERS file found [NoCodeowners]
}
18 changes: 10 additions & 8 deletions checkers/noowner.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ type NoOwner struct{}

// CheckLine runs this NoOwner's check against each line
func (c NoOwner) CheckLine(lineNo int, pattern string, owners ...string) []codeowners.CheckResult {
results := []codeowners.CheckResult{}
var results []codeowners.CheckResult

if len(owners) == 0 {
results = append(results, codeowners.CheckResult{
LineNo: lineNo,
Message: "No owners specified",
Severity: codeowners.Error,
CheckName: noOwnerCheckerName,
})

results = []codeowners.CheckResult{
{
LineNo: lineNo,
Message: "No owners specified",
Severity: codeowners.Error,
CheckName: noOwnerCheckerName,
},
}
}

return results
}
46 changes: 46 additions & 0 deletions cmd/linter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"io"
"path/filepath"
"text/template"

"github.com/fmenezes/codeowners"
_ "github.com/fmenezes/codeowners/checkers"
)

type options struct {
directory string
format string
}

func run(wr io.Writer, opt options) error {
dir, err := filepath.Abs(opt.directory)
if err != nil {
return err
}

format := "{{range .}}{{ .LineNo }} ::{{ .Severity }}:: {{ .Message }} [{{ .CheckName }}]\n{{end}}"
if len(opt.format) > 0 {
format = opt.format
}
tpl, err := template.New("main").Parse(format)
if err != nil {
return err
}

checkers := codeowners.AvailableCheckers()

checks, _ := codeowners.Check(dir, checkers...)

if len(checks) > 0 {
err = tpl.Execute(wr, checks)
if err != nil {
return err
}
} else {
wr.Write([]byte("Everything ok ;)\n"))
}

return nil
}
108 changes: 108 additions & 0 deletions cmd/linter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package main

import (
"bytes"
"testing"
)

func testRun(opt options) (string, error) {
var output bytes.Buffer
err := run(&output, opt)
if err != nil {
return "", err
}
return output.String(), nil
}

func TestPass(t *testing.T) {
opt := options{
directory: "../test/data/pass",
format: "",
}

got, err := testRun(opt)
if err != nil {
t.Error(err)
}

want := `Everything ok ;)
`
if got != want {
t.Errorf("Input: %v Want: '%s' Got: '%s'", opt, want, got)
}
}

func TestNoOwners(t *testing.T) {
opt := options{
directory: "../test/data/no_owners",
format: "",
}

got, err := testRun(opt)
if err != nil {
t.Error(err)
}

want := `1 ::Error:: No owners specified [NoOwner]
`
if got != want {
t.Errorf("Input: %v Want: '%s' Got: '%s'", opt, want, got)
}
}

func TestCustomFormat(t *testing.T) {
opt := options{
directory: "../test/data/noowners",
format: "test",
}

got, err := testRun(opt)
if err != nil {
t.Error(err)
}

want := `test`
if got != want {
t.Errorf("Input: %v Want: '%s' Got: '%s'", opt, want, got)
}
}

func TestInvalidFormat(t *testing.T) {
opt := options{
directory: "../test/data/noowners",
format: " {{template \"one\"}} ",
}

_, err := testRun(opt)
if err == nil {
t.Errorf("Should have errored")
}

opt = options{
directory: "../test/data/noowners",
format: " {{ . ",
}

_, err = testRun(opt)
if err == nil {
t.Errorf("Should have errored")
}
}

func TestInvalidDirectory(t *testing.T) {
opt := options{
directory: "'",
format: "",
}

got, err := testRun(opt)
if err != nil {
t.Error(err)
}

want := `0 ::Error:: No CODEOWNERS file found [NoCodeowners]
`
if got != want {
t.Errorf("Input: %v Want: '%s' Got: '%s'", opt, want, got)
}
}
Loading

0 comments on commit 98509d0

Please sign in to comment.