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