github.com/abayer/test-infra@v0.0.5/robots/issue-creator/sources/triage-filer_test.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package sources
    18  
    19  import (
    20  	"bytes"
    21  	"strconv"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  
    26  	"github.com/google/go-github/github"
    27  	"k8s.io/test-infra/robots/issue-creator/creator"
    28  	"k8s.io/test-infra/robots/issue-creator/testowner"
    29  )
    30  
    31  var (
    32  	// json1issue2job2test is a small example of the JSON format that loadClusters reads.
    33  	// It includes all of the different types of formatting that is accepted. Namely both types
    34  	// of buildnum to row index mappings.
    35  	json1issue2job2test []byte
    36  	// buildTimes is a map containing the build times of builds found in the json1issue2job2test JSON data.
    37  	buildTimes map[int]int64
    38  	// sampleOwnerCSV is a small sample test owners csv file that contains both real test owner
    39  	// data and owner/SIG info for a fake test in json1issue2job2test.
    40  	sampleOwnerCSV []byte
    41  	// latestBuildTime is the end time of the sliding window for these tests.
    42  	latestBuildTime int64
    43  )
    44  
    45  func init() {
    46  	latestBuildTime = int64(947462400) // Jan 10, 2000
    47  	hourSecs := int64(60 * 60)
    48  	dailySecs := hourSecs * 24
    49  	buildTimes = map[int]int64{
    50  		41:  latestBuildTime - (dailySecs * 10),           // before window start
    51  		42:  latestBuildTime + hourSecs - (dailySecs * 5), // just inside window start
    52  		43:  latestBuildTime + hourSecs - (dailySecs * 4),
    53  		52:  latestBuildTime + hourSecs - (dailySecs * 2),
    54  		142: latestBuildTime - dailySecs, // a day before window end
    55  		144: latestBuildTime - hourSecs,  // an hour before window end
    56  	}
    57  
    58  	json1issue2job2test = []byte(
    59  		`{
    60  		"builds":
    61  		{
    62  			"cols":
    63  			{
    64  				"started":
    65  				[
    66  					` + strconv.FormatInt(buildTimes[41], 10) + `,
    67  					` + strconv.FormatInt(buildTimes[42], 10) + `,
    68  					` + strconv.FormatInt(buildTimes[43], 10) + `,
    69  					10000000,
    70  					10000000,
    71  					10000000,
    72  					10000000,
    73  					10000000,
    74  					10000000,
    75  					10000000,
    76  					10000000,
    77  					` + strconv.FormatInt(buildTimes[52], 10) + `,
    78  					` + strconv.FormatInt(buildTimes[142], 10) + `,
    79  					10000000,
    80  					` + strconv.FormatInt(buildTimes[144], 10) + `
    81  				]
    82  			},
    83  			"jobs":
    84  			{
    85  				"jobname1": [41, 12, 0],
    86  				"jobname2": {"142": 12, "144": 14},
    87  				"pr:jobname3": {"200": 13}
    88  			},
    89  			"job_paths":
    90  			{
    91  				"jobname1": "path//to/jobname1",
    92  				"jobname2": "path//to/jobname2",
    93  				"pr:jobname3": "path//to/pr:jobname3"
    94  			}
    95  		},
    96  		"clustered":
    97  		[
    98  			{
    99  				"id": "key_hash",
   100  				"key": "key_text",
   101  				"tests": 
   102  				[
   103  					{
   104  						"jobs":
   105  						[
   106  							{
   107  								"builds": [42, 43, 52],
   108  								"name": "jobname1"
   109  							},
   110  							{
   111  								"builds": [144],
   112  								"name": "jobname2"
   113  							}
   114  						],
   115  						"name": "testname1"
   116  					},
   117  					{
   118  						"jobs":
   119  						[
   120  							{
   121  								"builds": [41, 42, 43],
   122  								"name": "jobname1"
   123  							},
   124  							{
   125  								"builds": [200],
   126  								"name": "pr:jobname3"
   127  							}
   128  						],
   129  						"name": "testname2"
   130  					}
   131  				],
   132  				"text":	"issue_name"
   133  			}
   134  		]
   135  	}`)
   136  
   137  	sampleOwnerCSV = []byte(
   138  		`name,owner,auto-assigned,sig
   139  Sysctls should support sysctls,Random-Liu,1,node
   140  Sysctls should support unsafe sysctls which are actually whitelisted,deads2k,1,node
   141  testname1,cjwagner ,1,sigarea
   142  testname2,spxtr,1,sigarea
   143  ThirdParty resources Simple Third Party creating/deleting thirdparty objects works,luxas,1,api-machinery
   144  Upgrade cluster upgrade should maintain a functioning cluster,luxas,1,cluster-lifecycle
   145  Upgrade master upgrade should maintain a functioning cluster,xiang90,1,cluster-lifecycle
   146  Upgrade node upgrade should maintain a functioning cluster,zmerlynn,1,cluster-lifecycle
   147  Variable Expansion should allow composing env vars into new env vars,derekwaynecarr,0,node
   148  Variable Expansion should allow substituting values in a container's args,dchen1107,1,node
   149  Variable Expansion should allow substituting values in a container's command,mml,1,node
   150  Volume Disk Format verify disk format type - eagerzeroedthick is honored for dynamically provisioned pv using storageclass,piosz,1,`)
   151  }
   152  
   153  // NewTestTriageFiler creates a new TriageFiler that isn't connected to an IssueCreator so that
   154  // it can be used for testing.
   155  func NewTestTriageFiler() *TriageFiler {
   156  	return &TriageFiler{
   157  		creator:          &creator.IssueCreator{},
   158  		topClustersCount: 3,
   159  		windowDays:       5,
   160  	}
   161  }
   162  
   163  func TestTFParserSimple(t *testing.T) {
   164  	f := NewTestTriageFiler()
   165  	issues, err := f.loadClusters(json1issue2job2test)
   166  	if err != nil {
   167  		t.Fatalf("Error parsing triage data: %v\n", err)
   168  	}
   169  
   170  	if len(issues) != 1 {
   171  		t.Error("Expected 1 issue, got ", len(issues))
   172  	}
   173  	if issues[0].Text != "issue_name" {
   174  		t.Error("Expected Text='issue_name', got ", issues[0].Text)
   175  	}
   176  	if issues[0].Identifier != "key_hash" {
   177  		t.Error("Expected Identifier='key_hash', got ", issues[0].Identifier)
   178  	}
   179  	// Note that 5 builds failed in json, but one is outside the time window.
   180  	if issues[0].totalBuilds != 4 {
   181  		t.Error("Expected totalBuilds failed = 4, got ", issues[0].totalBuilds)
   182  	}
   183  	// Note that 3 jobs failed in json, but one is a PR job and should be ignored.
   184  	if issues[0].totalJobs != 2 || len(issues[0].jobs) != 2 {
   185  		t.Error("Expected totalJobs failed = 2, got ", issues[0].totalJobs)
   186  	}
   187  	if issues[0].totalTests != 2 || len(issues[0].Tests) != 2 {
   188  		t.Error("Expected totalTests failed = 2, got ", issues[0].totalTests)
   189  	}
   190  	if f.data.Builds.JobPaths["jobname1"] != "path//to/jobname1" ||
   191  		f.data.Builds.JobPaths["jobname2"] != "path//to/jobname2" {
   192  		t.Error("Invalid jobpath. got jobname1: ", f.data.Builds.JobPaths["jobname1"],
   193  			" and jobname2: ", f.data.Builds.JobPaths["jobname2"])
   194  	}
   195  
   196  	checkBuildStart(t, f, "jobname1", 42, buildTimes[42])
   197  	checkBuildStart(t, f, "jobname1", 52, buildTimes[52])
   198  	checkBuildStart(t, f, "jobname2", 144, buildTimes[144])
   199  
   200  	checkCluster(issues[0], t)
   201  }
   202  
   203  func checkBuildStart(t *testing.T, f *TriageFiler, jobName string, build int, expected int64) {
   204  	row, err := f.data.Builds.Jobs[jobName].rowForBuild(build)
   205  	if err != nil {
   206  		t.Errorf("Failed to look up row index for %s:%d", jobName, build)
   207  	}
   208  	actual := f.data.Builds.Cols.Started[row]
   209  	if actual != expected {
   210  		t.Errorf("Expected build start time for build %s:%d to be %d, got %d.", jobName, build, expected, actual)
   211  	}
   212  }
   213  
   214  // checkCluster checks that the properties that should be true for all clusters hold for this cluster
   215  func checkCluster(clust *Cluster, t *testing.T) {
   216  	if !checkTopFailingsSorted(clust) {
   217  		t.Errorf("Top tests or jobs is improperly sorted for cluster: %s\n", clust.Identifier)
   218  	}
   219  	if clust.totalJobs != len(clust.jobs) {
   220  		t.Errorf("Total job count is invalid for cluster: %s\n", clust.Identifier)
   221  	}
   222  	if clust.totalTests != len(clust.Tests) {
   223  		t.Errorf("Total test count is invalid for cluster: %s\n", clust.Identifier)
   224  	}
   225  	title := clust.Title()
   226  	body := clust.Body(nil)
   227  	id := clust.ID()
   228  	if len(title) <= 0 {
   229  		t.Errorf("Title of cluster: %s is empty!", clust.Identifier)
   230  	}
   231  	if len(body) <= 0 {
   232  		t.Errorf("Body of cluster: %s is empty!", clust.Identifier)
   233  	}
   234  	if len(id) <= 0 {
   235  		t.Errorf("ID of cluster: %s is empty!", clust.Identifier)
   236  	}
   237  	if !strings.Contains(body, id) {
   238  		t.Errorf("The body text for cluster: %s does not contain its ID!\n", clust.Identifier)
   239  	}
   240  	//ensure that 'kind/flake' is among the label set
   241  	found := false
   242  	for _, label := range clust.Labels() {
   243  		if label == "kind/flake" {
   244  			found = true
   245  		} else {
   246  			if label == "" {
   247  				t.Errorf("Cluster: %s has an empty label!\n", clust.Identifier)
   248  			}
   249  		}
   250  	}
   251  	if !found {
   252  		t.Errorf("The cluster: %s does not have the label 'kind/flake'!", clust.Identifier)
   253  	}
   254  }
   255  
   256  func TestTFOwnersAndSIGs(t *testing.T) {
   257  	// Integration test for triage-filers use of issue-creator's TestsOwners, TestsSIGs, and
   258  	// ExplainTestAssignments. These functions in turn rely on OwnerList.
   259  	f := NewTestTriageFiler()
   260  	var err error
   261  	f.creator.Collaborators = []string{"cjwagner", "spxtr"}
   262  	f.creator.Owners, err = testowner.NewOwnerListFromCsv(bytes.NewReader(sampleOwnerCSV))
   263  	f.creator.MaxSIGCount = 3
   264  	f.creator.MaxAssignees = 3
   265  	if err != nil {
   266  		t.Fatalf("Failed to create a new OwnersList.  errmsg: %v", err)
   267  	}
   268  
   269  	// Check that the usernames and sig areas are as expected (no stay commas or anything like that).
   270  	clusters, err := f.loadClusters(json1issue2job2test)
   271  	if err != nil {
   272  		t.Fatalf("Failed to load clusters: %v", err)
   273  	}
   274  	foundSIG := false
   275  	for _, label := range clusters[0].Labels() {
   276  		if label == "sig/sigarea" {
   277  			foundSIG = true
   278  			break
   279  		}
   280  	}
   281  	if !foundSIG {
   282  		t.Errorf("Failed to get the SIG for cluster: %s\n", clusters[0].Identifier)
   283  	}
   284  
   285  	// Check that the body contains a table that correctly explains why users and sig areas were assigned.
   286  	body := clusters[0].Body(nil)
   287  	if !strings.Contains(body, "| cjwagner | testname1 |") {
   288  		t.Errorf("Body should contain a table row to explain that 'cjwagner' was assigned due to ownership of 'testname1'.")
   289  	}
   290  	if !strings.Contains(body, "| spxtr | testname2 |") {
   291  		t.Errorf("Body should contain a table row to explain that 'spxtr' was assigned due to ownership of 'testname2'.")
   292  	}
   293  	if !strings.Contains(body, "| sig/sigarea | testname1; testname2 |") {
   294  		t.Errorf("Body should contain a table row to explain that 'sigarea' was set as a SIG due to ownership of 'testname1' and 'testname2'.")
   295  	}
   296  
   297  	// Check that the body contains the assignments themselves:
   298  	if !strings.Contains(body, "/assign @cjwagner @spxtr") && !strings.Contains(body, "/assign @spxtr @cjwagner") {
   299  		t.Errorf("Failed to find the '/assign' command in the body of cluster: %s\n%q\n", clusters[0].Identifier, body)
   300  	}
   301  }
   302  
   303  // TestTFPrevCloseInWindow checks that Cluster issues will abort issue creation by returning an empty
   304  // body if there is a recently closed issue for the cluster.
   305  func TestTFPrevCloseInWindow(t *testing.T) {
   306  	f := NewTestTriageFiler()
   307  	clusters, err := f.loadClusters(json1issue2job2test)
   308  	if err != nil || len(clusters) == 0 {
   309  		t.Fatalf("Error parsing triage data: %v\n", err)
   310  	}
   311  	clust := clusters[0]
   312  
   313  	lastWeek := time.Unix(latestBuildTime, 0).AddDate(0, 0, -7)
   314  	yesterday := time.Unix(latestBuildTime, 0).AddDate(0, 0, -1)
   315  	five := 5
   316  	// Only need to populate the Issue.ClosedAt and Issue.Number field of the MungeObject.
   317  	prevIssues := []*github.Issue{{ClosedAt: &yesterday, Number: &five}}
   318  	if clust.Body(prevIssues) != "" {
   319  		t.Errorf("Cluster returned an issue body when there was a recently closed issue for the cluster.")
   320  	}
   321  
   322  	prevIssues = []*github.Issue{{ClosedAt: &lastWeek, Number: &five}}
   323  	if clust.Body(prevIssues) == "" {
   324  		t.Errorf("Cluster returned an empty issue body when it should have returned a valid body.")
   325  	}
   326  }
   327  
   328  func checkTopFailingsSorted(issue *Cluster) bool {
   329  	return checkTopJobsFailedSorted(issue) && checkTopTestsFailedSorted(issue)
   330  }
   331  
   332  func checkTopJobsFailedSorted(issue *Cluster) bool {
   333  	topJobs := issue.topJobsFailed(len(issue.jobs))
   334  	for i := 1; i < len(topJobs); i++ {
   335  		if len(topJobs[i-1].Builds) < len(topJobs[i].Builds) {
   336  			return false
   337  		}
   338  	}
   339  	return true
   340  }
   341  
   342  func checkTopTestsFailedSorted(issue *Cluster) bool {
   343  	topTests := issue.topTestsFailed(len(issue.Tests))
   344  	for i := 1; i < len(topTests); i++ {
   345  		if len(topTests[i-1].Jobs) < len(topTests[i].Jobs) {
   346  			return false
   347  		}
   348  	}
   349  	return true
   350  }