github.com/redhat-appstudio/e2e-tests@v0.0.0-20230619105049-9a422b2094d7/pkg/framework/custom_junit_reporter.go (about)

     1  /*
     2  This is modified JUnit XML Reporter for Ginkgo framework.
     3  Original version is available on https://github.com/onsi/ginkgo/blob/master/reporters/junit_report.go
     4  
     5  Ginkgo project repository: https://github.com/onsi/ginkgo
     6  
     7  MIT License:
     8  Copyright (c) 2013-2014 Onsi Fakhouri
     9  
    10  Permission is hereby granted, free of charge, to any person obtaining
    11  a copy of this software and associated documentation files (the
    12  "Software"), to deal in the Software without restriction, including
    13  without limitation the rights to use, copy, modify, merge, publish,
    14  distribute, sublicense, and/or sell copies of the Software, and to
    15  permit persons to whom the Software is furnished to do so, subject to
    16  the following conditions:
    17  
    18  The above copyright notice and this permission notice shall be
    19  included in all copies or substantial portions of the Software.
    20  
    21  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    22  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    23  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    24  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    25  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    26  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
    27  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    28  */
    29  
    30  package framework
    31  
    32  import (
    33  	"crypto/sha1"
    34  	"encoding/hex"
    35  	"encoding/xml"
    36  	"fmt"
    37  	"os"
    38  	"strings"
    39  
    40  	. "github.com/onsi/ginkgo/v2/reporters"
    41  	types "github.com/onsi/ginkgo/v2/types"
    42  	"k8s.io/klog/v2"
    43  )
    44  
    45  func GenerateCustomJUnitReport(report types.Report, dst string) error {
    46  	return GenerateCustomJUnitReportWithConfig(report, dst, JunitReportConfig{})
    47  }
    48  
    49  func GenerateCustomJUnitReportWithConfig(report types.Report, dst string, config JunitReportConfig) error {
    50  	suite := JUnitTestSuite{
    51  		Name:      report.SuiteDescription,
    52  		Package:   report.SuitePath,
    53  		Time:      report.RunTime.Seconds(),
    54  		Timestamp: report.StartTime.Format("2006-01-02T15:04:05"),
    55  		Properties: JUnitProperties{
    56  			Properties: []JUnitProperty{
    57  				{"SuiteSucceeded", fmt.Sprintf("%t", report.SuiteSucceeded)},
    58  				{"SuiteHasProgrammaticFocus", fmt.Sprintf("%t", report.SuiteHasProgrammaticFocus)},
    59  				{"SpecialSuiteFailureReason", strings.Join(report.SpecialSuiteFailureReasons, ",")},
    60  				{"SuiteLabels", fmt.Sprintf("[%s]", strings.Join(report.SuiteLabels, ","))},
    61  				{"RandomSeed", fmt.Sprintf("%d", report.SuiteConfig.RandomSeed)},
    62  				{"RandomizeAllSpecs", fmt.Sprintf("%t", report.SuiteConfig.RandomizeAllSpecs)},
    63  				{"LabelFilter", report.SuiteConfig.LabelFilter},
    64  				{"FocusStrings", strings.Join(report.SuiteConfig.FocusStrings, ",")},
    65  				{"SkipStrings", strings.Join(report.SuiteConfig.SkipStrings, ",")},
    66  				{"FocusFiles", strings.Join(report.SuiteConfig.FocusFiles, ";")},
    67  				{"SkipFiles", strings.Join(report.SuiteConfig.SkipFiles, ";")},
    68  				{"FailOnPending", fmt.Sprintf("%t", report.SuiteConfig.FailOnPending)},
    69  				{"FailFast", fmt.Sprintf("%t", report.SuiteConfig.FailFast)},
    70  				{"FlakeAttempts", fmt.Sprintf("%d", report.SuiteConfig.FlakeAttempts)},
    71  				{"DryRun", fmt.Sprintf("%t", report.SuiteConfig.DryRun)},
    72  				{"ParallelTotal", fmt.Sprintf("%d", report.SuiteConfig.ParallelTotal)},
    73  				{"OutputInterceptorMode", report.SuiteConfig.OutputInterceptorMode},
    74  			},
    75  		},
    76  	}
    77  	for _, spec := range report.SpecReports {
    78  
    79  		if spec.LeafNodeType != types.NodeTypeIt {
    80  			continue
    81  		}
    82  		test := JUnitTestCase{
    83  			Name:      shortenStringAddHash(spec),
    84  			Classname: getClassnameFromReport(spec),
    85  			Status:    spec.State.String(),
    86  			Time:      spec.RunTime.Seconds(),
    87  		}
    88  		if !spec.State.Is(config.OmitTimelinesForSpecState) {
    89  			test.SystemErr = systemErrForUnstructuredReporters(spec)
    90  		}
    91  		if !config.OmitCapturedStdOutErr {
    92  			test.SystemOut = systemOutForUnstructuredReporters(spec)
    93  		}
    94  		suite.Tests += 1
    95  
    96  		switch spec.State {
    97  		case types.SpecStateSkipped:
    98  			message := "skipped"
    99  			if spec.Failure.Message != "" {
   100  				message += " - " + spec.Failure.Message
   101  			}
   102  			test.Skipped = &JUnitSkipped{Message: message}
   103  			suite.Skipped += 1
   104  		case types.SpecStatePending:
   105  			test.Skipped = &JUnitSkipped{Message: "pending"}
   106  			suite.Disabled += 1
   107  		case types.SpecStateFailed:
   108  			test.Failure = &JUnitFailure{
   109  				Message:     spec.Failure.Message,
   110  				Type:        "failed",
   111  				Description: fmt.Sprintf("%s\n%s", spec.Failure.Location.String(), spec.Failure.Location.FullStackTrace),
   112  			}
   113  			suite.Failures += 1
   114  		case types.SpecStateInterrupted:
   115  			test.Error = &JUnitError{
   116  				Message:     "interrupted",
   117  				Type:        "interrupted",
   118  				Description: interruptDescriptionForUnstructuredReporters(spec.Failure),
   119  			}
   120  			suite.Errors += 1
   121  		case types.SpecStateAborted:
   122  			test.Failure = &JUnitFailure{
   123  				Message:     spec.Failure.Message,
   124  				Type:        "aborted",
   125  				Description: fmt.Sprintf("%s\n%s", spec.Failure.Location.String(), spec.Failure.Location.FullStackTrace),
   126  			}
   127  			suite.Errors += 1
   128  		case types.SpecStatePanicked:
   129  			test.Error = &JUnitError{
   130  				Message:     spec.Failure.ForwardedPanic,
   131  				Type:        "panicked",
   132  				Description: fmt.Sprintf("%s\n%s", spec.Failure.Location.String(), spec.Failure.Location.FullStackTrace),
   133  			}
   134  			suite.Errors += 1
   135  		}
   136  
   137  		suite.TestCases = append(suite.TestCases, test)
   138  	}
   139  
   140  	junitReport := JUnitTestSuites{
   141  		Tests:      suite.Tests,
   142  		Disabled:   suite.Disabled + suite.Skipped,
   143  		Errors:     suite.Errors,
   144  		Failures:   suite.Failures,
   145  		Time:       suite.Time,
   146  		TestSuites: []JUnitTestSuite{suite},
   147  	}
   148  
   149  	f, err := os.Create(dst)
   150  	if err != nil {
   151  		return err
   152  	}
   153  	_, err2 := f.WriteString(xml.Header)
   154  	if err2 != nil {
   155  		klog.Error(err2)
   156  	}
   157  	encoder := xml.NewEncoder(f)
   158  	encoder.Indent("  ", "    ")
   159  	err3 := encoder.Encode(junitReport)
   160  	if err3 != nil {
   161  		klog.Error(err3)
   162  	}
   163  	return f.Close()
   164  }
   165  
   166  // This function generates folder structure for the rp_preproc tool with logs for upload in Report Portal
   167  func GenerateRPPreprocReport(report types.Report, rpPreprocParentDir string) {
   168  	rpPreprocDir := rpPreprocParentDir + "/rp_preproc"
   169  	//Delete directory, if exists
   170  	if _, err := os.Stat(rpPreprocDir); !os.IsNotExist(err) {
   171  		err2 := os.RemoveAll(rpPreprocDir)
   172  		if err2 != nil {
   173  			klog.Error(err2)
   174  		}
   175  	}
   176  	//Generate folder structure for RPPreproc with logs
   177  	for i := range report.SpecReports {
   178  		reportSpec := report.SpecReports[i]
   179  		//generate folders only for failed tests
   180  		if !reportSpec.Failure.IsZero() {
   181  			if reportSpec.LeafNodeType == types.NodeTypeIt {
   182  				name := shortenStringAddHash(reportSpec)
   183  				filePath := rpPreprocDir + "/attachments/xunit/" + name
   184  				if err3 := os.MkdirAll(filePath, os.ModePerm); err3 != nil {
   185  					klog.Error(err3)
   186  				} else {
   187  					writeLogInFile(filePath+"/ginkgoWriter.log", reportSpec.CapturedGinkgoWriterOutput)
   188  					writeLogInFile(filePath+"/stdOutErr.log", reportSpec.CapturedStdOutErr)
   189  					writeLogInFile(filePath+"/failureMessage.log", reportSpec.FailureMessage())
   190  					writeLogInFile(filePath+"/failureLocation.log", reportSpec.FailureLocation().FullStackTrace)
   191  				}
   192  			}
   193  		}
   194  	}
   195  }
   196  
   197  func writeLogInFile(filePath string, log string) {
   198  	// Do not create empty files
   199  	if len(log) != 0 {
   200  		f, err := os.Create(filePath)
   201  		if err != nil {
   202  			klog.Error(err)
   203  		}
   204  		defer f.Close()
   205  
   206  		_, err2 := f.WriteString(log)
   207  
   208  		if err2 != nil {
   209  			klog.Error(err2)
   210  		}
   211  	}
   212  }
   213  
   214  func interruptDescriptionForUnstructuredReporters(failure types.Failure) string {
   215  	out := &strings.Builder{}
   216  	out.WriteString(failure.Message + "\n")
   217  	NewDefaultReporter(types.ReporterConfig{NoColor: true}, out).EmitProgressReport(failure.ProgressReport)
   218  	return out.String()
   219  }
   220  
   221  func systemErrForUnstructuredReporters(spec types.SpecReport) string {
   222  	out := &strings.Builder{}
   223  	gw := spec.CapturedGinkgoWriterOutput
   224  	cursor := 0
   225  	for _, pr := range spec.ProgressReports {
   226  		if cursor < pr.TimelineLocation.Offset {
   227  			if pr.TimelineLocation.Offset < len(gw) {
   228  				out.WriteString(gw[cursor:pr.TimelineLocation.Offset])
   229  				cursor = pr.TimelineLocation.Offset
   230  			} else if cursor < len(gw) {
   231  				out.WriteString(gw[cursor:])
   232  				cursor = len(gw)
   233  			}
   234  		}
   235  		NewDefaultReporter(types.ReporterConfig{NoColor: true}, out).EmitProgressReport(pr)
   236  	}
   237  
   238  	if cursor < len(gw) {
   239  		out.WriteString(gw[cursor:])
   240  	}
   241  	return out.String()
   242  }
   243  
   244  func systemOutForUnstructuredReporters(spec types.SpecReport) string {
   245  	return spec.CapturedStdOutErr
   246  }
   247  
   248  func getClassnameFromReport(report types.SpecReport) string {
   249  	texts := []string{}
   250  	texts = append(texts, report.ContainerHierarchyTexts...)
   251  	if report.LeafNodeText != "" {
   252  		texts = append(texts, report.LeafNodeText)
   253  	}
   254  	if len(texts) > 0 {
   255  		classStrings := strings.Fields(texts[0])
   256  		return classStrings[0][1:]
   257  	} else {
   258  		return strings.Join(texts, " ")
   259  	}
   260  }
   261  
   262  // This function is used to shorten classname and add hash to prevent issues with filesystems(255 chars for folder name) and to avoid conflicts(same shortened name of a classname)
   263  func shortenStringAddHash(report types.SpecReport) string {
   264  	className := getClassnameFromReport(report)
   265  	s := report.FullText()
   266  	replacedClass := strings.Replace(s, className, "", 1)
   267  	if len(replacedClass) > 100 {
   268  		stringToHash := replacedClass[100:]
   269  		h := sha1.New()
   270  		h.Write([]byte(stringToHash))
   271  		sha1_hash := hex.EncodeToString(h.Sum(nil))
   272  		stringWithHash := replacedClass[0:100] + " sha: " + sha1_hash
   273  		return stringWithHash
   274  	} else {
   275  		return replacedClass
   276  	}
   277  }