github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/gcs.go (about)

     1  /*
     2  Copyright 2020 The TestGrid 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 updater
    18  
    19  import (
    20  	"fmt"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/sirupsen/logrus"
    27  
    28  	"github.com/GoogleCloudPlatform/testgrid/internal/result"
    29  	"github.com/GoogleCloudPlatform/testgrid/metadata"
    30  	"github.com/GoogleCloudPlatform/testgrid/metadata/junit"
    31  	statepb "github.com/GoogleCloudPlatform/testgrid/pb/state"
    32  	statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status"
    33  	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
    34  )
    35  
    36  // gcsResult holds all the downloaded information for a build of a job.
    37  //
    38  // The suite results become rows and the job metadata is added to the column.
    39  type gcsResult struct {
    40  	podInfo   gcs.PodInfo
    41  	started   gcs.Started
    42  	finished  gcs.Finished
    43  	suites    []gcs.SuitesMeta
    44  	job       string
    45  	build     string
    46  	malformed []string
    47  }
    48  
    49  // deadline to collect information (24 hours after the job starts or an hour after finishing).
    50  func (r gcsResult) deadline() time.Time {
    51  	f := r.finished.Timestamp
    52  	if f == nil {
    53  		return time.Unix(r.started.Timestamp, 0).Add(24 * time.Hour)
    54  	}
    55  	return time.Unix(*f, 0).Add(time.Hour)
    56  }
    57  
    58  const maxDuplicates = 20
    59  
    60  // EmailListKey is the expected metadata key for email addresses.
    61  const EmailListKey = "EmailAddresses"
    62  
    63  var overflowCell = Cell{
    64  	Result:  statuspb.TestStatus_FAIL,
    65  	Icon:    "...",
    66  	Message: "Too many duplicately named rows",
    67  }
    68  
    69  func propertyMap(r *junit.Result) map[string][]string {
    70  	out := map[string][]string{}
    71  	if r.Properties == nil {
    72  		return out
    73  	}
    74  	for _, p := range r.Properties.PropertyList {
    75  		out[p.Name] = append(out[p.Name], p.Value)
    76  	}
    77  	return out
    78  }
    79  
    80  // Means returns means for each given property's values.
    81  func Means(properties map[string][]string) map[string]float64 {
    82  	out := make(map[string]float64, len(properties))
    83  	for name, values := range properties {
    84  		var sum float64
    85  		var n int
    86  		for _, str := range values {
    87  			v, err := strconv.ParseFloat(str, 64)
    88  			if err != nil {
    89  				continue
    90  			}
    91  			sum += v
    92  			n++
    93  		}
    94  		if n == 0 {
    95  			continue
    96  		}
    97  		out[name] = sum / float64(n)
    98  	}
    99  	return out
   100  }
   101  
   102  func first(properties map[string][]string) map[string]string {
   103  	out := make(map[string]string, len(properties))
   104  	for k, v := range properties {
   105  		if len(v) == 0 {
   106  			continue
   107  		}
   108  		out[k] = v[0]
   109  	}
   110  	return out
   111  }
   112  
   113  const (
   114  	overallRow = "Overall"
   115  	podInfoRow = "Pod"
   116  )
   117  
   118  // MergeCells will combine the cells into a single result.
   119  //
   120  // The flaky argument determines whether returned result
   121  // is flaky (true) or failing when merging cells with both passing
   122  // and failing results.
   123  //
   124  // Merging multiple results will set the icon to n/N passes
   125  //
   126  // Includes the message from the "most relevant" cell that includes a message.
   127  // Where relevance is determined by result.GTE.
   128  func MergeCells(flaky bool, cells ...Cell) Cell {
   129  	var out Cell
   130  	if len(cells) == 0 {
   131  		panic("empty cells")
   132  	}
   133  	out = cells[0]
   134  
   135  	if len(cells) == 1 {
   136  		return out
   137  	}
   138  
   139  	var pass int
   140  	var passMsg string
   141  	var fail int
   142  	var failMsg string
   143  
   144  	// determine the status and potential messages
   145  	// gather all metrics
   146  	means := map[string][]float64{}
   147  
   148  	issues := map[string]bool{}
   149  
   150  	current := out.Result
   151  	passMessageResult := current
   152  	failMessageResult := current
   153  
   154  	for _, c := range cells {
   155  		if result.GTE(c.Result, current) {
   156  			current = c.Result
   157  		}
   158  		switch {
   159  		case result.Passing(c.Result):
   160  			pass++
   161  			if c.Message != "" && result.GTE(c.Result, passMessageResult) {
   162  				passMsg = c.Message
   163  				passMessageResult = c.Result
   164  			}
   165  		case result.Failing(c.Result):
   166  			fail++
   167  			if c.Message != "" && result.GTE(c.Result, failMessageResult) {
   168  				failMsg = c.Message
   169  				failMessageResult = c.Result
   170  			}
   171  		}
   172  
   173  		for metric, mean := range c.Metrics {
   174  			means[metric] = append(means[metric], mean)
   175  		}
   176  
   177  		for _, i := range c.Issues {
   178  			issues[i] = true
   179  		}
   180  	}
   181  
   182  	if n := len(issues); n > 0 {
   183  		out.Issues = make([]string, 0, len(issues))
   184  		for key := range issues {
   185  			out.Issues = append(out.Issues, key)
   186  		}
   187  		sort.Strings(out.Issues)
   188  	}
   189  
   190  	if flaky && pass > 0 && fail > 0 {
   191  		out.Result = statuspb.TestStatus_FLAKY
   192  	} else {
   193  		out.Result = current
   194  	}
   195  
   196  	// determine the icon
   197  	total := len(cells)
   198  	out.Icon = strconv.Itoa(pass) + "/" + strconv.Itoa(total)
   199  
   200  	// compile the message
   201  	var msg string
   202  	if failMsg != "" {
   203  		msg = failMsg
   204  	} else if passMsg != "" {
   205  		msg = passMsg
   206  	}
   207  
   208  	if msg != "" {
   209  		msg = ": " + msg
   210  	}
   211  	out.Message = out.Icon + " runs passed" + msg
   212  
   213  	// merge metrics
   214  	if len(means) > 0 {
   215  		out.Metrics = make(map[string]float64, len(means))
   216  		for metric, means := range means {
   217  			var sum float64
   218  			for _, m := range means {
   219  				sum += m
   220  			}
   221  			out.Metrics[metric] = sum / float64(len(means))
   222  		}
   223  	}
   224  	return out
   225  }
   226  
   227  // SplitCells appends a unique suffix to each cell.
   228  //
   229  // When an excessive number of cells contain the same name
   230  // the list gets truncated, replaced with a synthetic "... [overflow]" cell.
   231  func SplitCells(originalName string, cells ...Cell) map[string]Cell {
   232  	n := len(cells)
   233  	if n == 0 {
   234  		return nil
   235  	}
   236  	if n > maxDuplicates {
   237  		n = maxDuplicates
   238  	}
   239  	out := make(map[string]Cell, n)
   240  	for idx, c := range cells {
   241  		// Ensure each name is unique
   242  		// If we have multiple results with the same name foo
   243  		// then append " [n]" to the name so we wind up with:
   244  		//   foo
   245  		//   foo [1]
   246  		//   foo [2]
   247  		//   etc
   248  		name := originalName
   249  		switch idx {
   250  		case 0:
   251  			// nothing
   252  		case maxDuplicates:
   253  			name = name + " [overflow]"
   254  			out[name] = overflowCell
   255  			return out
   256  		default:
   257  			name = name + " [" + strconv.Itoa(idx) + "]"
   258  		}
   259  		out[name] = c
   260  	}
   261  	return out
   262  }
   263  
   264  // ignoreStatus returns whether to ignore (equate to "NO_RESULT") a given status based on configuration.
   265  func ignoreStatus(opt groupOptions, status statuspb.TestStatus) bool {
   266  	if status == statuspb.TestStatus_NO_RESULT {
   267  		return true
   268  	}
   269  	if opt.ignoreSkip && status == statuspb.TestStatus_PASS_WITH_SKIPS {
   270  		return true
   271  	}
   272  	// TODO(michelle192837): Implement `ignore_built`, e.g. ignore statuspb.TestStatus_BUILD_PASSED.
   273  	// TODO(michelle192837): Implement `ignore_pending`, e.g. ignore statuspb.TestStatus_RUNNING.
   274  	return false
   275  }
   276  
   277  // convertResult returns an InflatedColumn representation of the GCS result.
   278  func convertResult(log logrus.FieldLogger, nameCfg nameConfig, id string, headers []string, result gcsResult, opt groupOptions) InflatedColumn {
   279  	cells := map[string][]Cell{}
   280  	var cellID string
   281  	if nameCfg.multiJob {
   282  		cellID = result.job + "/" + id
   283  	} else if opt.addCellID {
   284  		cellID = id
   285  	}
   286  
   287  	meta := result.finished.Metadata.Strings()
   288  	version := metadata.Version(result.started.Started, result.finished.Finished)
   289  
   290  	// Append each result into the column
   291  	for _, suite := range result.suites {
   292  		for _, r := range flattenResults(suite.Suites.Suites...) {
   293  			// "skipped" is the string that is always appended when the test is skipped without any reason in Ginkgo V2, e.g., "focus" is specified, and the test is skipped.
   294  			if r.Skipped != nil && r.Skipped.Value == "" && (r.Skipped.Message == "skipped" || r.Skipped.Message == "") {
   295  				continue
   296  			}
   297  			c := Cell{CellID: cellID}
   298  			if elapsed := r.Time; elapsed > 0 {
   299  				c.Metrics = setElapsed(c.Metrics, elapsed)
   300  			}
   301  
   302  			props := propertyMap(&r)
   303  			for metric, mean := range Means(props) {
   304  				if c.Metrics == nil {
   305  					c.Metrics = map[string]float64{}
   306  				}
   307  				c.Metrics[metric] = mean
   308  			}
   309  
   310  			const max = 140
   311  			if msg := r.Message(max); msg != "" {
   312  				c.Message = msg
   313  			}
   314  
   315  			switch {
   316  			case r.Errored != nil:
   317  				c.Result = statuspb.TestStatus_FAIL
   318  				if c.Message != "" {
   319  					c.Icon = "F"
   320  				}
   321  			case r.Failure != nil:
   322  				c.Result = statuspb.TestStatus_FAIL
   323  				if c.Message != "" {
   324  					c.Icon = "F"
   325  				}
   326  			case r.Skipped != nil:
   327  				c.Result = statuspb.TestStatus_PASS_WITH_SKIPS
   328  				c.Icon = "S"
   329  			default:
   330  				c.Result = statuspb.TestStatus_PASS
   331  			}
   332  
   333  			if override := CustomStatus(opt.rules, jUnitTestResult{&r}); override != nil {
   334  				c.Result = *override
   335  			}
   336  
   337  			if ignoreStatus(opt, c.Result) {
   338  				continue
   339  			}
   340  
   341  			for _, annotation := range opt.annotations {
   342  				_, ok := props[annotation.GetPropertyName()]
   343  				if !ok {
   344  					continue
   345  				}
   346  				c.Icon = annotation.ShortText
   347  				break
   348  			}
   349  
   350  			if f, ok := c.Metrics[opt.metricKey]; ok {
   351  				c.Icon = strconv.FormatFloat(f, 'g', 4, 64)
   352  			}
   353  
   354  			if values, ok := props[opt.userKey]; ok && len(values) > 0 {
   355  				c.UserProperty = values[0]
   356  			}
   357  
   358  			name := nameCfg.render(result.job, r.Name, first(props), suite.Metadata, meta)
   359  			cells[name] = append(cells[name], c)
   360  		}
   361  	}
   362  
   363  	overall := overallCell(result)
   364  	if overall.Result == statuspb.TestStatus_FAIL && overall.Message == "" { // Ensure failing build has a failing cell and/or overall message
   365  		var found bool
   366  		for _, namedCells := range cells {
   367  			for _, c := range namedCells {
   368  				if c.Result == statuspb.TestStatus_FAIL {
   369  					found = true // Failing test, huzzah!
   370  					break
   371  				}
   372  			}
   373  			if found {
   374  				break
   375  			}
   376  		}
   377  		if !found { // Nope, add the F icon and an explanatory Message
   378  			overall.Icon = "F"
   379  			overall.Message = "Build failed outside of test results"
   380  		}
   381  	}
   382  	injectedCells := map[string]Cell{
   383  		overallRow: overall,
   384  	}
   385  
   386  	if opt.analyzeProwJob {
   387  		if pic := podInfoCell(result); pic.Message != gcs.MissingPodInfo || overall.Result != statuspb.TestStatus_RUNNING {
   388  			injectedCells[podInfoRow] = pic
   389  		}
   390  	}
   391  
   392  	for name, c := range injectedCells {
   393  		c.CellID = cellID
   394  		jobName := result.job + "." + name
   395  		cells[jobName] = append([]Cell{c}, cells[jobName]...)
   396  		if nameCfg.multiJob {
   397  			cells[name] = append([]Cell{c}, cells[name]...)
   398  		}
   399  	}
   400  
   401  	buildID := id
   402  	if opt.buildKey != "" {
   403  		metadata := result.finished.Metadata.Strings()
   404  		if metadata != nil {
   405  			buildID = metadata[opt.buildKey]
   406  		}
   407  		if buildID == "" {
   408  			log.WithFields(logrus.Fields{
   409  				"metadata":         result.finished.Metadata.Strings(),
   410  				"overrideBuildKey": opt.buildKey,
   411  			}).Warning("No override build ID found in metadata.")
   412  		}
   413  	}
   414  
   415  	out := InflatedColumn{
   416  		Column: &statepb.Column{
   417  			Build:   buildID,
   418  			Started: float64(result.started.Timestamp * 1000),
   419  			Hint:    id,
   420  		},
   421  		Cells: map[string]Cell{},
   422  	}
   423  
   424  	for name, cells := range cells {
   425  		switch {
   426  		case opt.merge:
   427  			out.Cells[name] = MergeCells(true, cells...)
   428  		default:
   429  			for n, c := range SplitCells(name, cells...) {
   430  				out.Cells[n] = c
   431  			}
   432  		}
   433  	}
   434  
   435  	for _, h := range headers {
   436  		val, ok := meta[h]
   437  		if !ok && h == "Commit" && version != metadata.Missing {
   438  			val = version
   439  		} else if !ok && overall.Result != statuspb.TestStatus_RUNNING {
   440  			val = "missing"
   441  		}
   442  		out.Column.Extra = append(out.Column.Extra, val)
   443  	}
   444  
   445  	emails, found := result.finished.Finished.Metadata.MultiString(EmailListKey)
   446  	if len(emails) == 0 && found {
   447  		log.Error("failed to extract dynamic email list, the list is empty or cannot convert to []string")
   448  	}
   449  	out.Column.EmailAddresses = emails
   450  	return out
   451  }
   452  
   453  func podInfoCell(result gcsResult) Cell {
   454  	podInfo := result.podInfo
   455  	pass, msg := podInfo.Summarize()
   456  	var status statuspb.TestStatus
   457  	var icon string
   458  	switch {
   459  	case msg == gcs.MissingPodInfo && time.Now().Before(result.deadline()):
   460  		status = statuspb.TestStatus_RUNNING // Try and reprocess it next time.
   461  	case msg == gcs.MissingPodInfo:
   462  		status = statuspb.TestStatus_PASS_WITH_SKIPS // Probably won't receive it.
   463  	case pass:
   464  		status = statuspb.TestStatus_PASS
   465  	default:
   466  		status = statuspb.TestStatus_FAIL
   467  	}
   468  
   469  	switch {
   470  	case msg == gcs.NoPodUtils:
   471  		icon = "E"
   472  	case msg == gcs.MissingPodInfo:
   473  		icon = "!"
   474  	case !pass:
   475  		icon = "F"
   476  	}
   477  
   478  	return Cell{
   479  		Message: msg,
   480  		Icon:    icon,
   481  		Result:  status,
   482  	}
   483  }
   484  
   485  // overallCell generates the overall cell for this GCS result.
   486  func overallCell(result gcsResult) Cell {
   487  	var c Cell
   488  	var finished int64
   489  	if result.finished.Timestamp != nil {
   490  		finished = *result.finished.Timestamp
   491  	}
   492  	switch {
   493  	case len(result.malformed) > 0:
   494  		c.Result = statuspb.TestStatus_FAIL
   495  		c.Message = fmt.Sprintf("Malformed artifacts: %s", strings.Join(result.malformed, ", "))
   496  		c.Icon = "E"
   497  	case finished > 0: // completed result
   498  		var passed bool
   499  		res := result.finished.Result
   500  		switch {
   501  		case result.finished.Passed == nil:
   502  			if res != "" {
   503  				passed = res == "SUCCESS"
   504  				c.Icon = "E"
   505  				c.Message = fmt.Sprintf(`finished.json missing "passed": %t`, passed)
   506  			}
   507  		case result.finished.Passed != nil:
   508  			passed = *result.finished.Passed
   509  		}
   510  
   511  		if passed {
   512  			c.Result = statuspb.TestStatus_PASS
   513  		} else {
   514  			c.Result = statuspb.TestStatus_FAIL
   515  		}
   516  		c.Metrics = setElapsed(nil, float64(finished-result.started.Timestamp))
   517  	case time.Now().After(result.deadline()):
   518  		c.Result = statuspb.TestStatus_FAIL
   519  		c.Message = "Build did not complete within 24 hours"
   520  		c.Icon = "T"
   521  	default:
   522  		c.Result = statuspb.TestStatus_RUNNING
   523  		c.Message = "Build still running..."
   524  		c.Icon = "R"
   525  	}
   526  	return c
   527  }
   528  
   529  // ElapsedKey is the key for the target duration metric.
   530  const ElapsedKey = "test-duration-minutes"
   531  
   532  // TestMethodsElapsedKey is the key for the test results duration metric.
   533  const TestMethodsElapsedKey = "test-methods-duration-minutes"
   534  
   535  // setElapsed inserts the seconds-elapsed metric.
   536  func setElapsed(metrics map[string]float64, seconds float64) map[string]float64 {
   537  	if metrics == nil {
   538  		metrics = map[string]float64{}
   539  	}
   540  	metrics[ElapsedKey] = seconds / 60
   541  	return metrics
   542  }
   543  
   544  // flattenResults returns the DFS of all junit results in all suites.
   545  func flattenResults(suites ...junit.Suite) []junit.Result {
   546  	var results []junit.Result
   547  	for _, suite := range suites {
   548  		for _, innerSuite := range suite.Suites {
   549  			innerSuite.Name = dotName(suite.Name, innerSuite.Name)
   550  			results = append(results, flattenResults(innerSuite)...)
   551  		}
   552  		for _, r := range suite.Results {
   553  			r.Name = dotName(suite.Name, r.Name)
   554  			results = append(results, r)
   555  		}
   556  	}
   557  	return results
   558  }
   559  
   560  // dotName returns left.right or left or right
   561  func dotName(left, right string) string {
   562  	if left != "" && right != "" {
   563  		return left + "." + right
   564  	}
   565  	if right == "" {
   566  		return left
   567  	}
   568  	return right
   569  }