github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/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  }