Repository: MarvinJWendt/testza
Pull request #97 (2022-03-28 23:53:40)

feat: rich difference detection
Implements rich diffing as discussed per #96 and discord.

<img width="664" alt="image" src="https://user-images.githubusercontent.com/1759390/160505870-7e2eb179-8b59-4bd9-a8b5-0eb14f96de67.png">

Currently in a very ugly state with a lot of duplicate code.


Patch:
diff --git a/assert.go b/assert.go
index 2a83a25..58e5dc7 100644
--- a/assert.go
+++ b/assert.go
@@ -180,7 +180,7 @@ func AssertEqual(t testRunner, expected interface{}, actual interface{}, msg ...
 	}
 
 	if !internal.IsEqual(expected, actual) {
-		internal.Fail(t, "Two objects that !!should be equal!!, are not equal.", internal.NewObjectsExpectedActual(expected, actual), msg...)
+		internal.Fail(t, "Two objects that !!should be equal!!, are not equal.", internal.NewObjectsExpectedActualWithDiff(expected, actual), msg...)
 	}
 }
 
@@ -197,7 +197,7 @@ func AssertNotEqual(t testRunner, expected interface{}, actual interface{}, msg
 	}
 
 	if internal.IsEqual(expected, actual) {
-		internal.Fail(t, "Two objects that !!should not be equal!!, are equal.", internal.NewObjectsExpectedActual(expected, actual), msg...)
+		internal.Fail(t, "Two objects that !!should not be equal!!, are equal.", internal.NewObjectsExpectedActualWithDiff(expected, actual), msg...)
 	}
 }
 
@@ -231,7 +231,7 @@ func AssertEqualValues(t testRunner, expected interface{}, actual interface{}, m
 	}
 
 	if !internal.HasEqualValues(expected, actual) {
-		internal.Fail(t, "Two objects that !!should have equal values!!, do not have equal values.", internal.NewObjectsExpectedActual(expected, actual), msg...)
+		internal.Fail(t, "Two objects that !!should have equal values!!, do not have equal values.", internal.NewObjectsExpectedActualWithDiff(expected, actual), msg...)
 	}
 }
 
@@ -262,7 +262,7 @@ func AssertNotEqualValues(t testRunner, expected interface{}, actual interface{}
 	}
 
 	if internal.HasEqualValues(expected, actual) {
-		internal.Fail(t, "Two objects that !!should not have equal values!!, do have equal values.", internal.NewObjectsExpectedActual(expected, actual), msg...)
+		internal.Fail(t, "Two objects that !!should not have equal values!!, do have equal values.", internal.NewObjectsExpectedActualWithDiff(expected, actual), msg...)
 	}
 }
 
@@ -871,7 +871,7 @@ func AssertSameElements(t testRunner, expected interface{}, actual interface{},
 	}
 
 	if !internal.HasSameElements(expected, actual) {
-		internal.Fail(t, "Two objects that !!should have the same elements!!, do not have the same elements.", internal.NewObjectsExpectedActual(expected, actual), msg...)
+		internal.Fail(t, "Two objects that !!should have the same elements!!, do not have the same elements.", internal.NewObjectsExpectedActualWithDiff(expected, actual), msg...)
 	}
 }
 
@@ -894,7 +894,7 @@ func AssertNotSameElements(t testRunner, expected interface{}, actual interface{
 	}
 
 	if internal.HasSameElements(expected, actual) {
-		internal.Fail(t, "Two objects that !!should have the same elements!!, do not have the same elements.", internal.NewObjectsExpectedActual(expected, actual), msg...)
+		internal.Fail(t, "Two objects that !!should have the same elements!!, do not have the same elements.", internal.NewObjectsExpectedActualWithDiff(expected, actual), msg...)
 	}
 }
 
diff --git a/configuration.go b/configuration.go
index 15f0b25..012f726 100644
--- a/configuration.go
+++ b/configuration.go
@@ -63,3 +63,16 @@ func SetRandomSeed(seed int64) {
 func SetShowStartupMessage(show bool) {
 	showStartupMessage = show
 }
+
+// SetDiffContextLines controls how many lines are shown around a changed diff line.
+// If set to -1 it will show full diff.
+// You should use this in the init() method of the package, which contains your tests.
+//
+// Example:
+//  init() {
+//    testza.SetDiffContextLines(-1) // Show all diff lines
+//    testza.SetDiffContextLines(3)  // Show 3 lines around every changed line
+//  }
+func SetDiffContextLines(lines int) {
+	internal.DiffContextLines = lines
+}
diff --git a/go.mod b/go.mod
index 1f6a198..0afe779 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,6 @@ go 1.15
 require (
 	github.com/davecgh/go-spew v1.1.1
 	github.com/klauspost/cpuid/v2 v2.0.10
-	github.com/pmezard/go-difflib v1.0.0
 	github.com/pterm/pterm v0.12.39
+	github.com/sergi/go-diff v1.2.0
 )
diff --git a/go.sum b/go.sum
index bc06f3b..c99ca89 100644
--- a/go.sum
+++ b/go.sum
@@ -14,6 +14,11 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.0.10 h1:fv5GKR+e2UgD+gcxQECVT5rBwAmlFLl2mkKm7WK3ODY=
 github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
 github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -28,7 +33,10 @@ github.com/pterm/pterm v0.12.39 h1:KPzxGWWWo3b/GIOjbM/XYecjo9RECQEW6/Aoc3/dDC4=
 github.com/pterm/pterm v0.12.39/go.mod h1:rVlv3zMmJWONobDAX0UKXP4UzQGCGTa2j2mUXos+wbs=
 github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
+github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -43,8 +51,11 @@ golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9sn
 golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/difference.go b/internal/difference.go
index 611cb4c..80331ac 100644
--- a/internal/difference.go
+++ b/internal/difference.go
@@ -2,41 +2,243 @@ package internal
 
 import (
 	"fmt"
+	"math"
 	"strings"
 
 	"github.com/davecgh/go-spew/spew"
-	"github.com/pmezard/go-difflib/difflib"
 	"github.com/pterm/pterm"
+	"github.com/sergi/go-diff/diffmatchpatch"
 )
 
+var DiffContextLines = 2
+
+func NewDiffObject(expected, actual interface{}, raw ...bool) Object {
+	return Object{
+		Name:      "Difference",
+		NameStyle: pterm.NewStyle(pterm.FgYellow),
+		Data:      GetDifference(expected, actual, raw...),
+		Raw:       true,
+	}
+}
+
 // GetDifference returns the diff for two projects.
 func GetDifference(a, b interface{}, raw ...bool) string {
-	var diff difflib.UnifiedDiff
+	dmp := diffmatchpatch.New()
+
+	var aString, bString string
+	var diffs []diffmatchpatch.Diff
 	if len(raw) == 0 || !raw[0] {
-		diff = difflib.UnifiedDiff{
-			A: difflib.SplitLines(spew.Sdump(a)),
-			B: difflib.SplitLines(spew.Sdump(b)),
+		var aOk, bOk bool
+		aString, aOk = a.(string)
+		bString, bOk = b.(string)
+		if aOk && bOk {
+			diffs = dmp.DiffMain(aString, bString, false)
+		} else {
+			aString = strings.TrimSpace(spew.Sdump(a))
+			bString = strings.TrimSpace(spew.Sdump(b))
+			diffs = dmp.DiffMain(aString, bString, false)
 		}
 	} else {
-		diff = difflib.UnifiedDiff{
-			A: difflib.SplitLines(fmt.Sprint(a)),
-			B: difflib.SplitLines(fmt.Sprint(b)),
+		aString = fmt.Sprint(a)
+		bString = fmt.Sprint(b)
+		diffs = dmp.DiffMain(aString, bString, false)
+	}
+
+	diffs = dmp.DiffCleanupEfficiency(diffs)
+	diffs = dmp.DiffCleanupSemanticLossless(diffs)
+
+	maxNewlines := math.Max(float64(strings.Count(aString, "\n")), float64(strings.Count(bString, "\n"))) + 1
+
+	d := &diffPrinter{
+		ExpectedI:       1,
+		ActualI:         1,
+		UnchangedPrefix: "#",
+		ExpectedPrefix:  pterm.FgRed.Sprint("-"),
+		ActualPrefix:    pterm.FgGreen.Sprint("+"),
+		CounterWidth:    int(math.Log10(maxNewlines)) + 1,
+	}
+
+	return d.ProcessDiffs(diffs)
+}
+
+type textLine struct {
+	Text      string
+	Operation diffmatchpatch.Operation
+}
+
+type diffPrinter struct {
+	Text []textLine
+
+	ExpectedI int
+	ActualI   int
+
+	ExpectedLine strings.Builder
+	ActualLine   strings.Builder
+
+	DiffBuffer strings.Builder
+
+	ExpectedGroupBuffer []textLine
+	ActualGroupBuffer   []textLine
+
+	ExpectedFlushable bool
+	ActualFlushable   bool
+
+	UnchangedPrefix string
+	ExpectedPrefix  string
+	ActualPrefix    string
+
+	CounterWidth int
+}
+
+func (d *diffPrinter) ProcessDiffs(diffs []diffmatchpatch.Diff) string {
+	for _, diff := range diffs {
+		for _, char := range diff.Text {
+			if char == '\n' {
+				d.FlushDiff(diff.Type, true)
+				d.FlushLine(diff.Type)
+			} else {
+				d.DiffBuffer.WriteRune(char)
+			}
 		}
+		d.FlushDiff(diff.Type, false)
 	}
 
-	text, _ := difflib.GetUnifiedDiffString(diff)
+	lastOp := diffmatchpatch.DiffEqual
+	if len(diffs) > 0 {
+		lastOp = diffs[len(diffs)-1].Type
+	}
+
+	d.FlushLine(lastOp)
+	return d.Finalize(lastOp)
+}
 
-	var newText string
-	for _, v := range strings.Split(text, "\n") {
-		if strings.HasPrefix(v, "- ") {
-			v = strings.TrimPrefix(v, "- ")
-			newText += pterm.FgGreen.Sprint(v) + "\n"
-		} else if strings.HasPrefix(v, "+ ") {
-			v = strings.TrimPrefix(v, "+ ")
-			newText += pterm.FgRed.Sprint(v) + "\n"
+func (d *diffPrinter) FlushDiff(operation diffmatchpatch.Operation, newLine bool) {
+	if d.DiffBuffer.Len() > 0 {
+		if operation == diffmatchpatch.DiffDelete {
+			d.ExpectedLine.WriteString(pterm.BgDarkGray.Sprint(pterm.Bold.Sprint(d.DiffBuffer.String())))
+		} else if operation == diffmatchpatch.DiffInsert {
+			d.ActualLine.WriteString(pterm.BgDarkGray.Sprint(pterm.Bold.Sprint(d.DiffBuffer.String())))
+		} else {
+			d.ExpectedLine.WriteString(d.DiffBuffer.String())
+			d.ActualLine.WriteString(d.DiffBuffer.String())
+		}
+	}
+
+	if operation == diffmatchpatch.DiffDelete {
+		d.ExpectedFlushable = d.ExpectedFlushable || newLine
+	} else if operation == diffmatchpatch.DiffInsert {
+		d.ActualFlushable = d.ActualFlushable || newLine
+	} else {
+		d.ExpectedFlushable = d.ExpectedFlushable || newLine
+		d.ActualFlushable = d.ActualFlushable || newLine
+	}
+
+	d.DiffBuffer.Reset()
+}
+
+func (d *diffPrinter) FlushLine(operation diffmatchpatch.Operation) {
+	if d.ExpectedLine.String() == d.ActualLine.String() {
+		if operation == diffmatchpatch.DiffEqual {
+			d.Text = append(d.Text, d.ExpectedGroupBuffer...)
+			d.Text = append(d.Text, d.ActualGroupBuffer...)
+
+			d.ExpectedGroupBuffer = make([]textLine, 0)
+			d.ActualGroupBuffer = make([]textLine, 0)
+
+			d.Text = append(d.Text, textLine{
+				Text:      pterm.FgGray.Sprintf("(%*d. %s) ", d.CounterWidth, d.ActualI, d.UnchangedPrefix) + d.ExpectedLine.String() + "\n",
+				Operation: operation,
+			})
+
+			d.ActualI++
+			d.ExpectedI = d.ActualI
+
+			d.ExpectedFlushable = true
+			d.ActualFlushable = true
+		} else if operation == diffmatchpatch.DiffDelete {
+			d.ExpectedGroupBuffer = append(d.ExpectedGroupBuffer, textLine{
+				Text:      pterm.FgGray.Sprintfln("(%*d. %s) %s", d.CounterWidth, d.ExpectedI, d.ExpectedPrefix, pterm.FgRed.Sprint(d.ExpectedLine.String())),
+				Operation: diffmatchpatch.DiffDelete,
+			})
+			d.ExpectedI++
+			d.ExpectedFlushable = true
+		} else {
+			d.ActualGroupBuffer = append(d.ActualGroupBuffer, textLine{
+				Text:      pterm.FgGray.Sprintfln("(%*d. %s) %s", d.CounterWidth, d.ActualI, d.ActualPrefix, pterm.FgGreen.Sprint(d.ActualLine.String())),
+				Operation: diffmatchpatch.DiffInsert,
+			})
+			d.ActualI++
+			d.ActualFlushable = true
+		}
+	} else {
+		if d.ExpectedFlushable {
+			d.ExpectedGroupBuffer = append(d.ExpectedGroupBuffer, textLine{
+				Text:      pterm.FgGray.Sprintfln("(%*d. %s) %s", d.CounterWidth, d.ExpectedI, d.ExpectedPrefix, pterm.FgRed.Sprint(d.ExpectedLine.String())),
+				Operation: diffmatchpatch.DiffDelete,
+			})
+			d.ExpectedI++
+		}
+
+		if d.ActualFlushable {
+			d.ActualGroupBuffer = append(d.ActualGroupBuffer, textLine{
+				Text:      pterm.FgGray.Sprintfln("(%*d. %s) %s", d.CounterWidth, d.ActualI, d.ActualPrefix, pterm.FgGreen.Sprint(d.ActualLine.String())),
+				Operation: diffmatchpatch.DiffInsert,
+			})
+			d.ActualI++
+		}
+	}
+
+	if d.ExpectedFlushable {
+		d.ExpectedFlushable = false
+		d.ExpectedLine.Reset()
+	}
+
+	if d.ActualFlushable {
+		d.ActualFlushable = false
+		d.ActualLine.Reset()
+	}
+
+	d.DiffBuffer.Reset()
+}
+
+func (d *diffPrinter) Finalize(operation diffmatchpatch.Operation) string {
+	d.ExpectedFlushable = true
+	d.ActualFlushable = true
+
+	if d.ExpectedLine.Len() > 0 || d.ActualLine.Len() > 0 {
+		d.FlushLine(operation)
+	}
+
+	d.Text = append(d.Text, d.ExpectedGroupBuffer...)
+	d.Text = append(d.Text, d.ActualGroupBuffer...)
+
+	var resultBuffer strings.Builder
+	if DiffContextLines >= 0 {
+		requiredLines := make(map[int]bool)
+
+		for i, line := range d.Text {
+			if line.Operation != diffmatchpatch.DiffEqual {
+				for j := int(math.Max(0, float64(i-DiffContextLines))); j < int(math.Min(float64(len(d.Text)), float64(i+DiffContextLines+1))); j++ {
+					requiredLines[j] = true
+				}
+			}
+		}
+
+		hasSnip := false
+		for i, line := range d.Text {
+			if _, ok := requiredLines[i]; ok {
+				resultBuffer.WriteString(line.Text)
+				hasSnip = false
+			} else if !hasSnip {
+				resultBuffer.WriteString(pterm.FgMagenta.Sprint("[...]\n"))
+				hasSnip = true
+			}
+		}
+	} else {
+		for _, line := range d.Text {
+			resultBuffer.WriteString(line.Text)
 		}
 	}
-	text = newText
 
-	return text
+	return resultBuffer.String()
 }
diff --git a/internal/fail-printer.go b/internal/fail-printer.go
index 1ba258c..ed057c4 100644
--- a/internal/fail-printer.go
+++ b/internal/fail-printer.go
@@ -37,6 +37,24 @@ func NewObjectsExpectedActual(expected, actual interface{}) Objects {
 	}
 }
 
+func NewObjectsExpectedActualWithDiff(expected, actual interface{}) Objects {
+	return Objects{
+		{
+			Name:      "Expected",
+			NameStyle: pterm.NewStyle(pterm.FgLightGreen),
+			Data:      expected,
+			DataStyle: pterm.NewStyle(pterm.FgGreen),
+		},
+		{
+			Name:      "Actual",
+			NameStyle: pterm.NewStyle(pterm.FgLightRed),
+			Data:      actual,
+			DataStyle: pterm.NewStyle(pterm.FgRed),
+		},
+		NewDiffObject(expected, actual),
+	}
+}
+
 func NewObjectsUnknown(objs ...interface{}) Objects {
 	return Objects{
 		{
@@ -89,20 +107,6 @@ func FailS(message string, objects Objects, args ...interface{}) string {
 		message += "\n"
 	}
 
-	if len(objects) == 2 {
-
-		if strings.Count(spew.Sdump(objects[0].Data), "\n")+strings.Count(spew.Sdump(objects[1].Data), "\n") > 4 &&
-			objects[0].Name == "Expected" &&
-			objects[1].Name == "Actual" {
-			objects = append(Objects{{
-				Name:      "Difference",
-				NameStyle: pterm.NewStyle(pterm.FgYellow),
-				Data:      GetDifference(objects[0].Data, objects[1].Data),
-				Raw:       true,
-			}}, objects...)
-		}
-	}
-
 	for i, v := range objects {
 		if v.DataStyle == nil {
 			objects[i].DataStyle = pterm.NewStyle()
diff --git a/snapshot.go b/snapshot.go
index a6c8b06..a307a9e 100644
--- a/snapshot.go
+++ b/snapshot.go
@@ -5,6 +5,8 @@ import (
 	"io/ioutil"
 	"os"
 	"path"
+	"regexp"
+	"strconv"
 	"strings"
 
 	"github.com/MarvinJWendt/testza/internal"
@@ -56,6 +58,8 @@ func SnapshotValidate(t testRunner, name string, actual interface{}, msg ...inte
 	return snapshotValidateFromDir(dir, t, name, actual, msg...)
 }
 
+var snapshotStringMatcher = regexp.MustCompile(`(?m)^\(.+?\)\s\(len=\d+\)\s(".+")$`)
+
 func snapshotValidateFromDir(dir string, t testRunner, name string, actual interface{}, msg ...interface{}) error {
 	snapshotPath := path.Clean(dir + name + ".testza")
 	snapshotContent, err := ioutil.ReadFile(snapshotPath)
@@ -67,17 +71,26 @@ func snapshotValidateFromDir(dir string, t testRunner, name string, actual inter
 	spew.Config.DisablePointerAddresses = true
 
 	if spew.Sdump(actual) != snapshot {
+		var diffObject *internal.Object
+		if strActual, ok := actual.(string); ok {
+			if match := snapshotStringMatcher.FindStringSubmatch(snapshot); len(match) > 0 {
+				if unquoted, err := strconv.Unquote(match[1]); err == nil {
+					object := internal.NewDiffObject(unquoted, strActual, true)
+					diffObject = &object
+				}
+			}
+		}
+
+		if diffObject == nil {
+			object := internal.NewDiffObject(snapshot, spew.Sdump(actual), true)
+			diffObject = &object
+		}
+
 		internal.Fail(t,
 			generateMsg(msg,
 				fmt.Sprintf("Snapshot '%s' failed to validate", name)),
 			internal.Objects{
-				{
-					Name:      "Difference",
-					NameStyle: pterm.NewStyle(pterm.FgLightYellow),
-					Data:      internal.GetDifference(snapshot, spew.Sdump(actual), true),
-					DataStyle: pterm.NewStyle(pterm.FgGreen),
-					Raw:       true,
-				},
+				*diffObject,
 				{
 					Name:      "Expected",
 					NameStyle: pterm.NewStyle(pterm.FgLightGreen),


Test patch:
diff --git a/assert_test.go b/assert_test.go
index a7563a3..f578980 100644
--- a/assert_test.go
+++ b/assert_test.go
@@ -9,6 +9,8 @@ import (
 	"os"
 	"reflect"
 	"regexp"
+	"strconv"
+	"strings"
 	"testing"
 	"time"
 
@@ -1342,4 +1344,118 @@ func TestOutputConsistency(t *testing.T) {
 		err := SnapshotCreateOrValidate(t, t.Name(), tm.ErrorMessage)
 		AssertNoError(t, err)
 	})
+
+	t.Run("failed test string", func(t *testing.T) {
+		const expected = `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+Fusce consectetur quam id turpis blandit, pulvinar fermentum justo sollicitudin.
+Curabitur vehicula eros posuere efficitur egestas.
+
+Duis et lectus in nisi mattis convallis non a odio.
+Proin pulvinar felis consectetur condimentum tincidunt.`
+
+		const actual = `Lorem ipsum sit amet, consechbtetur adipiscing elit.z
+
+zFusce consecetur quam iod turpis blandit, pulvnar fermentum justo sollicitudin.
+Curdabitur veicular eros posuere hello efficitulr egestas.
+Duis et lectus in nisi mattis convallis non a odio.
+Proin puvinar feliss consectetur codiementum tincidunt.`
+
+		pterm.DisableStyling()
+		AssertEqual(&tm, expected, actual)
+		pterm.EnableStyling()
+
+		err := SnapshotCreateOrValidate(t, t.Name(), tm.ErrorMessage)
+		AssertNoError(t, err)
+	})
+
+	t.Run("failed test struct", func(t *testing.T) {
+		pterm.DisableStyling()
+		AssertEqual(&tm, assertionTestStruct{
+			Name: "John",
+			Age:  34,
+			Meta: assertionTestStructNested{
+				ID:    512345,
+				Admin: false,
+			},
+		}, assertionTestStruct{
+			Name: "Bob",
+			Age:  22,
+			Meta: assertionTestStructNested{
+				ID:    123456,
+				Admin: true,
+			},
+		})
+		pterm.EnableStyling()
+
+		err := SnapshotCreateOrValidate(t, t.Name(), tm.ErrorMessage)
+		AssertNoError(t, err)
+	})
+
+	t.Run("failed test slice", func(t *testing.T) {
+		pterm.DisableStyling()
+		AssertEqual(&tm, nil, []int{1, 2, 3})
+		pterm.EnableStyling()
+
+		err := SnapshotCreateOrValidate(t, t.Name(), tm.ErrorMessage)
+		AssertNoError(t, err)
+	})
+
+	t.Run("failed test newlines", func(t *testing.T) {
+		pterm.DisableStyling()
+		AssertEqual(&tm, `1234567890`, `1
+2
+3
+4
+5
+6
+7
+8
+9
+0`)
+		pterm.EnableStyling()
+
+		err := SnapshotCreateOrValidate(t, t.Name(), tm.ErrorMessage)
+		AssertNoError(t, err)
+	})
+
+	t.Run("failed test newline start", func(t *testing.T) {
+		pterm.DisableStyling()
+		AssertEqual(&tm, `1`, `
+1`)
+		pterm.EnableStyling()
+
+		err := SnapshotCreateOrValidate(t, t.Name(), tm.ErrorMessage)
+		AssertNoError(t, err)
+	})
+
+	t.Run("failed test newline end", func(t *testing.T) {
+		pterm.DisableStyling()
+		AssertEqual(&tm, `1`, `1
+`)
+		pterm.EnableStyling()
+
+		err := SnapshotCreateOrValidate(t, t.Name(), tm.ErrorMessage)
+		AssertNoError(t, err)
+	})
+
+	t.Run("failed test many matching lines", func(t *testing.T) {
+		dataExpected := make([]string, 50)
+		dataActual := make([]string, 50)
+		for i := 0; i < len(dataExpected); i++ {
+			dataExpected[i] = strconv.Itoa(i)
+			dataActual[i] = dataExpected[i]
+		}
+
+		dataActual[10] = "hello"
+		dataActual[20] = "world"
+		dataActual[30] = "foo"
+		dataActual[40] = "bar"
+
+		pterm.DisableStyling()
+		AssertEqual(&tm, strings.Join(dataExpected, "\n"), strings.Join(dataActual, "\n"))
+		pterm.EnableStyling()
+
+		err := SnapshotCreateOrValidate(t, t.Name(), tm.ErrorMessage)
+		AssertNoError(t, err)
+	})
 }
diff --git a/configuration_test.go b/configuration_test.go
index 68ca257..c10c593 100644
--- a/configuration_test.go
+++ b/configuration_test.go
@@ -57,3 +57,21 @@ func TestSetShowStartupMessage(t *testing.T) {
 		AssertTrue(t, showStartupMessage)
 	})
 }
+
+func TestSetEqualContextLineCount(t *testing.T) {
+	t.Run("Default is 2", func(t *testing.T) {
+		AssertEqual(t, 2, internal.DiffContextLines)
+	})
+
+	t.Run("Set to -1", func(t *testing.T) {
+		SetDiffContextLines(-1)
+		AssertEqual(t, -1, internal.DiffContextLines)
+	})
+
+	t.Run("Set to 3", func(t *testing.T) {
+		SetDiffContextLines(3)
+		AssertEqual(t, 3, internal.DiffContextLines)
+	})
+
+	SetDiffContextLines(2)
+}
diff --git a/snapshot_test.go b/snapshot_test.go
index 8be2dda..53fd2a6 100644
--- a/snapshot_test.go
+++ b/snapshot_test.go
@@ -25,6 +25,9 @@ var snapshotObject = snapshotObjectType{
 }
 var snapshotName = "TestSnapshotCreate_file_content"
 
+var snapshotString = `hello world`
+var snapshotNameString = "TestSnapshotCreate_file_content_string"
+
 func TestSnapshotCreate_file_content(t *testing.T) {
 	err := testza.SnapshotCreate(snapshotName, snapshotObject)
 	testza.AssertNoError(t, err)
@@ -35,9 +38,22 @@ func TestSnapshotCreate_file_content(t *testing.T) {
 	testza.AssertEqual(t, spew.Sdump(snapshotObject), string(snapshotContent))
 }
 
+func TestSnapshotCreate_file_content_string(t *testing.T) {
+	err := testza.SnapshotCreate(snapshotNameString, snapshotString)
+	testza.AssertNoError(t, err)
+
+	snapshotContent, err := ioutil.ReadFile(internal.GetCurrentScriptDirectory() + "/testdata/snapshots/" + t.Name() + ".testza")
+	testza.AssertNoError(t, err)
+
+	testza.AssertEqual(t, spew.Sdump(snapshotString), string(snapshotContent))
+}
+
 func TestSnapshotValidate(t *testing.T) {
 	err := testza.SnapshotValidate(t, snapshotName, snapshotObject)
 	testza.AssertNoError(t, err)
+
+	err = testza.SnapshotValidate(t, snapshotNameString, snapshotString)
+	testza.AssertNoError(t, err)
 }
 
 func TestSnapshotValidate_fails(t *testing.T) {
@@ -47,6 +63,11 @@ func TestSnapshotValidate_fails(t *testing.T) {
 		err := testza.SnapshotValidate(t, snapshotName, modifiedSnapshotObject)
 		testza.AssertNoError(t, err)
 	})
+
+	testza.AssertTestFails(t, func(t testza.TestingPackageWithFailFunctions) {
+		err := testza.SnapshotValidate(t, snapshotNameString, `foo bar`)
+		testza.AssertNoError(t, err)
+	})
 }
 
 func TestSnapshotCreateOrValidate(t *testing.T) {
diff --git a/testdata/snapshots/TestOutputConsistency/failed_test.testza b/testdata/snapshots/TestOutputConsistency/failed_test.testza
old mode 100644
new mode 100755
index 87140ee..e57ace0
--- a/testdata/snapshots/TestOutputConsistency/failed_test.testza
+++ b/testdata/snapshots/TestOutputConsistency/failed_test.testza
@@ -1,1 +1,1 @@
-(string) (len=139) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Expected:\n   3| (bool) true\n   4| \n   5| Actual:\n   6| (bool) false\n\n"
+(string) (len=215) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Expected:\n   3| (bool) true\n   4| \n   5| Actual:\n   6| (bool) false\n   7| \n   8| Difference:\n   9| (1. -) (bool) true\n  10| (1. +) (bool) false\n\n"
diff --git a/testdata/snapshots/TestOutputConsistency/failed_test_many_matching_lines.testza b/testdata/snapshots/TestOutputConsistency/failed_test_many_matching_lines.testza
new file mode 100755
index 0000000..6e72c00
--- /dev/null
+++ b/testdata/snapshots/TestOutputConsistency/failed_test_many_matching_lines.testza
@@ -0,0 +1,1 @@
+(string) (len=1041) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Expected:\n   3| (string) (len=139) \"0\\n1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\n9\\n10\\n11\\n12\\n13\\n14\\n15\\n16\\n17\\n18\\n19\\n20\\n21\\n22\\n23\\n24\\n25\\n26\\n27\\n28\\n29\\n30\\n31\\n32\\n33\\n34\\n35\\n36\\n37\\n38\\n39\\n40\\n41\\n42\\n43\\n44\\n45\\n46\\n47\\n48\\n49\"\n   4| \n   5| Actual:\n   6| (string) (len=147) \"0\\n1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\n9\\nhello\\n11\\n12\\n13\\n14\\n15\\n16\\n17\\n18\\n19\\nworld\\n21\\n22\\n23\\n24\\n25\\n26\\n27\\n28\\n29\\nfoo\\n31\\n32\\n33\\n34\\n35\\n36\\n37\\n38\\n39\\nbar\\n41\\n42\\n43\\n44\\n45\\n46\\n47\\n48\\n49\"\n   7| \n   8| Difference:\n   9| [...]\n  10| ( 9. #) 8\n  11| (10. #) 9\n  12| (11. -) 10\n  13| (11. +) hello\n  14| (12. #) 11\n  15| (13. #) 12\n  16| [...]\n  17| (19. #) 18\n  18| (20. #) 19\n  19| (21. -) 20\n  20| (21. +) world\n  21| (22. #) 21\n  22| (23. #) 22\n  23| [...]\n  24| (29. #) 28\n  25| (30. #) 29\n  26| (31. -) 30\n  27| (31. +) foo\n  28| (32. #) 31\n  29| (33. #) 32\n  30| [...]\n  31| (39. #) 38\n  32| (40. #) 39\n  33| (41. -) 40\n  34| (41. +) bar\n  35| (42. #) 41\n  36| (43. #) 42\n  37| [...]\n\n"
diff --git a/testdata/snapshots/TestOutputConsistency/failed_test_newline_end.testza b/testdata/snapshots/TestOutputConsistency/failed_test_newline_end.testza
new file mode 100755
index 0000000..3d706a7
--- /dev/null
+++ b/testdata/snapshots/TestOutputConsistency/failed_test_newline_end.testza
@@ -0,0 +1,1 @@
+(string) (len=227) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Expected:\n   3| (string) (len=1) \"1\"\n   4| \n   5| Actual:\n   6| (string) (len=2) \"1\\n\"\n   7| \n   8| Difference:\n   9| (1. -) 1\n  10| (1. +) 1\n  11| (2. +) \n\n"
diff --git a/testdata/snapshots/TestOutputConsistency/failed_test_newline_start.testza b/testdata/snapshots/TestOutputConsistency/failed_test_newline_start.testza
new file mode 100755
index 0000000..32098de
--- /dev/null
+++ b/testdata/snapshots/TestOutputConsistency/failed_test_newline_start.testza
@@ -0,0 +1,1 @@
+(string) (len=212) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Expected:\n   3| (string) (len=1) \"1\"\n   4| \n   5| Actual:\n   6| (string) (len=2) \"\\n1\"\n   7| \n   8| Difference:\n   9| (1. +) \n  10| (2. #) 1\n\n"
diff --git a/testdata/snapshots/TestOutputConsistency/failed_test_newlines.testza b/testdata/snapshots/TestOutputConsistency/failed_test_newlines.testza
new file mode 100755
index 0000000..09d085d
--- /dev/null
+++ b/testdata/snapshots/TestOutputConsistency/failed_test_newlines.testza
@@ -0,0 +1,1 @@
+(string) (len=404) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Expected:\n   3| (string) (len=10) \"1234567890\"\n   4| \n   5| Actual:\n   6| (string) (len=19) \"1\\n2\\n3\\n4\\n5\\n6\\n7\\n8\\n9\\n0\"\n   7| \n   8| Difference:\n   9| ( 1. -) 1234567890\n  10| ( 1. +) 1\n  11| ( 2. +) 2\n  12| ( 3. +) 3\n  13| ( 4. +) 4\n  14| ( 5. +) 5\n  15| ( 6. +) 6\n  16| ( 7. +) 7\n  17| ( 8. +) 8\n  18| ( 9. +) 9\n  19| (10. +) 0\n\n"
diff --git a/testdata/snapshots/TestOutputConsistency/failed_test_slice.testza b/testdata/snapshots/TestOutputConsistency/failed_test_slice.testza
new file mode 100755
index 0000000..9dd6c1d
--- /dev/null
+++ b/testdata/snapshots/TestOutputConsistency/failed_test_slice.testza
@@ -0,0 +1,1 @@
+(string) (len=393) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Expected:\n   3| (interface {}) <nil>\n   4| \n   5| Actual:\n   6| ([]int) (len=3 cap=3) {\n   7|  (int) 1,\n   8|  (int) 2,\n   9|  (int) 3\n  10| }\n  11| \n  12| Difference:\n  13| (1. -) (interface {}) <nil>\n  14| (1. +) ([]int) (len=3 cap=3) {\n  15| (2. +)  (int) 1,\n  16| (3. +)  (int) 2,\n  17| (4. +)  (int) 3\n  18| (5. +) }\n\n"
diff --git a/testdata/snapshots/TestOutputConsistency/failed_test_string.testza b/testdata/snapshots/TestOutputConsistency/failed_test_string.testza
new file mode 100755
index 0000000..85b10ad
--- /dev/null
+++ b/testdata/snapshots/TestOutputConsistency/failed_test_string.testza
@@ -0,0 +1,1 @@
+(string) (len=1484) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Expected:\n   3| (string) (len=297) \"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\\nFusce consectetur quam id turpis blandit, pulvinar fermentum justo sollicitudin.\\nCurabitur vehicula eros posuere efficitur egestas.\\n\\nDuis et lectus in nisi mattis convallis non a odio.\\nProin pulvinar felis consectetur condimentum tincidunt.\"\n   4| \n   5| Actual:\n   6| (string) (len=302) \"Lorem ipsum sit amet, consechbtetur adipiscing elit.z\\n\\nzFusce consecetur quam iod turpis blandit, pulvnar fermentum justo sollicitudin.\\nCurdabitur veicular eros posuere hello efficitulr egestas.\\nDuis et lectus in nisi mattis convallis non a odio.\\nProin puvinar feliss consectetur codiementum tincidunt.\"\n   7| \n   8| Difference:\n   9| (1. -) Lorem ipsum dolor sit amet, consectetur adipiscing elit.\n  10| (2. -) Fusce consectetur quam id turpis blandit, pulvinar fermentum justo sollicitudin.\n  11| (3. -) Curabitur vehicula eros posuere efficitur egestas.\n  12| (4. -) \n  13| (1. +) Lorem ipsum sit amet, consechbtetur adipiscing elit.z\n  14| (2. +) \n  15| (3. +) zFusce consecetur quam iod turpis blandit, pulvnar fermentum justo sollicitudin.\n  16| (4. +) Curdabitur veicular eros posuere hello efficitulr egestas.\n  17| (5. #) Duis et lectus in nisi mattis convallis non a odio.\n  18| (6. -) Proin pulvinar felis consectetur condimentum tincidunt.\n  19| (6. +) Proin puvinar feliss consectetur codiementum tincidunt.\n\n"
diff --git a/testdata/snapshots/TestOutputConsistency/failed_test_struct.testza b/testdata/snapshots/TestOutputConsistency/failed_test_struct.testza
new file mode 100755
index 0000000..1dbcb02
--- /dev/null
+++ b/testdata/snapshots/TestOutputConsistency/failed_test_struct.testza
@@ -0,0 +1,1 @@
+(string) (len=1005) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Expected:\n   3| (testza_test.assertionTestStruct) {\n   4|  Name: (string) (len=4) \"John\",\n   5|  Age: (int) 34,\n   6|  Meta: (testza_test.assertionTestStructNested) {\n   7|   ID: (int) 512345,\n   8|   Admin: (bool) false\n   9|  }\n  10| }\n  11| \n  12| Actual:\n  13| (testza_test.assertionTestStruct) {\n  14|  Name: (string) (len=3) \"Bob\",\n  15|  Age: (int) 22,\n  16|  Meta: (testza_test.assertionTestStructNested) {\n  17|   ID: (int) 123456,\n  18|   Admin: (bool) true\n  19|  }\n  20| }\n  21| \n  22| Difference:\n  23| (1. #) (testza_test.assertionTestStruct) {\n  24| (2. -)  Name: (string) (len=4) \"John\",\n  25| (3. -)  Age: (int) 34,\n  26| (2. +)  Name: (string) (len=3) \"Bob\",\n  27| (3. +)  Age: (int) 22,\n  28| (4. #)  Meta: (testza_test.assertionTestStructNested) {\n  29| (5. -)   ID: (int) 512345,\n  30| (6. -)   Admin: (bool) false\n  31| (5. +)   ID: (int) 123456,\n  32| (6. +)   Admin: (bool) true\n  33| (7. #)  }\n  34| (8. #) }\n\n"
diff --git a/testdata/snapshots/TestOutputConsistency/failed_test_with_custom_formatted_msg.testza b/testdata/snapshots/TestOutputConsistency/failed_test_with_custom_formatted_msg.testza
old mode 100644
new mode 100755
index f96d68d..79a746a
--- a/testdata/snapshots/TestOutputConsistency/failed_test_with_custom_formatted_msg.testza
+++ b/testdata/snapshots/TestOutputConsistency/failed_test_with_custom_formatted_msg.testza
@@ -1,1 +1,1 @@
-(string) (len=177) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Message: Custom message!\n   3| \n   4| Expected:\n   5| (bool) true\n   6| \n   7| Actual:\n   8| (bool) false\n\n"
+(string) (len=253) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Message: Custom message!\n   3| \n   4| Expected:\n   5| (bool) true\n   6| \n   7| Actual:\n   8| (bool) false\n   9| \n  10| Difference:\n  11| (1. -) (bool) true\n  12| (1. +) (bool) false\n\n"
diff --git a/testdata/snapshots/TestOutputConsistency/failed_test_with_custom_msg.testza b/testdata/snapshots/TestOutputConsistency/failed_test_with_custom_msg.testza
old mode 100644
new mode 100755
index f96d68d..79a746a
--- a/testdata/snapshots/TestOutputConsistency/failed_test_with_custom_msg.testza
+++ b/testdata/snapshots/TestOutputConsistency/failed_test_with_custom_msg.testza
@@ -1,1 +1,1 @@
-(string) (len=177) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Message: Custom message!\n   3| \n   4| Expected:\n   5| (bool) true\n   6| \n   7| Actual:\n   8| (bool) false\n\n"
+(string) (len=253) "\n\n   0| Two objects that should be equal, are not equal.\n   1| \n   2| Message: Custom message!\n   3| \n   4| Expected:\n   5| (bool) true\n   6| \n   7| Actual:\n   8| (bool) false\n   9| \n  10| Difference:\n  11| (1. -) (bool) true\n  12| (1. +) (bool) false\n\n"
diff --git a/testdata/snapshots/TestSnapshotCreate_file_content_string.testza b/testdata/snapshots/TestSnapshotCreate_file_content_string.testza
new file mode 100755
index 0000000..8b08678
--- /dev/null
+++ b/testdata/snapshots/TestSnapshotCreate_file_content_string.testza
@@ -0,0 +1,1 @@
+(string) (len=11) "hello world"