github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/spyglass/lenses/junit/lens.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package junit provides a junit viewer for Spyglass
    18  package junit
    19  
    20  import (
    21  	"bytes"
    22  	"encoding/json"
    23  	"fmt"
    24  	"html/template"
    25  	"path/filepath"
    26  	"sort"
    27  	"time"
    28  
    29  	"github.com/GoogleCloudPlatform/testgrid/metadata/junit"
    30  	"github.com/sirupsen/logrus"
    31  
    32  	"sigs.k8s.io/prow/pkg/config"
    33  	"sigs.k8s.io/prow/pkg/spyglass/api"
    34  	"sigs.k8s.io/prow/pkg/spyglass/lenses"
    35  )
    36  
    37  const (
    38  	name                     = "junit"
    39  	title                    = "JUnit"
    40  	priority                 = 5
    41  	passedStatus  testStatus = "Passed"
    42  	failedStatus  testStatus = "Failed"
    43  	skippedStatus testStatus = "Skipped"
    44  )
    45  
    46  func init() {
    47  	lenses.RegisterLens(Lens{})
    48  }
    49  
    50  type testStatus string
    51  
    52  // Lens is the implementation of a JUnit-rendering Spyglass lens.
    53  type Lens struct{}
    54  
    55  type JVD struct {
    56  	NumTests int
    57  	Passed   []TestResult
    58  	Failed   []TestResult
    59  	Skipped  []TestResult
    60  	Flaky    []TestResult
    61  }
    62  
    63  // Config returns the lens's configuration.
    64  func (lens Lens) Config() lenses.LensConfig {
    65  	return lenses.LensConfig{
    66  		Name:     name,
    67  		Title:    title,
    68  		Priority: priority,
    69  	}
    70  }
    71  
    72  // Header renders the content of <head> from template.html.
    73  func (lens Lens) Header(artifacts []api.Artifact, resourceDir string, config json.RawMessage, spyglassConfig config.Spyglass) string {
    74  	t, err := template.ParseFiles(filepath.Join(resourceDir, "template.html"))
    75  	if err != nil {
    76  		return fmt.Sprintf("<!-- FAILED LOADING HEADER: %v -->", err)
    77  	}
    78  	var buf bytes.Buffer
    79  	if err := t.ExecuteTemplate(&buf, "header", nil); err != nil {
    80  		return fmt.Sprintf("<!-- FAILED EXECUTING HEADER TEMPLATE: %v -->", err)
    81  	}
    82  	return buf.String()
    83  }
    84  
    85  // Callback does nothing.
    86  func (lens Lens) Callback(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string {
    87  	return ""
    88  }
    89  
    90  type JunitResult struct {
    91  	junit.Result
    92  }
    93  
    94  func (jr JunitResult) Duration() time.Duration {
    95  	return time.Duration(jr.Time * float64(time.Second)).Round(time.Second)
    96  }
    97  
    98  func (jr JunitResult) Status() testStatus {
    99  	res := passedStatus
   100  	if jr.Skipped != nil {
   101  		res = skippedStatus
   102  	} else if jr.Failure != nil || jr.Errored != nil {
   103  		res = failedStatus
   104  	}
   105  	return res
   106  }
   107  
   108  func (jr JunitResult) SkippedReason() string {
   109  	res := ""
   110  	if jr.Skipped != nil {
   111  		res = jr.Message(-1) // Don't truncate
   112  	}
   113  	return res
   114  }
   115  
   116  // TestResult holds data about a test extracted from junit output
   117  type TestResult struct {
   118  	Junit []JunitResult
   119  	Link  string
   120  }
   121  
   122  // Body renders the <body> for JUnit tests
   123  func (lens Lens) Body(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string {
   124  	jvd := lens.getJvd(artifacts)
   125  
   126  	junitTemplate, err := template.ParseFiles(filepath.Join(resourceDir, "template.html"))
   127  	if err != nil {
   128  		logrus.WithError(err).Error("Error executing template.")
   129  		return fmt.Sprintf("Failed to load template file: %v", err)
   130  	}
   131  
   132  	var buf bytes.Buffer
   133  	if err := junitTemplate.ExecuteTemplate(&buf, "body", jvd); err != nil {
   134  		logrus.WithError(err).Error("Error executing template.")
   135  	}
   136  
   137  	return buf.String()
   138  }
   139  
   140  func (lens Lens) getJvd(artifacts []api.Artifact) JVD {
   141  	type testResults struct {
   142  		// Group results based on their full path name
   143  		junit [][]JunitResult
   144  		link  string
   145  		path  string
   146  		err   error
   147  	}
   148  	type testIdentifier struct {
   149  		suite string
   150  		class string
   151  		name  string
   152  	}
   153  	resultChan := make(chan testResults)
   154  	for _, artifact := range artifacts {
   155  		go func(artifact api.Artifact) {
   156  			groups := make(map[testIdentifier][]JunitResult)
   157  			var testsSequence []testIdentifier
   158  			result := testResults{
   159  				link: artifact.CanonicalLink(),
   160  				path: artifact.JobPath(),
   161  			}
   162  			var contents []byte
   163  			contents, result.err = artifact.ReadAll()
   164  			if result.err != nil {
   165  				logrus.WithError(result.err).WithField("artifact", artifact.CanonicalLink()).Warn("Error reading artifact")
   166  				resultChan <- result
   167  				return
   168  			}
   169  			var suites *junit.Suites
   170  			suites, result.err = junit.Parse(contents)
   171  			if result.err != nil {
   172  				logrus.WithError(result.err).WithField("artifact", artifact.CanonicalLink()).Info("Error parsing junit file.")
   173  				resultChan <- result
   174  				return
   175  			}
   176  			var record func(suite junit.Suite)
   177  			record = func(suite junit.Suite) {
   178  				for _, subSuite := range suite.Suites {
   179  					record(subSuite)
   180  				}
   181  
   182  				for _, test := range suite.Results {
   183  					// There are cases where multiple entries of exactly the same
   184  					// testcase in a single junit result file, this could result
   185  					// from reruns of test cases by `go test --count=N` where N>1.
   186  					// Deduplicate them here in this case, and classify a test as being
   187  					// flaky if it both succeeded and failed
   188  					k := testIdentifier{suite.Name, test.ClassName, test.Name}
   189  					groups[k] = append(groups[k], JunitResult{Result: test})
   190  					if len(groups[k]) == 1 {
   191  						testsSequence = append(testsSequence, k)
   192  					}
   193  				}
   194  			}
   195  			for _, suite := range suites.Suites {
   196  				record(suite)
   197  			}
   198  			for _, identifier := range testsSequence {
   199  				result.junit = append(result.junit, groups[identifier])
   200  			}
   201  			resultChan <- result
   202  		}(artifact)
   203  	}
   204  	results := make([]testResults, 0, len(artifacts))
   205  	for range artifacts {
   206  		results = append(results, <-resultChan)
   207  	}
   208  	sort.Slice(results, func(i, j int) bool { return results[i].path < results[j].path })
   209  
   210  	var jvd JVD
   211  	var duplicates int
   212  
   213  	for _, result := range results {
   214  		if result.err != nil {
   215  			continue
   216  		}
   217  		for _, tests := range result.junit {
   218  			var (
   219  				skipped bool
   220  				passed  bool
   221  				failed  bool
   222  				flaky   bool
   223  			)
   224  			for _, test := range tests {
   225  				// skipped test has no reason to rerun, so no deduplication
   226  				if test.Status() == skippedStatus {
   227  					skipped = true
   228  				} else if test.Status() == failedStatus {
   229  					if passed {
   230  						passed = false
   231  						failed = false
   232  						flaky = true
   233  					}
   234  					if !flaky {
   235  						failed = true
   236  					}
   237  				} else if failed { // Test succeeded but marked failed previously
   238  					passed = false
   239  					failed = false
   240  					flaky = true
   241  				} else if !flaky { // Test succeeded and not marked as flaky
   242  					passed = true
   243  				}
   244  			}
   245  
   246  			if skipped {
   247  				jvd.Skipped = append(jvd.Skipped, TestResult{
   248  					Junit: tests,
   249  					Link:  result.link,
   250  				})
   251  				// if the skipped test is a rerun of a failed test
   252  				if failed {
   253  					// store it as failed too
   254  					jvd.Failed = append(jvd.Failed, TestResult{
   255  						Junit: tests,
   256  						Link:  result.link,
   257  					})
   258  					// account for the duplication
   259  					duplicates++
   260  				}
   261  			} else if failed {
   262  				jvd.Failed = append(jvd.Failed, TestResult{
   263  					Junit: tests,
   264  					Link:  result.link,
   265  				})
   266  			} else if flaky {
   267  				jvd.Flaky = append(jvd.Flaky, TestResult{
   268  					Junit: tests,
   269  					Link:  result.link,
   270  				})
   271  			} else {
   272  				jvd.Passed = append(jvd.Passed, TestResult{
   273  					Junit: tests,
   274  					Link:  result.link,
   275  				})
   276  			}
   277  		}
   278  	}
   279  
   280  	jvd.NumTests = len(jvd.Passed) + len(jvd.Failed) + len(jvd.Flaky) + len(jvd.Skipped) - duplicates
   281  	return jvd
   282  }