github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/model/project_selector.go (about) 1 package model 2 3 import ( 4 "bytes" 5 "strings" 6 7 "github.com/evergreen-ci/evergreen/util" 8 "github.com/pkg/errors" 9 ) 10 11 // Selectors are used in a project file to select groups of tasks/axes based on user-defined tags. 12 // Selection syntax is currently defined as a whitespace-delimited set of criteria, where each 13 // criterion is a different name or tag with optional modifiers. 14 // Formally, we define the syntax as: 15 // Selector := [whitespace-delimited list of Criterion] 16 // Criterion := (optional ! rune)(optional . rune)<Name> 17 // where "!" specifies a negation of the criteria and "." specifies a tag as opposed to a name 18 // Name := <any string> 19 // excluding whitespace, '.', and '!' 20 // 21 // Selectors return all items that satisfy all of the criteria. That is, they return the intersection 22 // of each individual criterion. 23 // 24 // For example: 25 // "red" would return the item named "red" 26 // ".primary" would return all items with the tag "primary" 27 // "!.primary" would return all items that are NOT tagged "primary" 28 // ".cool !blue" would return all items that are tagged "cool" and NOT named "blue" 29 30 const ( 31 SelectAll = "*" 32 InvalidCriterionRunes = "!." 33 ) 34 35 // Selector holds the information necessary to build a set of elements 36 // based on name and tag combinations. 37 type Selector []selectCriterion 38 39 // String returns a readable representation of the Selector. 40 func (s Selector) String() string { 41 buf := bytes.Buffer{} 42 for i, sc := range s { 43 if i > 0 { 44 buf.WriteRune(' ') 45 } 46 buf.WriteString(sc.String()) 47 } 48 return buf.String() 49 } 50 51 // selectCriterions are intersected to form the results of a selector. 52 type selectCriterion struct { 53 name string 54 55 // modifiers 56 tagged bool 57 negated bool 58 } 59 60 // String returns a readable representation of the criterion. 61 func (sc selectCriterion) String() string { 62 buf := bytes.Buffer{} 63 if sc.negated { 64 buf.WriteRune('!') 65 } 66 if sc.tagged { 67 buf.WriteRune('.') 68 } 69 buf.WriteString(sc.name) 70 return buf.String() 71 } 72 73 // Validate returns nil if the selectCriterion is valid, 74 // or an error describing why it is invalid. 75 func (sc selectCriterion) Validate() error { 76 if sc.name == "" { 77 return errors.New("name is empty") 78 } 79 if i := strings.IndexAny(sc.name, InvalidCriterionRunes); i == 0 { 80 return errors.Errorf("name starts with invalid character '%v'", sc.name[i]) 81 } 82 if sc.name == SelectAll { 83 if sc.tagged { 84 return errors.Errorf("cannot use '.' with special name '%v'", SelectAll) 85 } 86 if sc.negated { 87 return errors.Errorf("cannot use '!' with special name '%v'", SelectAll) 88 } 89 } 90 return nil 91 } 92 93 // ParseSelector reads in a set of selection criteria defined as a string. 94 // This function only parses; it does not evaluate. 95 // Returns nil on an empty selection string. 96 func ParseSelector(s string) Selector { 97 var criteria []selectCriterion 98 // read the white-space delimited criteria 99 critStrings := strings.Fields(s) 100 for _, c := range critStrings { 101 criteria = append(criteria, stringToCriterion(c)) 102 } 103 return criteria 104 } 105 106 // stringToCriterion parses out a single criterion. 107 // This helper assumes that s != "". 108 func stringToCriterion(s string) selectCriterion { 109 sc := selectCriterion{} 110 if len(s) > 0 && s[0] == '!' { // negation 111 sc.negated = true 112 s = s[1:] 113 } 114 if len(s) > 0 && s[0] == '.' { // tags 115 sc.tagged = true 116 s = s[1:] 117 } 118 sc.name = s 119 return sc 120 } 121 122 // the tagged interface allows the tagSelectorEvaluator to work for multiple types 123 type tagged interface { 124 name() string 125 tags() []string 126 } 127 128 // tagSelectorEvaluator evaluates selectors for arbitrary tagged items 129 type tagSelectorEvaluator struct { 130 items []tagged 131 byName map[string]tagged 132 byTag map[string][]tagged 133 } 134 135 // newTagSelectorEvaluator returns a new taskSelectorEvaluator. 136 func newTagSelectorEvaluator(selectees []tagged) *tagSelectorEvaluator { 137 // cache everything 138 byName := map[string]tagged{} 139 byTag := map[string][]tagged{} 140 items := []tagged{} 141 for _, s := range selectees { 142 items = append(items, s) 143 byName[s.name()] = s 144 for _, tag := range s.tags() { 145 byTag[tag] = append(byTag[tag], s) 146 } 147 } 148 return &tagSelectorEvaluator{ 149 items: items, 150 byName: byName, 151 byTag: byTag, 152 } 153 } 154 155 // evalSelector returns all names that fulfill a selector. This is done 156 // by evaluating each criterion individually and taking the intersection. 157 func (tse *tagSelectorEvaluator) evalSelector(s Selector) ([]string, error) { 158 // keep a slice of results per criterion 159 results := []string{} 160 if len(s) == 0 { 161 return nil, errors.New("cannot evaluate selector with no criteria") 162 } 163 for i, sc := range s { 164 names, err := tse.evalCriterion(sc) 165 if err != nil { 166 return nil, errors.Wrapf(err, "%v", s) 167 } 168 if i == 0 { 169 results = names 170 } else { 171 // intersect all evaluated criteria 172 results = util.StringSliceIntersection(results, names) 173 } 174 } 175 if len(results) == 0 { 176 return nil, errors.Errorf("nothing satisfies selector '%v'", s) 177 } 178 return results, nil 179 } 180 181 // evalCriterion returns all names that fulfill a single selection criterion. 182 func (tse *tagSelectorEvaluator) evalCriterion(sc selectCriterion) ([]string, error) { 183 switch { 184 case sc.Validate() != nil: 185 return nil, errors.Errorf("criterion '%v' is invalid: %v", sc, sc.Validate()) 186 187 case sc.name == SelectAll: // special * case 188 names := []string{} 189 for _, item := range tse.items { 190 names = append(names, item.name()) 191 } 192 return names, nil 193 194 case !sc.tagged && !sc.negated: // just a regular name 195 item := tse.byName[sc.name] 196 if item == nil { 197 return nil, errors.Errorf("nothing named '%v'", sc.name) 198 } 199 return []string{item.name()}, nil 200 201 case sc.tagged && !sc.negated: // expand a tag 202 taggedItems := tse.byTag[sc.name] 203 if len(taggedItems) == 0 { 204 return nil, errors.Errorf("nothing has the tag '%v'", sc.name) 205 } 206 names := []string{} 207 for _, item := range taggedItems { 208 names = append(names, item.name()) 209 } 210 return names, nil 211 212 case !sc.tagged && sc.negated: // everything *but* a specific item 213 if tse.byName[sc.name] == nil { 214 // we want to treat this as an error for better usability 215 return nil, errors.Errorf("nothing named '%v'", sc.name) 216 } 217 names := []string{} 218 for _, item := range tse.items { 219 if item.name() != sc.name { 220 names = append(names, item.name()) 221 } 222 } 223 return names, nil 224 225 case sc.tagged && sc.negated: // everything *but* a tag 226 items := tse.byTag[sc.name] 227 if len(items) == 0 { 228 // we want to treat this as an error for better usability 229 return nil, errors.Errorf("nothing has the tag '%v'", sc.name) 230 } 231 illegalItems := map[string]bool{} 232 for _, item := range items { 233 illegalItems[item.name()] = true 234 } 235 names := []string{} 236 // build slice of all items that aren't in the tag 237 for _, item := range tse.items { 238 if !illegalItems[item.name()] { 239 names = append(names, item.name()) 240 } 241 } 242 return names, nil 243 244 default: 245 // protection for if we edit this switch block later 246 panic("this should not be reachable") 247 } 248 } 249 250 // Task Selector Logic 251 252 // taskSelectorEvaluator expands tags used in build variant definitions. 253 type taskSelectorEvaluator struct { 254 tagEval *tagSelectorEvaluator 255 } 256 257 // NewParserTaskSelectorEvaluator returns a new taskSelectorEvaluator. 258 func NewParserTaskSelectorEvaluator(tasks []parserTask) *taskSelectorEvaluator { 259 // convert tasks into interface slice and use the tagSelectorEvaluator 260 var selectees []tagged 261 for i := range tasks { 262 selectees = append(selectees, &tasks[i]) 263 } 264 return &taskSelectorEvaluator{ 265 tagEval: newTagSelectorEvaluator(selectees), 266 } 267 } 268 269 // evalSelector returns all tasks selected by a selector. 270 func (t *taskSelectorEvaluator) evalSelector(s Selector) ([]string, error) { 271 results, err := t.tagEval.evalSelector(s) 272 if err != nil { 273 return nil, errors.Wrap(err, "evaluating task selector") 274 } 275 return results, nil 276 } 277 278 // Axis selector logic 279 280 // axisSelectorEvaluator expands tags used for selected matrix axis values 281 type axisSelectorEvaluator struct { 282 axisEvals map[string]*tagSelectorEvaluator 283 } 284 285 func NewAxisSelectorEvaluator(axes []matrixAxis) *axisSelectorEvaluator { 286 evals := map[string]*tagSelectorEvaluator{} 287 // convert axis values into interface slices and use the tagSelectorEvaluator 288 for i := range axes { 289 var selectees []tagged 290 for j := range axes[i].Values { 291 selectees = append(selectees, &(axes[i].Values[j])) 292 } 293 evals[axes[i].Id] = newTagSelectorEvaluator(selectees) 294 } 295 return &axisSelectorEvaluator{ 296 axisEvals: evals, 297 } 298 } 299 300 // evalSelector returns all variants selected by the selector. 301 func (ase *axisSelectorEvaluator) evalSelector(axis string, s Selector) ([]string, error) { 302 tagEval, ok := ase.axisEvals[axis] 303 if !ok { 304 return nil, errors.Errorf("axis '%v' does not exist", axis) 305 } 306 results, err := tagEval.evalSelector(s) 307 if err != nil { 308 return nil, errors.Wrapf(err, "evaluating axis '%v' selector", axis) 309 } 310 return results, nil 311 } 312 313 // Variant selector logic 314 315 // variantSelectorEvaluator expands tags used in build variant definitions. 316 type variantSelectorEvaluator struct { 317 tagEval *tagSelectorEvaluator 318 axisEval *axisSelectorEvaluator 319 variants []parserBV 320 } 321 322 // NewVariantSelectorEvaluator returns a new taskSelectorEvaluator. 323 func NewVariantSelectorEvaluator(variants []parserBV, ase *axisSelectorEvaluator) *variantSelectorEvaluator { 324 // convert variants into interface slice and use the tagSelectorEvaluator 325 var selectees []tagged 326 for i := range variants { 327 selectees = append(selectees, &variants[i]) 328 } 329 return &variantSelectorEvaluator{ 330 tagEval: newTagSelectorEvaluator(selectees), 331 variants: variants, 332 axisEval: ase, 333 } 334 } 335 336 // evalSelector returns all variants selected by the selector. 337 func (v *variantSelectorEvaluator) evalSelector(vs *variantSelector) ([]string, error) { 338 if vs == nil { 339 return nil, errors.New("empty selector") 340 } 341 if vs.matrixSelector != nil { 342 evaluatedSelector, errs := vs.matrixSelector.evaluatedCopy(v.axisEval) 343 if len(errs) > 0 { 344 return nil, errors.Errorf( 345 "errors evaluating variant selector %v: %v", vs.matrixSelector, errs) 346 } 347 results := []string{} 348 // this could be sped up considerably with caching, but I doubt we'll need to 349 for _, v := range v.variants { 350 if v.matrixVal != nil && evaluatedSelector.contains(v.matrixVal) { 351 results = append(results, v.Name) 352 } 353 } 354 if len(results) == 0 { 355 return nil, errors.Errorf("variant selector %v returns no variants", vs.matrixSelector) 356 } 357 return results, nil 358 } 359 results, err := v.tagEval.evalSelector(ParseSelector(vs.stringSelector)) 360 if err != nil { 361 return nil, errors.Wrap(err, "variant tag selector") 362 } 363 return results, nil 364 }