Repository: bpicode/fritzctl
Pull request #158 (2018-06-09 15:02:07)

Improve error message readability in CLI by printing causal stack lin…
…e-by-line

Relies on traversal of error chains for errors satisfying 'Cause() error' interface, as does e.g. github.com/pkg/errors. Heuristically removes duplicate error substrings.

codecov[bot]: # [Codecov](https://codecov.io/gh/bpicode/fritzctl/pull/158?src=pr&el=h1) Report
> Merging [#158](https://codecov.io/gh/bpicode/fritzctl/pull/158?src=pr&el=desc) into [master](https://codecov.io/gh/bpicode/fritzctl/commit/48212d7a1e21921bc031b886217e0da397a500b3?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/bpicode/fritzctl/pull/158/graphs/tree.svg?width=650&token=BE6YV2nOnc&height=150&src=pr)](https://codecov.io/gh/bpicode/fritzctl/pull/158?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master   #158   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files          99     99           
  Lines        2458   2494   +36     
=====================================
+ Hits         2458   2494   +36
```


| [Impacted Files](https://codecov.io/gh/bpicode/fritzctl/pull/158?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [cmd/temperature.go](https://codecov.io/gh/bpicode/fritzctl/pull/158/diff?src=pr&el=tree#diff-Y21kL3RlbXBlcmF0dXJlLmdv) | `100% <100%> (ø)` | :arrow_up: |
| [main.go](https://codecov.io/gh/bpicode/fritzctl/pull/158/diff?src=pr&el=tree#diff-bWFpbi5nbw==) | `100% <100%> (ø)` | :arrow_up: |
| [cmd/assertions.go](https://codecov.io/gh/bpicode/fritzctl/pull/158/diff?src=pr&el=tree#diff-Y21kL2Fzc2VydGlvbnMuZ28=) | `100% <100%> (ø)` | :arrow_up: |
| [cmd/certificate\_export.go](https://codecov.io/gh/bpicode/fritzctl/pull/158/diff?src=pr&el=tree#diff-Y21kL2NlcnRpZmljYXRlX2V4cG9ydC5nbw==) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/bpicode/fritzctl/pull/158?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/bpicode/fritzctl/pull/158?src=pr&el=footer). Last update [48212d7...bfd3332](https://codecov.io/gh/bpicode/fritzctl/pull/158?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/Gopkg.lock b/Gopkg.lock
index 81b4d14..527629a 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -43,12 +43,6 @@
   revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
   version = "v0.0.3"
 
-[[projects]]
-  name = "github.com/pkg/errors"
-  packages = ["."]
-  revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
-  version = "v0.8.0"
-
 [[projects]]
   name = "github.com/pmezard/go-difflib"
   packages = ["difflib"]
@@ -103,6 +97,6 @@
 [solve-meta]
   analyzer-name = "dep"
   analyzer-version = 1
-  inputs-digest = "692b67cac5d15d8276f5e716a6675a320bf397c9569550887ddabfa868d27e40"
+  inputs-digest = "c7cb275a6f308a02fafcfb7ee0a4835b142812c22b65fd450dc74b0f31d4b037"
   solver-name = "gps-cdcl"
   solver-version = 1
diff --git a/NOTICE b/NOTICE
index d08d4cf..0d7e98b 100644
--- a/NOTICE
+++ b/NOTICE
@@ -19,7 +19,6 @@ BSD 2-Clause License (FreeBSD/Simplified)
 The following software have components provided under the terms of this license:
 
 - blackfriday (from https://github.com/russross/blackfriday)
-- errors (from https://github.com/pkg/errors)
 
 ========================================================================
 BSD 3-Clause License (Revised)
diff --git a/cmd/assertions.go b/cmd/assertions.go
index 83150d3..56845e7 100644
--- a/cmd/assertions.go
+++ b/cmd/assertions.go
@@ -3,23 +3,20 @@ package cmd
 import (
 	"fmt"
 
-	"github.com/pkg/errors"
+	"github.com/bpicode/fritzctl/internal/errors"
 )
 
-// assertNoErr panics with log message if the argument error is not nil.
 func assertNoErr(err error, format string, args ...interface{}) {
 	wErr := errors.Wrapf(err, format, args...)
 	assertTrue(wErr == nil, wErr)
 }
 
-// assertMinLen panics with a log message if the slice passed as argument has a size smaller than expected.
-func assertMinLen(vals []string, num int, v ...interface{}) {
-	assertTrue(len(vals) >= num, v...)
+func assertMinLen(vals []string, num int, format string, v ...interface{}) {
+	assertTrue(len(vals) >= num, fmt.Errorf(format, v...))
 }
 
-// assertTrue fails with a log message if the value is not true.
-func assertTrue(val bool, v ...interface{}) {
+func assertTrue(val bool, err error) {
 	if !val {
-		panic(fmt.Sprint(v...))
+		panic(err)
 	}
 }
diff --git a/cmd/certificate_export.go b/cmd/certificate_export.go
index 5024b85..36ff6fb 100644
--- a/cmd/certificate_export.go
+++ b/cmd/certificate_export.go
@@ -6,13 +6,14 @@ import (
 	"crypto/x509"
 	"encoding/hex"
 	"encoding/pem"
+	"errors"
 	"fmt"
 	"io"
 	"os"
 
 	"github.com/bpicode/fritzctl/config"
 	"github.com/bpicode/fritzctl/console"
-	"github.com/bpicode/fritzctl/stringutils"
+	"github.com/bpicode/fritzctl/internal/stringutils"
 	"github.com/spf13/cobra"
 )
 
@@ -54,7 +55,7 @@ func mustConnect(cfg *config.Config) *tls.Conn {
 
 func mustHaveCert(conn *tls.Conn) *x509.Certificate {
 	state := conn.ConnectionState()
-	assertTrue(len(state.PeerCertificates) > 0, "certificate export failed, list of peer certificates is empty")
+	assertTrue(len(state.PeerCertificates) > 0, errors.New("certificate export failed, list of peer certificates is empty"))
 	crt := state.PeerCertificates[len(state.PeerCertificates)-1]
 	return crt
 }
diff --git a/cmd/list_groups.go b/cmd/list_groups.go
index 876f94f..87239ab 100644
--- a/cmd/list_groups.go
+++ b/cmd/list_groups.go
@@ -7,7 +7,7 @@ import (
 
 	"github.com/bpicode/fritzctl/console"
 	"github.com/bpicode/fritzctl/fritz"
-	"github.com/bpicode/fritzctl/stringutils"
+	"github.com/bpicode/fritzctl/internal/stringutils"
 	"github.com/spf13/cobra"
 )
 
diff --git a/cmd/temperature.go b/cmd/temperature.go
index 2ccee4c..1a3a305 100644
--- a/cmd/temperature.go
+++ b/cmd/temperature.go
@@ -87,7 +87,7 @@ func changeByCallback(supplier func(t fritz.Thermostat) string, names ...string)
 	assertNoErr(err, "cannot list available devices")
 	for _, name := range names {
 		device := deviceWithName(name, devices.Thermostats())
-		assertTrue(device != nil, fmt.Sprintf("device with name '%s' not found", name))
+		assertTrue(device != nil, fmt.Errorf("device with name '%s' not found", name))
 		changeByValue(c, supplier(device.Thermostat), name)
 	}
 }
diff --git a/config/configfiles.go b/config/configfiles.go
index 63d1134..d05f3ab 100644
--- a/config/configfiles.go
+++ b/config/configfiles.go
@@ -6,7 +6,7 @@ import (
 	"os/user"
 	"path"
 
-	"github.com/pkg/errors"
+	"github.com/bpicode/fritzctl/internal/errors"
 )
 
 var (
diff --git a/config/fritzclientconfig.go b/config/fritzclientconfig.go
index 5973f9d..0ced77c 100644
--- a/config/fritzclientconfig.go
+++ b/config/fritzclientconfig.go
@@ -5,8 +5,8 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/bpicode/fritzctl/internal/errors"
 	"github.com/bpicode/fritzctl/logger"
-	"github.com/pkg/errors"
 )
 
 // Config stores client configuration of your FRITZ!Box
diff --git a/config/parser.go b/config/parser.go
index bc0a20f..312732b 100644
--- a/config/parser.go
+++ b/config/parser.go
@@ -2,14 +2,14 @@ package config
 
 import (
 	"encoding/json"
-	"errors"
+	"fmt"
 	"io"
 	"io/ioutil"
 	"os"
 	"os/user"
 	"strings"
 
-	errors2 "github.com/pkg/errors"
+	"github.com/bpicode/fritzctl/internal/errors"
 	"gopkg.in/yaml.v2"
 )
 
@@ -59,7 +59,7 @@ func (h *homeDirOpenCloser) Open() (io.Reader, error) {
 	hd := homeDirOf(h.user)
 	path, err := hd(h.path)
 	if err != nil {
-		return nil, errors2.Wrapf(err, "cannot open '%s' in current user's home directory", h.path)
+		return nil, errors.Wrapf(err, "cannot open '%s' in current user's home directory", h.path)
 	}
 	f, err := os.Open(path)
 	h.Closer = f
@@ -128,7 +128,7 @@ func (p *parser) Parse() (*Config, error) {
 		return c, nil
 	}
 	err := p.joinErrors(errs)
-	return nil, errors2.Wrapf(err, "unable to find a usable config source")
+	return nil, errors.Wrapf(err, "unable to find a usable config source")
 }
 
 func (p *parser) joinErrors(errs []error) error {
@@ -136,7 +136,7 @@ func (p *parser) joinErrors(errs []error) error {
 	for _, err := range errs {
 		msgs = append(msgs, err.Error())
 	}
-	return errors.New("no valid config found in the following locations:\n  " + strings.Join(msgs, "\n  "))
+	return fmt.Errorf("no valid config found in the following locations:\n  %s", strings.Join(msgs, "\n  "))
 }
 
 func (p *parser) decode(s source, r io.Reader) (*Config, error) {
diff --git a/console/survey.go b/console/survey.go
index 56ecac7..b97d541 100644
--- a/console/survey.go
+++ b/console/survey.go
@@ -8,8 +8,8 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/bpicode/fritzctl/internal/errors"
 	"github.com/fatih/color"
-	"github.com/pkg/errors"
 	"golang.org/x/crypto/ssh/terminal"
 )
 
diff --git a/fritz/api_aha.go b/fritz/api_aha.go
index 5d63c08..df25803 100644
--- a/fritz/api_aha.go
+++ b/fritz/api_aha.go
@@ -1,12 +1,13 @@
 package fritz
 
 import (
+	"fmt"
 	"strings"
 	"sync"
 
+	"github.com/bpicode/fritzctl/internal/errors"
+	"github.com/bpicode/fritzctl/internal/stringutils"
 	"github.com/bpicode/fritzctl/logger"
-	"github.com/bpicode/fritzctl/stringutils"
-	"github.com/pkg/errors"
 )
 
 // HomeAuto is a client for the Home Automation HTTP Interface,
@@ -130,7 +131,7 @@ func genericErrorHandler(key, message string, err error) result {
 
 func genericResult(results []result) error {
 	if err := truncateToOne(results); err != nil {
-		return errors.Wrap(err, "not all operations could be completed")
+		return errors.Wrapf(err, "not all operations could be completed")
 	}
 	return nil
 }
@@ -144,7 +145,7 @@ func truncateToOne(results []result) error {
 	}
 	if len(errs) > 0 {
 		msgs := stringutils.ErrorMessages(errs)
-		return errors.New(strings.Join(msgs, "; "))
+		return fmt.Errorf(strings.Join(msgs, "; "))
 	}
 	return nil
 }
@@ -152,7 +153,7 @@ func truncateToOne(results []result) error {
 func buildBacklog(h HomeAuto, names []string, workFactory func(string) func() (string, error)) (map[string]func() (string, error), error) {
 	devList, err := h.List()
 	if err != nil {
-		return nil, errors.Wrap(err, "unable to list devices")
+		return nil, errors.Wrapf(err, "unable to list devices")
 	}
 	namesAndAins := devList.NamesAndAins()
 	targets := make(map[string]func() (string, error))
@@ -160,7 +161,7 @@ func buildBacklog(h HomeAuto, names []string, workFactory func(string) func() (s
 		ain, ok := namesAndAins[name]
 		if ain == "" || !ok {
 			quoted := stringutils.Quote(stringutils.Keys(namesAndAins))
-			return nil, errors.Errorf("nothing found with name '%s'; choose one out of '%s'", name, strings.Join(quoted, ", "))
+			return nil, fmt.Errorf("nothing found with name '%s'; choose one out of '%s'", name, strings.Join(quoted, ", "))
 		}
 		targets[name] = workFactory(ain)
 	}
diff --git a/fritz/api_internal.go b/fritz/api_internal.go
index f419412..12c9248 100644
--- a/fritz/api_internal.go
+++ b/fritz/api_internal.go
@@ -2,7 +2,7 @@ package fritz
 
 import (
 	"github.com/bpicode/fritzctl/httpread"
-	"github.com/pkg/errors"
+	"github.com/bpicode/fritzctl/internal/errors"
 )
 
 // Internal exposes Fritz!Box internal and undocumented API.
@@ -64,7 +64,7 @@ func (i *internal) BoxInfo() (*BoxData, error) {
 	}{}
 	err := httpread.XML(i.client.getf(url), &h)
 	if err != nil {
-		return nil, errors.Wrap(err, "could not obtain raw system status data")
+		return nil, errors.Wrapf(err, "could not obtain raw system status data")
 	}
 	return boxDataParser{}.parse(h.Body), nil
 }
diff --git a/fritz/api_phone.go b/fritz/api_phone.go
index d4c2fcd..4f08c0e 100644
--- a/fritz/api_phone.go
+++ b/fritz/api_phone.go
@@ -2,7 +2,7 @@ package fritz
 
 import (
 	"github.com/bpicode/fritzctl/httpread"
-	"github.com/pkg/errors"
+	"github.com/bpicode/fritzctl/internal/errors"
 )
 
 // NewPhone creates a client for interaction with the FB phone sector.
@@ -38,7 +38,7 @@ func (p *phone) Calls() ([]Call, error) {
 	url := p.client.query().path(phoneListURI).query("csv", "").build()
 	records, err := httpread.Csv(p.client.getf(url), ';')
 	if err != nil {
-		return nil, errors.Wrap(err, "unable read data for phone calls")
+		return nil, errors.Wrapf(err, "unable read data for phone calls")
 	}
 	var calls []Call
 	for _, r := range records[2:] { // Skip first two lines.
@@ -46,6 +46,7 @@ func (p *phone) Calls() ([]Call, error) {
 	}
 	return calls, err
 }
+
 func convertRecordToCall(r []string) Call {
 	return Call{
 		Type:           r[0],
diff --git a/fritz/box_data.go b/fritz/box_data.go
index 67ca150..4fab2a4 100644
--- a/fritz/box_data.go
+++ b/fritz/box_data.go
@@ -5,7 +5,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/bpicode/fritzctl/stringutils"
+	"github.com/bpicode/fritzctl/internal/stringutils"
 )
 
 // BoxData contains runtime information of the FRITZ!Box.
diff --git a/fritz/fritzapi_constants.go b/fritz/fritzapi_constants.go
index 521e7f7..3fc338c 100644
--- a/fritz/fritzapi_constants.go
+++ b/fritz/fritzapi_constants.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 
 	"github.com/bpicode/fritzctl/config"
-	"github.com/bpicode/fritzctl/stringutils"
+	"github.com/bpicode/fritzctl/internal/stringutils"
 )
 
 const (
diff --git a/fritz/fritzclient.go b/fritz/fritzclient.go
index 8f08204..5aa46e9 100644
--- a/fritz/fritzclient.go
+++ b/fritz/fritzclient.go
@@ -10,8 +10,8 @@ import (
 
 	"github.com/bpicode/fritzctl/config"
 	"github.com/bpicode/fritzctl/httpread"
+	"github.com/bpicode/fritzctl/internal/errors"
 	"github.com/bpicode/fritzctl/logger"
-	"github.com/pkg/errors"
 )
 
 // Client encapsulates the FRITZ!Box interaction API.
@@ -41,7 +41,7 @@ type Rights struct {
 func NewClient(configfile string) (*Client, error) {
 	cfg, err := config.New(configfile)
 	if err != nil {
-		return nil, errors.Wrap(err, "unable to read configuration")
+		return nil, errors.Wrapf(err, "unable to read configuration")
 	}
 	return NewClientFromConfig(cfg), nil
 }
@@ -58,13 +58,13 @@ func NewClientFromConfig(cfg *config.Config) *Client {
 func (client *Client) Login() error {
 	sessionInfo, err := client.obtainChallenge()
 	if err != nil {
-		return errors.Wrap(err, "unable to obtain login challenge")
+		return errors.Wrapf(err, "unable to obtain login challenge")
 	}
 	client.SessionInfo = sessionInfo
 	logger.Debug("FRITZ!Box challenge is", client.SessionInfo.Challenge)
 	newSession, err := client.solveChallenge()
 	if err != nil {
-		return errors.Wrap(err, "unable to solve login challenge")
+		return errors.Wrapf(err, "unable to solve login challenge")
 	}
 	client.SessionInfo = newSession
 	logger.Info("Login successful")
diff --git a/fritz/group.go b/fritz/group.go
index 430d3d7..59c4341 100644
--- a/fritz/group.go
+++ b/fritz/group.go
@@ -3,7 +3,7 @@ package fritz
 import (
 	"strings"
 
-	"github.com/bpicode/fritzctl/stringutils"
+	"github.com/bpicode/fritzctl/internal/stringutils"
 )
 
 // Group models a grouping of smart home devices. This corresponds to
diff --git a/httpread/httpread.go b/httpread/httpread.go
index 53652c1..7f4d7bb 100644
--- a/httpread/httpread.go
+++ b/httpread/httpread.go
@@ -11,8 +11,8 @@ import (
 	"net/http"
 	"strings"
 
+	"github.com/bpicode/fritzctl/internal/errors"
 	"github.com/bpicode/fritzctl/logger"
-	"github.com/pkg/errors"
 )
 
 var (
@@ -30,7 +30,7 @@ func (s *stringDecoder) Decode(v interface{}) error {
 	}
 	sp, ok := v.(*string)
 	if !ok {
-		return errors.New("cannot decode into string, call with string pointer")
+		return fmt.Errorf("cannot decode into string, call with string pointer")
 	}
 	*sp = string(bytesRead)
 	return nil
@@ -68,7 +68,7 @@ func (c *csvDecoder) Decode(v interface{}) error {
 	}
 	t, ok := v.(*[][]string)
 	if !ok {
-		return errors.New("cannot decode into csv, call with *[][]string slice")
+		return fmt.Errorf("cannot decode into csv, call with *[][]string slice")
 	}
 	*t = records
 	return nil
@@ -119,7 +119,7 @@ func JSON(f func() (*http.Response, error), v interface{}) error {
 func readDecode(f func() (*http.Response, error), df decoderFactory, v interface{}) error {
 	response, err := f()
 	if err != nil {
-		return errors.Wrap(err, "error obtaining HTTP response from remote")
+		return errors.Wrapf(err, "error obtaining HTTP response from remote")
 	}
 	defer response.Body.Close()
 	if response.StatusCode >= 400 {
diff --git a/internal/errors/errors.go b/internal/errors/errors.go
new file mode 100644
index 0000000..4e257b4
--- /dev/null
+++ b/internal/errors/errors.go
@@ -0,0 +1,37 @@
+package errors
+
+import (
+	"fmt"
+)
+
+// Wrapf creates a new error with cause, and an inherent error message specified by the format parameters.
+// If the cause is nil, Wrapf returns nil.
+func Wrapf(err error, format string, args ...interface{}) error {
+	if err == nil {
+		return nil
+	}
+	return &withCause{
+		cause: err,
+		msg:   fmt.Sprintf(format, args...),
+	}
+}
+
+type withCause struct {
+	cause error
+	msg   string
+}
+
+// Error makes *withCause an error type. The whole causal chain is concatenated with ": ".
+func (wc *withCause) Error() string {
+	return wc.msg + ": " + wc.cause.Error()
+}
+
+// Cause returns the wrapped error, see Wrapf.
+func (wc *withCause) Cause() error {
+	return wc.cause
+}
+
+// Msg returns the "bare" error message. It differs from Error in that the causal chain is omitted.
+func (wc *withCause) Msg() string {
+	return wc.msg
+}
diff --git a/stringutils/stringutils.go b/internal/stringutils/stringutils.go
similarity index 100%
rename from stringutils/stringutils.go
rename to internal/stringutils/stringutils.go
diff --git a/stringutils/stringutils_test.go b/internal/stringutils/stringutils_test.go
similarity index 100%
rename from stringutils/stringutils_test.go
rename to internal/stringutils/stringutils_test.go
diff --git a/logger/flags.go b/logger/flags.go
index e4eb4e2..5dee813 100644
--- a/logger/flags.go
+++ b/logger/flags.go
@@ -1,7 +1,7 @@
 package logger
 
 import (
-	"github.com/pkg/errors"
+	"github.com/bpicode/fritzctl/internal/errors"
 )
 
 // Level represents a Value for different logging configs.
diff --git a/logger/loglevel.go b/logger/loglevel.go
index ae31109..3280c05 100644
--- a/logger/loglevel.go
+++ b/logger/loglevel.go
@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/pkg/errors"
+	"github.com/bpicode/fritzctl/internal/errors"
 )
 
 type printers struct {
diff --git a/main.go b/main.go
index 2db2a3e..0ab0802 100644
--- a/main.go
+++ b/main.go
@@ -1,24 +1,26 @@
 package main
 
 import (
+	"fmt"
 	"os"
+	"strings"
 
 	"github.com/bpicode/fritzctl/cmd"
 	"github.com/bpicode/fritzctl/logger"
 )
 
 var (
-	exitAdvice = os.Exit
+	exit = os.Exit
 )
 
 func main() {
 	defer func() {
 		r := recover()
 		if r != nil {
-			logger.Error(r)
+			printErr(r)
 		}
 		exitCode := determineExitCode(r)
-		exitAdvice(exitCode)
+		exit(exitCode)
 	}()
 	err := cmd.RootCmd.Execute()
 	if err != nil {
@@ -32,3 +34,27 @@ func determineExitCode(v interface{}) int {
 	}
 	return 1
 }
+
+func printErr(r interface{}) {
+	st := stack(r)
+	logger.Error(strings.Join(st, "\n  "))
+}
+
+func stack(e interface{}) []string {
+	if e == nil {
+		return nil
+	}
+	type causer interface {
+		error
+		Cause() error
+		Msg() string
+	}
+	var frames []string
+	csr, ok := e.(causer)
+	if !ok {
+		return []string{fmt.Sprint(e)}
+	}
+	frames = append(frames, csr.Msg()+":")
+	frames = append(frames, stack(csr.Cause())...)
+	return frames
+}
diff --git a/vendor/github.com/pkg/errors/.gitignore b/vendor/github.com/pkg/errors/.gitignore
deleted file mode 100644
index daf913b..0000000
--- a/vendor/github.com/pkg/errors/.gitignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# Compiled Object files, Static and Dynamic libs (Shared Objects)
-*.o
-*.a
-*.so
-
-# Folders
-_obj
-_test
-
-# Architecture specific extensions/prefixes
-*.[568vq]
-[568vq].out
-
-*.cgo1.go
-*.cgo2.c
-_cgo_defun.c
-_cgo_gotypes.go
-_cgo_export.*
-
-_testmain.go
-
-*.exe
-*.test
-*.prof
diff --git a/vendor/github.com/pkg/errors/.travis.yml b/vendor/github.com/pkg/errors/.travis.yml
deleted file mode 100644
index 588ceca..0000000
--- a/vendor/github.com/pkg/errors/.travis.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-language: go
-go_import_path: github.com/pkg/errors
-go:
-  - 1.4.3
-  - 1.5.4
-  - 1.6.2
-  - 1.7.1
-  - tip
-
-script:
-  - go test -v ./...
diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE
deleted file mode 100644
index 835ba3e..0000000
--- a/vendor/github.com/pkg/errors/LICENSE
+++ /dev/null
@@ -1,23 +0,0 @@
-Copyright (c) 2015, Dave Cheney <dave@cheney.net>
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md
deleted file mode 100644
index 273db3c..0000000
--- a/vendor/github.com/pkg/errors/README.md
+++ /dev/null
@@ -1,52 +0,0 @@
-# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors)
-
-Package errors provides simple error handling primitives.
-
-`go get github.com/pkg/errors`
-
-The traditional error handling idiom in Go is roughly akin to
-```go
-if err != nil {
-        return err
-}
-```
-which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
-
-## Adding context to an error
-
-The errors.Wrap function returns a new error that adds context to the original error. For example
-```go
-_, err := ioutil.ReadAll(r)
-if err != nil {
-        return errors.Wrap(err, "read failed")
-}
-```
-## Retrieving the cause of an error
-
-Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
-```go
-type causer interface {
-        Cause() error
-}
-```
-`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
-```go
-switch err := errors.Cause(err).(type) {
-case *MyError:
-        // handle specifically
-default:
-        // unknown error
-}
-```
-
-[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
-
-## Contributing
-
-We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high.
-
-Before proposing a change, please discuss your change by raising an issue.
-
-## Licence
-
-BSD-2-Clause
diff --git a/vendor/github.com/pkg/errors/appveyor.yml b/vendor/github.com/pkg/errors/appveyor.yml
deleted file mode 100644
index a932ead..0000000
--- a/vendor/github.com/pkg/errors/appveyor.yml
+++ /dev/null
@@ -1,32 +0,0 @@
-version: build-{build}.{branch}
-
-clone_folder: C:\gopath\src\github.com\pkg\errors
-shallow_clone: true # for startup speed
-
-environment:
-  GOPATH: C:\gopath
-
-platform:
-  - x64
-
-# http://www.appveyor.com/docs/installed-software
-install:
-  # some helpful output for debugging builds
-  - go version
-  - go env
-  # pre-installed MinGW at C:\MinGW is 32bit only
-  # but MSYS2 at C:\msys64 has mingw64
-  - set PATH=C:\msys64\mingw64\bin;%PATH%
-  - gcc --version
-  - g++ --version
-
-build_script:
-  - go install -v ./...
-
-test_script:
-  - set PATH=C:\gopath\bin;%PATH%
-  - go test -v ./...
-
-#artifacts:
-#  - path: '%GOPATH%\bin\*.exe'
-deploy: off
diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go
deleted file mode 100644
index 842ee80..0000000
--- a/vendor/github.com/pkg/errors/errors.go
+++ /dev/null
@@ -1,269 +0,0 @@
-// Package errors provides simple error handling primitives.
-//
-// The traditional error handling idiom in Go is roughly akin to
-//
-//     if err != nil {
-//             return err
-//     }
-//
-// which applied recursively up the call stack results in error reports
-// without context or debugging information. The errors package allows
-// programmers to add context to the failure path in their code in a way
-// that does not destroy the original value of the error.
-//
-// Adding context to an error
-//
-// The errors.Wrap function returns a new error that adds context to the
-// original error by recording a stack trace at the point Wrap is called,
-// and the supplied message. For example
-//
-//     _, err := ioutil.ReadAll(r)
-//     if err != nil {
-//             return errors.Wrap(err, "read failed")
-//     }
-//
-// If additional control is required the errors.WithStack and errors.WithMessage
-// functions destructure errors.Wrap into its component operations of annotating
-// an error with a stack trace and an a message, respectively.
-//
-// Retrieving the cause of an error
-//
-// Using errors.Wrap constructs a stack of errors, adding context to the
-// preceding error. Depending on the nature of the error it may be necessary
-// to reverse the operation of errors.Wrap to retrieve the original error
-// for inspection. Any error value which implements this interface
-//
-//     type causer interface {
-//             Cause() error
-//     }
-//
-// can be inspected by errors.Cause. errors.Cause will recursively retrieve
-// the topmost error which does not implement causer, which is assumed to be
-// the original cause. For example:
-//
-//     switch err := errors.Cause(err).(type) {
-//     case *MyError:
-//             // handle specifically
-//     default:
-//             // unknown error
-//     }
-//
-// causer interface is not exported by this package, but is considered a part
-// of stable public API.
-//
-// Formatted printing of errors
-//
-// All error values returned from this package implement fmt.Formatter and can
-// be formatted by the fmt package. The following verbs are supported
-//
-//     %s    print the error. If the error has a Cause it will be
-//           printed recursively
-//     %v    see %s
-//     %+v   extended format. Each Frame of the error's StackTrace will
-//           be printed in detail.
-//
-// Retrieving the stack trace of an error or wrapper
-//
-// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
-// invoked. This information can be retrieved with the following interface.
-//
-//     type stackTracer interface {
-//             StackTrace() errors.StackTrace
-//     }
-//
-// Where errors.StackTrace is defined as
-//
-//     type StackTrace []Frame
-//
-// The Frame type represents a call site in the stack trace. Frame supports
-// the fmt.Formatter interface that can be used for printing information about
-// the stack trace of this error. For example:
-//
-//     if err, ok := err.(stackTracer); ok {
-//             for _, f := range err.StackTrace() {
-//                     fmt.Printf("%+s:%d", f)
-//             }
-//     }
-//
-// stackTracer interface is not exported by this package, but is considered a part
-// of stable public API.
-//
-// See the documentation for Frame.Format for more details.
-package errors
-
-import (
-	"fmt"
-	"io"
-)
-
-// New returns an error with the supplied message.
-// New also records the stack trace at the point it was called.
-func New(message string) error {
-	return &fundamental{
-		msg:   message,
-		stack: callers(),
-	}
-}
-
-// Errorf formats according to a format specifier and returns the string
-// as a value that satisfies error.
-// Errorf also records the stack trace at the point it was called.
-func Errorf(format string, args ...interface{}) error {
-	return &fundamental{
-		msg:   fmt.Sprintf(format, args...),
-		stack: callers(),
-	}
-}
-
-// fundamental is an error that has a message and a stack, but no caller.
-type fundamental struct {
-	msg string
-	*stack
-}
-
-func (f *fundamental) Error() string { return f.msg }
-
-func (f *fundamental) Format(s fmt.State, verb rune) {
-	switch verb {
-	case 'v':
-		if s.Flag('+') {
-			io.WriteString(s, f.msg)
-			f.stack.Format(s, verb)
-			return
-		}
-		fallthrough
-	case 's':
-		io.WriteString(s, f.msg)
-	case 'q':
-		fmt.Fprintf(s, "%q", f.msg)
-	}
-}
-
-// WithStack annotates err with a stack trace at the point WithStack was called.
-// If err is nil, WithStack returns nil.
-func WithStack(err error) error {
-	if err == nil {
-		return nil
-	}
-	return &withStack{
-		err,
-		callers(),
-	}
-}
-
-type withStack struct {
-	error
-	*stack
-}
-
-func (w *withStack) Cause() error { return w.error }
-
-func (w *withStack) Format(s fmt.State, verb rune) {
-	switch verb {
-	case 'v':
-		if s.Flag('+') {
-			fmt.Fprintf(s, "%+v", w.Cause())
-			w.stack.Format(s, verb)
-			return
-		}
-		fallthrough
-	case 's':
-		io.WriteString(s, w.Error())
-	case 'q':
-		fmt.Fprintf(s, "%q", w.Error())
-	}
-}
-
-// Wrap returns an error annotating err with a stack trace
-// at the point Wrap is called, and the supplied message.
-// If err is nil, Wrap returns nil.
-func Wrap(err error, message string) error {
-	if err == nil {
-		return nil
-	}
-	err = &withMessage{
-		cause: err,
-		msg:   message,
-	}
-	return &withStack{
-		err,
-		callers(),
-	}
-}
-
-// Wrapf returns an error annotating err with a stack trace
-// at the point Wrapf is call, and the format specifier.
-// If err is nil, Wrapf returns nil.
-func Wrapf(err error, format string, args ...interface{}) error {
-	if err == nil {
-		return nil
-	}
-	err = &withMessage{
-		cause: err,
-		msg:   fmt.Sprintf(format, args...),
-	}
-	return &withStack{
-		err,
-		callers(),
-	}
-}
-
-// WithMessage annotates err with a new message.
-// If err is nil, WithMessage returns nil.
-func WithMessage(err error, message string) error {
-	if err == nil {
-		return nil
-	}
-	return &withMessage{
-		cause: err,
-		msg:   message,
-	}
-}
-
-type withMessage struct {
-	cause error
-	msg   string
-}
-
-func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
-func (w *withMessage) Cause() error  { return w.cause }
-
-func (w *withMessage) Format(s fmt.State, verb rune) {
-	switch verb {
-	case 'v':
-		if s.Flag('+') {
-			fmt.Fprintf(s, "%+v\n", w.Cause())
-			io.WriteString(s, w.msg)
-			return
-		}
-		fallthrough
-	case 's', 'q':
-		io.WriteString(s, w.Error())
-	}
-}
-
-// Cause returns the underlying cause of the error, if possible.
-// An error value has a cause if it implements the following
-// interface:
-//
-//     type causer interface {
-//            Cause() error
-//     }
-//
-// If the error does not implement Cause, the original error will
-// be returned. If the error is nil, nil will be returned without further
-// investigation.
-func Cause(err error) error {
-	type causer interface {
-		Cause() error
-	}
-
-	for err != nil {
-		cause, ok := err.(causer)
-		if !ok {
-			break
-		}
-		err = cause.Cause()
-	}
-	return err
-}
diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go
deleted file mode 100644
index 6b1f289..0000000
--- a/vendor/github.com/pkg/errors/stack.go
+++ /dev/null
@@ -1,178 +0,0 @@
-package errors
-
-import (
-	"fmt"
-	"io"
-	"path"
-	"runtime"
-	"strings"
-)
-
-// Frame represents a program counter inside a stack frame.
-type Frame uintptr
-
-// pc returns the program counter for this frame;
-// multiple frames may have the same PC value.
-func (f Frame) pc() uintptr { return uintptr(f) - 1 }
-
-// file returns the full path to the file that contains the
-// function for this Frame's pc.
-func (f Frame) file() string {
-	fn := runtime.FuncForPC(f.pc())
-	if fn == nil {
-		return "unknown"
-	}
-	file, _ := fn.FileLine(f.pc())
-	return file
-}
-
-// line returns the line number of source code of the
-// function for this Frame's pc.
-func (f Frame) line() int {
-	fn := runtime.FuncForPC(f.pc())
-	if fn == nil {
-		return 0
-	}
-	_, line := fn.FileLine(f.pc())
-	return line
-}
-
-// Format formats the frame according to the fmt.Formatter interface.
-//
-//    %s    source file
-//    %d    source line
-//    %n    function name
-//    %v    equivalent to %s:%d
-//
-// Format accepts flags that alter the printing of some verbs, as follows:
-//
-//    %+s   path of source file relative to the compile time GOPATH
-//    %+v   equivalent to %+s:%d
-func (f Frame) Format(s fmt.State, verb rune) {
-	switch verb {
-	case 's':
-		switch {
-		case s.Flag('+'):
-			pc := f.pc()
-			fn := runtime.FuncForPC(pc)
-			if fn == nil {
-				io.WriteString(s, "unknown")
-			} else {
-				file, _ := fn.FileLine(pc)
-				fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
-			}
-		default:
-			io.WriteString(s, path.Base(f.file()))
-		}
-	case 'd':
-		fmt.Fprintf(s, "%d", f.line())
-	case 'n':
-		name := runtime.FuncForPC(f.pc()).Name()
-		io.WriteString(s, funcname(name))
-	case 'v':
-		f.Format(s, 's')
-		io.WriteString(s, ":")
-		f.Format(s, 'd')
-	}
-}
-
-// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
-type StackTrace []Frame
-
-func (st StackTrace) Format(s fmt.State, verb rune) {
-	switch verb {
-	case 'v':
-		switch {
-		case s.Flag('+'):
-			for _, f := range st {
-				fmt.Fprintf(s, "\n%+v", f)
-			}
-		case s.Flag('#'):
-			fmt.Fprintf(s, "%#v", []Frame(st))
-		default:
-			fmt.Fprintf(s, "%v", []Frame(st))
-		}
-	case 's':
-		fmt.Fprintf(s, "%s", []Frame(st))
-	}
-}
-
-// stack represents a stack of program counters.
-type stack []uintptr
-
-func (s *stack) Format(st fmt.State, verb rune) {
-	switch verb {
-	case 'v':
-		switch {
-		case st.Flag('+'):
-			for _, pc := range *s {
-				f := Frame(pc)
-				fmt.Fprintf(st, "\n%+v", f)
-			}
-		}
-	}
-}
-
-func (s *stack) StackTrace() StackTrace {
-	f := make([]Frame, len(*s))
-	for i := 0; i < len(f); i++ {
-		f[i] = Frame((*s)[i])
-	}
-	return f
-}
-
-func callers() *stack {
-	const depth = 32
-	var pcs [depth]uintptr
-	n := runtime.Callers(3, pcs[:])
-	var st stack = pcs[0:n]
-	return &st
-}
-
-// funcname removes the path prefix component of a function's name reported by func.Name().
-func funcname(name string) string {
-	i := strings.LastIndex(name, "/")
-	name = name[i+1:]
-	i = strings.Index(name, ".")
-	return name[i+1:]
-}
-
-func trimGOPATH(name, file string) string {
-	// Here we want to get the source file path relative to the compile time
-	// GOPATH. As of Go 1.6.x there is no direct way to know the compiled
-	// GOPATH at runtime, but we can infer the number of path segments in the
-	// GOPATH. We note that fn.Name() returns the function name qualified by
-	// the import path, which does not include the GOPATH. Thus we can trim
-	// segments from the beginning of the file path until the number of path
-	// separators remaining is one more than the number of path separators in
-	// the function name. For example, given:
-	//
-	//    GOPATH     /home/user
-	//    file       /home/user/src/pkg/sub/file.go
-	//    fn.Name()  pkg/sub.Type.Method
-	//
-	// We want to produce:
-	//
-	//    pkg/sub/file.go
-	//
-	// From this we can easily see that fn.Name() has one less path separator
-	// than our desired output. We count separators from the end of the file
-	// path until it finds two more than in the function name and then move
-	// one character forward to preserve the initial path segment without a
-	// leading separator.
-	const sep = "/"
-	goal := strings.Count(name, sep) + 2
-	i := len(file)
-	for n := 0; n < goal; n++ {
-		i = strings.LastIndex(file[:i], sep)
-		if i == -1 {
-			// not enough separators found, set i so that the slice expression
-			// below leaves file unmodified
-			i = -len(sep)
-			break
-		}
-	}
-	// get back to 0 or trim the leading separator
-	file = file[i+len(sep):]
-	return file
-}


Test patch:
diff --git a/cmd/assertions_test.go b/cmd/assertions_test.go
index 77dd7f0..9dbb9d6 100644
--- a/cmd/assertions_test.go
+++ b/cmd/assertions_test.go
@@ -41,13 +41,13 @@ func TestAssertMinLenPanicsOnViolation(t *testing.T) {
 // TestIsTruePassesOnVerification tests assertTrue, "happy path".
 func TestIsTruePassesOnVerification(t *testing.T) {
 	assert.NotPanics(t, func() {
-		assertTrue(true)
+		assertTrue(true, errors.New("would-be-error"))
 	})
 }
 
 // TestIsTruePanicsOnViolation tests assertTrue, "error path".
 func TestIsTruePanicsOnViolation(t *testing.T) {
 	assert.Panics(t, func() {
-		assertTrue(false)
+		assertTrue(false, errors.New("is-an-error"))
 	})
 }
diff --git a/internal/errors/errors_test.go b/internal/errors/errors_test.go
new file mode 100644
index 0000000..6aead57
--- /dev/null
+++ b/internal/errors/errors_test.go
@@ -0,0 +1,21 @@
+package errors
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// TestErrors demonstrates usage of errors api.
+func TestErrors(t *testing.T) {
+	assert.Equal(t, nil, Wrapf(nil, "would-be-error"))
+	err := Wrapf(fmt.Errorf("inner"), "outer")
+	assert.Error(t, err)
+	assert.Equal(t, "outer: inner", err.Error())
+
+	wc, ok := err.(*withCause)
+	assert.True(t, ok)
+	assert.Equal(t, wc.Cause().Error(), "inner")
+	assert.Equal(t, wc.Msg(), "outer")
+}
diff --git a/stringutils/doc.go b/internal/stringutils/doc.go
similarity index 100%
rename from stringutils/doc.go
rename to internal/stringutils/doc.go
diff --git a/stringutils/example_test.go b/internal/stringutils/example_test.go
similarity index 97%
rename from stringutils/example_test.go
rename to internal/stringutils/example_test.go
index ebd7506..2a87726 100644
--- a/stringutils/example_test.go
+++ b/internal/stringutils/example_test.go
@@ -6,7 +6,7 @@ import (
 	"sort"
 	"strings"
 
-	"github.com/bpicode/fritzctl/stringutils"
+	"github.com/bpicode/fritzctl/internal/stringutils"
 )
 
 // Transform applies a given operation to all elements of a string slice.
diff --git a/main_test.go b/main_test.go
index 0b92411..f15d3be 100644
--- a/main_test.go
+++ b/main_test.go
@@ -1,28 +1,30 @@
 package main
 
 import (
+	"fmt"
 	"os"
 	"testing"
 
+	"github.com/bpicode/fritzctl/internal/errors"
 	"github.com/stretchr/testify/assert"
 )
 
 // TestBlankRun runs the app without args.
 func TestBlankRun(t *testing.T) {
 	defer func() {
-		exitAdvice = os.Exit
+		exit = os.Exit
 	}()
-	exitAdvice = func(code int) {}
+	exit = func(code int) {}
 	main()
 }
 
 // TestHelp runs the app with the help flag.
 func TestHelp(t *testing.T) {
 	osArgsBefore := os.Args
-	exitAdvice = func(code int) {}
+	exit = func(code int) {}
 	defer func() {
 		os.Args = osArgsBefore
-		exitAdvice = os.Exit
+		exit = os.Exit
 	}()
 	os.Args = []string{"app_called_in_test_scope", "--help"}
 	main()
@@ -32,10 +34,10 @@ func TestHelp(t *testing.T) {
 // TestVersion runs the app with the version flag.
 func TestVersion(t *testing.T) {
 	osArgsBefore := os.Args
-	exitAdvice = func(code int) {}
+	exit = func(code int) {}
 	defer func() {
 		os.Args = osArgsBefore
-		exitAdvice = os.Exit
+		exit = os.Exit
 	}()
 	os.Args = []string{"app_called_in_test_scope", "--version"}
 	main()
@@ -46,3 +48,11 @@ func TestDetermineExitCode(t *testing.T) {
 	assert.Equal(t, 0, determineExitCode(nil))
 	assert.Equal(t, 1, determineExitCode("an error"))
 }
+
+//// TestStack exercises the stack traversal.
+func TestStack(t *testing.T) {
+	assert.Len(t, stack(nil), 0)
+	assert.Equal(t, []string{"error"}, stack("error"))
+	assert.Equal(t, []string{"error"}, stack(fmt.Errorf("error")))
+	assert.Equal(t, []string{"parent:", "root"}, stack(errors.Wrapf(fmt.Errorf("root"), "parent")))
+}