github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/gcs_test.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  	"reflect"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/google/go-cmp/cmp"
    26  	"github.com/sirupsen/logrus"
    27  	"google.golang.org/protobuf/testing/protocmp"
    28  	core "k8s.io/api/core/v1"
    29  
    30  	"github.com/GoogleCloudPlatform/testgrid/metadata"
    31  	"github.com/GoogleCloudPlatform/testgrid/metadata/junit"
    32  	configpb "github.com/GoogleCloudPlatform/testgrid/pb/config"
    33  	statepb "github.com/GoogleCloudPlatform/testgrid/pb/state"
    34  	statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status"
    35  	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
    36  )
    37  
    38  func TestMergeCells(t *testing.T) {
    39  	cases := []struct {
    40  		name     string
    41  		flaky    bool
    42  		cells    []Cell
    43  		expected Cell
    44  	}{
    45  		{
    46  			name: "basically works",
    47  			cells: []Cell{
    48  				{
    49  					Result:  statuspb.TestStatus_TOOL_FAIL,
    50  					CellID:  "random",
    51  					Icon:    "religious",
    52  					Message: "empty",
    53  					Metrics: map[string]float64{
    54  						"answer":   42,
    55  						"question": 1,
    56  					},
    57  				},
    58  			},
    59  			expected: Cell{
    60  				Result:  statuspb.TestStatus_TOOL_FAIL,
    61  				CellID:  "random",
    62  				Icon:    "religious",
    63  				Message: "empty",
    64  				Metrics: map[string]float64{
    65  					"answer":   42,
    66  					"question": 1,
    67  				},
    68  			},
    69  		},
    70  		{
    71  			name: "passes work and take highest filled message",
    72  			cells: []Cell{
    73  				{
    74  					Result: statuspb.TestStatus_PASS,
    75  					Icon:   "drop",
    76  				},
    77  				{
    78  					Result:  statuspb.TestStatus_BUILD_PASSED,
    79  					Message: "woah",
    80  				},
    81  				{
    82  					Result: statuspb.TestStatus_PASS_WITH_ERRORS, // highest but empty
    83  				},
    84  				{
    85  					Result:  statuspb.TestStatus_PASS_WITH_SKIPS, // highest with message
    86  					Message: "already got one",
    87  				},
    88  				{
    89  					Result:  statuspb.TestStatus_PASS,
    90  					Message: "there",
    91  				},
    92  			},
    93  			expected: Cell{
    94  				Result:  statuspb.TestStatus_PASS_WITH_ERRORS,
    95  				Message: "5/5 runs passed: already got one",
    96  				Icon:    "5/5",
    97  			},
    98  		},
    99  		{
   100  			name: "merge metrics",
   101  			cells: []Cell{
   102  				{
   103  					Result: statuspb.TestStatus_PASS,
   104  					Metrics: map[string]float64{
   105  						"common": 1,
   106  						"first":  1,
   107  					},
   108  				},
   109  				{
   110  					Result: statuspb.TestStatus_PASS,
   111  					Metrics: map[string]float64{
   112  						"common": 2,
   113  						"second": 2,
   114  					},
   115  				},
   116  				{
   117  					Result: statuspb.TestStatus_PASS,
   118  					Metrics: map[string]float64{
   119  						"common": 108, // total 111
   120  						"third":  3,
   121  					},
   122  				},
   123  			},
   124  			expected: Cell{
   125  				Result:  statuspb.TestStatus_PASS,
   126  				Message: "3/3 runs passed",
   127  				Icon:    "3/3",
   128  				Metrics: map[string]float64{
   129  					"common": 37,
   130  					"first":  1,
   131  					"second": 2,
   132  					"third":  3,
   133  				},
   134  			},
   135  		},
   136  		{
   137  			name: "issues",
   138  			cells: []Cell{
   139  				{
   140  					Result: statuspb.TestStatus_PASS,
   141  					Issues: []string{"a", "b", "common"},
   142  				},
   143  				{
   144  					Result: statuspb.TestStatus_PASS,
   145  					Issues: []string{"common", "c"},
   146  				},
   147  				{
   148  					Result: statuspb.TestStatus_PASS,
   149  					Issues: []string{"common", "d"},
   150  				},
   151  			},
   152  			expected: Cell{
   153  				Result:  statuspb.TestStatus_PASS,
   154  				Message: "3/3 runs passed",
   155  				Icon:    "3/3",
   156  				Issues: []string{
   157  					"a",
   158  					"b",
   159  					"c",
   160  					"common",
   161  					"d",
   162  				},
   163  			},
   164  		},
   165  		{
   166  			name: "failures take highest failure and highest failure message",
   167  			cells: []Cell{
   168  				{
   169  					Result:  statuspb.TestStatus_TIMED_OUT,
   170  					Message: "agonizingly slow",
   171  					Icon:    "drop",
   172  				},
   173  				{
   174  					Result:  statuspb.TestStatus_CATEGORIZED_FAIL, //highest with message
   175  					Icon:    "drop",
   176  					Message: "categorically wrong",
   177  				},
   178  				{
   179  					Result: statuspb.TestStatus_BUILD_FAIL, // highest
   180  					Icon:   "drop",
   181  				},
   182  			},
   183  			expected: Cell{
   184  				Result:  statuspb.TestStatus_BUILD_FAIL,
   185  				Icon:    "0/3",
   186  				Message: "0/3 runs passed: categorically wrong",
   187  			},
   188  		},
   189  		{
   190  			name:  "mix of passes and failures flake upon request",
   191  			flaky: true,
   192  			cells: []Cell{
   193  				{
   194  					Result:  statuspb.TestStatus_PASS,
   195  					Message: "yay",
   196  				},
   197  				{
   198  					Result:  statuspb.TestStatus_FAIL,
   199  					Message: "boom",
   200  				},
   201  			},
   202  			expected: Cell{
   203  				Result:  statuspb.TestStatus_FLAKY,
   204  				Icon:    "1/2",
   205  				Message: "1/2 runs passed: boom",
   206  			},
   207  		},
   208  		{
   209  			name: "mix of passes and failures will fail upon request",
   210  			cells: []Cell{
   211  				{
   212  					Result:  statuspb.TestStatus_PASS,
   213  					Message: "yay",
   214  				},
   215  				{
   216  					Result:  statuspb.TestStatus_TOOL_FAIL,
   217  					Message: "boom",
   218  				},
   219  				{
   220  					Result:  statuspb.TestStatus_FAIL, // highest result.GTE
   221  					Message: "bang",
   222  				},
   223  				{
   224  					Result:  statuspb.TestStatus_BUILD_FAIL,
   225  					Message: "missing ;",
   226  				},
   227  			},
   228  			expected: Cell{
   229  				Result:  statuspb.TestStatus_FAIL,
   230  				Icon:    "1/4",
   231  				Message: "1/4 runs passed: bang",
   232  			},
   233  		},
   234  	}
   235  
   236  	for _, tc := range cases {
   237  		t.Run(tc.name, func(t *testing.T) {
   238  			got := MergeCells(tc.flaky, tc.cells...)
   239  			if diff := cmp.Diff(tc.expected, got); diff != "" {
   240  				t.Errorf("MergeCells() got unexpected diff (-want +got):\n%s", diff)
   241  			}
   242  		})
   243  	}
   244  }
   245  
   246  func TestSplitCells(t *testing.T) {
   247  	const cellName = "foo"
   248  	cases := []struct {
   249  		name     string
   250  		cells    []Cell
   251  		expected map[string]Cell
   252  	}{
   253  		{
   254  			name: "basically works",
   255  		},
   256  		{
   257  			name:  "single item returns that item",
   258  			cells: []Cell{{Message: "hi"}},
   259  			expected: map[string]Cell{
   260  				"foo": {Message: "hi"},
   261  			},
   262  		},
   263  		{
   264  			name: "multiple items have [1] starting from second",
   265  			cells: []Cell{
   266  				{Message: "first"},
   267  				{Message: "second"},
   268  				{Message: "third"},
   269  			},
   270  			expected: map[string]Cell{
   271  				"foo":     {Message: "first"},
   272  				"foo [1]": {Message: "second"},
   273  				"foo [2]": {Message: "third"},
   274  			},
   275  		},
   276  		{
   277  			name: "many items eventually truncate",
   278  			cells: func() []Cell {
   279  				var out []Cell
   280  				for i := 0; i < maxDuplicates*2; i++ {
   281  					out = append(out, Cell{Icon: fmt.Sprintf("row %d", i)})
   282  				}
   283  				return out
   284  			}(),
   285  			expected: func() map[string]Cell {
   286  				out := map[string]Cell{}
   287  				out[cellName] = Cell{Icon: "row 0"}
   288  				for i := 1; i < maxDuplicates; i++ {
   289  					name := fmt.Sprintf("%s [%d]", cellName, i)
   290  					out[name] = Cell{Icon: fmt.Sprintf("row %d", i)}
   291  				}
   292  				out[cellName+" [overflow]"] = overflowCell
   293  				return out
   294  			}(),
   295  		},
   296  	}
   297  
   298  	for _, tc := range cases {
   299  		t.Run(tc.name, func(t *testing.T) {
   300  			actual := SplitCells(cellName, tc.cells...)
   301  			if diff := cmp.Diff(actual, tc.expected); diff != "" {
   302  				t.Errorf("SplitCells() got unexpected diff (-have, +want):\n%s", diff)
   303  			}
   304  		})
   305  	}
   306  }
   307  
   308  func TestConvertResult(t *testing.T) {
   309  	pint := func(v int64) *int64 {
   310  		return &v
   311  	}
   312  	pstr := func(s string) *string {
   313  		return &s
   314  	}
   315  	yes := true
   316  	now := time.Now().Unix()
   317  	cases := []struct {
   318  		name     string
   319  		nameCfg  nameConfig
   320  		id       string
   321  		headers  []string
   322  		result   gcsResult
   323  		opt      groupOptions
   324  		expected InflatedColumn
   325  	}{
   326  		{
   327  			name: "basically works",
   328  			expected: InflatedColumn{
   329  				Column: &statepb.Column{},
   330  				Cells: map[string]Cell{
   331  					"." + overallRow: {
   332  						Result:  statuspb.TestStatus_FAIL,
   333  						Icon:    "T",
   334  						Message: "Build did not complete within 24 hours",
   335  					},
   336  				},
   337  			},
   338  		},
   339  		{
   340  			name: "pass dynamic email list",
   341  			result: gcsResult{
   342  				finished: gcs.Finished{
   343  					Finished: metadata.Finished{
   344  						Metadata: metadata.Metadata{
   345  							EmailListKey: []string{"world"},
   346  						},
   347  					},
   348  				},
   349  			},
   350  			expected: InflatedColumn{
   351  				Column: &statepb.Column{
   352  					EmailAddresses: []string{"world"},
   353  				},
   354  				Cells: map[string]Cell{
   355  					"." + overallRow: {
   356  						Result:  statuspb.TestStatus_FAIL,
   357  						Icon:    "T",
   358  						Message: "Build did not complete within 24 hours",
   359  					},
   360  				},
   361  			},
   362  		},
   363  		{
   364  			name: "pass dynamic email list as list of interaces",
   365  			result: gcsResult{
   366  				finished: gcs.Finished{
   367  					Finished: metadata.Finished{
   368  						Metadata: metadata.Metadata{
   369  							EmailListKey: []interface{}{"world"},
   370  						},
   371  					},
   372  				},
   373  			},
   374  			expected: InflatedColumn{
   375  				Column: &statepb.Column{
   376  					EmailAddresses: []string{"world"},
   377  				},
   378  				Cells: map[string]Cell{
   379  					"." + overallRow: {
   380  						Result:  statuspb.TestStatus_FAIL,
   381  						Icon:    "T",
   382  						Message: "Build did not complete within 24 hours",
   383  					},
   384  				},
   385  			},
   386  		},
   387  		{
   388  			name: "multiple dynamic email list",
   389  			result: gcsResult{
   390  				finished: gcs.Finished{
   391  					Finished: metadata.Finished{
   392  						Metadata: metadata.Metadata{
   393  							EmailListKey: []string{"world", "olam"},
   394  						},
   395  					},
   396  				},
   397  			},
   398  			expected: InflatedColumn{
   399  				Column: &statepb.Column{
   400  					EmailAddresses: []string{"world", "olam"},
   401  				},
   402  				Cells: map[string]Cell{
   403  					"." + overallRow: {
   404  						Result:  statuspb.TestStatus_FAIL,
   405  						Icon:    "T",
   406  						Message: "Build did not complete within 24 hours",
   407  					},
   408  				},
   409  			},
   410  		},
   411  		{
   412  			name: "not a list",
   413  			result: gcsResult{
   414  				finished: gcs.Finished{
   415  					Finished: metadata.Finished{
   416  						Metadata: metadata.Metadata{
   417  							EmailListKey: "olam",
   418  						},
   419  					},
   420  				},
   421  			},
   422  			expected: InflatedColumn{
   423  				Column: &statepb.Column{
   424  					EmailAddresses: []string{},
   425  				},
   426  				Cells: map[string]Cell{
   427  					"." + overallRow: {
   428  						Result:  statuspb.TestStatus_FAIL,
   429  						Icon:    "T",
   430  						Message: "Build did not complete within 24 hours",
   431  					},
   432  				},
   433  			},
   434  		},
   435  		{
   436  			name: "invalid email list",
   437  			result: gcsResult{
   438  				finished: gcs.Finished{
   439  					Finished: metadata.Finished{
   440  						Metadata: metadata.Metadata{
   441  							EmailListKey: []interface{}{1},
   442  						},
   443  					},
   444  				},
   445  			},
   446  			expected: InflatedColumn{
   447  				Column: &statepb.Column{
   448  					EmailAddresses: []string{},
   449  				},
   450  				Cells: map[string]Cell{
   451  					"." + overallRow: {
   452  						Result:  statuspb.TestStatus_FAIL,
   453  						Icon:    "T",
   454  						Message: "Build did not complete within 24 hours",
   455  					},
   456  				},
   457  			},
   458  		},
   459  		{
   460  			name:    "correct column information",
   461  			headers: []string{"Commit", "hello", "spam", "do not have this one"},
   462  			id:      "hello",
   463  			result: gcsResult{
   464  				started: gcs.Started{
   465  					Started: metadata.Started{
   466  						Timestamp: 300,
   467  					},
   468  				},
   469  				finished: gcs.Finished{
   470  					Finished: metadata.Finished{
   471  						Metadata: metadata.Metadata{
   472  							"hello":             "world",
   473  							"spam":              "eggs",
   474  							metadata.JobVersion: "1.2.3",
   475  						},
   476  					},
   477  				},
   478  			},
   479  			expected: InflatedColumn{
   480  				Column: &statepb.Column{
   481  					Build:   "hello",
   482  					Hint:    "hello",
   483  					Started: 300 * 1000,
   484  					Extra: []string{
   485  						"1.2.3",
   486  						"world",
   487  						"eggs",
   488  						"missing",
   489  					},
   490  				},
   491  				Cells: map[string]Cell{
   492  					"." + overallRow: {
   493  						Result:  statuspb.TestStatus_FAIL,
   494  						Icon:    "T",
   495  						Message: "Build did not complete within 24 hours",
   496  					},
   497  				},
   498  			},
   499  		},
   500  		{
   501  			name:    "running results do not have missing column headers",
   502  			headers: []string{"Commit", "hello", "spam", "do not have this one"},
   503  			id:      "hello",
   504  			result: gcsResult{
   505  				started: gcs.Started{
   506  					Started: metadata.Started{
   507  						Timestamp: now,
   508  					},
   509  				},
   510  				finished: gcs.Finished{
   511  					Finished: metadata.Finished{
   512  						Metadata: metadata.Metadata{
   513  							"hello":             "world",
   514  							"spam":              "eggs",
   515  							metadata.JobVersion: "1.2.3",
   516  						},
   517  					},
   518  				},
   519  			},
   520  			expected: InflatedColumn{
   521  				Column: &statepb.Column{
   522  					Build:   "hello",
   523  					Hint:    "hello",
   524  					Started: float64(now * 1000),
   525  					Extra: []string{
   526  						"1.2.3",
   527  						"world",
   528  						"eggs",
   529  						"", // not missing
   530  					},
   531  				},
   532  				Cells: map[string]Cell{
   533  					"." + overallRow: {
   534  						Result:  statuspb.TestStatus_RUNNING,
   535  						Icon:    "R",
   536  						Message: "Build still running...",
   537  					},
   538  				},
   539  			},
   540  		},
   541  		{
   542  			name: "add overall when multiJob",
   543  			id:   "build",
   544  			nameCfg: nameConfig{
   545  				format:   "%s.%s",
   546  				parts:    []string{jobName, testsName},
   547  				multiJob: true,
   548  			},
   549  			result: gcsResult{
   550  				started: gcs.Started{
   551  					Started: metadata.Started{
   552  						Timestamp: now,
   553  					},
   554  				},
   555  				finished: gcs.Finished{
   556  					Finished: metadata.Finished{
   557  						Timestamp: pint(now + 1),
   558  					},
   559  				},
   560  				suites: []gcs.SuitesMeta{
   561  					{
   562  						Suites: &junit.Suites{
   563  							Suites: []junit.Suite{
   564  								{
   565  									Name: "this",
   566  									Results: []junit.Result{
   567  										{
   568  											Name: "that",
   569  										},
   570  									},
   571  								},
   572  							},
   573  						},
   574  					},
   575  				},
   576  				job: "job-name",
   577  			},
   578  			expected: InflatedColumn{
   579  				Column: &statepb.Column{
   580  					Started: float64(now * 1000),
   581  					Build:   "build",
   582  					Hint:    "build",
   583  				},
   584  				Cells: map[string]Cell{
   585  					overallRow: {
   586  						Result:  statuspb.TestStatus_FAIL,
   587  						Icon:    "F",
   588  						Message: "Build failed outside of test results",
   589  						Metrics: setElapsed(nil, 1),
   590  						CellID:  "job-name/build",
   591  					},
   592  					"job-name.Overall": {
   593  						Result:  statuspb.TestStatus_FAIL,
   594  						Icon:    "F",
   595  						Message: "Build failed outside of test results",
   596  						Metrics: setElapsed(nil, 1),
   597  						CellID:  "job-name/build",
   598  					},
   599  					"job-name.this.that": {
   600  						Result: statuspb.TestStatus_PASS,
   601  						CellID: "job-name/build",
   602  					},
   603  				},
   604  			},
   605  		},
   606  		{
   607  			name: "include job name upon request",
   608  			nameCfg: nameConfig{
   609  				format: "%s.%s",
   610  				parts:  []string{jobName, testsName},
   611  			},
   612  			result: gcsResult{
   613  				started: gcs.Started{
   614  					Started: metadata.Started{
   615  						Timestamp: now,
   616  					},
   617  				},
   618  				finished: gcs.Finished{
   619  					Finished: metadata.Finished{
   620  						Timestamp: pint(now + 1),
   621  					},
   622  				},
   623  				suites: []gcs.SuitesMeta{
   624  					{
   625  						Suites: &junit.Suites{
   626  							Suites: []junit.Suite{
   627  								{
   628  									Name: "this",
   629  									Results: []junit.Result{
   630  										{
   631  											Name: "that",
   632  										},
   633  									},
   634  								},
   635  							},
   636  						},
   637  					},
   638  				},
   639  				job: "job-name",
   640  			},
   641  			expected: InflatedColumn{
   642  				Column: &statepb.Column{
   643  					Started: float64(now * 1000),
   644  				},
   645  				Cells: map[string]Cell{
   646  					"job-name." + overallRow: {
   647  						Result:  statuspb.TestStatus_FAIL,
   648  						Icon:    "F",
   649  						Message: "Build failed outside of test results",
   650  						Metrics: setElapsed(nil, 1),
   651  					},
   652  					"job-name.this.that": {
   653  						Result: statuspb.TestStatus_PASS,
   654  					},
   655  				},
   656  			},
   657  		},
   658  		{
   659  			name: "failing job with only passing results has a failing overall message",
   660  			nameCfg: nameConfig{
   661  				format: "%s",
   662  				parts:  []string{testsName},
   663  			},
   664  			result: gcsResult{
   665  				started: gcs.Started{
   666  					Started: metadata.Started{
   667  						Timestamp: now,
   668  					},
   669  				},
   670  				finished: gcs.Finished{
   671  					Finished: metadata.Finished{
   672  						Timestamp: pint(now + 1),
   673  					},
   674  				},
   675  				suites: []gcs.SuitesMeta{
   676  					{
   677  						Suites: &junit.Suites{
   678  							Suites: []junit.Suite{
   679  								{
   680  									Name: "this",
   681  									Results: []junit.Result{
   682  										{
   683  											Name: "that",
   684  										},
   685  									},
   686  								},
   687  							},
   688  						},
   689  					},
   690  				},
   691  			},
   692  			expected: InflatedColumn{
   693  				Column: &statepb.Column{
   694  					Started: float64(now * 1000),
   695  				},
   696  				Cells: map[string]Cell{
   697  					"." + overallRow: {
   698  						Result:  statuspb.TestStatus_FAIL,
   699  						Icon:    "F",
   700  						Message: "Build failed outside of test results",
   701  						Metrics: setElapsed(nil, 1),
   702  					},
   703  					"this.that": {
   704  						Result: statuspb.TestStatus_PASS,
   705  					},
   706  				},
   707  			},
   708  		},
   709  		{
   710  			name: "result fields parsed properly",
   711  			nameCfg: nameConfig{
   712  				format: "%s",
   713  				parts:  []string{testsName},
   714  			},
   715  			result: gcsResult{
   716  				started: gcs.Started{
   717  					Started: metadata.Started{
   718  						Timestamp: now,
   719  					},
   720  				},
   721  				finished: gcs.Finished{
   722  					Finished: metadata.Finished{
   723  						Timestamp: pint(now + 1),
   724  					},
   725  				},
   726  				suites: []gcs.SuitesMeta{
   727  					{
   728  						Suites: &junit.Suites{
   729  							Suites: []junit.Suite{
   730  								{
   731  									Results: []junit.Result{
   732  										{
   733  											Name: "elapsed",
   734  											Time: 5,
   735  										},
   736  										{
   737  											Name:    "failed no message",
   738  											Failure: &junit.Failure{Value: *pstr("")},
   739  										},
   740  										{
   741  											Name:    "failed",
   742  											Failure: &junit.Failure{Value: *pstr("boom")},
   743  										},
   744  										{
   745  											Name:    "failed other message",
   746  											Failure: &junit.Failure{Value: *pstr("")},
   747  											Output:  pstr("irrelevant message"),
   748  										},
   749  										{
   750  											Name:    "errored no message",
   751  											Failure: &junit.Failure{Value: *pstr("")},
   752  										},
   753  										{
   754  											Name:    "errored",
   755  											Failure: &junit.Failure{Value: *pstr("oh no")},
   756  										},
   757  										{
   758  											Name:    "invisible skip",
   759  											Skipped: &junit.Skipped{Value: *pstr("")},
   760  										},
   761  										{
   762  											Name:    "visible skip",
   763  											Skipped: &junit.Skipped{Value: *pstr("tl;dr")},
   764  										},
   765  										{
   766  											Name:  "stderr message",
   767  											Error: pstr("ouch"),
   768  										},
   769  										{
   770  											Name:   "stdout message",
   771  											Output: pstr("bellybutton"),
   772  										},
   773  									},
   774  								},
   775  							},
   776  						},
   777  					},
   778  				},
   779  			},
   780  			expected: InflatedColumn{
   781  				Column: &statepb.Column{
   782  					Started: float64(now * 1000),
   783  				},
   784  				Cells: map[string]Cell{
   785  					"." + overallRow: {
   786  						Result:  statuspb.TestStatus_FAIL,
   787  						Metrics: setElapsed(nil, 1),
   788  					},
   789  					"elapsed": {
   790  						Result:  statuspb.TestStatus_PASS,
   791  						Metrics: setElapsed(nil, 5),
   792  					},
   793  					"failed no message": {
   794  						Result: statuspb.TestStatus_FAIL,
   795  					},
   796  					"failed": {
   797  						Message: "boom",
   798  						Result:  statuspb.TestStatus_FAIL,
   799  						Icon:    "F",
   800  					},
   801  					"failed other message": {
   802  						Message: "irrelevant message",
   803  						Result:  statuspb.TestStatus_FAIL,
   804  						Icon:    "F",
   805  					},
   806  					"errored no message": {
   807  						Result: statuspb.TestStatus_FAIL,
   808  					},
   809  					"errored": {
   810  						Message: "oh no",
   811  						Result:  statuspb.TestStatus_FAIL,
   812  						Icon:    "F",
   813  					},
   814  					// no invisible skip
   815  					"visible skip": {
   816  						Result:  statuspb.TestStatus_PASS_WITH_SKIPS,
   817  						Message: "tl;dr",
   818  						Icon:    "S",
   819  					},
   820  					"stderr message": {
   821  						Message: "ouch",
   822  						Result:  statuspb.TestStatus_PASS,
   823  					},
   824  					"stdout message": {
   825  						Message: "bellybutton",
   826  						Result:  statuspb.TestStatus_PASS,
   827  					},
   828  				},
   829  			},
   830  		},
   831  		{
   832  			name: "metricKey",
   833  			nameCfg: nameConfig{
   834  				format: "%s",
   835  				parts:  []string{testsName},
   836  			},
   837  			opt: groupOptions{
   838  				metricKey: "food",
   839  			},
   840  			result: gcsResult{
   841  				started: gcs.Started{
   842  					Started: metadata.Started{
   843  						Timestamp: now,
   844  					},
   845  				},
   846  				finished: gcs.Finished{
   847  					Finished: metadata.Finished{
   848  						Timestamp: pint(now + 1),
   849  					},
   850  				},
   851  				suites: []gcs.SuitesMeta{
   852  					{
   853  						Suites: &junit.Suites{
   854  							Suites: []junit.Suite{
   855  								{
   856  									Results: []junit.Result{
   857  										{
   858  											Name: "no properties",
   859  										},
   860  										{
   861  											Name: "missing property",
   862  											Properties: &junit.Properties{
   863  												PropertyList: []junit.Property{
   864  													{"random", "thing"},
   865  												},
   866  											},
   867  										},
   868  										{
   869  											Name: "not a number",
   870  											Properties: &junit.Properties{
   871  												PropertyList: []junit.Property{
   872  													{"food", "tasty"},
   873  												},
   874  											},
   875  										},
   876  										{
   877  											Name: "short number",
   878  											Properties: &junit.Properties{
   879  												PropertyList: []junit.Property{
   880  													{"food", "123"},
   881  												},
   882  											},
   883  										},
   884  										{
   885  											Name: "large number",
   886  											Properties: &junit.Properties{
   887  												PropertyList: []junit.Property{
   888  													{"food", "123456789"},
   889  												},
   890  											},
   891  										},
   892  										{
   893  											Name: "many digits",
   894  											Properties: &junit.Properties{
   895  												PropertyList: []junit.Property{
   896  													{"food", "1.567890"},
   897  												},
   898  											},
   899  										},
   900  										{
   901  											Name: "multiple values",
   902  											Properties: &junit.Properties{
   903  												PropertyList: []junit.Property{
   904  													{"food", "1"},
   905  													{"food", "2"},
   906  													{"food", "3"},
   907  													{"food", "4"},
   908  												},
   909  											},
   910  										},
   911  										{
   912  											Name: "preceds failure message",
   913  											Properties: &junit.Properties{
   914  												PropertyList: []junit.Property{
   915  													{"food", "1"},
   916  												},
   917  											},
   918  											Failure: &junit.Failure{Value: *pstr("boom")},
   919  										},
   920  										{
   921  											Name: "preceds skip message",
   922  											Properties: &junit.Properties{
   923  												PropertyList: []junit.Property{
   924  													{"food", "1"},
   925  												},
   926  											},
   927  											Skipped: &junit.Skipped{Value: *pstr("tl;dr")},
   928  										},
   929  									},
   930  								},
   931  							},
   932  						},
   933  					},
   934  				},
   935  			},
   936  			expected: InflatedColumn{
   937  				Column: &statepb.Column{
   938  					Started: float64(now * 1000),
   939  				},
   940  				Cells: map[string]Cell{
   941  					"." + overallRow: {
   942  						Result:  statuspb.TestStatus_FAIL,
   943  						Metrics: setElapsed(nil, 1),
   944  					},
   945  					"no properties": {
   946  						Result: statuspb.TestStatus_PASS,
   947  					},
   948  					"missing property": {
   949  						Result: statuspb.TestStatus_PASS,
   950  					},
   951  					"not a number": {
   952  						Result: statuspb.TestStatus_PASS,
   953  					},
   954  					"short number": {
   955  						Result: statuspb.TestStatus_PASS,
   956  						Icon:   "123",
   957  						Metrics: map[string]float64{
   958  							"food": 123,
   959  						},
   960  					},
   961  					"large number": {
   962  						Result: statuspb.TestStatus_PASS,
   963  						Icon:   "1.235e+08",
   964  						Metrics: map[string]float64{
   965  							"food": 123456789,
   966  						},
   967  					},
   968  					"many digits": {
   969  						Result: statuspb.TestStatus_PASS,
   970  						Icon:   "1.568",
   971  						Metrics: map[string]float64{
   972  							"food": 1.567890,
   973  						},
   974  					},
   975  					"multiple values": {
   976  						Result: statuspb.TestStatus_PASS,
   977  						Icon:   "2.5",
   978  						Metrics: map[string]float64{
   979  							"food": 2.5,
   980  						},
   981  					},
   982  					"preceds failure message": {
   983  						Result:  statuspb.TestStatus_FAIL,
   984  						Message: "boom",
   985  						Icon:    "1",
   986  						Metrics: map[string]float64{
   987  							"food": 1,
   988  						},
   989  					},
   990  					"preceds skip message": {
   991  						Result:  statuspb.TestStatus_PASS_WITH_SKIPS,
   992  						Message: "tl;dr",
   993  						Icon:    "1",
   994  						Metrics: map[string]float64{
   995  							"food": 1,
   996  						},
   997  					},
   998  				},
   999  			},
  1000  		},
  1001  		{
  1002  			name: "annotations",
  1003  			nameCfg: nameConfig{
  1004  				format: "%s",
  1005  				parts:  []string{testsName},
  1006  			},
  1007  			opt: groupOptions{
  1008  				annotations: []*configpb.TestGroup_TestAnnotation{
  1009  					{
  1010  						ShortText: "steak",
  1011  						ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{
  1012  							PropertyName: "fries",
  1013  						},
  1014  					},
  1015  				},
  1016  			},
  1017  			result: gcsResult{
  1018  				started: gcs.Started{
  1019  					Started: metadata.Started{
  1020  						Timestamp: now,
  1021  					},
  1022  				},
  1023  				finished: gcs.Finished{
  1024  					Finished: metadata.Finished{
  1025  						Timestamp: pint(now + 1),
  1026  						Passed:    &yes,
  1027  					},
  1028  				},
  1029  				suites: []gcs.SuitesMeta{
  1030  					{
  1031  						Suites: &junit.Suites{
  1032  							Suites: []junit.Suite{
  1033  								{
  1034  									Results: []junit.Result{
  1035  										{
  1036  											Name: "no properties",
  1037  										},
  1038  										{
  1039  											Name: "missing property",
  1040  											Properties: &junit.Properties{
  1041  												PropertyList: []junit.Property{
  1042  													{"random", "thing"},
  1043  												},
  1044  											},
  1045  										},
  1046  										{
  1047  											Name: "present",
  1048  											Properties: &junit.Properties{
  1049  												PropertyList: []junit.Property{
  1050  													{"fries", "irrelevant"},
  1051  												},
  1052  											},
  1053  										},
  1054  										{
  1055  											Name: "empty",
  1056  											Properties: &junit.Properties{
  1057  												PropertyList: []junit.Property{
  1058  													{"fries", ""},
  1059  												},
  1060  											},
  1061  										},
  1062  										{
  1063  											Name: "multiple",
  1064  											Properties: &junit.Properties{
  1065  												PropertyList: []junit.Property{
  1066  													{"fries", "shoestring"},
  1067  													{"fries", "curly"},
  1068  												},
  1069  											},
  1070  										},
  1071  										{
  1072  											Name:    "annotation over failure",
  1073  											Failure: &junit.Failure{Value: *pstr("boom")},
  1074  											Properties: &junit.Properties{
  1075  												PropertyList: []junit.Property{
  1076  													{"fries", "irrelevant"},
  1077  												},
  1078  											},
  1079  										},
  1080  										{
  1081  											Name:    "annotation over error",
  1082  											Errored: &junit.Errored{Value: *pstr("boom")},
  1083  											Properties: &junit.Properties{
  1084  												PropertyList: []junit.Property{
  1085  													{"fries", "irrelevant"},
  1086  												},
  1087  											},
  1088  										},
  1089  										{
  1090  											Name:    "annotation over skip",
  1091  											Skipped: &junit.Skipped{Value: *pstr("boom")},
  1092  											Properties: &junit.Properties{
  1093  												PropertyList: []junit.Property{
  1094  													{"fries", "irrelevant"},
  1095  												},
  1096  											},
  1097  										},
  1098  									},
  1099  								},
  1100  							},
  1101  						},
  1102  					},
  1103  				},
  1104  			},
  1105  			expected: InflatedColumn{
  1106  				Column: &statepb.Column{
  1107  					Started: float64(now * 1000),
  1108  				},
  1109  				Cells: map[string]Cell{
  1110  					"." + overallRow: {
  1111  						Result:  statuspb.TestStatus_PASS,
  1112  						Metrics: setElapsed(nil, 1),
  1113  					},
  1114  					"no properties": {
  1115  						Result: statuspb.TestStatus_PASS,
  1116  					},
  1117  					"missing property": {
  1118  						Result: statuspb.TestStatus_PASS,
  1119  					},
  1120  					"present": {
  1121  						Result: statuspb.TestStatus_PASS,
  1122  						Icon:   "steak",
  1123  					},
  1124  					"empty": {
  1125  						Result: statuspb.TestStatus_PASS,
  1126  						Icon:   "steak",
  1127  					},
  1128  					"multiple": {
  1129  						Result: statuspb.TestStatus_PASS,
  1130  						Icon:   "steak",
  1131  					},
  1132  					"annotation over failure": {
  1133  						Result:  statuspb.TestStatus_FAIL,
  1134  						Icon:    "steak",
  1135  						Message: "boom",
  1136  					},
  1137  					"annotation over error": {
  1138  						Result:  statuspb.TestStatus_FAIL,
  1139  						Icon:    "steak",
  1140  						Message: "boom",
  1141  					},
  1142  					"annotation over skip": {
  1143  						Result:  statuspb.TestStatus_PASS_WITH_SKIPS,
  1144  						Icon:    "steak",
  1145  						Message: "boom",
  1146  					},
  1147  				},
  1148  			},
  1149  		},
  1150  		{
  1151  			name: "userKey",
  1152  			nameCfg: nameConfig{
  1153  				format: "%s",
  1154  				parts:  []string{testsName},
  1155  			},
  1156  			opt: groupOptions{
  1157  				userKey: "fries",
  1158  			},
  1159  			result: gcsResult{
  1160  				started: gcs.Started{
  1161  					Started: metadata.Started{
  1162  						Timestamp: now,
  1163  					},
  1164  				},
  1165  				finished: gcs.Finished{
  1166  					Finished: metadata.Finished{
  1167  						Timestamp: pint(now + 1),
  1168  						Passed:    &yes,
  1169  					},
  1170  				},
  1171  				suites: []gcs.SuitesMeta{
  1172  					{
  1173  						Suites: &junit.Suites{
  1174  							Suites: []junit.Suite{
  1175  								{
  1176  									Results: []junit.Result{
  1177  										{
  1178  											Name: "no properties",
  1179  										},
  1180  										{
  1181  											Name: "missing property",
  1182  											Properties: &junit.Properties{
  1183  												PropertyList: []junit.Property{
  1184  													{"random", "thing"},
  1185  												},
  1186  											},
  1187  										},
  1188  										{
  1189  											Name: "present",
  1190  											Properties: &junit.Properties{
  1191  												PropertyList: []junit.Property{
  1192  													{"fries", "curly"},
  1193  												},
  1194  											},
  1195  										},
  1196  										{
  1197  											Name: "choose first",
  1198  											Properties: &junit.Properties{
  1199  												PropertyList: []junit.Property{
  1200  													{"fries", "shoestring"},
  1201  													{"fries", "curly"},
  1202  												},
  1203  											},
  1204  										},
  1205  									},
  1206  								},
  1207  							},
  1208  						},
  1209  					},
  1210  				},
  1211  			},
  1212  			expected: InflatedColumn{
  1213  				Column: &statepb.Column{
  1214  					Started: float64(now * 1000),
  1215  				},
  1216  				Cells: map[string]Cell{
  1217  					"." + overallRow: {
  1218  						Result:  statuspb.TestStatus_PASS,
  1219  						Metrics: setElapsed(nil, 1),
  1220  					},
  1221  					"no properties": {
  1222  						Result: statuspb.TestStatus_PASS,
  1223  					},
  1224  					"missing property": {
  1225  						Result: statuspb.TestStatus_PASS,
  1226  					},
  1227  					"present": {
  1228  						Result:       statuspb.TestStatus_PASS,
  1229  						UserProperty: "curly",
  1230  					},
  1231  					"choose first": {
  1232  						Result:       statuspb.TestStatus_PASS,
  1233  						UserProperty: "shoestring",
  1234  					},
  1235  				},
  1236  			},
  1237  		},
  1238  		{
  1239  			name: "names formatted correctly",
  1240  			nameCfg: nameConfig{
  1241  				format: "%s - %s [%s] (%s)",
  1242  				parts:  []string{testsName, "extra", "part", "property"},
  1243  			},
  1244  			result: gcsResult{
  1245  				started: gcs.Started{
  1246  					Started: metadata.Started{
  1247  						Timestamp: now,
  1248  					},
  1249  				},
  1250  				finished: gcs.Finished{
  1251  					Finished: metadata.Finished{
  1252  						Timestamp: pint(now + 1),
  1253  						Passed:    &yes,
  1254  					},
  1255  				},
  1256  				suites: []gcs.SuitesMeta{
  1257  					{
  1258  						Suites: &junit.Suites{
  1259  							Suites: []junit.Suite{
  1260  								{
  1261  									Results: []junit.Result{
  1262  										{
  1263  											Name: "elapsed",
  1264  											Properties: &junit.Properties{
  1265  												PropertyList: []junit.Property{
  1266  													{"property", "good-property"},
  1267  													{"ignore", "me"},
  1268  												},
  1269  											},
  1270  										},
  1271  									},
  1272  								},
  1273  							},
  1274  						},
  1275  						Metadata: map[string]string{
  1276  							"extra": "first",
  1277  							"part":  "second",
  1278  						},
  1279  					},
  1280  					{
  1281  						Suites: &junit.Suites{
  1282  							Suites: []junit.Suite{
  1283  								{
  1284  									Results: []junit.Result{
  1285  										{
  1286  											Name: "other",
  1287  											Properties: &junit.Properties{
  1288  												PropertyList: []junit.Property{
  1289  													// "property" missing
  1290  													{"ignore", "me"},
  1291  												},
  1292  											},
  1293  										},
  1294  									},
  1295  								},
  1296  							},
  1297  						},
  1298  						Metadata: map[string]string{
  1299  							"extra": "hey",
  1300  							// part missing
  1301  						},
  1302  					},
  1303  				},
  1304  			},
  1305  			expected: InflatedColumn{
  1306  				Column: &statepb.Column{
  1307  					Started: float64(now * 1000),
  1308  				},
  1309  				Cells: map[string]Cell{
  1310  					"." + overallRow: {
  1311  						Result:  statuspb.TestStatus_PASS,
  1312  						Metrics: setElapsed(nil, 1),
  1313  					},
  1314  					"elapsed - first [second] (good-property)": {
  1315  						Result: statuspb.TestStatus_PASS,
  1316  					},
  1317  					"other - hey [] ()": {
  1318  						Result: statuspb.TestStatus_PASS,
  1319  					},
  1320  				},
  1321  			},
  1322  		},
  1323  		{
  1324  			name: "duplicate row names can be merged",
  1325  			nameCfg: nameConfig{
  1326  				format: "%s - %s",
  1327  				parts:  []string{testsName, "extra"},
  1328  			},
  1329  			opt: groupOptions{
  1330  				merge: true,
  1331  			},
  1332  			result: gcsResult{
  1333  				started: gcs.Started{
  1334  					Started: metadata.Started{
  1335  						Timestamp: now,
  1336  					},
  1337  				},
  1338  				finished: gcs.Finished{
  1339  					Finished: metadata.Finished{
  1340  						Timestamp: pint(now + 1),
  1341  						Passed:    &yes,
  1342  					},
  1343  				},
  1344  				suites: []gcs.SuitesMeta{
  1345  					{
  1346  						Suites: &junit.Suites{
  1347  							Suites: []junit.Suite{
  1348  								{
  1349  									Results: []junit.Result{
  1350  										{
  1351  											Name: "same",
  1352  											Time: 1,
  1353  										},
  1354  										{
  1355  											Name: "same",
  1356  											Time: 2,
  1357  										},
  1358  									},
  1359  								},
  1360  							},
  1361  						},
  1362  						Metadata: map[string]string{
  1363  							"extra": "same",
  1364  						},
  1365  					},
  1366  					{
  1367  						Suites: &junit.Suites{
  1368  							Suites: []junit.Suite{
  1369  								{
  1370  									Results: []junit.Result{
  1371  										{
  1372  											Name:    "same",
  1373  											Time:    3,
  1374  											Failure: &junit.Failure{Value: *pstr("ugh")},
  1375  										},
  1376  									},
  1377  								},
  1378  							},
  1379  						},
  1380  						Metadata: map[string]string{
  1381  							"extra": "same",
  1382  						},
  1383  					},
  1384  				},
  1385  			},
  1386  			expected: InflatedColumn{
  1387  				Column: &statepb.Column{
  1388  					Started: float64(now * 1000),
  1389  				},
  1390  				Cells: map[string]Cell{
  1391  					"." + overallRow: {
  1392  						Result:  statuspb.TestStatus_PASS,
  1393  						Metrics: setElapsed(nil, 1),
  1394  					},
  1395  					"same - same": {
  1396  						Result:  statuspb.TestStatus_FLAKY,
  1397  						Icon:    "2/3",
  1398  						Message: "2/3 runs passed: ugh",
  1399  						Metrics: setElapsed(nil, 2), // mean
  1400  					},
  1401  				},
  1402  			},
  1403  		},
  1404  		{
  1405  			name: "duplicate row names can be disambiguated",
  1406  			nameCfg: nameConfig{
  1407  				format: "%s - %s",
  1408  				parts:  []string{testsName, "extra"},
  1409  			},
  1410  			result: gcsResult{
  1411  				started: gcs.Started{
  1412  					Started: metadata.Started{
  1413  						Timestamp: now,
  1414  					},
  1415  				},
  1416  				finished: gcs.Finished{
  1417  					Finished: metadata.Finished{
  1418  						Timestamp: pint(now + 1),
  1419  						Passed:    &yes,
  1420  					},
  1421  				},
  1422  				suites: []gcs.SuitesMeta{
  1423  					{
  1424  						Suites: &junit.Suites{
  1425  							Suites: []junit.Suite{
  1426  								{
  1427  									Results: []junit.Result{
  1428  										{
  1429  											Name: "same",
  1430  											Time: 1,
  1431  										},
  1432  										{
  1433  											Name: "same",
  1434  											Time: 2,
  1435  										},
  1436  									},
  1437  								},
  1438  							},
  1439  						},
  1440  						Metadata: map[string]string{
  1441  							"extra": "same",
  1442  						},
  1443  					},
  1444  					{
  1445  						Suites: &junit.Suites{
  1446  							Suites: []junit.Suite{
  1447  								{
  1448  									Results: []junit.Result{
  1449  										{
  1450  											Name: "same",
  1451  											Time: 3,
  1452  										},
  1453  									},
  1454  								},
  1455  							},
  1456  						},
  1457  						Metadata: map[string]string{
  1458  							"extra": "same",
  1459  						},
  1460  					},
  1461  				},
  1462  			},
  1463  			expected: InflatedColumn{
  1464  				Column: &statepb.Column{
  1465  					Started: float64(now * 1000),
  1466  				},
  1467  				Cells: map[string]Cell{
  1468  					"." + overallRow: {
  1469  						Result:  statuspb.TestStatus_PASS,
  1470  						Metrics: setElapsed(nil, 1),
  1471  					},
  1472  					"same - same": {
  1473  						Result:  statuspb.TestStatus_PASS,
  1474  						Metrics: setElapsed(nil, 1),
  1475  					},
  1476  					"same - same [1]": {
  1477  						Result:  statuspb.TestStatus_PASS,
  1478  						Metrics: setElapsed(nil, 2),
  1479  					},
  1480  					"same - same [2]": {
  1481  						Result:  statuspb.TestStatus_PASS,
  1482  						Metrics: setElapsed(nil, 3),
  1483  					},
  1484  				},
  1485  			},
  1486  		},
  1487  		{
  1488  			name: "excessively duplicated rows overflows",
  1489  			nameCfg: nameConfig{
  1490  				format: "%s",
  1491  				parts:  []string{testsName},
  1492  			},
  1493  			result: gcsResult{
  1494  				started: gcs.Started{
  1495  					Started: metadata.Started{
  1496  						Timestamp: now,
  1497  					},
  1498  				},
  1499  				finished: gcs.Finished{
  1500  					Finished: metadata.Finished{
  1501  						Timestamp: pint(now + 1),
  1502  						Passed:    &yes,
  1503  					},
  1504  				},
  1505  				suites: []gcs.SuitesMeta{
  1506  					{
  1507  						Suites: &junit.Suites{
  1508  							Suites: []junit.Suite{
  1509  								{
  1510  									Results: func() []junit.Result {
  1511  										var out []junit.Result
  1512  										under := junit.Result{Name: "under"}
  1513  										max := junit.Result{Name: "max"}
  1514  										over := junit.Result{Name: "over"}
  1515  										for i := 0; i < maxDuplicates; i++ {
  1516  											under.Time = float64(i)
  1517  											max.Time = float64(i)
  1518  											over.Time = float64(i)
  1519  											out = append(out, under, max, over)
  1520  										}
  1521  										max.Time = maxDuplicates
  1522  										over.Time = maxDuplicates
  1523  										out = append(out, max, over)
  1524  										over.Time++
  1525  										out = append(out, over)
  1526  										return out
  1527  									}(),
  1528  								},
  1529  							},
  1530  						},
  1531  						Metadata: map[string]string{
  1532  							"extra": "same",
  1533  						},
  1534  					},
  1535  				},
  1536  			},
  1537  			expected: InflatedColumn{
  1538  				Column: &statepb.Column{
  1539  					Started: float64(now * 1000),
  1540  				},
  1541  				Cells: func() map[string]Cell {
  1542  					out := map[string]Cell{
  1543  						"." + overallRow: {
  1544  							Result:  statuspb.TestStatus_PASS,
  1545  							Metrics: setElapsed(nil, 1),
  1546  						},
  1547  					}
  1548  					under := Cell{Result: statuspb.TestStatus_PASS}
  1549  					max := Cell{Result: statuspb.TestStatus_PASS}
  1550  					over := Cell{Result: statuspb.TestStatus_PASS}
  1551  					out["under"] = under
  1552  					out["max"] = max
  1553  					out["over"] = over
  1554  					for i := 1; i < maxDuplicates; i++ {
  1555  						t := float64(i)
  1556  						under.Metrics = setElapsed(nil, t)
  1557  						out[fmt.Sprintf("under [%d]", i)] = under
  1558  						max.Metrics = setElapsed(nil, t)
  1559  						out[fmt.Sprintf("max [%d]", i)] = max
  1560  						over.Metrics = setElapsed(nil, t)
  1561  						out[fmt.Sprintf("over [%d]", i)] = over
  1562  					}
  1563  					max.Metrics = setElapsed(nil, maxDuplicates)
  1564  					out[`max [overflow]`] = overflowCell
  1565  					out[`over [overflow]`] = overflowCell
  1566  					return out
  1567  				}(),
  1568  			},
  1569  		},
  1570  		{
  1571  			name: "can add missing podInfo",
  1572  			opt: groupOptions{
  1573  				analyzeProwJob: true,
  1574  			},
  1575  			result: gcsResult{
  1576  				started: gcs.Started{
  1577  					Started: metadata.Started{
  1578  						Timestamp: now,
  1579  					},
  1580  				},
  1581  				finished: gcs.Finished{
  1582  					Finished: metadata.Finished{
  1583  						Timestamp: pint(now + 1),
  1584  						Passed:    &yes,
  1585  					},
  1586  				},
  1587  			},
  1588  			expected: InflatedColumn{
  1589  				Column: &statepb.Column{
  1590  					Started: float64(now * 1000),
  1591  				},
  1592  				Cells: map[string]Cell{
  1593  					"." + overallRow: {
  1594  						Result:  statuspb.TestStatus_PASS,
  1595  						Metrics: setElapsed(nil, 1),
  1596  					},
  1597  					"." + podInfoRow: podInfoMissingCell,
  1598  				},
  1599  			},
  1600  		},
  1601  		{
  1602  			name: "can add interesting pod info",
  1603  			opt: groupOptions{
  1604  				analyzeProwJob: true,
  1605  			},
  1606  			result: gcsResult{
  1607  				started: gcs.Started{
  1608  					Started: metadata.Started{
  1609  						Timestamp: now,
  1610  					},
  1611  				},
  1612  				finished: gcs.Finished{
  1613  					Finished: metadata.Finished{
  1614  						Timestamp: pint(now + 1),
  1615  						Passed:    &yes,
  1616  					},
  1617  				},
  1618  				podInfo: gcs.PodInfo{
  1619  					Pod: &core.Pod{
  1620  						Status: core.PodStatus{Phase: core.PodSucceeded},
  1621  					},
  1622  				},
  1623  			},
  1624  			expected: InflatedColumn{
  1625  				Column: &statepb.Column{
  1626  					Started: float64(now * 1000),
  1627  				},
  1628  				Cells: map[string]Cell{
  1629  					"." + overallRow: {
  1630  						Result:  statuspb.TestStatus_PASS,
  1631  						Metrics: setElapsed(nil, 1),
  1632  					},
  1633  					"." + podInfoRow: podInfoPassCell,
  1634  				},
  1635  			},
  1636  		},
  1637  		{
  1638  			name: "do not add missing podinfo when still running",
  1639  			opt: groupOptions{
  1640  				analyzeProwJob: true,
  1641  			},
  1642  			result: gcsResult{
  1643  				started: gcs.Started{
  1644  					Started: metadata.Started{
  1645  						Timestamp: now,
  1646  					},
  1647  				},
  1648  			},
  1649  			expected: InflatedColumn{
  1650  				Column: &statepb.Column{
  1651  					Started: float64(now * 1000),
  1652  				},
  1653  				Cells: map[string]Cell{
  1654  					"." + overallRow: {
  1655  						Result:  statuspb.TestStatus_RUNNING,
  1656  						Icon:    "R",
  1657  						Message: "Build still running...",
  1658  					},
  1659  				},
  1660  			},
  1661  		},
  1662  		{
  1663  			name: "add intersting podinfo even still running",
  1664  			opt: groupOptions{
  1665  				analyzeProwJob: true,
  1666  			},
  1667  			result: gcsResult{
  1668  				started: gcs.Started{
  1669  					Started: metadata.Started{
  1670  						Timestamp: now,
  1671  					},
  1672  				},
  1673  				podInfo: gcs.PodInfo{
  1674  					Pod: &core.Pod{
  1675  						Status: core.PodStatus{Phase: core.PodSucceeded},
  1676  					},
  1677  				},
  1678  			},
  1679  			expected: InflatedColumn{
  1680  				Column: &statepb.Column{
  1681  					Started: float64(now * 1000),
  1682  				},
  1683  				Cells: map[string]Cell{
  1684  					"." + overallRow: {
  1685  						Result:  statuspb.TestStatus_RUNNING,
  1686  						Icon:    "R",
  1687  						Message: "Build still running...",
  1688  					},
  1689  					"." + podInfoRow: podInfoPassCell,
  1690  				},
  1691  			},
  1692  		},
  1693  		{
  1694  			name: "addCellID",
  1695  			nameCfg: nameConfig{
  1696  				format: "%s",
  1697  				parts:  []string{testsName},
  1698  			},
  1699  			opt: groupOptions{
  1700  				addCellID: true,
  1701  			},
  1702  			result: gcsResult{
  1703  				started: gcs.Started{
  1704  					Started: metadata.Started{
  1705  						Timestamp: now,
  1706  					},
  1707  				},
  1708  				finished: gcs.Finished{
  1709  					Finished: metadata.Finished{
  1710  						Timestamp: pint(now + 1),
  1711  						Passed:    &yes,
  1712  					},
  1713  				},
  1714  				suites: []gcs.SuitesMeta{
  1715  					{
  1716  						Suites: &junit.Suites{
  1717  							Suites: []junit.Suite{
  1718  								{
  1719  									Name: "this",
  1720  									Results: []junit.Result{
  1721  										{
  1722  											Name: "that",
  1723  										},
  1724  									},
  1725  								},
  1726  							},
  1727  						},
  1728  					},
  1729  				},
  1730  			},
  1731  			id: "McLovin",
  1732  			expected: InflatedColumn{
  1733  				Column: &statepb.Column{
  1734  					Started: float64(now * 1000),
  1735  					Build:   "McLovin",
  1736  					Hint:    "McLovin",
  1737  				},
  1738  				Cells: map[string]Cell{
  1739  					"." + overallRow: {
  1740  						Result:  statuspb.TestStatus_PASS,
  1741  						Metrics: setElapsed(nil, 1),
  1742  						CellID:  "McLovin",
  1743  					},
  1744  					"this.that": {
  1745  						Result: statuspb.TestStatus_PASS,
  1746  						CellID: "McLovin",
  1747  					},
  1748  				},
  1749  			},
  1750  		},
  1751  		{
  1752  			name: "override build key",
  1753  			nameCfg: nameConfig{
  1754  				format: "%s",
  1755  				parts:  []string{testsName},
  1756  			},
  1757  			opt: groupOptions{
  1758  				buildKey: "my-build-id",
  1759  			},
  1760  			result: gcsResult{
  1761  				started: gcs.Started{
  1762  					Started: metadata.Started{
  1763  						Timestamp: now,
  1764  					},
  1765  				},
  1766  				finished: gcs.Finished{
  1767  					Finished: metadata.Finished{
  1768  						Metadata: metadata.Metadata{
  1769  							"my-build-id": "id-1",
  1770  						},
  1771  						Timestamp: pint(now + 1),
  1772  						Passed:    &yes,
  1773  					},
  1774  				},
  1775  			},
  1776  			id: "McLovin",
  1777  			expected: InflatedColumn{
  1778  				Column: &statepb.Column{
  1779  					Started: float64(now * 1000),
  1780  					Build:   "id-1",
  1781  					Hint:    "McLovin",
  1782  				},
  1783  				Cells: map[string]Cell{
  1784  					"." + overallRow: {
  1785  						Result:  statuspb.TestStatus_PASS,
  1786  						Metrics: setElapsed(nil, 1),
  1787  					},
  1788  				},
  1789  			},
  1790  		},
  1791  		{
  1792  			name: "override build key missing",
  1793  			nameCfg: nameConfig{
  1794  				format: "%s",
  1795  				parts:  []string{testsName},
  1796  			},
  1797  			opt: groupOptions{
  1798  				buildKey: "my-build-id",
  1799  			},
  1800  			result: gcsResult{
  1801  				started: gcs.Started{
  1802  					Started: metadata.Started{
  1803  						Timestamp: now,
  1804  					},
  1805  				},
  1806  				finished: gcs.Finished{
  1807  					Finished: metadata.Finished{
  1808  						Metadata: metadata.Metadata{
  1809  							"something": "hello",
  1810  						},
  1811  						Timestamp: pint(now + 1),
  1812  						Passed:    &yes,
  1813  					},
  1814  				},
  1815  			},
  1816  			id: "McLovin",
  1817  			expected: InflatedColumn{
  1818  				Column: &statepb.Column{
  1819  					Started: float64(now * 1000),
  1820  					Build:   "",
  1821  					Hint:    "McLovin",
  1822  				},
  1823  				Cells: map[string]Cell{
  1824  					"." + overallRow: {
  1825  						Result:  statuspb.TestStatus_PASS,
  1826  						Metrics: setElapsed(nil, 1),
  1827  					},
  1828  				},
  1829  			},
  1830  		},
  1831  		{
  1832  			name: "Ginkgo V2 skipped with message",
  1833  			nameCfg: nameConfig{
  1834  				format: "%s",
  1835  				parts:  []string{testsName},
  1836  			},
  1837  			result: gcsResult{
  1838  				started: gcs.Started{
  1839  					Started: metadata.Started{
  1840  						Timestamp: now,
  1841  					},
  1842  				},
  1843  				finished: gcs.Finished{
  1844  					Finished: metadata.Finished{
  1845  						Timestamp: pint(now + 1),
  1846  						Passed:    &yes,
  1847  					},
  1848  				},
  1849  				suites: []gcs.SuitesMeta{
  1850  					{
  1851  						Suites: &junit.Suites{
  1852  							Suites: []junit.Suite{
  1853  								{
  1854  									Results: []junit.Result{
  1855  										{
  1856  											Name:    "visible skip non-default msg",
  1857  											Skipped: &junit.Skipped{Message: *pstr("non-default message")},
  1858  										},
  1859  										{
  1860  											Name:    "invisible skip default msg",
  1861  											Skipped: &junit.Skipped{Message: *pstr("skipped")},
  1862  										},
  1863  										{
  1864  											Name:    "invisible skip msg empty",
  1865  											Skipped: &junit.Skipped{Message: *pstr("")},
  1866  										},
  1867  									},
  1868  								},
  1869  							},
  1870  						},
  1871  					},
  1872  				},
  1873  			},
  1874  			expected: InflatedColumn{
  1875  				Column: &statepb.Column{
  1876  					Started: float64(now * 1000),
  1877  				},
  1878  				Cells: map[string]Cell{
  1879  					"." + overallRow: {
  1880  						Result:  statuspb.TestStatus_PASS,
  1881  						Metrics: setElapsed(nil, 1),
  1882  					},
  1883  					"visible skip non-default msg": {
  1884  						Result:  statuspb.TestStatus_PASS_WITH_SKIPS,
  1885  						Message: "non-default message",
  1886  						Icon:    "S",
  1887  					},
  1888  				},
  1889  			},
  1890  		},
  1891  		{
  1892  			name: "ignore_skip works",
  1893  			opt: groupOptions{
  1894  				ignoreSkip: true,
  1895  			},
  1896  			result: gcsResult{
  1897  				started: gcs.Started{
  1898  					Started: metadata.Started{
  1899  						Timestamp: now,
  1900  					},
  1901  				},
  1902  				finished: gcs.Finished{
  1903  					Finished: metadata.Finished{
  1904  						Timestamp: pint(now + 1),
  1905  						Passed:    &yes,
  1906  					},
  1907  				},
  1908  				suites: []gcs.SuitesMeta{
  1909  					{
  1910  						Suites: &junit.Suites{
  1911  							Suites: []junit.Suite{
  1912  								{
  1913  									Results: []junit.Result{
  1914  										{
  1915  											Name:    "visible skip non-default msg",
  1916  											Skipped: &junit.Skipped{Message: *pstr("non-default message")},
  1917  										},
  1918  									},
  1919  								},
  1920  							},
  1921  						},
  1922  					},
  1923  				},
  1924  			},
  1925  			expected: InflatedColumn{
  1926  				Column: &statepb.Column{
  1927  					Started: float64(now * 1000),
  1928  				},
  1929  				Cells: map[string]Cell{
  1930  					"." + overallRow: {
  1931  						Result:  statuspb.TestStatus_PASS,
  1932  						Metrics: setElapsed(nil, 1),
  1933  					},
  1934  				},
  1935  			},
  1936  		},
  1937  	}
  1938  	for _, tc := range cases {
  1939  		t.Run(tc.name, func(t *testing.T) {
  1940  			log := logrus.WithField("test name", tc.name)
  1941  			actual := convertResult(log, tc.nameCfg, tc.id, tc.headers, tc.result, tc.opt)
  1942  			if diff := cmp.Diff(tc.expected, actual, protocmp.Transform()); diff != "" {
  1943  				t.Errorf("convertResult() got unexpected diff (-want +got):\n%s", diff)
  1944  			}
  1945  		})
  1946  	}
  1947  }
  1948  
  1949  func TestIgnoreStatus(t *testing.T) {
  1950  	cases := []struct {
  1951  		opt    groupOptions
  1952  		status statuspb.TestStatus
  1953  		want   bool
  1954  	}{
  1955  		{
  1956  			opt:    groupOptions{},
  1957  			status: statuspb.TestStatus_NO_RESULT,
  1958  			want:   true,
  1959  		},
  1960  		{
  1961  			opt:    groupOptions{},
  1962  			status: statuspb.TestStatus_PASS,
  1963  			want:   false,
  1964  		},
  1965  		{
  1966  			opt:    groupOptions{},
  1967  			status: statuspb.TestStatus_PASS_WITH_SKIPS,
  1968  			want:   false,
  1969  		},
  1970  		{
  1971  			opt:    groupOptions{},
  1972  			status: statuspb.TestStatus_RUNNING,
  1973  			want:   false,
  1974  		},
  1975  		{
  1976  			opt:    groupOptions{ignoreSkip: true},
  1977  			status: statuspb.TestStatus_PASS,
  1978  			want:   false,
  1979  		},
  1980  		{
  1981  			opt:    groupOptions{ignoreSkip: true},
  1982  			status: statuspb.TestStatus_PASS_WITH_SKIPS,
  1983  			want:   true,
  1984  		},
  1985  		{
  1986  			opt:    groupOptions{ignoreSkip: true},
  1987  			status: statuspb.TestStatus_RUNNING,
  1988  			want:   false,
  1989  		},
  1990  	}
  1991  	for _, tc := range cases {
  1992  		t.Run(fmt.Sprintf("ignoreStatus(%+v, %v)", tc.opt, tc.status), func(t *testing.T) {
  1993  			if got := ignoreStatus(tc.opt, tc.status); got != tc.want {
  1994  				t.Errorf("ignoreStatus(%v, %v) got %v, want %v", tc.opt, tc.status, got, tc.want)
  1995  			}
  1996  		})
  1997  	}
  1998  }
  1999  
  2000  func TestPodInfoCell(t *testing.T) {
  2001  	now := time.Now().Add(-time.Second)
  2002  
  2003  	cases := []struct {
  2004  		name     string
  2005  		result   gcsResult
  2006  		expected Cell
  2007  	}{
  2008  		{
  2009  			name: "basically works",
  2010  			result: gcsResult{
  2011  				started: gcs.Started{
  2012  					Started: metadata.Started{
  2013  						Timestamp: now.Unix(),
  2014  					},
  2015  				},
  2016  			},
  2017  			expected: podInfoMissingCell,
  2018  		},
  2019  		{
  2020  			name: "wait for pod from running pod",
  2021  			result: gcsResult{
  2022  				finished: gcs.Finished{
  2023  					Finished: metadata.Finished{
  2024  						Timestamp: func() *int64 {
  2025  							when := now.Unix()
  2026  							return &when
  2027  						}(),
  2028  					},
  2029  				},
  2030  			},
  2031  			expected: podInfoMissingCell,
  2032  		},
  2033  		{
  2034  			name: "wait for pod info from finished pod",
  2035  			result: gcsResult{
  2036  				started: gcs.Started{
  2037  					Started: metadata.Started{
  2038  						Timestamp: now.Add(-24 * time.Hour).Unix(),
  2039  					},
  2040  				},
  2041  			},
  2042  			expected: func() Cell {
  2043  				c := podInfoMissingCell
  2044  				c.Result = statuspb.TestStatus_PASS_WITH_SKIPS
  2045  				return c
  2046  			}(),
  2047  		},
  2048  		{
  2049  			name: "finished pod without podinfo",
  2050  			result: gcsResult{
  2051  				finished: gcs.Finished{
  2052  					Finished: metadata.Finished{
  2053  						Timestamp: func() *int64 {
  2054  							when := now.Add(-time.Hour).Unix()
  2055  							return &when
  2056  						}(),
  2057  					},
  2058  				},
  2059  			},
  2060  			expected: func() Cell {
  2061  				c := podInfoMissingCell
  2062  				c.Result = statuspb.TestStatus_PASS_WITH_SKIPS
  2063  				return c
  2064  			}(),
  2065  		},
  2066  		{
  2067  			name: "passing pod",
  2068  			result: gcsResult{
  2069  				podInfo: podInfoSuccessPodInfo,
  2070  			},
  2071  			expected: podInfoPassCell,
  2072  		},
  2073  		{
  2074  			name: "no pod utils",
  2075  			result: gcsResult{
  2076  				podInfo: gcs.PodInfo{Pod: &core.Pod{}},
  2077  			},
  2078  			expected: Cell{
  2079  				Message: gcs.NoPodUtils,
  2080  				Icon:    "E",
  2081  				Result:  statuspb.TestStatus_PASS,
  2082  			},
  2083  		},
  2084  		{
  2085  			name: "pod failure",
  2086  			result: gcsResult{
  2087  				podInfo: gcs.PodInfo{
  2088  					Pod: &core.Pod{
  2089  						Status: core.PodStatus{
  2090  							Conditions: []core.PodCondition{
  2091  								{
  2092  									Type:    core.PodScheduled,
  2093  									Status:  core.ConditionFalse,
  2094  									Message: "hi there",
  2095  								},
  2096  							},
  2097  						},
  2098  					},
  2099  				},
  2100  			},
  2101  			expected: Cell{
  2102  				Message: "pod did not schedule: hi there",
  2103  				Icon:    "F",
  2104  				Result:  statuspb.TestStatus_FAIL,
  2105  			},
  2106  		},
  2107  	}
  2108  
  2109  	for _, tc := range cases {
  2110  		t.Run(tc.name, func(t *testing.T) {
  2111  			actual := podInfoCell(tc.result)
  2112  			if diff := cmp.Diff(actual, tc.expected); diff != "" {
  2113  				t.Errorf("podInfoCell(%v) got unexpected diff (-have, +want):\n%s", tc.result, diff)
  2114  			}
  2115  		})
  2116  	}
  2117  }
  2118  
  2119  func TestOverallCell(t *testing.T) {
  2120  	pint := func(v int64) *int64 {
  2121  		return &v
  2122  	}
  2123  	yes := true
  2124  	var no bool
  2125  	cases := []struct {
  2126  		name     string
  2127  		result   gcsResult
  2128  		expected Cell
  2129  	}{
  2130  		{
  2131  			name: "result timed out",
  2132  			result: gcsResult{
  2133  				started: gcs.Started{
  2134  					Started: metadata.Started{
  2135  						Timestamp: time.Now().Add(-25 * time.Hour).Unix(),
  2136  					},
  2137  				},
  2138  			},
  2139  			expected: Cell{
  2140  				Result:  statuspb.TestStatus_FAIL,
  2141  				Message: "Build did not complete within 24 hours",
  2142  				Icon:    "T",
  2143  			},
  2144  		},
  2145  		{
  2146  			name: "missing results fail",
  2147  			result: gcsResult{
  2148  				started: gcs.Started{
  2149  					Started: metadata.Started{
  2150  						Timestamp: 100,
  2151  					},
  2152  				},
  2153  				finished: gcs.Finished{
  2154  					Finished: metadata.Finished{
  2155  						Timestamp: pint(250),
  2156  						Passed:    &yes,
  2157  					},
  2158  				},
  2159  				malformed: []string{
  2160  					"podinfo.json",
  2161  				},
  2162  			},
  2163  			expected: Cell{
  2164  				Result:  statuspb.TestStatus_FAIL,
  2165  				Message: "Malformed artifacts: podinfo.json",
  2166  				Icon:    "E",
  2167  			},
  2168  		},
  2169  		{
  2170  			name: "passed result passes",
  2171  			result: gcsResult{
  2172  				started: gcs.Started{
  2173  					Started: metadata.Started{
  2174  						Timestamp: 100,
  2175  					},
  2176  				},
  2177  				finished: gcs.Finished{
  2178  					Finished: metadata.Finished{
  2179  						Timestamp: pint(250),
  2180  						Passed:    &yes,
  2181  					},
  2182  				},
  2183  			},
  2184  			expected: Cell{
  2185  				Result:  statuspb.TestStatus_PASS,
  2186  				Metrics: setElapsed(nil, 150),
  2187  			},
  2188  		},
  2189  		{
  2190  			name: "failed via deprecated result",
  2191  			result: gcsResult{
  2192  				started: gcs.Started{
  2193  					Started: metadata.Started{
  2194  						Timestamp: 100,
  2195  					},
  2196  				},
  2197  				finished: gcs.Finished{
  2198  					Finished: metadata.Finished{
  2199  						Timestamp: pint(250),
  2200  						Result:    "BOOM",
  2201  					},
  2202  				},
  2203  			},
  2204  			expected: Cell{
  2205  				Result:  statuspb.TestStatus_FAIL,
  2206  				Icon:    "E",
  2207  				Message: `finished.json missing "passed": false`,
  2208  				Metrics: setElapsed(nil, 150),
  2209  			},
  2210  		},
  2211  		{
  2212  			name: "passed via deprecated result",
  2213  			result: gcsResult{
  2214  				started: gcs.Started{
  2215  					Started: metadata.Started{
  2216  						Timestamp: 100,
  2217  					},
  2218  				},
  2219  				finished: gcs.Finished{
  2220  					Finished: metadata.Finished{
  2221  						Timestamp: pint(250),
  2222  						Result:    "SUCCESS",
  2223  					},
  2224  				},
  2225  			},
  2226  			expected: Cell{
  2227  				Result:  statuspb.TestStatus_PASS,
  2228  				Icon:    "E",
  2229  				Message: `finished.json missing "passed": true`,
  2230  				Metrics: setElapsed(nil, 150),
  2231  			},
  2232  		},
  2233  		{
  2234  			name: "failed result fails",
  2235  			result: gcsResult{
  2236  				started: gcs.Started{
  2237  					Started: metadata.Started{
  2238  						Timestamp: 100,
  2239  					},
  2240  				},
  2241  				finished: gcs.Finished{
  2242  					Finished: metadata.Finished{
  2243  						Timestamp: pint(250),
  2244  						Passed:    &no,
  2245  					},
  2246  				},
  2247  			},
  2248  			expected: Cell{
  2249  				Result:  statuspb.TestStatus_FAIL,
  2250  				Metrics: setElapsed(nil, 150),
  2251  			},
  2252  		},
  2253  		{
  2254  			name: "missing passed field is a failure",
  2255  			result: gcsResult{
  2256  				started: gcs.Started{
  2257  					Started: metadata.Started{
  2258  						Timestamp: 100,
  2259  					},
  2260  				},
  2261  				finished: gcs.Finished{
  2262  					Finished: metadata.Finished{
  2263  						Timestamp: pint(250),
  2264  					},
  2265  				},
  2266  			},
  2267  			expected: Cell{
  2268  				Result:  statuspb.TestStatus_FAIL,
  2269  				Metrics: setElapsed(nil, 150),
  2270  			},
  2271  		},
  2272  	}
  2273  
  2274  	for _, tc := range cases {
  2275  		t.Run(tc.name, func(t *testing.T) {
  2276  			actual := overallCell(tc.result)
  2277  			if diff := cmp.Diff(actual, tc.expected); diff != "" {
  2278  				t.Errorf("overallCell(%v) got unexpected diff:\n%s", tc.result, diff)
  2279  			}
  2280  		})
  2281  	}
  2282  }
  2283  
  2284  func TestSetElapsed(t *testing.T) {
  2285  	cases := []struct {
  2286  		name     string
  2287  		metrics  map[string]float64
  2288  		seconds  float64
  2289  		expected map[string]float64
  2290  	}{
  2291  		{
  2292  			name:    "nil map works",
  2293  			seconds: 10,
  2294  			expected: map[string]float64{
  2295  				ElapsedKey: 10 / 60.0,
  2296  			},
  2297  		},
  2298  		{
  2299  			name: "existing keys preserved",
  2300  			metrics: map[string]float64{
  2301  				"hello": 7,
  2302  			},
  2303  			seconds: 5,
  2304  			expected: map[string]float64{
  2305  				"hello":    7,
  2306  				ElapsedKey: 5 / 60.0,
  2307  			},
  2308  		},
  2309  		{
  2310  			name: "override existing value",
  2311  			metrics: map[string]float64{
  2312  				ElapsedKey: 3 / 60.0,
  2313  			},
  2314  			seconds: 10,
  2315  			expected: map[string]float64{
  2316  				ElapsedKey: 10 / 60.0,
  2317  			},
  2318  		},
  2319  	}
  2320  
  2321  	for _, tc := range cases {
  2322  		t.Run(tc.name, func(t *testing.T) {
  2323  			actual := setElapsed(tc.metrics, tc.seconds)
  2324  			if !reflect.DeepEqual(actual, tc.expected) {
  2325  				t.Errorf("setElapsed(%v, %v) got %v, want %v", tc.metrics, tc.seconds, actual, tc.expected)
  2326  			}
  2327  		})
  2328  	}
  2329  }
  2330  
  2331  func TestFlattenResults(t *testing.T) {
  2332  	pstr := func(s string) *string {
  2333  		return &s
  2334  	}
  2335  	cases := []struct {
  2336  		name     string
  2337  		suites   []junit.Suite
  2338  		expected []junit.Result
  2339  	}{
  2340  		{
  2341  			name: "basically works",
  2342  		},
  2343  		{
  2344  			name: "results from multiple suites",
  2345  			suites: []junit.Suite{
  2346  				{
  2347  					Name: "suite1",
  2348  					Results: []junit.Result{
  2349  						{
  2350  							Name:   "resultA",
  2351  							Output: pstr("hello"),
  2352  						},
  2353  						{
  2354  							Name:  "resultB",
  2355  							Error: pstr("bonk"),
  2356  						},
  2357  					},
  2358  				},
  2359  				{
  2360  					Name: "suite-two",
  2361  					Results: []junit.Result{
  2362  						{
  2363  							Name: "resultX",
  2364  						},
  2365  					},
  2366  				},
  2367  			},
  2368  			expected: []junit.Result{
  2369  				{
  2370  					Name:   "suite1.resultA",
  2371  					Output: pstr("hello"),
  2372  				},
  2373  				{
  2374  					Name:  "suite1.resultB",
  2375  					Error: pstr("bonk"),
  2376  				},
  2377  				{
  2378  					Name: "suite-two.resultX",
  2379  				},
  2380  			},
  2381  		},
  2382  		{
  2383  			name: "find results deeply nested in suites",
  2384  			suites: []junit.Suite{
  2385  				{
  2386  					Name: "must",
  2387  					Suites: []junit.Suite{
  2388  						{
  2389  							Name: "go",
  2390  							Suites: []junit.Suite{
  2391  								{
  2392  									Name: "deeper",
  2393  									Results: []junit.Result{
  2394  										{
  2395  											Name:    "leaf",
  2396  											Skipped: &junit.Skipped{Value: *pstr("first")},
  2397  										},
  2398  									},
  2399  								},
  2400  							},
  2401  							Results: []junit.Result{
  2402  								{
  2403  									Name:    "branch",
  2404  									Skipped: &junit.Skipped{Value: *pstr("second")},
  2405  								},
  2406  							},
  2407  						},
  2408  					},
  2409  					Results: []junit.Result{
  2410  						{
  2411  							Name:    "trunk",
  2412  							Skipped: &junit.Skipped{Value: *pstr("third")},
  2413  						},
  2414  					},
  2415  				},
  2416  			},
  2417  			expected: []junit.Result{
  2418  				{
  2419  					Name:    "must.go.deeper.leaf",
  2420  					Skipped: &junit.Skipped{Value: *pstr("first")},
  2421  				},
  2422  				{
  2423  					Name:    "must.go.branch",
  2424  					Skipped: &junit.Skipped{Value: *pstr("second")},
  2425  				},
  2426  				{
  2427  					Name:    "must.trunk",
  2428  					Skipped: &junit.Skipped{Value: *pstr("third")},
  2429  				},
  2430  			},
  2431  		},
  2432  	}
  2433  
  2434  	for _, tc := range cases {
  2435  		t.Run(tc.name, func(t *testing.T) {
  2436  			actual := flattenResults(tc.suites...)
  2437  			if !reflect.DeepEqual(actual, tc.expected) {
  2438  				t.Errorf("flattenResults(%v) got %v, want %v", tc.suites, actual, tc.expected)
  2439  			}
  2440  		})
  2441  	}
  2442  }
  2443  
  2444  func TestDotName(t *testing.T) {
  2445  	cases := []struct {
  2446  		name     string
  2447  		left     string
  2448  		right    string
  2449  		expected string
  2450  	}{
  2451  		{
  2452  			name: "basically works",
  2453  		},
  2454  		{
  2455  			name:     "left.right",
  2456  			left:     "left",
  2457  			right:    "right",
  2458  			expected: "left.right",
  2459  		},
  2460  		{
  2461  			name:     "only left",
  2462  			left:     "left",
  2463  			expected: "left",
  2464  		},
  2465  		{
  2466  			name:     "only right",
  2467  			right:    "right",
  2468  			expected: "right",
  2469  		},
  2470  	}
  2471  
  2472  	for _, tc := range cases {
  2473  		t.Run(tc.name, func(t *testing.T) {
  2474  			if actual := dotName(tc.left, tc.right); actual != tc.expected {
  2475  				t.Errorf("dotName(%q, %q) got %q, want %q", tc.left, tc.right, actual, tc.expected)
  2476  			}
  2477  		})
  2478  	}
  2479  }