github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/read_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  	"context"
    21  	"encoding/json"
    22  	"encoding/xml"
    23  	"errors"
    24  	"fmt"
    25  	"net/url"
    26  	"sort"
    27  	"sync"
    28  	"testing"
    29  	"time"
    30  
    31  	"cloud.google.com/go/storage"
    32  	"github.com/GoogleCloudPlatform/testgrid/metadata"
    33  	"github.com/GoogleCloudPlatform/testgrid/metadata/junit"
    34  	configpb "github.com/GoogleCloudPlatform/testgrid/pb/config"
    35  	statepb "github.com/GoogleCloudPlatform/testgrid/pb/state"
    36  	statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status"
    37  	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
    38  	"github.com/GoogleCloudPlatform/testgrid/util/gcs/fake"
    39  	"github.com/google/go-cmp/cmp"
    40  	"github.com/sirupsen/logrus"
    41  	"google.golang.org/protobuf/testing/protocmp"
    42  	core "k8s.io/api/core/v1"
    43  )
    44  
    45  type fakeObject = fake.Object
    46  type fakeClient = fake.Client
    47  type fakeIterator = fake.Iterator
    48  
    49  func TestDownloadGrid(t *testing.T) {
    50  	cases := []struct {
    51  		name string
    52  	}{
    53  		{},
    54  	}
    55  
    56  	for _, tc := range cases {
    57  		t.Run(tc.name, func(t *testing.T) {
    58  		})
    59  	}
    60  }
    61  
    62  func resolveOrDie(path *gcs.Path, s string) *gcs.Path {
    63  	p, err := path.ResolveReference(&url.URL{Path: s})
    64  	if err != nil {
    65  		panic(err)
    66  	}
    67  	return p
    68  }
    69  
    70  func jsonData(i interface{}) string {
    71  	buf, err := json.Marshal(i)
    72  	if err != nil {
    73  		panic(err)
    74  	}
    75  	return string(buf)
    76  }
    77  
    78  func xmlData(i interface{}) string {
    79  	buf, err := xml.Marshal(i)
    80  	if err != nil {
    81  		panic(err)
    82  	}
    83  	return string(buf)
    84  }
    85  
    86  func makeJunit(passed, failed []string) string {
    87  	var suite junit.Suite
    88  	for _, name := range passed {
    89  		suite.Results = append(suite.Results, junit.Result{Name: name})
    90  	}
    91  
    92  	for _, name := range failed {
    93  		f := name
    94  		suite.Results = append(suite.Results, junit.Result{
    95  			Name:    name,
    96  			Failure: &junit.Failure{Value: f},
    97  		})
    98  	}
    99  	return xmlData(suite)
   100  }
   101  
   102  func pint64(n int64) *int64 {
   103  	return &n
   104  }
   105  
   106  func TestHintStarted(t *testing.T) {
   107  	cases := []struct {
   108  		name string
   109  		cols []InflatedColumn
   110  		want string
   111  	}{
   112  		{
   113  			name: "basic",
   114  		},
   115  		{
   116  			name: "ordered",
   117  			cols: []InflatedColumn{
   118  				{
   119  					Column: &statepb.Column{
   120  						Hint:    "b",
   121  						Started: 1200,
   122  					},
   123  				},
   124  				{
   125  					Column: &statepb.Column{
   126  						Hint:    "a",
   127  						Started: 1100,
   128  					},
   129  				},
   130  			},
   131  			want: "b",
   132  		},
   133  		{
   134  			name: "reversed",
   135  			cols: []InflatedColumn{
   136  				{
   137  					Column: &statepb.Column{
   138  						Hint:    "a",
   139  						Started: 1100,
   140  					},
   141  				},
   142  				{
   143  					Column: &statepb.Column{
   144  						Hint:    "b",
   145  						Started: 1200,
   146  					},
   147  				},
   148  			},
   149  			want: "b",
   150  		},
   151  		{
   152  			name: "different", // hint and started come from diff cols
   153  			cols: []InflatedColumn{
   154  				{
   155  					Column: &statepb.Column{
   156  						Hint:    "a",
   157  						Started: 1100,
   158  					},
   159  				},
   160  				{
   161  					Column: &statepb.Column{
   162  						Hint:    "b",
   163  						Started: 900,
   164  					},
   165  				},
   166  			},
   167  			want: "b",
   168  		},
   169  		{
   170  			name: "numerical", // hint10 > hint2
   171  			cols: []InflatedColumn{
   172  				{
   173  					Column: &statepb.Column{
   174  						Hint: "hint2",
   175  					},
   176  				},
   177  				{
   178  					Column: &statepb.Column{
   179  						Hint: "hint10",
   180  					},
   181  				},
   182  			},
   183  			want: "hint10",
   184  		},
   185  	}
   186  
   187  	for _, tc := range cases {
   188  		t.Run(tc.name, func(t *testing.T) {
   189  			got := hintStarted(tc.cols)
   190  			if tc.want != got {
   191  				t.Errorf("hintStarted() got hint %q, want %q", got, tc.want)
   192  			}
   193  		})
   194  	}
   195  }
   196  
   197  func pstr(s string) *string { return &s }
   198  
   199  func TestReadColumns(t *testing.T) {
   200  	now := time.Now().Unix()
   201  	yes := true
   202  	var no bool
   203  	var noStartErr *noStartError
   204  	cases := []struct {
   205  		name               string
   206  		ctx                context.Context
   207  		builds             []fakeBuild
   208  		group              *configpb.TestGroup
   209  		stop               time.Time
   210  		dur                time.Duration
   211  		concurrency        int
   212  		readResultOverride *resultReader
   213  		enableIgnoreSkip   bool
   214  
   215  		expected []InflatedColumn
   216  		err      bool
   217  	}{
   218  		{
   219  			name: "basically works",
   220  		},
   221  		{
   222  			name: "convert results correctly",
   223  			builds: []fakeBuild{
   224  				{
   225  					id: "11",
   226  					started: &fakeObject{
   227  						Data: jsonData(metadata.Started{Timestamp: now + 11}),
   228  					},
   229  					finished: &fakeObject{
   230  						Data: jsonData(metadata.Finished{
   231  							Timestamp: pint64(now + 22),
   232  							Passed:    &no,
   233  						}),
   234  					},
   235  					podInfo: podInfoSuccess,
   236  				},
   237  				{
   238  					id: "10",
   239  					started: &fakeObject{
   240  						Data: jsonData(metadata.Started{Timestamp: now + 10}),
   241  					},
   242  					finished: &fakeObject{
   243  						Data: jsonData(metadata.Finished{
   244  							Timestamp: pint64(now + 20),
   245  							Passed:    &yes,
   246  						}),
   247  					},
   248  				},
   249  			},
   250  			group: &configpb.TestGroup{
   251  				GcsPrefix: "bucket/path/to/build/",
   252  			},
   253  			expected: []InflatedColumn{
   254  				{
   255  					Column: &statepb.Column{
   256  						Build:   "10",
   257  						Hint:    "10",
   258  						Started: float64(now+10) * 1000,
   259  					},
   260  					Cells: map[string]cell{
   261  						"build." + overallRow: {
   262  							Result: statuspb.TestStatus_PASS,
   263  							Metrics: map[string]float64{
   264  								"test-duration-minutes": 10 / 60.0,
   265  							},
   266  						},
   267  						"build." + podInfoRow: podInfoMissingCell,
   268  					},
   269  				},
   270  				{
   271  					Column: &statepb.Column{
   272  						Build:   "11",
   273  						Hint:    "11",
   274  						Started: float64(now+11) * 1000,
   275  					},
   276  					Cells: map[string]cell{
   277  						"build." + overallRow: {
   278  							Result:  statuspb.TestStatus_FAIL,
   279  							Icon:    "F",
   280  							Message: "Build failed outside of test results",
   281  							Metrics: map[string]float64{
   282  								"test-duration-minutes": 11 / 60.0,
   283  							},
   284  						},
   285  						"build." + podInfoRow: podInfoPassCell,
   286  					},
   287  				},
   288  			},
   289  		},
   290  		{
   291  			name: "column headers processed correctly",
   292  			builds: []fakeBuild{
   293  				{
   294  					id: "11",
   295  					started: &fakeObject{
   296  						Data: jsonData(metadata.Started{Timestamp: now + 11}),
   297  					},
   298  					finished: &fakeObject{
   299  						Data: jsonData(metadata.Finished{
   300  							Timestamp: pint64(now + 22),
   301  							Passed:    &no,
   302  							Metadata: metadata.Metadata{
   303  								metadata.JobVersion: "v0.0.0-alpha.0+build11",
   304  								"random":            "new information",
   305  							},
   306  						}),
   307  					},
   308  					podInfo: podInfoSuccess,
   309  				},
   310  				{
   311  					id: "10",
   312  					started: &fakeObject{
   313  						Data: jsonData(metadata.Started{Timestamp: now + 10}),
   314  					},
   315  					finished: &fakeObject{
   316  						Data: jsonData(metadata.Finished{
   317  							Timestamp: pint64(now + 20),
   318  							Passed:    &yes,
   319  							Metadata: metadata.Metadata{
   320  								metadata.JobVersion: "v0.0.0-alpha.0+build10",
   321  								"random":            "old information",
   322  							},
   323  						}),
   324  					},
   325  				},
   326  			},
   327  			group: &configpb.TestGroup{
   328  				GcsPrefix: "bucket/path/to/build/",
   329  				ColumnHeader: []*configpb.TestGroup_ColumnHeader{
   330  					{
   331  						ConfigurationValue: "Commit",
   332  					},
   333  					{
   334  						ConfigurationValue: "random",
   335  					},
   336  				},
   337  			},
   338  			expected: []InflatedColumn{
   339  				{
   340  					Column: &statepb.Column{
   341  						Build:   "10",
   342  						Hint:    "10",
   343  						Started: float64(now+10) * 1000,
   344  						Extra: []string{
   345  							"build10",
   346  							"old information",
   347  						},
   348  					},
   349  					Cells: map[string]cell{
   350  						"build." + overallRow: {
   351  							Result: statuspb.TestStatus_PASS,
   352  							Metrics: map[string]float64{
   353  								"test-duration-minutes": 10 / 60.0,
   354  							},
   355  						},
   356  						"build." + podInfoRow: podInfoMissingCell,
   357  					},
   358  				},
   359  				{
   360  					Column: &statepb.Column{
   361  						Build:   "11",
   362  						Hint:    "11",
   363  						Started: float64(now+11) * 1000,
   364  						Extra: []string{
   365  							"build11",
   366  							"new information",
   367  						},
   368  					},
   369  					Cells: map[string]cell{
   370  						"build." + overallRow: {
   371  							Result:  statuspb.TestStatus_FAIL,
   372  							Icon:    "F",
   373  							Message: "Build failed outside of test results",
   374  							Metrics: map[string]float64{
   375  								"test-duration-minutes": 11 / 60.0,
   376  							},
   377  						},
   378  						"build." + podInfoRow: podInfoPassCell,
   379  					},
   380  				},
   381  			},
   382  		},
   383  		{
   384  			name: "name config works correctly",
   385  			builds: []fakeBuild{
   386  				{
   387  					id: "10",
   388  					started: &fakeObject{
   389  						Data: jsonData(metadata.Started{Timestamp: now + 10}),
   390  					},
   391  					podInfo: podInfoSuccess,
   392  					finished: &fakeObject{
   393  						Data: jsonData(metadata.Finished{
   394  							Timestamp: pint64(now + 20),
   395  							Passed:    &yes,
   396  						}),
   397  					},
   398  					artifacts: map[string]fakeObject{
   399  						"junit_context-a_33.xml": {
   400  							Data: makeJunit([]string{"good"}, []string{"bad"}),
   401  						},
   402  						"junit_context-b_44.xml": {
   403  							Data: makeJunit([]string{"good"}, []string{"bad"}),
   404  						},
   405  					},
   406  				},
   407  			},
   408  			group: &configpb.TestGroup{
   409  				GcsPrefix: "bucket/path/to/build/",
   410  				TestNameConfig: &configpb.TestNameConfig{
   411  					NameFormat: "name %s - context %s - thread %s",
   412  					NameElements: []*configpb.TestNameConfig_NameElement{
   413  						{
   414  							TargetConfig: "Tests name",
   415  						},
   416  						{
   417  							TargetConfig: "Context",
   418  						},
   419  						{
   420  							TargetConfig: "Thread",
   421  						},
   422  					},
   423  				},
   424  			},
   425  			expected: []InflatedColumn{
   426  				{
   427  					Column: &statepb.Column{
   428  						Build:   "10",
   429  						Hint:    "10",
   430  						Started: float64(now+10) * 1000,
   431  					},
   432  					Cells: map[string]cell{
   433  						"build." + overallRow: {
   434  							Result: statuspb.TestStatus_PASS,
   435  							Metrics: map[string]float64{
   436  								"test-duration-minutes": 10 / 60.0,
   437  							},
   438  						},
   439  						"build." + podInfoRow: podInfoPassCell,
   440  						"name good - context context-a - thread 33": {
   441  							Result: statuspb.TestStatus_PASS,
   442  						},
   443  						"name bad - context context-a - thread 33": {
   444  							Result:  statuspb.TestStatus_FAIL,
   445  							Icon:    "F",
   446  							Message: "bad",
   447  						},
   448  						"name good - context context-b - thread 44": {
   449  							Result: statuspb.TestStatus_PASS,
   450  						},
   451  						"name bad - context context-b - thread 44": {
   452  							Result:  statuspb.TestStatus_FAIL,
   453  							Icon:    "F",
   454  							Message: "bad",
   455  						},
   456  					},
   457  				},
   458  			},
   459  		},
   460  		{
   461  			name: "truncate columns after the newest old result",
   462  			stop: time.Unix(now+13, 0), // should capture 14 and 13
   463  			builds: []fakeBuild{
   464  				{
   465  					id: "14",
   466  					started: &fakeObject{
   467  						Data: jsonData(metadata.Started{Timestamp: now + 14}),
   468  					},
   469  					finished: &fakeObject{
   470  						Data: jsonData(metadata.Finished{
   471  							Timestamp: pint64(now + 28),
   472  							Passed:    &yes,
   473  						}),
   474  					},
   475  					podInfo: podInfoSuccess,
   476  				},
   477  				{
   478  					id: "13",
   479  					started: &fakeObject{
   480  						Data: jsonData(metadata.Started{Timestamp: now + 13}),
   481  					},
   482  					finished: &fakeObject{
   483  						Data: jsonData(metadata.Finished{
   484  							Timestamp: pint64(now + 26),
   485  							Passed:    &yes,
   486  						}),
   487  					},
   488  					podInfo: podInfoSuccess,
   489  				},
   490  				{
   491  					id: "12",
   492  					started: &fakeObject{
   493  						Data: jsonData(metadata.Started{Timestamp: now + 12}),
   494  					},
   495  					finished: &fakeObject{
   496  						Data: jsonData(metadata.Finished{
   497  							Timestamp: pint64(now + 24),
   498  							Passed:    &yes,
   499  						}),
   500  					},
   501  				},
   502  				{
   503  					id: "11",
   504  					started: &fakeObject{
   505  						Data: jsonData(metadata.Started{Timestamp: now + 11}),
   506  					},
   507  					finished: &fakeObject{
   508  						Data: jsonData(metadata.Finished{
   509  							Timestamp: pint64(now + 22),
   510  							Passed:    &yes,
   511  						}),
   512  					},
   513  				},
   514  				{
   515  					id: "10",
   516  					started: &fakeObject{
   517  						Data: jsonData(metadata.Started{Timestamp: now + 10}),
   518  					},
   519  					finished: &fakeObject{
   520  						Data: jsonData(metadata.Finished{
   521  							Timestamp: pint64(now + 20),
   522  							Passed:    &yes,
   523  						}),
   524  					},
   525  				},
   526  			},
   527  			group: &configpb.TestGroup{
   528  				GcsPrefix: "bucket/path/to/build/",
   529  			},
   530  			expected: []InflatedColumn{
   531  				ancientColumn("10", .01, nil, fmt.Sprintf("build too old; started %v before %v)", now+10, now+13)),
   532  				ancientColumn("11", .02, nil, fmt.Sprintf("build too old; started %v before %v)", now+11, now+13)),
   533  				ancientColumn("12", .03, nil, fmt.Sprintf("build too old; started %v before %v)", now+12, now+13)),
   534  				{
   535  					Column: &statepb.Column{
   536  						Build:   "13",
   537  						Hint:    "13",
   538  						Started: float64(now+13) * 1000,
   539  					},
   540  					Cells: map[string]cell{
   541  						"build." + overallRow: {
   542  							Result: statuspb.TestStatus_PASS,
   543  							Metrics: map[string]float64{
   544  								"test-duration-minutes": 13 / 60.0,
   545  							},
   546  						},
   547  						"build." + podInfoRow: podInfoPassCell,
   548  					},
   549  				},
   550  				{
   551  					Column: &statepb.Column{
   552  						Build:   "14",
   553  						Hint:    "14",
   554  						Started: float64(now+14) * 1000,
   555  					},
   556  					Cells: map[string]cell{
   557  						"build." + overallRow: {
   558  							Result: statuspb.TestStatus_PASS,
   559  							Metrics: map[string]float64{
   560  								"test-duration-minutes": 14 / 60.0,
   561  							},
   562  						},
   563  						"build." + podInfoRow: podInfoPassCell,
   564  					},
   565  				},
   566  			},
   567  		},
   568  		{
   569  			name: "include no-start-time column",
   570  			stop: time.Unix(now+13, 0), // should capture 15, 14, 13
   571  			builds: []fakeBuild{
   572  				{
   573  					id: "15",
   574  					started: &fakeObject{
   575  						Data: jsonData(metadata.Started{Timestamp: now + 15}),
   576  					},
   577  					finished: &fakeObject{
   578  						Data: jsonData(metadata.Finished{
   579  							Timestamp: pint64(now + 30),
   580  							Passed:    &yes,
   581  						}),
   582  					},
   583  					podInfo: podInfoSuccess,
   584  				},
   585  				{
   586  					id: "14",
   587  					started: &fakeObject{
   588  						Data: jsonData(metadata.Started{Timestamp: 0}),
   589  					},
   590  					finished: &fakeObject{
   591  						Data: jsonData(metadata.Finished{
   592  							Timestamp: pint64(now + 28),
   593  							Passed:    &yes,
   594  						}),
   595  					},
   596  					podInfo: podInfoSuccess,
   597  				},
   598  				{
   599  					id: "13",
   600  					started: &fakeObject{
   601  						Data: jsonData(metadata.Started{Timestamp: now + 13}),
   602  					},
   603  					finished: &fakeObject{
   604  						Data: jsonData(metadata.Finished{
   605  							Timestamp: pint64(now + 26),
   606  							Passed:    &yes,
   607  						}),
   608  					},
   609  					podInfo: podInfoSuccess,
   610  				},
   611  			},
   612  			group: &configpb.TestGroup{
   613  				GcsPrefix: "bucket/path/to/build/",
   614  			},
   615  			expected: []InflatedColumn{
   616  				{
   617  					Column: &statepb.Column{
   618  						Build:   "13",
   619  						Hint:    "13",
   620  						Started: float64(now+13) * 1000,
   621  					},
   622  					Cells: map[string]cell{
   623  						"build." + overallRow: {
   624  							Result: statuspb.TestStatus_PASS,
   625  							Metrics: map[string]float64{
   626  								"test-duration-minutes": 13 / 60.0,
   627  							},
   628  						},
   629  						"build." + podInfoRow: podInfoPassCell,
   630  					},
   631  				},
   632  				noStartColumn("14", float64(now+13)*1000+0.01, nil, noStartErr.Error()), // start * 1000 + 0.01 * failures (1)
   633  				{
   634  					Column: &statepb.Column{
   635  						Build:   "15",
   636  						Hint:    "15",
   637  						Started: float64(now+15) * 1000,
   638  					},
   639  					Cells: map[string]cell{
   640  						"build." + overallRow: {
   641  							Result: statuspb.TestStatus_PASS,
   642  							Metrics: map[string]float64{
   643  								"test-duration-minutes": 15 / 60.0,
   644  							},
   645  						},
   646  						"build." + podInfoRow: podInfoPassCell,
   647  					},
   648  				},
   649  			},
   650  		},
   651  		{
   652  			name:        "high concurrency works",
   653  			concurrency: 4,
   654  			builds: []fakeBuild{
   655  				{
   656  					id: "13",
   657  					started: &fakeObject{
   658  						Data: jsonData(metadata.Started{Timestamp: now + 13}),
   659  					},
   660  					finished: &fakeObject{
   661  						Data: jsonData(metadata.Finished{
   662  							Timestamp: pint64(now + 26),
   663  							Passed:    &yes,
   664  						}),
   665  					},
   666  					podInfo: podInfoSuccess,
   667  				},
   668  				{
   669  					id: "12",
   670  					started: &fakeObject{
   671  						Data: jsonData(metadata.Started{Timestamp: now + 12}),
   672  					},
   673  					finished: &fakeObject{
   674  						Data: jsonData(metadata.Finished{
   675  							Timestamp: pint64(now + 24),
   676  							Passed:    &yes,
   677  						}),
   678  					},
   679  				},
   680  				{
   681  					id: "11",
   682  					started: &fakeObject{
   683  						Data: jsonData(metadata.Started{Timestamp: now + 11}),
   684  					},
   685  					finished: &fakeObject{
   686  						Data: jsonData(metadata.Finished{
   687  							Timestamp: pint64(now + 22),
   688  							Passed:    &yes,
   689  						}),
   690  					},
   691  					podInfo: podInfoSuccess,
   692  				},
   693  				{
   694  					id: "10",
   695  					started: &fakeObject{
   696  						Data: jsonData(metadata.Started{Timestamp: now + 10}),
   697  					},
   698  					finished: &fakeObject{
   699  						Data: jsonData(metadata.Finished{
   700  							Timestamp: pint64(now + 20),
   701  							Passed:    &yes,
   702  						}),
   703  					},
   704  				},
   705  			},
   706  			group: &configpb.TestGroup{
   707  				GcsPrefix: "bucket/path/to/build/",
   708  			},
   709  			expected: []InflatedColumn{
   710  				{
   711  					Column: &statepb.Column{
   712  						Build:   "13",
   713  						Hint:    "13",
   714  						Started: float64(now+13) * 1000,
   715  					},
   716  					Cells: map[string]cell{
   717  						"build." + overallRow: {
   718  							Result: statuspb.TestStatus_PASS,
   719  							Metrics: map[string]float64{
   720  								"test-duration-minutes": 13 / 60.0,
   721  							},
   722  						},
   723  						"build." + podInfoRow: podInfoPassCell,
   724  					},
   725  				},
   726  				{
   727  					Column: &statepb.Column{
   728  						Build:   "12",
   729  						Hint:    "12",
   730  						Started: float64(now+12) * 1000,
   731  					},
   732  					Cells: map[string]cell{
   733  						"build." + overallRow: {
   734  							Result: statuspb.TestStatus_PASS,
   735  							Metrics: map[string]float64{
   736  								"test-duration-minutes": 12 / 60.0,
   737  							},
   738  						},
   739  						"build." + podInfoRow: podInfoMissingCell,
   740  					},
   741  				},
   742  				{
   743  					Column: &statepb.Column{
   744  						Build:   "11",
   745  						Hint:    "11",
   746  						Started: float64(now+11) * 1000,
   747  					},
   748  					Cells: map[string]cell{
   749  						"build." + overallRow: {
   750  							Result: statuspb.TestStatus_PASS,
   751  							Metrics: map[string]float64{
   752  								"test-duration-minutes": 11 / 60.0,
   753  							},
   754  						},
   755  						"build." + podInfoRow: podInfoPassCell,
   756  					},
   757  				},
   758  				{
   759  					Column: &statepb.Column{
   760  						Build:   "10",
   761  						Hint:    "10",
   762  						Started: float64(now+10) * 1000,
   763  					},
   764  					Cells: map[string]cell{
   765  						"build." + overallRow: {
   766  							Result: statuspb.TestStatus_PASS,
   767  							Metrics: map[string]float64{
   768  								"test-duration-minutes": 10 / 60.0,
   769  							},
   770  						},
   771  						"build." + podInfoRow: podInfoMissingCell,
   772  					},
   773  				},
   774  			},
   775  		},
   776  		{
   777  			name:        "truncate columns after the newest old result with high concurrency",
   778  			concurrency: 30,
   779  			stop:        time.Unix(now+13, 0), // should capture 13 and 12
   780  			builds: []fakeBuild{
   781  				{
   782  					id: "13",
   783  					started: &fakeObject{
   784  						Data: jsonData(metadata.Started{Timestamp: now + 13}),
   785  					},
   786  					finished: &fakeObject{
   787  						Data: jsonData(metadata.Finished{
   788  							Timestamp: pint64(now + 26),
   789  							Passed:    &yes,
   790  						}),
   791  					},
   792  				},
   793  				{
   794  					id: "12",
   795  					started: &fakeObject{
   796  						Data: jsonData(metadata.Started{Timestamp: now + 12}),
   797  					},
   798  					finished: &fakeObject{
   799  						Data: jsonData(metadata.Finished{
   800  							Timestamp: pint64(now + 24),
   801  							Passed:    &yes,
   802  						}),
   803  					},
   804  					podInfo: podInfoSuccess,
   805  				},
   806  				{
   807  					id: "11",
   808  					started: &fakeObject{
   809  						Data: jsonData(metadata.Started{Timestamp: now + 11}),
   810  					},
   811  					finished: &fakeObject{
   812  						Data: jsonData(metadata.Finished{
   813  							Timestamp: pint64(now + 22),
   814  							Passed:    &yes,
   815  						}),
   816  					},
   817  				},
   818  				{
   819  					id: "10",
   820  					started: &fakeObject{
   821  						Data: jsonData(metadata.Started{Timestamp: now + 10}),
   822  					},
   823  					finished: &fakeObject{
   824  						Data: jsonData(metadata.Finished{
   825  							Timestamp: pint64(now + 20),
   826  							Passed:    &yes,
   827  						}),
   828  					},
   829  				},
   830  			},
   831  			group: &configpb.TestGroup{
   832  				GcsPrefix: "bucket/path/to/build/",
   833  			},
   834  			expected: []InflatedColumn{
   835  				{
   836  					Column: &statepb.Column{
   837  						Build:   "13",
   838  						Hint:    "13",
   839  						Started: float64(now+13) * 1000,
   840  					},
   841  					Cells: map[string]cell{
   842  						"build." + overallRow: {
   843  							Result: statuspb.TestStatus_PASS,
   844  							Metrics: map[string]float64{
   845  								"test-duration-minutes": 13 / 60.0,
   846  							},
   847  						},
   848  						"build." + podInfoRow: podInfoMissingCell,
   849  					},
   850  				},
   851  				{
   852  					Column: &statepb.Column{
   853  						Build:   "12",
   854  						Hint:    "12",
   855  						Started: float64(now+12) * 1000,
   856  					},
   857  					Cells: map[string]cell{
   858  						"build." + overallRow: {
   859  							Result: statuspb.TestStatus_PASS,
   860  							Metrics: map[string]float64{
   861  								"test-duration-minutes": 12 / 60.0,
   862  							},
   863  						},
   864  						"build." + podInfoRow: podInfoPassCell,
   865  					},
   866  				},
   867  				// drop 11 and 10
   868  			},
   869  		},
   870  		{
   871  			name: "cancelled context returns error",
   872  			builds: []fakeBuild{
   873  				{id: "10"},
   874  			},
   875  			ctx: func() context.Context {
   876  				ctx, cancel := context.WithCancel(context.Background())
   877  				cancel()
   878  				ctx.Err()
   879  				return ctx
   880  			}(),
   881  		},
   882  		{
   883  			name: "some errors",
   884  			builds: []fakeBuild{
   885  				{
   886  					id: "14-err",
   887  					started: &fakeObject{
   888  						OpenErr: errors.New("fake open 14-err"),
   889  					},
   890  				},
   891  				{
   892  					id: "13",
   893  					started: &fakeObject{
   894  						Data: jsonData(metadata.Started{Timestamp: now + 13}),
   895  					},
   896  					finished: &fakeObject{
   897  						Data: jsonData(metadata.Finished{
   898  							Timestamp: pint64(now + 26),
   899  							Passed:    &no,
   900  						}),
   901  					},
   902  					podInfo: podInfoSuccess,
   903  				},
   904  				{
   905  					id: "10-b-err",
   906  					started: &fakeObject{
   907  						OpenErr: errors.New("fake open 10-b-err"),
   908  					},
   909  				},
   910  				{
   911  					id: "10-a-err",
   912  					started: &fakeObject{
   913  						ReadErr: errors.New("fake read 10-a-err"),
   914  					},
   915  				},
   916  				{
   917  					id: "9",
   918  					started: &fakeObject{
   919  						Data: jsonData(metadata.Started{Timestamp: now + 9}),
   920  					},
   921  					finished: &fakeObject{
   922  						Data: jsonData(metadata.Finished{
   923  							Timestamp: pint64(now + 18),
   924  							Passed:    &yes,
   925  						}),
   926  					},
   927  				},
   928  				{
   929  					id: "8-err",
   930  					started: &fakeObject{
   931  						ReadErr: errors.New("fake read 8-err"),
   932  					},
   933  				},
   934  			},
   935  			group: &configpb.TestGroup{
   936  				GcsPrefix: "bucket/path/to/build/",
   937  			},
   938  			expected: []InflatedColumn{
   939  				{
   940  					Column: &statepb.Column{
   941  						Build:   "8-err",
   942  						Hint:    "8-err",
   943  						Started: .01,
   944  					},
   945  					Cells: map[string]cell{
   946  						overallRow: {
   947  							Result:  statuspb.TestStatus_TOOL_FAIL,
   948  							Message: "Failed to download gs://bucket/path/to/build/8-err/: started: read: decode: fake read 8-err",
   949  						},
   950  					},
   951  				},
   952  				{
   953  					Column: &statepb.Column{
   954  						Build:   "9",
   955  						Hint:    "9",
   956  						Started: float64(now+9) * 1000,
   957  					},
   958  					Cells: map[string]cell{
   959  						"build." + overallRow: {
   960  							Result: statuspb.TestStatus_PASS,
   961  							Metrics: map[string]float64{
   962  								"test-duration-minutes": 9 / 60.0,
   963  							},
   964  						},
   965  						"build." + podInfoRow: podInfoMissingCell,
   966  					},
   967  				},
   968  				{
   969  					Column: &statepb.Column{
   970  						Build:   "10-a-err",
   971  						Hint:    "10-a-err",
   972  						Started: float64(now+9)*1000 + .01,
   973  					},
   974  					Cells: map[string]cell{
   975  						overallRow: {
   976  							Result:  statuspb.TestStatus_TOOL_FAIL,
   977  							Message: "Failed to download gs://bucket/path/to/build/10-a-err/: started: read: decode: fake read 10-a-err",
   978  						},
   979  					},
   980  				},
   981  				{
   982  					Column: &statepb.Column{
   983  						Build:   "10-b-err",
   984  						Hint:    "10-b-err",
   985  						Started: float64(now+9)*1000 + 0.02,
   986  					},
   987  					Cells: map[string]cell{
   988  						overallRow: {
   989  							Result:  statuspb.TestStatus_TOOL_FAIL,
   990  							Message: "Failed to download gs://bucket/path/to/build/10-b-err/: started: read: open: fake open 10-b-err",
   991  						},
   992  					},
   993  				},
   994  				{
   995  					Column: &statepb.Column{
   996  						Build:   "13",
   997  						Hint:    "13",
   998  						Started: float64(now+13) * 1000,
   999  					},
  1000  					Cells: map[string]cell{
  1001  						"build." + overallRow: {
  1002  							Result:  statuspb.TestStatus_FAIL,
  1003  							Icon:    "F",
  1004  							Message: "Build failed outside of test results",
  1005  							Metrics: map[string]float64{
  1006  								"test-duration-minutes": 13 / 60.0,
  1007  							},
  1008  						},
  1009  						"build." + podInfoRow: podInfoPassCell,
  1010  					},
  1011  				},
  1012  				{
  1013  					Column: &statepb.Column{
  1014  						Build:   "14-err",
  1015  						Hint:    "14-err",
  1016  						Started: float64(now+13)*1000 + 0.01,
  1017  					},
  1018  					Cells: map[string]cell{
  1019  						overallRow: {
  1020  							Result:  statuspb.TestStatus_TOOL_FAIL,
  1021  							Message: "Failed to download gs://bucket/path/to/build/14-err/: started: read: open: fake open 14-err",
  1022  						},
  1023  					},
  1024  				},
  1025  			},
  1026  		},
  1027  		{
  1028  			name: "only errors",
  1029  			builds: []fakeBuild{
  1030  				{
  1031  					id: "10-b-err",
  1032  					started: &fakeObject{
  1033  						OpenErr: errors.New("fake open 10-b-err"),
  1034  					},
  1035  				},
  1036  				{
  1037  					id: "10-a-err",
  1038  					started: &fakeObject{
  1039  						ReadErr: errors.New("fake read 10-a-err"),
  1040  					},
  1041  				},
  1042  			},
  1043  			group: &configpb.TestGroup{
  1044  				GcsPrefix: "bucket/path/to/build/",
  1045  			},
  1046  			expected: []InflatedColumn{
  1047  				erroredColumn("10-a-err", 0.01, nil, "Failed to download gs://bucket/path/to/build/10-a-err/: started: read: decode: fake read 10-a-err"),
  1048  				erroredColumn("10-b-err", 0.02, nil, "Failed to download gs://bucket/path/to/build/10-b-err/: started: read: open: fake open 10-b-err"),
  1049  			},
  1050  		},
  1051  		{
  1052  			name: "ignore_skip works when enabled",
  1053  			group: &configpb.TestGroup{
  1054  				IgnoreSkip: true,
  1055  			},
  1056  			enableIgnoreSkip: true,
  1057  			builds: []fakeBuild{
  1058  				{
  1059  					id:      "build-1",
  1060  					podInfo: podInfoSuccess,
  1061  					started: &fakeObject{
  1062  						Data: jsonData(metadata.Started{Timestamp: now}),
  1063  					},
  1064  					finished: &fakeObject{
  1065  						Data: jsonData(metadata.Finished{
  1066  							Timestamp: pint64(now + 10),
  1067  							Passed:    &yes,
  1068  						}),
  1069  					},
  1070  					artifacts: map[string]fakeObject{
  1071  						"junit_context-a_33.xml": {
  1072  							Data: xmlData(
  1073  								junit.Suite{
  1074  									Results: []junit.Result{
  1075  										{
  1076  											Name:    "visible skip non-default msg",
  1077  											Skipped: &junit.Skipped{Message: *pstr("non-default message")},
  1078  										},
  1079  									},
  1080  								}),
  1081  						},
  1082  					},
  1083  				},
  1084  			},
  1085  			expected: []InflatedColumn{
  1086  				{
  1087  					Column: &statepb.Column{
  1088  						Build:   "build-1",
  1089  						Started: float64(now * 1000),
  1090  						Hint:    "build-1",
  1091  					},
  1092  					Cells: map[string]Cell{
  1093  						".." + overallRow: {
  1094  							Result: statuspb.TestStatus_PASS,
  1095  							Metrics: map[string]float64{
  1096  								"test-duration-minutes": 10 / 60.0,
  1097  							},
  1098  						},
  1099  						".." + podInfoRow: podInfoPassCell,
  1100  					},
  1101  				},
  1102  			},
  1103  		},
  1104  		{
  1105  			name: "ignore_skip ignored when disabled",
  1106  			group: &configpb.TestGroup{
  1107  				IgnoreSkip: true,
  1108  			},
  1109  			builds: []fakeBuild{
  1110  				{
  1111  					id:      "build-1",
  1112  					podInfo: podInfoSuccess,
  1113  					started: &fakeObject{
  1114  						Data: jsonData(metadata.Started{Timestamp: now}),
  1115  					},
  1116  					finished: &fakeObject{
  1117  						Data: jsonData(metadata.Finished{
  1118  							Timestamp: pint64(now + 10),
  1119  							Passed:    &yes,
  1120  						}),
  1121  					},
  1122  					artifacts: map[string]fakeObject{
  1123  						"junit_context-a_33.xml": {
  1124  							Data: xmlData(
  1125  								junit.Suite{
  1126  									Results: []junit.Result{
  1127  										{
  1128  											Name:    "visible skip non-default msg",
  1129  											Skipped: &junit.Skipped{Message: *pstr("non-default message")},
  1130  										},
  1131  									},
  1132  								}),
  1133  						},
  1134  					},
  1135  				},
  1136  			},
  1137  			expected: []InflatedColumn{
  1138  				{
  1139  					Column: &statepb.Column{
  1140  						Build:   "build-1",
  1141  						Started: float64(now * 1000),
  1142  						Hint:    "build-1",
  1143  					},
  1144  					Cells: map[string]Cell{
  1145  						".." + overallRow: {
  1146  							Result: statuspb.TestStatus_PASS,
  1147  							Metrics: map[string]float64{
  1148  								"test-duration-minutes": 10 / 60.0,
  1149  							},
  1150  						},
  1151  						".." + podInfoRow: podInfoPassCell,
  1152  						"visible skip non-default msg": {
  1153  							Result:  statuspb.TestStatus_PASS_WITH_SKIPS,
  1154  							Icon:    "S",
  1155  							Message: "non-default message",
  1156  						},
  1157  					},
  1158  				},
  1159  			},
  1160  		},
  1161  	}
  1162  
  1163  	poolCtx, poolCancel := context.WithCancel(context.Background())
  1164  	defer poolCancel()
  1165  	readResultPool := resultReaderPool(poolCtx, logrus.WithField("pool", "readResult"), 10)
  1166  
  1167  	for _, tc := range cases {
  1168  		t.Run(tc.name, func(t *testing.T) {
  1169  			if tc.group == nil {
  1170  				tc.group = &configpb.TestGroup{}
  1171  			}
  1172  			path := newPathOrDie("gs://" + tc.group.GcsPrefix)
  1173  			if tc.ctx == nil {
  1174  				tc.ctx = context.Background()
  1175  			}
  1176  			ctx, cancel := context.WithCancel(tc.ctx)
  1177  			ctx.Err()
  1178  			defer cancel()
  1179  			client := fakeClient{
  1180  				Lister: fake.Lister{},
  1181  				Opener: fake.Opener{
  1182  					Paths: map[gcs.Path]fake.Object{},
  1183  					Lock:  &sync.RWMutex{},
  1184  				},
  1185  			}
  1186  
  1187  			builds := addBuilds(&client, path, tc.builds...)
  1188  
  1189  			if tc.concurrency == 0 {
  1190  				tc.concurrency = 1
  1191  			} else {
  1192  				t.Skip("TODO(fejta): re-add concurrent build reading")
  1193  			}
  1194  
  1195  			if tc.dur == 0 {
  1196  				tc.dur = 5 * time.Minute
  1197  			}
  1198  
  1199  			var actual []InflatedColumn
  1200  
  1201  			ch := make(chan InflatedColumn)
  1202  			var wg sync.WaitGroup
  1203  
  1204  			wg.Add(1)
  1205  			go func() {
  1206  				defer wg.Done()
  1207  				time.Sleep(10 * time.Millisecond) // Give time for context to expire
  1208  				for col := range ch {
  1209  					actual = append(actual, col)
  1210  				}
  1211  
  1212  			}()
  1213  
  1214  			readResult := tc.readResultOverride
  1215  			if readResult == nil {
  1216  				readResult = readResultPool
  1217  			}
  1218  
  1219  			readColumns(ctx, client, logrus.WithField("name", tc.name), tc.group, builds, tc.stop, tc.dur, ch, readResult, tc.enableIgnoreSkip)
  1220  			close(ch)
  1221  			wg.Wait()
  1222  
  1223  			if diff := cmp.Diff(tc.expected, actual, protocmp.Transform()); diff != "" {
  1224  				t.Errorf("readColumns() got unexpected diff (-want +got):\n%s", diff)
  1225  			}
  1226  		})
  1227  	}
  1228  }
  1229  
  1230  func TestRender(t *testing.T) {
  1231  	cases := []struct {
  1232  		name      string
  1233  		format    string
  1234  		parts     []string
  1235  		job       string
  1236  		test      string
  1237  		metadatas []map[string]string
  1238  		expected  string
  1239  	}{
  1240  		{
  1241  			name: "basically works",
  1242  		},
  1243  		{
  1244  			name:     "test name works",
  1245  			format:   "%s",
  1246  			parts:    []string{"Tests name"}, // keep the literal
  1247  			test:     "hello",
  1248  			expected: "hello",
  1249  		},
  1250  		{
  1251  			name:     "missing fields work",
  1252  			format:   "%s -(%s)- %s",
  1253  			parts:    []string{testsName, "something", jobName},
  1254  			job:      "this",
  1255  			test:     "hi",
  1256  			expected: "hi -()- this",
  1257  		},
  1258  		{
  1259  			name:   "first and second metadata work",
  1260  			format: "first %s, second %s",
  1261  			parts:  []string{"first", "second"},
  1262  			metadatas: []map[string]string{
  1263  				{
  1264  					"first": "hi",
  1265  				},
  1266  				{
  1267  					"second": "there",
  1268  					"first":  "ignore this",
  1269  				},
  1270  			},
  1271  			expected: "first hi, second there",
  1272  		},
  1273  		{
  1274  			name:   "prefer first metadata value over second",
  1275  			format: "test: %s, job: %s, meta: %s",
  1276  			parts:  []string{testsName, jobName, "meta"},
  1277  			test:   "fancy",
  1278  			job:    "work",
  1279  			metadatas: []map[string]string{
  1280  				{
  1281  					"meta":    "yes",
  1282  					testsName: "ignore",
  1283  				},
  1284  				{
  1285  					"meta":  "no",
  1286  					jobName: "wrong",
  1287  				},
  1288  			},
  1289  			expected: "test: fancy, job: work, meta: yes",
  1290  		},
  1291  	}
  1292  
  1293  	for _, tc := range cases {
  1294  		t.Run(tc.name, func(t *testing.T) {
  1295  			nc := nameConfig{
  1296  				format: tc.format,
  1297  				parts:  tc.parts,
  1298  			}
  1299  			actual := nc.render(tc.job, tc.test, tc.metadatas...)
  1300  			if actual != tc.expected {
  1301  				t.Errorf("render() got %q want %q", actual, tc.expected)
  1302  			}
  1303  		})
  1304  	}
  1305  }
  1306  
  1307  func TestMakeNameConfig(t *testing.T) {
  1308  	cases := []struct {
  1309  		name     string
  1310  		group    *configpb.TestGroup
  1311  		expected nameConfig
  1312  	}{
  1313  		{
  1314  			name:  "basically works",
  1315  			group: &configpb.TestGroup{},
  1316  			expected: nameConfig{
  1317  				format: "%s",
  1318  				parts:  []string{testsName},
  1319  			},
  1320  		},
  1321  		{
  1322  			name: "explicit config works",
  1323  			group: &configpb.TestGroup{
  1324  				TestNameConfig: &configpb.TestNameConfig{
  1325  					NameFormat: "%s %s",
  1326  					NameElements: []*configpb.TestNameConfig_NameElement{
  1327  						{
  1328  							TargetConfig: "hello",
  1329  						},
  1330  						{
  1331  							TargetConfig: "world",
  1332  						},
  1333  					},
  1334  				},
  1335  			},
  1336  			expected: nameConfig{
  1337  				format: "%s %s",
  1338  				parts:  []string{"hello", "world"},
  1339  			},
  1340  		},
  1341  		{
  1342  			name: "test properties work",
  1343  			group: &configpb.TestGroup{
  1344  				TestNameConfig: &configpb.TestNameConfig{
  1345  					NameFormat: "%s %s",
  1346  					NameElements: []*configpb.TestNameConfig_NameElement{
  1347  						{
  1348  							TargetConfig: "hello",
  1349  						},
  1350  						{
  1351  							TestProperty: "world",
  1352  						},
  1353  					},
  1354  				},
  1355  			},
  1356  			expected: nameConfig{
  1357  				format: "%s %s",
  1358  				parts:  []string{"hello", "world"},
  1359  			},
  1360  		},
  1361  		{
  1362  			name: "target config precedes test property",
  1363  			group: &configpb.TestGroup{
  1364  				TestNameConfig: &configpb.TestNameConfig{
  1365  					NameFormat: "%s works",
  1366  					NameElements: []*configpb.TestNameConfig_NameElement{
  1367  						{
  1368  							TargetConfig: "good-target",
  1369  							TestProperty: "nope-property",
  1370  						},
  1371  					},
  1372  				},
  1373  			},
  1374  			expected: nameConfig{
  1375  				format: "%s works",
  1376  				parts:  []string{"good-target"},
  1377  			},
  1378  		},
  1379  		{
  1380  			name: "auto-inject job name into default config",
  1381  			group: &configpb.TestGroup{
  1382  				GcsPrefix: "this,that",
  1383  			},
  1384  			expected: nameConfig{
  1385  				format:   "%s.%s",
  1386  				parts:    []string{jobName, testsName},
  1387  				multiJob: true,
  1388  			},
  1389  		},
  1390  		{
  1391  			name: "auto-inject job name into explicit config",
  1392  			group: &configpb.TestGroup{
  1393  				GcsPrefix: "this,that",
  1394  				TestNameConfig: &configpb.TestNameConfig{
  1395  					NameFormat: "%s %s",
  1396  					NameElements: []*configpb.TestNameConfig_NameElement{
  1397  						{
  1398  							TargetConfig: "hello",
  1399  						},
  1400  						{
  1401  							TargetConfig: "world",
  1402  						},
  1403  					},
  1404  				},
  1405  			},
  1406  			expected: nameConfig{
  1407  				format:   "%s.%s %s",
  1408  				parts:    []string{jobName, "hello", "world"},
  1409  				multiJob: true,
  1410  			},
  1411  		},
  1412  		{
  1413  			name: "allow explicit job name config",
  1414  			group: &configpb.TestGroup{
  1415  				GcsPrefix: "this,that",
  1416  				TestNameConfig: &configpb.TestNameConfig{
  1417  					NameFormat: "%s %s (%s)",
  1418  					NameElements: []*configpb.TestNameConfig_NameElement{
  1419  						{
  1420  							TargetConfig: "hello",
  1421  						},
  1422  						{
  1423  							TargetConfig: "world",
  1424  						},
  1425  						{
  1426  							TargetConfig: jobName,
  1427  						},
  1428  					},
  1429  				},
  1430  			},
  1431  			expected: nameConfig{
  1432  				format:   "%s %s (%s)",
  1433  				parts:    []string{"hello", "world", jobName},
  1434  				multiJob: true,
  1435  			},
  1436  		},
  1437  	}
  1438  
  1439  	for _, tc := range cases {
  1440  		t.Run(tc.name, func(t *testing.T) {
  1441  			actual := makeNameConfig(tc.group)
  1442  			if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(nameConfig{})); diff != "" {
  1443  				t.Errorf("makeNameConfig() got unexpected diff (-got +want):\n%s", diff)
  1444  			}
  1445  		})
  1446  	}
  1447  }
  1448  
  1449  func TestReadResult(t *testing.T) {
  1450  	path := newPathOrDie("gs://bucket/path/to/some/build/")
  1451  	yes := true
  1452  	cases := []struct {
  1453  		name string
  1454  		ctx  context.Context
  1455  		data map[string]fakeObject
  1456  		stop time.Time
  1457  
  1458  		expected *gcsResult
  1459  	}{
  1460  		{
  1461  			name: "basically works",
  1462  			expected: &gcsResult{
  1463  				started: gcs.Started{
  1464  					Pending: true,
  1465  				},
  1466  				finished: gcs.Finished{
  1467  					Running: true,
  1468  				},
  1469  				job:   "some",
  1470  				build: "build",
  1471  			},
  1472  		},
  1473  		{
  1474  			name: "cancelled context returns error",
  1475  			ctx: func() context.Context {
  1476  				ctx, cancel := context.WithCancel(context.Background())
  1477  				cancel()
  1478  				return ctx
  1479  			}(),
  1480  		},
  1481  		{
  1482  			name: "all info present",
  1483  			data: map[string]fakeObject{
  1484  				"podinfo.json":       {Data: `{"pod":{"metadata":{"name":"woot"}}}`},
  1485  				"started.json":       {Data: `{"node": "fun"}`},
  1486  				"finished.json":      {Data: `{"passed": true}`},
  1487  				"junit_super_88.xml": {Data: `<testsuite><testcase name="foo"/></testsuite>`},
  1488  			},
  1489  			expected: &gcsResult{
  1490  				podInfo: func() gcs.PodInfo {
  1491  					out := gcs.PodInfo{Pod: &core.Pod{}}
  1492  					out.Pod.Name = "woot"
  1493  					return out
  1494  				}(),
  1495  				started: gcs.Started{
  1496  					Started: metadata.Started{Node: "fun"},
  1497  				},
  1498  				finished: gcs.Finished{
  1499  					Finished: metadata.Finished{Passed: &yes},
  1500  				},
  1501  				suites: []gcs.SuitesMeta{
  1502  					{
  1503  						Suites: &junit.Suites{
  1504  							Suites: []junit.Suite{
  1505  								{
  1506  									XMLName: xml.Name{Local: "testsuite"},
  1507  									Results: []junit.Result{
  1508  										{Name: "foo"},
  1509  									},
  1510  								},
  1511  							},
  1512  						},
  1513  						Metadata: map[string]string{
  1514  							"Context":   "super",
  1515  							"Thread":    "88",
  1516  							"Timestamp": "",
  1517  						},
  1518  						Path: "gs://bucket/path/to/some/build/junit_super_88.xml",
  1519  					},
  1520  				},
  1521  			},
  1522  		},
  1523  		{
  1524  			name: "empty files report missing",
  1525  			data: map[string]fakeObject{
  1526  				"finished.json": {Data: ""},
  1527  				"started.json":  {Data: ""},
  1528  				"podinfo.json":  {Data: ""},
  1529  			},
  1530  			expected: &gcsResult{
  1531  				malformed: []string{
  1532  					"finished.json",
  1533  					"podinfo.json",
  1534  					"started.json",
  1535  				},
  1536  			},
  1537  		},
  1538  		{
  1539  			name: "missing started.json reports pending",
  1540  			data: map[string]fakeObject{
  1541  				"finished.json":      {Data: `{"passed": true}`},
  1542  				"junit_super_88.xml": {Data: `<testsuite><testcase name="foo"/></testsuite>`},
  1543  			},
  1544  			expected: &gcsResult{
  1545  				started: gcs.Started{
  1546  					Pending: true,
  1547  				},
  1548  				finished: gcs.Finished{
  1549  					Finished: metadata.Finished{Passed: &yes},
  1550  				},
  1551  				suites: []gcs.SuitesMeta{
  1552  					{
  1553  						Suites: &junit.Suites{
  1554  							Suites: []junit.Suite{
  1555  								{
  1556  									XMLName: xml.Name{Local: "testsuite"},
  1557  									Results: []junit.Result{
  1558  										{Name: "foo"},
  1559  									},
  1560  								},
  1561  							},
  1562  						},
  1563  						Metadata: map[string]string{
  1564  							"Context":   "super",
  1565  							"Thread":    "88",
  1566  							"Timestamp": "",
  1567  						},
  1568  						Path: "gs://bucket/path/to/some/build/junit_super_88.xml",
  1569  					},
  1570  				},
  1571  			},
  1572  		},
  1573  		{
  1574  			name: "no finished reports running",
  1575  			data: map[string]fakeObject{
  1576  				"started.json":       {Data: `{"node": "fun"}`},
  1577  				"junit_super_88.xml": {Data: `<testsuite><testcase name="foo"/></testsuite>`},
  1578  			},
  1579  			expected: &gcsResult{
  1580  				started: gcs.Started{
  1581  					Started: metadata.Started{Node: "fun"},
  1582  				},
  1583  				finished: gcs.Finished{
  1584  					Running: true,
  1585  				},
  1586  				suites: []gcs.SuitesMeta{
  1587  					{
  1588  						Suites: &junit.Suites{
  1589  							Suites: []junit.Suite{
  1590  								{
  1591  									XMLName: xml.Name{Local: "testsuite"},
  1592  									Results: []junit.Result{
  1593  										{Name: "foo"},
  1594  									},
  1595  								},
  1596  							},
  1597  						},
  1598  						Metadata: map[string]string{
  1599  							"Context":   "super",
  1600  							"Thread":    "88",
  1601  							"Timestamp": "",
  1602  						},
  1603  						Path: "gs://bucket/path/to/some/build/junit_super_88.xml",
  1604  					},
  1605  				},
  1606  			},
  1607  		},
  1608  		{
  1609  			name: "no artifacts report no suites",
  1610  			data: map[string]fakeObject{
  1611  				"started.json":  {Data: `{"node": "fun"}`},
  1612  				"finished.json": {Data: `{"passed": true}`},
  1613  			},
  1614  			expected: &gcsResult{
  1615  				started: gcs.Started{
  1616  					Started: metadata.Started{Node: "fun"},
  1617  				},
  1618  				finished: gcs.Finished{
  1619  					Finished: metadata.Finished{Passed: &yes},
  1620  				},
  1621  			},
  1622  		},
  1623  		{
  1624  			name: "started error returns error",
  1625  			data: map[string]fakeObject{
  1626  				"started.json": {
  1627  					Data:     "{}",
  1628  					CloseErr: errors.New("injected closer error"),
  1629  				},
  1630  				"finished.json":      {Data: `{"passed": true}`},
  1631  				"junit_super_88.xml": {Data: `<testsuite><testcase name="foo"/></testsuite>`},
  1632  			},
  1633  		},
  1634  		{
  1635  			name: "finished error returns error",
  1636  			data: map[string]fakeObject{
  1637  				"started.json":       {Data: `{"node": "fun"}`},
  1638  				"finished.json":      {ReadErr: errors.New("injected read error")},
  1639  				"junit_super_88.xml": {Data: `<testsuite><testcase name="foo"/></testsuite>`},
  1640  			},
  1641  		},
  1642  		{
  1643  			name: "artifact error added to malformed list",
  1644  			data: map[string]fakeObject{
  1645  				"started.json":       {Data: `{"node": "fun"}`},
  1646  				"finished.json":      {Data: `{"passed": true}`},
  1647  				"junit_super_88.xml": {OpenErr: errors.New("injected open error")},
  1648  			},
  1649  			expected: &gcsResult{
  1650  				started: gcs.Started{
  1651  					Started: metadata.Started{Node: "fun"},
  1652  				},
  1653  				finished: gcs.Finished{
  1654  					Finished: metadata.Finished{Passed: &yes},
  1655  				},
  1656  				malformed: []string{"junit_super_88.xml: open: injected open error"},
  1657  			},
  1658  		},
  1659  	}
  1660  
  1661  	for _, tc := range cases {
  1662  		t.Run(tc.name, func(t *testing.T) {
  1663  			if tc.ctx == nil {
  1664  				tc.ctx = context.Background()
  1665  			}
  1666  			if tc.expected != nil {
  1667  				tc.expected.job = "some"
  1668  				tc.expected.build = "build"
  1669  			}
  1670  			ctx, cancel := context.WithCancel(tc.ctx)
  1671  			defer cancel()
  1672  			client := fakeClient{
  1673  				Lister: fake.Lister{},
  1674  				Opener: fake.Opener{
  1675  					Paths: map[gcs.Path]fake.Object{},
  1676  					Lock:  &sync.RWMutex{},
  1677  				},
  1678  			}
  1679  
  1680  			fi := fakeIterator{}
  1681  			for name, fo := range tc.data {
  1682  				p, err := path.ResolveReference(&url.URL{Path: name})
  1683  				if err != nil {
  1684  					t.Fatalf("path.ResolveReference(%q): %v", name, err)
  1685  				}
  1686  				fi.Objects = append(fi.Objects, storage.ObjectAttrs{
  1687  					Name: p.Object(),
  1688  				})
  1689  				client.Opener.Paths[*p] = fo
  1690  			}
  1691  			client.Lister[path] = fi
  1692  
  1693  			build := gcs.Build{
  1694  				Path: path,
  1695  			}
  1696  			actual, err := readResult(ctx, client, build, tc.stop)
  1697  			switch {
  1698  			case err != nil:
  1699  				if tc.expected != nil {
  1700  					t.Errorf("readResult(): unexpected error: %v", err)
  1701  				}
  1702  			case tc.expected == nil:
  1703  				t.Error("readResult(): failed to receive expected error")
  1704  			default:
  1705  				if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(gcsResult{})); diff != "" {
  1706  					t.Errorf("readResult() got unexpected diff (-have, +want):\n%s", diff)
  1707  				}
  1708  			}
  1709  		})
  1710  	}
  1711  }
  1712  
  1713  func newPathOrDie(s string) gcs.Path {
  1714  	p, err := gcs.NewPath(s)
  1715  	if err != nil {
  1716  		panic(err)
  1717  	}
  1718  	return *p
  1719  }
  1720  
  1721  func TestReadSuites(t *testing.T) {
  1722  	path := newPathOrDie("gs://bucket/path/to/build/")
  1723  	cases := []struct {
  1724  		name       string
  1725  		data       map[string]fakeObject
  1726  		listIdxErr int
  1727  		expected   []gcs.SuitesMeta
  1728  		err        bool
  1729  		ctx        context.Context
  1730  	}{
  1731  		{
  1732  			name: "basically works",
  1733  		},
  1734  		{
  1735  			name: "multiple suites from multiple artifacts work",
  1736  			data: map[string]fakeObject{
  1737  				"ignore-this": {Data: "<invalid></xml>"},
  1738  				"junit.xml":   {Data: `<testsuite><testcase name="hi"/></testsuite>`},
  1739  				"ignore-that": {Data: "<invalid></xml>"},
  1740  				"nested/junit_context_20201122-1234_88.xml": {
  1741  					Data: `
  1742                          <testsuites>
  1743                              <testsuite name="fun">
  1744                                  <testsuite name="knee">
  1745                                      <testcase name="bone" time="6" />
  1746                                  </testsuite>
  1747                                  <testcase name="word" time="7" />
  1748                              </testsuite>
  1749                          </testsuites>
  1750                      `,
  1751  				},
  1752  			},
  1753  			expected: []gcs.SuitesMeta{
  1754  				{
  1755  					Suites: &junit.Suites{
  1756  						Suites: []junit.Suite{
  1757  							{
  1758  								XMLName: xml.Name{Local: "testsuite"},
  1759  								Results: []junit.Result{
  1760  									{Name: "hi"},
  1761  								},
  1762  							},
  1763  						},
  1764  					},
  1765  					Metadata: map[string]string{
  1766  						"Context":   "",
  1767  						"Thread":    "",
  1768  						"Timestamp": "",
  1769  					},
  1770  					Path: "gs://bucket/path/to/build/junit.xml",
  1771  				},
  1772  				{
  1773  					Suites: &junit.Suites{
  1774  						XMLName: xml.Name{Local: "testsuites"},
  1775  						Suites: []junit.Suite{
  1776  							{
  1777  								XMLName: xml.Name{Local: "testsuite"},
  1778  								Name:    "fun",
  1779  								Suites: []junit.Suite{
  1780  									{
  1781  										XMLName: xml.Name{Local: "testsuite"},
  1782  										Name:    "knee",
  1783  										Results: []junit.Result{
  1784  											{
  1785  												Name: "bone",
  1786  												Time: 6,
  1787  											},
  1788  										},
  1789  									},
  1790  								},
  1791  								Results: []junit.Result{
  1792  									{
  1793  										Name: "word",
  1794  										Time: 7,
  1795  									},
  1796  								},
  1797  							},
  1798  						},
  1799  					},
  1800  					Metadata: map[string]string{
  1801  						"Context":   "context",
  1802  						"Thread":    "88",
  1803  						"Timestamp": "20201122-1234",
  1804  					},
  1805  					Path: "gs://bucket/path/to/build/nested/junit_context_20201122-1234_88.xml",
  1806  				},
  1807  			},
  1808  		},
  1809  		{
  1810  			name: "list error returns error",
  1811  			data: map[string]fakeObject{
  1812  				"ignore-this": {Data: "<invalid></xml>"},
  1813  				"junit.xml":   {Data: `<testsuite><testcase name="hi"/></testsuite>`},
  1814  				"ignore-that": {Data: "<invalid></xml>"},
  1815  			},
  1816  			listIdxErr: 1,
  1817  			err:        true,
  1818  		},
  1819  		{
  1820  			name: "cancelled context returns err",
  1821  			data: map[string]fakeObject{
  1822  				"junit.xml": {Data: `<testsuite><testcase name="hi"/></testsuite>`},
  1823  			},
  1824  			ctx: func() context.Context {
  1825  				ctx, cancel := context.WithCancel(context.Background())
  1826  				cancel()
  1827  				return ctx
  1828  			}(),
  1829  			err: true,
  1830  		},
  1831  		{
  1832  			name: "suites error contains error",
  1833  			data: map[string]fakeObject{
  1834  				"junit.xml": {Data: "<invalid></xml>"},
  1835  			},
  1836  			expected: []gcs.SuitesMeta{
  1837  				{
  1838  					Metadata: map[string]string{
  1839  						"Context":   "",
  1840  						"Thread":    "",
  1841  						"Timestamp": "",
  1842  					},
  1843  					Path: "gs://bucket/path/to/build/junit.xml",
  1844  					Err:  errors.New("foo"),
  1845  				},
  1846  			},
  1847  		},
  1848  	}
  1849  
  1850  	for _, tc := range cases {
  1851  		t.Run(tc.name, func(t *testing.T) {
  1852  			if tc.ctx == nil {
  1853  				tc.ctx = context.Background()
  1854  			}
  1855  			ctx, cancel := context.WithCancel(tc.ctx)
  1856  			defer cancel()
  1857  			client := fakeClient{
  1858  				Lister: fake.Lister{},
  1859  				Opener: fake.Opener{
  1860  					Paths: map[gcs.Path]fake.Object{},
  1861  				},
  1862  			}
  1863  
  1864  			fi := fakeIterator{
  1865  				Err: tc.listIdxErr,
  1866  			}
  1867  			for name, fo := range tc.data {
  1868  				p, err := path.ResolveReference(&url.URL{Path: name})
  1869  				if err != nil {
  1870  					t.Fatalf("path.ResolveReference(%q): %v", name, err)
  1871  				}
  1872  				fi.Objects = append(fi.Objects, storage.ObjectAttrs{
  1873  					Name: p.Object(),
  1874  				})
  1875  				client.Opener.Paths[*p] = fo
  1876  			}
  1877  			client.Lister[path] = fi
  1878  
  1879  			build := gcs.Build{
  1880  				Path: path,
  1881  			}
  1882  			actual, err := readSuites(ctx, &client, build)
  1883  			sort.SliceStable(actual, func(i, j int) bool {
  1884  				return actual[i].Path < actual[j].Path
  1885  			})
  1886  			sort.SliceStable(tc.expected, func(i, j int) bool {
  1887  				return tc.expected[i].Path < tc.expected[j].Path
  1888  			})
  1889  			switch {
  1890  			case err != nil:
  1891  				if !tc.err {
  1892  					t.Errorf("readSuites(): unexpected error: %v", err)
  1893  				}
  1894  			case tc.err:
  1895  				t.Error("readSuites(): failed to receive an error")
  1896  			default:
  1897  				cmpErrs := func(x, y error) bool {
  1898  					return (x == nil) == (y == nil)
  1899  				}
  1900  				if diff := cmp.Diff(tc.expected, actual, cmp.Comparer(cmpErrs)); diff != "" {
  1901  					t.Errorf("readSuites() got unexpected diff (-want +got):\n%s", diff)
  1902  				}
  1903  			}
  1904  		})
  1905  	}
  1906  }
  1907  
  1908  func addBuilds(fc *fake.Client, path gcs.Path, s ...fakeBuild) []gcs.Build {
  1909  	if fc.Opener.Lock != nil {
  1910  		fc.Opener.Lock.Lock()
  1911  		defer fc.Opener.Lock.Unlock()
  1912  	}
  1913  	var builds []gcs.Build
  1914  	for _, build := range s {
  1915  		buildPath := resolveOrDie(&path, build.id+"/")
  1916  		builds = append(builds, gcs.Build{Path: *buildPath})
  1917  		fi := fake.Iterator{}
  1918  
  1919  		if build.podInfo != nil {
  1920  			p := resolveOrDie(buildPath, "podinfo.json")
  1921  			fi.Objects = append(fi.Objects, storage.ObjectAttrs{
  1922  				Name: p.Object(),
  1923  			})
  1924  			fc.Opener.Paths[*p] = *build.podInfo
  1925  		}
  1926  		if build.started != nil {
  1927  			p := resolveOrDie(buildPath, "started.json")
  1928  			fi.Objects = append(fi.Objects, storage.ObjectAttrs{
  1929  				Name: p.Object(),
  1930  			})
  1931  			fc.Opener.Paths[*p] = *build.started
  1932  		}
  1933  		if build.finished != nil {
  1934  			p := resolveOrDie(buildPath, "finished.json")
  1935  			fi.Objects = append(fi.Objects, storage.ObjectAttrs{
  1936  				Name: p.Object(),
  1937  			})
  1938  			fc.Opener.Paths[*p] = *build.finished
  1939  		}
  1940  		if len(build.passed)+len(build.failed) > 0 {
  1941  			p := resolveOrDie(buildPath, "junit_automatic.xml")
  1942  			fc.Opener.Paths[*p] = fake.Object{Data: makeJunit(build.passed, build.failed)}
  1943  			fi.Objects = append(fi.Objects, storage.ObjectAttrs{
  1944  				Name: p.Object(),
  1945  			})
  1946  		}
  1947  		for n, fo := range build.artifacts {
  1948  			p := resolveOrDie(buildPath, n)
  1949  			fi.Objects = append(fi.Objects, storage.ObjectAttrs{
  1950  				Name: p.Object(),
  1951  			})
  1952  			fc.Opener.Paths[*p] = fo
  1953  		}
  1954  		fc.Lister[*buildPath] = fi
  1955  	}
  1956  	return builds
  1957  
  1958  }
  1959  
  1960  type fakeBuild struct {
  1961  	id        string
  1962  	started   *fakeObject
  1963  	finished  *fakeObject
  1964  	podInfo   *fakeObject
  1965  	artifacts map[string]fakeObject
  1966  	rawJunit  *fakeObject
  1967  	passed    []string
  1968  	failed    []string
  1969  }