github.com/onsi/ginkgo@v1.16.6-0.20211118180735-4e1925ba4c95/ginkgo/internal/profiles_and_reports.go (about)

     1  package internal
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strconv"
    11  
    12  	"github.com/google/pprof/profile"
    13  	"github.com/onsi/ginkgo/reporters"
    14  	"github.com/onsi/ginkgo/types"
    15  )
    16  
    17  func AbsPathForGeneratedAsset(assetName string, suite TestSuite, cliConfig types.CLIConfig, process int) string {
    18  	suffix := ""
    19  	if process != 0 {
    20  		suffix = fmt.Sprintf(".%d", process)
    21  	}
    22  	if cliConfig.OutputDir == "" {
    23  		return filepath.Join(suite.AbsPath(), assetName+suffix)
    24  	}
    25  	outputDir, _ := filepath.Abs(cliConfig.OutputDir)
    26  	return filepath.Join(outputDir, suite.NamespacedName()+"_"+assetName+suffix)
    27  }
    28  
    29  func FinalizeProfilesAndReportsForSuites(suites TestSuites, cliConfig types.CLIConfig, suiteConfig types.SuiteConfig, reporterConfig types.ReporterConfig, goFlagsConfig types.GoFlagsConfig) ([]string, error) {
    30  	messages := []string{}
    31  	suitesWithProfiles := suites.WithState(TestSuiteStatePassed, TestSuiteStateFailed) //anything else won't have actually run and generated a profile
    32  
    33  	// merge cover profiles if need be
    34  	if goFlagsConfig.Cover && !cliConfig.KeepSeparateCoverprofiles {
    35  		coverProfiles := []string{}
    36  		for _, suite := range suitesWithProfiles {
    37  			coverProfiles = append(coverProfiles, AbsPathForGeneratedAsset(goFlagsConfig.CoverProfile, suite, cliConfig, 0))
    38  		}
    39  		dst := goFlagsConfig.CoverProfile
    40  		if cliConfig.OutputDir != "" {
    41  			dst = filepath.Join(cliConfig.OutputDir, goFlagsConfig.CoverProfile)
    42  		}
    43  		err := MergeAndCleanupCoverProfiles(coverProfiles, dst)
    44  		if err != nil {
    45  			return messages, err
    46  		}
    47  
    48  		coverage, err := GetCoverageFromCoverProfile(dst)
    49  		if err != nil {
    50  			return messages, err
    51  		}
    52  		if coverage == 0 {
    53  			messages = append(messages, "composite coverage: [no statements]")
    54  		} else {
    55  			messages = append(messages, fmt.Sprintf("composite coverage: %.1f%% of statements", coverage))
    56  		}
    57  	}
    58  
    59  	// copy binaries if need be
    60  	for _, suite := range suitesWithProfiles {
    61  		if goFlagsConfig.BinaryMustBePreserved() && cliConfig.OutputDir != "" {
    62  			src := suite.PathToCompiledTest
    63  			dst := filepath.Join(cliConfig.OutputDir, suite.NamespacedName()+".test")
    64  			if suite.Precompiled {
    65  				if err := CopyFile(src, dst); err != nil {
    66  					return messages, err
    67  				}
    68  			} else {
    69  				if err := os.Rename(src, dst); err != nil {
    70  					return messages, err
    71  				}
    72  			}
    73  		}
    74  	}
    75  
    76  	type reportFormat struct {
    77  		ReportName   string
    78  		GenerateFunc func(types.Report, string) error
    79  		MergeFunc    func([]string, string) ([]string, error)
    80  	}
    81  	reportFormats := []reportFormat{}
    82  	if reporterConfig.JSONReport != "" {
    83  		reportFormats = append(reportFormats, reportFormat{ReportName: reporterConfig.JSONReport, GenerateFunc: reporters.GenerateJSONReport, MergeFunc: reporters.MergeAndCleanupJSONReports})
    84  	}
    85  	if reporterConfig.JUnitReport != "" {
    86  		reportFormats = append(reportFormats, reportFormat{ReportName: reporterConfig.JUnitReport, GenerateFunc: reporters.GenerateJUnitReport, MergeFunc: reporters.MergeAndCleanupJUnitReports})
    87  	}
    88  	if reporterConfig.TeamcityReport != "" {
    89  		reportFormats = append(reportFormats, reportFormat{ReportName: reporterConfig.TeamcityReport, GenerateFunc: reporters.GenerateTeamcityReport, MergeFunc: reporters.MergeAndCleanupTeamcityReports})
    90  	}
    91  
    92  	// Generate reports for suites that failed to run
    93  	reportableSuites := suites.ThatAreGinkgoSuites()
    94  	for _, suite := range reportableSuites.WithState(TestSuiteStateFailedToCompile, TestSuiteStateFailedDueToTimeout, TestSuiteStateSkippedDueToPriorFailures, TestSuiteStateSkippedDueToEmptyCompilation) {
    95  		report := types.Report{
    96  			SuitePath:      suite.AbsPath(),
    97  			SuiteConfig:    suiteConfig,
    98  			SuiteSucceeded: false,
    99  		}
   100  		switch suite.State {
   101  		case TestSuiteStateFailedToCompile:
   102  			report.SpecialSuiteFailureReasons = append(report.SpecialSuiteFailureReasons, suite.CompilationError.Error())
   103  		case TestSuiteStateFailedDueToTimeout:
   104  			report.SpecialSuiteFailureReasons = append(report.SpecialSuiteFailureReasons, TIMEOUT_ELAPSED_FAILURE_REASON)
   105  		case TestSuiteStateSkippedDueToPriorFailures:
   106  			report.SpecialSuiteFailureReasons = append(report.SpecialSuiteFailureReasons, PRIOR_FAILURES_FAILURE_REASON)
   107  		case TestSuiteStateSkippedDueToEmptyCompilation:
   108  			report.SpecialSuiteFailureReasons = append(report.SpecialSuiteFailureReasons, EMPTY_SKIP_FAILURE_REASON)
   109  			report.SuiteSucceeded = true
   110  		}
   111  
   112  		for _, format := range reportFormats {
   113  			format.GenerateFunc(report, AbsPathForGeneratedAsset(format.ReportName, suite, cliConfig, 0))
   114  		}
   115  	}
   116  
   117  	// Merge reports unless we've been asked to keep them separate
   118  	if !cliConfig.KeepSeparateReports {
   119  		for _, format := range reportFormats {
   120  			reports := []string{}
   121  			for _, suite := range reportableSuites {
   122  				reports = append(reports, AbsPathForGeneratedAsset(format.ReportName, suite, cliConfig, 0))
   123  			}
   124  			dst := format.ReportName
   125  			if cliConfig.OutputDir != "" {
   126  				dst = filepath.Join(cliConfig.OutputDir, format.ReportName)
   127  			}
   128  			mergeMessages, err := format.MergeFunc(reports, dst)
   129  			messages = append(messages, mergeMessages...)
   130  			if err != nil {
   131  				return messages, err
   132  			}
   133  		}
   134  	}
   135  
   136  	return messages, nil
   137  }
   138  
   139  //loads each profile, combines them, deletes them, stores them in destination
   140  func MergeAndCleanupCoverProfiles(profiles []string, destination string) error {
   141  	combined := &bytes.Buffer{}
   142  	modeRegex := regexp.MustCompile(`^mode: .*\n`)
   143  	for i, profile := range profiles {
   144  		contents, err := os.ReadFile(profile)
   145  		if err != nil {
   146  			return fmt.Errorf("Unable to read coverage file %s:\n%s", profile, err.Error())
   147  		}
   148  		os.Remove(profile)
   149  
   150  		// remove the cover mode line from every file
   151  		// except the first one
   152  		if i > 0 {
   153  			contents = modeRegex.ReplaceAll(contents, []byte{})
   154  		}
   155  
   156  		_, err = combined.Write(contents)
   157  
   158  		// Add a newline to the end of every file if missing.
   159  		if err == nil && len(contents) > 0 && contents[len(contents)-1] != '\n' {
   160  			_, err = combined.Write([]byte("\n"))
   161  		}
   162  
   163  		if err != nil {
   164  			return fmt.Errorf("Unable to append to coverprofile:\n%s", err.Error())
   165  		}
   166  	}
   167  
   168  	err := os.WriteFile(destination, combined.Bytes(), 0666)
   169  	if err != nil {
   170  		return fmt.Errorf("Unable to create combined cover profile:\n%s", err.Error())
   171  	}
   172  	return nil
   173  }
   174  
   175  func GetCoverageFromCoverProfile(profile string) (float64, error) {
   176  	cmd := exec.Command("go", "tool", "cover", "-func", profile)
   177  	output, err := cmd.CombinedOutput()
   178  	if err != nil {
   179  		return 0, fmt.Errorf("Could not process Coverprofile %s: %s", profile, err.Error())
   180  	}
   181  	re := regexp.MustCompile(`total:\s*\(statements\)\s*(\d*\.\d*)\%`)
   182  	matches := re.FindStringSubmatch(string(output))
   183  	if matches == nil {
   184  		return 0, fmt.Errorf("Could not parse Coverprofile to compute coverage percentage")
   185  	}
   186  	coverageString := matches[1]
   187  	coverage, err := strconv.ParseFloat(coverageString, 64)
   188  	if err != nil {
   189  		return 0, fmt.Errorf("Could not parse Coverprofile to compute coverage percentage: %s", err.Error())
   190  	}
   191  
   192  	return coverage, nil
   193  }
   194  
   195  func MergeProfiles(profilePaths []string, destination string) error {
   196  	profiles := []*profile.Profile{}
   197  	for _, profilePath := range profilePaths {
   198  		proFile, err := os.Open(profilePath)
   199  		if err != nil {
   200  			return fmt.Errorf("Could not open profile: %s\n%s", profilePath, err.Error())
   201  		}
   202  		prof, err := profile.Parse(proFile)
   203  		if err != nil {
   204  			return fmt.Errorf("Could not parse profile: %s\n%s", profilePath, err.Error())
   205  		}
   206  		profiles = append(profiles, prof)
   207  		os.Remove(profilePath)
   208  	}
   209  
   210  	mergedProfile, err := profile.Merge(profiles)
   211  	if err != nil {
   212  		return fmt.Errorf("Could not merge profiles:\n%s", err.Error())
   213  	}
   214  
   215  	outFile, err := os.Create(destination)
   216  	if err != nil {
   217  		return fmt.Errorf("Could not create merged profile %s:\n%s", destination, err.Error())
   218  	}
   219  	err = mergedProfile.Write(outFile)
   220  	if err != nil {
   221  		return fmt.Errorf("Could not write merged profile %s:\n%s", destination, err.Error())
   222  	}
   223  	err = outFile.Close()
   224  	if err != nil {
   225  		return fmt.Errorf("Could not close merged profile %s:\n%s", destination, err.Error())
   226  	}
   227  
   228  	return nil
   229  }