summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile29
-rw-r--r--pkg/cli/command.go1
-rw-r--r--pkg/cli/interface.go11
-rw-r--r--pkg/cli/query.go12
-rw-r--r--pkg/rtree/get_test.go42
-rw-r--r--pkg/rtree/main.go7
-rw-r--r--pkg/rtree/shared_test.go126
-rwxr-xr-xutil/gocovcat.go87
8 files changed, 307 insertions, 8 deletions
diff --git a/Makefile b/Makefile
index cc2c7ca..c1e786e 100644
--- a/Makefile
+++ b/Makefile
@@ -11,13 +11,40 @@ GO_LDFLAGS = -s -w
build/gofu: FORCE
$(GO) install $(GO_BUILDFLAGS) -ldflags '$(GO_LDFLAGS)' '$(PKG)'
-build/%:
+$(addprefix build/,$(APPLETS)):
ln -s gofu $@
install: FORCE all
install -D -m 0755 build/gofu "$(DESTDIR)$(PREFIX)/bin/gofu"
for APPLET in $(APPLETS); do ln -s gofu "$(DESTDIR)$(PREFIX)/bin/$${APPLET}"; done
+# which packages to test with static checkers?
+GO_ALLPKGS := $(PKG) $(shell go list $(PKG)/pkg/...)
+# which packages to test with `go test`?
+GO_TESTPKGS := $(shell go list -f '{{if .TestGoFiles}}{{.ImportPath}}{{end}}' $(PKG)/pkg/...)
+# which packages to measure coverage for?
+GO_COVERPKGS := $(shell go list $(PKG)/pkg/...)
+# output files from `go test`
+GO_COVERFILES := $(patsubst %,build/%.cover.out,$(subst /,_,$(GO_TESTPKGS)))
+
+# down below, I need to substitute spaces with commas; because of the syntax,
+# I have to get these separators from variables
+space := $(null) $(null)
+comma := ,
+
+check: all static-check build/cover.html FORCE
+ @echo -e "\e[1;32m>> All tests successful.\e[0m"
+static-check: FORCE
+ @if s="$$(gofmt -s -l *.go pkg 2>/dev/null)" && test -n "$$s"; then printf ' => %s\n%s\n' gofmt "$$s"; false; fi
+ @if s="$$(golint . && find pkg -type d -exec golint {} \; 2>/dev/null)" && test -n "$$s"; then printf ' => %s\n%s\n' golint "$$s"; false; fi
+ $(GO) vet $(GO_ALLPKGS)
+build/%.cover.out: FORCE
+ $(GO) test $(GO_BUILDFLAGS) -ldflags '$(GO_LDFLAGS)' -coverprofile=$@ -covermode=count -coverpkg=$(subst $(space),$(comma),$(GO_COVERPKGS)) $(subst _,/,$*)
+build/cover.out: $(GO_COVERFILES)
+ util/gocovcat.go $(GO_COVERFILES) > $@
+build/cover.html: build/cover.out
+ $(GO) tool cover -html $< -o $@
+
vendor: FORCE
golangvend
diff --git a/pkg/cli/command.go b/pkg/cli/command.go
index d07a120..a9938d1 100644
--- a/pkg/cli/command.go
+++ b/pkg/cli/command.go
@@ -54,6 +54,7 @@ func (e commandError) Error() string {
//suffices for all regular operation.
type CommandRunner func(c Command, stdin io.Reader, stdout, stderr io.Writer) error
+//DefaultCommandRunner is a CommandRunner that actually executes the command.
func DefaultCommandRunner(c Command, stdin io.Reader, stdout, stderr io.Writer) error {
cmd := exec.Command(c.Program[0], c.Program[1:]...)
cmd.Stdin = stdin
diff --git a/pkg/cli/interface.go b/pkg/cli/interface.go
index 0f1ba74..4d0e196 100644
--- a/pkg/cli/interface.go
+++ b/pkg/cli/interface.go
@@ -57,7 +57,6 @@ func SetupInterface(stdin io.Reader, stdout, stderr io.Writer, commandRunner Com
//Implementation wraps access to the CLI, including input, output and subprocesses.
type Implementation struct {
- //TODO: flag isStdinTerminal that disables color output and swaps out the TUI instance
stdin io.Reader
stdout io.Writer
stderr io.Writer
@@ -83,6 +82,10 @@ type TUI interface {
//Query displays a question and a set of answers and allows the user to select
//one of the answers. Returns the Return attribute of the selected Choice.
Query(prompt string, choices ...Choice) (string, error)
+ //Print writes the given string (potentially including ANSI escape codes) to
+ //the given writer. At this point, it can be decided whether to strip out the
+ //ANSI escape codes.
+ Print(w io.Writer, msg string)
}
func (i *Implementation) safeStdout() io.Writer {
@@ -145,17 +148,17 @@ func (i *Implementation) ShowResultsSorted(strs []string) {
//ShowProgress displays a progress message on stderr.
func (i *Implementation) ShowProgress(str string) {
- fmt.Fprintf(i.stderr, "\x1B[0;1;36m>>\x1B[0;36m %s\x1B[0m", strings.TrimSpace(str))
+ i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;36m>>\x1B[0;36m %s\x1B[0m", strings.TrimSpace(str)))
}
//ShowWarning displays a warning message on stderr.
func (i *Implementation) ShowWarning(str string) {
- fmt.Fprintf(i.stderr, "\x1B[0;1;33m!!\x1B[0;36m %s\x1B[0m", strings.TrimSpace(str))
+ i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;33m!!\x1B[0;36m %s\x1B[0m", strings.TrimSpace(str)))
}
//ShowError displays an error message on stderr.
func (i *Implementation) ShowError(str string) {
- fmt.Fprintf(i.stderr, "\x1B[0;1;31m!!\x1B[0;36m %s\x1B[0m", strings.TrimSpace(str))
+ i.tui.Print(i.stderr, fmt.Sprintf("\x1B[0;1;31m!!\x1B[0;36m %s\x1B[0m", strings.TrimSpace(str)))
}
//ShowUsage displays a usage synopsis on stderr.
diff --git a/pkg/cli/query.go b/pkg/cli/query.go
index d9aa9a2..aa8fa50 100644
--- a/pkg/cli/query.go
+++ b/pkg/cli/query.go
@@ -44,6 +44,10 @@ type terminalTUI struct {
i *Implementation
}
+func (t terminalTUI) Print(w io.Writer, msg string) {
+ w.Write([]byte(msg))
+}
+
func (t terminalTUI) ReadLine(prompt string) (string, error) {
if prompt != "" {
t.i.safeStdout().Write([]byte(strings.TrimSpace(prompt) + " "))
@@ -225,12 +229,18 @@ func (b *buffer) getNextInput() []byte {
}
////////////////////////////////////////////////////////////////////////////////
-// TUI implementation for when stdin is a pipe
+// TUI implementation for when stdin is a pipe (also used in unit tests)
type pipeTUI struct {
i *Implementation
}
+var ansiColorCodeRx = regexp.MustCompile("\x1B" + `\[[0-9;]*m`)
+
+func (t *pipeTUI) Print(w io.Writer, msg string) {
+ w.Write([]byte(ansiColorCodeRx.ReplaceAllString(msg, "")))
+}
+
func (t *pipeTUI) ReadLine(prompt string) (string, error) {
str, err := t.i.stdinBuf.ReadString('\n')
str = strings.TrimSpace(str)
diff --git a/pkg/rtree/get_test.go b/pkg/rtree/get_test.go
new file mode 100644
index 0000000..be7e092
--- /dev/null
+++ b/pkg/rtree/get_test.go
@@ -0,0 +1,42 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package rtree
+
+import (
+ "path/filepath"
+ "testing"
+)
+
+func TestGetRepoFromIndex(t *testing.T) {
+ idx := Index{
+ Repos: []*Repo{
+ {
+ CheckoutPath: "github.com/git/git",
+ Remotes: []Remote{
+ {Name: "origin", URL: "gh:git/git"},
+ },
+ },
+ },
+ }
+ Test{
+ Args: []string{"get", "gh:git/git"},
+ Index: idx,
+ ExpectOutput: filepath.Join(RootPath, "/github.com/git/git") + "\n",
+ }.Run(t, "TestGetRepoFromIndex")
+}
diff --git a/pkg/rtree/main.go b/pkg/rtree/main.go
index f08126e..762f457 100644
--- a/pkg/rtree/main.go
+++ b/pkg/rtree/main.go
@@ -24,8 +24,11 @@ import (
"github.com/majewsky/gofu/pkg/cli"
)
-//Exec executes the rtree applet and does not return. The argument is os.Args
-//minus the leading "rtree" or "gofu rtree".
+//Exec executes the rtree applet and returns an exit code (0 for success, >0
+//for error). The argument is os.Args minus the leading "rtree" or "gofu
+//rtree". All side-effects (reading from stdin, writing to stdout/stderr,
+//executing other programs) pass through cli.Interface and can be intercepted
+//there for the purpose of testing.
func Exec(args []string) int {
if !Init() {
return 1
diff --git a/pkg/rtree/shared_test.go b/pkg/rtree/shared_test.go
new file mode 100644
index 0000000..209acb4
--- /dev/null
+++ b/pkg/rtree/shared_test.go
@@ -0,0 +1,126 @@
+/*******************************************************************************
+*
+* Copyright 2017 Stefan Majewsky <majewsky@gmx.net>
+*
+* This program is free software: you can redistribute it and/or modify it under
+* the terms of the GNU General Public License as published by the Free Software
+* Foundation, either version 3 of the License, or (at your option) any later
+* version.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT ANY
+* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see <http://www.gnu.org/licenses/>.
+*
+*******************************************************************************/
+
+package rtree
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+
+ "github.com/majewsky/gofu/pkg/cli"
+ yaml "gopkg.in/yaml.v2"
+)
+
+//Path to a directory where tests can put their index files.
+var indexTmpDir = filepath.Join(os.TempDir(), fmt.Sprintf("rtree-test-%d", os.Getpid()))
+
+func TestMain(m *testing.M) {
+ //make sure that test does not accidentally access user's actual rtree or index
+ os.Setenv("HOME", "")
+ os.Setenv("GOPATH", "")
+ //setup test configuration
+ RootPath = "/unittest/gopath/src"
+ RemoteAliases = []*RemoteAlias{
+ {Alias: "gh:", Replacement: "https://github.com/"},
+ {Alias: "my/", Replacement: "git@git.example.com:"},
+ }
+
+ exitCode := m.Run()
+
+ //shared teardown
+ os.RemoveAll(indexTmpDir)
+
+ os.Exit(exitCode)
+}
+
+//Test describes a call to Main(), the environment that's given to it, and the
+//assertions that are checked after the call returns.
+type Test struct {
+ Args []string
+ Input string
+ Index Index
+ ExpectFailure bool
+ ExpectOutput string
+ ExpectError string
+ ExpectIndex *Index //if nil, .Index will be used instead
+}
+
+func (test Test) Run(t *testing.T, testName string) {
+ //write index file, if any
+ IndexPath = filepath.Join(indexTmpDir, testName+".yaml")
+ if test.Index.Repos != nil {
+ err := test.Index.Write()
+ if err != nil {
+ t.Fatalf("%s: cannot write index to %s: %s", testName, IndexPath, err.Error())
+ }
+ }
+
+ //setup cli.Interface for test
+ var stdout bytes.Buffer
+ var stderr bytes.Buffer
+ cli.SetupInterface(bytes.NewReader([]byte(test.Input)), &stdout, &stderr, nil)
+
+ //check exit code
+ exitCode := Exec(test.Args)
+ switch {
+ case exitCode == 0 && test.ExpectFailure:
+ t.Errorf("%s: expected failure, but returned success", testName)
+ case exitCode != 0 && !test.ExpectFailure:
+ t.Errorf("%s: expected success, but returned failure", testName)
+ }
+
+ //check output
+ output := string(stdout.Bytes())
+ if output != test.ExpectOutput {
+ t.Errorf("%s: expected stdout %#v, but got %#v", testName, test.ExpectOutput, output)
+ }
+ output = string(stderr.Bytes())
+ if output != test.ExpectError {
+ t.Errorf("%s: expected stderr %#v, but got %#v", testName, test.ExpectError, output)
+ }
+
+ //check index
+ idx := &test.Index
+ if test.ExpectIndex != nil {
+ idx = test.ExpectIndex
+ }
+ expectedIdxStr, err := yaml.Marshal(idx)
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ actualIdxStr, err := ioutil.ReadFile(IndexPath)
+ if err != nil {
+ t.Fatalf("%s: could not read index from %s: %s", testName, IndexPath, err.Error())
+ }
+ if string(expectedIdxStr) != string(actualIdxStr) {
+ t.Errorf("%s: index does not match expectation after test; diff follows", testName)
+ cmd := exec.Command("diff", "-u", "-", IndexPath)
+ cmd.Stdin = bytes.NewReader([]byte(expectedIdxStr))
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err := cmd.Wait()
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ }
+}
diff --git a/util/gocovcat.go b/util/gocovcat.go
new file mode 100755
index 0000000..bb03f87
--- /dev/null
+++ b/util/gocovcat.go
@@ -0,0 +1,87 @@
+///usr/bin/env go run "$0" "$@"; exit $?
+
+// Copyright 2017 Luke Shumaker <lukeshu@parabola.nu>
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+// Command gocovcat combines multiple go cover runs, and prints the
+// result on stdout.
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+func handleErr(err error) {
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(1)
+ }
+}
+
+func main() {
+ modeBool := false
+ blocks := map[string]int{}
+ for _, filename := range os.Args[1:] {
+ file, err := os.Open(filename)
+ handleErr(err)
+ buf := bufio.NewScanner(file)
+ for buf.Scan() {
+ line := buf.Text()
+
+ if strings.HasPrefix(line, "mode: ") {
+ m := strings.TrimPrefix(line, "mode: ")
+ switch m {
+ case "set":
+ modeBool = true
+ case "count", "atomic":
+ // do nothing
+ default:
+ fmt.Fprintf(os.Stderr, "Unrecognized mode: %s\n", m)
+ os.Exit(1)
+ }
+ } else {
+ sp := strings.LastIndexByte(line, ' ')
+ block := line[:sp]
+ cntStr := line[sp+1:]
+ cnt, err := strconv.Atoi(cntStr)
+ handleErr(err)
+ blocks[block] += cnt
+ }
+ }
+ handleErr(buf.Err())
+ }
+ keys := make([]string, 0, len(blocks))
+ for key := range blocks {
+ keys = append(keys, key)
+ }
+ sort.Strings(keys)
+ modeStr := "count"
+ if modeBool {
+ modeStr = "set"
+ }
+ fmt.Printf("mode: %s\n", modeStr)
+ for _, block := range keys {
+ cnt := blocks[block]
+ if modeBool && cnt > 1 {
+ cnt = 1
+ }
+ fmt.Printf("%s %d\n", block, cnt)
+ }
+}