diff --git a/pkg/test/ginkgo/test_suite.go b/pkg/test/ginkgo/test_suite.go index 67d54e7b1acb..48ea2a4b89ba 100644 --- a/pkg/test/ginkgo/test_suite.go +++ b/pkg/test/ginkgo/test_suite.go @@ -133,6 +133,7 @@ type TestSuite struct { TestTimeout time.Duration `json:"testTimeout,omitempty"` // OTE + Parents []string `json:"parents,omitempty"` Qualifiers []string `json:"qualifiers,omitempty"` Extension *extensions.Extension `json:"-"` } diff --git a/pkg/testsuites/parents_test.go b/pkg/testsuites/parents_test.go new file mode 100644 index 000000000000..80d4127f9c2f --- /dev/null +++ b/pkg/testsuites/parents_test.go @@ -0,0 +1,181 @@ +package testsuites + +import ( + "testing" + + "github.com/openshift/origin/pkg/test/ginkgo" +) + +func TestMergeParentQualifiers(t *testing.T) { + t.Run("child qualifiers merge into parent", func(t *testing.T) { + suites := []*ginkgo.TestSuite{ + {Name: "parent"}, + {Name: "child", Parents: []string{"parent"}, Qualifiers: []string{"q1", "q2"}}, + } + mergeParentQualifiers(suites) + assertQualifiers(t, suites, "parent", []string{"q1", "q2"}) + }) + + t.Run("multiple children merge into same parent", func(t *testing.T) { + suites := []*ginkgo.TestSuite{ + {Name: "parent"}, + {Name: "child-a", Parents: []string{"parent"}, Qualifiers: []string{"qa"}}, + {Name: "child-b", Parents: []string{"parent"}, Qualifiers: []string{"qb"}}, + } + mergeParentQualifiers(suites) + assertQualifiers(t, suites, "parent", []string{"qa", "qb"}) + }) + + t.Run("duplicate qualifiers are not added twice", func(t *testing.T) { + suites := []*ginkgo.TestSuite{ + {Name: "parent", Qualifiers: []string{"shared"}}, + {Name: "child", Parents: []string{"parent"}, Qualifiers: []string{"shared", "unique"}}, + } + mergeParentQualifiers(suites) + assertQualifiers(t, suites, "parent", []string{"shared", "unique"}) + }) + + t.Run("transitive parents propagate grandchild to grandparent", func(t *testing.T) { + suites := []*ginkgo.TestSuite{ + {Name: "grandparent"}, + {Name: "parent", Parents: []string{"grandparent"}, Qualifiers: []string{"qp"}}, + {Name: "child", Parents: []string{"parent"}, Qualifiers: []string{"qc"}}, + } + mergeParentQualifiers(suites) + assertQualifiers(t, suites, "parent", []string{"qp", "qc"}) + assertQualifiers(t, suites, "grandparent", []string{"qp", "qc"}) + }) + + t.Run("transitive parents work regardless of slice order", func(t *testing.T) { + suites := []*ginkgo.TestSuite{ + {Name: "child", Parents: []string{"parent"}, Qualifiers: []string{"qc"}}, + {Name: "grandparent"}, + {Name: "parent", Parents: []string{"grandparent"}, Qualifiers: []string{"qp"}}, + } + mergeParentQualifiers(suites) + assertQualifiers(t, suites, "parent", []string{"qp", "qc"}) + assertQualifiers(t, suites, "grandparent", []string{"qp", "qc"}) + }) + + t.Run("four levels deep propagates to root", func(t *testing.T) { + suites := []*ginkgo.TestSuite{ + {Name: "root"}, + {Name: "l1", Parents: []string{"root"}, Qualifiers: []string{"q1"}}, + {Name: "l2", Parents: []string{"l1"}, Qualifiers: []string{"q2"}}, + {Name: "l3", Parents: []string{"l2"}, Qualifiers: []string{"q3"}}, + } + mergeParentQualifiers(suites) + assertQualifiers(t, suites, "l2", []string{"q2", "q3"}) + assertQualifiers(t, suites, "l1", []string{"q1", "q2", "q3"}) + assertQualifiers(t, suites, "root", []string{"q1", "q2", "q3"}) + }) + + t.Run("child with multiple parents merges into all", func(t *testing.T) { + suites := []*ginkgo.TestSuite{ + {Name: "parent-a"}, + {Name: "parent-b"}, + {Name: "child", Parents: []string{"parent-a", "parent-b"}, Qualifiers: []string{"qc"}}, + } + mergeParentQualifiers(suites) + assertQualifiers(t, suites, "parent-a", []string{"qc"}) + assertQualifiers(t, suites, "parent-b", []string{"qc"}) + }) + + t.Run("child qualifiers are unchanged", func(t *testing.T) { + suites := []*ginkgo.TestSuite{ + {Name: "parent"}, + {Name: "child", Parents: []string{"parent"}, Qualifiers: []string{"qc"}}, + } + mergeParentQualifiers(suites) + assertQualifiers(t, suites, "child", []string{"qc"}) + }) + + t.Run("suite with no parents is unaffected", func(t *testing.T) { + suites := []*ginkgo.TestSuite{ + {Name: "standalone", Qualifiers: []string{"qs"}}, + {Name: "other", Qualifiers: []string{"qo"}}, + } + mergeParentQualifiers(suites) + assertQualifiers(t, suites, "standalone", []string{"qs"}) + assertQualifiers(t, suites, "other", []string{"qo"}) + }) + + t.Run("parent with no children keeps its own qualifiers", func(t *testing.T) { + suites := []*ginkgo.TestSuite{ + {Name: "parent", Qualifiers: []string{"qp"}}, + } + mergeParentQualifiers(suites) + assertQualifiers(t, suites, "parent", []string{"qp"}) + }) +} + +func TestConformanceInheritsFromChildren(t *testing.T) { + suites := InternalTestSuites() + + var conformance, parallel, serial *ginkgo.TestSuite + for _, s := range suites { + switch s.Name { + case "openshift/conformance": + conformance = s + case "openshift/conformance/parallel": + parallel = s + case "openshift/conformance/serial": + serial = s + } + } + + if conformance == nil || parallel == nil || serial == nil { + t.Fatal("expected to find conformance, parallel, and serial suites") + } + + if len(parallel.Qualifiers) == 0 { + t.Fatal("parallel suite has no qualifiers") + } + if len(serial.Qualifiers) == 0 { + t.Fatal("serial suite has no qualifiers") + } + + qualifierSet := make(map[string]bool, len(conformance.Qualifiers)) + for _, q := range conformance.Qualifiers { + qualifierSet[q] = true + } + + for _, q := range parallel.Qualifiers { + if !qualifierSet[q] { + t.Errorf("conformance suite missing qualifier from parallel: %s", q) + } + } + for _, q := range serial.Qualifiers { + if !qualifierSet[q] { + t.Errorf("conformance suite missing qualifier from serial: %s", q) + } + } +} + +func findSuite(suites []*ginkgo.TestSuite, name string) *ginkgo.TestSuite { + for _, s := range suites { + if s.Name == name { + return s + } + } + return nil +} + +func assertQualifiers(t *testing.T, suites []*ginkgo.TestSuite, suiteName string, expected []string) { + t.Helper() + suite := findSuite(suites, suiteName) + if suite == nil { + t.Fatalf("suite %q not found", suiteName) + } + if len(suite.Qualifiers) != len(expected) { + t.Errorf("suite %q: expected %d qualifiers %v, got %d: %v", + suiteName, len(expected), expected, len(suite.Qualifiers), suite.Qualifiers) + return + } + for i, q := range expected { + if suite.Qualifiers[i] != q { + t.Errorf("suite %q qualifier[%d]: expected %q, got %q", + suiteName, i, q, suite.Qualifiers[i]) + } + } +} diff --git a/pkg/testsuites/standard_suites.go b/pkg/testsuites/standard_suites.go index d1f403b54189..557d7e28ddd8 100644 --- a/pkg/testsuites/standard_suites.go +++ b/pkg/testsuites/standard_suites.go @@ -28,6 +28,7 @@ func InternalTestSuites() []*ginkgo.TestSuite { curr := staticSuites[i] copied = append(copied, &curr) } + mergeParentQualifiers(copied) return copied } @@ -88,6 +89,7 @@ func AllTestSuites(ctx context.Context) ([]*ginkgo.TestSuite, error) { Kind: ginkgo.KindExternal, Count: s.Count, Extension: e, + Parents: s.Parents, Parallelism: s.Parallelism, Qualifiers: s.Qualifiers, TestTimeout: timeout, @@ -96,19 +98,9 @@ func AllTestSuites(ctx context.Context) ([]*ginkgo.TestSuite, error) { } } - // Now handle setting qualifiers for parent suites once we've assembled the complete - // list of suites. - for _, e := range extensionInfos { - for _, s := range e.Suites { - for _, p := range s.Parents { - for _, parent := range suites { - if parent.Name == p { - parent.Qualifiers = append(parent.Qualifiers, s.Qualifiers...) - } - } - } - } - } + // Merge qualifiers from child suites into their declared parents. The fixed-point + // loop handles arbitrary depth (e.g. extension → parallel → conformance). + mergeParentQualifiers(suites) return suites, nil } @@ -120,9 +112,6 @@ var staticSuites = []ginkgo.TestSuite{ Description: templates.LongDesc(` Tests that ensure an OpenShift cluster and components are working properly. `), - Qualifiers: []string{ - withExcludedTestsFilter("name.contains('[Suite:openshift/conformance/')"), - }, Parallelism: 30, }, { @@ -130,6 +119,7 @@ var staticSuites = []ginkgo.TestSuite{ Description: templates.LongDesc(` Only the portion of the openshift/conformance test suite that run in parallel. `), + Parents: []string{"openshift/conformance"}, Qualifiers: []string{ withExcludedTestsFilter("name.contains('[Suite:openshift/conformance/parallel')"), }, @@ -141,6 +131,7 @@ var staticSuites = []ginkgo.TestSuite{ Description: templates.LongDesc(` Only the portion of the openshift/conformance test suite that run serially. `), + Parents: []string{"openshift/conformance"}, Qualifiers: []string{ // Standard early and late tests are included in the serial suite withExcludedTestsFilter(withStandardEarlyOrLateTests("name.contains('[Suite:openshift/conformance/serial')")), @@ -511,6 +502,37 @@ var staticSuites = []ginkgo.TestSuite{ }, } +// mergeParentQualifiers appends each suite's qualifiers to its declared parent +// suites, deduplicating so the same qualifier isn't added twice. It repeats +// until no new qualifiers are added, so transitive parent chains (grandchildren) +// propagate fully regardless of slice order. +func mergeParentQualifiers(suites []*ginkgo.TestSuite) { + for { + changed := false + for _, child := range suites { + for _, parentName := range child.Parents { + for _, parent := range suites { + if parent.Name == parentName { + existing := make(map[string]bool, len(parent.Qualifiers)) + for _, q := range parent.Qualifiers { + existing[q] = true + } + for _, q := range child.Qualifiers { + if !existing[q] { + parent.Qualifiers = append(parent.Qualifiers, q) + changed = true + } + } + } + } + } + } + if !changed { + break + } + } +} + func withExcludedTestsFilter(baseExpr string) string { filter := "" for i, s := range extensions.ExcludedTests {