diff --git a/core_dsl.go b/core_dsl.go index 7e165e473..099daa474 100644 --- a/core_dsl.go +++ b/core_dsl.go @@ -20,6 +20,7 @@ import ( "io" "os" "path/filepath" + "slices" "strings" "github.com/go-logr/logr" @@ -268,7 +269,7 @@ func RunSpecs(t GinkgoTestingT, description string, args ...any) bool { } defer global.PopClone() - suiteLabels, suiteSemVerConstraints, suiteAroundNodes := extractSuiteConfiguration(args) + suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, suiteAroundNodes := extractSuiteConfiguration(args) var reporter reporters.Reporter if suiteConfig.ParallelTotal == 1 { @@ -311,7 +312,7 @@ func RunSpecs(t GinkgoTestingT, description string, args ...any) bool { suitePath, err = filepath.Abs(suitePath) exitIfErr(err) - passed, hasFocusedTests := global.Suite.Run(description, suiteLabels, suiteSemVerConstraints, suiteAroundNodes, suitePath, global.Failer, reporter, writer, outputInterceptor, interrupt_handler.NewInterruptHandler(client), client, internal.RegisterForProgressSignal, suiteConfig) + passed, hasFocusedTests := global.Suite.Run(description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, suiteAroundNodes, suitePath, global.Failer, reporter, writer, outputInterceptor, interrupt_handler.NewInterruptHandler(client), client, internal.RegisterForProgressSignal, suiteConfig) outputInterceptor.Shutdown() flagSet.ValidateDeprecations(deprecationTracker) @@ -330,9 +331,10 @@ func RunSpecs(t GinkgoTestingT, description string, args ...any) bool { return passed } -func extractSuiteConfiguration(args []any) (Labels, SemVerConstraints, types.AroundNodes) { +func extractSuiteConfiguration(args []any) (Labels, SemVerConstraints, ComponentSemVerConstraints, types.AroundNodes) { suiteLabels := Labels{} suiteSemVerConstraints := SemVerConstraints{} + suiteComponentSemVerConstraints := ComponentSemVerConstraints{} aroundNodes := types.AroundNodes{} configErrors := []error{} for _, arg := range args { @@ -345,6 +347,11 @@ func extractSuiteConfiguration(args []any) (Labels, SemVerConstraints, types.Aro suiteLabels = append(suiteLabels, arg...) case SemVerConstraints: suiteSemVerConstraints = append(suiteSemVerConstraints, arg...) + case ComponentSemVerConstraints: + for component, constraints := range arg { + suiteComponentSemVerConstraints[component] = append(suiteComponentSemVerConstraints[component], constraints...) + suiteComponentSemVerConstraints[component] = slices.Compact(suiteComponentSemVerConstraints[component]) + } case types.AroundNodeDecorator: aroundNodes = append(aroundNodes, arg) default: @@ -362,7 +369,7 @@ func extractSuiteConfiguration(args []any) (Labels, SemVerConstraints, types.Aro os.Exit(1) } - return suiteLabels, suiteSemVerConstraints, aroundNodes + return suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, aroundNodes } func getwd() (string, error) { @@ -385,7 +392,7 @@ func PreviewSpecs(description string, args ...any) Report { } defer global.PopClone() - suiteLabels, suiteSemVerConstraints, suiteAroundNodes := extractSuiteConfiguration(args) + suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, suiteAroundNodes := extractSuiteConfiguration(args) priorDryRun, priorParallelTotal, priorParallelProcess := suiteConfig.DryRun, suiteConfig.ParallelTotal, suiteConfig.ParallelProcess suiteConfig.DryRun, suiteConfig.ParallelTotal, suiteConfig.ParallelProcess = true, 1, 1 defer func() { @@ -403,7 +410,7 @@ func PreviewSpecs(description string, args ...any) Report { suitePath, err = filepath.Abs(suitePath) exitIfErr(err) - global.Suite.Run(description, suiteLabels, suiteSemVerConstraints, suiteAroundNodes, suitePath, global.Failer, reporter, writer, outputInterceptor, interrupt_handler.NewInterruptHandler(client), client, internal.RegisterForProgressSignal, suiteConfig) + global.Suite.Run(description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, suiteAroundNodes, suitePath, global.Failer, reporter, writer, outputInterceptor, interrupt_handler.NewInterruptHandler(client), client, internal.RegisterForProgressSignal, suiteConfig) return global.Suite.GetPreviewReport() } diff --git a/decorator_dsl.go b/decorator_dsl.go index e331d7cf8..ce1d71cec 100644 --- a/decorator_dsl.go +++ b/decorator_dsl.go @@ -117,6 +117,27 @@ You can learn more here: https://onsi.github.io/ginkgo/#spec-semantic-version-fi */ type SemVerConstraints = internal.SemVerConstraints +/* +ComponentSemVerConstraint decorates specs with ComponentSemVerConstraints. Multiple components semantic version constraints can be passed to ComponentSemVerConstraint and the component can't be empy, also the version strings must follow the semantic version constraint rules. +ComponentSemVerConstraints can be applied to container and subject nodes, but not setup nodes. You can provide multiple ComponentSemVerConstraints to a given node and a spec's component semantic version constraints is the union of all component semantic version constraints in its node hierarchy. + +You can learn more here: https://onsi.github.io/ginkgo/#spec-semantic-version-filtering +You can learn more about decorators here: https://onsi.github.io/ginkgo/#decorator-reference +*/ +func ComponentSemVerConstraint(component string, semVerConstraints ...string) ComponentSemVerConstraints { + componentSemVerConstraints := ComponentSemVerConstraints{ + component: semVerConstraints, + } + + return componentSemVerConstraints +} + +/* +ComponentSemVerConstraints are the type for spec ComponentSemVerConstraint decorators. Use ComponentSemVerConstraint(...) to construct ComponentSemVerConstraints. +You can learn more here: https://onsi.github.io/ginkgo/#spec-semantic-version-filtering +*/ +type ComponentSemVerConstraints = internal.ComponentSemVerConstraints + /* PollProgressAfter allows you to override the configured value for --poll-progress-after for a particular node. diff --git a/docs/index.md b/docs/index.md index 3a68e7c26..9b2f6fe7f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2930,13 +2930,17 @@ Describe("Feature with version requirements", func() { It("should work in a specific version range (1.0.0, 2.0.0)", SemVerConstraint("> 1.0.0", "< 2.0.0"), func() { // This test will only run when version is between 1.0.0 (exclusive) and 2.0.0 (exclusive) }) + + It("should work in a specific version range (1.0.0, 2.0.0) and third-party dependency redis in [8.0.0, ~)", SemVerConstraint(">= 3.2.0"), ComponentSemVerConstraint("redis", ">= 8.0.0") func() { + // This test will only run when version is between 1.0.0 (exclusive) and 2.0.0 (exclusive) and redis version is >= 8.0.0 + }) }) ``` You can then filter specs by providing a semantic version via the `--sem-ver-filter` flag: ```bash -ginkgo --sem-ver-filter="2.1.1" +ginkgo --sem-ver-filter="2.1.1, redis=8.2.0" ``` This will only run specs whose semantic version constraints are satisfied by the provided version. @@ -2963,14 +2967,27 @@ Describe("Feature with version requirements", SemVerConstraint(">= 2.0.0, < 2.3. It("should only run in a narrower range", SemVerConstraint(">= 2.1.0, <= 2.2.0"), func() { // Effective constraint: >= 2.1.0, <= 2.2.0 (intersection with parent) }) + + Context("should work in the base version range and third-party dependency", ComponentSemVerConstraint("redis", ">= 6.0.0"), func() { + It("should run when both constraints are satisfied", func() { + // Effective constraint: >= 2.0.0, < 2.3.0 and redis version >= 6.0.0 + }) + + It("should run in a narrower range with third-party dependency", ComponentSemVerConstraint("redis", ">= 7.0.0", "< 8.0.0"), func() { + // Effective constraint: >= 2.0.0, < 2.3.0 and redis version >= 7.0.0, < 8.0.0 + }) + }) }) ``` In this example, the second spec will only run when the version satisfies both the parent constraint (`>= 2.0.0, < 2.3.0`) and its own constraint (`>= 2.1.0, <= 2.2.0`), resulting in an effective constraint of `>= 2.1.0, <= 2.2.0`. +Additionally, the specs within the `Context` will only run when both the parent constraint and the third-party dependency constraint are satisfied. + +Note that while using `ComponentSemVerConstraint` decorator, the component name can not be empty. ##### Unconstrained Specs -Specs that don't have a `SemVerConstraint` decorator are considered unconstrained and will always run when the `--sem-ver-filter` flag is provided. This allows you to have a mix of version-specific and version-agnostic tests in the same suite. +Specs that don't have `SemVerConstraint` or `ComponentSemVerConstraint` decorator are considered unconstrained and will always run when the `--sem-ver-filter` flag is provided. This allows you to have a mix of version-specific and version-agnostic tests in the same suite. #### Location-Based Filtering diff --git a/dsl/decorators/decorators_dsl.go b/dsl/decorators/decorators_dsl.go index 2cc5f1435..2ff332f92 100644 --- a/dsl/decorators/decorators_dsl.go +++ b/dsl/decorators/decorators_dsl.go @@ -22,6 +22,7 @@ type FlakeAttempts = ginkgo.FlakeAttempts type MustPassRepeatedly = ginkgo.MustPassRepeatedly type Labels = ginkgo.Labels type SemVerConstraints = ginkgo.SemVerConstraints +type ComponentSemVerConstraints = ginkgo.ComponentSemVerConstraints type PollProgressAfter = ginkgo.PollProgressAfter type PollProgressInterval = ginkgo.PollProgressInterval type NodeTimeout = ginkgo.NodeTimeout @@ -39,6 +40,7 @@ const SuppressProgressReporting = ginkgo.SuppressProgressReporting var Label = ginkgo.Label var SemVerConstraint = ginkgo.SemVerConstraint +var ComponentSemVerConstraint = ginkgo.ComponentSemVerConstraint func AroundNode[F types.AroundNodeAllowedFuncs](f F) types.AroundNodeDecorator { return types.AroundNode(f, types.NewCodeLocation(1)) diff --git a/integration/_fixtures/semver_fixture/semver_test.go b/integration/_fixtures/semver_fixture/semver_test.go index ce85f1b4e..82cf9996e 100644 --- a/integration/_fixtures/semver_fixture/semver_test.go +++ b/integration/_fixtures/semver_fixture/semver_test.go @@ -16,7 +16,21 @@ var _ = Describe("Semantic Version Filtering", func() { It("should run with version in range [2.0.0, 5.0.0)", SemVerConstraint(">= 2.0.0"), SemVerConstraint("< 5.0.0"), func() {}) + It("should run with ComponentA in range [2.0.0, ~)", ComponentSemVerConstraint("ComponentA", ">= 2.0.0"), func() {}) + + It("should run with ComponentA in range [2.0.0, 3.0.0)", ComponentSemVerConstraint("ComponentA", ">= 2.0.0, < 3.0.0"), func() {}) + + It("should run with ComponentA in range [2.0.0, 4.0.0)", ComponentSemVerConstraint("ComponentA", ">= 2.0.0", "< 4.0.0"), func() {}) + + It("should run with ComponentA in range [2.0.0, 5.0.0)", ComponentSemVerConstraint("ComponentA", ">= 2.0.0"), ComponentSemVerConstraint("ComponentA", "< 5.0.0"), func() {}) + + It("should run with a mixed of component-specific and non-component constraints", SemVerConstraint(">= 2.0.0"), ComponentSemVerConstraint("ComponentA", ">= 2.0.0"), func() {}) + It("shouldn't run with version in a conflict range", SemVerConstraint("2.0.0 - 6.0.0"), SemVerConstraint("<= 1.0.0"), func() {}) + + It("shouldn't run with ComponentA in a conflict range", ComponentSemVerConstraint("ComponentA", "2.0.0 - 6.0.0"), ComponentSemVerConstraint("ComponentA", "<= 1.0.0"), func() {}) + + It("shouldn't run with a mixed of component-specific and non-component conflict constraints", SemVerConstraint(">= 2.0.0"), ComponentSemVerConstraint("ComponentA", "<= 1.0.0"), func() {}) }) var _ = Describe("Hierarchy Semantic Version Filtering", func() { @@ -35,6 +49,27 @@ var _ = Describe("Hierarchy Semantic Version Filtering", func() { // So, this test case would be skipped. }) }) + + Context("with container component-specific constraints", ComponentSemVerConstraint("ComponentA", ">= 2.0.0", "< 3.0.0"), func() { + It("should inherit container component-specific constraint", func() {}) + + It("should narrow down the component-specific constraint", ComponentSemVerConstraint("ComponentA", ">= 2.1.0, < 2.8.0"), func() {}) + + It("shouldn't expand the component-specific constraint", ComponentSemVerConstraint("ComponentA", "< 4.0.0"), func() { + // If you pass '--sem-ver-filter=3.5.0', then the whole Context would be skipped since it doesn't match the top level ComponentSemVerConstraints. + // But if you pass '--sem-ver-filter=2.5.0', this test case would keep running since it matches the combined constraint '>= 2.0.0, < 3.0.0, < 4.0.0' + }) + + It("shouldn't combine with a component-specific conflict constraint", ComponentSemVerConstraint("ComponentA", "< 1.0.0"), func() { + // The new combined constraint is '>= 2.0.0, < 3.0.0, <1.0.0', there's no such a version can match this constraint. + // So, this test case would be skipped. + }) + }) + + Context("with mixed container constraints", SemVerConstraint(">= 2.0.0", "< 3.0.0"), + ComponentSemVerConstraint("ComponentA", ">= 2.0.0", "< 3.0.0"), func() { + It("should inherit container constraints and a new component-specific constraint", ComponentSemVerConstraint("ComponentB", ">= 0.1.0"), func() {}) + }) }) var _ = DescribeTable("Semantic Version Filtering in table-driven spec", func() { @@ -43,4 +78,7 @@ var _ = DescribeTable("Semantic Version Filtering in table-driven spec", func() Entry("should run without constraints by table driven"), Entry("should run with version in range [2.0.0, ~) by table driven", SemVerConstraint(">= 2.0.0")), Entry("shouldn't run with version in a conflict range by table driven", SemVerConstraint(">= 2.0.0"), SemVerConstraint("~1.2.3")), + Entry("should run with ComponentA in range [2.0.0, ~) by table driven", ComponentSemVerConstraint("ComponentA", ">= 2.0.0")), + Entry("shouldn't run with ComponentA in a conflict range by table driven", ComponentSemVerConstraint("ComponentA", ">= 2.0.0"), ComponentSemVerConstraint("ComponentA", "~1.2.3")), + Entry("should run with mixed constraints by table driven", SemVerConstraint(">= 2.0.0"), ComponentSemVerConstraint("ComponentA", ">= 2.0.0")), ) diff --git a/integration/_fixtures/semver_fixture/spechierarchy/spechierarchy_suite_test.go b/integration/_fixtures/semver_fixture/spechierarchy/spechierarchy_suite_test.go index c42e8bb86..032186310 100644 --- a/integration/_fixtures/semver_fixture/spechierarchy/spechierarchy_suite_test.go +++ b/integration/_fixtures/semver_fixture/spechierarchy/spechierarchy_suite_test.go @@ -16,4 +16,6 @@ var _ = Describe("Spec Hierarchy Semantic Version Filtering", func() { It("should inherit spec constraint", func() {}) It("should narrow down spec constraint", SemVerConstraint(">= 3.0.0, < 4.0.0"), func() {}) + + It("should integrate with component constraint", ComponentSemVerConstraint("compA", ">= 12.0.0"), func() {}) }) diff --git a/integration/filter_test.go b/integration/filter_test.go index 53a3183ad..5c849baae 100644 --- a/integration/filter_test.go +++ b/integration/filter_test.go @@ -1,7 +1,7 @@ package integration_test import ( - "path/filepath" + "path/filepath" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -113,7 +113,7 @@ var _ = Describe("Filter", func() { It("filters specs based on semantic version constraints", func() { session := startGinkgo(filepath.Join(fm.TmpDir, "semver"), - "--sem-ver-filter=2.2.0", + "--sem-ver-filter=2.2.0, ComponentA=2.5.0", "--json-report=report.json", ) Eventually(session).Should(gexec.Exit(0)) @@ -124,35 +124,51 @@ var _ = Describe("Filter", func() { "should run with version in range [2.0.0, 3.0.0)", "should run with version in range [2.0.0, 4.0.0)", "should run with version in range [2.0.0, 5.0.0)", + "should run with ComponentA in range [2.0.0, ~)", + "should run with ComponentA in range [2.0.0, 3.0.0)", + "should run with ComponentA in range [2.0.0, 4.0.0)", + "should run with ComponentA in range [2.0.0, 5.0.0)", + "should run with a mixed of component-specific and non-component constraints", "should inherit container constraint", "should narrow down the constraint", "shouldn't expand the constraint", + "should inherit container component-specific constraint", + "should narrow down the component-specific constraint", + "shouldn't expand the component-specific constraint", + "should inherit container constraints and a new component-specific constraint", "should run without constraints by table driven", "should run with version in range [2.0.0, ~) by table driven", + "should run with ComponentA in range [2.0.0, ~) by table driven", + "should run with mixed constraints by table driven", } skippedSpecs := []string{ "shouldn't run with version in a conflict range", + "shouldn't run with ComponentA in a conflict range", + "shouldn't run with a mixed of component-specific and non-component conflict constraints", "shouldn't combine with a conflict constraint", + "shouldn't combine with a component-specific conflict constraint", "shouldn't run with version in a conflict range by table driven", + "shouldn't run with ComponentA in a conflict range by table driven", } Ω(specs).Should(HaveLen(len(passedSpecs) + len(skippedSpecs))) for _, passed := range passedSpecs { - Ω(specs.Find(passed)).Should(HavePassed()) + Ω(specs.Find(passed)).Should(HavePassed(), passed) } for _, skipped := range skippedSpecs { - Ω(specs.Find(skipped)).Should(HaveBeenSkipped()) + Ω(specs.Find(skipped)).Should(HaveBeenSkipped(), skipped) } }) It("filters specs with hierarchy based on semantic version constraints", func() { session := startGinkgo(filepath.Join(fm.TmpDir, "semver", "spechierarchy"), - "--sem-ver-filter=2.2.0", + "--sem-ver-filter=2.2.0, compA=12.0.0", "--json-report=report.json", ) Eventually(session).Should(gexec.Exit(0)) specs := Reports(fm.LoadJSONReports(filepath.Join("semver", "spechierarchy"), "report.json")[0].SpecReports) passedSpecs := []string{ "should inherit spec constraint", + "should integrate with component constraint", } skippedSpecs := []string{ "should narrow down spec constraint", diff --git a/internal/focus.go b/internal/focus.go index a39daf5a6..498e707db 100644 --- a/internal/focus.go +++ b/internal/focus.go @@ -56,7 +56,7 @@ This function sets the `Skip` property on specs by applying Ginkgo's focus polic *Note:* specs with pending nodes are Skipped when created by NewSpec. */ -func ApplyFocusToSpecs(specs Specs, description string, suiteLabels Labels, suiteSemVerConstraints SemVerConstraints, suiteConfig types.SuiteConfig) (Specs, bool) { +func ApplyFocusToSpecs(specs Specs, description string, suiteLabels Labels, suiteSemVerConstraints SemVerConstraints, suiteComponentSemVerConstraints ComponentSemVerConstraints, suiteConfig types.SuiteConfig) (Specs, bool) { focusString := strings.Join(suiteConfig.FocusStrings, "|") skipString := strings.Join(suiteConfig.SkipStrings, "|") @@ -87,7 +87,24 @@ func ApplyFocusToSpecs(specs Specs, description string, suiteLabels Labels, suit if suiteConfig.SemVerFilter != "" { semVerFilter, _ := types.ParseSemVerFilter(suiteConfig.SemVerFilter) skipChecks = append(skipChecks, func(spec Spec) bool { - return !semVerFilter(UnionOfSemVerConstraints(suiteSemVerConstraints, spec.Nodes.UnionOfSemVerConstraints())) + noRun := false + + // non-component-specific constraints + constraints := UnionOfSemVerConstraints(suiteSemVerConstraints, spec.Nodes.UnionOfSemVerConstraints()) + if len(constraints) != 0 && semVerFilter("", constraints) == false { + noRun = true + } + + // component-specific constraints + componentConstraints := UnionOfComponentSemVerConstraints(suiteComponentSemVerConstraints, spec.Nodes.UnionOfComponentSemVerConstraints()) + for component, constraints := range componentConstraints { + if semVerFilter(component, constraints) == false { + noRun = true + break + } + } + + return noRun }) } diff --git a/internal/focus_test.go b/internal/focus_test.go index 2d43f58af..7c2e77afc 100644 --- a/internal/focus_test.go +++ b/internal/focus_test.go @@ -80,6 +80,7 @@ var _ = Describe("Focus", func() { var description string var suiteLabels Labels var suiteSemVerConstraints SemVerConstraints + var suiteComponentSemVerConstraints ComponentSemVerConstraints var conf types.SuiteConfig harvestSkips := func(specs Specs) []bool { @@ -108,7 +109,7 @@ var _ = Describe("Focus", func() { }) It("skips those specs", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{false, false, true, false, true})) Ω(hasProgrammaticFocus).Should(BeFalse()) }) @@ -125,7 +126,7 @@ var _ = Describe("Focus", func() { } }) It("skips any other specs and notes that it has programmatic focus", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{true, true, false, true, false})) Ω(hasProgrammaticFocus).Should(BeTrue()) }) @@ -140,7 +141,7 @@ var _ = Describe("Focus", func() { } }) It("does not skip any other specs and notes that it does not have programmatic focus", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{false, false, true, false})) Ω(hasProgrammaticFocus).Should(BeFalse()) }) @@ -166,14 +167,14 @@ var _ = Describe("Focus", func() { }) It("overrides any programmatic focus, runs only specs that match the focus string, and continues to skip specs with nodes marked pending", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{false, false, false, false, true, true, true})) Ω(hasProgrammaticFocus).Should(BeFalse()) }) It("includes the description string in the search", func() { conf.FocusStrings = []string{"Silmaril"} - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{false, false, false, false, true, false, false})) Ω(hasProgrammaticFocus).Should(BeFalse()) }) @@ -185,14 +186,14 @@ var _ = Describe("Focus", func() { }) It("overrides any programmatic focus, and runs specs that don't match the skip strings, and continues to skip specs with nodes marked pending", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{true, true, true, false, true, false, false})) Ω(hasProgrammaticFocus).Should(BeFalse()) }) It("includes the description string in the search", func() { conf.SkipStrings = []string{"Silmaril"} - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{true, true, true, true, true, true, true})) Ω(hasProgrammaticFocus).Should(BeFalse()) }) @@ -205,7 +206,7 @@ var _ = Describe("Focus", func() { }) It("ORs both together", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{false, true, true, false, true, true, true})) Ω(hasProgrammaticFocus).Should(BeFalse()) }) @@ -228,7 +229,7 @@ var _ = Describe("Focus", func() { }) It("applies a file-based focus and skip filter", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{false, false, true, true, true, false})) Ω(hasProgrammaticFocus).Should(BeFalse()) }) @@ -248,7 +249,7 @@ var _ = Describe("Focus", func() { }) It("applies the label filters", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{true, false, true, true, false, true})) Ω(hasProgrammaticFocus).Should(BeFalse()) @@ -269,7 +270,7 @@ var _ = Describe("Focus", func() { }) It("applies the label filters", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{true, false, true, false, true, true})) Ω(hasProgrammaticFocus).Should(BeFalse()) @@ -285,7 +286,7 @@ var _ = Describe("Focus", func() { } }) It("honors the suite level label", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{false, true})) Ω(hasProgrammaticFocus).Should(BeFalse()) }) @@ -311,7 +312,7 @@ var _ = Describe("Focus", func() { }) It("applies all filters", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{false, true, true, true, true, true, true})) Ω(hasProgrammaticFocus).Should(BeFalse()) }) @@ -338,7 +339,7 @@ var _ = Describe("Focus", func() { }) It("applies all filters", func() { - specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, conf) + specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, conf) Ω(harvestSkips(specs)).Should(Equal([]bool{true, false, true, true, true, true, true, true})) Ω(hasProgrammaticFocus).Should(BeTrue()) }) diff --git a/internal/group.go b/internal/group.go index cc794903e..5e6611334 100644 --- a/internal/group.go +++ b/internal/group.go @@ -113,22 +113,24 @@ func newGroup(suite *Suite) *group { // initialReportForSpec constructs a new SpecReport right before running the spec. func (g *group) initialReportForSpec(spec Spec) types.SpecReport { return types.SpecReport{ - ContainerHierarchyTexts: spec.Nodes.WithType(types.NodeTypeContainer).Texts(), - ContainerHierarchyLocations: spec.Nodes.WithType(types.NodeTypeContainer).CodeLocations(), - ContainerHierarchyLabels: spec.Nodes.WithType(types.NodeTypeContainer).Labels(), - ContainerHierarchySemVerConstraints: spec.Nodes.WithType(types.NodeTypeContainer).SemVerConstraints(), - LeafNodeLocation: spec.FirstNodeWithType(types.NodeTypeIt).CodeLocation, - LeafNodeType: types.NodeTypeIt, - LeafNodeText: spec.FirstNodeWithType(types.NodeTypeIt).Text, - LeafNodeLabels: []string(spec.FirstNodeWithType(types.NodeTypeIt).Labels), - LeafNodeSemVerConstraints: []string(spec.FirstNodeWithType(types.NodeTypeIt).SemVerConstraints), - ParallelProcess: g.suite.config.ParallelProcess, - RunningInParallel: g.suite.isRunningInParallel(), - IsSerial: spec.Nodes.HasNodeMarkedSerial(), - IsInOrderedContainer: !spec.Nodes.FirstNodeMarkedOrdered().IsZero(), - MaxFlakeAttempts: spec.Nodes.GetMaxFlakeAttempts(), - MaxMustPassRepeatedly: spec.Nodes.GetMaxMustPassRepeatedly(), - SpecPriority: spec.Nodes.GetSpecPriority(), + ContainerHierarchyTexts: spec.Nodes.WithType(types.NodeTypeContainer).Texts(), + ContainerHierarchyLocations: spec.Nodes.WithType(types.NodeTypeContainer).CodeLocations(), + ContainerHierarchyLabels: spec.Nodes.WithType(types.NodeTypeContainer).Labels(), + ContainerHierarchySemVerConstraints: spec.Nodes.WithType(types.NodeTypeContainer).SemVerConstraints(), + ContainerHierarchyComponentSemVerConstraints: spec.Nodes.WithType(types.NodeTypeContainer).ComponentSemVerConstraints(), + LeafNodeLocation: spec.FirstNodeWithType(types.NodeTypeIt).CodeLocation, + LeafNodeType: types.NodeTypeIt, + LeafNodeText: spec.FirstNodeWithType(types.NodeTypeIt).Text, + LeafNodeLabels: []string(spec.FirstNodeWithType(types.NodeTypeIt).Labels), + LeafNodeSemVerConstraints: []string(spec.FirstNodeWithType(types.NodeTypeIt).SemVerConstraints), + LeafNodeComponentSemVerConstraints: map[string][]string(spec.FirstNodeWithType(types.NodeTypeIt).ComponentSemVerConstraints), + ParallelProcess: g.suite.config.ParallelProcess, + RunningInParallel: g.suite.isRunningInParallel(), + IsSerial: spec.Nodes.HasNodeMarkedSerial(), + IsInOrderedContainer: !spec.Nodes.FirstNodeMarkedOrdered().IsZero(), + MaxFlakeAttempts: spec.Nodes.GetMaxFlakeAttempts(), + MaxMustPassRepeatedly: spec.Nodes.GetMaxMustPassRepeatedly(), + SpecPriority: spec.Nodes.GetSpecPriority(), } } @@ -152,6 +154,7 @@ func addNodeToReportForNode(report *types.ConstructionNodeReport, node *TreeNode report.ContainerHierarchyLocations = append(report.ContainerHierarchyLocations, node.Node.CodeLocation) report.ContainerHierarchyLabels = append(report.ContainerHierarchyLabels, node.Node.Labels) report.ContainerHierarchySemVerConstraints = append(report.ContainerHierarchySemVerConstraints, node.Node.SemVerConstraints) + report.ContainerHierarchyComponentSemVerConstraints = append(report.ContainerHierarchyComponentSemVerConstraints, node.Node.ComponentSemVerConstraints) if node.Node.MarkedSerial { report.IsSerial = true } diff --git a/internal/internal_integration/internal_integration_suite_test.go b/internal/internal_integration/internal_integration_suite_test.go index 6aa0e15a4..087321174 100644 --- a/internal/internal_integration/internal_integration_suite_test.go +++ b/internal/internal_integration/internal_integration_suite_test.go @@ -94,7 +94,7 @@ func RunFixture(description string, callback func(), aroundNodes ...types.Around WithSuite(suite, func() { callback() Ω(suite.BuildTree()).Should(Succeed()) - success, hasProgrammaticFocus = suite.Run(description, Label("TopLevelLabel"), SemVerConstraints{}, aroundNodes, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, progressSignalRegistrar, conf) + success, hasProgrammaticFocus = suite.Run(description, Label("TopLevelLabel"), SemVerConstraints{}, ComponentSemVerConstraints{}, aroundNodes, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, progressSignalRegistrar, conf) }) return success, hasProgrammaticFocus } @@ -128,7 +128,7 @@ func RunFixtureInParallel(description string, callback func(proc int)) bool { interruptHandler := interrupt_handler.NewInterruptHandler(client) defer interruptHandler.Stop() - success, _ := suite.Run(fmt.Sprintf("%s - %d", description, proc), Label("TopLevelLabel"), SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, noopProgressSignalRegistrar, c) + success, _ := suite.Run(fmt.Sprintf("%s - %d", description, proc), Label("TopLevelLabel"), SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, noopProgressSignalRegistrar, c) close(exit) finished <- success }() diff --git a/internal/internal_integration/parallel_test.go b/internal/internal_integration/parallel_test.go index 7faefeeaf..ee0612864 100644 --- a/internal/internal_integration/parallel_test.go +++ b/internal/internal_integration/parallel_test.go @@ -116,7 +116,7 @@ var _ = Describe("Running tests in parallel", func() { exit1 := exitChannels[1] //avoid a race around exitChannels access in a separate goroutine //now launch suite 1... go func() { - success, _ := suite1.Run("proc 1", Label("TopLevelLabel"), SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, noopProgressSignalRegistrar, conf) + success, _ := suite1.Run("proc 1", Label("TopLevelLabel"), SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, noopProgressSignalRegistrar, conf) finished <- success close(exit1) }() @@ -125,7 +125,7 @@ var _ = Describe("Running tests in parallel", func() { reporter2 = NewFakeReporter() exit2 := exitChannels[2] //avoid a race around exitChannels access in a separate goroutine go func() { - success, _ := suite2.Run("proc 2", Label("TopLevelLabel"), SemVerConstraints{}, nil, "/path/to/suite", internal.NewFailer(), reporter2, writer, outputInterceptor, interruptHandler, client, noopProgressSignalRegistrar, conf2) + success, _ := suite2.Run("proc 2", Label("TopLevelLabel"), SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", internal.NewFailer(), reporter2, writer, outputInterceptor, interruptHandler, client, noopProgressSignalRegistrar, conf2) finished <- success close(exit2) }() diff --git a/internal/node.go b/internal/node.go index 2bccec2db..b0c8de8d6 100644 --- a/internal/node.go +++ b/internal/node.go @@ -57,6 +57,7 @@ type Node struct { MustPassRepeatedly int Labels Labels SemVerConstraints SemVerConstraints + ComponentSemVerConstraints ComponentSemVerConstraints PollProgressAfter time.Duration PollProgressInterval time.Duration NodeTimeout time.Duration @@ -106,7 +107,24 @@ func (l Labels) MatchesLabelFilter(query string) bool { type SemVerConstraints []string func (svc SemVerConstraints) MatchesSemVerFilter(version string) bool { - return types.MustParseSemVerFilter(version)(svc) + return types.MustParseSemVerFilter(version)("", svc) +} + +type ComponentSemVerConstraints map[string][]string + +func (csvc ComponentSemVerConstraints) MatchesSemVerFilter(component, version string) bool { + for comp, constraints := range csvc { + if comp != component { + continue + } + + input := version + if len(component) > 0 { + input = fmt.Sprintf("%s=%s", component, version) + } + return types.MustParseSemVerFilter(input)(component, constraints) + } + return false } func unionOf[S ~[]E, E comparable](slices ...S) S { @@ -131,6 +149,16 @@ func UnionOfSemVerConstraints(semVerConstraints ...SemVerConstraints) SemVerCons return unionOf(semVerConstraints...) } +func UnionOfComponentSemVerConstraints(componentSemVerConstraintsSlice ...ComponentSemVerConstraints) ComponentSemVerConstraints { + unionComponentSemVerConstraints := ComponentSemVerConstraints{} + for _, componentSemVerConstraints := range componentSemVerConstraintsSlice { + for component, constraints := range componentSemVerConstraints { + unionComponentSemVerConstraints[component] = unionOf(unionComponentSemVerConstraints[component], constraints) + } + } + return unionComponentSemVerConstraints +} + func PartitionDecorations(args ...any) ([]any, []any) { decorations := []any{} remainingArgs := []any{} @@ -174,6 +202,8 @@ func isDecoration(arg any) bool { return true case t == reflect.TypeOf(SemVerConstraints{}): return true + case t == reflect.TypeOf(ComponentSemVerConstraints{}): + return true case t == reflect.TypeOf(PollProgressInterval(0)): return true case t == reflect.TypeOf(PollProgressAfter(0)): @@ -214,16 +244,17 @@ var specContextType = reflect.TypeOf(new(SpecContext)).Elem() func NewNode(deprecationTracker *types.DeprecationTracker, nodeType types.NodeType, text string, args ...any) (Node, []error) { baseOffset := 2 node := Node{ - ID: UniqueNodeID(), - NodeType: nodeType, - Text: text, - Labels: Labels{}, - SemVerConstraints: SemVerConstraints{}, - CodeLocation: types.NewCodeLocation(baseOffset), - NestingLevel: -1, - PollProgressAfter: -1, - PollProgressInterval: -1, - GracePeriod: -1, + ID: UniqueNodeID(), + NodeType: nodeType, + Text: text, + Labels: Labels{}, + SemVerConstraints: SemVerConstraints{}, + ComponentSemVerConstraints: ComponentSemVerConstraints{}, + CodeLocation: types.NewCodeLocation(baseOffset), + NestingLevel: -1, + PollProgressAfter: -1, + PollProgressInterval: -1, + GracePeriod: -1, } errors := []error{} @@ -360,6 +391,36 @@ func NewNode(deprecationTracker *types.DeprecationTracker, nodeType types.NodeTy appendError(err) } } + case t == reflect.TypeOf(ComponentSemVerConstraints{}): + if !nodeType.Is(types.NodeTypesForContainerAndIt) { + appendError(types.GinkgoErrors.InvalidDecoratorForNodeType(node.CodeLocation, nodeType, "ComponentSemVerConstraint")) + } + for component, semVerConstraints := range arg.(ComponentSemVerConstraints) { + // while using ComponentSemVerConstraints, we should not allow empty component names. + // you should use SemVerConstraints for that. + hasErr := false + if len(component) == 0 { + appendError(types.GinkgoErrors.InvalidEmptyComponentForSemVerConstraint(node.CodeLocation)) + hasErr = true + } + for _, semVerConstraint := range semVerConstraints { + _, err := types.ValidateAndCleanupSemVerConstraint(semVerConstraint, node.CodeLocation) + if err != nil { + appendError(err) + hasErr = true + } + } + + if !hasErr { + // merge constraints if the component already exists + constraints := slices.Clone(semVerConstraints) + if existingConstraints, exists := node.ComponentSemVerConstraints[component]; exists { + constraints = UnionOfSemVerConstraints([]string(existingConstraints), constraints) + } + + node.ComponentSemVerConstraints[component] = slices.Clone(constraints) + } + } case t.Kind() == reflect.Func: if nodeType.Is(types.NodeTypeContainer) { if node.Body != nil { @@ -899,6 +960,34 @@ func (n Nodes) UnionOfSemVerConstraints() []string { return out } +func (n Nodes) ComponentSemVerConstraints() []map[string][]string { + out := make([]map[string][]string, len(n)) + for i := range n { + if n[i].ComponentSemVerConstraints == nil { + out[i] = map[string][]string{} + } else { + out[i] = map[string][]string(n[i].ComponentSemVerConstraints) + } + } + return out +} + +func (n Nodes) UnionOfComponentSemVerConstraints() map[string][]string { + out := map[string][]string{} + seen := map[string]bool{} + for i := range n { + for component := range n[i].ComponentSemVerConstraints { + if !seen[component] { + seen[component] = true + out[component] = n[i].ComponentSemVerConstraints[component] + } else { + out[component] = UnionOfSemVerConstraints(out[component], n[i].ComponentSemVerConstraints[component]) + } + } + } + return out +} + func (n Nodes) CodeLocations() []types.CodeLocation { out := make([]types.CodeLocation, len(n)) for i := range n { diff --git a/internal/node_test.go b/internal/node_test.go index ad6519595..063c8944d 100644 --- a/internal/node_test.go +++ b/internal/node_test.go @@ -111,6 +111,19 @@ var _ = Describe("Combining SemVerConstraints", func() { }) }) +var _ = Describe("Combining ComponentSemVerConstraints", func() { + It("can combine component semantic version constraints and produce the unique union", func() { + Ω(internal.UnionOfComponentSemVerConstraints( + ComponentSemVerConstraint("compA", "> 2.1.0", "< 2.2.0"), + ComponentSemVerConstraint("compA", "> 2.1.0", "< 2.3.0"), + ComponentSemVerConstraint("compB", ">= 1.0.0"), + )).Should(Equal(ComponentSemVerConstraints{ + "compA": []string{"> 2.1.0", "< 2.2.0", "< 2.3.0"}, + "compB": []string{">= 1.0.0"}, + })) + }) +}) + var _ = Describe("Constructing nodes", func() { var dt *types.DeprecationTracker var didRun bool @@ -505,6 +518,69 @@ var _ = Describe("Constructing nodes", func() { }) }) + Describe("The ComponentSemVerConstraint decoration", func() { + It("has no ComponentSemVerConstraints by default", func() { + node, errors := internal.NewNode(dt, ntIt, "text", body) + Ω(node).ShouldNot(BeZero()) + Ω(node.ComponentSemVerConstraints).Should(Equal(ComponentSemVerConstraints{})) + ExpectAllWell(errors) + }) + + It("can track ComponentSemVerConstraints", func() { + node, errors := internal.NewNode(dt, ntIt, "text", body, ComponentSemVerConstraint("compA", ">= 1.0.0", "< 2.0.0")) + Ω(node.ComponentSemVerConstraints).Should(Equal(ComponentSemVerConstraints{ + "compA": SemVerConstraints{">= 1.0.0", "< 2.0.0"}, + })) + ExpectAllWell(errors) + }) + + It("appends and dedupes all ComponentSemVerConstraints together, even if nested", func() { + node, errors := internal.NewNode(dt, ntIt, "text", body, + ComponentSemVerConstraint("compA", ">= 1.0.0"), + ComponentSemVerConstraint("compA", "< 2.0.0"), + ComponentSemVerConstraint("compB", ">= 0.1.0"), + []any{ + ComponentSemVerConstraint("compA", ">= 1.0.0"), + []any{ + ComponentSemVerConstraint("compA", "< 1.9.0"), + ComponentSemVerConstraint("compB", ">= 0.1.0"), + }, + }, + ) + Ω(node.ComponentSemVerConstraints).Should(Equal(ComponentSemVerConstraints{ + "compA": SemVerConstraints{">= 1.0.0", "< 2.0.0", "< 1.9.0"}, + "compB": SemVerConstraints{">= 0.1.0"}, + })) + ExpectAllWell(errors) + }) + + It("can be applied to containers", func() { + node, errors := internal.NewNode(dt, ntCon, "text", body, ComponentSemVerConstraint("compA", ">= 1.0.0", "< 2.0.0")) + Ω(node.ComponentSemVerConstraints).Should(Equal(ComponentSemVerConstraints{ + "compA": SemVerConstraints{">= 1.0.0", "< 2.0.0"}, + })) + ExpectAllWell(errors) + }) + + It("cannot be applied to non-container/it nodes", func() { + node, errors := internal.NewNode(dt, ntBef, "", body, cl, ComponentSemVerConstraint("compA", ">= 1.0.0", "< 2.0.0")) + Ω(node).Should(BeZero()) + Ω(errors).Should(ConsistOf(types.GinkgoErrors.InvalidDecoratorForNodeType(cl, ntBef, "ComponentSemVerConstraint"))) + Ω(dt.DidTrackDeprecations()).Should(BeFalse()) + }) + + It("validates ComponentSemVerConstraints", func() { + node, errors := internal.NewNode(dt, ntIt, "", body, cl, ComponentSemVerConstraint("", "&| 1.0.0"), ComponentSemVerConstraint("compA", "")) + Ω(node).Should(BeZero()) + Ω(errors).Should(ConsistOf( + types.GinkgoErrors.InvalidEmptyComponentForSemVerConstraint(cl), + types.GinkgoErrors.InvalidSemVerConstraint("&| 1.0.0", "improper constraint: &| 1.0.0", cl), + types.GinkgoErrors.InvalidEmptySemVerConstraint(cl), + )) + Ω(dt.DidTrackDeprecations()).Should(BeFalse()) + }) + }) + Describe("The SpecPriority decorator", func() { It("has no SpecPriority by default", func() { node, errors := internal.NewNode(dt, ntIt, "text", body) @@ -1569,6 +1645,36 @@ var _ = Describe("Nodes", func() { }) }) + Describe("ComponentSemVerConstraints and UnionOfComponentSemVerConstraints", func() { + var nodes Nodes + BeforeEach(func() { + nodes = Nodes{ + N(ComponentSemVerConstraint("database", ">= 1.0.0", "< 2.0.0")), + N(ComponentSemVerConstraint("cache", "^1.2.3")), + N(), + N(ComponentSemVerConstraint("database", ">= 1.0.0")), + N(ComponentSemVerConstraint("cache", "~1.2.x")), + } + }) + + It("ComponentSemVerConstraints returns a map containing the component semver constraints for each node in order", func() { + Ω(nodes.ComponentSemVerConstraints()).Should(Equal([]map[string][]string{ + {"database": {">= 1.0.0", "< 2.0.0"}}, + {"cache": {"^1.2.3"}}, + {}, + {"database": {">= 1.0.0"}}, + {"cache": {"~1.2.x"}}, + })) + }) + + It("UnionOfComponentSemVerConstraints returns a single map of component semver constraints harvested from all nodes and deduped", func() { + Ω(nodes.UnionOfComponentSemVerConstraints()).Should(Equal(map[string][]string{ + "database": {">= 1.0.0", "< 2.0.0"}, + "cache": {"^1.2.3", "~1.2.x"}, + })) + }) + }) + Describe("CodeLocation", func() { var nodes Nodes var cl1, cl2 types.CodeLocation @@ -1819,6 +1925,22 @@ var _ = Describe("Nodes", func() { }).Should(Panic()) }) }) + + Describe("ComponentSemVerConstraints", func() { + It("can match against a filter", func() { + Ω(ComponentSemVerConstraint("").MatchesSemVerFilter("", "")).Should(BeTrue()) + Ω(ComponentSemVerConstraint("compA", ">= 1.0.0", "< 2.0.0").MatchesSemVerFilter("compA", "1.2.0")).Should(BeTrue()) + Ω(ComponentSemVerConstraint("compB", "^1.0.x").MatchesSemVerFilter("compB", "1.2.0")).Should(BeTrue()) + Ω(ComponentSemVerConstraint("compC", "~1.2.3").MatchesSemVerFilter("compC", "1.2.5")).Should(BeTrue()) + Ω(ComponentSemVerConstraint("compD", "1.0.0 - 2.0.0").MatchesSemVerFilter("compD", "1.2.0")).Should(BeTrue()) + Ω(ComponentSemVerConstraint("compE", "!= 1.2.0").MatchesSemVerFilter("compE", "1.2.0")).Should(BeFalse()) + Ω(ComponentSemVerConstraint("compF", "> 1.2.0").MatchesSemVerFilter("compF", "1.2.0")).Should(BeFalse()) + Ω(ComponentSemVerConstraint("compFFF", "> 1.2.0").MatchesSemVerFilter("compF", "1.2.0")).Should(BeFalse()) + Ω(func() { + ComponentSemVerConstraint("compG", "> 1.0.0").MatchesSemVerFilter("compG", "aaa") + }).Should(Panic()) + }) + }) }) var _ = Describe("Iteration Performance", Serial, Label("performance"), func() { @@ -2078,20 +2200,22 @@ var _ = Describe("ConstructionNodeReport", func() { } actualDescribeReport := CurrentTreeConstructionNodeReport() - expectDescribeReport := newConstructionNodeReport(types.ConstructionNodeReport{}, []container{{"", 0, nil, nil}, {"ConstructionNodeReport", describeLine + 1, []string{}, []string{}}}) + expectDescribeReport := newConstructionNodeReport(types.ConstructionNodeReport{}, []container{{"", 0, nil, nil, nil}, {"ConstructionNodeReport", describeLine + 1, []string{}, []string{}, map[string][]string{}}}) expectEqual(actualDescribeReport, expectDescribeReport) _, _, contextLine, _ := runtime.Caller(0) Context("context", func() { actual := CurrentTreeConstructionNodeReport() - expect := newConstructionNodeReport(expectDescribeReport, []container{{"context", contextLine + 1, []string{}, []string{}}}) + expect := newConstructionNodeReport(expectDescribeReport, []container{{"context", contextLine + 1, []string{}, []string{}, map[string][]string{}}}) expectEqual(actual, expect) }) _, _, complexLine, _ := runtime.Caller(0) - Context("complex", Label("A"), Label("B"), SemVerConstraint("> 1.0.0", "<= 3.0.0"), func() { + Context("complex", Label("A"), Label("B"), SemVerConstraint("> 1.0.0", "<= 3.0.0"), ComponentSemVerConstraint("etcd", "> 0.1.0", "<= 0.5.0"), func() { actual := CurrentTreeConstructionNodeReport() - expect := newConstructionNodeReport(expectDescribeReport, []container{{"complex", complexLine + 1, []string{"A", "B"}, []string{"> 1.0.0", "<= 3.0.0"}}}) + expect := newConstructionNodeReport(expectDescribeReport, []container{ + {"complex", complexLine + 1, []string{"A", "B"}, []string{"> 1.0.0", "<= 3.0.0"}, map[string][]string{"etcd": {"> 0.1.0", "<= 0.5.0"}}}, + }) expectEqual(actual, expect) }) @@ -2100,7 +2224,7 @@ var _ = Describe("ConstructionNodeReport", func() { actual := CurrentTreeConstructionNodeReport() expect := expectDescribeReport expect.IsSerial = true - expect = newConstructionNodeReport(expect, []container{{"serial", serialLine + 1, []string{"Serial"}, []string{}}}) + expect = newConstructionNodeReport(expect, []container{{"serial", serialLine + 1, []string{"Serial"}, []string{}, map[string][]string{}}}) expectEqual(actual, expect) }) @@ -2109,7 +2233,7 @@ var _ = Describe("ConstructionNodeReport", func() { actual := CurrentTreeConstructionNodeReport() expect := expectDescribeReport expect.IsInOrderedContainer = true - expect = newConstructionNodeReport(expect, []container{{"ordered", orderedLine + 1, []string{}, []string{}}}) + expect = newConstructionNodeReport(expect, []container{{"ordered", orderedLine + 1, []string{}, []string{}, map[string][]string{}}}) expectEqual(actual, expect) }) @@ -2117,7 +2241,7 @@ var _ = Describe("ConstructionNodeReport", func() { Context("outer", func() { Context("inner", func() { actual := CurrentTreeConstructionNodeReport() - expect := newConstructionNodeReport(expectDescribeReport, []container{{"outer", outerLine + 1, []string{}, []string{}}, {"inner", outerLine + 2, []string{}, []string{}}}) + expect := newConstructionNodeReport(expectDescribeReport, []container{{"outer", outerLine + 1, []string{}, []string{}, map[string][]string{}}, {"inner", outerLine + 2, []string{}, []string{}, map[string][]string{}}}) expectEqual(actual, expect) // The transformer runs while constructing the following It node. @@ -2133,7 +2257,7 @@ var _ = Describe("ConstructionNodeReport", func() { }) var actual ConstructionNodeReport - expect := newConstructionNodeReport(expectDescribeReport, []container{{"outer", outerLine + 1, []string{}, []string{}}}) + expect := newConstructionNodeReport(expectDescribeReport, []container{{"outer", outerLine + 1, []string{}, []string{}, map[string][]string{}}}) remove := AddTreeConstructionNodeArgsTransformer(func(nodeType types.NodeType, offset Offset, text string, args []any) (string, []any, []error) { actual = CurrentTreeConstructionNodeReport() return text, args, nil @@ -2145,10 +2269,11 @@ var _ = Describe("ConstructionNodeReport", func() { }) type container struct { - text string - line int - labels []string - semVerConstraints []string + text string + line int + labels []string + semVerConstraints []string + componentSemVerConstraints map[string][]string } // newConstructionNodeReport makes a deep copy and extends the given report. @@ -2157,6 +2282,7 @@ func newConstructionNodeReport(report types.ConstructionNodeReport, containers [ report.ContainerHierarchyLocations = slices.Clone(report.ContainerHierarchyLocations) report.ContainerHierarchyLabels = slices.Clone(report.ContainerHierarchyLabels) report.ContainerHierarchySemVerConstraints = slices.Clone(report.ContainerHierarchySemVerConstraints) + report.ContainerHierarchyComponentSemVerConstraints = slices.Clone(report.ContainerHierarchyComponentSemVerConstraints) for _, container := range containers { report.ContainerHierarchyTexts = append(report.ContainerHierarchyTexts, container.text) fileName := "" @@ -2166,6 +2292,7 @@ func newConstructionNodeReport(report types.ConstructionNodeReport, containers [ report.ContainerHierarchyLocations = append(report.ContainerHierarchyLocations, types.CodeLocation{FileName: fileName, LineNumber: container.line}) report.ContainerHierarchyLabels = append(report.ContainerHierarchyLabels, container.labels) report.ContainerHierarchySemVerConstraints = append(report.ContainerHierarchySemVerConstraints, container.semVerConstraints) + report.ContainerHierarchyComponentSemVerConstraints = append(report.ContainerHierarchyComponentSemVerConstraints, container.componentSemVerConstraints) } return report } diff --git a/internal/reporters/gojson.go b/internal/reporters/gojson.go index 8b7a9ceab..751543ea7 100644 --- a/internal/reporters/gojson.go +++ b/internal/reporters/gojson.go @@ -83,7 +83,7 @@ func goJSONActionFromSpecState(state types.SpecState) GoJSONAction { type gojsonReport struct { o types.Report // Extra calculated fields - goPkg string + goPkg string elapsed float64 } @@ -109,8 +109,8 @@ type gojsonSpecReport struct { o types.SpecReport // extra calculated fields testName string - elapsed float64 - action GoJSONAction + elapsed float64 + action GoJSONAction } func newSpecReport(in types.SpecReport) *gojsonSpecReport { @@ -141,18 +141,31 @@ func suitePathToPkg(dir string) (string, error) { } func createTestName(spec types.SpecReport) string { - name := fmt.Sprintf("[%s]", spec.LeafNodeType) - if spec.FullText() != "" { - name = name + " " + spec.FullText() - } - labels := spec.Labels() - if len(labels) > 0 { - name = name + " [" + strings.Join(labels, ", ") + "]" - } - semVerConstraints := spec.SemVerConstraints() - if len(semVerConstraints) > 0 { - name = name + " [" + strings.Join(semVerConstraints, ", ") + "]" - } - name = strings.TrimSpace(name) - return name + name := fmt.Sprintf("[%s]", spec.LeafNodeType) + if spec.FullText() != "" { + name = name + " " + spec.FullText() + } + labels := spec.Labels() + if len(labels) > 0 { + name = name + " [" + strings.Join(labels, ", ") + "]" + } + semVerConstraints := spec.SemVerConstraints() + if len(semVerConstraints) > 0 { + name = name + " [" + strings.Join(semVerConstraints, ", ") + "]" + } + componentSemVerConstraints := spec.ComponentSemVerConstraints() + if len(componentSemVerConstraints) > 0 { + name = name + " [" + formatComponentSemVerConstraintsToString(componentSemVerConstraints) + "]" + } + name = strings.TrimSpace(name) + return name +} + +func formatComponentSemVerConstraintsToString(componentSemVerConstraints map[string][]string) string { + var tmpStr string + for component, semVerConstraints := range componentSemVerConstraints { + tmpStr = tmpStr + fmt.Sprintf("%s: %s, ", component, semVerConstraints) + } + tmpStr = strings.TrimSuffix(tmpStr, ", ") + return tmpStr } diff --git a/internal/suite.go b/internal/suite.go index 9d5f59001..8f711e507 100644 --- a/internal/suite.go +++ b/internal/suite.go @@ -108,13 +108,13 @@ func (suite *Suite) BuildTree() error { return nil } -func (suite *Suite) Run(description string, suiteLabels Labels, suiteSemVerConstraints SemVerConstraints, suiteAroundNodes types.AroundNodes, suitePath string, failer *Failer, reporter reporters.Reporter, writer WriterInterface, outputInterceptor OutputInterceptor, interruptHandler interrupt_handler.InterruptHandlerInterface, client parallel_support.Client, progressSignalRegistrar ProgressSignalRegistrar, suiteConfig types.SuiteConfig) (bool, bool) { +func (suite *Suite) Run(description string, suiteLabels Labels, suiteSemVerConstraints SemVerConstraints, suiteComponentSemVerConstraints ComponentSemVerConstraints, suiteAroundNodes types.AroundNodes, suitePath string, failer *Failer, reporter reporters.Reporter, writer WriterInterface, outputInterceptor OutputInterceptor, interruptHandler interrupt_handler.InterruptHandlerInterface, client parallel_support.Client, progressSignalRegistrar ProgressSignalRegistrar, suiteConfig types.SuiteConfig) (bool, bool) { if suite.phase != PhaseBuildTree { panic("cannot run before building the tree = call suite.BuildTree() first") } ApplyNestedFocusPolicyToTree(suite.tree) specs := GenerateSpecsFromTreeRoot(suite.tree) - specs, hasProgrammaticFocus := ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteConfig) + specs, hasProgrammaticFocus := ApplyFocusToSpecs(specs, description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, suiteConfig) specs = ComputeAroundNodes(specs) suite.phase = PhaseRun @@ -133,7 +133,7 @@ func (suite *Suite) Run(description string, suiteLabels Labels, suiteSemVerConst cancelProgressHandler := progressSignalRegistrar(suite.handleProgressSignal) - success := suite.runSpecs(description, suiteLabels, suiteSemVerConstraints, suitePath, hasProgrammaticFocus, specs) + success := suite.runSpecs(description, suiteLabels, suiteSemVerConstraints, suiteComponentSemVerConstraints, suitePath, hasProgrammaticFocus, specs) cancelProgressHandler() @@ -456,16 +456,17 @@ func (suite *Suite) processCurrentSpecReport() { } } -func (suite *Suite) runSpecs(description string, suiteLabels Labels, suiteSemVerConstraints SemVerConstraints, suitePath string, hasProgrammaticFocus bool, specs Specs) bool { +func (suite *Suite) runSpecs(description string, suiteLabels Labels, suiteSemVerConstraints SemVerConstraints, suiteComponentSemVerConstraints ComponentSemVerConstraints, suitePath string, hasProgrammaticFocus bool, specs Specs) bool { numSpecsThatWillBeRun := specs.CountWithoutSkip() suite.report = types.Report{ - SuitePath: suitePath, - SuiteDescription: description, - SuiteLabels: suiteLabels, - SuiteSemVerConstraints: suiteSemVerConstraints, - SuiteConfig: suite.config, - SuiteHasProgrammaticFocus: hasProgrammaticFocus, + SuitePath: suitePath, + SuiteDescription: description, + SuiteLabels: suiteLabels, + SuiteSemVerConstraints: suiteSemVerConstraints, + SuiteComponentSemVerConstraints: suiteComponentSemVerConstraints, + SuiteConfig: suite.config, + SuiteHasProgrammaticFocus: hasProgrammaticFocus, PreRunStats: types.PreRunStats{ TotalSpecs: len(specs), SpecsThatWillRun: numSpecsThatWillBeRun, diff --git a/internal/suite_test.go b/internal/suite_test.go index 8e9728003..a2e31845c 100644 --- a/internal/suite_test.go +++ b/internal/suite_test.go @@ -61,7 +61,7 @@ var _ = Describe("Suite", func() { Ω(rt).Should(HaveTracked("traversing outer", "traversing nested")) rt.Reset() - suite.Run("suite", Labels{}, SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + suite.Run("suite", Labels{}, SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) Ω(rt).Should(HaveTracked("running it")) Ω(err1).ShouldNot(HaveOccurred()) @@ -98,7 +98,7 @@ var _ = Describe("Suite", func() { Ω(suite.BuildTree()).Should(Succeed()) Ω(rt).Should(HaveTracked("traversing outer", "traversing nested")) rt.Reset() - suite.Run("suite", Labels{}, SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + suite.Run("suite", Labels{}, SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) Ω(rt).Should(HaveTracked("before-suite", "running it")) Ω(err1).ShouldNot(HaveOccurred()) @@ -111,7 +111,7 @@ var _ = Describe("Suite", func() { Ω(clone.BuildTree()).Should(Succeed()) Ω(rt).Should(HaveTracked("traversing outer", "traversing nested")) rt.Reset() - clone.Run("suite", Labels{}, SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + clone.Run("suite", Labels{}, SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) Ω(rt).Should(HaveTracked("before-suite", "running it")) Ω(err1).ShouldNot(HaveOccurred()) @@ -134,7 +134,7 @@ var _ = Describe("Suite", func() { })) Ω(suite.BuildTree()).Should(Succeed()) - suite.Run("suite", Labels{}, SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + suite.Run("suite", Labels{}, SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) Ω(err).ShouldNot(HaveOccurred()) Ω(truey).Should(BeTrue()) @@ -158,7 +158,7 @@ var _ = Describe("Suite", func() { }) It("errors", func() { - suite.Run("suite", Labels{}, SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + suite.Run("suite", Labels{}, SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) Ω(pushNodeErrDuringRun).Should(HaveOccurred()) Ω(rt).Should(HaveTracked("in it")) }) @@ -342,7 +342,7 @@ var _ = Describe("Suite", func() { Ω(err).ShouldNot(HaveOccurred()) Ω(suite.BuildTree()).Should(Succeed()) - suite.Run("suite", Labels{}, SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + suite.Run("suite", Labels{}, SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) Ω(pushSuiteNodeErr).Should(HaveOccurred()) }) }) @@ -385,7 +385,7 @@ var _ = Describe("Suite", func() { Ω(errors[1]).ShouldNot(HaveOccurred()) Ω(errors[2]).ShouldNot(HaveOccurred()) - suite.Run("suite", Labels{}, SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + suite.Run("suite", Labels{}, SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) Ω(errors[3]).Should(MatchError(types.GinkgoErrors.PushingCleanupInReportingNode(cl, types.NodeTypeReportBeforeEach))) }) }) @@ -407,7 +407,7 @@ var _ = Describe("Suite", func() { Ω(errors[1]).ShouldNot(HaveOccurred()) Ω(errors[2]).ShouldNot(HaveOccurred()) - suite.Run("suite", Labels{}, SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + suite.Run("suite", Labels{}, SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) Ω(errors[3]).Should(MatchError(types.GinkgoErrors.PushingCleanupInReportingNode(cl, types.NodeTypeReportAfterEach))) }) }) @@ -429,7 +429,7 @@ var _ = Describe("Suite", func() { Ω(suite.BuildTree()).Should(Succeed()) Ω(errors[2]).ShouldNot(HaveOccurred()) - suite.Run("suite", Labels{}, SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + suite.Run("suite", Labels{}, SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) Ω(errors[3]).Should(MatchError(types.GinkgoErrors.PushingCleanupInReportingNode(cl, types.NodeTypeReportBeforeSuite))) }) }) @@ -451,7 +451,7 @@ var _ = Describe("Suite", func() { Ω(suite.BuildTree()).Should(Succeed()) Ω(errors[2]).ShouldNot(HaveOccurred()) - suite.Run("suite", Labels{}, SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + suite.Run("suite", Labels{}, SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) Ω(errors[3]).Should(MatchError(types.GinkgoErrors.PushingCleanupInReportingNode(cl, types.NodeTypeReportAfterSuite))) }) }) @@ -468,7 +468,7 @@ var _ = Describe("Suite", func() { })) Ω(errors[0]).ShouldNot(HaveOccurred()) Ω(suite.BuildTree()).Should(Succeed()) - suite.Run("suite", Labels{}, SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) + suite.Run("suite", Labels{}, SemVerConstraints{}, ComponentSemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, internal.RegisterForProgressSignal, conf) Ω(errors[1]).ShouldNot(HaveOccurred()) Ω(errors[2]).Should(MatchError(types.GinkgoErrors.PushingCleanupInCleanupNode(cl))) }) diff --git a/reporters/__snapshots__/junit_report_test.snap b/reporters/__snapshots__/junit_report_test.snap index 0e366291c..22d50f0d3 100755 --- a/reporters/__snapshots__/junit_report_test.snap +++ b/reporters/__snapshots__/junit_report_test.snap @@ -9,6 +9,7 @@ + diff --git a/reporters/default_reporter.go b/reporters/default_reporter.go index 026d9cf9b..ef66b2289 100644 --- a/reporters/default_reporter.go +++ b/reporters/default_reporter.go @@ -75,6 +75,9 @@ func (r *DefaultReporter) SuiteWillBegin(report types.Report) { if len(report.SuiteSemVerConstraints) > 0 { r.emit(r.f("{{coral}}[%s]{{/}} ", strings.Join(report.SuiteSemVerConstraints, ", "))) } + if len(report.SuiteComponentSemVerConstraints) > 0 { + r.emit(r.f("{{coral}}[Components: %s]{{/}} ", formatComponentSemVerConstraintsToString(report.SuiteComponentSemVerConstraints))) + } r.emit(r.f("- %d/%d specs ", report.PreRunStats.SpecsThatWillRun, report.PreRunStats.TotalSpecs)) if report.SuiteConfig.ParallelTotal > 1 { r.emit(r.f("- %d procs ", report.SuiteConfig.ParallelTotal)) @@ -97,6 +100,13 @@ func (r *DefaultReporter) SuiteWillBegin(report types.Report) { bannerWidth = len(semVerConstraints) + 2 } } + if len(report.SuiteComponentSemVerConstraints) > 0 { + componentSemVerConstraints := formatComponentSemVerConstraintsToString(report.SuiteComponentSemVerConstraints) + r.emitBlock(r.f("{{coral}}[Components: %s]{{/}} ", componentSemVerConstraints)) + if len(componentSemVerConstraints)+2 > bannerWidth { + bannerWidth = len(componentSemVerConstraints) + 2 + } + } r.emitBlock(strings.Repeat("=", bannerWidth)) out := r.f("Random Seed: {{bold}}%d{{/}}", report.SuiteConfig.RandomSeed) @@ -725,8 +735,12 @@ func (r *DefaultReporter) cycleJoin(elements []string, joiner string) string { } func (r *DefaultReporter) codeLocationBlock(report types.SpecReport, highlightColor string, veryVerbose bool, usePreciseFailureLocation bool) string { - texts, locations, labels, semVerConstraints := []string{}, []types.CodeLocation{}, [][]string{}, [][]string{} - texts, locations, labels, semVerConstraints = append(texts, report.ContainerHierarchyTexts...), append(locations, report.ContainerHierarchyLocations...), append(labels, report.ContainerHierarchyLabels...), append(semVerConstraints, report.ContainerHierarchySemVerConstraints...) + texts, locations, labels, semVerConstraints, componentSemVerConstraints := []string{}, []types.CodeLocation{}, [][]string{}, [][]string{}, []map[string][]string{} + texts = append(texts, report.ContainerHierarchyTexts...) + locations = append(locations, report.ContainerHierarchyLocations...) + labels = append(labels, report.ContainerHierarchyLabels...) + semVerConstraints = append(semVerConstraints, report.ContainerHierarchySemVerConstraints...) + componentSemVerConstraints = append(componentSemVerConstraints, report.ContainerHierarchyComponentSemVerConstraints...) if report.LeafNodeType.Is(types.NodeTypesForSuiteLevelNodes) { texts = append(texts, r.f("[%s] %s", report.LeafNodeType, report.LeafNodeText)) @@ -735,6 +749,7 @@ func (r *DefaultReporter) codeLocationBlock(report types.SpecReport, highlightCo } labels = append(labels, report.LeafNodeLabels) semVerConstraints = append(semVerConstraints, report.LeafNodeSemVerConstraints) + componentSemVerConstraints = append(componentSemVerConstraints, report.LeafNodeComponentSemVerConstraints) locations = append(locations, report.LeafNodeLocation) failureLocation := report.Failure.FailureNodeLocation @@ -749,6 +764,7 @@ func (r *DefaultReporter) codeLocationBlock(report types.SpecReport, highlightCo locations = append([]types.CodeLocation{failureLocation}, locations...) labels = append([][]string{{}}, labels...) semVerConstraints = append([][]string{{}}, semVerConstraints...) + componentSemVerConstraints = append([]map[string][]string{{}}, componentSemVerConstraints...) highlightIndex = 0 case types.FailureNodeInContainer: i := report.Failure.FailureNodeContainerIndex @@ -779,6 +795,9 @@ func (r *DefaultReporter) codeLocationBlock(report types.SpecReport, highlightCo if len(semVerConstraints[i]) > 0 { out += r.f(" {{coral}}[%s]{{/}}", strings.Join(semVerConstraints[i], ", ")) } + if len(componentSemVerConstraints[i]) > 0 { + out += r.f(" {{coral}}[%s]{{/}}", formatComponentSemVerConstraintsToString(componentSemVerConstraints[i])) + } out += "\n" out += r.fi(uint(i), "{{gray}}%s{{/}}\n", locations[i]) } @@ -806,6 +825,10 @@ func (r *DefaultReporter) codeLocationBlock(report types.SpecReport, highlightCo if len(flattenedSemVerConstraints) > 0 { out += r.f(" {{coral}}[%s]{{/}}", strings.Join(flattenedSemVerConstraints, ", ")) } + flattenedComponentSemVerConstraints := report.ComponentSemVerConstraints() + if len(flattenedComponentSemVerConstraints) > 0 { + out += r.f(" {{coral}}[%s]{{/}}", formatComponentSemVerConstraintsToString(flattenedComponentSemVerConstraints)) + } out += "\n" if usePreciseFailureLocation { out += r.f("{{gray}}%s{{/}}", failureLocation) diff --git a/reporters/default_reporter_test.go b/reporters/default_reporter_test.go index 8aba93ab9..9788a5fd4 100644 --- a/reporters/default_reporter_test.go +++ b/reporters/default_reporter_test.go @@ -29,6 +29,7 @@ var cl1 = types.CodeLocation{FileName: "cl1.go", LineNumber: 37, FullStackTrace: var cl2 = types.CodeLocation{FileName: "cl2.go", LineNumber: 80, FullStackTrace: "full-trace\ncl-2"} var cl3 = types.CodeLocation{FileName: "cl3.go", LineNumber: 103, FullStackTrace: "full-trace\ncl-3"} var cl4 = types.CodeLocation{FileName: "cl4.go", LineNumber: 144, FullStackTrace: "full-trace\ncl-4"} + // simulate current time ast static time var now = time.Date(2025, time.September, 9, 10, 50, 0, 0, time.UTC) @@ -38,6 +39,9 @@ func CLabels(labels ...Labels) []Labels { return labels } func CSemVerConstraints(semVerConstraints ...SemVerConstraints) []SemVerConstraints { return semVerConstraints } +func CComponentSemVerConstraints(compSemVerConstraints ...ComponentSemVerConstraints) []ComponentSemVerConstraints { + return compSemVerConstraints +} func spr(format string, args ...any) string { return fmt.Sprintf(format, args...) } type FailureNodeLocation types.CodeLocation @@ -134,6 +138,11 @@ func S(options ...any) types.SpecReport { for _, semVerConstraints := range x { report.ContainerHierarchySemVerConstraints = append(report.ContainerHierarchySemVerConstraints, []string(semVerConstraints)) } + case []ComponentSemVerConstraints: + report.ContainerHierarchyComponentSemVerConstraints = []map[string][]string{} + for _, compSemVerConstraints := range x { + report.ContainerHierarchyComponentSemVerConstraints = append(report.ContainerHierarchyComponentSemVerConstraints, map[string][]string(compSemVerConstraints)) + } case string: report.LeafNodeText = x case types.NodeType: @@ -144,6 +153,8 @@ func S(options ...any) types.SpecReport { report.LeafNodeLabels = x case SemVerConstraints: report.LeafNodeSemVerConstraints = x + case ComponentSemVerConstraints: + report.LeafNodeComponentSemVerConstraints = x case types.SpecState: report.State = x case time.Duration: @@ -180,6 +191,11 @@ func S(options ...any) types.SpecReport { report.ContainerHierarchySemVerConstraints = append(report.ContainerHierarchySemVerConstraints, []string{}) } } + if len(report.ContainerHierarchyComponentSemVerConstraints) == 0 { + for range report.ContainerHierarchyTexts { + report.ContainerHierarchyComponentSemVerConstraints = append(report.ContainerHierarchyComponentSemVerConstraints, map[string][]string{}) + } + } return report } @@ -495,6 +511,21 @@ var _ = Describe("DefaultReporter", func() { "Will run {{bold}}15{{/}} of {{bold}}20{{/}} specs", "", ), + Entry("With ComponentSemVerConstraints", + C(), + types.Report{ + SuiteDescription: "My Suite", SuitePath: "/path/to/suite", SuiteComponentSemVerConstraints: map[string][]string{"kubelet": []string{">=1.0.0, <2.0.0"}, "etcd": []string{">=3.0.0", "<4.0.0"}}, + PreRunStats: types.PreRunStats{SpecsThatWillRun: 15, TotalSpecs: 20}, + SuiteConfig: types.SuiteConfig{RandomSeed: 17, ParallelTotal: 1}, + }, + "Running Suite: My Suite - /path/to/suite", + "{{coral}}[Components: etcd: [>=3.0.0 <4.0.0], kubelet: [>=1.0.0, <2.0.0]]{{/}} ", + "====================================================", + "Random Seed: {{bold}}17{{/}}", + "", + "Will run {{bold}}15{{/}} of {{bold}}20{{/}} specs", + "", + ), Entry("When configured to randomize all specs", C(), types.Report{ @@ -554,6 +585,15 @@ var _ = Describe("DefaultReporter", func() { }, "[17] {{bold}}My Suite{{/}} {{coral}}[> 1.0.0, < 2.0.0]{{/}} - 15/20 specs - 3 procs ", ), + Entry("when succinct and with ComponentSemVerConstraints", + C(Succinct), + types.Report{ + SuiteDescription: "My Suite", SuitePath: "/path/to/suite", SuiteComponentSemVerConstraints: map[string][]string{"kubelet": []string{">=1.0.0, <2.0.0"}, "etcd": []string{">=3.0.0", "<4.0.0"}}, + PreRunStats: types.PreRunStats{SpecsThatWillRun: 15, TotalSpecs: 20}, + SuiteConfig: types.SuiteConfig{RandomSeed: 17, ParallelTotal: 3}, + }, + "[17] {{bold}}My Suite{{/}} {{coral}}[Components: etcd: [>=3.0.0 <4.0.0], kubelet: [>=1.0.0, <2.0.0]]{{/}} - 15/20 specs - 3 procs ", + ), ) DescribeTable("WillRun", @@ -607,6 +647,18 @@ var _ = Describe("DefaultReporter", func() { "{{gray}}"+cl2.String()+"{{/}}", "", ), + Entry("specs with ComponentSemVerConstraints", C(Verbose), + S(CTS("Container", "Nested Container"), "My Test", CLS(cl0, cl1), cl2, CComponentSemVerConstraints( + ComponentSemVerConstraint("etcd", ">=3.0.0, <4.0.0"), + ComponentSemVerConstraint("kubelet", ">=1.0.0", "<2.0.0"), + ), + ComponentSemVerConstraint("kubelet", ">1.5.0"), + ), + DELIMITER, + "{{/}}Container {{gray}}Nested Container {{/}}{{bold}}My Test{{/}} {{coral}}[etcd: [>=3.0.0, <4.0.0], kubelet: [>=1.0.0 <2.0.0 >1.5.0]]{{/}}", + "{{gray}}"+cl2.String()+"{{/}}", + "", + ), ) DescribeTable("WillRun => DidRun", @@ -2033,6 +2085,10 @@ var _ = Describe("DefaultReporter", func() { types.SpecStateFailed, 2, F("FAILURE MESSAGE\nWITH DETAILS", types.FailureNodeInContainer, FailureNodeLocation(cl3), types.NodeTypeJustBeforeEach, 1, cl4), ), + S(CTS("Describe A", " Context D"), "The Test", CLS(cl0, cl1), cl2, CComponentSemVerConstraints(ComponentSemVerConstraint("etcd", "> 1.0.0", "< 2.0.0"), ComponentSemVerConstraint("kubelet", "> 1.0.0", "< 3.0.0")), ComponentSemVerConstraint("api-server", ">5.0.0"), + types.SpecStateFailed, 2, + F("FAILURE MESSAGE\nWITH DETAILS", types.FailureNodeInContainer, FailureNodeLocation(cl3), types.NodeTypeJustBeforeEach, 1, cl4), + ), S(CTS("Describe A"), "The Test", CLS(cl0), cl1, types.SpecStatePanicked, 2, F("FAILURE MESSAGE\nWITH DETAILS", types.FailureNodeIsLeafNode, FailureNodeLocation(cl1), types.NodeTypeIt, cl2), @@ -2053,7 +2109,7 @@ var _ = Describe("DefaultReporter", func() { }, }, "", - "{{red}}{{bold}}Summarizing 8 Failures:{{/}}", + "{{red}}{{bold}}Summarizing 9 Failures:{{/}}", " {{red}}[FAIL]{{/}} {{red}}{{bold}}[It] repeater{{/}}", " {{gray}}cl3.go:103{{/}}", " {{red}}[FAIL]{{/}} {{red}}{{bold}}[It] another-repeater{{/}}", @@ -2062,6 +2118,8 @@ var _ = Describe("DefaultReporter", func() { " {{gray}}cl4.go:144{{/}}", " {{red}}[FAIL]{{/}} {{/}}Describe A {{red}}{{bold}}Context C [JustBeforeEach] {{/}}The Test{{/}} {{coral}}[> 1.0.0, < 2.0.0, < 3.0.0, > 2.1.0]{{/}}", " {{gray}}cl4.go:144{{/}}", + " {{red}}[FAIL]{{/}} {{/}}Describe A {{red}}{{bold}} Context D [JustBeforeEach] {{/}}The Test{{/}} {{coral}}[api-server: [>5.0.0], etcd: [> 1.0.0 < 2.0.0], kubelet: [> 1.0.0 < 3.0.0]]{{/}}", + " {{gray}}cl4.go:144{{/}}", " {{magenta}}[PANICKED!]{{/}} {{/}}Describe A {{magenta}}{{bold}}[It] The Test{{/}}", " {{gray}}cl2.go:80{{/}}", " {{orange}}[INTERRUPTED]{{/}} {{orange}}{{bold}}[It] The Test{{/}}", @@ -2071,8 +2129,8 @@ var _ = Describe("DefaultReporter", func() { " {{orange}}[TIMEDOUT]{{/}} {{orange}}{{bold}}[It] The Test{{/}}", " {{gray}}cl1.go:37{{/}}", "", - "{{red}}{{bold}}Ran 14 of 14 Specs in 60.000 seconds{{/}}", - "{{red}}{{bold}}FAIL!{{/}} -- {{green}}{{bold}}6 Passed{{/}} | {{red}}{{bold}}8 Failed{{/}} | {{light-yellow}}{{bold}}2 Flaked{{/}} | {{light-yellow}}{{bold}}2 Repeated{{/}} | {{yellow}}{{bold}}2 Pending{{/}} | {{cyan}}{{bold}}3 Skipped{{/}}", + "{{red}}{{bold}}Ran 15 of 14 Specs in 60.000 seconds{{/}}", + "{{red}}{{bold}}FAIL!{{/}} -- {{green}}{{bold}}6 Passed{{/}} | {{red}}{{bold}}9 Failed{{/}} | {{light-yellow}}{{bold}}2 Flaked{{/}} | {{light-yellow}}{{bold}}2 Repeated{{/}} | {{yellow}}{{bold}}2 Pending{{/}} | {{cyan}}{{bold}}3 Skipped{{/}}", "", ), Entry("the suite fails with failed suite setups", diff --git a/reporters/junit_report.go b/reporters/junit_report.go index 828f893fb..d4720ee94 100644 --- a/reporters/junit_report.go +++ b/reporters/junit_report.go @@ -13,9 +13,11 @@ package reporters import ( "encoding/xml" "fmt" + "maps" "os" "path" "regexp" + "slices" "strings" "github.com/onsi/ginkgo/v2/config" @@ -39,6 +41,9 @@ type JunitReportConfig struct { // Enable OmitSpecSemVerConstraints to prevent semantic version constraints from appearing in the spec name OmitSpecSemVerConstraints bool + // Enable OmitSpecComponentSemVerConstraints to prevent component semantic version constraints from appearing in the spec name + OmitSpecComponentSemVerConstraints bool + // Enable OmitLeafNodeType to prevent the spec leaf node type from appearing in the spec name OmitLeafNodeType bool @@ -173,6 +178,7 @@ func GenerateJUnitReportWithConfig(report types.Report, dst string, config Junit {"SpecialSuiteFailureReason", strings.Join(report.SpecialSuiteFailureReasons, ",")}, {"SuiteLabels", fmt.Sprintf("[%s]", strings.Join(report.SuiteLabels, ","))}, {"SuiteSemVerConstraints", fmt.Sprintf("[%s]", strings.Join(report.SuiteSemVerConstraints, ","))}, + {"SuiteComponentSemVerConstraints", fmt.Sprintf("[%s]", formatComponentSemVerConstraintsToString(report.SuiteComponentSemVerConstraints))}, {"RandomSeed", fmt.Sprintf("%d", report.SuiteConfig.RandomSeed)}, {"RandomizeAllSpecs", fmt.Sprintf("%t", report.SuiteConfig.RandomizeAllSpecs)}, {"LabelFilter", report.SuiteConfig.LabelFilter}, @@ -216,6 +222,10 @@ func GenerateJUnitReportWithConfig(report types.Report, dst string, config Junit if len(semVerConstraints) > 0 && !config.OmitSpecSemVerConstraints { name = name + " [" + strings.Join(semVerConstraints, ", ") + "]" } + componentSemVerConstraints := spec.ComponentSemVerConstraints() + if len(componentSemVerConstraints) > 0 && !config.OmitSpecComponentSemVerConstraints { + name = name + " [" + formatComponentSemVerConstraintsToString(componentSemVerConstraints) + "]" + } name = strings.TrimSpace(name) test := JUnitTestCase{ @@ -387,6 +397,16 @@ func systemOutForUnstructuredReporters(spec types.SpecReport) string { return spec.CapturedStdOutErr } +func formatComponentSemVerConstraintsToString(componentSemVerConstraints map[string][]string) string { + var tmpStr string + for _, key := range slices.Sorted(maps.Keys(componentSemVerConstraints)) { + tmpStr = tmpStr + fmt.Sprintf("%s: %s, ", key, componentSemVerConstraints[key]) + } + + tmpStr = strings.TrimSuffix(tmpStr, ", ") + return tmpStr +} + // Deprecated JUnitReporter (so folks can still compile their suites) type JUnitReporter struct{} diff --git a/reporters/teamcity_report.go b/reporters/teamcity_report.go index 55e1d1f4f..ed3e3a2bb 100644 --- a/reporters/teamcity_report.go +++ b/reporters/teamcity_report.go @@ -39,12 +39,16 @@ func GenerateTeamcityReport(report types.Report, dst string) error { name := report.SuiteDescription labels := report.SuiteLabels semVerConstraints := report.SuiteSemVerConstraints + componentSemVerConstraints := report.SuiteComponentSemVerConstraints if len(labels) > 0 { name = name + " [" + strings.Join(labels, ", ") + "]" } if len(semVerConstraints) > 0 { name = name + " [" + strings.Join(semVerConstraints, ", ") + "]" } + if len(componentSemVerConstraints) > 0 { + name = name + " [" + formatComponentSemVerConstraintsToString(componentSemVerConstraints) + "]" + } fmt.Fprintf(f, "##teamcity[testSuiteStarted name='%s']\n", tcEscape(name)) for _, spec := range report.SpecReports { name := fmt.Sprintf("[%s]", spec.LeafNodeType) @@ -59,6 +63,10 @@ func GenerateTeamcityReport(report types.Report, dst string) error { if len(semVerConstraints) > 0 { name = name + " [" + strings.Join(semVerConstraints, ", ") + "]" } + componentSemVerConstraints := spec.ComponentSemVerConstraints() + if len(componentSemVerConstraints) > 0 { + name = name + " [" + formatComponentSemVerConstraintsToString(componentSemVerConstraints) + "]" + } name = tcEscape(name) fmt.Fprintf(f, "##teamcity[testStarted name='%s']\n", name) diff --git a/types/errors.go b/types/errors.go index 59313238c..623e54b66 100644 --- a/types/errors.go +++ b/types/errors.go @@ -450,6 +450,15 @@ func (g ginkgoErrors) InvalidEmptySemVerConstraint(cl CodeLocation) error { } } +func (g ginkgoErrors) InvalidEmptyComponentForSemVerConstraint(cl CodeLocation) error { + return GinkgoError{ + Heading: "Invalid Empty Component for ComponentSemVerConstraint", + Message: "ComponentSemVerConstraint requires a non-empty component name", + CodeLocation: cl, + DocLink: "spec-semantic-version-filtering", + } +} + /* Table errors */ func (g ginkgoErrors) MultipleEntryBodyFunctionsForTable(cl CodeLocation) error { return GinkgoError{ diff --git a/types/semver_filter.go b/types/semver_filter.go index 3fc2ed144..71778078d 100644 --- a/types/semver_filter.go +++ b/types/semver_filter.go @@ -2,11 +2,12 @@ package types import ( "fmt" + "strings" "github.com/Masterminds/semver/v3" ) -type SemVerFilter func([]string) bool +type SemVerFilter func(component string, constraints []string) bool func MustParseSemVerFilter(input string) SemVerFilter { filter, err := ParseSemVerFilter(input) @@ -16,30 +17,90 @@ func MustParseSemVerFilter(input string) SemVerFilter { return filter } -func ParseSemVerFilter(filterVersion string) (SemVerFilter, error) { - if filterVersion == "" { - return func(_ []string) bool { return true }, nil +// ParseSemVerFilter parses non-component and component-specific semantic version filter string. +// The filter string can contain multiple non-component and component-specific versions separated by commas. +// Each component-specific version is in the format "component=version". +// If a version is specified without a component, it applies to non-component-specific constraints. +func ParseSemVerFilter(componentFilterVersions string) (SemVerFilter, error) { + if componentFilterVersions == "" { + return func(_ string, _ []string) bool { return true }, nil } - targetVersion, err := semver.NewVersion(filterVersion) - if err != nil { - return nil, fmt.Errorf("invalid filter version: %w", err) + result := map[string]*semver.Version{} + parts := strings.Split(componentFilterVersions, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if len(part) == 0 { + continue + } + if strings.Contains(part, "=") { + // validate component-specific version string + invalidPart, invalidErr := false, fmt.Errorf("invalid component filter version: %s", part) + subParts := strings.Split(part, "=") + if len(subParts) != 2 { + invalidPart = true + } + component := strings.TrimSpace(subParts[0]) + versionStr := strings.TrimSpace(subParts[1]) + if len(component) == 0 || len(versionStr) == 0 { + invalidPart = true + } + if invalidPart { + return nil, invalidErr + } + + // validate semver + v, err := semver.NewVersion(versionStr) + if err != nil { + return nil, fmt.Errorf("invalid component filter version: %s, error: %w", part, err) + } + result[component] = v + } else { + v, err := semver.NewVersion(part) + if err != nil { + return nil, fmt.Errorf("invalid filter version: %s, error: %w", part, err) + } + result[""] = v + } } - return func(constraints []string) bool { + return func(component string, constraints []string) bool { // unconstrained specs always run - if len(constraints) == 0 { + if len(component) == 0 && len(constraints) == 0 { return true } - for _, constraintStr := range constraints { - constraint, err := semver.NewConstraint(constraintStr) - if err != nil { - return false + // check non-component specific version constraints + if len(component) == 0 && len(constraints) != 0 { + v := result[""] + if v != nil { + for _, constraintStr := range constraints { + constraint, err := semver.NewConstraint(constraintStr) + if err != nil { + return false + } + + if !constraint.Check(v) { + return false + } + } } + } + + // check component-specific version constraints + if len(component) != 0 && len(constraints) != 0 { + v := result[component] + if v != nil { + for _, constraintStr := range constraints { + constraint, err := semver.NewConstraint(constraintStr) + if err != nil { + return false + } - if !constraint.Check(targetVersion) { - return false + if !constraint.Check(v) { + return false + } + } } } diff --git a/types/semver_filter_test.go b/types/semver_filter_test.go index 2502a6d60..a12444c77 100644 --- a/types/semver_filter_test.go +++ b/types/semver_filter_test.go @@ -23,31 +23,146 @@ var _ = Describe("SemVerFilter", func() { Entry("valid constraint", "2.x.0", ""), ) - DescribeTable("ParseSemVerFilter", func(version string, constraints []string, expectedErrMsg string, shouldPass bool) { - filterFn, err := types.ParseSemVerFilter(version) - if expectedErrMsg != "" { - Expect(err.Error()).Should(ContainSubstring(expectedErrMsg)) + type input struct { + version string + component string + constraints []string + expectedErrMsg string + shouldPass bool + } + DescribeTable("ParseSemVerFilter", func(i input) { + filterFn, err := types.ParseSemVerFilter(i.version) + if i.expectedErrMsg != "" { + Expect(err.Error()).Should(ContainSubstring(i.expectedErrMsg)) return } Expect(err).ShouldNot(HaveOccurred()) - Expect(filterFn(constraints)).To(Equal(shouldPass)) + Expect(filterFn(i.component, i.constraints)).To(Equal(i.shouldPass)) }, - Entry("no semantic version filter", "", []string{"> 1.0.0"}, "", true), - Entry("no semantic version constraints", "2.0.0", []string{}, "", true), - Entry("invalid semantic version filter", "a1.0.0", []string{"> 1.0.0"}, "invalid filter version", false), - Entry("matched semantic version with single constraint", "2.0.0", []string{"> 1.0.0"}, "", true), - Entry("matched semantic version with multiple constraints", "2.0.0", []string{"> 1.0.0", "< 3.0.0"}, "", true), - Entry("matched semantic version with complex constraint", "2.0.0", []string{"> 1.0.0, < 3.0.0"}, "", true), - Entry("unmatched semantic version with constraint", "0.1.0", []string{"> 1.0.0, < 3.0.0"}, "", false), + Entry("no semantic version filter for constraints", input{ + version: "", + component: "", + constraints: []string{"> 1.0.0"}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("no semantic version filter for component constraints", input{ + version: "", + component: "componentA", + constraints: []string{"> 1.0.0"}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("no semantic version constraints", input{ + version: "2.0.0", + component: "", + constraints: []string{}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("no semantic version constraints for a component", input{ + version: "2.0.0", + component: "componentA", + constraints: []string{}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("invalid semantic version filter", input{ + version: "a1.0.0", + component: "", + constraints: []string{"> 1.0.0"}, + expectedErrMsg: "invalid filter version", + shouldPass: false, + }), + Entry("matched semantic version with single constraint", input{ + version: "2.0.0", + component: "", + constraints: []string{"> 1.0.0"}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("matched semantic version with component single constraint", input{ + version: "compA=2.0.0", + component: "compA", + constraints: []string{"> 1.0.0"}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("matched semantic version with multiple constraints", input{ + version: "2.0.0", + component: "", + constraints: []string{"> 1.0.0", "< 3.0.0"}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("matched semantic version with component multiple constraints", input{ + version: "compA=2.0.0", + component: "compA", + constraints: []string{"> 1.0.0", "< 3.0.0"}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("ignore non-component semantic version with component constraint", input{ + version: "2.0.0", + component: "compA", + constraints: []string{"== 1.0.0"}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("matched semantic version with complex constraint", input{ + version: "2.0.0", + component: "", + constraints: []string{"> 1.0.0, < 3.0.0"}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("matched semantic version with component complex constraint", input{ + version: "compA=2.0.0", + component: "compA", + constraints: []string{"> 1.0.0, < 3.0.0"}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("matched mixed semantic version with constraints", input{ + version: "1.2.0, compA=2.0.0", + component: "", + constraints: []string{"> 1.0.0, < 3.0.0"}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("matched mixed semantic version with component constraints", input{ + version: "1.0.0, compA=2.0.0", + component: "compA", + constraints: []string{"> 1.0.0, < 3.0.0"}, + expectedErrMsg: "", + shouldPass: true, + }), + Entry("unmatched semantic version with constraint", input{ + version: "0.1.0", + component: "", + constraints: []string{"> 1.0.0, < 3.0.0"}, + expectedErrMsg: "", + shouldPass: false, + }), + Entry("unmatched component semantic version with component constraint", input{ + version: "compA=0.1.0", + component: "compA", + constraints: []string{"> 1.0.0, < 3.0.0"}, + expectedErrMsg: "", + shouldPass: false, + }), ) Describe("MustParseSemVerFilter", func() { It("panics if passed an invalid filter", func() { - Ω(types.MustParseSemVerFilter("1.2.3")([]string{"> 1.0.0"})).Should(BeTrue()) - Ω(types.MustParseSemVerFilter("3.2.0")([]string{"> 1.0.0", "< 2.0.0"})).Should(BeFalse()) + Ω(types.MustParseSemVerFilter("1.2.3")("", []string{"> 1.0.0"})).Should(BeTrue()) + Ω(types.MustParseSemVerFilter("3.2.0")("", []string{"> 1.0.0", "< 2.0.0"})).Should(BeFalse()) Ω(func() { types.MustParseSemVerFilter("a1.2.3") }).Should(Panic()) + Ω(types.MustParseSemVerFilter("compA=1.2.3")("compA", []string{"> 1.0.0"})).Should(BeTrue()) + Ω(types.MustParseSemVerFilter("compA=3.2.0")("compA", []string{"> 1.0.0", "< 2.0.0"})).Should(BeFalse()) + Ω(types.MustParseSemVerFilter("1.0.0")("compA", []string{"> 2.0.0"})).Should(BeTrue()) }) }) }) diff --git a/types/types.go b/types/types.go index 9981a0dd6..240150512 100644 --- a/types/types.go +++ b/types/types.go @@ -38,6 +38,10 @@ type ConstructionNodeReport struct { // all Describe/Context/When containers in this spec's hierarchy ContainerHierarchySemVerConstraints [][]string + // ContainerHierarchyComponentSemVerConstraints is a slice containing the component-specific semVerConstraints of + // all Describe/Context/When containers in this spec's hierarchy + ContainerHierarchyComponentSemVerConstraints []map[string][]string + // IsSerial captures whether the any container has the Serial decorator IsSerial bool @@ -85,6 +89,9 @@ type Report struct { //SuiteSemVerConstraints captures any semVerConstraints attached to the suite by the DSL's RunSpecs() function SuiteSemVerConstraints []string + //SuiteComponentSemVerConstraints captures any component-specific semVerConstraints attached to the suite by the DSL's RunSpecs() function + SuiteComponentSemVerConstraints map[string][]string + //SuiteSucceeded captures the success or failure status of the test run //If true, the test run is considered successful. //If false, the test run is considered unsuccessful @@ -188,14 +195,19 @@ type SpecReport struct { // all Describe/Context/When containers in this spec's hierarchy ContainerHierarchySemVerConstraints [][]string + // ContainerHierarchyComponentSemVerConstraints is a slice containing the component-specific semVerConstraints of + // all Describe/Context/When containers in this spec's hierarchy + ContainerHierarchyComponentSemVerConstraints []map[string][]string + // LeafNodeType, LeafNodeLocation, LeafNodeLabels, LeafNodeSemVerConstraints and LeafNodeText capture the NodeType, CodeLocation, and text // of the Ginkgo node being tested (typically an NodeTypeIt node, though this can also be // one of the NodeTypesForSuiteLevelNodes node types) - LeafNodeType NodeType - LeafNodeLocation CodeLocation - LeafNodeLabels []string - LeafNodeSemVerConstraints []string - LeafNodeText string + LeafNodeType NodeType + LeafNodeLocation CodeLocation + LeafNodeLabels []string + LeafNodeSemVerConstraints []string + LeafNodeComponentSemVerConstraints map[string][]string + LeafNodeText string // Captures the Spec Priority SpecPriority int @@ -261,52 +273,54 @@ type SpecReport struct { func (report SpecReport) MarshalJSON() ([]byte, error) { //All this to avoid emitting an empty Failure struct in the JSON out := struct { - ContainerHierarchyTexts []string - ContainerHierarchyLocations []CodeLocation - ContainerHierarchyLabels [][]string - ContainerHierarchySemVerConstraints [][]string - LeafNodeType NodeType - LeafNodeLocation CodeLocation - LeafNodeLabels []string - LeafNodeSemVerConstraints []string - LeafNodeText string - State SpecState - StartTime time.Time - EndTime time.Time - RunTime time.Duration - ParallelProcess int - Failure *Failure `json:",omitempty"` - NumAttempts int - MaxFlakeAttempts int - MaxMustPassRepeatedly int - CapturedGinkgoWriterOutput string `json:",omitempty"` - CapturedStdOutErr string `json:",omitempty"` - ReportEntries ReportEntries `json:",omitempty"` - ProgressReports []ProgressReport `json:",omitempty"` - AdditionalFailures []AdditionalFailure `json:",omitempty"` - SpecEvents SpecEvents `json:",omitempty"` + ContainerHierarchyTexts []string + ContainerHierarchyLocations []CodeLocation + ContainerHierarchyLabels [][]string + ContainerHierarchySemVerConstraints [][]string + ContainerHierarchyComponentSemVerConstraints []map[string][]string + LeafNodeType NodeType + LeafNodeLocation CodeLocation + LeafNodeLabels []string + LeafNodeSemVerConstraints []string + LeafNodeText string + State SpecState + StartTime time.Time + EndTime time.Time + RunTime time.Duration + ParallelProcess int + Failure *Failure `json:",omitempty"` + NumAttempts int + MaxFlakeAttempts int + MaxMustPassRepeatedly int + CapturedGinkgoWriterOutput string `json:",omitempty"` + CapturedStdOutErr string `json:",omitempty"` + ReportEntries ReportEntries `json:",omitempty"` + ProgressReports []ProgressReport `json:",omitempty"` + AdditionalFailures []AdditionalFailure `json:",omitempty"` + SpecEvents SpecEvents `json:",omitempty"` }{ - ContainerHierarchyTexts: report.ContainerHierarchyTexts, - ContainerHierarchyLocations: report.ContainerHierarchyLocations, - ContainerHierarchyLabels: report.ContainerHierarchyLabels, - ContainerHierarchySemVerConstraints: report.ContainerHierarchySemVerConstraints, - LeafNodeType: report.LeafNodeType, - LeafNodeLocation: report.LeafNodeLocation, - LeafNodeLabels: report.LeafNodeLabels, - LeafNodeSemVerConstraints: report.LeafNodeSemVerConstraints, - LeafNodeText: report.LeafNodeText, - State: report.State, - StartTime: report.StartTime, - EndTime: report.EndTime, - RunTime: report.RunTime, - ParallelProcess: report.ParallelProcess, - Failure: nil, - ReportEntries: nil, - NumAttempts: report.NumAttempts, - MaxFlakeAttempts: report.MaxFlakeAttempts, - MaxMustPassRepeatedly: report.MaxMustPassRepeatedly, - CapturedGinkgoWriterOutput: report.CapturedGinkgoWriterOutput, - CapturedStdOutErr: report.CapturedStdOutErr, + ContainerHierarchyTexts: report.ContainerHierarchyTexts, + ContainerHierarchyLocations: report.ContainerHierarchyLocations, + ContainerHierarchyLabels: report.ContainerHierarchyLabels, + ContainerHierarchySemVerConstraints: report.ContainerHierarchySemVerConstraints, + ContainerHierarchyComponentSemVerConstraints: report.ContainerHierarchyComponentSemVerConstraints, + LeafNodeType: report.LeafNodeType, + LeafNodeLocation: report.LeafNodeLocation, + LeafNodeLabels: report.LeafNodeLabels, + LeafNodeSemVerConstraints: report.LeafNodeSemVerConstraints, + LeafNodeText: report.LeafNodeText, + State: report.State, + StartTime: report.StartTime, + EndTime: report.EndTime, + RunTime: report.RunTime, + ParallelProcess: report.ParallelProcess, + Failure: nil, + ReportEntries: nil, + NumAttempts: report.NumAttempts, + MaxFlakeAttempts: report.MaxFlakeAttempts, + MaxMustPassRepeatedly: report.MaxMustPassRepeatedly, + CapturedGinkgoWriterOutput: report.CapturedGinkgoWriterOutput, + CapturedStdOutErr: report.CapturedStdOutErr, } if !report.Failure.IsZero() { @@ -404,6 +418,34 @@ func (report SpecReport) SemVerConstraints() []string { return out } +// ComponentSemVerConstraints returns a deduped map of all the spec's component-specific SemVerConstraints. +func (report SpecReport) ComponentSemVerConstraints() map[string][]string { + out := map[string][]string{} + seen := map[string]bool{} + for _, compSemVerConstraints := range report.ContainerHierarchyComponentSemVerConstraints { + for component := range compSemVerConstraints { + if !seen[component] { + seen[component] = true + out[component] = compSemVerConstraints[component] + } else { + out[component] = append(out[component], compSemVerConstraints[component]...) + out[component] = slices.Compact(out[component]) + } + } + } + for component := range report.LeafNodeComponentSemVerConstraints { + if !seen[component] { + seen[component] = true + out[component] = report.LeafNodeComponentSemVerConstraints[component] + } else { + out[component] = append(out[component], report.LeafNodeComponentSemVerConstraints[component]...) + out[component] = slices.Compact(out[component]) + } + } + + return out +} + // MatchesLabelFilter returns true if the spec satisfies the passed in label filter query func (report SpecReport) MatchesLabelFilter(query string) (bool, error) { filter, err := ParseLabelFilter(query) @@ -419,7 +461,22 @@ func (report SpecReport) MatchesSemVerFilter(version string) (bool, error) { if err != nil { return false, err } - return filter(report.SemVerConstraints()), nil + + semVerConstraints := report.SemVerConstraints() + if len(semVerConstraints) != 0 && filter("", report.SemVerConstraints()) == false { + return false, nil + } + + componentSemVerConstraints := report.ComponentSemVerConstraints() + if len(componentSemVerConstraints) != 0 { + for component, constraints := range componentSemVerConstraints { + if filter(component, constraints) == false { + return false, nil + } + } + } + + return true, nil } // FileName() returns the name of the file containing the spec diff --git a/types/types_test.go b/types/types_test.go index 5028ab96a..f5aeb69f8 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -234,12 +234,21 @@ var _ = Describe("Types", func() { }) }) - Describe("", SemVerConstraint(">= 1.0.0", "< 2.0.0"), func() { + Describe("SemVerConstraints", SemVerConstraint(">= 1.0.0", "< 2.0.0"), func() { It("returns a concatenated, deduped, set of SemVerConstraints", SemVerConstraint(">= 1.0.0", "< 2.2.0"), func() { Ω(CurrentSpecReport().SemVerConstraints()).Should(Equal([]string{">= 1.0.0", "< 2.0.0", "< 2.2.0"})) }) }) + Describe("ComponentSemVerConstraints", ComponentSemVerConstraint("compA", ">= 1.0.0"), ComponentSemVerConstraint("compB", "< 3.0.0"), func() { + It("returns a map of component names to their concatenated, deduped, set of SemVerConstraints", ComponentSemVerConstraint("compA", "< 2.0.0"), func() { + Ω(CurrentSpecReport().ComponentSemVerConstraints()).Should(Equal(map[string][]string{ + "compA": {">= 1.0.0", "< 2.0.0"}, + "compB": {"< 3.0.0"}, + })) + }) + }) + Describe("MatchesSemVerFilter", SemVerConstraint(">= 1.0.0"), func() { It("returns an error when passed an invalid filter version", func() { matches, err := CurrentSpecReport().MatchesSemVerFilter("aaa") @@ -251,6 +260,15 @@ var _ = Describe("Types", func() { Ω(CurrentSpecReport().MatchesSemVerFilter("1.1.0")).Should(BeTrue()) Ω(CurrentSpecReport().MatchesSemVerFilter("2.0.0")).Should(BeFalse()) }) + + It("returns whether or not the version matches with the mix of SemVerConstraint and ComponentSemVerConstraint", ComponentSemVerConstraint("compA", ">= 2.0.0"), func() { + Ω(CurrentSpecReport().MatchesSemVerFilter("1.1.0")).Should(BeTrue()) + Ω(CurrentSpecReport().MatchesSemVerFilter("compA=2.2.2")).Should(BeTrue()) + Ω(CurrentSpecReport().MatchesSemVerFilter("1.1.0, compA=2.2.2")).Should(BeTrue()) + Ω(CurrentSpecReport().MatchesSemVerFilter("0.1.0, compA=2.2.2")).Should(BeFalse()) + Ω(CurrentSpecReport().MatchesSemVerFilter("1.1.0, compA=1.9.0")).Should(BeFalse()) + Ω(CurrentSpecReport().MatchesSemVerFilter("0.1.0, compA=1.9.0")).Should(BeFalse()) + }) }) It("can report on whether state is a failed state", func() {