Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions compilers/openapi/cycles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package openapi
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
63 changes: 50 additions & 13 deletions compilers/openapi/danglingcheck_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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},
Expand All @@ -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)
Expand Down
35 changes: 34 additions & 1 deletion compilers/openapi/internal/scan/scan_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package scan
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
Loading