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  }