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 }