blob: 5fb55f789771f07f63429354ca9f5561882821e6 [file] [log] [blame]
// Package compatibility provides functions for backwards compatiblity with
// test platform.
//
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package compatibility
import (
"errors"
"fmt"
"math/rand"
"sort"
"strings"
"go.chromium.org/chromiumos/test/plan/internal/compatibility/priority"
"github.com/golang/glog"
"github.com/golang/protobuf/proto"
testpb "go.chromium.org/chromiumos/config/go/test/api"
test_api_v1 "go.chromium.org/chromiumos/config/go/test/api/v1"
"go.chromium.org/chromiumos/infra/proto/go/chromiumos"
"go.chromium.org/chromiumos/infra/proto/go/lab"
"go.chromium.org/chromiumos/infra/proto/go/testplans"
bbpb "go.chromium.org/luci/buildbucket/proto"
"google.golang.org/protobuf/types/known/structpb"
"google.golang.org/protobuf/types/known/wrapperspb"
)
// criterionMatchesAttribute returns true if criterion's AttributeId matches
// attr's Id or any of attr's Aliases.
func criterionMatchesAttribute(criterion *testpb.DutCriterion, attr *testpb.DutAttribute) bool {
if criterion.GetAttributeId().GetValue() == attr.GetId().GetValue() {
return true
}
for _, alias := range attr.GetAliases() {
if criterion.GetAttributeId().GetValue() == alias {
return true
}
}
return false
}
// getAttrFromCriteria finds the DutCriterion with attribute id attr in
// criteria. If the DutCriterion is not found a nil array is returned. If there
// is more than one DutCriterion that matches attr, an error is returned.
func getAttrFromCriteria(criteria []*testpb.DutCriterion, attr *testpb.DutAttribute) ([]string, error) {
var values []string
matched := false
for _, criterion := range criteria {
if criterionMatchesAttribute(criterion, attr) {
if matched {
return nil, fmt.Errorf("DutAttribute %q specified twice", attr)
}
if len(criterion.GetValues()) == 0 {
return nil, fmt.Errorf("only DutCriterion with at least one value supported, got %q", criterion)
}
values = criterion.GetValues()
matched = true
}
}
return values, nil
}
// getAllAttrFromCriteria is similar to getAttrFromCriteria, but if more than
// one DutCriterion that matches attr, values for all matches are returned,
// instead of returning an error.
func getAllAttrFromCriteria(criteria []*testpb.DutCriterion, attr *testpb.DutAttribute) ([][]string, error) {
var values [][]string
for _, criterion := range criteria {
if criterionMatchesAttribute(criterion, attr) {
if len(criterion.GetValues()) == 0 {
return nil, fmt.Errorf("only DutCriterion with at least one value supported, got %q", criterion)
}
values = append(values, criterion.GetValues())
}
}
return values, nil
}
// checkCriteriaValid returns an error if any of criteria don't match the set
// of validAttrs.
func checkCriteriaValid(criteria []*testpb.DutCriterion, validAttrs ...*testpb.DutAttribute) error {
for _, criterion := range criteria {
matches := false
for _, attr := range validAttrs {
if criterionMatchesAttribute(criterion, attr) {
matches = true
}
}
if !matches {
return fmt.Errorf("criterion %q doesn't match any valid attributes (%q)", criterion, validAttrs)
}
}
return nil
}
// sortedValuesFromMap returns the values from m as a list, sorted by the keys
// of the map.
func sortedValuesFromMap[V any](m map[string]V) []V {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
values := make([]V, 0, len(m))
for _, k := range keys {
values = append(values, m[k])
}
return values
}
// Chooses a program from the options in programs to test. The choice is
// determined by:
// 1. Choose a program with a critical completed build. If there are multiple
// programs, choose with prioritySelector.
//
// 2. Choose a program with a non-critical completed build. If there are
// multiple programs, choose with prioritySelector.
//
// 3. Choose a program with prioritySelector.
func chooseProgramToTest(
pool string,
programs []string,
buildInfos map[string]*buildInfo,
prioritySelector *priority.RandomWeightedSelector,
) (priority.BoardInfo, error) {
var criticalPrograms, completedPrograms []string
for _, program := range programs {
buildInfo, found := buildInfos[program]
if found {
completedPrograms = append(completedPrograms, program)
if buildInfo.criticality == bbpb.Trinary_YES {
criticalPrograms = append(criticalPrograms, program)
}
}
}
if len(criticalPrograms) > 0 {
glog.V(2).Infof("Choosing between critical programs: %q", criticalPrograms)
return prioritySelector.SelectPriority(pool, criticalPrograms)
}
if len(completedPrograms) > 0 {
glog.V(2).Infof("Choosing between completed programs: %q", completedPrograms)
return prioritySelector.SelectPriority(pool, completedPrograms)
}
glog.V(2).Info("No completed programs found.")
return prioritySelector.SelectPriority(pool, programs)
}
// extractFromProtoStruct returns the path pointed to by fields. For example,
// for the struct `"a": {"b": 1}`, if fields is ["a", "b"], 1 is returned. The
// bool return value indicates if the path was found.
func extractFromProtoStruct(s *structpb.Struct, fields ...string) (*structpb.Value, bool) {
var value *structpb.Value
for i, field := range fields {
var ok bool
value, ok = s.GetFields()[field]
if !ok {
return nil, false
}
// All of the fields before the last one must be structs (otherwise
// fields cannot form a valid path). Since the last field may not be a
// struct, (and we don't need to use the struct if it is) break here and
// return the value. Otherwise check that the value is a struct, and
// set s to the new struct.
if i == len(fields)-1 {
break
}
s = value.GetStructValue()
if s == nil {
return nil, false
}
}
return value, true
}
// extractStringFromProtoStruct invokes extractFromProtoStruct, but also checks
// that the value pointed to by fields is a non-empty string.
func extractStringFromProtoStruct(s *structpb.Struct, fields ...string) (string, bool) {
v, ok := extractFromProtoStruct(s, fields...)
if !ok {
return "", false
}
if v.GetStringValue() == "" {
return "", false
}
return v.GetStringValue(), true
}
// buildInfo describes properties parsed from a Buildbucket build and
// BuilderConfigs.
type buildInfo struct {
// the build_target.name input property.
buildTarget string
// the builder.builder field.
builderName string
// the build.portage_profile.profile field from the BuilderConfig. If the
// build didn't have a corresponding BuilderConfig, this will be empty.
profile string
// the critical field.
criticality bbpb.Trinary
// a payload containing information about the build's artifacts. If the
// build doesn't have testable artifacts, this will be nil.
payload *testplans.BuildPayload
// the containerBuildingFailed output property, which indicates if
// building the CFT containers failed. Note this property is not set on
// builds that ran before crrev.com/c/4906244. If the property is not set
// on the build, it is assumed false, i.e. building the CFT containers was
// successful.
containerBuildingFailed bool
// a pointer to the Build itself, useful for functions that need the above
// extracted fields for computation, but still return results containing
// the actual Build.
build *bbpb.Build
}
// getTestArtifacts returns a BuildPayload pointing to the test artifacts in
// build. If the build doesn't contain test artifacts, nil is returned.
func getTestArtifacts(build *bbpb.Build) (*testplans.BuildPayload, error) {
builderName := build.GetBuilder().GetBuilder()
filesByArtifact, ok := extractFromProtoStruct(
build.GetOutput().GetProperties(),
"artifacts", "files_by_artifact",
)
if !ok {
glog.Warningf("artifacts.files_by_artifact not found in output properties of build %q", builderName)
return nil, nil
}
if filesByArtifact.GetStructValue() == nil {
return nil, fmt.Errorf("artifacts.files_by_artifact must be a non-empty struct")
}
// The presence of any one of these artifacts is enough to tell us that this
// build should be considered for testing. It is possible they are present
// as keys in the map but empty lists; in this case, skip the artifact
// and log a warning, as this is somewhat unexpected.
testArtifacts := []string{
"IMAGE_ZIP",
"PINNED_GUEST_IMAGES",
"TEST_UPDATE_PAYLOAD",
}
foundTestArtifact := false
for _, testArtifact := range testArtifacts {
files, found := filesByArtifact.GetStructValue().GetFields()[testArtifact]
if found {
// The key exists in the map, check that it is a non-empty list.
switch files.GetKind().(type) {
case *structpb.Value_ListValue:
if len(files.GetListValue().GetValues()) > 0 {
glog.Infof(
"found test artifact %q on build %q",
testArtifact,
builderName,
)
foundTestArtifact = true
break
}
glog.Warningf(
"test artifact %q is present but empty on build %q",
testArtifact,
builderName,
)
default:
glog.Warningf(
"test artifact %q is present but not a list, this is unexpected. On build %q. value is: %q",
testArtifact,
builderName,
files,
)
}
}
}
if !foundTestArtifact {
glog.Warningf("no test artifacts found for build %q", builderName)
return nil, nil
}
// If files_by_artifact was populated with test artifacts, but the GS fields
// are missing, return an error.
artifactsGsBucket, ok := extractStringFromProtoStruct(
build.GetOutput().GetProperties(),
"artifacts", "gs_bucket",
)
if !ok {
return nil, fmt.Errorf("artifacts.gs_bucket not found for build %q", builderName)
}
artifactsGsPath, ok := extractStringFromProtoStruct(
build.GetOutput().GetProperties(),
"artifacts", "gs_path",
)
if !ok {
return nil, fmt.Errorf("artifacts.gs_path not found for build %q", builderName)
}
return &testplans.BuildPayload{
ArtifactsGsBucket: artifactsGsBucket,
ArtifactsGsPath: artifactsGsPath,
FilesByArtifact: filesByArtifact.GetStructValue(),
}, nil
}
// buildsToBuildInfos extracts properties from builds into buildInfos. If
// skipIfNoTestArtifacts is true, builds without test artifacts will not be
// returned. Builds that have set the pointless_build output property are always
// skipped.
func buildsToBuildInfos(
builds []*bbpb.Build,
builderConfigs *chromiumos.BuilderConfigs,
skipIfNoTestArtifacts bool,
) ([]*buildInfo, error) {
builderToBuilderConfig := make(map[string]*chromiumos.BuilderConfig)
for _, builder := range builderConfigs.GetBuilderConfigs() {
builderToBuilderConfig[builder.GetId().GetName()] = builder
}
buildInfos := []*buildInfo{}
for _, build := range builds {
builderName := build.GetBuilder().GetBuilder()
pointless, ok := extractFromProtoStruct(build.GetOutput().GetProperties(), "pointless_build")
if ok && pointless.GetBoolValue() {
glog.Warningf("build %q is pointless, skipping", builderName)
continue
}
buildTarget, ok := extractStringFromProtoStruct(
build.GetInput().GetProperties(),
"build_target", "name",
)
if !ok {
glog.Warningf("build_target.name not found in input properties of build %q, skipping", builderName)
continue
}
payload, err := getTestArtifacts(build)
if err != nil {
return nil, err
}
if payload == nil && skipIfNoTestArtifacts {
glog.Warningf("skipIfNoTestArtifacts set, skipping build %q", builderName)
continue
}
// Attempt to lookup the BuilderConfig to find profile information. If
// no BuilderConfig is found, keep the profile empty and log a warning,
// this build will match against CoverageRules that don't specify a
// profile.
profile := ""
builderConfig, ok := builderToBuilderConfig[builderName]
if ok {
profile = builderConfig.GetBuild().GetPortageProfile().GetProfile()
} else {
glog.Warningf("no BuilderConfig found for %q", builderName)
}
var containerBuildingFailed bool
containerBuildingFailedValue, ok := extractFromProtoStruct(build.GetOutput().GetProperties(), "container_building_failed")
if ok {
containerBuildingFailed = containerBuildingFailedValue.GetBoolValue()
} else {
glog.Warningf("container_building_failed not found in the output properties of build %q, assuming false (CFT containers build successfully)", builderName)
containerBuildingFailed = false
}
buildInfos = append(buildInfos, &buildInfo{
buildTarget: buildTarget,
builderName: build.GetBuilder().GetBuilder(),
profile: profile,
criticality: build.GetCritical(),
containerBuildingFailed: containerBuildingFailed,
payload: payload,
build: build,
},
)
}
return buildInfos, nil
}
// Defines the environment a test should run in, e.g. hardware, Tast on VM, Tast
// on GCE.
type testEnvironment int64
const (
undefined testEnvironment = iota
hw
tastVM
tastGCE
)
// suiteInfo is a struct internal to this package to track information about a
// test suite to run.
type suiteInfo struct {
// program that was chosen to run the suite on
program string
// optional, only multi-dut tests to hold chosen program and provision config
// for each comapnion
companions []*testplans.TestCompanion
// optional, design that was chosen to run the suite on
design string
// pool to run the suite in.
pool string
// name of the suite.
suite string
// environment to run the suite in.
environment testEnvironment
// optional, tagCriteria that define the suite. Only valid if runViaCft is
// set. If not set, the name of the suite is used as the id to lookup and
// execute the suite.
tagCriteria *testpb.TestSuite_TestCaseTagCriteria
// whether the test suite is critical or not
critical bool
// optional, variant of the build target to test. For example, if program
// is "coral" and boardVariant is "kernelnext", the "coral-kernelnext" build
// will be used.
boardVariant string
// optional, profile of the build target to test. For example "asan".
profile string
// optional, the licenses required for the DUT the test will run on.
licenses []lab.LicenseType
// optional, the total number of shards to be used in a test run.
totalShards int64
// optional, if true then run test suites in this rule via CFT workflow.
runViaCft bool
}
// getBuildTarget returns the build target for the suiteInfo. If boardVariant is
// set on the suite info, "<program>-<boardVariant>" is returned, otherwise
// "<program>" is returned.
func (si *suiteInfo) getBuildTarget() string {
if len(si.boardVariant) > 0 {
return fmt.Sprintf("%s-%s", si.program, si.boardVariant)
}
return si.program
}
// getTastExpr converts suiteInfo's TestCaseTagCriteria to a Tast expression.
// All of the included and excluded tags are joined together with " && ", i.e.
// Tast expressions with "|" cannot be generated. Excluded tags are negated with
// "!". The entire expression is surrounded in parens.
//
// Test name includes and excludes are turned into tags like "name:...".
//
// For backwards compatibility between TestCaseTagCriteria and pure Tast
// expressions, the following transformations are done:
//
// - Tags are quoted if they aren't already.
// - If test names have "tast." as a prefix, this prefix is stripped.
//
// See https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/running_tests.md
// for a description of Tast expressions.
func (si *suiteInfo) getTastExpr() string {
// The expr would be the AND of all attributes.
attributes := []string{}
// Note: Due to the backward compatibility issue, we also support `"group:mainline"` and
// `("name:a.*"||"name:b.*")` these two kinds of tags. To prevent double quoting or making
// expression as a string, we only add quote when the tag is not starting with `"` or `(`.
// Match ALL of the tags.
for _, tag := range si.tagCriteria.GetTags() {
if !strings.HasPrefix(tag, "(") && !strings.HasPrefix(tag, "\"") {
tag = fmt.Sprintf("\"%s\"", tag)
}
attributes = append(attributes, tag)
}
// Don't match ANY excluded tags.
for _, tag := range si.tagCriteria.GetTagExcludes() {
if !strings.HasPrefix(tag, "(") && !strings.HasPrefix(tag, "\"") {
tag = fmt.Sprintf("\"%s\"", tag)
}
attributes = append(attributes, "!"+tag)
}
// Match ONE of the name.
if len(si.tagCriteria.GetTestNames()) > 0 {
oneOfNames := []string{}
for _, name := range si.tagCriteria.GetTestNames() {
name = fmt.Sprintf("\"name:%s\"", strings.TrimPrefix(name, "tast."))
oneOfNames = append(oneOfNames, name)
}
attr := "(" + strings.Join(oneOfNames, "||") + ")"
attributes = append(attributes, attr)
}
// Don't match ANY excluded name.
for _, name := range si.tagCriteria.GetTestNameExcludes() {
name = fmt.Sprintf("\"name:%s\"", strings.TrimPrefix(name, "tast."))
attributes = append(attributes, "!"+name)
}
return "(" + strings.Join(attributes, "&&") + ")"
}
// coverageRuleToSuiteInfo converts a CoverageRule (CTPv2 compatible) to a
// suiteInfo (internal representation used by this package).
//
// coverageRuleToSuiteInfo does the following steps:
// 1. Extract the relevant DutAttributes from rule. pool and program attributes
// are required.
// 2. Choose a program using prioritySelector.
// 3. Convert each TestSuite (CTPv2 compatible) in rule into a suiteInfo, using
// either the tag criteria or test case id list.
func coverageRuleToSuiteInfo(
rule *testpb.CoverageRule,
poolAttr, programAttr, designAttr, licenseAttr *testpb.DutAttribute,
buildTargetToBuildInfo map[string]*buildInfo,
prioritySelector *priority.RandomWeightedSelector,
isVM bool,
) ([]*suiteInfo, error) {
dutTarget := rule.GetDutTargets()[0]
// Check that all criteria in dutTarget specify one of the expected
// DutAttributes.
if err := checkCriteriaValid(dutTarget.GetCriteria(), poolAttr, programAttr, designAttr, licenseAttr); err != nil {
return nil, err
}
pools, err := getAttrFromCriteria(dutTarget.GetCriteria(), poolAttr)
if err != nil {
return nil, err
}
if len(pools) != 1 {
return nil, fmt.Errorf("only DutCriteria with exactly one \"swarming-pool\" attribute are supported, got %q", pools)
}
pool := pools[0]
programs, err := getAttrFromCriteria(dutTarget.GetCriteria(), programAttr)
if err != nil {
return nil, err
}
if len(programs) == 0 {
return nil, errors.New("DutCriteria must contain at least one \"attr-program\" attribute")
}
// For simplicitly, only allow a board variant or profile to be specified if
// a single program is specified. I.e. a rule that specifies it wants to
// test the "kernelnext" or "asan" build chosen from multiple programs is
// not currently supported.
var chosenProgram string
var chosenIndex int
boardVariant := dutTarget.GetProvisionConfig().GetBoardVariant()
profile := dutTarget.GetProvisionConfig().GetProfile()
if len(boardVariant) > 0 || len(profile) > 0 {
if len(programs) != 1 {
return nil, fmt.Errorf(
"board_variant (%q) and profile (%q) cannot be specified if multiple programs (%q) are specified",
boardVariant, profile, programs,
)
}
chosenProgram = programs[0]
chosenIndex = 0
} else {
chosenBoardInfo, err := chooseProgramToTest(
pool, programs, buildTargetToBuildInfo, prioritySelector,
)
if err != nil {
return nil, err
}
chosenProgram = chosenBoardInfo.GetBoard()
chosenIndex = chosenBoardInfo.GetIndex()
glog.V(2).Infof("chose program %q from possible programs %q", chosenProgram, programs)
}
companions, err := chooseCompanions(chosenIndex, rule, programAttr)
if err != nil {
return nil, err
}
// The design attribute is optional. If a design is specified, only one
// program can be specified. Multiple designs cannot be specified.
var design string
designs, err := getAttrFromCriteria(dutTarget.GetCriteria(), designAttr)
if err != nil {
return nil, err
}
if len(designs) == 1 {
if len(programs) != 1 {
return nil, fmt.Errorf("if \"attr-design\" is specified, multiple \"attr-programs\" cannot be used")
}
design = designs[0]
} else if len(designs) > 1 {
return nil, fmt.Errorf("only DutCriteria with one \"attr-design\" attribute are supported, got %q", designs)
}
allLicenseNames, err := getAllAttrFromCriteria(dutTarget.GetCriteria(), licenseAttr)
if err != nil {
return nil, err
}
var licenses []lab.LicenseType
for _, names := range allLicenseNames {
if len(names) != 1 {
return nil, fmt.Errorf("only exactly one value can be specified in \"misc-licence\" DutCriteria, got %q", names)
}
name := names[0]
licenseInt, found := lab.LicenseType_value[name]
if !found {
return nil, fmt.Errorf("invalid LicenseType %q", name)
}
licence := lab.LicenseType(licenseInt)
if licence == lab.LicenseType_LICENSE_TYPE_UNSPECIFIED {
return nil, fmt.Errorf("LICENSE_TYPE_UNSPECIFIED not allowed")
}
licenses = append(licenses, licence)
}
ruleCritical := true
if rule.Critical != nil {
ruleCritical = rule.GetCritical().GetValue()
glog.V(2).Infof("rule %q explicitly sets criticality %v", rule.GetName(), ruleCritical)
}
var buildTarget string
if len(boardVariant) > 0 {
buildTarget = fmt.Sprintf("%s-%s", chosenProgram, boardVariant)
} else {
buildTarget = chosenProgram
}
programCritical := true
buildInfo, found := buildTargetToBuildInfo[buildTarget]
if found && buildInfo.criticality != bbpb.Trinary_YES {
programCritical = false
glog.V(2).Infof("build target %q explicitly sets criticality %q", buildTarget, buildInfo.criticality)
}
// The rule and program must both be critical in order for it to be blocking
// in CQ.
critical := ruleCritical && programCritical
var suiteInfos []*suiteInfo
for _, suite := range rule.GetTestSuites() {
switch spec := suite.Spec.(type) {
case *testpb.TestSuite_TestCaseIds:
if isVM {
return nil, fmt.Errorf("TestCaseIdLists are only valid for HW tests")
}
for _, id := range spec.TestCaseIds.GetTestCaseIds() {
suiteInfos = append(
suiteInfos,
&suiteInfo{
program: chosenProgram,
companions: companions,
design: design,
pool: pool,
suite: id.Value,
tagCriteria: nil,
environment: hw,
critical: critical,
boardVariant: boardVariant,
profile: profile,
licenses: licenses,
runViaCft: rule.RunViaCft,
})
}
case *testpb.TestSuite_TestCaseTagCriteria_:
var env testEnvironment
if isVM {
name := suite.GetName()
switch {
case strings.HasPrefix(name, "tast_vm"):
env = tastVM
case strings.HasPrefix(name, "tast_gce"):
env = tastGCE
default:
return nil, fmt.Errorf("VM suite names must start with either \"tast_vm\" or \"tast_gce\" in CTP1 compatibility mode, got %q", name)
}
} else {
// Check that the suite still has a name, as this is required
// required for the display name.
if suite.GetName() == "" {
return nil, fmt.Errorf("TestSuites must still specify a name if they are using TagCriteria")
}
env = hw
}
suiteInfos = append(suiteInfos,
&suiteInfo{
program: chosenProgram,
companions: companions,
design: design,
pool: pool,
suite: suite.GetName(),
tagCriteria: suite.GetTestCaseTagCriteria(),
environment: env,
critical: critical,
boardVariant: boardVariant,
profile: profile,
totalShards: suite.GetTotalShards(),
licenses: licenses,
runViaCft: rule.GetRunViaCft(),
})
default:
return nil, fmt.Errorf("TestSuite spec type %T is not supported", spec)
}
}
return suiteInfos, nil
}
// getDutAttribute returns the DutAttribute matching id from dutAttributeList.
// If no matching DutAttribute is found, returns an error.
func getDutAttribute(dutAttributeList *testpb.DutAttributeList, id string) (*testpb.DutAttribute, error) {
for _, attr := range dutAttributeList.GetDutAttributes() {
if attr.GetId().GetValue() == id {
return attr, nil
}
}
return nil, fmt.Errorf("%q not found in DutAttributeList", id)
}
// extractSuiteInfos returns a map from build target name to suiteInfos for the
// build target. There is one suiteInfo per CoverageRule in hwTestPlans and
// vmTestPlans.
func extractSuiteInfos(
rnd *rand.Rand,
hwTestPlans []*test_api_v1.HWTestPlan,
vmTestPlans []*test_api_v1.VMTestPlan,
dutAttributeList *testpb.DutAttributeList,
boardPriorityList *testplans.BoardPriorityList,
buildTargetToBuildInfo map[string]*buildInfo,
) (map[string][]*suiteInfo, error) {
// Find relevant attributes in the DutAttributeList.
programAttr, err := getDutAttribute(dutAttributeList, "attr-program")
if err != nil {
return nil, err
}
designAttr, err := getDutAttribute(dutAttributeList, "attr-design")
if err != nil {
return nil, err
}
poolAttr, err := getDutAttribute(dutAttributeList, "swarming-pool")
if err != nil {
return nil, err
}
licenseAttr, err := getDutAttribute(dutAttributeList, "misc-license")
if err != nil {
return nil, err
}
buildTargetToSuiteInfos := map[string][]*suiteInfo{}
prioritySelector := priority.NewRandomWeightedSelector(
rnd,
boardPriorityList,
)
for _, hwTestPlan := range hwTestPlans {
for _, rule := range hwTestPlan.GetCoverageRules() {
isVM := false
suiteInfos, err := coverageRuleToSuiteInfo(
rule, poolAttr, programAttr, designAttr, licenseAttr, buildTargetToBuildInfo, prioritySelector, isVM,
)
if err != nil {
return nil, err
}
for _, info := range suiteInfos {
buildTarget := info.getBuildTarget()
if _, ok := buildTargetToSuiteInfos[buildTarget]; !ok {
buildTargetToSuiteInfos[buildTarget] = []*suiteInfo{}
}
buildTargetToSuiteInfos[buildTarget] = append(buildTargetToSuiteInfos[buildTarget], info)
}
}
}
for _, vmTestPlan := range vmTestPlans {
for _, rule := range vmTestPlan.GetCoverageRules() {
isVM := true
suiteInfos, err := coverageRuleToSuiteInfo(
rule, poolAttr, programAttr, designAttr, licenseAttr, buildTargetToBuildInfo, prioritySelector, isVM,
)
if err != nil {
return nil, err
}
for _, info := range suiteInfos {
buildTarget := info.getBuildTarget()
if _, ok := buildTargetToSuiteInfos[buildTarget]; !ok {
buildTargetToSuiteInfos[buildTarget] = []*suiteInfo{}
}
buildTargetToSuiteInfos[buildTarget] = append(buildTargetToSuiteInfos[buildTarget], info)
}
}
}
return buildTargetToSuiteInfos, nil
}
// createTastVMTest creates a TastVmTestCfg_TastVmTest based on a suiteInfo
// and shardIndex. Note that shardIndex is 0-based.
func createTastVMTest(buildInfo *buildInfo, suiteInfo *suiteInfo, shardIndex int64) (*testplans.TastVmTestCfg_TastVmTest, error) {
if suiteInfo.totalShards != 0 && shardIndex > suiteInfo.totalShards {
return nil, fmt.Errorf("shardIndex cannot be greater than suiteInfo.totalShards")
}
// If the suite is configured to run more than one shard, include that info in the display name
var displayName string
if suiteInfo.totalShards > 1 {
// shardIndex is 0-based, but we use 1-based indexing for the display name.
displayName = fmt.Sprintf("%s.tast_vm.%s_shard_%d_of_%d", buildInfo.builderName, suiteInfo.suite, shardIndex+1, suiteInfo.totalShards)
} else {
displayName = fmt.Sprintf("%s.tast_vm.%s", buildInfo.builderName, suiteInfo.suite)
}
tastVMTest := &testplans.TastVmTestCfg_TastVmTest{
SuiteName: suiteInfo.suite,
TastTestExpr: []*testplans.TastVmTestCfg_TastTestExpr{
{
TestExpr: suiteInfo.getTastExpr(),
},
},
Common: &testplans.TestSuiteCommon{
DisplayName: displayName,
Critical: &wrapperspb.BoolValue{
Value: suiteInfo.critical,
},
},
}
if suiteInfo.totalShards > 1 {
tastVMTest.TastTestShard = &testplans.TastTestShard{
TotalShards: suiteInfo.totalShards,
ShardIndex: shardIndex,
}
}
return tastVMTest, nil
}
// createTastGCETest creates a TastGceTestCfg_TastGceTest based on a suiteInfo
// and shardIndex. Note that shardIndex is 0-based.
func createTastGCETest(buildInfo *buildInfo, suiteInfo *suiteInfo, shardIndex int64) (*testplans.TastGceTestCfg_TastGceTest, error) {
if suiteInfo.totalShards != 0 && shardIndex > suiteInfo.totalShards {
return nil, fmt.Errorf("shardIndex cannot be greater than suiteInfo.totalShards")
}
// If the suite is configured to run more than one shard, include that info in the display name
var displayName string
if suiteInfo.totalShards > 1 {
// shardIndex is 0-based, but we use 1-based indexing for the display name.
displayName = fmt.Sprintf("%s.tast_gce.%s_shard_%d_of_%d", buildInfo.builderName, suiteInfo.suite, shardIndex+1, suiteInfo.totalShards)
} else {
displayName = fmt.Sprintf("%s.tast_gce.%s", buildInfo.builderName, suiteInfo.suite)
}
tastGCETest := &testplans.TastGceTestCfg_TastGceTest{
SuiteName: suiteInfo.suite,
TastTestExpr: []*testplans.TastGceTestCfg_TastTestExpr{
{
TestExpr: suiteInfo.getTastExpr(),
},
},
GceMetadata: &testplans.TastGceTestCfg_TastGceTest_GceMetadata{
Project: "chromeos-gce-tests",
Zone: "us-central1-a",
MachineType: "n1-standard-4",
Network: "chromeos-gce-tests",
Subnet: "us-central1",
},
Common: &testplans.TestSuiteCommon{
DisplayName: displayName,
Critical: &wrapperspb.BoolValue{
Value: suiteInfo.critical,
},
},
}
if suiteInfo.totalShards > 1 {
tastGCETest.TastTestShard = &testplans.TastTestShard{
TotalShards: suiteInfo.totalShards,
ShardIndex: shardIndex,
}
}
return tastGCETest, nil
}
// ToCTP1 converts a [VM|HW]TestPlan to a GenerateTestPlansResponse, which can
// be used with CTP1.
//
// [HW|VM]TestPlan protos target CTP2, this method is meant to provide backwards
// compatibility with CTP1. Because CTP1 does not support rules-based testing,
// there are some limitations to the [HW|VM]TestPlans that can be converted:
//
// - Both the "attr-program" and "swarming-pool" DutAttributes must be used in
// each DutTarget. The "attr-design" and "misc-license" DutAttributes are
// optional.
//
// - Multiple values for "attr-program" are allowed, a program will be chosen
// randomly proportional to the board's priority in boardPriorityList (lowest
// priority is most likely to get chosen, negative priorities are allowed,
// programs without a priority get priority 0).
//
// - Only TestCaseIds are supported for hardware testing, and only
// TestCaseTagCriteria are supported for VM testing.
//
// - generateTestPlanReq is needed to provide Build protos for the builds being
// tested.
//
// - builderConfigs is needed to provide Portage profile information for the
// builds being tested.
func ToCTP1(
rnd *rand.Rand,
hwTestPlans []*test_api_v1.HWTestPlan,
vmTestPlans []*test_api_v1.VMTestPlan,
generateTestPlanReq *testplans.GenerateTestPlanRequest,
dutAttributeList *testpb.DutAttributeList,
boardPriorityList *testplans.BoardPriorityList,
builderConfigs *chromiumos.BuilderConfigs,
) (*testplans.GenerateTestPlanResponse, error) {
builds := make([]*bbpb.Build, 0, len(generateTestPlanReq.GetBuildbucketProtos()))
for _, protoBytes := range generateTestPlanReq.GetBuildbucketProtos() {
build := &bbpb.Build{}
if err := proto.Unmarshal(protoBytes.GetSerializedProto(), build); err != nil {
return nil, err
}
builds = append(builds, build)
}
buildInfos, err := buildsToBuildInfos(builds, builderConfigs, true)
if err != nil {
return nil, err
}
// Form a map from buildTarget to buildInfo, which will be needed by calls to
// extractSuiteInfos.
buildTargetToBuildInfo := map[string]*buildInfo{}
for _, buildInfo := range buildInfos {
buildTargetToBuildInfo[buildInfo.buildTarget] = buildInfo
}
buildTargetToSuiteInfos, err := extractSuiteInfos(
rnd, hwTestPlans, vmTestPlans, dutAttributeList, boardPriorityList, buildTargetToBuildInfo,
)
if err != nil {
return nil, err
}
var hwTestUnits []*testplans.HwTestUnit
var vmTestUnits []*testplans.TastVmTestUnit
var gceTestUnits []*testplans.TastGceTestUnit
// Join the buildInfos and suiteInfos on build target name. Each build maps
// to one HwTestUnit, each suite maps to one HwTestCfg_HwTest.
for _, buildInfo := range buildInfos {
suiteInfos, ok := buildTargetToSuiteInfos[buildInfo.buildTarget]
if !ok {
glog.Warningf("no suites found for build %q, skipping tests", buildInfo.buildTarget)
continue
}
// Maps from display name, which should uniquely identify a test for a
// given (buildTarget, model, suite) combo, to a test.
hwTests := make(map[string]*testplans.HwTestCfg_HwTest)
tastVMTests := make(map[string]*testplans.TastVmTestCfg_TastVmTest)
tastGCETests := make(map[string]*testplans.TastGceTestCfg_TastGceTest)
for _, suiteInfo := range suiteInfos {
// If the build's profile doesn't match the suite's profile, skip
// the suite. Note that majority of builds and suites don't specify
// a profile, so they should match on the empty string.
if buildInfo.profile != suiteInfo.profile {
glog.Infof(
"profile for build %q (%q) doesn't match profile for suite %q (%q), skipping",
buildInfo.builderName, buildInfo.profile, suiteInfo.suite, suiteInfo.profile,
)
continue
}
if suiteInfo.runViaCft && buildInfo.containerBuildingFailed {
glog.Errorf(
"builder %q failed container building, cannot run suite %q with CFT",
buildInfo.builderName, suiteInfo.suite,
)
continue
}
switch env := suiteInfo.environment; env {
case hw:
// If a design is set, include it in the display name
var displayName string
if len(suiteInfo.design) > 0 {
displayName = fmt.Sprintf("%s.%s.hw.%s", buildInfo.builderName, suiteInfo.design, suiteInfo.suite)
} else {
displayName = fmt.Sprintf("%s.hw.%s", buildInfo.builderName, suiteInfo.suite)
}
hwTest := &testplans.HwTestCfg_HwTest{
Suite: suiteInfo.suite,
SkylabBoard: suiteInfo.program,
SkylabModel: suiteInfo.design,
Pool: suiteInfo.pool,
Licenses: suiteInfo.licenses,
Common: &testplans.TestSuiteCommon{
DisplayName: displayName,
Critical: &wrapperspb.BoolValue{
Value: suiteInfo.critical,
},
},
RunViaCft: suiteInfo.runViaCft,
TagCriteria: suiteInfo.tagCriteria,
TotalShards: suiteInfo.totalShards,
Companions: suiteInfo.companions,
}
if _, found := hwTests[displayName]; found {
glog.V(2).Infof("HwTest already added: %q", hwTest)
} else {
hwTests[displayName] = hwTest
glog.V(2).Infof("added HwTest: %q", hwTest)
}
case tastVM:
if suiteInfo.totalShards > 0 {
var i int64
for i = 0; i < suiteInfo.totalShards; i++ {
tastVMTest, err := createTastVMTest(buildInfo, suiteInfo, i)
if err != nil {
return nil, err
}
displayName := tastVMTest.GetCommon().GetDisplayName()
if _, found := tastVMTests[displayName]; found {
glog.V(2).Infof("TastVmTest already added: %q", tastVMTest)
} else {
tastVMTests[displayName] = tastVMTest
glog.V(2).Infof("added TastVmTest %q", tastVMTest)
}
}
} else {
tastVMTest, err := createTastVMTest(buildInfo, suiteInfo, 0)
if err != nil {
return nil, err
}
displayName := tastVMTest.GetCommon().GetDisplayName()
if _, found := tastVMTests[displayName]; found {
glog.V(2).Infof("TastVmTest already added: %q", tastVMTest)
} else {
tastVMTests[displayName] = tastVMTest
glog.V(2).Infof("added TastVmTest %q", tastVMTest)
}
}
case tastGCE:
if suiteInfo.totalShards > 0 {
var i int64
for i = 0; i < suiteInfo.totalShards; i++ {
tastGCETest, err := createTastGCETest(buildInfo, suiteInfo, i)
if err != nil {
return nil, err
}
displayName := tastGCETest.GetCommon().GetDisplayName()
if _, found := tastGCETests[displayName]; found {
glog.V(2).Infof("TastGceTest already added: %q", tastGCETest)
} else {
tastGCETests[displayName] = tastGCETest
glog.V(2).Infof("added TastGceTest %q", tastGCETest)
}
}
} else {
tastGCETest, err := createTastGCETest(buildInfo, suiteInfo, 0)
if err != nil {
return nil, err
}
displayName := tastGCETest.GetCommon().GetDisplayName()
if _, found := tastGCETests[displayName]; found {
glog.V(2).Infof("TastGceTest already added: %q", tastGCETest)
} else {
tastGCETests[displayName] = tastGCETest
glog.V(2).Infof("added TastGceTest %q", tastGCETest)
}
}
default:
return nil, fmt.Errorf("unsupported environment %T", env)
}
}
common := &testplans.TestUnitCommon{
BuildTarget: &chromiumos.BuildTarget{
Name: buildInfo.buildTarget,
},
BuilderName: buildInfo.builderName,
BuildPayload: buildInfo.payload,
}
if len(hwTests) > 0 {
hwTestUnit := &testplans.HwTestUnit{
Common: common,
HwTestCfg: &testplans.HwTestCfg{
HwTest: sortedValuesFromMap(hwTests),
},
}
hwTestUnits = append(hwTestUnits, hwTestUnit)
}
if len(tastVMTests) > 0 {
vmTestUnit := &testplans.TastVmTestUnit{
Common: common,
TastVmTestCfg: &testplans.TastVmTestCfg{
TastVmTest: sortedValuesFromMap(tastVMTests),
},
}
vmTestUnits = append(vmTestUnits, vmTestUnit)
}
if len(tastGCETests) > 0 {
gceTestUnit := &testplans.TastGceTestUnit{
Common: common,
TastGceTestCfg: &testplans.TastGceTestCfg{
TastGceTest: sortedValuesFromMap(tastGCETests),
},
}
gceTestUnits = append(gceTestUnits, gceTestUnit)
}
}
return &testplans.GenerateTestPlanResponse{
HwTestUnits: hwTestUnits,
DirectTastVmTestUnits: vmTestUnits,
TastGceTestUnits: gceTestUnits,
}, nil
}