Initial QSfera import
This commit is contained in:
+34
@@ -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
@@ -0,0 +1,9 @@
|
||||
package internal
|
||||
|
||||
func MakeIncrementingIndexCounter() func() (int, error) {
|
||||
idx := -1
|
||||
return func() (int, error) {
|
||||
idx += 1
|
||||
return idx, nil
|
||||
}
|
||||
}
|
||||
+99
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+177
@@ -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
|
||||
}
|
||||
Generated
Vendored
+15
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package interrupt_handler
|
||||
|
||||
func SwallowSigQuit() {
|
||||
//noop
|
||||
}
|
||||
+1167
File diff suppressed because it is too large
Load Diff
+192
@@ -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
@@ -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
|
||||
}
|
||||
+73
@@ -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()
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
//go:build wasm
|
||||
|
||||
package internal
|
||||
|
||||
func NewOutputInterceptor() OutputInterceptor {
|
||||
return &NoopOutputInterceptor{}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// +build windows
|
||||
|
||||
package internal
|
||||
|
||||
func NewOutputInterceptor() OutputInterceptor {
|
||||
return NewOSGlobalReassigningOutputInterceptor()
|
||||
}
|
||||
Generated
Vendored
+72
@@ -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)
|
||||
}
|
||||
}
|
||||
+166
@@ -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
|
||||
}
|
||||
+242
@@ -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)
|
||||
}
|
||||
}
|
||||
+136
@@ -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
|
||||
}
|
||||
+75
@@ -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)
|
||||
}
|
||||
Generated
Vendored
+234
@@ -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
@@ -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
|
||||
}
|
||||
+11
@@ -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}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
//go:build linux || solaris
|
||||
// +build linux solaris
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var PROGRESS_SIGNALS = []os.Signal{syscall.SIGUSR1}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
//go:build wasm
|
||||
|
||||
package internal
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var PROGRESS_SIGNALS = []os.Signal{syscall.SIGUSR1}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package internal
|
||||
|
||||
import "os"
|
||||
|
||||
var PROGRESS_SIGNALS = []os.Signal{}
|
||||
+79
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
Generated
Vendored
+111
@@ -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)
|
||||
}
|
||||
+45
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
+1099
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+251
@@ -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
@@ -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
@@ -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{})
|
||||
}
|
||||
Reference in New Issue
Block a user