blob: 4d335b995dafa5cce5e01955ada4c957bbc38197 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package gen_tasks_logic
/*
This file contains logic related to task/job name schemas.
*/
import (
"encoding/json"
"fmt"
"log"
"os"
"regexp"
"strings"
)
// Parts represents the key/value pairs which make up task and job names.
type Parts map[string]string
// Equal returns true if the given part of this job's name equals any of the
// given values. Panics if no values are provided.
func (p Parts) Equal(part string, eq ...string) bool {
if len(eq) == 0 {
log.Fatal("No values provided for equal!")
}
v := p[part]
for _, e := range eq {
if v == e {
return true
}
}
return false
}
// Role returns true if the Role for this job equals any of the given values.
func (p Parts) Role(eq ...string) bool {
return p.Equal("role", eq...)
}
// Os returns true if the OS for this job equals any of the given values.
func (p Parts) Os(eq ...string) bool {
return p.Equal("os", eq...)
}
// Compiler returns true if the Compiler for this job equals any of the given
// values.
func (p Parts) Compiler(eq ...string) bool {
return p.Equal("compiler", eq...)
}
// Model returns true if the Model for this job equals any of the given values.
func (p Parts) Model(eq ...string) bool {
return p.Equal("model", eq...)
}
// Frequency returns true if the Frequency for this job equals any of the given
// values.
func (p Parts) Frequency(eq ...string) bool {
return p.Equal("frequency", eq...)
}
// CPU returns true if the task's cpu_or_gpu is "CPU" and the CPU for this
// task equals any of the given values. If no values are provided, CPU returns
// true if this task runs on CPU.
func (p Parts) CPU(eq ...string) bool {
if p["cpu_or_gpu"] == "CPU" {
if len(eq) == 0 {
return true
}
return p.Equal("cpu_or_gpu_value", eq...)
}
return false
}
// GPU returns true if the task's cpu_or_gpu is "GPU" and the GPU for this task
// equals any of the given values. If no values are provided, GPU returns true
// if this task runs on GPU.
func (p Parts) GPU(eq ...string) bool {
if p["cpu_or_gpu"] == "GPU" {
if len(eq) == 0 {
return true
}
return p.Equal("cpu_or_gpu_value", eq...)
}
return false
}
// Arch returns true if the architecture for this job equals any of the
// given values.
func (p Parts) Arch(eq ...string) bool {
return p.Equal("arch", eq...) || p.Equal("target_arch", eq...)
}
// ExtraConfig returns true if any of the extra_configs for this job equals
// any of the given values. If the extra_config starts with "SK_",
// it is considered to be a single config.
func (p Parts) ExtraConfig(eq ...string) bool {
if len(eq) == 0 {
log.Fatal("No values provided for extraConfig()!")
}
ec := p["extra_config"]
if ec == "" {
return false
}
splitCfgs := strings.Split(ec, "_")
cfgs := make([]string, 0, len(splitCfgs))
for idx, c := range splitCfgs {
if c == "SK" {
cfgs = append(cfgs, strings.Join(cfgs[idx:], "_"))
break
}
cfgs = append(cfgs, c)
}
for _, c := range cfgs {
for _, e := range eq {
if c == e {
return true
}
}
}
return false
}
// NoExtraConfig returns true if there are no extra_configs for this job.
func (p Parts) NoExtraConfig(eq ...string) bool {
ec := p["extra_config"]
return ec == ""
}
// MatchPart returns true if the given part of this job's name matches any of
// the given regular expressions. Note that a regular expression might match any
// substring, so if you need an exact match on the entire string you'll need to
// use `^` and `$`. Panics if no regular expressions are provided.
func (p Parts) MatchPart(part string, re ...string) bool {
if len(re) == 0 {
log.Fatal("No regular expressions provided for matchPart()!")
}
v := p[part]
for _, r := range re {
if regexp.MustCompile(r).MatchString(v) {
return true
}
}
return false
}
// MatchRole returns true if the role for this job matches any of the given
// regular expressions.
func (p Parts) MatchRole(re ...string) bool {
return p.MatchPart("role", re...)
}
func (p Parts) Project(re ...string) bool {
return p.MatchPart("project", re...)
}
// MatchOs returns true if the OS for this job matches any of the given regular
// expressions.
func (p Parts) MatchOs(re ...string) bool {
return p.MatchPart("os", re...)
}
// MatchCompiler returns true if the compiler for this job matches any of the
// given regular expressions.
func (p Parts) MatchCompiler(re ...string) bool {
return p.MatchPart("compiler", re...)
}
// MatchModel returns true if the model for this job matches any of the given
// regular expressions.
func (p Parts) MatchModel(re ...string) bool {
return p.MatchPart("model", re...)
}
// MatchCpu returns true if the task's cpu_or_gpu is "CPU" and the CPU for this
// task matches any of the given regular expressions. If no regular expressions
// are provided, cpu returns true if this task runs on CPU.
func (p Parts) MatchCpu(re ...string) bool {
if p["cpu_or_gpu"] == "CPU" {
if len(re) == 0 {
return true
}
return p.MatchPart("cpu_or_gpu_value", re...)
}
return false
}
// MatchGpu returns true if the task's cpu_or_gpu is "GPU" and the GPU for this task
// matches any of the given regular expressions. If no regular expressions are
// provided, gpu returns true if this task runs on GPU.
func (p Parts) MatchGpu(re ...string) bool {
if p["cpu_or_gpu"] == "GPU" {
if len(re) == 0 {
return true
}
return p.MatchPart("cpu_or_gpu_value", re...)
}
return false
}
// MatchArch returns true if the architecture for this job matches any of the
// given regular expressions.
func (p Parts) MatchArch(re ...string) bool {
return p.MatchPart("arch", re...) || p.MatchPart("target_arch", re...)
}
// MatchBazelHost returns true if the Bazel host for this job matches any of the
// given regular expressions.
func (p Parts) MatchBazelHost(re ...string) bool {
return p.MatchPart("host", re...)
}
// MatchExtraConfig returns true if any of the extra_configs for this job matches
// any of the given regular expressions. If the extra_config starts with "SK_",
// it is considered to be a single config.
func (p Parts) MatchExtraConfig(re ...string) bool {
if len(re) == 0 {
log.Fatal("No regular expressions provided for matchExtraConfig()!")
}
ec := p["extra_config"]
if ec == "" {
return false
}
var cfgs []string
if strings.HasPrefix(ec, "SK_") {
cfgs = []string{ec}
} else {
cfgs = strings.Split(ec, "_")
}
compiled := make([]*regexp.Regexp, 0, len(re))
for _, r := range re {
compiled = append(compiled, regexp.MustCompile(r))
}
for _, c := range cfgs {
for _, r := range compiled {
if r.MatchString(c) {
return true
}
}
}
return false
}
// Debug returns true if this task runs in Debug mode.
func (p Parts) Debug() bool {
return p["configuration"] == "Debug"
}
// Release returns true if this task runs in Release mode.
func (p Parts) Release() bool {
return p["configuration"] == "Release"
}
// IsLinux returns true if the task runs on Linux.
func (p Parts) IsLinux() bool {
return p.MatchOs("Debian", "Ubuntu") ||
p.MatchExtraConfig("Debian", "Ubuntu") ||
p.MatchBazelHost("linux", "on_rpi") ||
p.Role("Housekeeper", "Canary", "Upload")
}
// IsWindows returns true if the task runs on Windows.
func (p Parts) IsWindows() bool {
return !p.Role("Upload") && (p.MatchOs("Win") || p.MatchExtraConfig("Win") || p.MatchBazelHost("windows"))
}
// IsMac returns true if the task runs on Mac.
func (p Parts) IsMac() bool {
return !p.Role("Upload") && (p.MatchOs("Mac") || p.MatchExtraConfig("Mac") || p.MatchBazelHost("darwin"))
}
// BazelBuildParts returns all parts from the BazelBuild schema. All parts are required.
func (p Parts) BazelBuildParts() (label string, config string, host string) {
return p["label"], p["config"], p["host"]
}
// BazelTestParts returns all parts from the BazelTest schema. task_driver, label, build_config,
// and host are required; test_config is optional.
func (p Parts) BazelTestParts() (taskDriver string, label string, buildConfig string, host string, testConfig string) {
return p["task_driver"], p["label"], p["build_config"], p["host"], p["test_config"]
}
// TODO(borenet): The below really belongs in its own file, probably next to the
// builder_name_schema.json file.
// schema is a sub-struct of JobNameSchema.
type schema struct {
Keys []string `json:"keys"`
OptionalKeys []string `json:"optional_keys"`
RecurseRoles []string `json:"recurse_roles"`
}
// JobNameSchema is a struct used for (de)constructing Job names in a
// predictable format.
type JobNameSchema struct {
Schema map[string]*schema `json:"builder_name_schema"`
Sep string `json:"builder_name_sep"`
}
// NewJobNameSchema returns a JobNameSchema instance based on the given JSON
// file.
func NewJobNameSchema(jsonFile string) (*JobNameSchema, error) {
var rv JobNameSchema
f, err := os.Open(jsonFile)
if err != nil {
return nil, err
}
defer func() {
if err := f.Close(); err != nil {
log.Println(fmt.Sprintf("Failed to close %s: %s", jsonFile, err))
}
}()
if err := json.NewDecoder(f).Decode(&rv); err != nil {
return nil, err
}
return &rv, nil
}
// ParseJobName splits the given Job name into its component parts, according
// to the schema.
func (s *JobNameSchema) ParseJobName(n string) (map[string]string, error) {
popFront := func(items []string) (string, []string, error) {
if len(items) == 0 {
return "", nil, fmt.Errorf("Invalid job name: %s (not enough parts)", n)
}
return items[0], items[1:], nil
}
result := map[string]string{}
var parse func(int, string, []string) ([]string, error)
parse = func(depth int, role string, parts []string) ([]string, error) {
s, ok := s.Schema[role]
if !ok {
return nil, fmt.Errorf("Invalid job name; %q is not a valid role.", role)
}
if depth == 0 {
result["role"] = role
} else {
result[fmt.Sprintf("sub-role-%d", depth)] = role
}
var err error
for _, key := range s.Keys {
var value string
value, parts, err = popFront(parts)
if err != nil {
return nil, err
}
result[key] = value
}
for _, subRole := range s.RecurseRoles {
if len(parts) > 0 && parts[0] == subRole {
parts, err = parse(depth+1, parts[0], parts[1:])
if err != nil {
return nil, err
}
}
}
for _, key := range s.OptionalKeys {
if len(parts) > 0 {
var value string
value, parts, err = popFront(parts)
if err != nil {
return nil, err
}
result[key] = value
}
}
if len(parts) > 0 {
return nil, fmt.Errorf("Invalid job name: %s (too many parts)", n)
}
return parts, nil
}
split := strings.Split(n, s.Sep)
if len(split) < 2 {
return nil, fmt.Errorf("Invalid job name: %s (not enough parts)", n)
}
role := split[0]
split = split[1:]
_, err := parse(0, role, split)
return result, err
}
// MakeJobName assembles the given parts of a Job name, according to the schema.
func (s *JobNameSchema) MakeJobName(parts map[string]string) (string, error) {
rvParts := make([]string, 0, len(parts))
var process func(int, map[string]string) (map[string]string, error)
process = func(depth int, parts map[string]string) (map[string]string, error) {
roleKey := "role"
if depth != 0 {
roleKey = fmt.Sprintf("sub-role-%d", depth)
}
role, ok := parts[roleKey]
if !ok {
return nil, fmt.Errorf("Invalid job parts; missing key %q", roleKey)
}
s, ok := s.Schema[role]
if !ok {
return nil, fmt.Errorf("Invalid job parts; unknown role %q", role)
}
rvParts = append(rvParts, role)
delete(parts, roleKey)
for _, key := range s.Keys {
value, ok := parts[key]
if !ok {
return nil, fmt.Errorf("Invalid job parts; missing %q", key)
}
rvParts = append(rvParts, value)
delete(parts, key)
}
if len(s.RecurseRoles) > 0 {
subRoleKey := fmt.Sprintf("sub-role-%d", depth+1)
subRole, ok := parts[subRoleKey]
if !ok {
return nil, fmt.Errorf("Invalid job parts; missing %q", subRoleKey)
}
rvParts = append(rvParts, subRole)
delete(parts, subRoleKey)
found := false
for _, recurseRole := range s.RecurseRoles {
if recurseRole == subRole {
found = true
var err error
parts, err = process(depth+1, parts)
if err != nil {
return nil, err
}
break
}
}
if !found {
return nil, fmt.Errorf("Invalid job parts; unknown sub-role %q", subRole)
}
}
for _, key := range s.OptionalKeys {
if value, ok := parts[key]; ok {
rvParts = append(rvParts, value)
delete(parts, key)
}
}
if len(parts) > 0 {
return nil, fmt.Errorf("Invalid job parts: too many parts: %v", parts)
}
return parts, nil
}
// Copy the parts map, so that we can modify at will.
partsCpy := make(map[string]string, len(parts))
for k, v := range parts {
partsCpy[k] = v
}
if _, err := process(0, partsCpy); err != nil {
return "", err
}
return strings.Join(rvParts, s.Sep), nil
}