Skip to content

Commit 28801fe

Browse files
committed
AttachProgressReporter allows users to provide arbitrary information when a ProgressReport is requested
1 parent da91bbf commit 28801fe

11 files changed

+247
-62
lines changed

core_dsl.go

+19
Original file line numberDiff line numberDiff line change
@@ -771,3 +771,22 @@ func DeferCleanup(args ...interface{}) {
771771
}
772772
pushNode(internal.NewCleanupNode(deprecationTracker, fail, args...))
773773
}
774+
775+
/*
776+
AttachProgressReporter allows you to register a function that will be called whenever Ginkgo generates a Progress Report. The contents returned by the function will be included in the report.
777+
778+
Progress Reports are generated:
779+
- whenever the user explicitly requests one (via `SIGINFO` or `SIGUSR1`)
780+
- on nodes decorated with PollProgressAfter
781+
- on suites run with --poll-progress-after
782+
- whenever a test times out
783+
784+
Ginkgo uses Progress Reports to convey the current state of the test suite, including any running goroutines. By attaching a progress reporter you are able to supplement these reports with additional information.
785+
786+
# AttachProgressReporter returns a function that can be called to detach the progress reporter
787+
788+
You can learn more about AttachProgressReporter here: https://onsi.github.io/ginkgo/#attaching-additional-information-to-progress-reports
789+
*/
790+
func AttachProgressReporter(reporter func() string) func() {
791+
return global.Suite.AttachProgressReporter(reporter)
792+
}

docs/index.md

+30-2
Original file line numberDiff line numberDiff line change
@@ -2669,18 +2669,46 @@ Stepping back - it bears repeating: you should use `FlakeAttempts` judiciously.
26692669
### Getting Visibility Into Long-Running Specs
26702670
Ginkgo is often used to build large, complex, integration suites and it is a common - if painful - experience for these suites to run slowly. Ginkgo provides numerous mechanisms that enable developers to get visibility into what part of a suite is running and where, precisely, a spec may be lagging or hanging.
26712671

2672-
Ginkgo can provide a **Progress Report** of what is currently running in response to the `SIGINFO` and `SIGUSR1` signals. The Progress Report includes information about which node is currently running and the exact line of code that it is currently executing, along with any relevant goroutines that were launched by the spec. The report also includes the 10 most recent lines written to the `GinkgoWriter`. A developer waiting for a stuck spec can get this information immediately by sending either the `SIGINFO` or `SIGUSR1` signal (on MacOS/BSD systems, `SIGINFO` can be sent via `^T` - making it especially convenient; if you're on linux you'll need to send `SIGUSR1` to the actual test process spanwed by `ginkgo` - not the `ginkgo` cli process itself).
2672+
Ginkgo can provide a **Progress Report** of what is currently running in response to the `SIGINFO` and `SIGUSR1` signals. The Progress Report includes information about which node is currently running and the exact line of code that it is currently executing, along with any relevant goroutines that were launched by the spec. The report also includes the 10 most recent lines written to the `GinkgoWriter`. A developer waiting for a stuck spec can get this information immediately by sending either the `SIGINFO` or `SIGUSR1` signal (on MacOS/BSD systems, `SIGINFO` can be sent via `^T` - making it especially convenient; if you're on linux you'll need to send `SIGUSR1` to the actual test process spawned by `ginkgo` - not the `ginkgo` cli process itself).
26732673

26742674
These Progress Reports can also show you a preview of the running source code, but only if Ginkgo can find your source files. If need be you can tell Ginkgo where to look for source files by specifying `--source-root`.
26752675

2676-
Finally - you can instruct Ginkgo to provide these Progress Reports automatically whenever a node takes too long to complete. You do this by passing the `--poll-progress-after=INTERVAL` flag to specify how long Ginkgo should wait before emitting a progress report. Once this interval is passed Ginkgo can periodically emit Progress Reports - the interval between these reports is controlled via the `--poll-progress-interval=INTERVAL` flag. By default `--poll-progress-after` is set to `0` and so Ginkgo does not emit Progress Reports.
2676+
Finally - you can instruct Ginkgo to provide Progress Reports automatically whenever a node takes too long to complete. You do this by passing the `--poll-progress-after=INTERVAL` flag to specify how long Ginkgo should wait before emitting a progress report. Once this interval is passed Ginkgo can periodically emit Progress Reports - the interval between these reports is controlled via the `--poll-progress-interval=INTERVAL` flag. By default `--poll-progress-after` is set to `0` and so Ginkgo does not emit Progress Reports.
26772677

26782678
You can override the global setting of `poll-progess-after` and `poll-progress-interval` on a per-node basis by using the `PollProgressAfter(INTERVAL)` and `PollProgressInterval(INTERVAL)` decorators. A value of `0` will explicitly turn off Progress Reports for a given node regardless of the global setting.
26792679

26802680
All Progress Reports generated by Ginkgo - whether interactively via `SIGINFO/SIGUSR1` or automatically via the `PollProgressAfter` configuration - also appear in Ginkgo's [machine-readable reports](#generating-machine-readable-reports).
26812681

26822682
In addition to these formal Progress Reports, Ginkgo tracks whenever a node begins and ends. These node `> Enter` and `< Exit` events are usually only logged in the spec's timeline when running with `-vv`, however you can turn them on for other verbosity modes using the `--show-node-events` flag.
26832683

2684+
#### Attaching Additional Information to Progress Reports
2685+
2686+
Ginkgo also allows you to attach Progress Report providers to provide additional information when a progress report is generated. For example, these could query the system under test for diagnostic information about its internal state and report back. You attach these providers via `AttachProgressReporter`. For example:
2687+
2688+
```go
2689+
AttachProgressReporter(func() string {
2690+
libraryState := library.GetStatusReport()
2691+
return fmt.Sprintf("%s: %s", library.ClientID, libraryState.Summary)
2692+
})
2693+
```
2694+
2695+
`AttachProgressReporter` returns a `cancel` func that you can call to unregister the progress reporter. This allow you to do things like:
2696+
2697+
```go
2698+
BeforeEach(func() {
2699+
library = libraryClient.ConnectAs("Jean ValJean")
2700+
2701+
//we attach a progress reporter and can trust that it will be cleaned up after the spec runs
2702+
DeferCleanup(AttachProgressReporter(func() string {
2703+
libraryState := library.GetStatusReport()
2704+
return fmt.Sprintf("%s: %s", library.ClientID, libraryState.Summary)
2705+
}))
2706+
})
2707+
```
2708+
2709+
Note that the functions called by `AttachProgressReporter` must not block. Ginkgo currently has a hard-coded 5 second limit. If all attached progress reporters take longer than 5 seconds to report back, Ginkgo will move on so as to prevent the suite from blocking.
2710+
2711+
26842712
### Spec Timeouts and Interruptible Nodes
26852713

26862714
Sometimes specs get stuck. Perhaps a network call is running slowly; or a newly introduced bug has caused an asynchronous process the test is relying on to hang. It's important, in such cases, to be able to set a deadline for a given spec or node and require the spec/node to complete before the deadline has elapsed.

dsl/core/core_dsl.go

+1
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ var BeforeAll = ginkgo.BeforeAll
6161
var AfterAll = ginkgo.AfterAll
6262
var DeferCleanup = ginkgo.DeferCleanup
6363
var GinkgoT = ginkgo.GinkgoT
64+
var AttachProgressReporter = ginkgo.AttachProgressReporter

integration/_fixtures/progress_report_fixture/progress_report_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import (
99
)
1010

1111
var _ = Describe("ProgressReport", func() {
12+
BeforeEach(func() {
13+
DeferCleanup(AttachProgressReporter(func() string {
14+
return fmt.Sprintf("Some global information: %d", GinkgoParallelProcess())
15+
}))
16+
})
1217
It("can track on demand", func() {
1318
By("Step A")
1419
By("Step B")

integration/progress_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ var _ = Describe("Emitting progress", func() {
4646
//first poll
4747
Eventually(session).Should(gbytes.Say(`--poll-progress-after tracks things that take too long \(Spec Runtime: 1\.5\d*s\)`))
4848
Eventually(session).Should(gbytes.Say(`>\s*time.Sleep\(2 \* time\.Second\)`))
49+
Eventually(session).Should(gbytes.Say(`Begin Additional Progress Reports >>`))
50+
Eventually(session).Should(gbytes.Say(`Some global information: 1`))
51+
Eventually(session).Should(gbytes.Say(`<< End Additional Progress Reports`))
4952

5053
//second poll
5154
Eventually(session).Should(gbytes.Say(`--poll-progress-after tracks things that take too long \(Spec Runtime: 1\.7\d*s\)`))
@@ -88,6 +91,8 @@ var _ = Describe("Emitting progress", func() {
8891

8992
Eventually(session.Out.Contents()).Should(ContainSubstring(`Progress Report for Ginkgo Process #1`))
9093
Eventually(session.Out.Contents()).Should(ContainSubstring(`Progress Report for Ginkgo Process #2`))
94+
Eventually(session.Out.Contents()).Should(ContainSubstring(`Some global information: 1`))
95+
Eventually(session.Out.Contents()).Should(ContainSubstring(`Some global information: 2`))
9196

9297
})
9398

internal/internal_integration/progress_report_test.go

+40-1
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ var _ = Describe("Progress Reporting", func() {
305305
})
306306
})
307307

308-
Context("when a test takes longer then the overriden PollProgressAfter", func() {
308+
Context("when a test takes longer then the overridden PollProgressAfter", func() {
309309
BeforeEach(func() {
310310
success, _ := RunFixture("emitting spec progress", func() {
311311
Describe("a container", func() {
@@ -439,4 +439,43 @@ var _ = Describe("Progress Reporting", func() {
439439
Ω(pr.AdditionalReports).Should(BeEmpty())
440440
})
441441
})
442+
443+
Context("when a global progress report provider has been registered", func() {
444+
BeforeEach(func() {
445+
success, _ := RunFixture("emitting spec progress", func() {
446+
Describe("a container", func() {
447+
It("A", func(ctx SpecContext) {
448+
cancelGlobal := AttachProgressReporter(func() string { return "Some Global Information" })
449+
AttachProgressReporter(func() string { return "Some More (Never Cancelled) Global Information" })
450+
ctx.AttachProgressReporter(func() string { return "Some Additional Information" })
451+
cl = types.NewCodeLocation(0)
452+
triggerProgressSignal()
453+
cancelGlobal()
454+
triggerProgressSignal()
455+
})
456+
457+
It("B", func() {
458+
triggerProgressSignal()
459+
})
460+
})
461+
})
462+
Ω(success).Should(BeTrue())
463+
})
464+
465+
It("includes information from that progress report provider", func() {
466+
Ω(reporter.ProgressReports).Should(HaveLen(3))
467+
pr := reporter.ProgressReports[0]
468+
469+
Ω(pr.LeafNodeText).Should(Equal("A"))
470+
Ω(pr.AdditionalReports).Should(Equal([]string{"Some Additional Information", "Some Global Information", "Some More (Never Cancelled) Global Information"}))
471+
472+
pr = reporter.ProgressReports[1]
473+
Ω(pr.LeafNodeText).Should(Equal("A"))
474+
Ω(pr.AdditionalReports).Should(Equal([]string{"Some Additional Information", "Some More (Never Cancelled) Global Information"}))
475+
476+
pr = reporter.ProgressReports[2]
477+
Ω(pr.LeafNodeText).Should(Equal("B"))
478+
Ω(pr.AdditionalReports).Should(Equal([]string{"Some More (Never Cancelled) Global Information"}))
479+
})
480+
})
442481
})

internal/progress_reporter_manager.go

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package internal
2+
3+
import (
4+
"context"
5+
"sort"
6+
"sync"
7+
)
8+
9+
type ProgressReporterManager struct {
10+
lock *sync.Mutex
11+
progressReporters map[int]func() string
12+
prCounter int
13+
}
14+
15+
func NewProgressReporterManager() *ProgressReporterManager {
16+
return &ProgressReporterManager{
17+
progressReporters: map[int]func() string{},
18+
lock: &sync.Mutex{},
19+
}
20+
}
21+
22+
func (prm *ProgressReporterManager) AttachProgressReporter(reporter func() string) func() {
23+
prm.lock.Lock()
24+
defer prm.lock.Unlock()
25+
prm.prCounter += 1
26+
prCounter := prm.prCounter
27+
prm.progressReporters[prCounter] = reporter
28+
29+
return func() {
30+
prm.lock.Lock()
31+
defer prm.lock.Unlock()
32+
delete(prm.progressReporters, prCounter)
33+
}
34+
}
35+
36+
func (prm *ProgressReporterManager) QueryProgressReporters(ctx context.Context) []string {
37+
prm.lock.Lock()
38+
keys := []int{}
39+
for key := range prm.progressReporters {
40+
keys = append(keys, key)
41+
}
42+
sort.Ints(keys)
43+
reporters := []func() string{}
44+
for _, key := range keys {
45+
reporters = append(reporters, prm.progressReporters[key])
46+
}
47+
prm.lock.Unlock()
48+
49+
if len(reporters) == 0 {
50+
return nil
51+
}
52+
out := []string{}
53+
for _, reporter := range reporters {
54+
reportC := make(chan string, 1)
55+
go func() {
56+
reportC <- reporter()
57+
}()
58+
var report string
59+
select {
60+
case report = <-reportC:
61+
case <-ctx.Done():
62+
return out
63+
}
64+
out = append(out, report)
65+
}
66+
return out
67+
}
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package internal_test
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
"github.com/onsi/gomega/gleak"
10+
11+
"github.com/onsi/ginkgo/v2/internal"
12+
)
13+
14+
var _ = Describe("ProgressReporterManager", func() {
15+
var manager *internal.ProgressReporterManager
16+
17+
BeforeEach(func() {
18+
manager = internal.NewProgressReporterManager()
19+
})
20+
21+
It("can attach and detach progress reporters", func() {
22+
Ω(manager.QueryProgressReporters(context.Background())).Should(BeEmpty())
23+
cancelA := manager.AttachProgressReporter(func() string { return "A" })
24+
Ω(manager.QueryProgressReporters(context.Background())).Should(Equal([]string{"A"}))
25+
cancelB := manager.AttachProgressReporter(func() string { return "B" })
26+
Ω(manager.QueryProgressReporters(context.Background())).Should(Equal([]string{"A", "B"}))
27+
cancelC := manager.AttachProgressReporter(func() string { return "C" })
28+
Ω(manager.QueryProgressReporters(context.Background())).Should(Equal([]string{"A", "B", "C"}))
29+
cancelB()
30+
Ω(manager.QueryProgressReporters(context.Background())).Should(Equal([]string{"A", "C"}))
31+
cancelA()
32+
Ω(manager.QueryProgressReporters(context.Background())).Should(Equal([]string{"C"}))
33+
cancelC()
34+
Ω(manager.QueryProgressReporters(context.Background())).Should(BeEmpty())
35+
})
36+
37+
It("bails if a progress reporter takes longer than the passed-in context's deadline", func() {
38+
startingGoroutines := gleak.Goroutines()
39+
c := make(chan struct{})
40+
manager.AttachProgressReporter(func() string { return "A" })
41+
manager.AttachProgressReporter(func() string { return "B" })
42+
manager.AttachProgressReporter(func() string {
43+
<-c
44+
return "C"
45+
})
46+
manager.AttachProgressReporter(func() string { return "D" })
47+
context, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
48+
result := manager.QueryProgressReporters(context)
49+
Ω(result).Should(Equal([]string{"A", "B"}))
50+
cancel()
51+
close(c)
52+
53+
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(startingGoroutines))
54+
})
55+
})

internal/spec_context.go

+5-48
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package internal
22

33
import (
44
"context"
5-
"sort"
6-
"sync"
75

86
"github.com/onsi/ginkgo/v2/types"
97
)
@@ -17,11 +15,9 @@ type SpecContext interface {
1715

1816
type specContext struct {
1917
context.Context
18+
*ProgressReporterManager
2019

21-
cancel context.CancelFunc
22-
lock *sync.Mutex
23-
progressReporters map[int]func() string
24-
prCounter int
20+
cancel context.CancelFunc
2521

2622
suite *Suite
2723
}
@@ -36,11 +32,9 @@ This is because Ginkgo needs finer control over when the context is canceled. S
3632
func NewSpecContext(suite *Suite) *specContext {
3733
ctx, cancel := context.WithCancel(context.Background())
3834
sc := &specContext{
39-
cancel: cancel,
40-
suite: suite,
41-
lock: &sync.Mutex{},
42-
prCounter: 0,
43-
progressReporters: map[int]func() string{},
35+
cancel: cancel,
36+
suite: suite,
37+
ProgressReporterManager: NewProgressReporterManager(),
4438
}
4539
ctx = context.WithValue(ctx, "GINKGO_SPEC_CONTEXT", sc) //yes, yes, the go docs say don't use a string for a key... but we'd rather avoid a circular dependency between Gomega and Ginkgo
4640
sc.Context = ctx //thank goodness for garbage collectors that can handle circular dependencies
@@ -51,40 +45,3 @@ func NewSpecContext(suite *Suite) *specContext {
5145
func (sc *specContext) SpecReport() types.SpecReport {
5246
return sc.suite.CurrentSpecReport()
5347
}
54-
55-
func (sc *specContext) AttachProgressReporter(reporter func() string) func() {
56-
sc.lock.Lock()
57-
defer sc.lock.Unlock()
58-
sc.prCounter += 1
59-
prCounter := sc.prCounter
60-
sc.progressReporters[prCounter] = reporter
61-
62-
return func() {
63-
sc.lock.Lock()
64-
defer sc.lock.Unlock()
65-
delete(sc.progressReporters, prCounter)
66-
}
67-
}
68-
69-
func (sc *specContext) QueryProgressReporters() []string {
70-
sc.lock.Lock()
71-
keys := []int{}
72-
for key := range sc.progressReporters {
73-
keys = append(keys, key)
74-
}
75-
sort.Ints(keys)
76-
reporters := []func() string{}
77-
for _, key := range keys {
78-
reporters = append(reporters, sc.progressReporters[key])
79-
}
80-
sc.lock.Unlock()
81-
82-
if len(reporters) == 0 {
83-
return nil
84-
}
85-
out := []string{}
86-
for _, reporter := range reporters {
87-
out = append(out, reporter())
88-
}
89-
return out
90-
}

internal/spec_context_test.go

+8-8
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,25 @@ var _ = Describe("SpecContext", func() {
2525
It("can attach and detach progress reporters", func(c SpecContext) {
2626
type CompleteSpecContext interface {
2727
AttachProgressReporter(func() string) func()
28-
QueryProgressReporters() []string
28+
QueryProgressReporters(ctx context.Context) []string
2929
}
3030

3131
wrappedC := context.WithValue(c, "foo", "bar")
3232
ctx := wrappedC.Value("GINKGO_SPEC_CONTEXT").(CompleteSpecContext)
3333

34-
Ω(ctx.QueryProgressReporters()).Should(BeEmpty())
34+
Ω(ctx.QueryProgressReporters(context.Background())).Should(BeEmpty())
3535

3636
cancelA := ctx.AttachProgressReporter(func() string { return "A" })
37-
Ω(ctx.QueryProgressReporters()).Should(Equal([]string{"A"}))
37+
Ω(ctx.QueryProgressReporters(context.Background())).Should(Equal([]string{"A"}))
3838
cancelB := ctx.AttachProgressReporter(func() string { return "B" })
39-
Ω(ctx.QueryProgressReporters()).Should(Equal([]string{"A", "B"}))
39+
Ω(ctx.QueryProgressReporters(context.Background())).Should(Equal([]string{"A", "B"}))
4040
cancelC := ctx.AttachProgressReporter(func() string { return "C" })
41-
Ω(ctx.QueryProgressReporters()).Should(Equal([]string{"A", "B", "C"}))
41+
Ω(ctx.QueryProgressReporters(context.Background())).Should(Equal([]string{"A", "B", "C"}))
4242
cancelB()
43-
Ω(ctx.QueryProgressReporters()).Should(Equal([]string{"A", "C"}))
43+
Ω(ctx.QueryProgressReporters(context.Background())).Should(Equal([]string{"A", "C"}))
4444
cancelA()
45-
Ω(ctx.QueryProgressReporters()).Should(Equal([]string{"C"}))
45+
Ω(ctx.QueryProgressReporters(context.Background())).Should(Equal([]string{"C"}))
4646
cancelC()
47-
Ω(ctx.QueryProgressReporters()).Should(BeEmpty())
47+
Ω(ctx.QueryProgressReporters(context.Background())).Should(BeEmpty())
4848
})
4949
})

0 commit comments

Comments
 (0)