1 package internal
2
3 import (
4 "bufio"
5 "bytes"
6 "context"
7 "fmt"
8 "io"
9 "os"
10 "os/signal"
11 "path/filepath"
12 "runtime"
13 "strconv"
14 "strings"
15 "time"
16
17 "github.com/onsi/ginkgo/v2/types"
18 )
19
20 var _SOURCE_CACHE = map[string][]string{}
21
22 type ProgressSignalRegistrar func(func()) context.CancelFunc
23
24 func RegisterForProgressSignal(handler func()) context.CancelFunc {
25 signalChannel := make(chan os.Signal, 1)
26 if len(PROGRESS_SIGNALS) > 0 {
27 signal.Notify(signalChannel, PROGRESS_SIGNALS...)
28 }
29 ctx, cancel := context.WithCancel(context.Background())
30 go func() {
31 for {
32 select {
33 case <-signalChannel:
34 handler()
35 case <-ctx.Done():
36 signal.Stop(signalChannel)
37 return
38 }
39 }
40 }()
41
42 return cancel
43 }
44
45 type ProgressStepCursor struct {
46 Text string
47 CodeLocation types.CodeLocation
48 StartTime time.Time
49 }
50
51 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) {
52 pr := types.ProgressReport{
53 ParallelProcess: report.ParallelProcess,
54 RunningInParallel: isRunningInParallel,
55 ContainerHierarchyTexts: report.ContainerHierarchyTexts,
56 LeafNodeText: report.LeafNodeText,
57 LeafNodeLocation: report.LeafNodeLocation,
58 SpecStartTime: report.StartTime,
59
60 CurrentNodeType: currentNode.NodeType,
61 CurrentNodeText: currentNode.Text,
62 CurrentNodeLocation: currentNode.CodeLocation,
63 CurrentNodeStartTime: currentNodeStartTime,
64
65 CurrentStepText: currentStep.Message,
66 CurrentStepLocation: currentStep.CodeLocation,
67 CurrentStepStartTime: currentStep.TimelineLocation.Time,
68
69 AdditionalReports: additionalReports,
70
71 CapturedGinkgoWriterOutput: gwOutput,
72 TimelineLocation: timelineLocation,
73 }
74
75 goroutines, err := extractRunningGoroutines()
76 if err != nil {
77 return pr, err
78 }
79 pr.Goroutines = goroutines
80
81
82 packagesOfInterest := map[string]bool{}
83 packageFromFilename := func(filename string) string {
84 return filepath.Dir(filename)
85 }
86 addPackageFor := func(filename string) {
87 if filename != "" {
88 packagesOfInterest[packageFromFilename(filename)] = true
89 }
90 }
91 isPackageOfInterest := func(filename string) bool {
92 stackPackage := packageFromFilename(filename)
93 for packageOfInterest := range packagesOfInterest {
94 if strings.HasPrefix(stackPackage, packageOfInterest) {
95 return true
96 }
97 }
98 return false
99 }
100 for _, location := range report.ContainerHierarchyLocations {
101 addPackageFor(location.FileName)
102 }
103 addPackageFor(report.LeafNodeLocation.FileName)
104 addPackageFor(currentNode.CodeLocation.FileName)
105 addPackageFor(currentStep.CodeLocation.FileName)
106
107
108 specGoRoutineIdx := -1
109 runNodeFunctionCallIdx := -1
110 OUTER:
111 for goroutineIdx, goroutine := range pr.Goroutines {
112 for functionCallIdx, functionCall := range goroutine.Stack {
113 if strings.Contains(functionCall.Function, "ginkgo/v2/internal.(*Suite).runNode.func") {
114 specGoRoutineIdx = goroutineIdx
115 runNodeFunctionCallIdx = functionCallIdx
116 break OUTER
117 }
118 }
119 }
120
121
122 if specGoRoutineIdx > -1 {
123 for runNodeFunctionCallIdx >= 0 {
124 fn := goroutines[specGoRoutineIdx].Stack[runNodeFunctionCallIdx].Function
125 file := goroutines[specGoRoutineIdx].Stack[runNodeFunctionCallIdx].Filename
126
127 if strings.Contains(fn, "ginkgo/v2/internal") || strings.Contains(fn, "reflect.Value") || strings.Contains(file, "ginkgo/table_dsl") || strings.Contains(file, "ginkgo/core_dsl") {
128 runNodeFunctionCallIdx--
129 continue
130 }
131 if strings.Contains(goroutines[specGoRoutineIdx].Stack[runNodeFunctionCallIdx].Function, "ginkgo/table_dsl") {
132
133 }
134
135 addPackageFor(goroutines[specGoRoutineIdx].Stack[runNodeFunctionCallIdx].Filename)
136 break
137 }
138 }
139
140 ginkgoEntryPointIdx := -1
141 OUTER_GINKGO_ENTRY_POINT:
142 for goroutineIdx, goroutine := range pr.Goroutines {
143 for _, functionCall := range goroutine.Stack {
144 if strings.Contains(functionCall.Function, "ginkgo/v2.RunSpecs") {
145 ginkgoEntryPointIdx = goroutineIdx
146 break OUTER_GINKGO_ENTRY_POINT
147 }
148 }
149 }
150
151
152
153 for goroutineIdx, goroutine := range pr.Goroutines {
154 if goroutineIdx == ginkgoEntryPointIdx {
155 continue
156 }
157 if goroutineIdx == specGoRoutineIdx {
158 pr.Goroutines[goroutineIdx].IsSpecGoroutine = true
159 }
160 for functionCallIdx, functionCall := range goroutine.Stack {
161 if isPackageOfInterest(functionCall.Filename) {
162 goroutine.Stack[functionCallIdx].Highlight = true
163 goroutine.Stack[functionCallIdx].Source, goroutine.Stack[functionCallIdx].SourceHighlight = fetchSource(functionCall.Filename, functionCall.Line, 2, sourceRoots)
164 }
165 }
166 }
167
168 if !includeAll {
169 goroutines := []types.Goroutine{pr.SpecGoroutine()}
170 goroutines = append(goroutines, pr.HighlightedGoroutines()...)
171 pr.Goroutines = goroutines
172 }
173
174 return pr, nil
175 }
176
177 func extractRunningGoroutines() ([]types.Goroutine, error) {
178 var stack []byte
179 for size := 64 * 1024; ; size *= 2 {
180 stack = make([]byte, size)
181 if n := runtime.Stack(stack, true); n < size {
182 stack = stack[:n]
183 break
184 }
185 }
186 r := bufio.NewReader(bytes.NewReader(stack))
187 out := []types.Goroutine{}
188 idx := -1
189 for {
190 line, err := r.ReadString('\n')
191 if err == io.EOF {
192 break
193 }
194
195 line = strings.TrimSuffix(line, "\n")
196
197
198 if line == "" {
199 continue
200 }
201
202
203 if strings.HasPrefix(line, "goroutine") {
204 out = append(out, types.Goroutine{})
205 idx = len(out) - 1
206
207 line = strings.TrimPrefix(line, "goroutine ")
208 line = strings.TrimSuffix(line, ":")
209 fields := strings.SplitN(line, " ", 2)
210 if len(fields) != 2 {
211 return nil, types.GinkgoErrors.FailedToParseStackTrace(fmt.Sprintf("Invalid goroutine frame header: %s", line))
212 }
213 out[idx].ID, err = strconv.ParseUint(fields[0], 10, 64)
214 if err != nil {
215 return nil, types.GinkgoErrors.FailedToParseStackTrace(fmt.Sprintf("Invalid goroutine ID: %s", fields[1]))
216 }
217
218 out[idx].State = strings.TrimSuffix(strings.TrimPrefix(fields[1], "["), "]")
219 continue
220 }
221
222
223 functionCall := types.FunctionCall{
224 Function: strings.TrimPrefix(line, "created by "),
225 }
226
227 line, err = r.ReadString('\n')
228 line = strings.TrimSuffix(line, "\n")
229 if err == io.EOF {
230 return nil, types.GinkgoErrors.FailedToParseStackTrace(fmt.Sprintf("Invalid function call: %s -- missing file name and line number", functionCall.Function))
231 }
232 line = strings.TrimLeft(line, " \t")
233 delimiterIdx := strings.LastIndex(line, ":")
234 if delimiterIdx == -1 {
235 return nil, types.GinkgoErrors.FailedToParseStackTrace(fmt.Sprintf("Invalid filename and line number: %s", line))
236 }
237 functionCall.Filename = line[:delimiterIdx]
238 line = strings.Split(line[delimiterIdx+1:], " ")[0]
239 lineNumber, err := strconv.ParseInt(line, 10, 64)
240 functionCall.Line = int(lineNumber)
241 if err != nil {
242 return nil, types.GinkgoErrors.FailedToParseStackTrace(fmt.Sprintf("Invalid function call line number: %s\n%s", line, err.Error()))
243 }
244 out[idx].Stack = append(out[idx].Stack, functionCall)
245 }
246
247 return out, nil
248 }
249
250 func fetchSource(filename string, lineNumber int, span int, configuredSourceRoots []string) ([]string, int) {
251 if filename == "" {
252 return []string{}, 0
253 }
254
255 var lines []string
256 var ok bool
257 if lines, ok = _SOURCE_CACHE[filename]; !ok {
258 sourceRoots := []string{""}
259 sourceRoots = append(sourceRoots, configuredSourceRoots...)
260 var data []byte
261 var err error
262 var found bool
263 for _, root := range sourceRoots {
264 data, err = os.ReadFile(filepath.Join(root, filename))
265 if err == nil {
266 found = true
267 break
268 }
269 }
270 if !found {
271 return []string{}, 0
272 }
273 lines = strings.Split(string(data), "\n")
274 _SOURCE_CACHE[filename] = lines
275 }
276
277 startIndex := lineNumber - span - 1
278 endIndex := startIndex + span + span + 1
279 if startIndex < 0 {
280 startIndex = 0
281 }
282 if endIndex > len(lines) {
283 endIndex = len(lines)
284 }
285 highlightIndex := lineNumber - 1 - startIndex
286 return lines[startIndex:endIndex], highlightIndex
287 }
288
View as plain text