testmark-go

testexec: first pass on how regen might work.

commit 699341b1a297b874994b73598e9d574ee9b6dcb0 · parent 82f06f1d3e · browse this tree
Eric Myhre, 2021-09-19 15:19:10 UTC

This will probably change again, nearly immediately.
I'm not thrilled with the ergonomics of a `*[]` -- unboxing all that
in common cases pushes an unreasonable amount of work to the user.
diff --git a/flags.go b/flags.go
new file mode 100644
index 0000000..c80bc9d
--- /dev/null
+++ b/flags.go
@@ -0,0 +1,7 @@
+package testmark
+
+import (
+	"flag"
+)
+
+var Regen = flag.Bool("testmark.regen", false, "Setting this flag hints to systems using testmark tests that fixtures should be regenerated.")
diff --git a/testexec/testexec.go b/testexec/testexec.go
index 85910d7..8d056ef 100644
--- a/testexec/testexec.go
+++ b/testexec/testexec.go
@@ -54,11 +54,19 @@ type AssertFn func(t *testing.T, actual, expect string)
 // (A nil ExecFn will result in an OS exec being used (specifically: `ExecFn_Exec`);
 // a nil FilterFn means no filtering will occur;
 // a nil AssertFn means a very basic check using t.Errorf will be used.)
+//
+// The 'Patches' a slice for patched hunks will be appended to if `testmark.Regen` is true.
+// (If the pointer is nil, a new slice will be made;
+// if the pointer is handed in before usage, it can be used to accumulate patches
+// batched up with other patches destined for the same document.)
+// It is the user's responsibility to actually apply the patches and save the updated document.
 type Tester struct {
 	ExecFn
 	ScriptFn
 	FilterFn
 	AssertFn
+
+	Patches *[]testmark.Hunk
 }
 
 func (tcfg *Tester) init() {
@@ -116,7 +124,15 @@ func (tcfg *Tester) init() {
 // There is no faculty for ignoring an exitcode.
 // Surely the systems you're so rigorously ensuring the quality of don't have nondeterministic exit codes;
 // so why would you need to do this?  If it's allowed to be anything but zero, it should be expected to be that; and if it's expected, say so.
-func (tcfg Tester) TestSequence(t *testing.T, data testmark.DirEnt) {
+//
+// This test system comes complete with fixture regeneration support.
+// If `testmark.Regen` is true (e.g. you have invoked "go test" with the argument "-testmark.regen"),
+// then instead of making any assertions, this function will accumulate patches
+// in the `Tester.Patches` slice.
+// Regen mode will only update hunks that already exist; it won't add them.
+// As an edge case, note that if that an exitcode hunk is absent, but a nonzero exitcode is encountered,
+// the test will still be failed, even though in patch regen mode most assertions are usually skipped.
+func (tcfg *Tester) TestSequence(t *testing.T, data testmark.DirEnt) {
 	t.Helper()
 	tcfg.init()
 
@@ -164,24 +180,44 @@ func (tcfg Tester) TestSequence(t *testing.T, data testmark.DirEnt) {
 	}
 
 	// Okay, comparisons time.
-	if hunk, exists := data.Children["output"]; exists {
-		t.Run("check-combined-output", func(t *testing.T) {
-			tcfg.AssertFn(t, string(stdout.(*bytes.Buffer).Bytes()), string(hunk.Hunk.Body))
-		})
+	// Or, regen time!
+	if ent, exists := data.Children["output"]; exists {
+		bs := stdout.(*bytes.Buffer).Bytes()
+		if *testmark.Regen {
+			tcfg.maybeAppendPatch(*ent.Hunk, bs)
+		} else {
+			t.Run("check-combined-output", func(t *testing.T) {
+				tcfg.AssertFn(t, string(bs), string(ent.Hunk.Body))
+			})
+		}
 	}
-	if hunk, exists := data.Children["stdout"]; exists {
-		t.Run("check-stdout", func(t *testing.T) {
-			tcfg.AssertFn(t, string(stdout.(*bytes.Buffer).Bytes()), string(hunk.Hunk.Body))
-		})
+	if ent, exists := data.Children["stdout"]; exists {
+		bs := stdout.(*bytes.Buffer).Bytes()
+		if *testmark.Regen {
+			tcfg.maybeAppendPatch(*ent.Hunk, bs)
+		} else {
+			t.Run("check-stdout", func(t *testing.T) {
+				tcfg.AssertFn(t, string(bs), string(ent.Hunk.Body))
+			})
+		}
 	}
-	if hunk, exists := data.Children["stderr"]; exists {
-		t.Run("check-stderr", func(t *testing.T) {
-			tcfg.AssertFn(t, string(stderr.(*bytes.Buffer).Bytes()), string(hunk.Hunk.Body))
-		})
+	if ent, exists := data.Children["stderr"]; exists {
+		bs := stderr.(*bytes.Buffer).Bytes()
+		if *testmark.Regen {
+			tcfg.maybeAppendPatch(*ent.Hunk, bs)
+		} else {
+			t.Run("check-stderr", func(t *testing.T) {
+				tcfg.AssertFn(t, string(bs), string(ent.Hunk.Body))
+			})
+		}
 	}
 	t.Run("check-exitcode", func(t *testing.T) {
-		if hunk, exists := data.Children["exitcode"]; exists {
-			tcfg.AssertFn(t, strconv.Itoa(exitcode), strings.TrimSpace(string(hunk.Hunk.Body)))
+		if ent, exists := data.Children["exitcode"]; exists {
+			if *testmark.Regen {
+				tcfg.maybeAppendPatch(*ent.Hunk, []byte(strconv.Itoa(exitcode)))
+			} else {
+				tcfg.AssertFn(t, strconv.Itoa(exitcode), strings.TrimSpace(string(ent.Hunk.Body)))
+			}
 		} else {
 			tcfg.AssertFn(t, strconv.Itoa(exitcode), "0")
 		}
@@ -190,11 +226,11 @@ func (tcfg Tester) TestSequence(t *testing.T, data testmark.DirEnt) {
 	// TODO: look for "then-*" dirs.
 }
 
-func (tcfg Tester) TestScript(t *testing.T, data testmark.DirEnt) {
+func (tcfg *Tester) TestScript(t *testing.T, data testmark.DirEnt) {
 	panic("not yet implemented")
 }
 
-func (tcfg Tester) Test(t *testing.T, data testmark.DirEnt) {
+func (tcfg *Tester) Test(t *testing.T, data testmark.DirEnt) {
 	panic("not yet implemented")
 }
 
@@ -202,4 +238,17 @@ func (tcfg Tester) Test(t *testing.T, data testmark.DirEnt) {
 
 // Not yet defined if these should complain loudly if they _don't_ find something that matches.  I think being able to shrug is useful; otherwise that check will often get offloaded to callers.
 
-// TODO: "patch" variants of all of the functions.
+func (tcfg *Tester) maybeAppendPatch(hunk testmark.Hunk, newBody []byte) {
+	if !bytes.Equal(hunk.Body, newBody) {
+		hunk.Body = newBody
+		tcfg.appendPatch(hunk)
+	}
+}
+
+func (tcfg *Tester) appendPatch(hunk testmark.Hunk) {
+	if tcfg.Patches == nil {
+		patches := make([]testmark.Hunk, 0)
+		tcfg.Patches = &patches
+	}
+	*tcfg.Patches = append(*tcfg.Patches, hunk)
+}
diff --git a/write.go b/write.go
index 0a6856e..eb5179b 100644
--- a/write.go
+++ b/write.go
@@ -2,6 +2,7 @@ package testmark
 
 import (
 	"io"
+	"os"
 	"strings"
 )
 
@@ -27,3 +28,29 @@ func Write(doc *Document, wr io.Writer) (int, error) {
 	}
 	return n, nil
 }
+
+func WriteFile(doc *Document, filename string) error {
+	f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+	_, err = Write(doc, f)
+	return err
+}
+
+func WriteWithPatches(doc *Document, wr io.Writer, patches ...Hunk) (int, error) {
+	if len(patches) == 0 {
+		return 0, nil
+	}
+	doc = Patch(doc, patches...)
+	return Write(doc, wr)
+}
+
+func WriteFileWithPatches(doc *Document, filename string, patches ...Hunk) error {
+	if len(patches) == 0 {
+		return nil
+	}
+	doc = Patch(doc, patches...)
+	return WriteFile(doc, filename)
+}