Merge pull request #12 from TripleDogDare/recursion-control
commit fc3ad548627b47515c711ef80ae9ef37e3beb061 · parent 095106dc5d · parent 785560439c
· browse this tree
Eric Myhre, 2022-11-27 01:43:03 UTC
Strict mode for testexec structure
diff --git a/index.go b/index.go index 6d3463d..6c56b29 100644 --- a/index.go +++ b/index.go @@ -16,27 +16,28 @@ import ( func (doc *Document) BuildDirIndex() { doc.DirEnt = &DirEnt{} for _, hunk := range doc.DataHunks { - doc.DirEnt.fill(strings.Split(hunk.Name, "/"), hunk.Hunk) + doc.DirEnt.fill(strings.Split(hunk.Name, "/"), 0, hunk.Hunk) } } -func (dirent *DirEnt) fill(pathSegs []string, hunk Hunk) { - if len(pathSegs) == 0 { +func (dirent *DirEnt) fill(pathSegs []string, pathIdx int, hunk Hunk) { + if pathIdx >= len(pathSegs) { dirent.Hunk = &hunk return } if dirent.Children == nil { dirent.Children = make(map[string]*DirEnt) } - if next, exists := dirent.Children[pathSegs[0]]; exists { - next.fill(pathSegs[1:], hunk) - } else { - l := len(dirent.ChildrenList) - child := DirEnt{ - Name: pathSegs[0], - } - dirent.ChildrenList = append(dirent.ChildrenList, &child) - dirent.Children[pathSegs[0]] = &child - dirent.ChildrenList[l].fill(pathSegs[1:], hunk) + if next, exists := dirent.Children[pathSegs[pathIdx]]; exists { + next.fill(pathSegs, pathIdx+1, hunk) + return } + + l := len(dirent.ChildrenList) + dirent.ChildrenList = append(dirent.ChildrenList, &DirEnt{ + Name: pathSegs[pathIdx], + Path: strings.Join(pathSegs[:pathIdx+1], "/"), + }) + dirent.Children[pathSegs[pathIdx]] = dirent.ChildrenList[l] + dirent.ChildrenList[l].fill(pathSegs, pathIdx+1, hunk) } diff --git a/testexec/invalidexercise.md b/testexec/invalidexercise.md new file mode 100644 index 0000000..b8440e6 --- /dev/null +++ b/testexec/invalidexercise.md @@ -0,0 +1,37 @@ +testexec invalid file +=== + +Shows examples of invalid testexec files + +--- + +Blocks cannot test combined output and stdout + +[testmark]:# (stdout-combo/script) +``` + echo "hi" +``` +[testmark]:# (stdout-combo/output) +``` +hi +``` +[testmark]:# (stdout-combo/stdout) +``` +hi +``` + +--- + +Similarly, blocks may not test combined output and stderr + +[testmark]:# (stderr-combo/script) +``` +echo "hi" +``` +[testmark]:# (stderr-combo/output) +``` +hi +``` +[testmark]:# (stderr-combo/stderr) +``` +``` diff --git a/testexec/selfexercise.md b/testexec/selfexercise.md index 67d4842..0052615 100644 --- a/testexec/selfexercise.md +++ b/testexec/selfexercise.md @@ -99,3 +99,4 @@ cat - | sed 's/ is/ was/' | sed s/will/should/ ``` this was stdin and should be echoed ``` + diff --git a/testexec/strictexercise.md b/testexec/strictexercise.md new file mode 100644 index 0000000..5182591 --- /dev/null +++ b/testexec/strictexercise.md @@ -0,0 +1,47 @@ +testexec strict file +=== + +Shows examples of poorly formed testexec files. These will be skipped if strict mode is disabled. + +--- + +A hunk with an empty intermediate step will not execute. + +[testmark]:# (parent/script) +``` + # no-op +``` + +[testmark]:# (parent/then-missing-script/then-another-thing/script) +``` + # no-op, this will not be reached because it missing executable blocks in parent steps +``` + +--- + +Recursion won't happen if child blocks don't begin with `then-` +Any unrecognized pattern will be an error in strict mode. + +[testmark]:# (norecurse/script) +``` + # no-op +``` +[testmark]:# (norecurse/not-a-then-statement/script) +``` + # no-op, this script will not run because the subdirectory does not begin with "then-" +``` + +--- + +Blocks can't contain both a `script` and a `sequence` child + +[testmark]:# (multiexec/script) +``` + # no-op, this script will not run because it has both types of exec blocks +``` + +[testmark]:# (multiexec/sequence) +``` + # no-op, this script will not run because it has both types of exec blocks +``` + diff --git a/testexec/testexec.go b/testexec/testexec.go index 8006d83..cf19f17 100644 --- a/testexec/testexec.go +++ b/testexec/testexec.go @@ -68,6 +68,8 @@ type Tester struct { AssertFn Patches *testmark.PatchAccumulator + // Will allow unrecognized directory structures. + DisableStrictMode bool } func (tcfg *Tester) init() { @@ -161,21 +163,22 @@ func (tcfg Tester) test(t *testing.T, data *testmark.DirEnt, allowExec, allowScr sequenceHunk, sequenceMode := data.Children["sequence"] scriptHunk, scriptMode := data.Children["script"] if !sequenceMode && !scriptMode { - return + tcfg.skipOrFailStrictlyf(t, "dir %q does not contain a 'script' or 'sequence' hunk", data.Path) } if sequenceMode && scriptMode { - t.Logf("warning: dir %q contained both a 'script' and a 'sequence' hunk, which is nonsensical", data.Name) - t.SkipNow() + tcfg.skipOrFailStrictlyf(t, "dir %q contained both a 'script' and a 'sequence' hunk, which is nonsensical", data.Path) } if sequenceMode && !allowExec { - t.Skipf("found sequence hunk but the test framework was invoked without permission to run those") + tcfg.skipOrFailStrictlyf(t, "found sequence hunk but the test framework was invoked without permission to run those") } if scriptMode && !allowScript { - t.Skipf("found script hunk but the test framework was invoked without permission to run those") + tcfg.skipOrFailStrictlyf(t, "found script hunk but the test framework was invoked without permission to run those") } if *testmark.Regen && tcfg.Patches == nil { - t.Logf("warning: testmark.regen mode engaged, but there is no patch accumulator available here") - t.Skipf("nothing to do if requested to regenerate test fixtures but have nowhere to put data") + tcfg.skipOrFailStrictlyf(t, "%s\n%s", + "testmark.regen mode engaged, but there is no patch accumulator available here", + "nothing to do if requested to regenerate test fixtures but have nowhere to put data", + ) } // Create a tempdir, and fill it with any files. @@ -250,14 +253,17 @@ func (tcfg Tester) test(t *testing.T, data *testmark.DirEnt, allowExec, allowScr // Do the thing. switch { case sequenceMode: + t.Logf("exec: %q", sequenceHunk.Hunk.Name) exitcode = tcfg.doSequence(t, sequenceHunk.Hunk, stdin, stdout, stderr) case scriptMode: + t.Logf("exec: %q", scriptHunk.Hunk.Name) exitcode = tcfg.doScript(t, scriptHunk.Hunk, stdin, stdout, stderr) } // Okay, comparisons time. // Or, regen time! if ent, exists := data.Children["output"]; exists { + // stdout buffer should be prepared to be both stdout and stderr earlier before execution. bs := stdout.(*bytes.Buffer).Bytes() if *testmark.Regen { tcfg.Patches.AppendPatchIfBodyDiffers(*ent.Hunk, bs) @@ -299,21 +305,49 @@ func (tcfg Tester) test(t *testing.T, data *testmark.DirEnt, allowExec, allowScr } }) - // Look for "then-*" dirs. - // If we're already failed -- make the run block, but skip it. - // If we're going to procede: make a new tempdir, copy the contents, and then procede by recursing. + tcfg.recurse(t, data, allowExec, allowScript, dir) +} + +func (tcfg Tester) recurse(t *testing.T, data *testmark.DirEnt, allowExec bool, allowScript bool, parentTmpDir string) { + alreadyFailed := t.Failed() for _, child := range data.ChildrenList { - if len(child.Name) > 5 && strings.HasPrefix(child.Name, "then-") { - alreadyFailed := t.Failed() - t.Run(child.Name, func(t *testing.T) { - if alreadyFailed { - t.Skipf("parent commands failed, so while more commands are specified, testing them is not meaningful") - } - tcfg.test(t, child, allowExec, allowScript, dir) - }) + if _, exists := leafNodeTable[child.Name]; exists { + t.Logf("%s will not recurse into special leaf node %q", t.Name(), child.Name) + continue } + t.Run(child.Name, func(t *testing.T) { + if len(child.Name) <= 5 || !strings.HasPrefix(child.Name, "then-") { + tcfg.skipOrFailStrictlyf(t, "%q does not begin with %q", child.Name, "then-") + } + if alreadyFailed { + // This comes after the recursion test because a file structure error should still fail + t.Skipf("parent commands failed, so while more commands are specified, testing them is not meaningful") + } + tcfg.test(t, child, allowExec, allowScript, parentTmpDir) + }) } +} + +func (tcfg Tester) skipOrFailStrictlyf(t *testing.T, format string, args ...interface{}) { + if format != "" || len(args) > 0 { + t.Logf(format, args...) + } + if tcfg.DisableStrictMode { + t.SkipNow() + } + t.FailNow() +} +// Hash Table of all the "special" nodes used by testexec. +var leafNodeTable = map[string]struct{}{ + "exitcode": {}, + "stderr": {}, + "stdout": {}, + "output": {}, + "input": {}, + "sequence": {}, + "script": {}, + "fs": {}, } func (tcfg Tester) doSequence(t *testing.T, hunk *testmark.Hunk, stdin io.Reader, stdout, stderr io.Writer) (exitcode int) { diff --git a/testexec/testexec_test.go b/testexec/testexec_test.go index 0701f4a..c712e2e 100644 --- a/testexec/testexec_test.go +++ b/testexec/testexec_test.go @@ -1,13 +1,16 @@ package testexec_test import ( + "flag" "testing" "github.com/warpfork/go-testmark" "github.com/warpfork/go-testmark/testexec" ) -func Test(t *testing.T) { +var RunFailTest = flag.Bool("run-fail-test", false, "Executes the tests which are expected to fail") + +func TestSelfExercise(t *testing.T) { filename := "selfexercise.md" doc, err := testmark.ReadFile(filename) if err != nil { @@ -26,3 +29,70 @@ func Test(t *testing.T) { } patches.WriteFileWithPatches(doc, filename) } + +func TestInvalid(t *testing.T) { + if !(*RunFailTest) { + t.Skipf("%s requires %q flag to execute", t.Name(), "run-fail-test") + } + filename := "invalidexercise.md" + doc, err := testmark.ReadFile(filename) + if err != nil { + t.Fatalf("spec file parse failed?!: %s", err) + } + + doc.BuildDirIndex() + patches := testmark.PatchAccumulator{} + for _, dir := range doc.DirEnt.ChildrenList { + t.Run(dir.Name, func(t *testing.T) { + test := testexec.Tester{ + Patches: &patches, + } + test.TestScript(t, dir) + }) + } + patches.WriteFileWithPatches(doc, filename) +} + +func TestStrict(t *testing.T) { + if !(*RunFailTest) { + t.Skipf("%s requires %q flag to execute", t.Name(), "run-fail-test") + } + filename := "strictexercise.md" + doc, err := testmark.ReadFile(filename) + if err != nil { + t.Fatalf("spec file parse failed?!: %s", err) + } + + doc.BuildDirIndex() + patches := testmark.PatchAccumulator{} + for _, dir := range doc.DirEnt.ChildrenList { + t.Run(dir.Name, func(t *testing.T) { + test := testexec.Tester{ + Patches: &patches, + } + test.TestScript(t, dir) + }) + } + patches.WriteFileWithPatches(doc, filename) +} + +func TestStrictDisabled(t *testing.T) { + filename := "strictexercise.md" + doc, err := testmark.ReadFile(filename) + if err != nil { + t.Fatalf("spec file parse failed?!: %s", err) + } + + doc.BuildDirIndex() + patches := testmark.PatchAccumulator{} + for _, dir := range doc.DirEnt.ChildrenList { + t.Run(dir.Name, func(t *testing.T) { + test := testexec.Tester{ + Patches: &patches, + DisableStrictMode: true, + } + test.TestScript(t, dir) + }) + } + patches.WriteFileWithPatches(doc, filename) +} diff --git a/testmark.go b/testmark.go index 4113279..5603c0d 100644 --- a/testmark.go +++ b/testmark.go @@ -67,6 +67,9 @@ type DirEnt struct { // (Note that if there's a Hunk in this DirEnt, its name may be different -- it still has the *full* path name.) Name string + // The fullpath + Path string + // A hunk, or nil. Hunk *Hunk