Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
package internal
import (
"github.com/onsi/ginkgo/v2/types"
)
func ComputeAroundNodes(specs Specs) Specs {
out := Specs{}
for _, spec := range specs {
nodes := Nodes{}
currentNestingLevel := 0
aroundNodes := types.AroundNodes{}
nestingLevelIndices := []int{}
for _, node := range spec.Nodes {
switch node.NodeType {
case types.NodeTypeContainer:
currentNestingLevel = node.NestingLevel + 1
nestingLevelIndices = append(nestingLevelIndices, len(aroundNodes))
aroundNodes = aroundNodes.Append(node.AroundNodes...)
nodes = append(nodes, node)
default:
if currentNestingLevel > node.NestingLevel {
currentNestingLevel = node.NestingLevel
aroundNodes = aroundNodes[:nestingLevelIndices[currentNestingLevel]]
}
node.AroundNodes = types.AroundNodes{}.Append(aroundNodes...).Append(node.AroundNodes...)
nodes = append(nodes, node)
}
}
spec.Nodes = nodes
out = append(out, spec)
}
return out
}
+9
View File
@@ -0,0 +1,9 @@
package internal
func MakeIncrementingIndexCounter() func() (int, error) {
idx := -1
return func() (int, error) {
idx += 1
return idx, nil
}
}
+99
View File
@@ -0,0 +1,99 @@
package internal
import (
"fmt"
"sync"
"github.com/onsi/ginkgo/v2/types"
)
type Failer struct {
lock *sync.Mutex
failure types.Failure
state types.SpecState
}
func NewFailer() *Failer {
return &Failer{
lock: &sync.Mutex{},
state: types.SpecStatePassed,
}
}
func (f *Failer) GetState() types.SpecState {
f.lock.Lock()
defer f.lock.Unlock()
return f.state
}
func (f *Failer) GetFailure() types.Failure {
f.lock.Lock()
defer f.lock.Unlock()
return f.failure
}
func (f *Failer) Panic(location types.CodeLocation, forwardedPanic any) {
f.lock.Lock()
defer f.lock.Unlock()
if f.state == types.SpecStatePassed {
f.state = types.SpecStatePanicked
f.failure = types.Failure{
Message: "Test Panicked",
Location: location,
ForwardedPanic: fmt.Sprintf("%v", forwardedPanic),
}
}
}
func (f *Failer) Fail(message string, location types.CodeLocation) {
f.lock.Lock()
defer f.lock.Unlock()
if f.state == types.SpecStatePassed {
f.state = types.SpecStateFailed
f.failure = types.Failure{
Message: message,
Location: location,
}
}
}
func (f *Failer) Skip(message string, location types.CodeLocation) {
f.lock.Lock()
defer f.lock.Unlock()
if f.state == types.SpecStatePassed {
f.state = types.SpecStateSkipped
f.failure = types.Failure{
Message: message,
Location: location,
}
}
}
func (f *Failer) AbortSuite(message string, location types.CodeLocation) {
f.lock.Lock()
defer f.lock.Unlock()
if f.state == types.SpecStatePassed {
f.state = types.SpecStateAborted
f.failure = types.Failure{
Message: message,
Location: location,
}
}
}
func (f *Failer) Drain() (types.SpecState, types.Failure) {
f.lock.Lock()
defer f.lock.Unlock()
failure := f.failure
outcome := f.state
f.state = types.SpecStatePassed
f.failure = types.Failure{}
return outcome, failure
}
+146
View File
@@ -0,0 +1,146 @@
package internal
import (
"regexp"
"strings"
"github.com/onsi/ginkgo/v2/types"
)
/*
If a container marked as focus has a descendant that is also marked as focus, Ginkgo's policy is to
unmark the container's focus. This gives developers a more intuitive experience when debugging specs.
It is common to focus a container to just run a subset of specs, then identify the specific specs within the container to focus -
this policy allows the developer to simply focus those specific specs and not need to go back and turn the focus off of the container:
As a common example, consider:
FDescribe("something to debug", function() {
It("works", function() {...})
It("works", function() {...})
FIt("doesn't work", function() {...})
It("works", function() {...})
})
here the developer's intent is to focus in on the `"doesn't work"` spec and not to run the adjacent specs in the focused `"something to debug"` container.
The nested policy applied by this function enables this behavior.
*/
func ApplyNestedFocusPolicyToTree(tree *TreeNode) {
var walkTree func(tree *TreeNode) bool
walkTree = func(tree *TreeNode) bool {
if tree.Node.MarkedPending {
return false
}
hasFocusedDescendant := false
for _, child := range tree.Children {
childHasFocus := walkTree(child)
hasFocusedDescendant = hasFocusedDescendant || childHasFocus
}
tree.Node.MarkedFocus = tree.Node.MarkedFocus && !hasFocusedDescendant
return tree.Node.MarkedFocus || hasFocusedDescendant
}
walkTree(tree)
}
/*
Ginkgo supports focussing specs using `FIt`, `FDescribe`, etc. - this is called "programmatic focus"
It also supports focussing specs using regular expressions on the command line (`-focus=`, `-skip=`) that match against spec text and file filters (`-focus-files=`, `-skip-files=`) that match against code locations for nodes in specs.
When both programmatic and file filters are provided their results are ANDed together. If multiple kinds of filters are provided, the file filters run first followed by the regex filters.
This function sets the `Skip` property on specs by applying Ginkgo's focus policy:
- If there are no CLI arguments and no programmatic focus, do nothing.
- If a spec somewhere has programmatic focus skip any specs that have no programmatic focus.
- If there are CLI arguments parse them and skip any specs that either don't match the focus filters or do match the skip filters.
*Note:* specs with pending nodes are Skipped when created by NewSpec.
*/
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, "|")
type SkipCheck func(spec Spec) bool
// by default, skip any specs marked pending
skipChecks := []SkipCheck{func(spec Spec) bool { return spec.Nodes.HasNodeMarkedPending() }}
hasProgrammaticFocus := false
for _, spec := range specs {
if spec.Nodes.HasNodeMarkedFocus() && !spec.Nodes.HasNodeMarkedPending() {
hasProgrammaticFocus = true
break
}
}
if hasProgrammaticFocus {
skipChecks = append(skipChecks, func(spec Spec) bool { return !spec.Nodes.HasNodeMarkedFocus() })
}
if suiteConfig.LabelFilter != "" {
labelFilter, _ := types.ParseLabelFilter(suiteConfig.LabelFilter)
skipChecks = append(skipChecks, func(spec Spec) bool {
return !labelFilter(UnionOfLabels(suiteLabels, spec.Nodes.UnionOfLabels()))
})
}
if suiteConfig.SemVerFilter != "" {
semVerFilter, _ := types.ParseSemVerFilter(suiteConfig.SemVerFilter)
skipChecks = append(skipChecks, func(spec Spec) bool {
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
})
}
if len(suiteConfig.FocusFiles) > 0 {
focusFilters, _ := types.ParseFileFilters(suiteConfig.FocusFiles)
skipChecks = append(skipChecks, func(spec Spec) bool { return !focusFilters.Matches(spec.Nodes.CodeLocations()) })
}
if len(suiteConfig.SkipFiles) > 0 {
skipFilters, _ := types.ParseFileFilters(suiteConfig.SkipFiles)
skipChecks = append(skipChecks, func(spec Spec) bool { return skipFilters.Matches(spec.Nodes.CodeLocations()) })
}
if focusString != "" {
// skip specs that don't match the focus string
re := regexp.MustCompile(focusString)
skipChecks = append(skipChecks, func(spec Spec) bool { return !re.MatchString(description + " " + spec.Text()) })
}
if skipString != "" {
// skip specs that match the skip string
re := regexp.MustCompile(skipString)
skipChecks = append(skipChecks, func(spec Spec) bool { return re.MatchString(description + " " + spec.Text()) })
}
// skip specs if shouldSkip() is true. note that we do nothing if shouldSkip() is false to avoid overwriting skip status established by the node's pending status
processedSpecs := Specs{}
for _, spec := range specs {
for _, skipCheck := range skipChecks {
if skipCheck(spec) {
spec.Skip = true
break
}
}
processedSpecs = append(processedSpecs, spec)
}
return processedSpecs, hasProgrammaticFocus
}
+28
View File
@@ -0,0 +1,28 @@
package global
import (
"github.com/onsi/ginkgo/v2/internal"
)
var Suite *internal.Suite
var Failer *internal.Failer
var backupSuite *internal.Suite
func init() {
InitializeGlobals()
}
func InitializeGlobals() {
Failer = internal.NewFailer()
Suite = internal.NewSuite()
}
func PushClone() error {
var err error
backupSuite, err = Suite.Clone()
return err
}
func PopClone() {
Suite = backupSuite
}
+418
View File
@@ -0,0 +1,418 @@
package internal
import (
"fmt"
"time"
"github.com/onsi/ginkgo/v2/types"
)
type runOncePair struct {
//nodeId should only run once...
nodeID uint
nodeType types.NodeType
//...for specs in a hierarchy that includes this context
containerID uint
}
func (pair runOncePair) isZero() bool {
return pair.nodeID == 0
}
func runOncePairForNode(node Node, containerID uint) runOncePair {
return runOncePair{
nodeID: node.ID,
nodeType: node.NodeType,
containerID: containerID,
}
}
type runOncePairs []runOncePair
func runOncePairsForSpec(spec Spec) runOncePairs {
pairs := runOncePairs{}
containers := spec.Nodes.WithType(types.NodeTypeContainer)
for _, node := range spec.Nodes {
if node.NodeType.Is(types.NodeTypeBeforeAll | types.NodeTypeAfterAll) {
pairs = append(pairs, runOncePairForNode(node, containers.FirstWithNestingLevel(node.NestingLevel-1).ID))
} else if node.NodeType.Is(types.NodeTypeBeforeEach|types.NodeTypeJustBeforeEach|types.NodeTypeAfterEach|types.NodeTypeJustAfterEach) && node.MarkedOncePerOrdered {
passedIntoAnOrderedContainer := false
firstOrderedContainerDeeperThanNode := containers.FirstSatisfying(func(container Node) bool {
passedIntoAnOrderedContainer = passedIntoAnOrderedContainer || container.MarkedOrdered
return container.NestingLevel >= node.NestingLevel && passedIntoAnOrderedContainer
})
if firstOrderedContainerDeeperThanNode.IsZero() {
continue
}
pairs = append(pairs, runOncePairForNode(node, firstOrderedContainerDeeperThanNode.ID))
}
}
return pairs
}
func (pairs runOncePairs) runOncePairFor(nodeID uint) runOncePair {
for i := range pairs {
if pairs[i].nodeID == nodeID {
return pairs[i]
}
}
return runOncePair{}
}
func (pairs runOncePairs) hasRunOncePair(pair runOncePair) bool {
for i := range pairs {
if pairs[i] == pair {
return true
}
}
return false
}
func (pairs runOncePairs) withType(nodeTypes types.NodeType) runOncePairs {
count := 0
for i := range pairs {
if pairs[i].nodeType.Is(nodeTypes) {
count++
}
}
out, j := make(runOncePairs, count), 0
for i := range pairs {
if pairs[i].nodeType.Is(nodeTypes) {
out[j] = pairs[i]
j++
}
}
return out
}
type group struct {
suite *Suite
specs Specs
runOncePairs map[uint]runOncePairs
runOnceTracker map[runOncePair]types.SpecState
succeeded bool
failedInARunOnceBefore bool
continueOnFailure bool
}
func newGroup(suite *Suite) *group {
return &group{
suite: suite,
runOncePairs: map[uint]runOncePairs{},
runOnceTracker: map[runOncePair]types.SpecState{},
succeeded: true,
failedInARunOnceBefore: false,
continueOnFailure: false,
}
}
// 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(),
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(),
}
}
// constructionNodeReportForTreeNode constructs a new SpecReport right before invoking the body
// of a container node during construction of the full tree.
func constructionNodeReportForTreeNode(node *TreeNode) *types.ConstructionNodeReport {
var report types.ConstructionNodeReport
// Walk up the tree and set attributes accordingly.
addNodeToReportForNode(&report, node)
return &report
}
// addNodeToReportForNode is conceptually similar to initialReportForSpec and therefore placed here
// although it doesn't do anything with a group.
func addNodeToReportForNode(report *types.ConstructionNodeReport, node *TreeNode) {
if node.Parent != nil {
// First add the parent node, then the current one.
addNodeToReportForNode(report, node.Parent)
}
report.ContainerHierarchyTexts = append(report.ContainerHierarchyTexts, node.Node.Text)
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
}
if node.Node.MarkedOrdered {
report.IsInOrderedContainer = true
}
}
func (g *group) evaluateSkipStatus(spec Spec) (types.SpecState, types.Failure) {
if spec.Nodes.HasNodeMarkedPending() {
return types.SpecStatePending, types.Failure{}
}
if spec.Skip {
return types.SpecStateSkipped, types.Failure{}
}
if g.suite.interruptHandler.Status().Interrupted() || g.suite.skipAll {
return types.SpecStateSkipped, types.Failure{}
}
if !g.suite.deadline.IsZero() && g.suite.deadline.Before(time.Now()) {
return types.SpecStateSkipped, types.Failure{}
}
if !g.succeeded && !g.continueOnFailure {
return types.SpecStateSkipped, g.suite.failureForLeafNodeWithMessage(spec.FirstNodeWithType(types.NodeTypeIt),
"Spec skipped because an earlier spec in an ordered container failed")
}
if g.failedInARunOnceBefore && g.continueOnFailure {
return types.SpecStateSkipped, g.suite.failureForLeafNodeWithMessage(spec.FirstNodeWithType(types.NodeTypeIt),
"Spec skipped because a BeforeAll node failed")
}
beforeOncePairs := g.runOncePairs[spec.SubjectID()].withType(types.NodeTypeBeforeAll | types.NodeTypeBeforeEach | types.NodeTypeJustBeforeEach)
for _, pair := range beforeOncePairs {
if g.runOnceTracker[pair].Is(types.SpecStateSkipped) {
return types.SpecStateSkipped, g.suite.failureForLeafNodeWithMessage(spec.FirstNodeWithType(types.NodeTypeIt),
fmt.Sprintf("Spec skipped because Skip() was called in %s", pair.nodeType))
}
}
if g.suite.config.DryRun {
return types.SpecStatePassed, types.Failure{}
}
return g.suite.currentSpecReport.State, g.suite.currentSpecReport.Failure
}
func (g *group) isLastSpecWithPair(specID uint, pair runOncePair) bool {
lastSpecID := uint(0)
for idx := range g.specs {
if g.specs[idx].Skip {
continue
}
sID := g.specs[idx].SubjectID()
if g.runOncePairs[sID].hasRunOncePair(pair) {
lastSpecID = sID
}
}
return lastSpecID == specID
}
func (g *group) attemptSpec(isFinalAttempt bool, spec Spec) bool {
failedInARunOnceBefore := false
pairs := g.runOncePairs[spec.SubjectID()]
nodes := spec.Nodes.WithType(types.NodeTypeBeforeAll)
nodes = append(nodes, spec.Nodes.WithType(types.NodeTypeBeforeEach)...).SortedByAscendingNestingLevel()
nodes = append(nodes, spec.Nodes.WithType(types.NodeTypeJustBeforeEach).SortedByAscendingNestingLevel()...)
nodes = append(nodes, spec.Nodes.FirstNodeWithType(types.NodeTypeIt))
terminatingNode, terminatingPair := Node{}, runOncePair{}
deadline := time.Time{}
if spec.SpecTimeout() > 0 {
deadline = time.Now().Add(spec.SpecTimeout())
}
for _, node := range nodes {
oncePair := pairs.runOncePairFor(node.ID)
if !oncePair.isZero() && g.runOnceTracker[oncePair].Is(types.SpecStatePassed) {
continue
}
g.suite.currentSpecReport.State, g.suite.currentSpecReport.Failure = g.suite.runNode(node, deadline, spec.Nodes.BestTextFor(node))
g.suite.currentSpecReport.RunTime = time.Since(g.suite.currentSpecReport.StartTime)
if !oncePair.isZero() {
g.runOnceTracker[oncePair] = g.suite.currentSpecReport.State
}
if g.suite.currentSpecReport.State != types.SpecStatePassed {
terminatingNode, terminatingPair = node, oncePair
failedInARunOnceBefore = !terminatingPair.isZero()
break
}
}
afterNodeWasRun := map[uint]bool{}
includeDeferCleanups := false
for {
nodes := spec.Nodes.WithType(types.NodeTypeAfterEach)
nodes = append(nodes, spec.Nodes.WithType(types.NodeTypeAfterAll)...).SortedByDescendingNestingLevel()
nodes = append(spec.Nodes.WithType(types.NodeTypeJustAfterEach).SortedByDescendingNestingLevel(), nodes...)
if !terminatingNode.IsZero() {
nodes = nodes.WithinNestingLevel(terminatingNode.NestingLevel)
}
if includeDeferCleanups {
nodes = append(nodes, g.suite.cleanupNodes.WithType(types.NodeTypeCleanupAfterEach).Reverse()...)
nodes = append(nodes, g.suite.cleanupNodes.WithType(types.NodeTypeCleanupAfterAll).Reverse()...)
}
nodes = nodes.Filter(func(node Node) bool {
if afterNodeWasRun[node.ID] {
//this node has already been run on this attempt, don't rerun it
return false
}
var pair runOncePair
switch node.NodeType {
case types.NodeTypeCleanupAfterEach, types.NodeTypeCleanupAfterAll:
// check if we were generated in an AfterNode that has already run
if afterNodeWasRun[node.NodeIDWhereCleanupWasGenerated] {
return true // we were, so we should definitely run this cleanup now
}
// looks like this cleanup nodes was generated by a before node or it.
// the run-once status of a cleanup node is governed by the run-once status of its generator
pair = pairs.runOncePairFor(node.NodeIDWhereCleanupWasGenerated)
default:
pair = pairs.runOncePairFor(node.ID)
}
if pair.isZero() {
// this node is not governed by any run-once policy, we should run it
return true
}
// it's our last chance to run if we're the last spec for our oncePair
isLastSpecWithPair := g.isLastSpecWithPair(spec.SubjectID(), pair)
switch g.suite.currentSpecReport.State {
case types.SpecStatePassed: //this attempt is passing...
return isLastSpecWithPair //...we should run-once if we'this is our last chance
case types.SpecStateSkipped: //the spec was skipped by the user...
if isLastSpecWithPair {
return true //...we're the last spec, so we should run the AfterNode
}
if !terminatingPair.isZero() && terminatingNode.NestingLevel == node.NestingLevel {
return true //...or, a run-once node at our nesting level was skipped which means this is our last chance to run
}
case types.SpecStateFailed, types.SpecStatePanicked, types.SpecStateTimedout: // the spec has failed...
if isFinalAttempt {
if g.continueOnFailure {
return isLastSpecWithPair || failedInARunOnceBefore //...we're configured to continue on failures - so we should only run if we're the last spec for this pair or if we failed in a runOnceBefore (which means we _are_ the last spec to run)
} else {
return true //...this was the last attempt and continueOnFailure is false therefore we are the last spec to run and so the AfterNode should run
}
}
if !terminatingPair.isZero() { // ...and it failed in a run-once. which will be running again
if node.NodeType.Is(types.NodeTypeCleanupAfterEach | types.NodeTypeCleanupAfterAll) {
return terminatingNode.ID == node.NodeIDWhereCleanupWasGenerated // we should run this node if we're a clean-up generated by it
} else {
return terminatingNode.NestingLevel == node.NestingLevel // ...or if we're at the same nesting level
}
}
case types.SpecStateInterrupted, types.SpecStateAborted: // ...we've been interrupted and/or aborted
return true //...that means the test run is over and we should clean up the stack. Run the AfterNode
}
return false
})
if len(nodes) == 0 && includeDeferCleanups {
break
}
for _, node := range nodes {
afterNodeWasRun[node.ID] = true
state, failure := g.suite.runNode(node, deadline, spec.Nodes.BestTextFor(node))
g.suite.currentSpecReport.RunTime = time.Since(g.suite.currentSpecReport.StartTime)
if g.suite.currentSpecReport.State == types.SpecStatePassed || state == types.SpecStateAborted {
g.suite.currentSpecReport.State = state
g.suite.currentSpecReport.Failure = failure
} else if state.Is(types.SpecStateFailureStates) {
g.suite.currentSpecReport.AdditionalFailures = append(g.suite.currentSpecReport.AdditionalFailures, types.AdditionalFailure{State: state, Failure: failure})
}
}
includeDeferCleanups = true
}
return failedInARunOnceBefore
}
func (g *group) run(specs Specs) {
g.specs = specs
g.continueOnFailure = specs[0].Nodes.FirstNodeMarkedOrdered().MarkedContinueOnFailure
for _, spec := range g.specs {
g.runOncePairs[spec.SubjectID()] = runOncePairsForSpec(spec)
}
for _, spec := range g.specs {
g.suite.selectiveLock.Lock()
g.suite.currentSpecReport = g.initialReportForSpec(spec)
g.suite.selectiveLock.Unlock()
g.suite.currentSpecReport.State, g.suite.currentSpecReport.Failure = g.evaluateSkipStatus(spec)
g.suite.reporter.WillRun(g.suite.currentSpecReport)
g.suite.reportEach(spec, types.NodeTypeReportBeforeEach)
skip := g.suite.config.DryRun || g.suite.currentSpecReport.State.Is(types.SpecStateFailureStates|types.SpecStateSkipped|types.SpecStatePending)
g.suite.currentSpecReport.StartTime = time.Now()
failedInARunOnceBefore := false
if !skip {
var maxAttempts = 1
if g.suite.config.MustPassRepeatedly > 0 {
maxAttempts = g.suite.config.MustPassRepeatedly
g.suite.currentSpecReport.MaxMustPassRepeatedly = maxAttempts
} else if g.suite.currentSpecReport.MaxMustPassRepeatedly > 0 {
maxAttempts = max(1, spec.MustPassRepeatedly())
} else if g.suite.config.FlakeAttempts > 0 {
maxAttempts = g.suite.config.FlakeAttempts
g.suite.currentSpecReport.MaxFlakeAttempts = maxAttempts
} else if g.suite.currentSpecReport.MaxFlakeAttempts > 0 {
maxAttempts = max(1, spec.FlakeAttempts())
}
for attempt := 0; attempt < maxAttempts; attempt++ {
g.suite.currentSpecReport.NumAttempts = attempt + 1
g.suite.writer.Truncate()
g.suite.outputInterceptor.StartInterceptingOutput()
if attempt > 0 {
if g.suite.currentSpecReport.MaxMustPassRepeatedly > 0 {
g.suite.handleSpecEvent(types.SpecEvent{SpecEventType: types.SpecEventSpecRepeat, Attempt: attempt})
}
if g.suite.currentSpecReport.MaxFlakeAttempts > 0 {
g.suite.handleSpecEvent(types.SpecEvent{SpecEventType: types.SpecEventSpecRetry, Attempt: attempt})
}
}
failedInARunOnceBefore = g.attemptSpec(attempt == maxAttempts-1, spec)
g.suite.currentSpecReport.EndTime = time.Now()
g.suite.currentSpecReport.RunTime = g.suite.currentSpecReport.EndTime.Sub(g.suite.currentSpecReport.StartTime)
g.suite.currentSpecReport.CapturedGinkgoWriterOutput += string(g.suite.writer.Bytes())
g.suite.currentSpecReport.CapturedStdOutErr += g.suite.outputInterceptor.StopInterceptingAndReturnOutput()
if g.suite.currentSpecReport.MaxMustPassRepeatedly > 0 {
if g.suite.currentSpecReport.State.Is(types.SpecStateFailureStates | types.SpecStateSkipped) {
break
}
}
if g.suite.currentSpecReport.MaxFlakeAttempts > 0 {
if g.suite.currentSpecReport.State.Is(types.SpecStatePassed | types.SpecStateSkipped | types.SpecStateAborted | types.SpecStateInterrupted) {
break
} else if attempt < maxAttempts-1 {
af := types.AdditionalFailure{State: g.suite.currentSpecReport.State, Failure: g.suite.currentSpecReport.Failure}
af.Failure.Message = fmt.Sprintf("Failure recorded during attempt %d:\n%s", attempt+1, af.Failure.Message)
g.suite.currentSpecReport.AdditionalFailures = append(g.suite.currentSpecReport.AdditionalFailures, af)
}
}
}
}
g.suite.reportEach(spec, types.NodeTypeReportAfterEach)
g.suite.processCurrentSpecReport()
if g.suite.currentSpecReport.State.Is(types.SpecStateFailureStates) {
g.succeeded = false
g.failedInARunOnceBefore = g.failedInARunOnceBefore || failedInARunOnceBefore
}
g.suite.selectiveLock.Lock()
g.suite.currentSpecReport = types.SpecReport{}
g.suite.selectiveLock.Unlock()
}
}
@@ -0,0 +1,177 @@
package interrupt_handler
import (
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/onsi/ginkgo/v2/internal/parallel_support"
)
var ABORT_POLLING_INTERVAL = 500 * time.Millisecond
type InterruptCause uint
const (
InterruptCauseInvalid InterruptCause = iota
InterruptCauseSignal
InterruptCauseAbortByOtherProcess
)
type InterruptLevel uint
const (
InterruptLevelUninterrupted InterruptLevel = iota
InterruptLevelCleanupAndReport
InterruptLevelReportOnly
InterruptLevelBailOut
)
func (ic InterruptCause) String() string {
switch ic {
case InterruptCauseSignal:
return "Interrupted by User"
case InterruptCauseAbortByOtherProcess:
return "Interrupted by Other Ginkgo Process"
}
return "INVALID_INTERRUPT_CAUSE"
}
type InterruptStatus struct {
Channel chan any
Level InterruptLevel
Cause InterruptCause
}
func (s InterruptStatus) Interrupted() bool {
return s.Level != InterruptLevelUninterrupted
}
func (s InterruptStatus) Message() string {
return s.Cause.String()
}
func (s InterruptStatus) ShouldIncludeProgressReport() bool {
return s.Cause != InterruptCauseAbortByOtherProcess
}
type InterruptHandlerInterface interface {
Status() InterruptStatus
}
type InterruptHandler struct {
c chan any
lock *sync.Mutex
level InterruptLevel
cause InterruptCause
client parallel_support.Client
stop chan any
signals []os.Signal
requestAbortCheck chan any
}
func NewInterruptHandler(client parallel_support.Client, signals ...os.Signal) *InterruptHandler {
if len(signals) == 0 {
signals = []os.Signal{os.Interrupt, syscall.SIGTERM}
}
handler := &InterruptHandler{
c: make(chan any),
lock: &sync.Mutex{},
stop: make(chan any),
requestAbortCheck: make(chan any),
client: client,
signals: signals,
}
handler.registerForInterrupts()
return handler
}
func (handler *InterruptHandler) Stop() {
close(handler.stop)
}
func (handler *InterruptHandler) registerForInterrupts() {
// os signal handling
signalChannel := make(chan os.Signal, 1)
signal.Notify(signalChannel, handler.signals...)
// cross-process abort handling
var abortChannel chan any
if handler.client != nil {
abortChannel = make(chan any)
go func() {
pollTicker := time.NewTicker(ABORT_POLLING_INTERVAL)
for {
select {
case <-pollTicker.C:
if handler.client.ShouldAbort() {
close(abortChannel)
pollTicker.Stop()
return
}
case <-handler.requestAbortCheck:
if handler.client.ShouldAbort() {
close(abortChannel)
pollTicker.Stop()
return
}
case <-handler.stop:
pollTicker.Stop()
return
}
}
}()
}
go func(abortChannel chan any) {
var interruptCause InterruptCause
for {
select {
case <-signalChannel:
interruptCause = InterruptCauseSignal
case <-abortChannel:
interruptCause = InterruptCauseAbortByOtherProcess
case <-handler.stop:
signal.Stop(signalChannel)
return
}
abortChannel = nil
handler.lock.Lock()
oldLevel := handler.level
handler.cause = interruptCause
if handler.level == InterruptLevelUninterrupted {
handler.level = InterruptLevelCleanupAndReport
} else if handler.level == InterruptLevelCleanupAndReport {
handler.level = InterruptLevelReportOnly
} else if handler.level == InterruptLevelReportOnly {
handler.level = InterruptLevelBailOut
}
if handler.level != oldLevel {
close(handler.c)
handler.c = make(chan any)
}
handler.lock.Unlock()
}
}(abortChannel)
}
func (handler *InterruptHandler) Status() InterruptStatus {
handler.lock.Lock()
status := InterruptStatus{
Level: handler.level,
Channel: handler.c,
Cause: handler.cause,
}
handler.lock.Unlock()
if handler.client != nil && handler.client.ShouldAbort() && !status.Interrupted() {
close(handler.requestAbortCheck)
<-status.Channel
return handler.Status()
}
return status
}
@@ -0,0 +1,15 @@
//go:build freebsd || openbsd || netbsd || dragonfly || darwin || linux || solaris
// +build freebsd openbsd netbsd dragonfly darwin linux solaris
package interrupt_handler
import (
"os"
"os/signal"
"syscall"
)
func SwallowSigQuit() {
c := make(chan os.Signal, 1024)
signal.Notify(c, syscall.SIGQUIT)
}
@@ -0,0 +1,8 @@
//go:build windows
// +build windows
package interrupt_handler
func SwallowSigQuit() {
//noop
}
File diff suppressed because it is too large Load Diff
+192
View File
@@ -0,0 +1,192 @@
package internal
import (
"math/rand"
"sort"
"github.com/onsi/ginkgo/v2/types"
)
type SortableSpecs struct {
Specs Specs
Indexes []int
}
func NewSortableSpecs(specs Specs) *SortableSpecs {
indexes := make([]int, len(specs))
for i := range specs {
indexes[i] = i
}
return &SortableSpecs{
Specs: specs,
Indexes: indexes,
}
}
func (s *SortableSpecs) Len() int { return len(s.Indexes) }
func (s *SortableSpecs) Swap(i, j int) { s.Indexes[i], s.Indexes[j] = s.Indexes[j], s.Indexes[i] }
func (s *SortableSpecs) Less(i, j int) bool {
a, b := s.Specs[s.Indexes[i]], s.Specs[s.Indexes[j]]
aNodes, bNodes := a.Nodes.WithType(types.NodeTypesForContainerAndIt), b.Nodes.WithType(types.NodeTypesForContainerAndIt)
firstOrderedAIdx, firstOrderedBIdx := aNodes.IndexOfFirstNodeMarkedOrdered(), bNodes.IndexOfFirstNodeMarkedOrdered()
if firstOrderedAIdx > -1 && firstOrderedBIdx > -1 && aNodes[firstOrderedAIdx].ID == bNodes[firstOrderedBIdx].ID {
// strictly preserve order within an ordered containers. ID will track this as IDs are generated monotonically
return aNodes.FirstNodeWithType(types.NodeTypeIt).ID < bNodes.FirstNodeWithType(types.NodeTypeIt).ID
}
// if either spec is in an ordered container - only use the nodes up to the outermost ordered container
if firstOrderedAIdx > -1 {
aNodes = aNodes[:firstOrderedAIdx+1]
}
if firstOrderedBIdx > -1 {
bNodes = bNodes[:firstOrderedBIdx+1]
}
for i := 0; i < len(aNodes) && i < len(bNodes); i++ {
aCL, bCL := aNodes[i].CodeLocation, bNodes[i].CodeLocation
if aCL.FileName != bCL.FileName {
return aCL.FileName < bCL.FileName
}
if aCL.LineNumber != bCL.LineNumber {
return aCL.LineNumber < bCL.LineNumber
}
}
// either everything is equal or we have different lengths of CLs
if len(aNodes) != len(bNodes) {
return len(aNodes) < len(bNodes)
}
// ok, now we are sure everything was equal. so we use the spec text to break ties
for i := 0; i < len(aNodes); i++ {
if aNodes[i].Text != bNodes[i].Text {
return aNodes[i].Text < bNodes[i].Text
}
}
// ok, all those texts were equal. we'll use the ID of the most deeply nested node as a last resort
return aNodes[len(aNodes)-1].ID < bNodes[len(bNodes)-1].ID
}
type GroupedSpecIndices []SpecIndices
type SpecIndices []int
func OrderSpecs(specs Specs, suiteConfig types.SuiteConfig) (GroupedSpecIndices, GroupedSpecIndices) {
/*
Ginkgo has sophisticated support for randomizing specs. Specs are guaranteed to have the same
order for a given seed across test runs.
By default only top-level containers and specs are shuffled - this makes for a more intuitive debugging
experience - specs within a given container run in the order they appear in the file.
Developers can set -randomizeAllSpecs to shuffle _all_ specs.
In addition, spec containers can be marked as Ordered. Specs within an Ordered container are never shuffled.
Finally, specs and spec containers can be marked as Serial. When running in parallel, serial specs run on Process #1 _after_ all other processes have finished.
*/
// Seed a new random source based on thee configured random seed.
r := rand.New(rand.NewSource(suiteConfig.RandomSeed))
// first, we sort the entire suite to ensure a deterministic order. the sort is performed by filename, then line number, and then spec text. this ensures every parallel process has the exact same spec order and is only necessary to cover the edge case where the user iterates over a map to generate specs.
sortableSpecs := NewSortableSpecs(specs)
sort.Sort(sortableSpecs)
// then we break things into execution groups
// a group represents a single unit of execution and is a collection of SpecIndices
// usually a group is just a single spec, however ordered containers must be preserved as a single group
executionGroupIDs := []uint{}
executionGroups := map[uint]SpecIndices{}
for _, idx := range sortableSpecs.Indexes {
spec := specs[idx]
groupNode := spec.Nodes.FirstNodeMarkedOrdered()
if groupNode.IsZero() {
groupNode = spec.Nodes.FirstNodeWithType(types.NodeTypeIt)
}
executionGroups[groupNode.ID] = append(executionGroups[groupNode.ID], idx)
if len(executionGroups[groupNode.ID]) == 1 {
executionGroupIDs = append(executionGroupIDs, groupNode.ID)
}
}
// now, we only shuffle all the execution groups if we're randomizing all specs, otherwise
// we shuffle outermost containers. so we need to form shufflable groupings of GroupIDs
shufflableGroupingIDs := []uint{}
shufflableGroupingIDToGroupIDs := map[uint][]uint{}
// for each execution group we're going to have to pick a node to represent how the
// execution group is grouped for shuffling:
nodeTypesToShuffle := types.NodeTypesForContainerAndIt
if suiteConfig.RandomizeAllSpecs {
nodeTypesToShuffle = types.NodeTypeIt
}
//so, for each execution group:
for _, groupID := range executionGroupIDs {
// pick out a representative spec
representativeSpec := specs[executionGroups[groupID][0]]
// and grab the node on the spec that will represent which shufflable group this execution group belongs to
shufflableGroupingNode := representativeSpec.Nodes.FirstNodeWithType(nodeTypesToShuffle)
//add the execution group to its shufflable group
shufflableGroupingIDToGroupIDs[shufflableGroupingNode.ID] = append(shufflableGroupingIDToGroupIDs[shufflableGroupingNode.ID], groupID)
//and if it's the first one in
if len(shufflableGroupingIDToGroupIDs[shufflableGroupingNode.ID]) == 1 {
// record the shuffleable group ID
shufflableGroupingIDs = append(shufflableGroupingIDs, shufflableGroupingNode.ID)
}
}
// now, for each shuffleable group, we compute the priority
shufflableGroupingIDPriorities := map[uint]int{}
for shufflableGroupingID, groupIDs := range shufflableGroupingIDToGroupIDs {
// the priority of a shufflable grouping is the max priority of any spec in any execution group in the shufflable grouping
maxPriority := -1 << 31 // min int
for _, groupID := range groupIDs {
for _, specIdx := range executionGroups[groupID] {
specPriority := specs[specIdx].Nodes.GetSpecPriority()
maxPriority = max(specPriority, maxPriority)
}
}
shufflableGroupingIDPriorities[shufflableGroupingID] = maxPriority
}
// now we permute the sorted shufflable grouping IDs and build the ordered Groups
permutation := r.Perm(len(shufflableGroupingIDs))
shuffledGroupingIds := make([]uint, len(shufflableGroupingIDs))
for i, j := range permutation {
shuffledGroupingIds[i] = shufflableGroupingIDs[j]
}
// now, we need to stable sort the shuffledGroupingIds by priority (higher priority first)
sort.SliceStable(shuffledGroupingIds, func(i, j int) bool {
return shufflableGroupingIDPriorities[shuffledGroupingIds[i]] > shufflableGroupingIDPriorities[shuffledGroupingIds[j]]
})
// we can now take these prioritized, shuffled, groupings and form the final set of ordered spec groups
orderedGroups := GroupedSpecIndices{}
for _, id := range shuffledGroupingIds {
for _, executionGroupID := range shufflableGroupingIDToGroupIDs[id] {
orderedGroups = append(orderedGroups, executionGroups[executionGroupID])
}
}
// If we're running in series, we're done.
if suiteConfig.ParallelTotal == 1 {
return orderedGroups, GroupedSpecIndices{}
}
// We're running in parallel so we need to partition the ordered groups into a parallelizable set and a serialized set.
// The parallelizable groups will run across all Ginkgo processes...
// ...the serial groups will only run on Process #1 after all other processes have exited.
parallelizableGroups, serialGroups := GroupedSpecIndices{}, GroupedSpecIndices{}
for _, specIndices := range orderedGroups {
if specs[specIndices[0]].Nodes.HasNodeMarkedSerial() {
serialGroups = append(serialGroups, specIndices)
} else {
parallelizableGroups = append(parallelizableGroups, specIndices)
}
}
return parallelizableGroups, serialGroups
}
+250
View File
@@ -0,0 +1,250 @@
package internal
import (
"bytes"
"io"
"os"
"time"
)
const BAILOUT_TIME = 1 * time.Second
const BAILOUT_MESSAGE = `Ginkgo detected an issue while intercepting output.
When running in parallel, Ginkgo captures stdout and stderr output
and attaches it to the running spec. It looks like that process is getting
stuck for this suite.
This usually happens if you, or a library you are using, spin up an external
process and set cmd.Stdout = os.Stdout and/or cmd.Stderr = os.Stderr. This
causes the external process to keep Ginkgo's output interceptor pipe open and
causes output interception to hang.
Ginkgo has detected this and shortcircuited the capture process. The specs
will continue running after this message however output from the external
process that caused this issue will not be captured.
You have several options to fix this. In preferred order they are:
1. Pass GinkgoWriter instead of os.Stdout or os.Stderr to your process.
2. Ensure your process exits before the current spec completes. If your
process is long-lived and must cross spec boundaries, this option won't
work for you.
3. Pause Ginkgo's output interceptor before starting your process and then
resume it after. Use PauseOutputInterception() and ResumeOutputInterception()
to do this.
4. Set --output-interceptor-mode=none when running your Ginkgo suite. This will
turn off all output interception but allow specs to run in parallel without this
issue. You may miss important output if you do this including output from Go's
race detector.
More details on issue #851 - https://github.com/onsi/ginkgo/issues/851
`
/*
The OutputInterceptor is used by to
intercept and capture all stdin and stderr output during a test run.
*/
type OutputInterceptor interface {
StartInterceptingOutput()
StartInterceptingOutputAndForwardTo(io.Writer)
StopInterceptingAndReturnOutput() string
PauseIntercepting()
ResumeIntercepting()
Shutdown()
}
type NoopOutputInterceptor struct{}
func (interceptor NoopOutputInterceptor) StartInterceptingOutput() {}
func (interceptor NoopOutputInterceptor) StartInterceptingOutputAndForwardTo(io.Writer) {}
func (interceptor NoopOutputInterceptor) StopInterceptingAndReturnOutput() string { return "" }
func (interceptor NoopOutputInterceptor) PauseIntercepting() {}
func (interceptor NoopOutputInterceptor) ResumeIntercepting() {}
func (interceptor NoopOutputInterceptor) Shutdown() {}
type pipePair struct {
reader *os.File
writer *os.File
}
func startPipeFactory(pipeChannel chan pipePair, shutdown chan any) {
for {
//make the next pipe...
pair := pipePair{}
pair.reader, pair.writer, _ = os.Pipe()
select {
//...and provide it to the next consumer (they are responsible for closing the files)
case pipeChannel <- pair:
continue
//...or close the files if we were told to shutdown
case <-shutdown:
pair.reader.Close()
pair.writer.Close()
return
}
}
}
type interceptorImplementation interface {
CreateStdoutStderrClones() (*os.File, *os.File)
ConnectPipeToStdoutStderr(*os.File)
RestoreStdoutStderrFromClones(*os.File, *os.File)
ShutdownClones(*os.File, *os.File)
}
type genericOutputInterceptor struct {
intercepting bool
stdoutClone *os.File
stderrClone *os.File
pipe pipePair
shutdown chan any
emergencyBailout chan any
pipeChannel chan pipePair
interceptedContent chan string
forwardTo io.Writer
accumulatedOutput string
implementation interceptorImplementation
}
func (interceptor *genericOutputInterceptor) StartInterceptingOutput() {
interceptor.StartInterceptingOutputAndForwardTo(io.Discard)
}
func (interceptor *genericOutputInterceptor) StartInterceptingOutputAndForwardTo(w io.Writer) {
if interceptor.intercepting {
return
}
interceptor.accumulatedOutput = ""
interceptor.forwardTo = w
interceptor.ResumeIntercepting()
}
func (interceptor *genericOutputInterceptor) StopInterceptingAndReturnOutput() string {
if interceptor.intercepting {
interceptor.PauseIntercepting()
}
return interceptor.accumulatedOutput
}
func (interceptor *genericOutputInterceptor) ResumeIntercepting() {
if interceptor.intercepting {
return
}
interceptor.intercepting = true
if interceptor.stdoutClone == nil {
interceptor.stdoutClone, interceptor.stderrClone = interceptor.implementation.CreateStdoutStderrClones()
interceptor.shutdown = make(chan any)
go startPipeFactory(interceptor.pipeChannel, interceptor.shutdown)
}
// Now we make a pipe, we'll use this to redirect the input to the 1 and 2 file descriptors (this is how everything else in the world is string to log to stdout and stderr)
// we get the pipe from our pipe factory. it runs in the background so we can request the next pipe while the spec being intercepted is running
interceptor.pipe = <-interceptor.pipeChannel
interceptor.emergencyBailout = make(chan any)
//Spin up a goroutine to copy data from the pipe into a buffer, this is how we capture any output the user is emitting
go func() {
buffer := &bytes.Buffer{}
destination := io.MultiWriter(buffer, interceptor.forwardTo)
copyFinished := make(chan any)
reader := interceptor.pipe.reader
go func() {
io.Copy(destination, reader)
reader.Close() // close the read end of the pipe so we don't leak a file descriptor
close(copyFinished)
}()
select {
case <-copyFinished:
interceptor.interceptedContent <- buffer.String()
case <-interceptor.emergencyBailout:
interceptor.interceptedContent <- ""
}
}()
interceptor.implementation.ConnectPipeToStdoutStderr(interceptor.pipe.writer)
}
func (interceptor *genericOutputInterceptor) PauseIntercepting() {
if !interceptor.intercepting {
return
}
// first we have to close the write end of the pipe. To do this we have to close all file descriptors pointing
// to the write end. So that would be the pipewriter itself, and FD #1 and FD #2 if we've Dup2'd them
interceptor.pipe.writer.Close() // the pipewriter itself
// we also need to stop intercepting. we do that by reconnecting the stdout and stderr file descriptions back to their respective #1 and #2 file descriptors;
// this also closes #1 and #2 before it points that their original stdout and stderr file descriptions
interceptor.implementation.RestoreStdoutStderrFromClones(interceptor.stdoutClone, interceptor.stderrClone)
var content string
select {
case content = <-interceptor.interceptedContent:
case <-time.After(BAILOUT_TIME):
/*
By closing all the pipe writer's file descriptors associated with the pipe writer's file description the io.Copy reading from the reader
should eventually receive an EOF and exit.
**However**, if the user has spun up an external process and passed in os.Stdout/os.Stderr to cmd.Stdout/cmd.Stderr then the external process
will have a file descriptor pointing to the pipe writer's file description and it will not close until the external process exits.
That would leave us hanging here waiting for the io.Copy to close forever. Instead we invoke this emergency escape valve. This returns whatever
content we've got but leaves the io.Copy running. This ensures the external process can continue writing without hanging at the cost of leaking a goroutine
and file descriptor (those these will be cleaned up when the process exits).
We tack on a message to notify the user that they've hit this edgecase and encourage them to address it.
*/
close(interceptor.emergencyBailout)
content = <-interceptor.interceptedContent + BAILOUT_MESSAGE
}
interceptor.accumulatedOutput += content
interceptor.intercepting = false
}
func (interceptor *genericOutputInterceptor) Shutdown() {
interceptor.PauseIntercepting()
if interceptor.stdoutClone != nil {
close(interceptor.shutdown)
interceptor.implementation.ShutdownClones(interceptor.stdoutClone, interceptor.stderrClone)
interceptor.stdoutClone = nil
interceptor.stderrClone = nil
}
}
/* This is used on windows builds but included here so it can be explicitly tested on unix systems too */
func NewOSGlobalReassigningOutputInterceptor() OutputInterceptor {
return &genericOutputInterceptor{
interceptedContent: make(chan string),
pipeChannel: make(chan pipePair),
shutdown: make(chan any),
implementation: &osGlobalReassigningOutputInterceptorImpl{},
}
}
type osGlobalReassigningOutputInterceptorImpl struct{}
func (impl *osGlobalReassigningOutputInterceptorImpl) CreateStdoutStderrClones() (*os.File, *os.File) {
return os.Stdout, os.Stderr
}
func (impl *osGlobalReassigningOutputInterceptorImpl) ConnectPipeToStdoutStderr(pipeWriter *os.File) {
os.Stdout = pipeWriter
os.Stderr = pipeWriter
}
func (impl *osGlobalReassigningOutputInterceptorImpl) RestoreStdoutStderrFromClones(stdoutClone *os.File, stderrClone *os.File) {
os.Stdout = stdoutClone
os.Stderr = stderrClone
}
func (impl *osGlobalReassigningOutputInterceptorImpl) ShutdownClones(_ *os.File, _ *os.File) {
//noop
}
@@ -0,0 +1,73 @@
//go:build freebsd || openbsd || netbsd || dragonfly || darwin || linux || solaris
// +build freebsd openbsd netbsd dragonfly darwin linux solaris
package internal
import (
"os"
"golang.org/x/sys/unix"
)
func NewOutputInterceptor() OutputInterceptor {
return &genericOutputInterceptor{
interceptedContent: make(chan string),
pipeChannel: make(chan pipePair),
shutdown: make(chan any),
implementation: &dupSyscallOutputInterceptorImpl{},
}
}
type dupSyscallOutputInterceptorImpl struct{}
func (impl *dupSyscallOutputInterceptorImpl) CreateStdoutStderrClones() (*os.File, *os.File) {
// To clone stdout and stderr we:
// First, create two clone file descriptors that point to the stdout and stderr file descriptions
stdoutCloneFD, _ := unix.Dup(1)
stderrCloneFD, _ := unix.Dup(2)
// Important, set the fds to FD_CLOEXEC to prevent them leaking into childs
// https://github.com/onsi/ginkgo/issues/1191
flags, err := unix.FcntlInt(uintptr(stdoutCloneFD), unix.F_GETFD, 0)
if err == nil {
unix.FcntlInt(uintptr(stdoutCloneFD), unix.F_SETFD, flags|unix.FD_CLOEXEC)
}
flags, err = unix.FcntlInt(uintptr(stderrCloneFD), unix.F_GETFD, 0)
if err == nil {
unix.FcntlInt(uintptr(stderrCloneFD), unix.F_SETFD, flags|unix.FD_CLOEXEC)
}
// And then wrap the clone file descriptors in files.
// One benefit of this (that we don't use yet) is that we can actually write
// to these files to emit output to the console even though we're intercepting output
stdoutClone := os.NewFile(uintptr(stdoutCloneFD), "stdout-clone")
stderrClone := os.NewFile(uintptr(stderrCloneFD), "stderr-clone")
//these clones remain alive throughout the lifecycle of the suite and don't need to be recreated
//this speeds things up a bit, actually.
return stdoutClone, stderrClone
}
func (impl *dupSyscallOutputInterceptorImpl) ConnectPipeToStdoutStderr(pipeWriter *os.File) {
// To redirect output to our pipe we need to point the 1 and 2 file descriptors (which is how the world tries to log things)
// to the write end of the pipe.
// We do this with Dup2 (possibly Dup3 on some architectures) to have file descriptors 1 and 2 point to the same file description as the pipeWriter
// This effectively shunts data written to stdout and stderr to the write end of our pipe
unix.Dup2(int(pipeWriter.Fd()), 1)
unix.Dup2(int(pipeWriter.Fd()), 2)
}
func (impl *dupSyscallOutputInterceptorImpl) RestoreStdoutStderrFromClones(stdoutClone *os.File, stderrClone *os.File) {
// To restore stdour/stderr from the clones we have the 1 and 2 file descriptors
// point to the original file descriptions that we saved off in the clones.
// This has the added benefit of closing the connection between these descriptors and the write end of the pipe
// which is important to cause the io.Copy on the pipe.Reader to end.
unix.Dup2(int(stdoutClone.Fd()), 1)
unix.Dup2(int(stderrClone.Fd()), 2)
}
func (impl *dupSyscallOutputInterceptorImpl) ShutdownClones(stdoutClone *os.File, stderrClone *os.File) {
// We're done with the clones so we can close them to clean up after ourselves
stdoutClone.Close()
stderrClone.Close()
}
@@ -0,0 +1,7 @@
//go:build wasm
package internal
func NewOutputInterceptor() OutputInterceptor {
return &NoopOutputInterceptor{}
}
@@ -0,0 +1,7 @@
// +build windows
package internal
func NewOutputInterceptor() OutputInterceptor {
return NewOSGlobalReassigningOutputInterceptor()
}
@@ -0,0 +1,72 @@
package parallel_support
import (
"fmt"
"io"
"os"
"time"
"github.com/onsi/ginkgo/v2/reporters"
"github.com/onsi/ginkgo/v2/types"
)
type BeforeSuiteState struct {
Data []byte
State types.SpecState
}
type ParallelIndexCounter struct {
Index int
}
var ErrorGone = fmt.Errorf("gone")
var ErrorFailed = fmt.Errorf("failed")
var ErrorEarly = fmt.Errorf("early")
var POLLING_INTERVAL = 50 * time.Millisecond
type Server interface {
Start()
Close()
Address() string
RegisterAlive(node int, alive func() bool)
GetSuiteDone() chan any
GetOutputDestination() io.Writer
SetOutputDestination(io.Writer)
}
type Client interface {
Connect() bool
Close() error
PostSuiteWillBegin(report types.Report) error
PostDidRun(report types.SpecReport) error
PostSuiteDidEnd(report types.Report) error
PostReportBeforeSuiteCompleted(state types.SpecState) error
BlockUntilReportBeforeSuiteCompleted() (types.SpecState, error)
PostSynchronizedBeforeSuiteCompleted(state types.SpecState, data []byte) error
BlockUntilSynchronizedBeforeSuiteData() (types.SpecState, []byte, error)
BlockUntilNonprimaryProcsHaveFinished() error
BlockUntilAggregatedNonprimaryProcsReport() (types.Report, error)
FetchNextCounter() (int, error)
PostAbort() error
ShouldAbort() bool
PostEmitProgressReport(report types.ProgressReport) error
Write(p []byte) (int, error)
}
func NewServer(parallelTotal int, reporter reporters.Reporter) (Server, error) {
if os.Getenv("GINKGO_PARALLEL_PROTOCOL") == "HTTP" {
return newHttpServer(parallelTotal, reporter)
} else {
return newRPCServer(parallelTotal, reporter)
}
}
func NewClient(serverHost string) Client {
if os.Getenv("GINKGO_PARALLEL_PROTOCOL") == "HTTP" {
return newHttpClient(serverHost)
} else {
return newRPCClient(serverHost)
}
}
@@ -0,0 +1,166 @@
package parallel_support
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/onsi/ginkgo/v2/types"
)
type httpClient struct {
serverHost string
}
func newHttpClient(serverHost string) *httpClient {
return &httpClient{
serverHost: serverHost,
}
}
func (client *httpClient) Connect() bool {
resp, err := http.Get(client.serverHost + "/up")
if err != nil {
return false
}
resp.Body.Close()
return resp.StatusCode == http.StatusOK
}
func (client *httpClient) Close() error {
return nil
}
func (client *httpClient) post(path string, data any) error {
var body io.Reader
if data != nil {
encoded, err := json.Marshal(data)
if err != nil {
return err
}
body = bytes.NewBuffer(encoded)
}
resp, err := http.Post(client.serverHost+path, "application/json", body)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("received unexpected status code %d", resp.StatusCode)
}
return nil
}
func (client *httpClient) poll(path string, data any) error {
for {
resp, err := http.Get(client.serverHost + path)
if err != nil {
return err
}
if resp.StatusCode == http.StatusTooEarly {
resp.Body.Close()
time.Sleep(POLLING_INTERVAL)
continue
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusGone {
return ErrorGone
}
if resp.StatusCode == http.StatusFailedDependency {
return ErrorFailed
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("received unexpected status code %d", resp.StatusCode)
}
if data != nil {
return json.NewDecoder(resp.Body).Decode(data)
}
return nil
}
}
func (client *httpClient) PostSuiteWillBegin(report types.Report) error {
return client.post("/suite-will-begin", report)
}
func (client *httpClient) PostDidRun(report types.SpecReport) error {
return client.post("/did-run", report)
}
func (client *httpClient) PostSuiteDidEnd(report types.Report) error {
return client.post("/suite-did-end", report)
}
func (client *httpClient) PostEmitProgressReport(report types.ProgressReport) error {
return client.post("/progress-report", report)
}
func (client *httpClient) PostReportBeforeSuiteCompleted(state types.SpecState) error {
return client.post("/report-before-suite-completed", state)
}
func (client *httpClient) BlockUntilReportBeforeSuiteCompleted() (types.SpecState, error) {
var state types.SpecState
err := client.poll("/report-before-suite-state", &state)
if err == ErrorGone {
return types.SpecStateFailed, nil
}
return state, err
}
func (client *httpClient) PostSynchronizedBeforeSuiteCompleted(state types.SpecState, data []byte) error {
beforeSuiteState := BeforeSuiteState{
State: state,
Data: data,
}
return client.post("/before-suite-completed", beforeSuiteState)
}
func (client *httpClient) BlockUntilSynchronizedBeforeSuiteData() (types.SpecState, []byte, error) {
var beforeSuiteState BeforeSuiteState
err := client.poll("/before-suite-state", &beforeSuiteState)
if err == ErrorGone {
return types.SpecStateInvalid, nil, types.GinkgoErrors.SynchronizedBeforeSuiteDisappearedOnProc1()
}
return beforeSuiteState.State, beforeSuiteState.Data, err
}
func (client *httpClient) BlockUntilNonprimaryProcsHaveFinished() error {
return client.poll("/have-nonprimary-procs-finished", nil)
}
func (client *httpClient) BlockUntilAggregatedNonprimaryProcsReport() (types.Report, error) {
var report types.Report
err := client.poll("/aggregated-nonprimary-procs-report", &report)
if err == ErrorGone {
return types.Report{}, types.GinkgoErrors.AggregatedReportUnavailableDueToNodeDisappearing()
}
return report, err
}
func (client *httpClient) FetchNextCounter() (int, error) {
var counter ParallelIndexCounter
err := client.poll("/counter", &counter)
return counter.Index, err
}
func (client *httpClient) PostAbort() error {
return client.post("/abort", nil)
}
func (client *httpClient) ShouldAbort() bool {
err := client.poll("/abort", nil)
return err == ErrorGone
}
func (client *httpClient) Write(p []byte) (int, error) {
resp, err := http.Post(client.serverHost+"/emit-output", "text/plain;charset=UTF-8 ", bytes.NewReader(p))
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return 0, fmt.Errorf("failed to emit output")
}
return len(p), err
}
@@ -0,0 +1,242 @@
/*
The remote package provides the pieces to allow Ginkgo test suites to report to remote listeners.
This is used, primarily, to enable streaming parallel test output but has, in principal, broader applications (e.g. streaming test output to a browser).
*/
package parallel_support
import (
"encoding/json"
"io"
"net"
"net/http"
"github.com/onsi/ginkgo/v2/reporters"
"github.com/onsi/ginkgo/v2/types"
)
/*
httpServer spins up on an automatically selected port and listens for communication from the forwarding reporter.
It then forwards that communication to attached reporters.
*/
type httpServer struct {
listener net.Listener
handler *ServerHandler
}
// Create a new server, automatically selecting a port
func newHttpServer(parallelTotal int, reporter reporters.Reporter) (*httpServer, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
return &httpServer{
listener: listener,
handler: newServerHandler(parallelTotal, reporter),
}, nil
}
// Start the server. You don't need to `go s.Start()`, just `s.Start()`
func (server *httpServer) Start() {
httpServer := &http.Server{}
mux := http.NewServeMux()
httpServer.Handler = mux
//streaming endpoints
mux.HandleFunc("/suite-will-begin", server.specSuiteWillBegin)
mux.HandleFunc("/did-run", server.didRun)
mux.HandleFunc("/suite-did-end", server.specSuiteDidEnd)
mux.HandleFunc("/emit-output", server.emitOutput)
mux.HandleFunc("/progress-report", server.emitProgressReport)
//synchronization endpoints
mux.HandleFunc("/report-before-suite-completed", server.handleReportBeforeSuiteCompleted)
mux.HandleFunc("/report-before-suite-state", server.handleReportBeforeSuiteState)
mux.HandleFunc("/before-suite-completed", server.handleBeforeSuiteCompleted)
mux.HandleFunc("/before-suite-state", server.handleBeforeSuiteState)
mux.HandleFunc("/have-nonprimary-procs-finished", server.handleHaveNonprimaryProcsFinished)
mux.HandleFunc("/aggregated-nonprimary-procs-report", server.handleAggregatedNonprimaryProcsReport)
mux.HandleFunc("/counter", server.handleCounter)
mux.HandleFunc("/up", server.handleUp)
mux.HandleFunc("/abort", server.handleAbort)
go httpServer.Serve(server.listener)
}
// Stop the server
func (server *httpServer) Close() {
server.listener.Close()
}
// The address the server can be reached it. Pass this into the `ForwardingReporter`.
func (server *httpServer) Address() string {
return "http://" + server.listener.Addr().String()
}
func (server *httpServer) GetSuiteDone() chan any {
return server.handler.done
}
func (server *httpServer) GetOutputDestination() io.Writer {
return server.handler.outputDestination
}
func (server *httpServer) SetOutputDestination(w io.Writer) {
server.handler.outputDestination = w
}
func (server *httpServer) RegisterAlive(node int, alive func() bool) {
server.handler.registerAlive(node, alive)
}
//
// Streaming Endpoints
//
// The server will forward all received messages to Ginkgo reporters registered with `RegisterReporters`
func (server *httpServer) decode(writer http.ResponseWriter, request *http.Request, object any) bool {
defer request.Body.Close()
if json.NewDecoder(request.Body).Decode(object) != nil {
writer.WriteHeader(http.StatusBadRequest)
return false
}
return true
}
func (server *httpServer) handleError(err error, writer http.ResponseWriter) bool {
if err == nil {
return false
}
switch err {
case ErrorEarly:
writer.WriteHeader(http.StatusTooEarly)
case ErrorGone:
writer.WriteHeader(http.StatusGone)
case ErrorFailed:
writer.WriteHeader(http.StatusFailedDependency)
default:
writer.WriteHeader(http.StatusInternalServerError)
}
return true
}
func (server *httpServer) specSuiteWillBegin(writer http.ResponseWriter, request *http.Request) {
var report types.Report
if !server.decode(writer, request, &report) {
return
}
server.handleError(server.handler.SpecSuiteWillBegin(report, voidReceiver), writer)
}
func (server *httpServer) didRun(writer http.ResponseWriter, request *http.Request) {
var report types.SpecReport
if !server.decode(writer, request, &report) {
return
}
server.handleError(server.handler.DidRun(report, voidReceiver), writer)
}
func (server *httpServer) specSuiteDidEnd(writer http.ResponseWriter, request *http.Request) {
var report types.Report
if !server.decode(writer, request, &report) {
return
}
server.handleError(server.handler.SpecSuiteDidEnd(report, voidReceiver), writer)
}
func (server *httpServer) emitOutput(writer http.ResponseWriter, request *http.Request) {
output, err := io.ReadAll(request.Body)
if err != nil {
writer.WriteHeader(http.StatusInternalServerError)
return
}
var n int
server.handleError(server.handler.EmitOutput(output, &n), writer)
}
func (server *httpServer) emitProgressReport(writer http.ResponseWriter, request *http.Request) {
var report types.ProgressReport
if !server.decode(writer, request, &report) {
return
}
server.handleError(server.handler.EmitProgressReport(report, voidReceiver), writer)
}
func (server *httpServer) handleReportBeforeSuiteCompleted(writer http.ResponseWriter, request *http.Request) {
var state types.SpecState
if !server.decode(writer, request, &state) {
return
}
server.handleError(server.handler.ReportBeforeSuiteCompleted(state, voidReceiver), writer)
}
func (server *httpServer) handleReportBeforeSuiteState(writer http.ResponseWriter, request *http.Request) {
var state types.SpecState
if server.handleError(server.handler.ReportBeforeSuiteState(voidSender, &state), writer) {
return
}
json.NewEncoder(writer).Encode(state)
}
func (server *httpServer) handleBeforeSuiteCompleted(writer http.ResponseWriter, request *http.Request) {
var beforeSuiteState BeforeSuiteState
if !server.decode(writer, request, &beforeSuiteState) {
return
}
server.handleError(server.handler.BeforeSuiteCompleted(beforeSuiteState, voidReceiver), writer)
}
func (server *httpServer) handleBeforeSuiteState(writer http.ResponseWriter, request *http.Request) {
var beforeSuiteState BeforeSuiteState
if server.handleError(server.handler.BeforeSuiteState(voidSender, &beforeSuiteState), writer) {
return
}
json.NewEncoder(writer).Encode(beforeSuiteState)
}
func (server *httpServer) handleHaveNonprimaryProcsFinished(writer http.ResponseWriter, request *http.Request) {
if server.handleError(server.handler.HaveNonprimaryProcsFinished(voidSender, voidReceiver), writer) {
return
}
writer.WriteHeader(http.StatusOK)
}
func (server *httpServer) handleAggregatedNonprimaryProcsReport(writer http.ResponseWriter, request *http.Request) {
var aggregatedReport types.Report
if server.handleError(server.handler.AggregatedNonprimaryProcsReport(voidSender, &aggregatedReport), writer) {
return
}
json.NewEncoder(writer).Encode(aggregatedReport)
}
func (server *httpServer) handleCounter(writer http.ResponseWriter, request *http.Request) {
var n int
if server.handleError(server.handler.Counter(voidSender, &n), writer) {
return
}
json.NewEncoder(writer).Encode(ParallelIndexCounter{Index: n})
}
func (server *httpServer) handleUp(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusOK)
}
func (server *httpServer) handleAbort(writer http.ResponseWriter, request *http.Request) {
if request.Method == "GET" {
var shouldAbort bool
server.handler.ShouldAbort(voidSender, &shouldAbort)
if shouldAbort {
writer.WriteHeader(http.StatusGone)
} else {
writer.WriteHeader(http.StatusOK)
}
} else {
server.handler.Abort(voidSender, voidReceiver)
}
}
@@ -0,0 +1,136 @@
package parallel_support
import (
"net/rpc"
"time"
"github.com/onsi/ginkgo/v2/types"
)
type rpcClient struct {
serverHost string
client *rpc.Client
}
func newRPCClient(serverHost string) *rpcClient {
return &rpcClient{
serverHost: serverHost,
}
}
func (client *rpcClient) Connect() bool {
var err error
if client.client != nil {
return true
}
client.client, err = rpc.DialHTTPPath("tcp", client.serverHost, "/")
if err != nil {
client.client = nil
return false
}
return true
}
func (client *rpcClient) Close() error {
return client.client.Close()
}
func (client *rpcClient) poll(method string, data any) error {
for {
err := client.client.Call(method, voidSender, data)
if err == nil {
return nil
}
switch err.Error() {
case ErrorEarly.Error():
time.Sleep(POLLING_INTERVAL)
case ErrorGone.Error():
return ErrorGone
case ErrorFailed.Error():
return ErrorFailed
default:
return err
}
}
}
func (client *rpcClient) PostSuiteWillBegin(report types.Report) error {
return client.client.Call("Server.SpecSuiteWillBegin", report, voidReceiver)
}
func (client *rpcClient) PostDidRun(report types.SpecReport) error {
return client.client.Call("Server.DidRun", report, voidReceiver)
}
func (client *rpcClient) PostSuiteDidEnd(report types.Report) error {
return client.client.Call("Server.SpecSuiteDidEnd", report, voidReceiver)
}
func (client *rpcClient) Write(p []byte) (int, error) {
var n int
err := client.client.Call("Server.EmitOutput", p, &n)
return n, err
}
func (client *rpcClient) PostEmitProgressReport(report types.ProgressReport) error {
return client.client.Call("Server.EmitProgressReport", report, voidReceiver)
}
func (client *rpcClient) PostReportBeforeSuiteCompleted(state types.SpecState) error {
return client.client.Call("Server.ReportBeforeSuiteCompleted", state, voidReceiver)
}
func (client *rpcClient) BlockUntilReportBeforeSuiteCompleted() (types.SpecState, error) {
var state types.SpecState
err := client.poll("Server.ReportBeforeSuiteState", &state)
if err == ErrorGone {
return types.SpecStateFailed, nil
}
return state, err
}
func (client *rpcClient) PostSynchronizedBeforeSuiteCompleted(state types.SpecState, data []byte) error {
beforeSuiteState := BeforeSuiteState{
State: state,
Data: data,
}
return client.client.Call("Server.BeforeSuiteCompleted", beforeSuiteState, voidReceiver)
}
func (client *rpcClient) BlockUntilSynchronizedBeforeSuiteData() (types.SpecState, []byte, error) {
var beforeSuiteState BeforeSuiteState
err := client.poll("Server.BeforeSuiteState", &beforeSuiteState)
if err == ErrorGone {
return types.SpecStateInvalid, nil, types.GinkgoErrors.SynchronizedBeforeSuiteDisappearedOnProc1()
}
return beforeSuiteState.State, beforeSuiteState.Data, err
}
func (client *rpcClient) BlockUntilNonprimaryProcsHaveFinished() error {
return client.poll("Server.HaveNonprimaryProcsFinished", voidReceiver)
}
func (client *rpcClient) BlockUntilAggregatedNonprimaryProcsReport() (types.Report, error) {
var report types.Report
err := client.poll("Server.AggregatedNonprimaryProcsReport", &report)
if err == ErrorGone {
return types.Report{}, types.GinkgoErrors.AggregatedReportUnavailableDueToNodeDisappearing()
}
return report, err
}
func (client *rpcClient) FetchNextCounter() (int, error) {
var counter int
err := client.client.Call("Server.Counter", voidSender, &counter)
return counter, err
}
func (client *rpcClient) PostAbort() error {
return client.client.Call("Server.Abort", voidSender, voidReceiver)
}
func (client *rpcClient) ShouldAbort() bool {
var shouldAbort bool
client.client.Call("Server.ShouldAbort", voidSender, &shouldAbort)
return shouldAbort
}
@@ -0,0 +1,75 @@
/*
The remote package provides the pieces to allow Ginkgo test suites to report to remote listeners.
This is used, primarily, to enable streaming parallel test output but has, in principal, broader applications (e.g. streaming test output to a browser).
*/
package parallel_support
import (
"io"
"net"
"net/http"
"net/rpc"
"github.com/onsi/ginkgo/v2/reporters"
)
/*
RPCServer spins up on an automatically selected port and listens for communication from the forwarding reporter.
It then forwards that communication to attached reporters.
*/
type RPCServer struct {
listener net.Listener
handler *ServerHandler
}
// Create a new server, automatically selecting a port
func newRPCServer(parallelTotal int, reporter reporters.Reporter) (*RPCServer, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
return &RPCServer{
listener: listener,
handler: newServerHandler(parallelTotal, reporter),
}, nil
}
// Start the server. You don't need to `go s.Start()`, just `s.Start()`
func (server *RPCServer) Start() {
rpcServer := rpc.NewServer()
rpcServer.RegisterName("Server", server.handler) //register the handler's methods as the server
httpServer := &http.Server{}
httpServer.Handler = rpcServer
go httpServer.Serve(server.listener)
}
// Stop the server
func (server *RPCServer) Close() {
server.listener.Close()
}
// The address the server can be reached it. Pass this into the `ForwardingReporter`.
func (server *RPCServer) Address() string {
return server.listener.Addr().String()
}
func (server *RPCServer) GetSuiteDone() chan any {
return server.handler.done
}
func (server *RPCServer) GetOutputDestination() io.Writer {
return server.handler.outputDestination
}
func (server *RPCServer) SetOutputDestination(w io.Writer) {
server.handler.outputDestination = w
}
func (server *RPCServer) RegisterAlive(node int, alive func() bool) {
server.handler.registerAlive(node, alive)
}
@@ -0,0 +1,234 @@
package parallel_support
import (
"io"
"os"
"sync"
"github.com/onsi/ginkgo/v2/reporters"
"github.com/onsi/ginkgo/v2/types"
)
type Void struct{}
var voidReceiver *Void = &Void{}
var voidSender Void
// ServerHandler is an RPC-compatible handler that is shared between the http server and the rpc server.
// It handles all the business logic to avoid duplication between the two servers
type ServerHandler struct {
done chan any
outputDestination io.Writer
reporter reporters.Reporter
alives []func() bool
lock *sync.Mutex
beforeSuiteState BeforeSuiteState
reportBeforeSuiteState types.SpecState
parallelTotal int
counter int
counterLock *sync.Mutex
shouldAbort bool
numSuiteDidBegins int
numSuiteDidEnds int
aggregatedReport types.Report
reportHoldingArea []types.SpecReport
}
func newServerHandler(parallelTotal int, reporter reporters.Reporter) *ServerHandler {
return &ServerHandler{
reporter: reporter,
lock: &sync.Mutex{},
counterLock: &sync.Mutex{},
alives: make([]func() bool, parallelTotal),
beforeSuiteState: BeforeSuiteState{Data: nil, State: types.SpecStateInvalid},
parallelTotal: parallelTotal,
outputDestination: os.Stdout,
done: make(chan any),
}
}
func (handler *ServerHandler) SpecSuiteWillBegin(report types.Report, _ *Void) error {
handler.lock.Lock()
defer handler.lock.Unlock()
handler.numSuiteDidBegins += 1
// all summaries are identical, so it's fine to simply emit the last one of these
if handler.numSuiteDidBegins == handler.parallelTotal {
handler.reporter.SuiteWillBegin(report)
for _, summary := range handler.reportHoldingArea {
handler.reporter.WillRun(summary)
handler.reporter.DidRun(summary)
}
handler.reportHoldingArea = nil
}
return nil
}
func (handler *ServerHandler) DidRun(report types.SpecReport, _ *Void) error {
handler.lock.Lock()
defer handler.lock.Unlock()
if handler.numSuiteDidBegins == handler.parallelTotal {
handler.reporter.WillRun(report)
handler.reporter.DidRun(report)
} else {
handler.reportHoldingArea = append(handler.reportHoldingArea, report)
}
return nil
}
func (handler *ServerHandler) SpecSuiteDidEnd(report types.Report, _ *Void) error {
handler.lock.Lock()
defer handler.lock.Unlock()
handler.numSuiteDidEnds += 1
if handler.numSuiteDidEnds == 1 {
handler.aggregatedReport = report
} else {
handler.aggregatedReport = handler.aggregatedReport.Add(report)
}
if handler.numSuiteDidEnds == handler.parallelTotal {
handler.reporter.SuiteDidEnd(handler.aggregatedReport)
close(handler.done)
}
return nil
}
func (handler *ServerHandler) EmitOutput(output []byte, n *int) error {
var err error
*n, err = handler.outputDestination.Write(output)
return err
}
func (handler *ServerHandler) EmitProgressReport(report types.ProgressReport, _ *Void) error {
handler.lock.Lock()
defer handler.lock.Unlock()
handler.reporter.EmitProgressReport(report)
return nil
}
func (handler *ServerHandler) registerAlive(proc int, alive func() bool) {
handler.lock.Lock()
defer handler.lock.Unlock()
handler.alives[proc-1] = alive
}
func (handler *ServerHandler) procIsAlive(proc int) bool {
handler.lock.Lock()
defer handler.lock.Unlock()
alive := handler.alives[proc-1]
if alive == nil {
return true
}
return alive()
}
func (handler *ServerHandler) haveNonprimaryProcsFinished() bool {
for i := 2; i <= handler.parallelTotal; i++ {
if handler.procIsAlive(i) {
return false
}
}
return true
}
func (handler *ServerHandler) ReportBeforeSuiteCompleted(reportBeforeSuiteState types.SpecState, _ *Void) error {
handler.lock.Lock()
defer handler.lock.Unlock()
handler.reportBeforeSuiteState = reportBeforeSuiteState
return nil
}
func (handler *ServerHandler) ReportBeforeSuiteState(_ Void, reportBeforeSuiteState *types.SpecState) error {
proc1IsAlive := handler.procIsAlive(1)
handler.lock.Lock()
defer handler.lock.Unlock()
if handler.reportBeforeSuiteState == types.SpecStateInvalid {
if proc1IsAlive {
return ErrorEarly
} else {
return ErrorGone
}
}
*reportBeforeSuiteState = handler.reportBeforeSuiteState
return nil
}
func (handler *ServerHandler) BeforeSuiteCompleted(beforeSuiteState BeforeSuiteState, _ *Void) error {
handler.lock.Lock()
defer handler.lock.Unlock()
handler.beforeSuiteState = beforeSuiteState
return nil
}
func (handler *ServerHandler) BeforeSuiteState(_ Void, beforeSuiteState *BeforeSuiteState) error {
proc1IsAlive := handler.procIsAlive(1)
handler.lock.Lock()
defer handler.lock.Unlock()
if handler.beforeSuiteState.State == types.SpecStateInvalid {
if proc1IsAlive {
return ErrorEarly
} else {
return ErrorGone
}
}
*beforeSuiteState = handler.beforeSuiteState
return nil
}
func (handler *ServerHandler) HaveNonprimaryProcsFinished(_ Void, _ *Void) error {
if handler.haveNonprimaryProcsFinished() {
return nil
} else {
return ErrorEarly
}
}
func (handler *ServerHandler) AggregatedNonprimaryProcsReport(_ Void, report *types.Report) error {
if handler.haveNonprimaryProcsFinished() {
handler.lock.Lock()
defer handler.lock.Unlock()
if handler.numSuiteDidEnds == handler.parallelTotal-1 {
*report = handler.aggregatedReport
return nil
} else {
return ErrorGone
}
} else {
return ErrorEarly
}
}
func (handler *ServerHandler) Counter(_ Void, counter *int) error {
handler.counterLock.Lock()
defer handler.counterLock.Unlock()
*counter = handler.counter
handler.counter++
return nil
}
func (handler *ServerHandler) Abort(_ Void, _ *Void) error {
handler.lock.Lock()
defer handler.lock.Unlock()
handler.shouldAbort = true
return nil
}
func (handler *ServerHandler) ShouldAbort(_ Void, shouldAbort *bool) error {
handler.lock.Lock()
defer handler.lock.Unlock()
*shouldAbort = handler.shouldAbort
return nil
}
+287
View File
@@ -0,0 +1,287 @@
package internal
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"github.com/onsi/ginkgo/v2/types"
)
var _SOURCE_CACHE = map[string][]string{}
type ProgressSignalRegistrar func(func()) context.CancelFunc
func RegisterForProgressSignal(handler func()) context.CancelFunc {
signalChannel := make(chan os.Signal, 1)
if len(PROGRESS_SIGNALS) > 0 {
signal.Notify(signalChannel, PROGRESS_SIGNALS...)
}
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-signalChannel:
handler()
case <-ctx.Done():
signal.Stop(signalChannel)
return
}
}
}()
return cancel
}
type ProgressStepCursor struct {
Text string
CodeLocation types.CodeLocation
StartTime time.Time
}
func NewProgressReport(isRunningInParallel bool, report types.SpecReport, currentNode Node, currentNodeStartTime time.Time, currentStep types.SpecEvent, gwOutput string, timelineLocation types.TimelineLocation, additionalReports []string, sourceRoots []string, includeAll bool) (types.ProgressReport, error) {
pr := types.ProgressReport{
ParallelProcess: report.ParallelProcess,
RunningInParallel: isRunningInParallel,
ContainerHierarchyTexts: report.ContainerHierarchyTexts,
LeafNodeText: report.LeafNodeText,
LeafNodeLocation: report.LeafNodeLocation,
SpecStartTime: report.StartTime,
CurrentNodeType: currentNode.NodeType,
CurrentNodeText: currentNode.Text,
CurrentNodeLocation: currentNode.CodeLocation,
CurrentNodeStartTime: currentNodeStartTime,
CurrentStepText: currentStep.Message,
CurrentStepLocation: currentStep.CodeLocation,
CurrentStepStartTime: currentStep.TimelineLocation.Time,
AdditionalReports: additionalReports,
CapturedGinkgoWriterOutput: gwOutput,
TimelineLocation: timelineLocation,
}
goroutines, err := extractRunningGoroutines()
if err != nil {
return pr, err
}
pr.Goroutines = goroutines
// now we want to try to find goroutines of interest. these will be goroutines that have any function calls with code in packagesOfInterest:
packagesOfInterest := map[string]bool{}
packageFromFilename := func(filename string) string {
return filepath.Dir(filename)
}
addPackageFor := func(filename string) {
if filename != "" {
packagesOfInterest[packageFromFilename(filename)] = true
}
}
isPackageOfInterest := func(filename string) bool {
stackPackage := packageFromFilename(filename)
for packageOfInterest := range packagesOfInterest {
if strings.HasPrefix(stackPackage, packageOfInterest) {
return true
}
}
return false
}
for _, location := range report.ContainerHierarchyLocations {
addPackageFor(location.FileName)
}
addPackageFor(report.LeafNodeLocation.FileName)
addPackageFor(currentNode.CodeLocation.FileName)
addPackageFor(currentStep.CodeLocation.FileName)
//First, we find the SpecGoroutine - this will be the goroutine that includes `runNode`
specGoRoutineIdx := -1
runNodeFunctionCallIdx := -1
OUTER:
for goroutineIdx, goroutine := range pr.Goroutines {
for functionCallIdx, functionCall := range goroutine.Stack {
if strings.Contains(functionCall.Function, "ginkgo/v2/internal.(*Suite).runNode.func") {
specGoRoutineIdx = goroutineIdx
runNodeFunctionCallIdx = functionCallIdx
break OUTER
}
}
}
//Now, we find the first non-Ginkgo function call
if specGoRoutineIdx > -1 {
for runNodeFunctionCallIdx >= 0 {
fn := goroutines[specGoRoutineIdx].Stack[runNodeFunctionCallIdx].Function
file := goroutines[specGoRoutineIdx].Stack[runNodeFunctionCallIdx].Filename
// these are all things that could potentially happen from within ginkgo
if strings.Contains(fn, "ginkgo/v2/internal") || strings.Contains(fn, "reflect.Value") || strings.Contains(file, "ginkgo/table_dsl") || strings.Contains(file, "ginkgo/core_dsl") {
runNodeFunctionCallIdx--
continue
}
if strings.Contains(goroutines[specGoRoutineIdx].Stack[runNodeFunctionCallIdx].Function, "ginkgo/table_dsl") {
}
//found it! lets add its package of interest
addPackageFor(goroutines[specGoRoutineIdx].Stack[runNodeFunctionCallIdx].Filename)
break
}
}
ginkgoEntryPointIdx := -1
OUTER_GINKGO_ENTRY_POINT:
for goroutineIdx, goroutine := range pr.Goroutines {
for _, functionCall := range goroutine.Stack {
if strings.Contains(functionCall.Function, "ginkgo/v2.RunSpecs") {
ginkgoEntryPointIdx = goroutineIdx
break OUTER_GINKGO_ENTRY_POINT
}
}
}
// Now we go through all goroutines and highlight any lines with packages in `packagesOfInterest`
// Any goroutines with highlighted lines end up in the HighlightGoRoutines
for goroutineIdx, goroutine := range pr.Goroutines {
if goroutineIdx == ginkgoEntryPointIdx {
continue
}
if goroutineIdx == specGoRoutineIdx {
pr.Goroutines[goroutineIdx].IsSpecGoroutine = true
}
for functionCallIdx, functionCall := range goroutine.Stack {
if isPackageOfInterest(functionCall.Filename) {
goroutine.Stack[functionCallIdx].Highlight = true
goroutine.Stack[functionCallIdx].Source, goroutine.Stack[functionCallIdx].SourceHighlight = fetchSource(functionCall.Filename, functionCall.Line, 2, sourceRoots)
}
}
}
if !includeAll {
goroutines := []types.Goroutine{pr.SpecGoroutine()}
goroutines = append(goroutines, pr.HighlightedGoroutines()...)
pr.Goroutines = goroutines
}
return pr, nil
}
func extractRunningGoroutines() ([]types.Goroutine, error) {
var stack []byte
for size := 64 * 1024; ; size *= 2 {
stack = make([]byte, size)
if n := runtime.Stack(stack, true); n < size {
stack = stack[:n]
break
}
}
r := bufio.NewReader(bytes.NewReader(stack))
out := []types.Goroutine{}
idx := -1
for {
line, err := r.ReadString('\n')
if err == io.EOF {
break
}
line = strings.TrimSuffix(line, "\n")
//skip blank lines
if line == "" {
continue
}
//parse headers for new goroutine frames
if strings.HasPrefix(line, "goroutine") {
out = append(out, types.Goroutine{})
idx = len(out) - 1
line = strings.TrimPrefix(line, "goroutine ")
line = strings.TrimSuffix(line, ":")
fields := strings.SplitN(line, " ", 2)
if len(fields) != 2 {
return nil, types.GinkgoErrors.FailedToParseStackTrace(fmt.Sprintf("Invalid goroutine frame header: %s", line))
}
out[idx].ID, err = strconv.ParseUint(fields[0], 10, 64)
if err != nil {
return nil, types.GinkgoErrors.FailedToParseStackTrace(fmt.Sprintf("Invalid goroutine ID: %s", fields[1]))
}
out[idx].State = strings.TrimSuffix(strings.TrimPrefix(fields[1], "["), "]")
continue
}
//if we are here we must be at a function call entry in the stack
functionCall := types.FunctionCall{
Function: strings.TrimPrefix(line, "created by "), // no need to track 'created by'
}
line, err = r.ReadString('\n')
line = strings.TrimSuffix(line, "\n")
if err == io.EOF {
return nil, types.GinkgoErrors.FailedToParseStackTrace(fmt.Sprintf("Invalid function call: %s -- missing file name and line number", functionCall.Function))
}
line = strings.TrimLeft(line, " \t")
delimiterIdx := strings.LastIndex(line, ":")
if delimiterIdx == -1 {
return nil, types.GinkgoErrors.FailedToParseStackTrace(fmt.Sprintf("Invalid filename and line number: %s", line))
}
functionCall.Filename = line[:delimiterIdx]
line = strings.Split(line[delimiterIdx+1:], " ")[0]
lineNumber, err := strconv.ParseInt(line, 10, 32)
functionCall.Line = int(lineNumber)
if err != nil {
return nil, types.GinkgoErrors.FailedToParseStackTrace(fmt.Sprintf("Invalid function call line number: %s\n%s", line, err.Error()))
}
out[idx].Stack = append(out[idx].Stack, functionCall)
}
return out, nil
}
func fetchSource(filename string, lineNumber int, span int, configuredSourceRoots []string) ([]string, int) {
if filename == "" {
return []string{}, 0
}
var lines []string
var ok bool
if lines, ok = _SOURCE_CACHE[filename]; !ok {
sourceRoots := []string{""}
sourceRoots = append(sourceRoots, configuredSourceRoots...)
var data []byte
var err error
var found bool
for _, root := range sourceRoots {
data, err = os.ReadFile(filepath.Join(root, filename))
if err == nil {
found = true
break
}
}
if !found {
return []string{}, 0
}
lines = strings.Split(string(data), "\n")
_SOURCE_CACHE[filename] = lines
}
startIndex := lineNumber - span - 1
endIndex := startIndex + span + span + 1
if startIndex < 0 {
startIndex = 0
}
if endIndex > len(lines) {
endIndex = len(lines)
}
highlightIndex := lineNumber - 1 - startIndex
return lines[startIndex:endIndex], highlightIndex
}
@@ -0,0 +1,11 @@
//go:build freebsd || openbsd || netbsd || darwin || dragonfly
// +build freebsd openbsd netbsd darwin dragonfly
package internal
import (
"os"
"syscall"
)
var PROGRESS_SIGNALS = []os.Signal{syscall.SIGINFO, syscall.SIGUSR1}
@@ -0,0 +1,11 @@
//go:build linux || solaris
// +build linux solaris
package internal
import (
"os"
"syscall"
)
var PROGRESS_SIGNALS = []os.Signal{syscall.SIGUSR1}
@@ -0,0 +1,10 @@
//go:build wasm
package internal
import (
"os"
"syscall"
)
var PROGRESS_SIGNALS = []os.Signal{syscall.SIGUSR1}
@@ -0,0 +1,8 @@
//go:build windows
// +build windows
package internal
import "os"
var PROGRESS_SIGNALS = []os.Signal{}
@@ -0,0 +1,79 @@
package internal
import (
"context"
"sort"
"strings"
"sync"
"github.com/onsi/ginkgo/v2/types"
)
type ProgressReporterManager struct {
lock *sync.Mutex
progressReporters map[int]func() string
prCounter int
}
func NewProgressReporterManager() *ProgressReporterManager {
return &ProgressReporterManager{
progressReporters: map[int]func() string{},
lock: &sync.Mutex{},
}
}
func (prm *ProgressReporterManager) AttachProgressReporter(reporter func() string) func() {
prm.lock.Lock()
defer prm.lock.Unlock()
prm.prCounter += 1
prCounter := prm.prCounter
prm.progressReporters[prCounter] = reporter
return func() {
prm.lock.Lock()
defer prm.lock.Unlock()
delete(prm.progressReporters, prCounter)
}
}
func (prm *ProgressReporterManager) QueryProgressReporters(ctx context.Context, failer *Failer) []string {
prm.lock.Lock()
keys := []int{}
for key := range prm.progressReporters {
keys = append(keys, key)
}
sort.Ints(keys)
reporters := []func() string{}
for _, key := range keys {
reporters = append(reporters, prm.progressReporters[key])
}
prm.lock.Unlock()
if len(reporters) == 0 {
return nil
}
out := []string{}
for _, reporter := range reporters {
reportC := make(chan string, 1)
go func() {
defer func() {
e := recover()
if e != nil {
failer.Panic(types.NewCodeLocationWithStackTrace(1), e)
reportC <- "failed to query attached progress reporter"
}
}()
reportC <- reporter()
}()
var report string
select {
case report = <-reportC:
case <-ctx.Done():
return out
}
if strings.TrimSpace(report) != "" {
out = append(out, report)
}
}
return out
}
+39
View File
@@ -0,0 +1,39 @@
package internal
import (
"time"
"github.com/onsi/ginkgo/v2/types"
)
type ReportEntry = types.ReportEntry
func NewReportEntry(name string, cl types.CodeLocation, args ...any) (ReportEntry, error) {
out := ReportEntry{
Visibility: types.ReportEntryVisibilityAlways,
Name: name,
Location: cl,
Time: time.Now(),
}
var didSetValue = false
for _, arg := range args {
switch x := arg.(type) {
case types.ReportEntryVisibility:
out.Visibility = x
case types.CodeLocation:
out.Location = x
case Offset:
out.Location = types.NewCodeLocation(2 + int(x))
case time.Time:
out.Time = x
default:
if didSetValue {
return ReportEntry{}, types.GinkgoErrors.TooManyReportEntryValues(out.Location, arg)
}
out.Value = types.WrapEntryValue(arg)
didSetValue = true
}
}
return out, nil
}
+171
View File
@@ -0,0 +1,171 @@
package reporters
import (
"errors"
"fmt"
"strings"
"time"
"github.com/onsi/ginkgo/v2/types"
"golang.org/x/tools/go/packages"
)
func ptr[T any](in T) *T {
return &in
}
type encoder interface {
Encode(v any) error
}
// gojsonEvent matches the format from go internals
// https://github.com/golang/go/blob/master/src/cmd/internal/test2json/test2json.go#L31-L41
// https://pkg.go.dev/cmd/test2json
type gojsonEvent struct {
Time *time.Time `json:",omitempty"`
Action GoJSONAction
Package string `json:",omitempty"`
Test string `json:",omitempty"`
Elapsed *float64 `json:",omitempty"`
Output *string `json:",omitempty"`
FailedBuild string `json:",omitempty"`
}
type GoJSONAction string
const (
// start - the test binary is about to be executed
GoJSONStart GoJSONAction = "start"
// run - the test has started running
GoJSONRun GoJSONAction = "run"
// pause - the test has been paused
GoJSONPause GoJSONAction = "pause"
// cont - the test has continued running
GoJSONCont GoJSONAction = "cont"
// pass - the test passed
GoJSONPass GoJSONAction = "pass"
// bench - the benchmark printed log output but did not fail
GoJSONBench GoJSONAction = "bench"
// fail - the test or benchmark failed
GoJSONFail GoJSONAction = "fail"
// output - the test printed output
GoJSONOutput GoJSONAction = "output"
// skip - the test was skipped or the package contained no tests
GoJSONSkip GoJSONAction = "skip"
)
func goJSONActionFromSpecState(state types.SpecState) GoJSONAction {
switch state {
case types.SpecStateInvalid:
return GoJSONFail
case types.SpecStatePending:
return GoJSONSkip
case types.SpecStateSkipped:
return GoJSONSkip
case types.SpecStatePassed:
return GoJSONPass
case types.SpecStateFailed:
return GoJSONFail
case types.SpecStateAborted:
return GoJSONFail
case types.SpecStatePanicked:
return GoJSONFail
case types.SpecStateInterrupted:
return GoJSONFail
case types.SpecStateTimedout:
return GoJSONFail
default:
panic("unexpected state should not happen")
}
}
// gojsonReport wraps types.Report and calcualtes extra fields requires by gojson
type gojsonReport struct {
o types.Report
// Extra calculated fields
goPkg string
elapsed float64
}
func newReport(in types.Report) *gojsonReport {
return &gojsonReport{
o: in,
}
}
func (r *gojsonReport) Fill() error {
// NOTE: could the types.Report include the go package name?
goPkg, err := suitePathToPkg(r.o.SuitePath)
if err != nil {
return err
}
r.goPkg = goPkg
r.elapsed = r.o.RunTime.Seconds()
return nil
}
// gojsonSpecReport wraps types.SpecReport and calculates extra fields required by gojson
type gojsonSpecReport struct {
o types.SpecReport
// extra calculated fields
testName string
elapsed float64
action GoJSONAction
}
func newSpecReport(in types.SpecReport) *gojsonSpecReport {
return &gojsonSpecReport{
o: in,
}
}
func (sr *gojsonSpecReport) Fill() error {
sr.elapsed = sr.o.RunTime.Seconds()
sr.testName = createTestName(sr.o)
sr.action = goJSONActionFromSpecState(sr.o.State)
return nil
}
func suitePathToPkg(dir string) (string, error) {
cfg := &packages.Config{
Mode: packages.NeedFiles | packages.NeedSyntax,
}
pkgs, err := packages.Load(cfg, dir)
if err != nil {
return "", err
}
if len(pkgs) != 1 {
return "", errors.New("error")
}
return pkgs[0].ID, nil
}
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, ", ") + "]"
}
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
}
@@ -0,0 +1,111 @@
package reporters
type GoJSONEventWriter struct {
enc encoder
specSystemErrFn specSystemExtractFn
specSystemOutFn specSystemExtractFn
}
func NewGoJSONEventWriter(enc encoder, errFn specSystemExtractFn, outFn specSystemExtractFn) *GoJSONEventWriter {
return &GoJSONEventWriter{
enc: enc,
specSystemErrFn: errFn,
specSystemOutFn: outFn,
}
}
func (r *GoJSONEventWriter) writeEvent(e *gojsonEvent) error {
return r.enc.Encode(e)
}
func (r *GoJSONEventWriter) WriteSuiteStart(report *gojsonReport) error {
e := &gojsonEvent{
Time: &report.o.StartTime,
Action: GoJSONStart,
Package: report.goPkg,
Output: nil,
FailedBuild: "",
}
return r.writeEvent(e)
}
func (r *GoJSONEventWriter) WriteSuiteResult(report *gojsonReport) error {
var action GoJSONAction
switch {
case report.o.PreRunStats.SpecsThatWillRun == 0:
action = GoJSONSkip
case report.o.SuiteSucceeded:
action = GoJSONPass
default:
action = GoJSONFail
}
e := &gojsonEvent{
Time: &report.o.EndTime,
Action: action,
Package: report.goPkg,
Output: nil,
FailedBuild: "",
Elapsed: ptr(report.elapsed),
}
return r.writeEvent(e)
}
func (r *GoJSONEventWriter) WriteSpecStart(report *gojsonReport, specReport *gojsonSpecReport) error {
e := &gojsonEvent{
Time: &specReport.o.StartTime,
Action: GoJSONRun,
Test: specReport.testName,
Package: report.goPkg,
Output: nil,
FailedBuild: "",
}
return r.writeEvent(e)
}
func (r *GoJSONEventWriter) WriteSpecOut(report *gojsonReport, specReport *gojsonSpecReport) error {
events := []*gojsonEvent{}
stdErr := r.specSystemErrFn(specReport.o)
if stdErr != "" {
events = append(events, &gojsonEvent{
Time: &specReport.o.EndTime,
Action: GoJSONOutput,
Test: specReport.testName,
Package: report.goPkg,
Output: ptr(stdErr),
FailedBuild: "",
})
}
stdOut := r.specSystemOutFn(specReport.o)
if stdOut != "" {
events = append(events, &gojsonEvent{
Time: &specReport.o.EndTime,
Action: GoJSONOutput,
Test: specReport.testName,
Package: report.goPkg,
Output: ptr(stdOut),
FailedBuild: "",
})
}
for _, ev := range events {
err := r.writeEvent(ev)
if err != nil {
return err
}
}
return nil
}
func (r *GoJSONEventWriter) WriteSpecResult(report *gojsonReport, specReport *gojsonSpecReport) error {
e := &gojsonEvent{
Time: &specReport.o.EndTime,
Action: specReport.action,
Test: specReport.testName,
Package: report.goPkg,
Elapsed: ptr(specReport.elapsed),
Output: nil,
FailedBuild: "",
}
return r.writeEvent(e)
}
@@ -0,0 +1,45 @@
package reporters
import (
"github.com/onsi/ginkgo/v2/types"
)
type GoJSONReporter struct {
ev *GoJSONEventWriter
}
type specSystemExtractFn func (spec types.SpecReport) string
func NewGoJSONReporter(enc encoder, errFn specSystemExtractFn, outFn specSystemExtractFn) *GoJSONReporter {
return &GoJSONReporter{
ev: NewGoJSONEventWriter(enc, errFn, outFn),
}
}
func (r *GoJSONReporter) Write(originalReport types.Report) error {
// suite start events
report := newReport(originalReport)
err := report.Fill()
if err != nil {
return err
}
r.ev.WriteSuiteStart(report)
for _, originalSpecReport := range originalReport.SpecReports {
specReport := newSpecReport(originalSpecReport)
err := specReport.Fill()
if err != nil {
return err
}
if specReport.o.LeafNodeType == types.NodeTypeIt {
// handle any It leaf node as a spec
r.ev.WriteSpecStart(report, specReport)
r.ev.WriteSpecOut(report, specReport)
r.ev.WriteSpecResult(report, specReport)
} else {
// handle any other leaf node as generic output
r.ev.WriteSpecOut(report, specReport)
}
}
r.ev.WriteSuiteResult(report)
return nil
}
+87
View File
@@ -0,0 +1,87 @@
package internal
import (
"strings"
"time"
"github.com/onsi/ginkgo/v2/types"
)
type Spec struct {
Nodes Nodes
Skip bool
}
func (s Spec) SubjectID() uint {
return s.Nodes.FirstNodeWithType(types.NodeTypeIt).ID
}
func (s Spec) Text() string {
texts := []string{}
for i := range s.Nodes {
if s.Nodes[i].Text != "" {
texts = append(texts, s.Nodes[i].Text)
}
}
return strings.Join(texts, " ")
}
func (s Spec) FirstNodeWithType(nodeTypes types.NodeType) Node {
return s.Nodes.FirstNodeWithType(nodeTypes)
}
func (s Spec) FlakeAttempts() int {
flakeAttempts := 0
for i := range s.Nodes {
if s.Nodes[i].FlakeAttempts > 0 {
flakeAttempts = s.Nodes[i].FlakeAttempts
}
}
return flakeAttempts
}
func (s Spec) MustPassRepeatedly() int {
mustPassRepeatedly := 0
for i := range s.Nodes {
if s.Nodes[i].MustPassRepeatedly > 0 {
mustPassRepeatedly = s.Nodes[i].MustPassRepeatedly
}
}
return mustPassRepeatedly
}
func (s Spec) SpecTimeout() time.Duration {
return s.FirstNodeWithType(types.NodeTypeIt).SpecTimeout
}
type Specs []Spec
func (s Specs) HasAnySpecsMarkedPending() bool {
for i := range s {
if s[i].Nodes.HasNodeMarkedPending() {
return true
}
}
return false
}
func (s Specs) CountWithoutSkip() int {
n := 0
for i := range s {
if !s[i].Skip {
n += 1
}
}
return n
}
func (s Specs) AtIndices(indices SpecIndices) Specs {
out := make(Specs, len(indices))
for i, idx := range indices {
out[i] = s[idx]
}
return out
}
+74
View File
@@ -0,0 +1,74 @@
package internal
import (
"context"
"reflect"
"github.com/onsi/ginkgo/v2/types"
)
type SpecContext interface {
context.Context
SpecReport() types.SpecReport
AttachProgressReporter(func() string) func()
WrappedContext() context.Context
}
type specContext struct {
context.Context
*ProgressReporterManager
cancel context.CancelCauseFunc
suite *Suite
}
/*
SpecContext includes a reference to `suite` and embeds itself in itself as a "GINKGO_SPEC_CONTEXT" value. This allows users to create child Contexts without having down-stream consumers (e.g. Gomega) lose access to the SpecContext and its methods. This allows us to build extensions on top of Ginkgo that simply take an all-encompassing context.
Note that while SpecContext is used to enforce deadlines by Ginkgo it is not configured as a context.WithDeadline. Instead, Ginkgo owns responsibility for cancelling the context when the deadline elapses.
This is because Ginkgo needs finer control over when the context is canceled. Specifically, Ginkgo needs to generate a ProgressReport before it cancels the context to ensure progress is captured where the spec is currently running. The only way to avoid a race here is to manually control the cancellation.
*/
func NewSpecContext(suite *Suite) *specContext {
ctx, cancel := context.WithCancelCause(context.Background())
sc := &specContext{
cancel: cancel,
suite: suite,
ProgressReporterManager: NewProgressReporterManager(),
}
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
sc.Context = ctx //thank goodness for garbage collectors that can handle circular dependencies
return sc
}
func (sc *specContext) SpecReport() types.SpecReport {
return sc.suite.CurrentSpecReport()
}
func (sc *specContext) WrappedContext() context.Context {
return sc.Context
}
/*
The user is allowed to wrap `SpecContext` in a new context.Context when using AroundNodes. But body functions expect SpecContext.
We support this by taking their context.Context and returning a SpecContext that wraps it.
*/
func wrapContextChain(ctx context.Context) SpecContext {
if ctx == nil {
return nil
}
if reflect.TypeOf(ctx) == reflect.TypeOf(&specContext{}) {
return ctx.(*specContext)
} else if sc, ok := ctx.Value("GINKGO_SPEC_CONTEXT").(*specContext); ok {
return &specContext{
Context: ctx,
ProgressReporterManager: sc.ProgressReporterManager,
cancel: sc.cancel,
suite: sc.suite,
}
}
return nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,251 @@
package testingtproxy
import (
"context"
"fmt"
"io"
"os"
"github.com/onsi/ginkgo/v2/formatter"
"github.com/onsi/ginkgo/v2/internal"
"github.com/onsi/ginkgo/v2/reporters"
"github.com/onsi/ginkgo/v2/types"
)
type failFunc func(message string, callerSkip ...int)
type skipFunc func(message string, callerSkip ...int)
type cleanupFunc func(args ...any)
type reportFunc func() types.SpecReport
type addReportEntryFunc func(names string, args ...any)
type ginkgoWriterInterface interface {
io.Writer
Print(a ...any)
Printf(format string, a ...any)
Println(a ...any)
}
type ginkgoRecoverFunc func()
type attachProgressReporterFunc func(func() string) func()
var formatters = map[bool]formatter.Formatter{
true: formatter.NewWithNoColorBool(true),
false: formatter.NewWithNoColorBool(false),
}
func New(writer ginkgoWriterInterface, fail failFunc, skip skipFunc, cleanup cleanupFunc, report reportFunc, addReportEntry addReportEntryFunc, ginkgoRecover ginkgoRecoverFunc, attachProgressReporter attachProgressReporterFunc, randomSeed int64, parallelProcess int, parallelTotal int, noColor bool, offset int) *ginkgoTestingTProxy {
return &ginkgoTestingTProxy{
fail: fail,
offset: offset,
writer: writer,
skip: skip,
cleanup: cleanup,
report: report,
addReportEntry: addReportEntry,
ginkgoRecover: ginkgoRecover,
attachProgressReporter: attachProgressReporter,
randomSeed: randomSeed,
parallelProcess: parallelProcess,
parallelTotal: parallelTotal,
f: formatters[noColor], //minimize allocations by reusing formatters
}
}
type ginkgoTestingTProxy struct {
fail failFunc
skip skipFunc
cleanup cleanupFunc
report reportFunc
offset int
writer ginkgoWriterInterface
addReportEntry addReportEntryFunc
ginkgoRecover ginkgoRecoverFunc
attachProgressReporter attachProgressReporterFunc
randomSeed int64
parallelProcess int
parallelTotal int
f formatter.Formatter
}
// basic testing.T support
func (t *ginkgoTestingTProxy) Cleanup(f func()) {
t.cleanup(f, internal.Offset(1))
}
func (t *ginkgoTestingTProxy) Setenv(key, value string) {
originalValue, exists := os.LookupEnv(key)
if exists {
t.cleanup(os.Setenv, key, originalValue, internal.Offset(1))
} else {
t.cleanup(os.Unsetenv, key, internal.Offset(1))
}
err := os.Setenv(key, value)
if err != nil {
t.fail(fmt.Sprintf("Failed to set environment variable: %v", err), 1)
}
}
func (t *ginkgoTestingTProxy) Chdir(dir string) {
currentDir, err := os.Getwd()
if err != nil {
t.fail(fmt.Sprintf("Failed to get current directory: %v", err), 1)
}
t.cleanup(os.Chdir, currentDir, internal.Offset(1))
err = os.Chdir(dir)
if err != nil {
t.fail(fmt.Sprintf("Failed to change directory: %v", err), 1)
}
}
func (t *ginkgoTestingTProxy) Context() context.Context {
ctx, cancel := context.WithCancel(context.Background())
t.cleanup(cancel, internal.Offset(1))
return ctx
}
func (t *ginkgoTestingTProxy) Error(args ...any) {
t.fail(fmt.Sprintln(args...), t.offset)
}
func (t *ginkgoTestingTProxy) Errorf(format string, args ...any) {
t.fail(fmt.Sprintf(format, args...), t.offset)
}
func (t *ginkgoTestingTProxy) Fail() {
t.fail("failed", t.offset)
}
func (t *ginkgoTestingTProxy) FailNow() {
t.fail("failed", t.offset)
}
func (t *ginkgoTestingTProxy) Failed() bool {
return t.report().Failed()
}
func (t *ginkgoTestingTProxy) Fatal(args ...any) {
t.fail(fmt.Sprintln(args...), t.offset)
}
func (t *ginkgoTestingTProxy) Fatalf(format string, args ...any) {
t.fail(fmt.Sprintf(format, args...), t.offset)
}
func (t *ginkgoTestingTProxy) Helper() {
types.MarkAsHelper(1)
}
func (t *ginkgoTestingTProxy) Log(args ...any) {
fmt.Fprintln(t.writer, args...)
}
func (t *ginkgoTestingTProxy) Logf(format string, args ...any) {
t.Log(fmt.Sprintf(format, args...))
}
func (t *ginkgoTestingTProxy) Name() string {
return t.report().FullText()
}
func (t *ginkgoTestingTProxy) Parallel() {
// No-op
}
func (t *ginkgoTestingTProxy) Skip(args ...any) {
t.skip(fmt.Sprintln(args...), t.offset)
}
func (t *ginkgoTestingTProxy) SkipNow() {
t.skip("skip", t.offset)
}
func (t *ginkgoTestingTProxy) Skipf(format string, args ...any) {
t.skip(fmt.Sprintf(format, args...), t.offset)
}
func (t *ginkgoTestingTProxy) Skipped() bool {
return t.report().State.Is(types.SpecStateSkipped)
}
func (t *ginkgoTestingTProxy) TempDir() string {
tmpDir, err := os.MkdirTemp("", "ginkgo")
if err != nil {
t.fail(fmt.Sprintf("Failed to create temporary directory: %v", err), 1)
return ""
}
t.cleanup(os.RemoveAll, tmpDir)
return tmpDir
}
func (t *ginkgoTestingTProxy) ArtifactDir() string {
artifactDir, err := os.MkdirTemp("", "ginkgo")
if err != nil {
t.fail(fmt.Sprintf("Failed to create artifact directory: %v", err), 1)
return ""
}
return artifactDir
}
// FullGinkgoTInterface
func (t *ginkgoTestingTProxy) AddReportEntryVisibilityAlways(name string, args ...any) {
finalArgs := []any{internal.Offset(1), types.ReportEntryVisibilityAlways}
t.addReportEntry(name, append(finalArgs, args...)...)
}
func (t *ginkgoTestingTProxy) AddReportEntryVisibilityFailureOrVerbose(name string, args ...any) {
finalArgs := []any{internal.Offset(1), types.ReportEntryVisibilityFailureOrVerbose}
t.addReportEntry(name, append(finalArgs, args...)...)
}
func (t *ginkgoTestingTProxy) AddReportEntryVisibilityNever(name string, args ...any) {
finalArgs := []any{internal.Offset(1), types.ReportEntryVisibilityNever}
t.addReportEntry(name, append(finalArgs, args...)...)
}
func (t *ginkgoTestingTProxy) Print(a ...any) {
t.writer.Print(a...)
}
func (t *ginkgoTestingTProxy) Printf(format string, a ...any) {
t.writer.Printf(format, a...)
}
func (t *ginkgoTestingTProxy) Println(a ...any) {
t.writer.Println(a...)
}
func (t *ginkgoTestingTProxy) F(format string, args ...any) string {
return t.f.F(format, args...)
}
func (t *ginkgoTestingTProxy) Fi(indentation uint, format string, args ...any) string {
return t.f.Fi(indentation, format, args...)
}
func (t *ginkgoTestingTProxy) Fiw(indentation uint, maxWidth uint, format string, args ...any) string {
return t.f.Fiw(indentation, maxWidth, format, args...)
}
func (t *ginkgoTestingTProxy) RenderTimeline() string {
return reporters.RenderTimeline(t.report(), false)
}
func (t *ginkgoTestingTProxy) GinkgoRecover() {
t.ginkgoRecover()
}
func (t *ginkgoTestingTProxy) DeferCleanup(args ...any) {
finalArgs := []any{internal.Offset(1)}
t.cleanup(append(finalArgs, args...)...)
}
func (t *ginkgoTestingTProxy) RandomSeed() int64 {
return t.randomSeed
}
func (t *ginkgoTestingTProxy) ParallelProcess() int {
return t.parallelProcess
}
func (t *ginkgoTestingTProxy) ParallelTotal() int {
return t.parallelTotal
}
func (t *ginkgoTestingTProxy) AttachProgressReporter(f func() string) func() {
return t.attachProgressReporter(f)
}
func (t *ginkgoTestingTProxy) Output() io.Writer {
return t.writer
}
func (t *ginkgoTestingTProxy) Attr(key, value string) {
t.addReportEntry(key, value, internal.Offset(1), types.ReportEntryVisibilityFailureOrVerbose)
}
+77
View File
@@ -0,0 +1,77 @@
package internal
import "github.com/onsi/ginkgo/v2/types"
type TreeNode struct {
Node Node
Parent *TreeNode
Children TreeNodes
}
func (tn *TreeNode) AppendChild(child *TreeNode) {
tn.Children = append(tn.Children, child)
child.Parent = tn
}
func (tn *TreeNode) AncestorNodeChain() Nodes {
if tn.Parent == nil || tn.Parent.Node.IsZero() {
return Nodes{tn.Node}
}
return append(tn.Parent.AncestorNodeChain(), tn.Node)
}
type TreeNodes []*TreeNode
func (tn TreeNodes) Nodes() Nodes {
out := make(Nodes, len(tn))
for i := range tn {
out[i] = tn[i].Node
}
return out
}
func (tn TreeNodes) WithID(id uint) *TreeNode {
for i := range tn {
if tn[i].Node.ID == id {
return tn[i]
}
}
return nil
}
func GenerateSpecsFromTreeRoot(tree *TreeNode) Specs {
var walkTree func(nestingLevel int, lNodes Nodes, rNodes Nodes, trees TreeNodes) Specs
walkTree = func(nestingLevel int, lNodes Nodes, rNodes Nodes, trees TreeNodes) Specs {
tests := Specs{}
nodes := make(Nodes, len(trees))
for i := range trees {
nodes[i] = trees[i].Node
nodes[i].NestingLevel = nestingLevel
}
for i := range nodes {
if !nodes[i].NodeType.Is(types.NodeTypesForContainerAndIt) {
continue
}
leftNodes, rightNodes := nodes.SplitAround(nodes[i])
leftNodes = leftNodes.WithoutType(types.NodeTypesForContainerAndIt)
rightNodes = rightNodes.WithoutType(types.NodeTypesForContainerAndIt)
leftNodes = lNodes.CopyAppend(leftNodes...)
rightNodes = rightNodes.CopyAppend(rNodes...)
if nodes[i].NodeType.Is(types.NodeTypeIt) {
tests = append(tests, Spec{Nodes: leftNodes.CopyAppend(nodes[i]).CopyAppend(rightNodes...)})
} else {
treeNode := trees.WithID(nodes[i].ID)
tests = append(tests, walkTree(nestingLevel+1, leftNodes.CopyAppend(nodes[i]), rightNodes, treeNode.Children)...)
}
}
return tests
}
return walkTree(0, Nodes{}, Nodes{}, tree.Children)
}
+144
View File
@@ -0,0 +1,144 @@
package internal
import (
"bytes"
"fmt"
"io"
"sync"
"github.com/go-logr/logr"
"github.com/go-logr/logr/funcr"
)
type WriterMode uint
const (
WriterModeStreamAndBuffer WriterMode = iota
WriterModeBufferOnly
)
type WriterInterface interface {
io.Writer
Truncate()
Bytes() []byte
Len() int
}
// Writer implements WriterInterface and GinkgoWriterInterface
type Writer struct {
buffer *bytes.Buffer
outWriter io.Writer
lock *sync.Mutex
mode WriterMode
streamIndent []byte
indentNext bool
teeWriters []io.Writer
}
func NewWriter(outWriter io.Writer) *Writer {
return &Writer{
buffer: &bytes.Buffer{},
lock: &sync.Mutex{},
outWriter: outWriter,
mode: WriterModeStreamAndBuffer,
streamIndent: []byte(" "),
indentNext: true,
}
}
func (w *Writer) SetMode(mode WriterMode) {
w.lock.Lock()
defer w.lock.Unlock()
w.mode = mode
}
func (w *Writer) Len() int {
w.lock.Lock()
defer w.lock.Unlock()
return w.buffer.Len()
}
var newline = []byte("\n")
func (w *Writer) Write(b []byte) (n int, err error) {
w.lock.Lock()
defer w.lock.Unlock()
for _, teeWriter := range w.teeWriters {
teeWriter.Write(b)
}
if w.mode == WriterModeStreamAndBuffer {
line, remaining, found := []byte{}, b, false
for len(remaining) > 0 {
line, remaining, found = bytes.Cut(remaining, newline)
if len(line) > 0 {
if w.indentNext {
w.outWriter.Write(w.streamIndent)
w.indentNext = false
}
w.outWriter.Write(line)
}
if found {
w.outWriter.Write(newline)
w.indentNext = true
}
}
}
return w.buffer.Write(b)
}
func (w *Writer) Truncate() {
w.lock.Lock()
defer w.lock.Unlock()
w.buffer.Reset()
}
func (w *Writer) Bytes() []byte {
w.lock.Lock()
defer w.lock.Unlock()
b := w.buffer.Bytes()
copied := make([]byte, len(b))
copy(copied, b)
return copied
}
// GinkgoWriterInterface
func (w *Writer) TeeTo(writer io.Writer) {
w.lock.Lock()
defer w.lock.Unlock()
w.teeWriters = append(w.teeWriters, writer)
}
func (w *Writer) ClearTeeWriters() {
w.lock.Lock()
defer w.lock.Unlock()
w.teeWriters = []io.Writer{}
}
func (w *Writer) Print(a ...any) {
fmt.Fprint(w, a...)
}
func (w *Writer) Printf(format string, a ...any) {
fmt.Fprintf(w, format, a...)
}
func (w *Writer) Println(a ...any) {
fmt.Fprintln(w, a...)
}
func GinkgoLogrFunc(writer *Writer) logr.Logger {
return funcr.New(func(prefix, args string) {
if prefix == "" {
writer.Printf("%s\n", args)
} else {
writer.Printf("%s %s\n", prefix, args)
}
}, funcr.Options{})
}