Repository: alecthomas/gometalinter
Pull request #293 (2017-06-14 19:32:22)

Replace deadline crutch again
Take 2 of #288, sorry about take 1.
I left the changes in a separate commit so it's easier to see what has changed. 

Testing manually with a deadline, I can see the correct value is printed. I've also added a test to cover the case that was broke by take 1. 

alecthomas: How's this PR looking?
dnephin: I was able to manually verify that the previous bug was fixed, but I don't have a test for it.

I'm not sure why the test (in the description) is giving me the default value instead of the flag value. My suspicious is that I'm missing something about how kingpin works. 
dnephin: Ok, I fixed the test. I changed `init()` to be `setupFlags(app)`, and in `main()` I pass in `kingpin.CommandLine` as the app. In the test I create a new app, add the flags, and the test passes.

This is ready for review.


Patch:
diff --git a/config.go b/config.go
index 4bc1c00..3f6bbca 100644
--- a/config.go
+++ b/config.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"encoding/json"
 	"runtime"
 	"text/template"
 	"time"
@@ -43,15 +44,30 @@ type Config struct { // nolint: aligncheck
 	DuplThreshold   int
 	Sort            []string
 	Test            bool
-	Deadline        time.Duration `json:"-"`
+	Deadline        jsonDuration
 	Errors          bool
 	JSON            bool
 	Checkstyle      bool
 	EnableGC        bool
 	Aggregate       bool
 	EnableAll       bool
+}
+
+type jsonDuration time.Duration
+
+func (td *jsonDuration) UnmarshalJSON(raw []byte) error {
+	var durationAsString string
+	if err := json.Unmarshal(raw, &durationAsString); err != nil {
+		return err
+	}
+	duration, err := time.ParseDuration(durationAsString)
+	*td = jsonDuration(duration)
+	return err
+}
 
-	DeadlineJSONCrutch string `json:"Deadline"`
+// Duration returns the value as a time.Duration
+func (td *jsonDuration) Duration() time.Duration {
+	return time.Duration(*td)
 }
 
 // Configuration defaults.
@@ -92,6 +108,24 @@ var (
 
 	linterTakesFilesGroupedByPackage = newStringSet("vet", "vetshadow")
 
+	linterTakesPackagePaths = newStringSet(
+		"errcheck",
+		"aligncheck",
+		"errcheck",
+		"gosimple",
+		"interfacer",
+		"megacheck",
+		"safesql",
+		"staticcheck",
+		"structcheck",
+		"test",
+		"testify",
+		"unconvert",
+		"unparam",
+		"unused",
+		"varcheck",
+	)
+
 	// Linter definitions.
 	linterDefinitions = map[string]string{
 		"aligncheck":  `aligncheck:^(?:[^:]+: )?(?P<path>.*?\.go):(?P<line>\d+):(?P<col>\d+):\s*(?P<message>.+)$`,
@@ -170,6 +204,6 @@ var (
 		MinConstLength:  3,
 		DuplThreshold:   50,
 		Sort:            []string{"none"},
-		Deadline:        time.Second * 30,
+		Deadline:        jsonDuration(time.Second * 30),
 	}
 )
diff --git a/execute.go b/execute.go
index 041f8a1..3a58ad0 100644
--- a/execute.go
+++ b/execute.go
@@ -82,15 +82,15 @@ func (l *linterState) Partitions() ([][]string, error) {
 	if err != nil {
 		return nil, err
 	}
-	parts := l.Linter.partitionStrategy(cmdArgs, l.paths)
-	if len(parts) == 0 {
-		return nil, fmt.Errorf("%s: no files to lint", l.Name)
+	parts, err := l.Linter.partitionStrategy(cmdArgs, l.paths)
+	if err != nil {
+		return nil, err
 	}
 	return parts, nil
 }
 
 func runLinters(linters map[string]*Linter, paths []string, concurrency int, exclude, include *regexp.Regexp) (chan *Issue, chan error) {
-	errch := make(chan error)
+	errch := make(chan error, len(linters))
 	concurrencych := make(chan bool, concurrency)
 	incomingIssues := make(chan *Issue, 1000000)
 	processedIssues := filterIssuesViaDirectives(
@@ -112,7 +112,7 @@ func runLinters(linters map[string]*Linter, paths []string, concurrency int, exc
 
 	wg := &sync.WaitGroup{}
 	for _, linter := range linters {
-		deadline := time.After(config.Deadline)
+		deadline := time.After(config.Deadline.Duration())
 		state := &linterState{
 			Linter:   linter,
 			issues:   incomingIssues,
diff --git a/linters.go b/linters.go
index bd19e0c..366b056 100644
--- a/linters.go
+++ b/linters.go
@@ -57,7 +57,7 @@ func LinterFromName(name string) *Linter {
 		SeverityOverride:  Severity(config.Severity[name]),
 		MessageOverride:   config.MessageOverride[name],
 		regex:             re,
-		partitionStrategy: getParitionStrategy(name),
+		partitionStrategy: getPartitionStrategy(name),
 	}
 }
 
diff --git a/main.go b/main.go
index 3fa755d..b741abf 100644
--- a/main.go
+++ b/main.go
@@ -25,43 +25,43 @@ var (
 	}
 )
 
-func init() {
-	kingpin.Flag("config", "Load JSON configuration from file.").Action(loadConfig).String()
-	kingpin.Flag("disable", "Disable previously enabled linters.").PlaceHolder("LINTER").Short('D').Action(disableAction).Strings()
-	kingpin.Flag("enable", "Enable previously disabled linters.").PlaceHolder("LINTER").Short('E').Action(enableAction).Strings()
-	kingpin.Flag("linter", "Define a linter.").PlaceHolder("NAME:COMMAND:PATTERN").StringMapVar(&config.Linters)
-	kingpin.Flag("message-overrides", "Override message from linter. {message} will be expanded to the original message.").PlaceHolder("LINTER:MESSAGE").StringMapVar(&config.MessageOverride)
-	kingpin.Flag("severity", "Map of linter severities.").PlaceHolder("LINTER:SEVERITY").StringMapVar(&config.Severity)
-	kingpin.Flag("disable-all", "Disable all linters.").Action(disableAllAction).Bool()
-	kingpin.Flag("enable-all", "Enable all linters.").Action(enableAllAction).Bool()
-	kingpin.Flag("format", "Output format.").PlaceHolder(config.Format).StringVar(&config.Format)
-	kingpin.Flag("vendored-linters", "Use vendored linters (recommended).").BoolVar(&config.VendoredLinters)
-	kingpin.Flag("fast", "Only run fast linters.").BoolVar(&config.Fast)
-	kingpin.Flag("install", "Attempt to install all known linters.").Short('i').BoolVar(&config.Install)
-	kingpin.Flag("update", "Pass -u to go tool when installing.").Short('u').BoolVar(&config.Update)
-	kingpin.Flag("force", "Pass -f to go tool when installing.").Short('f').BoolVar(&config.Force)
-	kingpin.Flag("download-only", "Pass -d to go tool when installing.").BoolVar(&config.DownloadOnly)
-	kingpin.Flag("debug", "Display messages for failed linters, etc.").Short('d').BoolVar(&config.Debug)
-	kingpin.Flag("concurrency", "Number of concurrent linters to run.").PlaceHolder(fmt.Sprintf("%d", runtime.NumCPU())).Short('j').IntVar(&config.Concurrency)
-	kingpin.Flag("exclude", "Exclude messages matching these regular expressions.").Short('e').PlaceHolder("REGEXP").StringsVar(&config.Exclude)
-	kingpin.Flag("include", "Include messages matching these regular expressions.").Short('I').PlaceHolder("REGEXP").StringsVar(&config.Include)
-	kingpin.Flag("skip", "Skip directories with this name when expanding '...'.").Short('s').PlaceHolder("DIR...").StringsVar(&config.Skip)
-	kingpin.Flag("vendor", "Enable vendoring support (skips 'vendor' directories and sets GO15VENDOREXPERIMENT=1).").BoolVar(&config.Vendor)
-	kingpin.Flag("cyclo-over", "Report functions with cyclomatic complexity over N (using gocyclo).").PlaceHolder("10").IntVar(&config.Cyclo)
-	kingpin.Flag("line-length", "Report lines longer than N (using lll).").PlaceHolder("80").IntVar(&config.LineLength)
-	kingpin.Flag("min-confidence", "Minimum confidence interval to pass to golint.").PlaceHolder(".80").FloatVar(&config.MinConfidence)
-	kingpin.Flag("min-occurrences", "Minimum occurrences to pass to goconst.").PlaceHolder("3").IntVar(&config.MinOccurrences)
-	kingpin.Flag("min-const-length", "Minimumum constant length.").PlaceHolder("3").IntVar(&config.MinConstLength)
-	kingpin.Flag("dupl-threshold", "Minimum token sequence as a clone for dupl.").PlaceHolder("50").IntVar(&config.DuplThreshold)
-	kingpin.Flag("sort", fmt.Sprintf("Sort output by any of %s.", strings.Join(sortKeys, ", "))).PlaceHolder("none").EnumsVar(&config.Sort, sortKeys...)
-	kingpin.Flag("tests", "Include test files for linters that support this option").Short('t').BoolVar(&config.Test)
-	kingpin.Flag("deadline", "Cancel linters if they have not completed within this duration.").PlaceHolder("30s").DurationVar(&config.Deadline)
-	kingpin.Flag("errors", "Only show errors.").BoolVar(&config.Errors)
-	kingpin.Flag("json", "Generate structured JSON rather than standard line-based output.").BoolVar(&config.JSON)
-	kingpin.Flag("checkstyle", "Generate checkstyle XML rather than standard line-based output.").BoolVar(&config.Checkstyle)
-	kingpin.Flag("enable-gc", "Enable GC for linters (useful on large repositories).").BoolVar(&config.EnableGC)
-	kingpin.Flag("aggregate", "Aggregate issues reported by several linters.").BoolVar(&config.Aggregate)
-	kingpin.CommandLine.GetFlag("help").Short('h')
+func setupFlags(app *kingpin.Application) {
+	app.Flag("config", "Load JSON configuration from file.").Action(loadConfig).String()
+	app.Flag("disable", "Disable previously enabled linters.").PlaceHolder("LINTER").Short('D').Action(disableAction).Strings()
+	app.Flag("enable", "Enable previously disabled linters.").PlaceHolder("LINTER").Short('E').Action(enableAction).Strings()
+	app.Flag("linter", "Define a linter.").PlaceHolder("NAME:COMMAND:PATTERN").StringMapVar(&config.Linters)
+	app.Flag("message-overrides", "Override message from linter. {message} will be expanded to the original message.").PlaceHolder("LINTER:MESSAGE").StringMapVar(&config.MessageOverride)
+	app.Flag("severity", "Map of linter severities.").PlaceHolder("LINTER:SEVERITY").StringMapVar(&config.Severity)
+	app.Flag("disable-all", "Disable all linters.").Action(disableAllAction).Bool()
+	app.Flag("enable-all", "Enable all linters.").Action(enableAllAction).Bool()
+	app.Flag("format", "Output format.").PlaceHolder(config.Format).StringVar(&config.Format)
+	app.Flag("vendored-linters", "Use vendored linters (recommended).").BoolVar(&config.VendoredLinters)
+	app.Flag("fast", "Only run fast linters.").BoolVar(&config.Fast)
+	app.Flag("install", "Attempt to install all known linters.").Short('i').BoolVar(&config.Install)
+	app.Flag("update", "Pass -u to go tool when installing.").Short('u').BoolVar(&config.Update)
+	app.Flag("force", "Pass -f to go tool when installing.").Short('f').BoolVar(&config.Force)
+	app.Flag("download-only", "Pass -d to go tool when installing.").BoolVar(&config.DownloadOnly)
+	app.Flag("debug", "Display messages for failed linters, etc.").Short('d').BoolVar(&config.Debug)
+	app.Flag("concurrency", "Number of concurrent linters to run.").PlaceHolder(fmt.Sprintf("%d", runtime.NumCPU())).Short('j').IntVar(&config.Concurrency)
+	app.Flag("exclude", "Exclude messages matching these regular expressions.").Short('e').PlaceHolder("REGEXP").StringsVar(&config.Exclude)
+	app.Flag("include", "Include messages matching these regular expressions.").Short('I').PlaceHolder("REGEXP").StringsVar(&config.Include)
+	app.Flag("skip", "Skip directories with this name when expanding '...'.").Short('s').PlaceHolder("DIR...").StringsVar(&config.Skip)
+	app.Flag("vendor", "Enable vendoring support (skips 'vendor' directories and sets GO15VENDOREXPERIMENT=1).").BoolVar(&config.Vendor)
+	app.Flag("cyclo-over", "Report functions with cyclomatic complexity over N (using gocyclo).").PlaceHolder("10").IntVar(&config.Cyclo)
+	app.Flag("line-length", "Report lines longer than N (using lll).").PlaceHolder("80").IntVar(&config.LineLength)
+	app.Flag("min-confidence", "Minimum confidence interval to pass to golint.").PlaceHolder(".80").FloatVar(&config.MinConfidence)
+	app.Flag("min-occurrences", "Minimum occurrences to pass to goconst.").PlaceHolder("3").IntVar(&config.MinOccurrences)
+	app.Flag("min-const-length", "Minimumum constant length.").PlaceHolder("3").IntVar(&config.MinConstLength)
+	app.Flag("dupl-threshold", "Minimum token sequence as a clone for dupl.").PlaceHolder("50").IntVar(&config.DuplThreshold)
+	app.Flag("sort", fmt.Sprintf("Sort output by any of %s.", strings.Join(sortKeys, ", "))).PlaceHolder("none").EnumsVar(&config.Sort, sortKeys...)
+	app.Flag("tests", "Include test files for linters that support this option").Short('t').BoolVar(&config.Test)
+	app.Flag("deadline", "Cancel linters if they have not completed within this duration.").PlaceHolder("30s").DurationVar((*time.Duration)(&config.Deadline))
+	app.Flag("errors", "Only show errors.").BoolVar(&config.Errors)
+	app.Flag("json", "Generate structured JSON rather than standard line-based output.").BoolVar(&config.JSON)
+	app.Flag("checkstyle", "Generate checkstyle XML rather than standard line-based output.").BoolVar(&config.Checkstyle)
+	app.Flag("enable-gc", "Enable GC for linters (useful on large repositories).").BoolVar(&config.EnableGC)
+	app.Flag("aggregate", "Aggregate issues reported by several linters.").BoolVar(&config.Aggregate)
+	app.GetFlag("help").Short('h')
 }
 
 func loadConfig(app *kingpin.Application, element *kingpin.ParseElement, ctx *kingpin.ParseContext) error {
@@ -74,9 +74,6 @@ func loadConfig(app *kingpin.Application, element *kingpin.ParseElement, ctx *ki
 	if err != nil {
 		return err
 	}
-	if config.DeadlineJSONCrutch != "" {
-		config.Deadline, err = time.ParseDuration(config.DeadlineJSONCrutch)
-	}
 	for _, disable := range config.Disable {
 		for i, enable := range config.Enable {
 			if enable == disable {
@@ -149,8 +146,10 @@ func formatSeverity() string {
 }
 
 func main() {
-	var pathsArg = kingpin.Arg("path", "Directories to lint. Defaults to \".\". <path>/... will recurse.").Strings()
-	kingpin.CommandLine.Help = fmt.Sprintf(`Aggregate and normalise the output of a whole bunch of Go linters.
+	pathsArg := kingpin.Arg("path", "Directories to lint. Defaults to \".\". <path>/... will recurse.").Strings()
+	app := kingpin.CommandLine
+	setupFlags(app)
+	app.Help = fmt.Sprintf(`Aggregate and normalise the output of a whole bunch of Go linters.
 
 PlaceHolder linters:
 
@@ -204,9 +203,9 @@ func processConfig(config *Config) (include *regexp.Regexp, exclude *regexp.Rege
 	kingpin.FatalIfError(err, "invalid format %q", config.Format)
 	formatTemplate = tmpl
 
+	// Linters are by their very nature, short lived, so disable GC.
+	// Reduced (user) linting time on kingpin from 0.97s to 0.64s.
 	if !config.EnableGC {
-		// Linters are by their very nature, short lived, so disable GC.
-		// Reduced (user) linting time on kingpin from 0.97s to 0.64s.
 		_ = os.Setenv("GOGC", "off")
 	}
 	if config.VendoredLinters && config.Install && config.Update {
@@ -392,7 +391,7 @@ func replaceWithMegacheck(enabled []string) []string {
 }
 
 func findVendoredLinters() string {
-	gopaths := strings.Split(getGoPath(), string(os.PathListSeparator))
+	gopaths := getGoPathList()
 	for _, home := range vendoredSearchPaths {
 		for _, p := range gopaths {
 			joined := append([]string{p, "src"}, home...)
@@ -416,6 +415,10 @@ func getGoPath() string {
 	return path
 }
 
+func getGoPathList() []string {
+	return strings.Split(getGoPath(), string(os.PathListSeparator))
+}
+
 // addPath appends p to paths and returns it if:
 // 1. p is not a blank string
 // 2. p doesn't already exist in paths
@@ -434,7 +437,7 @@ func addPath(p string, paths []string) []string {
 
 // Ensure all "bin" directories from GOPATH exists in PATH, as well as GOBIN if set.
 func configureEnvironment() {
-	gopaths := strings.Split(getGoPath(), string(os.PathListSeparator))
+	gopaths := getGoPathList()
 	paths := strings.Split(os.Getenv("PATH"), string(os.PathListSeparator))
 	gobin := os.Getenv("GOBIN")
 
diff --git a/parition.go b/parition.go
deleted file mode 100644
index a58e10e..0000000
--- a/parition.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package main
-
-import (
-	"path/filepath"
-)
-
-// MaxCommandBytes is the maximum number of bytes used when executing a command
-const MaxCommandBytes = 32000
-
-type partitionStrategy func([]string, []string) [][]string
-
-func getParitionStrategy(name string) partitionStrategy {
-	switch {
-	case linterTakesFiles.contains(name):
-		return partitionToMaxArgSizeWithFileGlobs
-	case linterTakesFilesGroupedByPackage.contains(name):
-		return partitionToPackageFileGlobs
-	}
-	return partitionToMaxArgSize
-}
-
-func packagesToFileGlobs(paths []string) []string {
-	filePaths := []string{}
-	for _, dir := range paths {
-		// ignore error because the glob pattern is hardcoded
-		paths, _ := filepath.Glob(filepath.Join(dir, "*.go"))
-		filePaths = append(filePaths, paths...)
-	}
-	return filePaths
-}
-
-func partitionToMaxArgSize(cmdArgs []string, paths []string) [][]string {
-	return partitionToMaxSize(cmdArgs, paths, MaxCommandBytes)
-}
-
-func partitionToMaxSize(cmdArgs []string, paths []string, maxSize int) [][]string {
-	partitions := newSizeParitioner(cmdArgs, maxSize)
-	for _, path := range paths {
-		partitions.add(path)
-	}
-	return partitions.end()
-}
-
-type sizePartitioner struct {
-	base    []string
-	parts   [][]string
-	current []string
-	size    int
-	max     int
-}
-
-func newSizeParitioner(base []string, max int) *sizePartitioner {
-	p := &sizePartitioner{base: base, max: max}
-	p.new()
-	return p
-}
-
-func (p *sizePartitioner) add(arg string) {
-	if p.size+len(arg)+1 > p.max {
-		p.new()
-	}
-	p.current = append(p.current, arg)
-	p.size += len(arg) + 1
-}
-
-func (p *sizePartitioner) new() {
-	p.end()
-	p.size = 0
-	p.current = []string{}
-	for _, arg := range p.base {
-		p.add(arg)
-	}
-}
-
-func (p *sizePartitioner) end() [][]string {
-	if len(p.current) > 0 {
-		p.parts = append(p.parts, p.current)
-	}
-	return p.parts
-}
-
-func partitionToMaxArgSizeWithFileGlobs(cmdArgs []string, paths []string) [][]string {
-	filePaths := packagesToFileGlobs(paths)
-	if len(filePaths) == 0 {
-		return nil
-	}
-	return partitionToMaxArgSize(cmdArgs, filePaths)
-}
-
-func partitionToPackageFileGlobs(cmdArgs []string, paths []string) [][]string {
-	parts := [][]string{}
-	for _, path := range paths {
-		filePaths := packagesToFileGlobs([]string{path})
-		if len(filePaths) == 0 {
-			continue
-		}
-		parts = append(parts, append(cmdArgs, filePaths...))
-	}
-	return parts
-}
diff --git a/partition.go b/partition.go
new file mode 100644
index 0000000..33355df
--- /dev/null
+++ b/partition.go
@@ -0,0 +1,143 @@
+package main
+
+import (
+	"fmt"
+	"path/filepath"
+)
+
+// MaxCommandBytes is the maximum number of bytes used when executing a command
+const MaxCommandBytes = 32000
+
+type partitionStrategy func([]string, []string) ([][]string, error)
+
+func getPartitionStrategy(name string) partitionStrategy {
+	switch {
+	case linterTakesFiles.contains(name):
+		return partitionToMaxArgSizeWithFileGlobs
+	case linterTakesFilesGroupedByPackage.contains(name):
+		return partitionToPackageFileGlobs
+	case linterTakesPackagePaths.contains(name):
+		return partitionToMaxArgSizeWithPackagePaths
+	}
+	return partitionToMaxArgSize
+}
+
+func pathsToFileGlobs(paths []string) ([]string, error) {
+	filePaths := []string{}
+	for _, dir := range paths {
+		paths, err := filepath.Glob(filepath.Join(dir, "*.go"))
+		if err != nil {
+			return nil, err
+		}
+		filePaths = append(filePaths, paths...)
+	}
+	return filePaths, nil
+}
+
+func partitionToMaxArgSize(cmdArgs []string, paths []string) ([][]string, error) {
+	return partitionToMaxSize(cmdArgs, paths, MaxCommandBytes), nil
+}
+
+func partitionToMaxSize(cmdArgs []string, paths []string, maxSize int) [][]string {
+	partitions := newSizePartitioner(cmdArgs, maxSize)
+	for _, path := range paths {
+		partitions.add(path)
+	}
+	return partitions.end()
+}
+
+type sizePartitioner struct {
+	base    []string
+	parts   [][]string
+	current []string
+	size    int
+	max     int
+}
+
+func newSizePartitioner(base []string, max int) *sizePartitioner {
+	p := &sizePartitioner{base: base, max: max}
+	p.new()
+	return p
+}
+
+func (p *sizePartitioner) add(arg string) {
+	if p.size+len(arg)+1 > p.max {
+		p.new()
+	}
+	p.current = append(p.current, arg)
+	p.size += len(arg) + 1
+}
+
+func (p *sizePartitioner) new() {
+	p.end()
+	p.size = 0
+	p.current = []string{}
+	for _, arg := range p.base {
+		p.add(arg)
+	}
+}
+
+func (p *sizePartitioner) end() [][]string {
+	if len(p.current) > 0 {
+		p.parts = append(p.parts, p.current)
+	}
+	return p.parts
+}
+
+func partitionToMaxArgSizeWithFileGlobs(cmdArgs []string, paths []string) ([][]string, error) {
+	filePaths, err := pathsToFileGlobs(paths)
+	if err != nil || len(filePaths) == 0 {
+		return nil, err
+	}
+	return partitionToMaxArgSize(cmdArgs, filePaths)
+}
+
+func partitionToPackageFileGlobs(cmdArgs []string, paths []string) ([][]string, error) {
+	parts := [][]string{}
+	for _, path := range paths {
+		filePaths, err := pathsToFileGlobs([]string{path})
+		if err != nil {
+			return nil, err
+		}
+		if len(filePaths) == 0 {
+			continue
+		}
+		parts = append(parts, append(cmdArgs, filePaths...))
+	}
+	return parts, nil
+}
+
+func partitionToMaxArgSizeWithPackagePaths(cmdArgs []string, paths []string) ([][]string, error) {
+	packagePaths, err := pathsToPackagePaths(paths)
+	if err != nil || len(packagePaths) == 0 {
+		return nil, err
+	}
+	return partitionToMaxArgSize(cmdArgs, packagePaths)
+}
+
+func pathsToPackagePaths(paths []string) ([]string, error) {
+	packages := []string{}
+
+	for _, path := range paths {
+		pkg, err := packageNameFromPath(path)
+		if err != nil {
+			return nil, err
+		}
+		packages = append(packages, pkg)
+	}
+	return packages, nil
+}
+
+func packageNameFromPath(path string) (string, error) {
+	if !filepath.IsAbs(path) {
+		return path, nil
+	}
+	for _, gopath := range getGoPathList() {
+		rel, err := filepath.Rel(filepath.Join(gopath, "src"), path)
+		if err != nil {
+			continue
+		}
+		return rel, nil
+	}
+	return "", fmt.Errorf("%s not in GOPATH", path)
+}


Test patch:
diff --git a/execute_test.go b/execute_test.go
index 4454310..15fb069 100644
--- a/execute_test.go
+++ b/execute_test.go
@@ -4,7 +4,6 @@ import (
 	"sort"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -28,20 +27,3 @@ func TestSortedIssues(t *testing.T) {
 	}
 	require.Equal(t, expected, actual)
 }
-
-func TestLinterStateParitions(t *testing.T) {
-	noPartitions := func(_ []string, _ []string) [][]string {
-		return nil
-	}
-
-	state := &linterState{
-		Linter: &Linter{
-			Name:              "thelinter",
-			partitionStrategy: noPartitions,
-			Command:           "go",
-		},
-	}
-
-	_, err := state.Partitions()
-	assert.EqualError(t, err, "thelinter: no files to lint")
-}
diff --git a/main_test.go b/main_test.go
index 6c73cf7..7742b89 100644
--- a/main_test.go
+++ b/main_test.go
@@ -5,9 +5,11 @@ import (
 	"os"
 	"path/filepath"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"gopkg.in/alecthomas/kingpin.v3-unstable"
 )
 
 func TestRelativePackagePath(t *testing.T) {
@@ -126,3 +128,30 @@ func TestPathFilter(t *testing.T) {
 		assert.Equal(t, testcase.expected, pathFilter(testcase.path), testcase.path)
 	}
 }
+
+func TestLoadConfigWithDeadline(t *testing.T) {
+	originalConfig := *config
+	defer func() { config = &originalConfig }()
+
+	tmpfile, err := ioutil.TempFile("", "test-config")
+	require.NoError(t, err)
+	defer os.Remove(tmpfile.Name())
+
+	_, err = tmpfile.Write([]byte(`{"Deadline": "3m"}`))
+	require.NoError(t, err)
+	require.NoError(t, tmpfile.Close())
+
+	filename := tmpfile.Name()
+	err = loadConfig(nil, &kingpin.ParseElement{Value: &filename}, nil)
+	require.NoError(t, err)
+
+	require.Equal(t, 3*time.Minute, config.Deadline.Duration())
+}
+
+func TestDeadlineFlag(t *testing.T) {
+	app := kingpin.New("test-app", "")
+	setupFlags(app)
+	_, err := app.Parse([]string{"--deadline", "2m"})
+	require.NoError(t, err)
+	require.Equal(t, 2*time.Minute, config.Deadline.Duration())
+}
diff --git a/parition_test.go b/partition_test.go
similarity index 68%
rename from parition_test.go
rename to partition_test.go
index bdba527..07ac305 100644
--- a/parition_test.go
+++ b/partition_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func TestParitionToMaxSize(t *testing.T) {
+func TestPartitionToMaxSize(t *testing.T) {
 	cmdArgs := []string{"/usr/bin/foo", "-c"}
 	paths := []string{"one", "two", "three", "four"}
 
@@ -38,7 +38,8 @@ func TestPartitionToPackageFileGlobs(t *testing.T) {
 		mkGoFile(t, dir, "other.go")
 	}
 
-	parts := partitionToPackageFileGlobs(cmdArgs, paths)
+	parts, err := partitionToPackageFileGlobs(cmdArgs, paths)
+	require.NoError(t, err)
 	expected := [][]string{
 		append(cmdArgs, packagePaths(paths[0], "file.go", "other.go")...),
 		append(cmdArgs, packagePaths(paths[1], "file.go", "other.go")...),
@@ -61,7 +62,8 @@ func TestPartitionToPackageFileGlobsNoFiles(t *testing.T) {
 
 	cmdArgs := []string{"/usr/bin/foo", "-c"}
 	paths := []string{filepath.Join(tmpdir, "one"), filepath.Join(tmpdir, "two")}
-	parts := partitionToPackageFileGlobs(cmdArgs, paths)
+	parts, err := partitionToPackageFileGlobs(cmdArgs, paths)
+	require.NoError(t, err)
 	assert.Len(t, parts, 0)
 }
 
@@ -72,6 +74,26 @@ func TestPartitionToMaxArgSizeWithFileGlobsNoFiles(t *testing.T) {
 
 	cmdArgs := []string{"/usr/bin/foo", "-c"}
 	paths := []string{filepath.Join(tmpdir, "one"), filepath.Join(tmpdir, "two")}
-	parts := partitionToMaxArgSizeWithFileGlobs(cmdArgs, paths)
+	parts, err := partitionToMaxArgSizeWithFileGlobs(cmdArgs, paths)
+	require.NoError(t, err)
 	assert.Len(t, parts, 0)
 }
+
+func TestPathsToPackagePaths(t *testing.T) {
+	root := "/fake/root"
+	defer fakeGoPath(t, root)()
+
+	packagePaths, err := pathsToPackagePaths([]string{
+		filepath.Join(root, "src", "example.com", "foo"),
+		"./relative/package",
+	})
+	require.NoError(t, err)
+	expected := []string{"example.com/foo", "./relative/package"}
+	assert.Equal(t, expected, packagePaths)
+}
+
+func fakeGoPath(t *testing.T, path string) func() {
+	oldpath := os.Getenv("GOPATH")
+	require.NoError(t, os.Setenv("GOPATH", path))
+	return func() { require.NoError(t, os.Setenv("GOPATH", oldpath)) }
+}