Skip to content

Commit d41f01e

Browse files
committed
AroundNode - preview
1 parent 3d9dae2 commit d41f01e

15 files changed

+443
-27
lines changed

core_dsl.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ func RunSpecs(t GinkgoTestingT, description string, args ...any) bool {
268268
}
269269
defer global.PopClone()
270270

271-
suiteLabels, suiteSemVerConstraints := extractSuiteConfiguration(args)
271+
suiteLabels, suiteSemVerConstraints, suiteAroundNodes := extractSuiteConfiguration(args)
272272

273273
var reporter reporters.Reporter
274274
if suiteConfig.ParallelTotal == 1 {
@@ -311,7 +311,7 @@ func RunSpecs(t GinkgoTestingT, description string, args ...any) bool {
311311
suitePath, err = filepath.Abs(suitePath)
312312
exitIfErr(err)
313313

314-
passed, hasFocusedTests := global.Suite.Run(description, suiteLabels, suiteSemVerConstraints, suitePath, global.Failer, reporter, writer, outputInterceptor, interrupt_handler.NewInterruptHandler(client), client, internal.RegisterForProgressSignal, suiteConfig)
314+
passed, hasFocusedTests := global.Suite.Run(description, suiteLabels, suiteSemVerConstraints, suiteAroundNodes, suitePath, global.Failer, reporter, writer, outputInterceptor, interrupt_handler.NewInterruptHandler(client), client, internal.RegisterForProgressSignal, suiteConfig)
315315
outputInterceptor.Shutdown()
316316

317317
flagSet.ValidateDeprecations(deprecationTracker)
@@ -330,9 +330,10 @@ func RunSpecs(t GinkgoTestingT, description string, args ...any) bool {
330330
return passed
331331
}
332332

333-
func extractSuiteConfiguration(args []any) (Labels, SemVerConstraints) {
333+
func extractSuiteConfiguration(args []any) (Labels, SemVerConstraints, internal.AroundNodes) {
334334
suiteLabels := Labels{}
335335
suiteSemVerConstraints := SemVerConstraints{}
336+
aroundNodes := internal.AroundNodes{}
336337
configErrors := []error{}
337338
for _, arg := range args {
338339
switch arg := arg.(type) {
@@ -344,6 +345,8 @@ func extractSuiteConfiguration(args []any) (Labels, SemVerConstraints) {
344345
suiteLabels = append(suiteLabels, arg...)
345346
case SemVerConstraints:
346347
suiteSemVerConstraints = append(suiteSemVerConstraints, arg...)
348+
case internal.AroundNode:
349+
aroundNodes = append(aroundNodes, arg)
347350
default:
348351
configErrors = append(configErrors, types.GinkgoErrors.UnknownTypePassedToRunSpecs(arg))
349352
}
@@ -359,7 +362,7 @@ func extractSuiteConfiguration(args []any) (Labels, SemVerConstraints) {
359362
os.Exit(1)
360363
}
361364

362-
return suiteLabels, suiteSemVerConstraints
365+
return suiteLabels, suiteSemVerConstraints, aroundNodes
363366
}
364367

365368
func getwd() (string, error) {
@@ -382,7 +385,7 @@ func PreviewSpecs(description string, args ...any) Report {
382385
}
383386
defer global.PopClone()
384387

385-
suiteLabels, suiteSemVerConstraints := extractSuiteConfiguration(args)
388+
suiteLabels, suiteSemVerConstraints, suiteAroundNodes := extractSuiteConfiguration(args)
386389
priorDryRun, priorParallelTotal, priorParallelProcess := suiteConfig.DryRun, suiteConfig.ParallelTotal, suiteConfig.ParallelProcess
387390
suiteConfig.DryRun, suiteConfig.ParallelTotal, suiteConfig.ParallelProcess = true, 1, 1
388391
defer func() {
@@ -400,7 +403,7 @@ func PreviewSpecs(description string, args ...any) Report {
400403
suitePath, err = filepath.Abs(suitePath)
401404
exitIfErr(err)
402405

403-
global.Suite.Run(description, suiteLabels, suiteSemVerConstraints, suitePath, global.Failer, reporter, writer, outputInterceptor, interrupt_handler.NewInterruptHandler(client), client, internal.RegisterForProgressSignal, suiteConfig)
406+
global.Suite.Run(description, suiteLabels, suiteSemVerConstraints, suiteAroundNodes, suitePath, global.Failer, reporter, writer, outputInterceptor, interrupt_handler.NewInterruptHandler(client), client, internal.RegisterForProgressSignal, suiteConfig)
404407

405408
return global.Suite.GetPreviewReport()
406409
}

decorator_dsl.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,15 @@ SuppressProgressReporting is a decorator that allows you to disable progress rep
158158
if you have a `ReportAfterEach` node that is running for every skipped spec and is generating lots of progress reports.
159159
*/
160160
const SuppressProgressReporting = internal.SuppressProgressReporting
161+
162+
/*
163+
AroundNode is a decorator to enable more advanced patterns for setting up and configuring individual nodes.
164+
165+
When possible you should favor using setup nodes (e.g. BeforeEach, AfterEach, JustBeforeEach, JustAfterEach) to set up and configure nodes.
166+
However there are contexts where the AroundNode patterns is more appropriate - or even necessary.
167+
168+
For example, each Ginkgo node runs in its own goroutine. Some linux namespace commands require goroutines to be fixed to a thread. You could use an AroundNode to call runtime.LockOSThread() and perform any additional setup.
169+
170+
WIP
171+
*/
172+
var AroundNode = internal.BuildAroundNode

docs/index.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,16 @@ Describe("book", func() {
15451545
We're now accessing the `shelf` variable in the spec closure during the Run Phase and can trust that it has been correctly instantiated by the setup node closure.
15461546

15471547
Be sure to check out the [Table Patterns](#table-specs-patterns) section of the [Ginkgo and Gomega Patterns](#ginkgo-and-gomega-patterns) chapter to learn about a few more table-based patterns.
1548+
<!--
1549+
### Advanced: Around Node
1550+
1551+
When possible you should favor using setup nodes (e.g. `BeforeEach` etc.) and `DeferCleanup` to set up and tear down specs. However, there are certain contexts where a different approach is required. In particular:
1552+
1553+
1. Ginkgo runs each node in its own goroutine. This is necessary to manage the separate timeouts for each node. However some libraries and programming models (e.g. linux namespaces) require that goroutines be locked to a thread and that some thread-specific setup be performed when the goroutine is launched. Such configuration cannot go in a top-level `BeforeEach` because the `BeforeEach` goroutine will end before subsequent node goroutines run. You could use an `AroundNode` here.
1554+
1555+
2. Some libraries pass configuration through `context.Context` objects. While you can generate your own `context.Context` in a `BeforeEach` and pass it around this context won't inherit the cancellation of Ginkgo's `SpecContext` in the event of a timeout or interrupt. Moreover, each node gets a fresh `SpecContext` - so a context that inherits from the `SpecContext` passed into a `BeforeEach` will be invalidated by the time the `It` runs. If you want to configure a context that also inherits from Ginkgo's `SpecContext` you could use an `AroundNode` instead.
1556+
1557+
The `AroundNode` decorator takes a function with signature `func(ctx context.Context, callback func(context.Context))`. -->
15481558

15491559
#### Generating Entry Descriptions
15501560
In the examples we've shown so far, we are explicitly passing in a description for each table entry. Recall that this description is used to generate the description of the resulting spec's Subject node. That means it's important as it conveys the intent of the spec and is printed out in case the spec fails.

dsl/decorators/decorators_dsl.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ const SuppressProgressReporting = ginkgo.SuppressProgressReporting
3737

3838
var Label = ginkgo.Label
3939
var SemVerConstraint = ginkgo.SemVerConstraint
40+
var AroundNode = ginkgo.AroundNode

internal/around_node.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package internal
2+
3+
import (
4+
"github.com/onsi/ginkgo/v2/types"
5+
)
6+
7+
func ComputeAroundNodes(specs Specs) Specs {
8+
out := Specs{}
9+
for _, spec := range specs {
10+
nodes := Nodes{}
11+
currentNestingLevel := 0
12+
aroundNodes := []AroundNode{}
13+
nestingLevelIndices := []int{}
14+
for _, node := range spec.Nodes {
15+
switch node.NodeType {
16+
case types.NodeTypeContainer:
17+
currentNestingLevel = node.NestingLevel + 1
18+
nestingLevelIndices = append(nestingLevelIndices, len(aroundNodes))
19+
aroundNodes = append(aroundNodes, node.AroundNodes...)
20+
nodes = append(nodes, node)
21+
default:
22+
if currentNestingLevel > node.NestingLevel {
23+
currentNestingLevel = node.NestingLevel
24+
aroundNodes = aroundNodes[:nestingLevelIndices[currentNestingLevel]]
25+
}
26+
nodeAroundNodes := []AroundNode{}
27+
nodeAroundNodes = append(nodeAroundNodes, aroundNodes...)
28+
nodeAroundNodes = append(nodeAroundNodes, node.AroundNodes...)
29+
node.AroundNodes = nodeAroundNodes
30+
nodes = append(nodes, node)
31+
}
32+
}
33+
spec.Nodes = nodes
34+
out = append(out, spec)
35+
}
36+
return out
37+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package internal_integration_test
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
"github.com/onsi/ginkgo/v2/internal"
9+
. "github.com/onsi/ginkgo/v2/internal/test_helpers"
10+
. "github.com/onsi/gomega"
11+
)
12+
13+
func AN(run string) internal.AroundNode {
14+
return AroundNode(func(ctx context.Context, body func(ctx context.Context)) {
15+
rt.Run(run)
16+
body(ctx)
17+
})
18+
}
19+
20+
var _ = Describe("The AroundNode decorator", func() {
21+
Context("when applied to individual nodes", func() {
22+
It("is scoped to run just around that node", func() {
23+
success, hPF := RunFixture("around node test", func() {
24+
BeforeSuite(rt.T("before-suite"), AN("before-suite-around"))
25+
Describe("container", func() {
26+
BeforeEach(rt.T("before-each"), AN("before-each-around"))
27+
It("runs", rt.T("it"), AN("it-around"), AroundNode(func(ctx context.Context, body func(ctx context.Context)) {
28+
rt.Run("it-around-2-before")
29+
body(ctx)
30+
rt.Run("it-around-2-after")
31+
}))
32+
})
33+
})
34+
35+
Ω(success).Should(BeTrue())
36+
Ω(rt).Should(HaveTracked(
37+
"before-suite-around", "before-suite",
38+
"before-each-around", "before-each",
39+
"it-around", "it-around-2-before", "it", "it-around-2-after",
40+
))
41+
Ω(hPF).Should(BeFalse())
42+
})
43+
})
44+
45+
Context("when DeferCleanup is called", func() {
46+
It("attaches the caller's AroundNode", func() {
47+
success, _ := RunFixture("around node test with defer cleanup", func() {
48+
It("A", AN("A-around"), func() {
49+
rt.Run("A")
50+
DeferCleanup(AN("DC-around"), func() {
51+
rt.Run("DC")
52+
})
53+
})
54+
})
55+
Ω(success).Should(BeTrue())
56+
Ω(rt).Should(HaveTracked(
57+
"A-around", "A",
58+
"A-around", "DC-around", "DC",
59+
))
60+
})
61+
})
62+
63+
Context("when included in a hierarchy", func() {
64+
It("correctly tracks nodes in the hierarchy", func() {
65+
success, _ := RunFixture("around node test with hierarchy", func() {
66+
Describe("outer", AN("outer-around"), AN("outer-around-2"), func() {
67+
It("A", AN("A-around"), rt.T("A"))
68+
Describe("inner", AN("inner-around"), func() {
69+
It("B", AN("B-around"), rt.T("B", func() {
70+
DeferCleanup(AN("DC-around"), rt.T("DC"))
71+
}))
72+
})
73+
BeforeEach(AN("before-each-around"), rt.T("before-each"))
74+
})
75+
AfterEach(AN("after-each-around"), rt.T("after-each"))
76+
})
77+
78+
Ω(success).Should(BeTrue())
79+
Ω(rt).Should(HaveTracked(
80+
"outer-around", "outer-around-2", "before-each-around", "before-each",
81+
"outer-around", "outer-around-2", "A-around", "A",
82+
"after-each-around", "after-each",
83+
"outer-around", "outer-around-2", "before-each-around", "before-each",
84+
"outer-around", "outer-around-2", "inner-around", "B-around", "B",
85+
"after-each-around", "after-each",
86+
"outer-around", "outer-around-2", "inner-around", "B-around", "DC-around", "DC",
87+
))
88+
})
89+
})
90+
91+
Context("when applied to RunSpecs", func() {
92+
It("runs for all nodes, including the suite-level nodes", func() {
93+
success, hPF := RunFixture("around node test with suite-level around", func() {
94+
BeforeSuite(rt.T("before-suite"), AN("before-suite-around"))
95+
Describe("container", AN("container-around"), func() {
96+
BeforeEach(rt.T("before-each"), AN("before-each-around"))
97+
It("runs", rt.T("it"), AN("it-around"), AroundNode(func(ctx context.Context, body func(ctx context.Context)) {
98+
rt.Run("it-around-2-before")
99+
body(ctx)
100+
rt.Run("it-around-2-after")
101+
}))
102+
})
103+
}, AN("suite-around-1"), AN("suite-around-2"))
104+
105+
Ω(success).Should(BeTrue())
106+
Ω(rt).Should(HaveTracked(
107+
"suite-around-1", "suite-around-2", "before-suite-around", "before-suite",
108+
"suite-around-1", "suite-around-2", "container-around", "before-each-around", "before-each",
109+
"suite-around-1", "suite-around-2", "container-around", "it-around", "it-around-2-before", "it", "it-around-2-after",
110+
))
111+
Ω(hPF).Should(BeFalse())
112+
})
113+
})
114+
115+
Context("when it modifies the context", func() {
116+
var newCtx context.Context
117+
It("provides the node with a wrapped version of the context that can, nonetheless, be accessed and unwrapped", AroundNode(func(ctx context.Context, body func(ctx context.Context)) {
118+
newCtx = context.WithValue(ctx, "wrapped", "value")
119+
body(newCtx)
120+
}), func(ctx SpecContext) {
121+
Ω(ctx.Value("wrapped")).Should(Equal("value"))
122+
Ω(ctx).ShouldNot(Equal(newCtx))
123+
Ω(ctx.WrappedContext()).Should(Equal(newCtx))
124+
})
125+
126+
It("works with the more complex suite setup nodes too", func() {
127+
succes, _ := RunFixture("around node test with complex context", func() {
128+
SynchronizedBeforeSuite(func(ctx SpecContext) []byte {
129+
rt.Run("SBS-primary")
130+
Ω(ctx.Value("wrapped")).Should(Equal("value"))
131+
return []byte("data")
132+
}, func(ctx SpecContext, data []byte) {
133+
rt.Run("SBS-all")
134+
Ω(ctx.Value("wrapped")).Should(Equal("value"))
135+
Ω(data).Should(Equal([]byte("data")))
136+
}, AroundNode(func(ctx context.Context, body func(ctx context.Context)) {
137+
rt.Run("SBS-around")
138+
newCtx = context.WithValue(ctx, "wrapped", "value")
139+
body(newCtx)
140+
}))
141+
It("runs", rt.T("A"))
142+
}, AN("suite-around-1"))
143+
Ω(succes).Should(BeTrue())
144+
Ω(rt).Should(HaveTracked(
145+
"suite-around-1", "SBS-around", "SBS-primary",
146+
"suite-around-1", "SBS-around", "SBS-all",
147+
"suite-around-1", "A",
148+
))
149+
Ω(reporter.Did).Should(HaveEach(HavePassed()))
150+
})
151+
152+
It("should still allow timeouts", func(ctx SpecContext) {
153+
c := make(chan struct{})
154+
success, _ := RunFixture("around node test with timeout", func() {
155+
It("A",
156+
AroundNode(func(ctx context.Context, body func(ctx context.Context)) {
157+
ctx, cancel := context.WithTimeout(ctx, 100*time.Minute)
158+
defer cancel()
159+
ctx = context.WithValue(ctx, "timeout", "value")
160+
body(ctx)
161+
}),
162+
func(ctx SpecContext) {
163+
Ω(ctx.Value("timeout")).Should(Equal("value"))
164+
<-ctx.Done()
165+
close(c)
166+
},
167+
NodeTimeout(100*time.Millisecond))
168+
})
169+
170+
Ω(success).Should(BeFalse())
171+
Ω(reporter.Did.Find("A")).Should(HaveTimedOut())
172+
Eventually(c, ctx).Should(BeClosed())
173+
}, NodeTimeout(time.Second))
174+
175+
})
176+
177+
Context("when the user fails to call the body function", func() {
178+
It("fails", func() {
179+
success, _ := RunFixture("around node test that fails to call the body function", func() {
180+
It("A", AroundNode(func(ctx context.Context, body func(ctx context.Context)) {}), func() {
181+
rt.Run("A")
182+
})
183+
})
184+
Ω(success).Should(BeFalse())
185+
Ω(reporter.Did.Find("A")).Should(HaveFailed("An AroundNode failed to call the passed in function."))
186+
Ω(rt).Should(HaveTrackedNothing())
187+
})
188+
})
189+
190+
Context("when the user passes in a nil context", func() {
191+
It("fails", func() {
192+
success, _ := RunFixture("around node test that passes in a nil context", func() {
193+
It("A", AroundNode(func(ctx context.Context, body func(ctx context.Context)) {
194+
body(nil)
195+
}), func() {
196+
rt.Run("A")
197+
})
198+
})
199+
Ω(success).Should(BeFalse())
200+
Ω(reporter.Did.Find("A")).Should(HaveFailed("An AroundNode failed to pass a valid Ginkgo SpecContext in. You must always pass in a context derived from the context passed to you."))
201+
Ω(rt).Should(HaveTrackedNothing())
202+
})
203+
})
204+
205+
Context("when the user passes in a context that does not inherit from the chain", func() {
206+
It("fails", func() {
207+
success, _ := RunFixture("around node test that passes in a context that does not inherit from the chain", func() {
208+
It("A", AroundNode(func(ctx context.Context, body func(ctx context.Context)) {
209+
body(context.Background())
210+
}), func() {
211+
rt.Run("A")
212+
})
213+
})
214+
Ω(success).Should(BeFalse())
215+
Ω(reporter.Did.Find("A")).Should(HaveFailed("An AroundNode failed to pass a valid Ginkgo SpecContext in. You must always pass in a context derived from the context passed to you."))
216+
Ω(rt).Should(HaveTrackedNothing())
217+
})
218+
})
219+
})

internal/internal_integration/internal_integration_suite_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,13 @@ func SetUpForParallel(parallelTotal int) {
8888
conf.ParallelHost = server.Address()
8989
}
9090

91-
func RunFixture(description string, callback func()) (bool, bool) {
91+
func RunFixture(description string, callback func(), aroundNodes ...internal.AroundNode) (bool, bool) {
9292
suite := internal.NewSuite()
9393
var success, hasProgrammaticFocus bool
9494
WithSuite(suite, func() {
9595
callback()
9696
Ω(suite.BuildTree()).Should(Succeed())
97-
success, hasProgrammaticFocus = suite.Run(description, Label("TopLevelLabel"), SemVerConstraints{}, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, progressSignalRegistrar, conf)
97+
success, hasProgrammaticFocus = suite.Run(description, Label("TopLevelLabel"), SemVerConstraints{}, aroundNodes, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, progressSignalRegistrar, conf)
9898
})
9999
return success, hasProgrammaticFocus
100100
}
@@ -128,7 +128,7 @@ func RunFixtureInParallel(description string, callback func(proc int)) bool {
128128
interruptHandler := interrupt_handler.NewInterruptHandler(client)
129129
defer interruptHandler.Stop()
130130

131-
success, _ := suite.Run(fmt.Sprintf("%s - %d", description, proc), Label("TopLevelLabel"), SemVerConstraints{}, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, noopProgressSignalRegistrar, c)
131+
success, _ := suite.Run(fmt.Sprintf("%s - %d", description, proc), Label("TopLevelLabel"), SemVerConstraints{}, nil, "/path/to/suite", failer, reporter, writer, outputInterceptor, interruptHandler, client, noopProgressSignalRegistrar, c)
132132
close(exit)
133133
finished <- success
134134
}()

0 commit comments

Comments
 (0)