package test_helpers
import (
"fmt"
"strings"
"sync"
"github.com/onsi/ginkgo/v2/formatter"
. "github.com/onsi/gomega"
"github.com/onsi/gomega/types"
)
/*
RunTracker tracks invocations of functions - useful to assert orders in which nodes run
*/
type RunTracker struct {
lock *sync.Mutex
trackedRuns []string
trackedData map[string]map[string]interface{}
}
func NewRunTracker() *RunTracker {
return &RunTracker{
lock: &sync.Mutex{},
trackedData: map[string]map[string]interface{}{},
}
}
func (rt *RunTracker) Reset() {
rt.lock.Lock()
defer rt.lock.Unlock()
rt.trackedRuns = []string{}
}
func (rt *RunTracker) Run(text string) {
rt.lock.Lock()
defer rt.lock.Unlock()
rt.trackedRuns = append(rt.trackedRuns, text)
}
func (rt *RunTracker) RunWithData(text string, kv ...interface{}) {
rt.lock.Lock()
defer rt.lock.Unlock()
rt.trackedRuns = append(rt.trackedRuns, text)
data := map[string]interface{}{}
for i := 0; i < len(kv); i += 2 {
key := kv[i].(string)
value := kv[i+1]
data[key] = value
}
rt.trackedData[text] = data
}
func (rt *RunTracker) TrackedRuns() []string {
rt.lock.Lock()
defer rt.lock.Unlock()
trackedRuns := make([]string, len(rt.trackedRuns))
copy(trackedRuns, rt.trackedRuns)
return trackedRuns
}
func (rt *RunTracker) DataFor(text string) map[string]interface{} {
rt.lock.Lock()
defer rt.lock.Unlock()
return rt.trackedData[text]
}
func (rt *RunTracker) T(text string, callback ...func()) func() {
return func() {
rt.Run(text)
if len(callback) > 0 {
callback[0]()
}
}
}
func (rt *RunTracker) C(text string, callback ...func()) func(args []string, additionalArgs []string) {
return func(args []string, additionalArgs []string) {
rt.RunWithData(text, "Args", args, "AdditionalArgs", additionalArgs)
if len(callback) > 0 {
callback[0]()
}
}
}
func HaveRun(run string) OmegaMatcher {
return WithTransform(func(rt *RunTracker) []string {
return rt.TrackedRuns()
}, ContainElement(run))
}
func HaveRunWithData(run string, kv ...interface{}) OmegaMatcher {
matchers := []types.GomegaMatcher{}
for i := 0; i < len(kv); i += 2 {
matchers = append(matchers, HaveKeyWithValue(kv[i], kv[i+1]))
}
return And(
HaveRun(run),
WithTransform(func(rt *RunTracker) map[string]interface{} {
return rt.DataFor(run)
}, And(matchers...)),
)
}
func HaveTrackedNothing() OmegaMatcher {
return WithTransform(func(rt *RunTracker) []string {
return rt.TrackedRuns()
}, BeEmpty())
}
type HaveTrackedMatcher struct {
expectedRuns []string
message string
}
func (m *HaveTrackedMatcher) Match(actual interface{}) (bool, error) {
rt, ok := actual.(*RunTracker)
if !ok {
return false, fmt.Errorf("HaveTracked() must be passed a RunTracker - got %T instead", actual)
}
actualRuns := rt.TrackedRuns()
n := len(actualRuns)
if n < len(m.expectedRuns) {
n = len(m.expectedRuns)
}
failureMessage, success := &strings.Builder{}, true
fmt.Fprintf(failureMessage, "{{/}}%10s == %-10s{{/}}\n", "Actual", "Expected")
fmt.Fprintf(failureMessage, "{{/}}========================\n{{/}}")
for i := 0; i < n; i++ {
var expected, actual string
if i < len(actualRuns) {
actual = actualRuns[i]
}
if i < len(m.expectedRuns) {
expected = m.expectedRuns[i]
}
if actual != expected {
success = false
fmt.Fprintf(failureMessage, "{{red}}%10s != %-10s{{/}}\n", actual, expected)
} else {
fmt.Fprintf(failureMessage, "{{green}}%10s == %-10s{{/}}\n", actual, expected)
}
}
m.message = failureMessage.String()
return success, nil
}
func (m *HaveTrackedMatcher) FailureMessage(actual interface{}) string {
return "Expected runs did not match tracked runs:\n" + formatter.F(m.message)
}
func (m *HaveTrackedMatcher) NegatedFailureMessage(actual interface{}) string {
return "Expected runs matched tracked runs:\n" + formatter.F(m.message)
}
func HaveTracked(runs ...string) OmegaMatcher {
return &HaveTrackedMatcher{expectedRuns: runs}
}
package internal_integration_test
import (
"time"
. "github.com/onsi/ginkgo/v2"
"github.com/onsi/ginkgo/v2/internal/interrupt_handler"
. "github.com/onsi/ginkgo/v2/internal/test_helpers"
"github.com/onsi/ginkgo/v2/types"
. "github.com/onsi/gomega"
)
var _ = Describe("Sending reports to ReportAfterSuite procs", func() {
var failInReportAfterSuiteA, interruptSuiteB bool
var fixture func()
BeforeEach(func() {
failInReportAfterSuiteA = false
interruptSuiteB = false
conf.RandomSeed = 17
fixture = func() {
BeforeSuite(rt.T("before-suite", func() {
outputInterceptor.AppendInterceptedOutput("out-before-suite")
}))
Context("container", func() {
It("A", rt.T("A"))
It("B", rt.T("B", func() {
F("fail in B")
}))
It("C", rt.T("C"))
PIt("D", rt.T("D"))
})
ReportAfterSuite("Report A", func(report Report) {
rt.RunWithData("report-A", "report", report)
writer.Print("gw-report-A")
outputInterceptor.AppendInterceptedOutput("out-report-A")
if failInReportAfterSuiteA {
F("fail in report-A")
}
})
ReportAfterSuite("Report B", func(report Report) {
if interruptSuiteB {
interruptHandler.Interrupt(interrupt_handler.InterruptCauseTimeout)
time.Sleep(100 * time.Millisecond)
}
rt.RunWithData("report-B", "report", report, "emitted-interrupt", interruptHandler.EmittedInterruptPlaceholderMessage())
writer.Print("gw-report-B")
outputInterceptor.AppendInterceptedOutput("out-report-B")
})
AfterSuite(rt.T("after-suite", func() {
writer.Print("gw-after-suite")
F("fail in after-suite")
}))
}
})
Context("when running in series", func() {
BeforeEach(func() {
conf.ParallelTotal = 1
conf.ParallelProcess = 1
})
Context("the happy path", func() {
BeforeEach(func() {
success, _ := RunFixture("happy-path", fixture)
Ω(success).Should(BeFalse())
})
It("runs all the functions", func() {
Ω(rt).Should(HaveTracked(
"before-suite",
"A", "B", "C",
"after-suite",
"report-A", "report-B",
))
})
It("reports on the report procs", func() {
Ω(reporter.Did.Find("Report A")).Should(HavePassed(
types.NodeTypeReportAfterSuite,
CapturedGinkgoWriterOutput("gw-report-A"),
CapturedStdOutput("out-report-A"),
))
Ω(reporter.Did.Find("Report B")).Should(HavePassed(
types.NodeTypeReportAfterSuite,
CapturedGinkgoWriterOutput("gw-report-B"),
CapturedStdOutput("out-report-B"),
))
})
It("passes the report in to each reporter", func() {
reportA := rt.DataFor("report-A")["report"].(types.Report)
reportB := rt.DataFor("report-B")["report"].(types.Report)
for _, report := range []types.Report{reportA, reportB} {
Ω(report.SuiteDescription).Should(Equal("happy-path"))
Ω(report.SuiteSucceeded).Should(BeFalse())
Ω(report.SuiteConfig.RandomSeed).Should(Equal(int64(17)))
reports := Reports(report.SpecReports)
Ω(reports.FindByLeafNodeType(types.NodeTypeBeforeSuite)).Should(HavePassed(CapturedStdOutput("out-before-suite")))
Ω(reports.Find("A")).Should(HavePassed())
Ω(reports.Find("B")).Should(HaveFailed("fail in B"))
Ω(reports.Find("C")).Should(HavePassed())
Ω(reports.Find("D")).Should(BePending())
Ω(reports.FindByLeafNodeType(types.NodeTypeAfterSuite)).Should(HaveFailed("fail in after-suite", CapturedGinkgoWriterOutput("gw-after-suite")))
}
Ω(len(reportB.SpecReports)-len(reportA.SpecReports)).Should(Equal(1), "Report B includes the invocation of ReporteAfterSuite A")
Ω(Reports(reportB.SpecReports).Find("Report A")).Should(Equal(reporter.Did.Find("Report A")))
})
})
Context("when a ReportAfterSuite proc fails", func() {
BeforeEach(func() {
failInReportAfterSuiteA = true
success, _ := RunFixture("report-A-fails", fixture)
Ω(success).Should(BeFalse())
})
It("keeps running subseuqent reporting functions", func() {
Ω(rt).Should(HaveTracked(
"before-suite",
"A", "B", "C",
"after-suite",
"report-A", "report-B",
))
})
It("reports on the faitlure, to Ginkgo's reporter and any subsequent reporters", func() {
Ω(reporter.Did.Find("Report A")).Should(HaveFailed(
types.NodeTypeReportAfterSuite,
"fail in report-A",
CapturedGinkgoWriterOutput("gw-report-A"),
CapturedStdOutput("out-report-A"),
))
reportB := rt.DataFor("report-B")["report"].(types.Report)
Ω(Reports(reportB.SpecReports).Find("Report A")).Should(Equal(reporter.Did.Find("Report A")))
})
})
Context("when an interrupt is attempted in a ReportAfterSuiteNode", func() {
BeforeEach(func() {
interruptSuiteB = true
success, _ := RunFixture("report-B-interrupted", fixture)
Ω(success).Should(BeFalse())
})
It("ignores the interrupt and soliders on", func() {
Ω(rt).Should(HaveTracked(
"before-suite",
"A", "B", "C",
"after-suite",
"report-A", "report-B",
))
Ω(rt.DataFor("report-B")["report"]).ShouldNot(BeZero())
Ω(rt.DataFor("report-B")["emitted-interrupt"]).Should(ContainSubstring("The running ReportAfterSuite node is at:\n%s", reporter.Did.Find("Report B").LeafNodeLocation.FileName))
})
})
})
Context("when running in parallel", func() {
var otherNodeReport types.Report
BeforeEach(func() {
SetUpForParallel(2)
otherNodeReport = types.Report{
SpecReports: types.SpecReports{
types.SpecReport{LeafNodeText: "E", LeafNodeLocation: cl, State: types.SpecStatePassed, LeafNodeType: types.NodeTypeIt},
types.SpecReport{LeafNodeText: "F", LeafNodeLocation: cl, State: types.SpecStateSkipped, LeafNodeType: types.NodeTypeIt},
},
}
})
Context("on proc 1", func() {
BeforeEach(func() {
conf.ParallelProcess = 1
})
Context("the happy path", func() {
BeforeEach(func() {
// proc 2 has reported back and exited
client.PostSuiteDidEnd(otherNodeReport)
close(exitChannels[2])
success, _ := RunFixture("happy-path", fixture)
Ω(success).Should(BeFalse())
})
It("runs all the functions", func() {
Ω(rt).Should(HaveTracked(
"before-suite",
"A", "B", "C",
"after-suite",
"report-A", "report-B",
))
})
It("passes the report in to each reporter, including information from other procs", func() {
reportA := rt.DataFor("report-A")["report"].(types.Report)
reportB := rt.DataFor("report-B")["report"].(types.Report)
for _, report := range []types.Report{reportA, reportB} {
Ω(report.SuiteDescription).Should(Equal("happy-path"))
Ω(report.SuiteSucceeded).Should(BeFalse())
reports := Reports(report.SpecReports)
Ω(reports.FindByLeafNodeType(types.NodeTypeBeforeSuite)).Should(HavePassed(CapturedStdOutput("out-before-suite")))
Ω(reports.Find("A")).Should(HavePassed())
Ω(reports.Find("B")).Should(HaveFailed("fail in B"))
Ω(reports.Find("C")).Should(HavePassed())
Ω(reports.Find("D")).Should(BePending())
Ω(reports.Find("E")).Should(HavePassed())
Ω(reports.Find("F")).Should(HaveBeenSkipped())
Ω(reports.FindByLeafNodeType(types.NodeTypeAfterSuite)).Should(HaveFailed("fail in after-suite", CapturedGinkgoWriterOutput("gw-after-suite")))
}
Ω(len(reportB.SpecReports)-len(reportA.SpecReports)).Should(Equal(1), "Report B includes the invocation of ReporteAfterSuite A")
Ω(Reports(reportB.SpecReports).Find("Report A")).Should(Equal(reporter.Did.Find("Report A")))
})
})
Describe("waiting for reports from other procs", func() {
It("blocks until the other procs have finished", func() {
done := make(chan interface{})
go func() {
defer GinkgoRecover()
success, _ := RunFixture("happy-path", fixture)
Ω(success).Should(BeFalse())
close(done)
}()
Consistently(done).ShouldNot(BeClosed())
client.PostSuiteDidEnd(otherNodeReport)
Consistently(done).ShouldNot(BeClosed())
close(exitChannels[2])
Eventually(done).Should(BeClosed())
})
})
Context("when a non-primary proc disappears before it reports", func() {
BeforeEach(func() {
close(exitChannels[2]) //proc 2 disappears before reporting
success, _ := RunFixture("disappearing-proc-2", fixture)
Ω(success).Should(BeFalse())
})
It("does not run the ReportAfterSuite procs", func() {
Ω(rt).Should(HaveTracked(
"before-suite",
"A", "B", "C",
"after-suite",
))
})
It("reports all the ReportAfterSuite procs as failed", func() {
Ω(reporter.Did.Find("Report A")).Should(HaveFailed(types.GinkgoErrors.AggregatedReportUnavailableDueToNodeDisappearing().Error()))
Ω(reporter.Did.Find("Report B")).Should(HaveFailed(types.GinkgoErrors.AggregatedReportUnavailableDueToNodeDisappearing().Error()))
})
})
})
Context("on a non-primary proc", func() {
BeforeEach(func() {
conf.ParallelProcess = 2
success, _ := RunFixture("happy-path", fixture)
Ω(success).Should(BeFalse())
})
It("does not run the ReportAfterSuite procs", func() {
Ω(rt).Should(HaveTracked(
"before-suite",
"A", "B", "C",
"after-suite",
))
})
})
})
})