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, } } 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(), LeafNodeLocation: spec.FirstNodeWithType(types.NodeTypeIt).CodeLocation, LeafNodeType: types.NodeTypeIt, LeafNodeText: spec.FirstNodeWithType(types.NodeTypeIt).Text, LeafNodeLabels: []string(spec.FirstNodeWithType(types.NodeTypeIt).Labels), 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(), } } 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.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() } }