blob: 37010e7d9ba74e1de1a3569498c30d0e1dc9d4fd [file] [log] [blame]
// Copyright 2021 The Chromium OS 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 coveragerules converts a merged SourceTestPlan into CoverageRules.
package coveragerules
import (
"fmt"
"reflect"
"strings"
"github.com/golang/glog"
configpb "go.chromium.org/chromiumos/config/go/api"
buildpb "go.chromium.org/chromiumos/config/go/build/api"
testpb "go.chromium.org/chromiumos/config/go/test/api"
"go.chromium.org/chromiumos/config/go/test/plan"
"go.chromium.org/luci/common/data/stringset"
)
var (
buildTargetAttributeID = &testpb.DutAttribute_Id{Value: "system_build_target"}
fingerprintAttributeID = &testpb.DutAttribute_Id{Value: "fingerprint_location"}
)
func getSingleDutCriterionOrPanic(rule *testpb.CoverageRule) *testpb.DutCriterion {
if len(rule.DutCriteria) != 1 {
panic(fmt.Sprintf("expected exactly one DutCriterion, got rule %s", rule))
}
return rule.DutCriteria[0]
}
// expandCoverageRules joins newRules to curRules, by intersecting DutCriteria.
//
// For each combination of CoverageRules (a, b), where a is in curRules, and b
// is in newRules, a new CoverageRule is added to the result, with
// DutCriterion.Values that are the intersection of a.DutCriteria[0].Values and
// b.DutCriteria[0].Values
//
// It is assumed each CoverageRule has exactly one DutCriterion (this function
// panics if this is not true).
//
// All CoverageRules in either newRules or curRules that don't have any
// intersection of DutCriterion.Values are added to the result as is.
//
// For example, if curRules is
// {
// {
// Name: "A", DutCriteria: {{Values:"1"}},
// },
// {
// Name: "B", DutCriteria: {{Values:"2"}},
// },
// }
//
// and newRules is
//
// {
// {
// Name: "C", DutCriteria: {{Values:"1", "3"}},
// },
// {
// Name: "D", DutCriteria: {{Values:"4"}},
// },
// }
//
// the result is
//
// {
// {
// Name: "A_C", DutCriteria: {{Values:"1"}},
// },
// {
// Name: "B", DutCriteria: {{Values:"2"}},
// },
// {
// Name: "D", DutCriteria: {{Values:"4"}},
// },
// }
//
// because "A" and "C" are joined, "B" and "D" are passed through as is.
//
// If curRules is empty, newRules is returned (this function is intended to
// be called multiple times to build up a result, curRules is empty in the
// first call).
func expandCoverageRules(curRules, newRules []*testpb.CoverageRule) []*testpb.CoverageRule {
for _, rule := range newRules {
glog.V(2).Infof("Joining CoverageRule %s", rule)
}
if len(curRules) == 0 {
return newRules
}
// Make a map from name to CoverageRule for all CoverageRules in curRules
// and newRules. If a CoverageRule is involved in an intersection, it is
// removed from unjoinedRules.
unjoinedRules := make(map[string]*testpb.CoverageRule)
for _, rule := range append(curRules, newRules...) {
unjoinedRules[rule.Name] = rule
}
expandedRules := make([]*testpb.CoverageRule, 0)
for _, cur := range curRules {
for _, new := range newRules {
curDC := getSingleDutCriterionOrPanic(cur)
newDC := getSingleDutCriterionOrPanic(new)
if curDC.AttributeId.Value != newDC.AttributeId.Value {
continue
}
valueIntersection := stringset.NewFromSlice(
curDC.Values...,
).Intersect(
stringset.NewFromSlice(newDC.Values...),
)
if len(valueIntersection) > 0 {
delete(unjoinedRules, cur.Name)
delete(unjoinedRules, new.Name)
expandedRules = append(expandedRules, &testpb.CoverageRule{
Name: fmt.Sprintf("%s_%s", cur.Name, new.Name),
DutCriteria: []*testpb.DutCriterion{
{
AttributeId: curDC.AttributeId,
Values: valueIntersection.ToSlice(),
},
},
TestSuites: cur.TestSuites,
})
}
}
}
// Return all unjoined rules as is.
for _, rule := range unjoinedRules {
expandedRules = append(expandedRules, rule)
}
return expandedRules
}
// buildTargetCoverageRules groups BuildTarget overlay names in
// buildSummaryList and returns one CoverageRule per group.
//
// For each BuildSummary in buildSummaryList, keyFn is called to get a string
// key. All overlay names that share the same string key are used to create a
// CoverageRule.
//
// nameFn converts a key returned by keyFn to a Name for the CoverageRule.
//
// For example, to create one CoverageRule for each kernel version, keyFn should
// return the kernel version found in a BuildSummary, and nameFn could return
// the string "kernel:<key>".
//
// If keyFn returns the empty string, that BuildSummary is skipped.
func buildTargetCoverageRules(
keyFn func(*buildpb.SystemImage_BuildSummary) string,
nameFn func(string) string,
buildSummaryList *buildpb.SystemImage_BuildSummaryList,
sourceTestPlan *plan.SourceTestPlan,
) []*testpb.CoverageRule {
keyToBuildTargets := make(map[string][]string)
for _, value := range buildSummaryList.GetValues() {
key := keyFn(value)
if key == "" {
continue
}
if _, found := keyToBuildTargets[key]; !found {
keyToBuildTargets[key] = []string{}
}
keyToBuildTargets[key] = append(
keyToBuildTargets[key], value.GetBuildTarget().GetPortageBuildTarget().GetOverlayName(),
)
}
coverageRules := make([]*testpb.CoverageRule, 0, len(keyToBuildTargets))
for key, buildTargets := range keyToBuildTargets {
coverageRules = append(coverageRules, &testpb.CoverageRule{
Name: nameFn(key),
TestSuites: []*testpb.TestSuite{
{
TestCaseTagCriteria: &testpb.TestSuite_TestCaseTagCriteria{
Tags: sourceTestPlan.TestTags,
TagExcludes: sourceTestPlan.TestTagExcludes,
},
},
},
DutCriteria: []*testpb.DutCriterion{
{
AttributeId: buildTargetAttributeID,
Values: buildTargets,
},
},
})
}
return coverageRules
}
// kernelCoverageRules returns CoverageRules requiring each kernel version.
func kernelCoverageRules(
sourceTestPlan *plan.SourceTestPlan, buildSummaryList *buildpb.SystemImage_BuildSummaryList,
) []*testpb.CoverageRule {
return buildTargetCoverageRules(
func(buildSummary *buildpb.SystemImage_BuildSummary) string {
return buildSummary.GetKernel().GetVersion()
},
func(key string) string {
return fmt.Sprintf("kernel:%s", key)
},
buildSummaryList,
sourceTestPlan,
)
}
// socCoverageRules returns CoverageRules requiring each SoC family.
func socCoverageRules(
sourceTestPlan *plan.SourceTestPlan, buildSummaryList *buildpb.SystemImage_BuildSummaryList,
) []*testpb.CoverageRule {
return buildTargetCoverageRules(
func(buildSummary *buildpb.SystemImage_BuildSummary) string {
return buildSummary.GetChipset().GetOverlay()
},
func(key string) string {
return fmt.Sprintf("soc:%s", key)
},
buildSummaryList,
sourceTestPlan,
)
}
// arcCoverageRules returns a CoverageRule requiring each ARC version.
func arcCoverageRules(
sourceTestPlan *plan.SourceTestPlan, buildSummaryList *buildpb.SystemImage_BuildSummaryList,
) []*testpb.CoverageRule {
return buildTargetCoverageRules(
func(buildSummary *buildpb.SystemImage_BuildSummary) string {
return buildSummary.GetArc().GetVersion()
},
func(key string) string {
return fmt.Sprintf("arc:%s", key)
},
buildSummaryList,
sourceTestPlan,
)
}
// fingerprintCoverageRule returns a CoverageRule requiring a fingerprint
// sensor.
func fingerprintCoverageRule(sourceTestPlan *plan.SourceTestPlan) *testpb.CoverageRule {
presentEnums := []string{}
for name, value := range configpb.HardwareFeatures_Fingerprint_Location_value {
if value != int32(configpb.HardwareFeatures_Fingerprint_LOCATION_UNKNOWN) &&
value != int32(configpb.HardwareFeatures_Fingerprint_NOT_PRESENT) {
presentEnums = append(presentEnums, name)
}
}
return &testpb.CoverageRule{
Name: "fp:present",
TestSuites: []*testpb.TestSuite{
{
TestCaseTagCriteria: &testpb.TestSuite_TestCaseTagCriteria{
Tags: sourceTestPlan.TestTags,
TagExcludes: sourceTestPlan.TestTagExcludes,
},
},
},
DutCriteria: []*testpb.DutCriterion{
{
AttributeId: fingerprintAttributeID,
Values: presentEnums,
},
},
}
}
// checkDutAttributesValid checks that the ids of all attributes in rules are in
// dutAttributeList.
func checkDutAttributesValid(rules []*testpb.CoverageRule, dutAttributeList *testpb.DutAttributeList) error {
validAttributes := stringset.New(0)
for _, dutAttribute := range dutAttributeList.DutAttributes {
validAttributes.Add(dutAttribute.Id.Value)
}
invalidAttributes := []string{}
for _, rule := range rules {
for _, criterion := range rule.DutCriteria {
if !validAttributes.Has(criterion.AttributeId.Value) {
invalidAttributes = append(invalidAttributes, criterion.AttributeId.Value)
}
}
}
if len(invalidAttributes) > 0 {
return fmt.Errorf("CoverageRule contains invalid DutAttributes: %q", invalidAttributes)
}
return nil
}
// Generate computes a list of CoverageRules, based on sourceTestPlan and
// buildSummaryList.
func Generate(
sourceTestPlan *plan.SourceTestPlan,
buildSummaryList *buildpb.SystemImage_BuildSummaryList,
dutAttributeList *testpb.DutAttributeList,
) ([]*testpb.CoverageRule, error) {
coverageRules := []*testpb.CoverageRule{}
// For each requirement set in sourceTestPlan, switch on the type of the
// requirement and call the corresponding <requirement>Outputs function.
//
// Return an error if no requirements are set, or a requirement is
// unimplemented.
hasRequirement := false
reqs := sourceTestPlan.Requirements
if reqs != nil {
// As of 6/9/2021, the dev-go/protobuf ebuild installs v1.3.2 of the
// github.com/golang/protobuf package, which does not contain the
// MessageReflect function.
//
// Bumping the version of this package broke dependent packages. Use the
// standard reflect package instead.
//
// TODO(b/189223005): Fix dependent package or install multiple versions
// of the protobuf package.
reqsValue := reflect.ValueOf(*reqs)
for i := 0; i < reqsValue.NumField(); i++ {
fieldValue := reqsValue.Field(i)
// All Requirements should be messages, so pointers to structs in
// the generated Go. Use IsZero to check if they are set (even if
// no fields within them are set).
if fieldValue.Kind() != reflect.Ptr || fieldValue.IsZero() {
continue
}
hasRequirement = true
// Get the type name of the field, removing "*plan.", for use in log
// and error messages.
typeName := strings.ReplaceAll(fieldValue.Type().String(), "*plan.", "")
switch fieldValue.Interface().(type) {
case *plan.SourceTestPlan_Requirements_ArcVersions:
coverageRules = expandCoverageRules(coverageRules, arcCoverageRules(sourceTestPlan, buildSummaryList))
case *plan.SourceTestPlan_Requirements_KernelVersions:
coverageRules = expandCoverageRules(coverageRules, kernelCoverageRules(sourceTestPlan, buildSummaryList))
case *plan.SourceTestPlan_Requirements_SocFamilies:
coverageRules = expandCoverageRules(coverageRules, socCoverageRules(sourceTestPlan, buildSummaryList))
case *plan.SourceTestPlan_Requirements_Fingerprint:
coverageRules = expandCoverageRules(
coverageRules, []*testpb.CoverageRule{fingerprintCoverageRule(sourceTestPlan)},
)
default:
return nil, fmt.Errorf("unimplemented requirement %q", typeName)
}
glog.V(1).Infof("Added CoverageRules for %q, now have %d CoverageRules", typeName, len(coverageRules))
}
}
if !hasRequirement {
return nil, fmt.Errorf("at least one requirement must be set in SourceTestPlan: %v", sourceTestPlan)
}
if err := checkDutAttributesValid(coverageRules, dutAttributeList); err != nil {
return nil, err
}
return coverageRules, nil
}