Browse Source

Improve stack comparisons to make tests succeed.

The stack traces created by the go-errors package and debug.Stack() are no
longer equal and cannot be used in tests. Instead we now compare the actual
program counter values in the stack trace with reference stack traces created
in the tests. This will work even if the formatting of stack traces change.
Gabriel Falkenberg 7 years ago
parent
commit
7d9bdff2da
1 changed files with 68 additions and 24 deletions
  1. 68 24
      error_test.go

+ 68 - 24
error_test.go

@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"io"
 	"reflect"
-	"runtime/debug"
+	"runtime"
 	"strings"
 	"testing"
 )
@@ -18,16 +18,11 @@ func TestStackFormatMatches(t *testing.T) {
 			t.Fatal(err)
 		}
 
-		bs := [][]byte{Errorf("hi").Stack(), debug.Stack()}
+		bs := [][]uintptr{Errorf("hi").stack, callers()}
 
-		// Ignore the first line (as it contains the PC of the .Stack() call)
-		bs[0] = bytes.SplitN(bs[0], []byte("\n"), 2)[1]
-		bs[1] = bytes.SplitN(bs[1], []byte("\n"), 2)[1]
-
-		if bytes.Compare(bs[0], bs[1]) != 0 {
+		if err := compareStacks(bs[0], bs[1]); err != nil {
 			t.Errorf("Stack didn't match")
-			t.Errorf("%s", bs[0])
-			t.Errorf("%s", bs[1])
+			t.Errorf(err.Error())
 		}
 	}()
 
@@ -42,15 +37,11 @@ func TestSkipWorks(t *testing.T) {
 			t.Fatal(err)
 		}
 
-		bs := [][]byte{Wrap("hi", 2).Stack(), debug.Stack()}
-
-		// should skip four lines of debug.Stack()
-		bs[1] = bytes.SplitN(bs[1], []byte("\n"), 5)[4]
+		bs := [][]uintptr{Wrap("hi", 2).stack, callersSkip(2)}
 
-		if bytes.Compare(bs[0], bs[1]) != 0 {
+		if err := compareStacks(bs[0], bs[1]); err != nil {
 			t.Errorf("Stack didn't match")
-			t.Errorf("%s", bs[0])
-			t.Errorf("%s", bs[1])
+			t.Errorf(err.Error())
 		}
 	}()
 
@@ -71,16 +62,11 @@ func TestNew(t *testing.T) {
 		t.Errorf("Wrong message")
 	}
 
-	bs := [][]byte{New("foo").Stack(), debug.Stack()}
-
-	// Ignore the first line (as it contains the PC of the .Stack() call)
-	bs[0] = bytes.SplitN(bs[0], []byte("\n"), 2)[1]
-	bs[1] = bytes.SplitN(bs[1], []byte("\n"), 2)[1]
+	bs := [][]uintptr{New("foo").stack, callers()}
 
-	if bytes.Compare(bs[0], bs[1]) != 0 {
+	if err := compareStacks(bs[0], bs[1]); err != nil {
 		t.Errorf("Stack didn't match")
-		t.Errorf("%s", bs[0])
-		t.Errorf("%s", bs[1])
+		t.Errorf(err.Error())
 	}
 
 	if err.ErrorStack() != err.TypeName()+" "+err.Error()+"\n"+string(err.Stack()) {
@@ -245,3 +231,61 @@ func b(i int) {
 func c() {
 	panic('a')
 }
+
+// compareStacks will compare a stack created using the errors package (actual)
+// with a reference stack created with the callers function (expected). The
+// first entry is compared inexact since the actual and expected stacks cannot
+// be created at the exact same program counter position so the first entry
+// will always differ somewhat. Returns nil if the stacks are equal enough and
+// an error containing a detailed error message otherwise.
+func compareStacks(actual, expected []uintptr) error {
+	if len(actual) != len(expected) {
+		return stackCompareError("Stacks does not have equal length", actual, expected)
+	}
+	for i, pc := range actual {
+		if i == 0 {
+			firstEntryDiff := (int)(expected[i]) - (int)(pc)
+			if firstEntryDiff < -27 || firstEntryDiff > 27 {
+				return stackCompareError(fmt.Sprintf("First entry PC diff to large (%d)", firstEntryDiff), actual, expected)
+			}
+		} else if pc != expected[i] {
+			return stackCompareError(fmt.Sprintf("Stacks does not match entry %d (and maybe others)", i), actual, expected)
+		}
+	}
+	return nil
+}
+
+func stackCompareError(msg string, actual, expected []uintptr) error {
+	return fmt.Errorf("%s\nActual stack trace:\n%s\nExpected stack trace:\n%s", msg, readableStackTrace(actual), readableStackTrace(expected))
+}
+
+func callers() []uintptr {
+	return callersSkip(1)
+}
+
+func callersSkip(skip int) []uintptr {
+	callers := make([]uintptr, MaxStackDepth)
+	length := runtime.Callers(skip+2, callers[:])
+	return callers[:length]
+}
+
+func readableStackTrace(callers []uintptr) string {
+	var result bytes.Buffer
+	frames := callersToFrames(callers)
+	for _, frame := range frames {
+		result.WriteString(fmt.Sprintf("%s:%d (%#x)\n\t%s\n", frame.File, frame.Line, frame.PC, frame.Function))
+	}
+	return result.String()
+}
+
+func callersToFrames(callers []uintptr) []runtime.Frame {
+	frames := make([]runtime.Frame, 0, len(callers))
+	framesPtr := runtime.CallersFrames(callers)
+	for {
+		frame, more := framesPtr.Next()
+		frames = append(frames, frame)
+		if !more {
+			return frames
+		}
+	}
+}