diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2017-07-07 11:30:47 +0200 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2017-07-07 11:33:00 +0200 |
| commit | 79933615f7988cb731baa0768a45b39008f18b20 (patch) | |
| tree | ab2199aa7940fdbdca7c6e6e0faaf47854342ef5 | |
| parent | 58a4af83e0caf09744dd698b5be937205b81e733 (diff) | |
| download | gofu-79933615f7988cb731baa0768a45b39008f18b20.tar.gz | |
add first test
| -rw-r--r-- | Makefile | 29 | ||||
| -rw-r--r-- | pkg/cli/command.go | 1 | ||||
| -rw-r--r-- | pkg/cli/interface.go | 11 | ||||
| -rw-r--r-- | pkg/cli/query.go | 12 | ||||
| -rw-r--r-- | pkg/rtree/get_test.go | 42 | ||||
| -rw-r--r-- | pkg/rtree/main.go | 7 | ||||
| -rw-r--r-- | pkg/rtree/shared_test.go | 126 | ||||
| -rwxr-xr-x | util/gocovcat.go | 87 |
8 files changed, 307 insertions, 8 deletions
@@ -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) + } +} |
