xref: /aosp_15_r20/external/skia/infra/bots/gen_tasks_logic/schema.go (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1// Copyright 2020 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4package gen_tasks_logic
5
6/*
7   This file contains logic related to task/job name schemas.
8*/
9
10import (
11	"encoding/json"
12	"fmt"
13	"log"
14	"os"
15	"regexp"
16	"strings"
17)
18
19// parts represents the key/value pairs which make up task and job names.
20type parts map[string]string
21
22// equal returns true if the given part of this job's name equals any of the
23// given values. Panics if no values are provided.
24func (p parts) equal(part string, eq ...string) bool {
25	if len(eq) == 0 {
26		log.Fatal("No values provided for equal!")
27	}
28	v := p[part]
29	for _, e := range eq {
30		if v == e {
31			return true
32		}
33	}
34	return false
35}
36
37// role returns true if the role for this job equals any of the given values.
38func (p parts) role(eq ...string) bool {
39	return p.equal("role", eq...)
40}
41
42// os returns true if the OS for this job equals any of the given values.
43func (p parts) os(eq ...string) bool {
44	return p.equal("os", eq...)
45}
46
47// compiler returns true if the compiler for this job equals any of the given
48// values.
49func (p parts) compiler(eq ...string) bool {
50	return p.equal("compiler", eq...)
51}
52
53// model returns true if the model for this job equals any of the given values.
54func (p parts) model(eq ...string) bool {
55	return p.equal("model", eq...)
56}
57
58// frequency returns true if the frequency for this job equals any of the given
59// values.
60func (p parts) frequency(eq ...string) bool {
61	return p.equal("frequency", eq...)
62}
63
64// cpu returns true if the task's cpu_or_gpu is "CPU" and the CPU for this
65// task equals any of the given values. If no values are provided, cpu returns
66// true if this task runs on CPU.
67func (p parts) cpu(eq ...string) bool {
68	if p["cpu_or_gpu"] == "CPU" {
69		if len(eq) == 0 {
70			return true
71		}
72		return p.equal("cpu_or_gpu_value", eq...)
73	}
74	return false
75}
76
77// gpu returns true if the task's cpu_or_gpu is "GPU" and the GPU for this task
78// equals any of the given values. If no values are provided, gpu returns true
79// if this task runs on GPU.
80func (p parts) gpu(eq ...string) bool {
81	if p["cpu_or_gpu"] == "GPU" {
82		if len(eq) == 0 {
83			return true
84		}
85		return p.equal("cpu_or_gpu_value", eq...)
86	}
87	return false
88}
89
90// arch returns true if the architecture for this job equals any of the
91// given values.
92func (p parts) arch(eq ...string) bool {
93	return p.equal("arch", eq...) || p.equal("target_arch", eq...)
94}
95
96// extraConfig returns true if any of the extra_configs for this job equals
97// any of the given values. If the extra_config starts with "SK_",
98// it is considered to be a single config.
99func (p parts) extraConfig(eq ...string) bool {
100	if len(eq) == 0 {
101		log.Fatal("No values provided for extraConfig()!")
102	}
103	ec := p["extra_config"]
104	if ec == "" {
105		return false
106	}
107	var cfgs []string
108	if strings.HasPrefix(ec, "SK_") {
109		cfgs = []string{ec}
110	} else {
111		cfgs = strings.Split(ec, "_")
112	}
113	for _, c := range cfgs {
114		for _, e := range eq {
115			if c == e {
116				return true
117			}
118		}
119	}
120	return false
121}
122
123// noExtraConfig returns true if there are no extra_configs for this job.
124func (p parts) noExtraConfig(eq ...string) bool {
125	ec := p["extra_config"]
126	return ec == ""
127}
128
129// matchPart returns true if the given part of this job's name matches any of
130// the given regular expressions. Note that a regular expression might match any
131// substring, so if you need an exact match on the entire string you'll need to
132// use `^` and `$`. Panics if no regular expressions are provided.
133func (p parts) matchPart(part string, re ...string) bool {
134	if len(re) == 0 {
135		log.Fatal("No regular expressions provided for matchPart()!")
136	}
137	v := p[part]
138	for _, r := range re {
139		if regexp.MustCompile(r).MatchString(v) {
140			return true
141		}
142	}
143	return false
144}
145
146// matchRole returns true if the role for this job matches any of the given
147// regular expressions.
148func (p parts) matchRole(re ...string) bool {
149	return p.matchPart("role", re...)
150}
151
152func (p parts) project(re ...string) bool {
153	return p.matchPart("project", re...)
154}
155
156// matchOs returns true if the OS for this job matches any of the given regular
157// expressions.
158func (p parts) matchOs(re ...string) bool {
159	return p.matchPart("os", re...)
160}
161
162// matchCompiler returns true if the compiler for this job matches any of the
163// given regular expressions.
164func (p parts) matchCompiler(re ...string) bool {
165	return p.matchPart("compiler", re...)
166}
167
168// matchModel returns true if the model for this job matches any of the given
169// regular expressions.
170func (p parts) matchModel(re ...string) bool {
171	return p.matchPart("model", re...)
172}
173
174// matchCpu returns true if the task's cpu_or_gpu is "CPU" and the CPU for this
175// task matches any of the given regular expressions. If no regular expressions
176// are provided, cpu returns true if this task runs on CPU.
177func (p parts) matchCpu(re ...string) bool {
178	if p["cpu_or_gpu"] == "CPU" {
179		if len(re) == 0 {
180			return true
181		}
182		return p.matchPart("cpu_or_gpu_value", re...)
183	}
184	return false
185}
186
187// matchGpu returns true if the task's cpu_or_gpu is "GPU" and the GPU for this task
188// matches any of the given regular expressions. If no regular expressions are
189// provided, gpu returns true if this task runs on GPU.
190func (p parts) matchGpu(re ...string) bool {
191	if p["cpu_or_gpu"] == "GPU" {
192		if len(re) == 0 {
193			return true
194		}
195		return p.matchPart("cpu_or_gpu_value", re...)
196	}
197	return false
198}
199
200// matchArch returns true if the architecture for this job matches any of the
201// given regular expressions.
202func (p parts) matchArch(re ...string) bool {
203	return p.matchPart("arch", re...) || p.matchPart("target_arch", re...)
204}
205
206// matchBazelHost returns true if the Bazel host for this job matches any of the
207// given regular expressions.
208func (p parts) matchBazelHost(re ...string) bool {
209	return p.matchPart("host", re...)
210}
211
212// matchExtraConfig returns true if any of the extra_configs for this job matches
213// any of the given regular expressions. If the extra_config starts with "SK_",
214// it is considered to be a single config.
215func (p parts) matchExtraConfig(re ...string) bool {
216	if len(re) == 0 {
217		log.Fatal("No regular expressions provided for matchExtraConfig()!")
218	}
219	ec := p["extra_config"]
220	if ec == "" {
221		return false
222	}
223	var cfgs []string
224	if strings.HasPrefix(ec, "SK_") {
225		cfgs = []string{ec}
226	} else {
227		cfgs = strings.Split(ec, "_")
228	}
229	compiled := make([]*regexp.Regexp, 0, len(re))
230	for _, r := range re {
231		compiled = append(compiled, regexp.MustCompile(r))
232	}
233	for _, c := range cfgs {
234		for _, r := range compiled {
235			if r.MatchString(c) {
236				return true
237			}
238		}
239	}
240	return false
241}
242
243// debug returns true if this task runs in debug mode.
244func (p parts) debug() bool {
245	return p["configuration"] == "Debug"
246}
247
248// release returns true if this task runs in release mode.
249func (p parts) release() bool {
250	return p["configuration"] == "Release"
251}
252
253// isLinux returns true if the task runs on Linux.
254func (p parts) isLinux() bool {
255	return p.matchOs("Debian", "Ubuntu") ||
256		p.matchExtraConfig("Debian", "Ubuntu") ||
257		p.matchBazelHost("linux", "on_rpi") ||
258		p.role("Housekeeper", "Canary", "Upload")
259
260}
261
262// isWindows returns true if the task runs on Windows.
263func (p parts) isWindows() bool {
264	return !p.role("Upload") && (p.matchOs("Win") || p.matchExtraConfig("Win") || p.matchBazelHost("windows"))
265}
266
267// isMac returns true if the task runs on Mac.
268func (p parts) isMac() bool {
269	return !p.role("Upload") && (p.matchOs("Mac") || p.matchExtraConfig("Mac") || p.matchBazelHost("darwin"))
270}
271
272// bazelBuildParts returns all parts from the BazelBuild schema. All parts are required.
273func (p parts) bazelBuildParts() (label string, config string, host string) {
274	return p["label"], p["config"], p["host"]
275}
276
277// bazelTestParts returns all parts from the BazelTest schema. task_driver, label, build_config,
278// and host are required; test_config is optional.
279func (p parts) bazelTestParts() (taskDriver string, label string, buildConfig string, host string, testConfig string) {
280	return p["task_driver"], p["label"], p["build_config"], p["host"], p["test_config"]
281}
282
283// TODO(borenet): The below really belongs in its own file, probably next to the
284// builder_name_schema.json file.
285
286// schema is a sub-struct of JobNameSchema.
287type schema struct {
288	Keys         []string `json:"keys"`
289	OptionalKeys []string `json:"optional_keys"`
290	RecurseRoles []string `json:"recurse_roles"`
291}
292
293// JobNameSchema is a struct used for (de)constructing Job names in a
294// predictable format.
295type JobNameSchema struct {
296	Schema map[string]*schema `json:"builder_name_schema"`
297	Sep    string             `json:"builder_name_sep"`
298}
299
300// NewJobNameSchema returns a JobNameSchema instance based on the given JSON
301// file.
302func NewJobNameSchema(jsonFile string) (*JobNameSchema, error) {
303	var rv JobNameSchema
304	f, err := os.Open(jsonFile)
305	if err != nil {
306		return nil, err
307	}
308	defer func() {
309		if err := f.Close(); err != nil {
310			log.Println(fmt.Sprintf("Failed to close %s: %s", jsonFile, err))
311		}
312	}()
313	if err := json.NewDecoder(f).Decode(&rv); err != nil {
314		return nil, err
315	}
316	return &rv, nil
317}
318
319// ParseJobName splits the given Job name into its component parts, according
320// to the schema.
321func (s *JobNameSchema) ParseJobName(n string) (map[string]string, error) {
322	popFront := func(items []string) (string, []string, error) {
323		if len(items) == 0 {
324			return "", nil, fmt.Errorf("Invalid job name: %s (not enough parts)", n)
325		}
326		return items[0], items[1:], nil
327	}
328
329	result := map[string]string{}
330
331	var parse func(int, string, []string) ([]string, error)
332	parse = func(depth int, role string, parts []string) ([]string, error) {
333		s, ok := s.Schema[role]
334		if !ok {
335			return nil, fmt.Errorf("Invalid job name; %q is not a valid role.", role)
336		}
337		if depth == 0 {
338			result["role"] = role
339		} else {
340			result[fmt.Sprintf("sub-role-%d", depth)] = role
341		}
342		var err error
343		for _, key := range s.Keys {
344			var value string
345			value, parts, err = popFront(parts)
346			if err != nil {
347				return nil, err
348			}
349			result[key] = value
350		}
351		for _, subRole := range s.RecurseRoles {
352			if len(parts) > 0 && parts[0] == subRole {
353				parts, err = parse(depth+1, parts[0], parts[1:])
354				if err != nil {
355					return nil, err
356				}
357			}
358		}
359		for _, key := range s.OptionalKeys {
360			if len(parts) > 0 {
361				var value string
362				value, parts, err = popFront(parts)
363				if err != nil {
364					return nil, err
365				}
366				result[key] = value
367			}
368		}
369		if len(parts) > 0 {
370			return nil, fmt.Errorf("Invalid job name: %s (too many parts)", n)
371		}
372		return parts, nil
373	}
374
375	split := strings.Split(n, s.Sep)
376	if len(split) < 2 {
377		return nil, fmt.Errorf("Invalid job name: %s (not enough parts)", n)
378	}
379	role := split[0]
380	split = split[1:]
381	_, err := parse(0, role, split)
382	return result, err
383}
384
385// MakeJobName assembles the given parts of a Job name, according to the schema.
386func (s *JobNameSchema) MakeJobName(parts map[string]string) (string, error) {
387	rvParts := make([]string, 0, len(parts))
388
389	var process func(int, map[string]string) (map[string]string, error)
390	process = func(depth int, parts map[string]string) (map[string]string, error) {
391		roleKey := "role"
392		if depth != 0 {
393			roleKey = fmt.Sprintf("sub-role-%d", depth)
394		}
395		role, ok := parts[roleKey]
396		if !ok {
397			return nil, fmt.Errorf("Invalid job parts; missing key %q", roleKey)
398		}
399
400		s, ok := s.Schema[role]
401		if !ok {
402			return nil, fmt.Errorf("Invalid job parts; unknown role %q", role)
403		}
404		rvParts = append(rvParts, role)
405		delete(parts, roleKey)
406
407		for _, key := range s.Keys {
408			value, ok := parts[key]
409			if !ok {
410				return nil, fmt.Errorf("Invalid job parts; missing %q", key)
411			}
412			rvParts = append(rvParts, value)
413			delete(parts, key)
414		}
415
416		if len(s.RecurseRoles) > 0 {
417			subRoleKey := fmt.Sprintf("sub-role-%d", depth+1)
418			subRole, ok := parts[subRoleKey]
419			if !ok {
420				return nil, fmt.Errorf("Invalid job parts; missing %q", subRoleKey)
421			}
422			rvParts = append(rvParts, subRole)
423			delete(parts, subRoleKey)
424			found := false
425			for _, recurseRole := range s.RecurseRoles {
426				if recurseRole == subRole {
427					found = true
428					var err error
429					parts, err = process(depth+1, parts)
430					if err != nil {
431						return nil, err
432					}
433					break
434				}
435			}
436			if !found {
437				return nil, fmt.Errorf("Invalid job parts; unknown sub-role %q", subRole)
438			}
439		}
440		for _, key := range s.OptionalKeys {
441			if value, ok := parts[key]; ok {
442				rvParts = append(rvParts, value)
443				delete(parts, key)
444			}
445		}
446		if len(parts) > 0 {
447			return nil, fmt.Errorf("Invalid job parts: too many parts: %v", parts)
448		}
449		return parts, nil
450	}
451
452	// Copy the parts map, so that we can modify at will.
453	partsCpy := make(map[string]string, len(parts))
454	for k, v := range parts {
455		partsCpy[k] = v
456	}
457	if _, err := process(0, partsCpy); err != nil {
458		return "", err
459	}
460	return strings.Join(rvParts, s.Sep), nil
461}
462