diff --git a/compilers/openapi/cycles_test.go b/compilers/openapi/cycles_test.go index f28ebff..0446436 100644 --- a/compilers/openapi/cycles_test.go +++ b/compilers/openapi/cycles_test.go @@ -3,6 +3,7 @@ package openapi import ( "fmt" "os" + "path/filepath" "strings" "testing" @@ -53,6 +54,34 @@ var cycleReproducers = []struct{ name, file string }{ {"pointer-whitespace-self", "cycle_pointer_whitespace_self"}, } +// TestCycleReproducers_EveryFixtureIsExercised holds cycleReproducers to the +// fixtures on disk, the way TestDanglingRefs_EveryReproducerIsExercised holds +// its own table: a cycle_*.yaml added to the corpus and left out of the table is +// compiled by nothing here, and the suite stays green while the corpus grows +// past it. +// +// The scan package keeps a table of the same fixtures for its own assertion and +// carries the twin of this test. Holding both to one directory is what stops the +// two from drifting apart as well as from the corpus, which neither package can +// check directly — a test package cannot import another's. +func TestCycleReproducers_EveryFixtureIsExercised(t *testing.T) { + t.Parallel() + onDisk, err := filepath.Glob(filepath.Join(reproducerDir, "cycle_*.yaml")) + require.NoError(t, err, "globbing the reproducer corpus") + require.NotEmpty(t, onDisk, "the corpus holds cycle reproducers") + + listed := make(map[string]bool, len(cycleReproducers)) + for _, tc := range cycleReproducers { + listed[tc.file+".yaml"] = true + } + for _, path := range onDisk { + assert.True(t, listed[filepath.Base(path)], + "%s is in %s but not in cycleReproducers", filepath.Base(path), reproducerDir) + } + assert.Len(t, cycleReproducers, len(onDisk), + "cycleReproducers and %s hold different numbers of fixtures", reproducerDir) +} + func TestCompile_CyclicSpecDoesNotCrash(t *testing.T) { t.Parallel() for _, tc := range cycleReproducers { @@ -249,9 +278,16 @@ components: } } +// reproducerDir is where the cycle fixtures live. The reader below, the table +// guard above and the fuzz seeds all derive their paths from it, so none of them +// can end up looking somewhere the others are not — which matters most for the +// seed loader, since it ignores a read error and a wrong path there would cost +// the fuzzer its seeds in silence. +const reproducerDir = "../../testdata/openapi" + func readReproducer(t *testing.T, file string) []byte { t.Helper() - data, err := os.ReadFile("../../testdata/openapi/" + file + ".yaml") + data, err := os.ReadFile(filepath.Join(reproducerDir, file+".yaml")) require.NoError(t, err) return data } @@ -285,14 +321,14 @@ func TestCompile_MergeChainPastBoundStillCompiles(t *testing.T) { func FuzzCycleDetector(f *testing.F) { for _, tc := range cycleReproducers { - if data, err := os.ReadFile("../../testdata/openapi/" + tc.file + ".yaml"); err == nil { + if data, err := os.ReadFile(filepath.Join(reproducerDir, tc.file+".yaml")); err == nil { f.Add(data) } } for _, tc := range refShapedDataSpecs { f.Add([]byte(tc.data)) } - if data, err := os.ReadFile("../../testdata/openapi/amplification_alias_bomb.yaml"); err == nil { + if data, err := os.ReadFile(amplificationBombFixture); err == nil { f.Add(data) // the GitHub #27 reproducer: refused for amplification, not a cycle } f.Add([]byte(" ")) // whitespace-only: recoverable parser panic diff --git a/compilers/openapi/danglingcheck_test.go b/compilers/openapi/danglingcheck_test.go index a144e46..66c83e8 100644 --- a/compilers/openapi/danglingcheck_test.go +++ b/compilers/openapi/danglingcheck_test.go @@ -22,8 +22,9 @@ import ( "github.com/dexpace/morphic/ir/irverify" ) -// danglingDir holds the twelve issue-#14 reproducers, copied out of triage so the -// tests are self-contained. +// danglingDir holds the issue-#14 reproducers, copied out of triage so the tests +// are self-contained. danglingReproducers is the enumeration, and +// TestDanglingRefs_EveryReproducerIsExercised holds it to the directory. const danglingDir = "../../testdata/dangling/openapi" // danglingRefs returns a sorted, human-readable list of every dangling reference @@ -83,16 +84,19 @@ const ( internsNoisy ) -// TestDanglingRefs_Reproducers compiles each issue-#14 reproducer and asserts the -// produced IR has zero dangling references — every offending entry either interns -// correctly or is dropped with an error-severity diagnostic. -func TestDanglingRefs_Reproducers(t *testing.T) { - t.Parallel() - cases := []struct { - file string - srcPath string - want outcome - }{ +// danglingCase is one reproducer: the fixture under danglingDir, the source path +// it is compiled as, and what the compiler is expected to do with it. +type danglingCase struct { + file string + srcPath string + want outcome +} + +// danglingReproducers enumerates the fixtures under danglingDir. +// TestDanglingRefs_EveryReproducerIsExercised fails when the directory holds one +// this list does not, so a fixture added to the corpus cannot sit unexercised. +func danglingReproducers() []danglingCase { + return []danglingCase{ {"f04-composition.yaml", "f04.yaml", drops}, {"f05-discriminator.yaml", "f05.yaml", drops}, {"f06-discriminator.yaml", "f06.yaml", drops}, @@ -108,7 +112,40 @@ func TestDanglingRefs_Reproducers(t *testing.T) { {"f31-discriminator-empty-name.yaml", "f31.yaml", interns}, {"f32-ref-noncanonical-escape.yaml", "f32.yaml", internsNoisy}, } - for _, tc := range cases { +} + +// TestDanglingRefs_EveryReproducerIsExercised holds danglingReproducers to the +// directory it enumerates. A fixture copied into danglingDir and not added to the +// table would otherwise be compiled by nothing, and the suite would stay green +// while the corpus grew past it — the drift a hand-maintained list invites. +func TestDanglingRefs_EveryReproducerIsExercised(t *testing.T) { + t.Parallel() + onDisk, err := filepath.Glob(filepath.Join(danglingDir, "*.yaml")) + require.NoError(t, err, "globbing the reproducer directory") + require.NotEmpty(t, onDisk, "the reproducer directory is not empty") + + table := danglingReproducers() + listed := make(map[string]bool, len(table)) + for _, tc := range table { + listed[tc.file] = true + } + for _, path := range onDisk { + assert.True(t, listed[filepath.Base(path)], + "%s is in %s but not in danglingReproducers", filepath.Base(path), danglingDir) + } + // Neutral about which side is short, because it catches both: a row naming no + // fixture is what it is here for, and it fires again beside the check above + // when the directory is the side holding the extra. + assert.Len(t, table, len(onDisk), + "danglingReproducers and %s hold different numbers of fixtures", danglingDir) +} + +// TestDanglingRefs_Reproducers compiles each issue-#14 reproducer and asserts the +// produced IR has zero dangling references — every offending entry either interns +// correctly or is dropped with an error-severity diagnostic. +func TestDanglingRefs_Reproducers(t *testing.T) { + t.Parallel() + for _, tc := range danglingReproducers() { t.Run(tc.file, func(t *testing.T) { t.Parallel() doc, diags := compileFile(t, danglingDir, tc.file, tc.srcPath) diff --git a/compilers/openapi/internal/scan/scan_internal_test.go b/compilers/openapi/internal/scan/scan_internal_test.go index cc033a4..d14dc30 100644 --- a/compilers/openapi/internal/scan/scan_internal_test.go +++ b/compilers/openapi/internal/scan/scan_internal_test.go @@ -3,6 +3,7 @@ package scan import ( "fmt" "os" + "path/filepath" "strconv" "strings" "testing" @@ -53,6 +54,33 @@ var cycleReproducers = []struct{ name, file string }{ {"pointer-whitespace-self", "cycle_pointer_whitespace_self"}, } +// TestCycleReproducers_EveryFixtureIsExercised holds cycleReproducers to the +// fixtures on disk. A cycle_*.yaml added to the corpus and left out of the table +// is scanned by nothing here, and the suite stays green while the corpus grows +// past it. +// +// The compiler package keeps a table of the same fixtures for its own assertion +// and carries the twin of this test. Holding both to one directory is what stops +// the two from drifting apart as well as from the corpus, which neither package +// can check directly — a test package cannot import another's. +func TestCycleReproducers_EveryFixtureIsExercised(t *testing.T) { + t.Parallel() + onDisk, err := filepath.Glob(filepath.Join(reproducerDir, "cycle_*.yaml")) + require.NoError(t, err, "globbing the reproducer corpus") + require.NotEmpty(t, onDisk, "the corpus holds cycle reproducers") + + listed := make(map[string]bool, len(cycleReproducers)) + for _, tc := range cycleReproducers { + listed[tc.file+".yaml"] = true + } + for _, path := range onDisk { + assert.True(t, listed[filepath.Base(path)], + "%s is in %s but not in cycleReproducers", filepath.Base(path), reproducerDir) + } + assert.Len(t, cycleReproducers, len(onDisk), + "cycleReproducers and %s hold different numbers of fixtures", reproducerDir) +} + func TestDetectCycles_Reproducers(t *testing.T) { t.Parallel() for _, tc := range cycleReproducers { @@ -248,9 +276,14 @@ func rawNodes(n *yaml.Node) int64 { return sourceindex.Build(n, sourceindex.MaxIndexedNodes).Nodes() } +// reproducerDir is where the cycle fixtures live. The reader below and the table +// guard both derive their paths from it, so neither can end up looking somewhere +// the other is not. +const reproducerDir = "../../../../testdata/openapi" + func readReproducer(t *testing.T, file string) []byte { t.Helper() - data, err := os.ReadFile("../../../../testdata/openapi/" + file + ".yaml") + data, err := os.ReadFile(filepath.Join(reproducerDir, file+".yaml")) require.NoError(t, err) return data }