Skip to content

Commit cd231fd

Browse files
committed
Label sets allow for more expressive label filtering
1 parent eb27ca8 commit cd231fd

File tree

8 files changed

+391
-14
lines changed

8 files changed

+391
-14
lines changed

docs/index.md

+56-2
Original file line numberDiff line numberDiff line change
@@ -2590,8 +2590,8 @@ The real power, of labels, however, is around filtering. You can filter by labe
25902590
- The `!` unary operator representing the NOT operation.
25912591
- The `,` binary operator equivalent to `||`.
25922592
- The `()` for grouping expressions.
2593-
- All other characters will match as label literals. Label matches are **case insensitive** and trailing and leading whitespace is trimmed.
25942593
- Regular expressions can be provided using `/REGEXP/` notation.
2594+
- All other characters will match as label literals. Label matches are **case insensitive** and trailing and leading whitespace is trimmed.
25952595

25962596
To build on our example above, here are some label filter queries and their behavior:
25972597

@@ -2602,10 +2602,63 @@ To build on our example above, here are some label filter queries and their beha
26022602
| `ginkgo --label-filter="network && !slow"` | Run specs labelled `network` that aren't `slow` |
26032603
| `ginkgo --label-filter=/library/` | Run specs with labels matching the regular expression `library` - this will match the three library-related specs in our example.
26042604

2605+
##### Label Sets
2606+
2607+
In addition to flat strings, Labels can also construct sets. If a label has the format `KEY:VALUE` then a set with key `KEY` is created and the value `VALUE` is added to the set. For example:
2608+
2609+
```go
2610+
Describe("The Library API", Label("API:Library"), func() {
2611+
It("can fetch a list of books", func() {
2612+
// has the labels [API:Library]
2613+
// API is a set with value {Library}
2614+
})
2615+
It("can fetch a list of books by shelf", Label("API:Shelf", "Readiness:Alpha"), func() {
2616+
// has the labels [API:Library, API:Shelf, Readiness:Alpha]
2617+
// API is a set with value {Library, Shelf}
2618+
// Readiness is a set with value {Alpha}
2619+
2620+
})
2621+
It("can fetch a list of books by zip code", Label("API:Geo", "Readiness:Beta"), func() {
2622+
// has the labels [API:Library, API:Geo, Readiness:Beta]
2623+
// API is a set with value {Library, Geo}
2624+
// Readiness is a set with value {Beta}
2625+
})
2626+
})
2627+
```
2628+
2629+
Label filters can operate on sets using the notation: `KEY: SET_OPERATION <ARGUMENT>`. The following set operations are supported:
2630+
2631+
| Set Operation | Argument | Description |
2632+
| --- | --- | --- |
2633+
| `isEmpty` | None | Matches if the set with key `KEY` is empty (i.e. no label of the form `KEY:*` exists) |
2634+
| `containsAny` | `SINGLE_VALUE` or `{VALUE1, VALUE2, ...}` | Matches if the `KEY` set contains _any_ of the elements in `ARGUMENT` |
2635+
| `containsAll` | `SINGLE_VALUE` or `{VALUE1, VALUE2, ...}` | Matches if the `KEY` set contains _all_ of the elements in `ARGUMENT` |
2636+
| `consistsOf` | `SINGLE_VALUE` or `{VALUE1, VALUE2, ...}` | Matches if the `KEY` set contains _exactly_ the elements in `ARGUMENT` |
2637+
| `isSubsetOf` | `SINGLE_VALUE` or `{VALUE1, VALUE2, ...}` | Matches if the elements in the `KEY` set are a subset of the elements in `ARGUMENT` |
2638+
2639+
leading and trailing whitespace is alwasy trimmed around keys and values and comparisons are always case-insensitive. Keys and values in the filter-language set operations are always literals; regular expressions are not supported. A special note should be made about the behavior of `isSubsetOf`: if the `KEY` set is empty then the filter will always match. This is because an empty set is always a subset of any other set.
2640+
2641+
You can combine set operations with other label filters using the logical operators. For example: `ginkgo --label-filter="integration && !slow && Readiness: isSubsetOf {Beta, RC}"` will run all tests that have the label `integration`, do not have the label `slow` and have a `Readiness` set that is a subset of `{Beta, RC}`. This would exclude `Readiness:Alpha` but include specs with `Readiness:Beta` and `Readiness:RC` as well as specs with no `Readiness:*` label.
2642+
2643+
Some more examples:
2644+
2645+
| Query | Behavior |
2646+
| --- | --- |
2647+
| `ginkgo --label-filter="API: consistsOf {Library, Geo}"` | Match any specs for which the `API` set contains exactly `Library` and `Geo` |
2648+
| `ginkgo --label-filter="API: containsAny Library"` | Match any specs for which the `API` set contains either `Library` |
2649+
| `ginkgo --label-filter="Readiness: isEmpty"` | Match any specs for which the `Readiness` set is empty |
2650+
| `ginkgo --label-filter="Readiness: isSubsetOf Beta && !(API: containsAny Geo)"` | Match any specs for which the `Readiness` set is a subset of `{Beta}` (or empty) and the `API` set does not contain `Geo` |
2651+
2652+
Label sets are helpful for organizing and filtering large spec suites in which different specs satisfy multiple overlapping concerns. The use of label set filters is intended to be a more powerful and expressive alterantive to the use of regular expressions. If you find yourself using a regular expression, consider if you should be using a label set instead.
2653+
2654+
##### Listing Labels
2655+
26052656
You can list the labels used in a given package using the `ginkgo labels` subcommand. This does a simple/naive scan of your test files for calls to `Label` and returns any labels it finds.
26062657

26072658
You can iterate on different filters quickly with `ginkgo --dry-run -v --label-filter=FILTER`. This will cause Ginkgo to tell you which specs it will run for a given filter without actually running anything.
26082659

2660+
##### Runtime Label Evaluation
2661+
26092662
If you want to have finer-grained control within a test about what code to run/not-run depending on what labels match/don't match the filter you can perform a manual check against the label-filter passed into Ginkgo like so:
26102663

26112664
```go
@@ -2620,6 +2673,8 @@ It("can save books remotely", Label("network", "slow", "library query") {
26202673

26212674
here `GinkgoLabelFilter()` returns the configured label filter passed in via `--label-filter`. With a setup like this you could run `ginkgo --label-filter="network && !performance"` - this would select the `"can save books remotely"` spec but not run the benchmarking code in the spec. Of course, this could also have been modeled as a separate spec with the `performance` label.
26222675

2676+
##### Suite-Level Labels
2677+
26232678
Finally, in addition to specifying Labels on subject and container nodes you can also specify suite-wide labels by decorating the `RunSpecs` command with `Label`:
26242679

26252680
```go
@@ -2631,7 +2686,6 @@ func TestBooks(t *testing.T) {
26312686

26322687
Suite-level labels apply to the entire suite making it easy to filter out entire suites using label filters.
26332688

2634-
26352689
#### Location-Based Filtering
26362690

26372691
Ginkgo allows you to filter specs based on their source code location from the command line. You do this using the `ginkgo --focus-file` and `ginkgo --skip-file` flags. Ginkgo will only run specs that are in files that _do_ match the `--focus-file` filter *and* _don't_ match the `--skip-file` filter. You can provide multiple `--focus-file` and `--skip-file` flags. The `--focus-file`s will be ORed together and the `--skip-file`s will be ORed together.

integration/_fixtures/filter_fixture/widget_b_test.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@ var _ = Describe("WidgetB", func() {
1313

1414
})
1515

16+
It("fish", Label("Feature:Alpha"), func() {
17+
18+
})
19+
1620
It("cat fish", func() {
1721

1822
})
1923

20-
It("dog fish", func() {
24+
It("dog fish", Label("Feature:Beta"), func() {
2125

2226
})
2327
})

integration/filter_test.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ var _ = Describe("Filter", func() {
2121
"--focus-file=sprocket", "--focus-file=widget:1-24", "--focus-file=_b:24-42",
2222
"--skip-file=_c",
2323
"--json-report=report.json",
24-
"--label-filter=TopLevelLabel && !SLOW",
24+
"--label-filter=TopLevelLabel && !SLOW && !(Feature: containsAny Alpha)",
2525
)
2626
Eventually(session).Should(gexec.Exit(0))
2727
specs := Reports(fm.LoadJSONReports("filter", "report.json")[0].SpecReports)
@@ -43,6 +43,8 @@ var _ = Describe("Filter", func() {
4343
"SprocketA cat", "SprocketB cat", "WidgetA cat", "WidgetB cat", "More WidgetB cat",
4444
// fish is in -focus but cat is in -skip
4545
"SprocketA cat fish", "SprocketB cat fish", "WidgetA cat fish", "WidgetB cat fish", "More WidgetB cat fish",
46+
// Tests with Feature:Alpha
47+
"WidgetB fish",
4648
// Tests labelled 'slow'
4749
"WidgetB dog",
4850
"SprocketB fish",
@@ -95,7 +97,7 @@ var _ = Describe("Filter", func() {
9597
It("can list labels", func() {
9698
session := startGinkgo(fm.TmpDir, "labels", "-r")
9799
Eventually(session).Should(gexec.Exit(0))
98-
Ω(session).Should(gbytes.Say(`filter: \["TopLevelLabel", "slow"\]`))
100+
Ω(session).Should(gbytes.Say(`filter: \["Feature:Alpha", "Feature:Beta", "TopLevelLabel", "slow"\]`))
99101
Ω(session).Should(gbytes.Say(`labels: \["beluga", "bird", "cat", "chicken", "cow", "dog", "giraffe", "koala", "monkey", "otter", "owl", "panda"\]`))
100102
Ω(session).Should(gbytes.Say(`nolabels: No labels found`))
101103
Ω(session).Should(gbytes.Say(`onepkg: \["beluga", "bird", "cat", "chicken", "cow", "dog", "giraffe", "koala", "monkey", "otter", "owl", "panda"\]`))

internal/focus_test.go

+21
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,27 @@ var _ = Describe("Focus", func() {
254254
})
255255
})
256256

257+
Context("when configured with a label set filter", func() {
258+
BeforeEach(func() {
259+
conf.LabelFilter = "Feature: consistsOf {A, B} || Feature: containsAny C"
260+
specs = Specs{
261+
S(N(ntCon, Label("Feature:A", "dog")), N(ntIt, "A", Label("fish"))), //skip because fish no feature:B
262+
S(N(ntCon, Label("Feature:A", "dog")), N(ntIt, "B", Label("apple", "Feature:B"))), //include because has Feature:A and Feature:B
263+
S(N(ntCon, Label("Feature:A")), N(ntIt, "C", Label("Feature:B", "Feature:D"))), //skip because it has Feature:D
264+
S(N(ntCon, Label("Feature:C")), N(ntIt, "D", Label("fish", "Feature:D"))), //include because it has Feature:C
265+
S(N(ntCon, Label("cow")), N(ntIt, "E")), //skip because no Feature:
266+
S(N(ntCon, Label("Feature:A", "Feature:B")), N(ntIt, "F", Pending)), //skip because pending
267+
}
268+
})
269+
270+
It("applies the label filters", func() {
271+
specs, hasProgrammaticFocus := internal.ApplyFocusToSpecs(specs, description, suiteLabels, conf)
272+
Ω(harvestSkips(specs)).Should(Equal([]bool{true, false, true, false, true, true}))
273+
Ω(hasProgrammaticFocus).Should(BeFalse())
274+
275+
})
276+
})
277+
257278
Context("when configured with a label filter that filters on the suite level label", func() {
258279
BeforeEach(func() {
259280
conf.LabelFilter = "cat && TopLevelLabel"

internal/internal_integration/labels_test.go

+25-5
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,18 @@ var _ = Describe("Labels", func() {
2727
It("H", rt.T("H"), Label("fish", "chicken"))
2828
})
2929
})
30+
Describe("feature container", Label("Feature:Beta"), func() {
31+
It("I", rt.T("I"), Label("Feature: Gamma"))
32+
Describe("inner container", Label(" feature : alpha "), func() {
33+
It("J", rt.T("J"), Label("Feature:Alpha"))
34+
It("K", rt.T("K"), Label("Feature:Delta", "Feature:Beta"))
35+
})
36+
37+
})
3038
})
3139
}
3240
BeforeEach(func() {
33-
conf.LabelFilter = "TopLevelLabel && (dog || cow)"
41+
conf.LabelFilter = "TopLevelLabel && (dog || cow) || Feature: containsAny Alpha"
3442
success, hPF := RunFixture("labelled tests", fixture)
3543
Ω(success).Should(BeTrue())
3644
Ω(hPF).Should(BeFalse())
@@ -68,6 +76,18 @@ var _ = Describe("Labels", func() {
6876
Ω(reporter.Did.Find("H").ContainerHierarchyLabels).Should(Equal([][]string{{}, {"giraffe"}, {"cow"}}))
6977
Ω(reporter.Did.Find("H").LeafNodeLabels).Should(Equal([]string{"fish", "chicken"}))
7078
Ω(reporter.Did.Find("H").Labels()).Should(Equal([]string{"giraffe", "cow", "fish", "chicken"}))
79+
80+
Ω(reporter.Did.Find("I").ContainerHierarchyLabels).Should(Equal([][]string{{}, {"Feature:Beta"}}))
81+
Ω(reporter.Did.Find("I").LeafNodeLabels).Should(Equal([]string{"Feature: Gamma"}))
82+
Ω(reporter.Did.Find("I").Labels()).Should(Equal([]string{"Feature:Beta", "Feature: Gamma"}))
83+
84+
Ω(reporter.Did.Find("J").ContainerHierarchyLabels).Should(Equal([][]string{{}, {"Feature:Beta"}, {"feature : alpha"}}))
85+
Ω(reporter.Did.Find("J").LeafNodeLabels).Should(Equal([]string{"Feature:Alpha"}))
86+
Ω(reporter.Did.Find("J").Labels()).Should(Equal([]string{"Feature:Beta", "feature : alpha", "Feature:Alpha"}))
87+
88+
Ω(reporter.Did.Find("K").ContainerHierarchyLabels).Should(Equal([][]string{{}, {"Feature:Beta"}, {"feature : alpha"}}))
89+
Ω(reporter.Did.Find("K").LeafNodeLabels).Should(Equal([]string{"Feature:Delta", "Feature:Beta"}))
90+
Ω(reporter.Did.Find("K").Labels()).Should(Equal([]string{"Feature:Beta", "feature : alpha", "Feature:Delta"}))
7191
})
7292

7393
It("includes suite labels in the suite report", func() {
@@ -76,11 +96,11 @@ var _ = Describe("Labels", func() {
7696
})
7797

7898
It("honors the LabelFilter config and skips tests appropriately", func() {
79-
Ω(rt).Should(HaveTracked("B", "C", "D", "F", "H"))
80-
Ω(reporter.Did.WithState(types.SpecStatePassed).Names()).Should(ConsistOf("B", "C", "D", "F", "H"))
81-
Ω(reporter.Did.WithState(types.SpecStateSkipped).Names()).Should(ConsistOf("A", "E"))
99+
Ω(rt).Should(HaveTracked("B", "C", "D", "F", "H", "J", "K"))
100+
Ω(reporter.Did.WithState(types.SpecStatePassed).Names()).Should(ConsistOf("B", "C", "D", "F", "H", "J", "K"))
101+
Ω(reporter.Did.WithState(types.SpecStateSkipped).Names()).Should(ConsistOf("A", "E", "I"))
82102
Ω(reporter.Did.WithState(types.SpecStatePending).Names()).Should(ConsistOf("G"))
83-
Ω(reporter.End).Should(BeASuiteSummary(true, NPassed(5), NSkipped(2), NPending(1), NSpecs(8), NWillRun(5)))
103+
Ω(reporter.End).Should(BeASuiteSummary(true, NPassed(7), NSkipped(3), NPending(1), NSpecs(11), NWillRun(7)))
84104
})
85105
})
86106

internal/node_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -444,9 +444,9 @@ var _ = Describe("Constructing nodes", func() {
444444
})
445445

446446
It("validates labels", func() {
447-
node, errors := internal.NewNode(dt, ntIt, "", body, cl, Label("A", "B&C", "C,D", "C,D ", " "))
447+
node, errors := internal.NewNode(dt, ntIt, "", body, cl, Label("A", "B&C", "C,D", "C,D ", " ", ":Foo"))
448448
Ω(node).Should(BeZero())
449-
Ω(errors).Should(ConsistOf(types.GinkgoErrors.InvalidLabel("B&C", cl), types.GinkgoErrors.InvalidLabel("C,D", cl), types.GinkgoErrors.InvalidLabel("C,D ", cl), types.GinkgoErrors.InvalidEmptyLabel(cl)))
449+
Ω(errors).Should(ConsistOf(types.GinkgoErrors.InvalidLabel("B&C", cl), types.GinkgoErrors.InvalidLabel("C,D", cl), types.GinkgoErrors.InvalidLabel("C,D ", cl), types.GinkgoErrors.InvalidEmptyLabel(cl), types.GinkgoErrors.InvalidLabel(":Foo", cl)))
450450
Ω(dt.DidTrackDeprecations()).Should(BeFalse())
451451
})
452452
})

0 commit comments

Comments
 (0)