Skip to content

Commit

Permalink
Support reading per repository configurations from .gopherci.yml
Browse files Browse the repository at this point in the history
Users should be able to apply per repository configuration for GopherCI via
a config file in the repository, this allows non-admin users to modify
GopherCI on a per repository basis if they have write access. Which practically
means users can also have their pull request modify GopherCI if they require
additional packages to be installed (for cgo for example) or if they want to
modify the tools being run.

We achieve this by including a YAMLConfig which implements a configReader
interface, an interface was chosen primarily to keep tests simpler, but
the analyser doesn't really care where the config came from, just that it's
able to receive one.

The YAMLConfig should be preloaded with the global settings and then it should
filter the config based on reading the repository's config file, and finally
returning the config to analyser to use.

Relates #8.
  • Loading branch information
bradleyfalzon committed May 17, 2017
1 parent 7609790 commit 32a8f39
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 10 deletions.
10 changes: 8 additions & 2 deletions internal/analyser/analyser.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (e *NonZeroError) Error() string {
// Analyse downloads a repository set in config in an environment provided by
// analyser, running the series of tools. Writes results to provided analysis,
// or an error. The repository is expected to contain at least one Go package.
func Analyse(ctx context.Context, analyser Analyser, cloner Cloner, tools []db.Tool, config Config, analysis *db.Analysis) error {
func Analyse(ctx context.Context, analyser Analyser, cloner Cloner, configReader ConfigReader, config Config, analysis *db.Analysis) error {
start := time.Now()
defer func() {
analysis.TotalDuration = db.Duration(time.Since(start))
Expand All @@ -89,6 +89,12 @@ func Analyse(ctx context.Context, analyser Analyser, cloner Cloner, tools []db.T
}
analysis.CloneDuration = db.Duration(time.Since(deltaStart))

// read repository's configuration
repoConfig, err := configReader.Read(ctx, exec)
if err != nil {
return errors.WithMessage(err, "could not configure repository")
}

// create a unified diff for use by revgrep
patch, err := getPatch(ctx, exec, config.BaseRef, config.HeadRef)
if err != nil {
Expand All @@ -115,7 +121,7 @@ func Analyse(ctx context.Context, analyser Analyser, cloner Cloner, tools []db.T
}
pwd := string(bytes.TrimSpace(out))

for _, tool := range tools {
for _, tool := range repoConfig.Tools {
deltaStart = time.Now()
args := []string{tool.Path}
for _, arg := range strings.Fields(tool.Args) {
Expand Down
27 changes: 20 additions & 7 deletions internal/analyser/analyser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,22 @@ func (c *mockCloner) Clone(context.Context, Executer) error {
return nil
}

type mockConfig struct {
RepoConfig RepoConfig
}

var _ ConfigReader = &mockConfig{}

func (c *mockConfig) Read(context.Context, Executer) (RepoConfig, error) {
return c.RepoConfig, nil
}

func TestAnalyse(t *testing.T) {
cfg := Config{
BaseRef: "base-branch",
HeadRef: "head-branch",
}

tools := []db.Tool{
{ID: 1, Name: "Name1", Path: "tool1", Args: "-flag %BASE_BRANCH% ./..."},
{ID: 2, Name: "Name2", Path: "tool2"},
{ID: 3, Name: "Name2", Path: "tool3"},
}

diff := []byte(`diff --git a/subdir/main.go b/subdir/main.go
new file mode 100644
index 0000000..6362395
Expand Down Expand Up @@ -90,8 +94,17 @@ index 0000000..6362395
mockDB := db.NewMockDB()
analysis, _ := mockDB.StartAnalysis(1, 2)
cloner := &mockCloner{}
configReader := &mockConfig{
RepoConfig{
Tools: []db.Tool{
{ID: 1, Name: "Name1", Path: "tool1", Args: "-flag %BASE_BRANCH% ./..."},
{ID: 2, Name: "Name2", Path: "tool2"},
{ID: 3, Name: "Name2", Path: "tool3"},
},
},
}

err := Analyse(context.Background(), analyser, cloner, tools, cfg, analysis)
err := Analyse(context.Background(), analyser, cloner, configReader, cfg, analysis)
if err != nil {
t.Fatal("unexpected error:", err)
}
Expand Down
53 changes: 53 additions & 0 deletions internal/analyser/configReader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package analyser

import (
"context"

yaml "gopkg.in/yaml.v1"

"github.com/bradleyfalzon/gopherci/internal/db"
"github.com/pkg/errors"
)

// RepoConfig contains the analyser configuration for the repository.
type RepoConfig struct {
Tools []db.Tool
}

// A ConfigReader returns a repository's configuration.
type ConfigReader interface {
Read(context.Context, Executer) (RepoConfig, error)
}

// YAMLConfig implements a ConfigReader by reading a yaml configuration file
// from the repositories root.
type YAMLConfig struct {
Tools []db.Tool // Preset tools to use, before per repo config has been applied
}

var _ ConfigReader = &YAMLConfig{}

// Read implements the ConfigReader interface.
func (c *YAMLConfig) Read(ctx context.Context, exec Executer) (RepoConfig, error) {
cfg := RepoConfig{
Tools: c.Tools,
}

const configFilename = ".gopherci.yml"

args := []string{"cat", configFilename}
yml, err := exec.Execute(ctx, args)
switch err.(type) {
case nil:
case *NonZeroError:
return cfg, nil
default:
return cfg, errors.Wrapf(err, "could not read %s", configFilename)
}

if err = yaml.Unmarshal(yml, &cfg); err != nil {
return cfg, errors.Wrapf(err, "could not unmarshal %s", configFilename)
}

return cfg, nil
}
80 changes: 80 additions & 0 deletions internal/analyser/configReader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package analyser

import (
"context"
"errors"
"reflect"
"testing"

"github.com/bradleyfalzon/gopherci/internal/db"
)

func TestYAMLConfig_default(t *testing.T) {
exec := &mockAnalyser{
ExecuteOut: [][]byte{{}},
ExecuteErr: []error{&NonZeroError{ExitCode: 1}},
}

reader := &YAMLConfig{
Tools: []db.Tool{{Name: "tool1"}},
}
have, err := reader.Read(context.Background(), exec)
if err != nil {
t.Errorf("unexpected error: %v", err)
}

want := RepoConfig{Tools: reader.Tools}

if !reflect.DeepEqual(have, want) {
t.Errorf("\nhave: %v\nwant: %v", have, want)
}
}

func TestYAMLConfig_unknownError(t *testing.T) {
exec := &mockAnalyser{
ExecuteOut: [][]byte{{}},
ExecuteErr: []error{errors.New("unknown error")},
}

reader := &YAMLConfig{}
_, err := reader.Read(context.Background(), exec)
if err == nil {
t.Errorf("expected error, have: %v", err)
}
}

func TestYAMLConfig_unmarshalError(t *testing.T) {
contents := []byte("\t")
exec := &mockAnalyser{
ExecuteOut: [][]byte{contents},
ExecuteErr: []error{nil},
}

reader := &YAMLConfig{}
_, err := reader.Read(context.Background(), exec)
if err == nil {
t.Errorf("expected error, have: %v", err)
}
}

func TestYAMLConfig(t *testing.T) {
contents := []byte(`# .gopherci.yml config`)
exec := &mockAnalyser{
ExecuteOut: [][]byte{contents},
ExecuteErr: []error{nil},
}

reader := &YAMLConfig{
Tools: []db.Tool{{Name: "tool1"}},
}
have, err := reader.Read(context.Background(), exec)
if err != nil {
t.Errorf("unexpected error: %v", err)
}

want := RepoConfig{Tools: reader.Tools}

if !reflect.DeepEqual(have, want) {
t.Errorf("\nhave: %v\nwant: %v", have, want)
}
}
6 changes: 5 additions & 1 deletion internal/github/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,11 @@ func (g *GitHub) Analyse(cfg AnalyseConfig) (err error) {
GoSrcPath: cfg.goSrcPath,
}

err = analyser.Analyse(ctx, g.analyser, cfg.cloner, tools, acfg, analysis)
configReader := &analyser.YAMLConfig{
Tools: tools,
}

err = analyser.Analyse(ctx, g.analyser, cfg.cloner, configReader, acfg, analysis)
if err != nil {
return errors.Wrap(err, "could not run analyser")
}
Expand Down

0 comments on commit 32a8f39

Please sign in to comment.