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// matchExtraConfig returns true if any of the extra_configs for this job matches 207// any of the given regular expressions. If the extra_config starts with "SK_", 208// it is considered to be a single config. 209func (p parts) matchExtraConfig(re ...string) bool { 210 if len(re) == 0 { 211 log.Fatal("No regular expressions provided for matchExtraConfig()!") 212 } 213 ec := p["extra_config"] 214 if ec == "" { 215 return false 216 } 217 var cfgs []string 218 if strings.HasPrefix(ec, "SK_") { 219 cfgs = []string{ec} 220 } else { 221 cfgs = strings.Split(ec, "_") 222 } 223 compiled := make([]*regexp.Regexp, 0, len(re)) 224 for _, r := range re { 225 compiled = append(compiled, regexp.MustCompile(r)) 226 } 227 for _, c := range cfgs { 228 for _, r := range compiled { 229 if r.MatchString(c) { 230 return true 231 } 232 } 233 } 234 return false 235} 236 237// debug returns true if this task runs in debug mode. 238func (p parts) debug() bool { 239 return p["configuration"] == "Debug" 240} 241 242// release returns true if this task runs in release mode. 243func (p parts) release() bool { 244 return p["configuration"] == "Release" 245} 246 247// isLinux returns true if the task runs on Linux. 248func (p parts) isLinux() bool { 249 return p.matchOs("Debian", "Ubuntu") 250} 251 252// bazelBuildParts returns all parts from the BazelBuild schema. All parts are required. 253func (p parts) bazelBuildParts() (label string, config string, host string) { 254 return p["label"], p["config"], p["host"] 255} 256 257// bazelTestParts returns all parts from the BazelTest schema. task_driver, label, build_config, 258// and host are required; test_config is optional. 259func (p parts) bazelTestParts() (taskDriver string, label string, buildConfig string, host string, testConfig string) { 260 return p["task_driver"], p["label"], p["build_config"], p["host"], p["test_config"] 261} 262 263// TODO(borenet): The below really belongs in its own file, probably next to the 264// builder_name_schema.json file. 265 266// schema is a sub-struct of JobNameSchema. 267type schema struct { 268 Keys []string `json:"keys"` 269 OptionalKeys []string `json:"optional_keys"` 270 RecurseRoles []string `json:"recurse_roles"` 271} 272 273// JobNameSchema is a struct used for (de)constructing Job names in a 274// predictable format. 275type JobNameSchema struct { 276 Schema map[string]*schema `json:"builder_name_schema"` 277 Sep string `json:"builder_name_sep"` 278} 279 280// NewJobNameSchema returns a JobNameSchema instance based on the given JSON 281// file. 282func NewJobNameSchema(jsonFile string) (*JobNameSchema, error) { 283 var rv JobNameSchema 284 f, err := os.Open(jsonFile) 285 if err != nil { 286 return nil, err 287 } 288 defer func() { 289 if err := f.Close(); err != nil { 290 log.Println(fmt.Sprintf("Failed to close %s: %s", jsonFile, err)) 291 } 292 }() 293 if err := json.NewDecoder(f).Decode(&rv); err != nil { 294 return nil, err 295 } 296 return &rv, nil 297} 298 299// ParseJobName splits the given Job name into its component parts, according 300// to the schema. 301func (s *JobNameSchema) ParseJobName(n string) (map[string]string, error) { 302 popFront := func(items []string) (string, []string, error) { 303 if len(items) == 0 { 304 return "", nil, fmt.Errorf("Invalid job name: %s (not enough parts)", n) 305 } 306 return items[0], items[1:], nil 307 } 308 309 result := map[string]string{} 310 311 var parse func(int, string, []string) ([]string, error) 312 parse = func(depth int, role string, parts []string) ([]string, error) { 313 s, ok := s.Schema[role] 314 if !ok { 315 return nil, fmt.Errorf("Invalid job name; %q is not a valid role.", role) 316 } 317 if depth == 0 { 318 result["role"] = role 319 } else { 320 result[fmt.Sprintf("sub-role-%d", depth)] = role 321 } 322 var err error 323 for _, key := range s.Keys { 324 var value string 325 value, parts, err = popFront(parts) 326 if err != nil { 327 return nil, err 328 } 329 result[key] = value 330 } 331 for _, subRole := range s.RecurseRoles { 332 if len(parts) > 0 && parts[0] == subRole { 333 parts, err = parse(depth+1, parts[0], parts[1:]) 334 if err != nil { 335 return nil, err 336 } 337 } 338 } 339 for _, key := range s.OptionalKeys { 340 if len(parts) > 0 { 341 var value string 342 value, parts, err = popFront(parts) 343 if err != nil { 344 return nil, err 345 } 346 result[key] = value 347 } 348 } 349 if len(parts) > 0 { 350 return nil, fmt.Errorf("Invalid job name: %s (too many parts)", n) 351 } 352 return parts, nil 353 } 354 355 split := strings.Split(n, s.Sep) 356 if len(split) < 2 { 357 return nil, fmt.Errorf("Invalid job name: %s (not enough parts)", n) 358 } 359 role := split[0] 360 split = split[1:] 361 _, err := parse(0, role, split) 362 return result, err 363} 364 365// MakeJobName assembles the given parts of a Job name, according to the schema. 366func (s *JobNameSchema) MakeJobName(parts map[string]string) (string, error) { 367 rvParts := make([]string, 0, len(parts)) 368 369 var process func(int, map[string]string) (map[string]string, error) 370 process = func(depth int, parts map[string]string) (map[string]string, error) { 371 roleKey := "role" 372 if depth != 0 { 373 roleKey = fmt.Sprintf("sub-role-%d", depth) 374 } 375 role, ok := parts[roleKey] 376 if !ok { 377 return nil, fmt.Errorf("Invalid job parts; missing key %q", roleKey) 378 } 379 380 s, ok := s.Schema[role] 381 if !ok { 382 return nil, fmt.Errorf("Invalid job parts; unknown role %q", role) 383 } 384 rvParts = append(rvParts, role) 385 delete(parts, roleKey) 386 387 for _, key := range s.Keys { 388 value, ok := parts[key] 389 if !ok { 390 return nil, fmt.Errorf("Invalid job parts; missing %q", key) 391 } 392 rvParts = append(rvParts, value) 393 delete(parts, key) 394 } 395 396 if len(s.RecurseRoles) > 0 { 397 subRoleKey := fmt.Sprintf("sub-role-%d", depth+1) 398 subRole, ok := parts[subRoleKey] 399 if !ok { 400 return nil, fmt.Errorf("Invalid job parts; missing %q", subRoleKey) 401 } 402 rvParts = append(rvParts, subRole) 403 delete(parts, subRoleKey) 404 found := false 405 for _, recurseRole := range s.RecurseRoles { 406 if recurseRole == subRole { 407 found = true 408 var err error 409 parts, err = process(depth+1, parts) 410 if err != nil { 411 return nil, err 412 } 413 break 414 } 415 } 416 if !found { 417 return nil, fmt.Errorf("Invalid job parts; unknown sub-role %q", subRole) 418 } 419 } 420 for _, key := range s.OptionalKeys { 421 if value, ok := parts[key]; ok { 422 rvParts = append(rvParts, value) 423 delete(parts, key) 424 } 425 } 426 if len(parts) > 0 { 427 return nil, fmt.Errorf("Invalid job parts: too many parts: %v", parts) 428 } 429 return parts, nil 430 } 431 432 // Copy the parts map, so that we can modify at will. 433 partsCpy := make(map[string]string, len(parts)) 434 for k, v := range parts { 435 partsCpy[k] = v 436 } 437 if _, err := process(0, partsCpy); err != nil { 438 return "", err 439 } 440 return strings.Join(rvParts, s.Sep), nil 441} 442