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