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) +}