github.com/abayer/test-infra@v0.0.5/robots/issue-creator/testowner/owner.go (about) 1 /* 2 Copyright 2016 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 testowner 18 19 import ( 20 "encoding/csv" 21 "errors" 22 "fmt" 23 "io" 24 "math/rand" 25 "os" 26 "path/filepath" 27 "regexp" 28 "sort" 29 "strings" 30 "time" 31 32 "github.com/golang/glog" 33 ) 34 35 var tagRegex = regexp.MustCompile(`\[.*?\]|\{.*?\}`) 36 var whiteSpaceRegex = regexp.MustCompile(`\s+`) 37 38 // Turn a test name into a canonical form (without tags, lowercase, etc.) 39 func normalize(name string) string { 40 tagLess := tagRegex.ReplaceAll([]byte(name), []byte("")) 41 squeezed := whiteSpaceRegex.ReplaceAll(tagLess, []byte(" ")) 42 return strings.ToLower(strings.TrimSpace(string(squeezed))) 43 } 44 45 // OwnerInfo stores the SIG and user which have responsibility for the test. 46 type OwnerInfo struct { 47 // User assigned to this test. 48 User string 49 // SIG holding responsibility for this test. 50 SIG string 51 } 52 53 func (o OwnerInfo) String() string { 54 return "OwnerInfo{User:'" + o.User + "', SIG:'" + o.SIG + "'}" 55 } 56 57 // OwnerList uses a map to get owners for a given test name. 58 type OwnerList struct { 59 mapping map[string]*OwnerInfo 60 rng *rand.Rand 61 } 62 63 // get returns the Owner for the test with the exact name or the first blob match. Nil is returned 64 // if none are matched. 65 func (o *OwnerList) get(testName string) (owner *OwnerInfo) { 66 name := normalize(testName) 67 68 // exact mapping 69 owner, _ = o.mapping[name] 70 71 // glob matching 72 if owner == nil { 73 keys := []string{} 74 for k := range o.mapping { 75 keys = append(keys, k) 76 } 77 sort.Strings(keys) 78 for _, k := range keys { 79 if match, _ := filepath.Match(k, name); match { 80 owner = o.mapping[k] 81 return 82 } 83 } 84 } 85 return 86 } 87 88 // TestOwner returns the owner for a test or the empty string if none is found. 89 func (o *OwnerList) TestOwner(testName string) (owner string) { 90 ownerInfo := o.get(testName) 91 if ownerInfo != nil { 92 owner = ownerInfo.User 93 } 94 95 if strings.Contains(owner, "/") { 96 ownerSet := strings.Split(owner, "/") 97 owner = ownerSet[o.rng.Intn(len(ownerSet))] 98 } 99 return strings.TrimSpace(owner) 100 } 101 102 // TestSIG returns the SIG assigned to a test, or else the empty string if none is found. 103 func (o *OwnerList) TestSIG(testName string) string { 104 ownerInfo := o.get(testName) 105 if ownerInfo == nil { 106 return "" 107 } 108 return strings.TrimSpace(ownerInfo.SIG) 109 } 110 111 // NewOwnerList constructs an OwnerList given a mapping from test names to test owners. 112 func NewOwnerList(mapping map[string]*OwnerInfo) *OwnerList { 113 list := OwnerList{} 114 list.rng = rand.New(rand.NewSource(time.Now().UnixNano())) 115 list.mapping = make(map[string]*OwnerInfo) 116 for input, output := range mapping { 117 list.mapping[normalize(input)] = output 118 } 119 return &list 120 } 121 122 // NewOwnerListFromCsv constructs an OwnerList given a CSV file that includes 123 // 'owner' and 'test name' columns. 124 func NewOwnerListFromCsv(r io.Reader) (*OwnerList, error) { 125 reader := csv.NewReader(r) 126 records, err := reader.ReadAll() 127 if err != nil { 128 return nil, err 129 } 130 mapping := make(map[string]*OwnerInfo) 131 ownerCol := -1 132 nameCol := -1 133 sigCol := -1 134 for _, record := range records { 135 if ownerCol == -1 || nameCol == -1 || sigCol == -1 { 136 for col, val := range record { 137 switch strings.ToLower(val) { 138 case "owner": 139 ownerCol = col 140 case "name": 141 nameCol = col 142 case "sig": 143 sigCol = col 144 } 145 146 } 147 } else { 148 mapping[record[nameCol]] = &OwnerInfo{ 149 User: record[ownerCol], 150 SIG: record[sigCol], 151 } 152 } 153 } 154 if len(mapping) == 0 { 155 return nil, errors.New("no mappings found in test owners CSV") 156 } 157 return NewOwnerList(mapping), nil 158 } 159 160 // ReloadingOwnerList maps test names to owners, reloading the mapping when the 161 // underlying file is changed. 162 type ReloadingOwnerList struct { 163 path string 164 mtime time.Time 165 ownerList *OwnerList 166 } 167 168 // NewReloadingOwnerList creates a ReloadingOwnerList given a path to a CSV 169 // file containing owner mapping information. 170 func NewReloadingOwnerList(path string) (*ReloadingOwnerList, error) { 171 ownerList := &ReloadingOwnerList{path: path} 172 err := ownerList.reload() 173 if err != nil { 174 if _, ok := err.(badCsv); !ok { 175 return nil, err // Error is not a bad csv file 176 } 177 glog.Errorf("Unable to load test owners at %s: %v", path, err) 178 ownerList.ownerList = NewOwnerList(nil) 179 } 180 return ownerList, err // err != nil if badCsv (but can recover) 181 } 182 183 // TestOwner returns the owner for a test, or the empty string if none is found. 184 func (o *ReloadingOwnerList) TestOwner(testName string) string { 185 err := o.reload() 186 if err != nil { 187 glog.Errorf("Unable to reload test owners at %s: %v", o.path, err) 188 // Process using the previous data. 189 } 190 return o.ownerList.TestOwner(testName) 191 } 192 193 // TestSIG returns the SIG for a test, or the empty string if none is found. 194 func (o *ReloadingOwnerList) TestSIG(testName string) string { 195 err := o.reload() 196 if err != nil { 197 glog.Errorf("Unable to reload test owners at %s: %v", o.path, err) 198 // Process using the previous data. 199 } 200 return o.ownerList.TestSIG(testName) 201 } 202 203 type badCsv string 204 205 func (b badCsv) Error() string { 206 return string(b) 207 } 208 209 func (o *ReloadingOwnerList) reload() error { 210 info, err := os.Stat(o.path) 211 if err != nil { 212 return err 213 } 214 if info.ModTime() == o.mtime { 215 return nil 216 } 217 file, err := os.Open(o.path) 218 if err != nil { 219 return err 220 } 221 defer file.Close() 222 ownerList, err := NewOwnerListFromCsv(file) 223 if err != nil { 224 return badCsv(fmt.Sprintf("could not parse owner list: %v", err)) 225 } 226 o.ownerList = ownerList 227 o.mtime = info.ModTime() 228 return nil 229 }