...

Source file src/github.com/onsi/ginkgo/v2/internal/progress_report.go

Documentation: github.com/onsi/ginkgo/v2/internal

     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  	// now we want to try to find goroutines of interest.  these will be goroutines that have any function calls with code in packagesOfInterest:
    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  	//First, we find the SpecGoroutine - this will be the goroutine that includes `runNode`
   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  	//Now, we find the first non-Ginkgo function call
   122  	if specGoRoutineIdx > -1 {
   123  		for runNodeFunctionCallIdx >= 0 {
   124  			fn := goroutines[specGoRoutineIdx].Stack[runNodeFunctionCallIdx].Function
   125  			file := goroutines[specGoRoutineIdx].Stack[runNodeFunctionCallIdx].Filename
   126  			// these are all things that could potentially happen from within ginkgo
   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  			//found it!  lets add its package of interest
   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  	// Now we go through all goroutines and highlight any lines with packages in `packagesOfInterest`
   152  	// Any goroutines with highlighted lines end up in the HighlightGoRoutines
   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  		//skip blank lines
   198  		if line == "" {
   199  			continue
   200  		}
   201  
   202  		//parse headers for new goroutine frames
   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  		//if we are here we must be at a function call entry in the stack
   223  		functionCall := types.FunctionCall{
   224  			Function: strings.TrimPrefix(line, "created by "), // no need to track '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