convertv1: Pass extra DutCriteria through as FreeformAttributes.

- To allow passing additional dimensions from test config to
scheduling.

BUG=b:336864067
TEST=CQ

Change-Id: I88ec857d8b59e7523f927364771e48eaa2431a98
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/6143013
Reviewed-by: Navil Perez <[email protected]>
Tested-by: Andrew Lamb <[email protected]>
Commit-Queue: Navil Perez <[email protected]>
Commit-Queue: Andrew Lamb <[email protected]>
Auto-Submit: Andrew Lamb <[email protected]>
diff --git a/src/go.chromium.org/chromiumos/test/go.mod b/src/go.chromium.org/chromiumos/test/go.mod
index 70013c8..6fde125 100644
--- a/src/go.chromium.org/chromiumos/test/go.mod
+++ b/src/go.chromium.org/chromiumos/test/go.mod
@@ -27,7 +27,7 @@
 	github.com/pkg/errors v0.9.1
 	github.com/smartystreets/goconvey v1.8.1
 	go.chromium.org/chromiumos/config/go v0.0.0-20240309015314-b8a183866804
-	go.chromium.org/chromiumos/infra/proto/go v0.0.0-20240530000842-7d34be97f98c
+	go.chromium.org/chromiumos/infra/proto/go v0.0.0-20250102200227-b13b715cea73
 	go.chromium.org/chromiumos/lro v0.0.0-00010101000000-000000000000
 	go.chromium.org/luci v0.0.0-20240531181147-0c7c729b2fcf
 	go.chromium.org/tast v0.0.0-00010101000000-000000000000
diff --git a/src/go.chromium.org/chromiumos/test/go.sum b/src/go.chromium.org/chromiumos/test/go.sum
index 2ddab89..8b03e60 100644
--- a/src/go.chromium.org/chromiumos/test/go.sum
+++ b/src/go.chromium.org/chromiumos/test/go.sum
@@ -216,6 +216,8 @@
 go.chromium.org/chromiumos/config/go/src/go.chromium.org/chromiumos/config/go v0.0.0-20241119011516-a08b3fa78d0c/go.mod h1:JR2kUZJhcMXE7Uw+DZIWn6+a4P21Rutfiak7MGK2gQY=
 go.chromium.org/chromiumos/infra/proto/go v0.0.0-20240530000842-7d34be97f98c h1:I+jrUdSw57R07U0r4Y2BU3YD20IAuyY1DEs3x0GDkvw=
 go.chromium.org/chromiumos/infra/proto/go v0.0.0-20240530000842-7d34be97f98c/go.mod h1:FA/cWXnIm9fHk7ZY1UX9yvquxllhmGuyOoaf6xmgT3k=
+go.chromium.org/chromiumos/infra/proto/go v0.0.0-20250102200227-b13b715cea73 h1:9NvAuiyGKFuxD1JUNGqTsGmMRc+8saopsUOLABp7h7U=
+go.chromium.org/chromiumos/infra/proto/go v0.0.0-20250102200227-b13b715cea73/go.mod h1:FA/cWXnIm9fHk7ZY1UX9yvquxllhmGuyOoaf6xmgT3k=
 go.chromium.org/chromiumos/platform/dev-util/src/go.chromium.org/chromiumos/lro v0.0.0-20241119133351-1b564358cf15 h1:MX0tqVjChbFT5WwptXLUXF7EoaaC1zgJXV1fpzLOCX0=
 go.chromium.org/chromiumos/platform/dev-util/src/go.chromium.org/chromiumos/lro v0.0.0-20241119133351-1b564358cf15/go.mod h1:tSvAX6a7PcR9fZ6i5Q9a16QN/LMhjeGH4qnKnIeDhHY=
 go.chromium.org/luci v0.0.0-20240531181147-0c7c729b2fcf h1:kQpts0oB2O6TJtQMgzM7K0+G4DY9dYRDxWNNMErCO4Q=
diff --git a/src/go.chromium.org/chromiumos/test/plan/internal/compatibility/convertv1.go b/src/go.chromium.org/chromiumos/test/plan/internal/compatibility/convertv1.go
index 5fb55f7..26f342f 100644
--- a/src/go.chromium.org/chromiumos/test/plan/internal/compatibility/convertv1.go
+++ b/src/go.chromium.org/chromiumos/test/plan/internal/compatibility/convertv1.go
@@ -13,15 +13,15 @@
 	"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/test_platform"
 	"go.chromium.org/chromiumos/infra/proto/go/testplans"
+	"go.chromium.org/chromiumos/test/plan/internal/compatibility/priority"
 	bbpb "go.chromium.org/luci/buildbucket/proto"
 	"google.golang.org/protobuf/types/known/structpb"
 	"google.golang.org/protobuf/types/known/wrapperspb"
@@ -84,9 +84,11 @@
 	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 {
+// getFreeformAttributes returns the criteria that don't match any of
+// validAttrs, converted to FreeformAttributes
+func getFreeformAttributes(criteria []*testpb.DutCriterion, validAttrs ...*testpb.DutAttribute) (*test_platform.Request_Params_FreeformAttributes, error) {
+	freeformAttributes := &test_platform.Request_Params_FreeformAttributes{}
+
 	for _, criterion := range criteria {
 		matches := false
 		for _, attr := range validAttrs {
@@ -96,11 +98,17 @@
 		}
 
 		if !matches {
-			return fmt.Errorf("criterion %q doesn't match any valid attributes (%q)", criterion, validAttrs)
+			if len(criterion.GetValues()) != 1 {
+				return nil, fmt.Errorf("only DutCriterion with exactly one value supported, got %q", criterion)
+			}
+			freeformAttributes.SwarmingDimensions = append(freeformAttributes.SwarmingDimensions, fmt.Sprintf(
+				"%s:%s", criterion.GetAttributeId().GetValue(),
+				criterion.GetValues()[0],
+			))
 		}
 	}
 
-	return nil
+	return freeformAttributes, nil
 }
 
 // sortedValuesFromMap returns the values from m as a list, sorted by the keys
@@ -458,6 +466,9 @@
 	totalShards int64
 	// optional, if true then run test suites in this rule via CFT workflow.
 	runViaCft bool
+	// optional, any unexpected DutCriteria are passed through as
+	// FreeformAttributes.
+	freeformAttributes *test_platform.Request_Params_FreeformAttributes
 }
 
 // getBuildTarget returns the build target for the suiteInfo. If boardVariant is
@@ -549,11 +560,13 @@
 
 	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 {
+	freeformAttributes, err := getFreeformAttributes(dutTarget.GetCriteria(), poolAttr, programAttr, designAttr, licenseAttr)
+	if err != nil {
 		return nil, err
 	}
+	if len(freeformAttributes.GetSwarmingDimensions()) > 0 {
+		glog.Infof("Passing additional DutCriteria as FreeformAttributes: %s", freeformAttributes)
+	}
 
 	pools, err := getAttrFromCriteria(dutTarget.GetCriteria(), poolAttr)
 	if err != nil {
@@ -687,18 +700,19 @@
 				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,
+						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,
+						freeformAttributes: freeformAttributes,
 					})
 			}
 		case *testpb.TestSuite_TestCaseTagCriteria_:
@@ -724,19 +738,20 @@
 
 			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(),
+					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(),
+					freeformAttributes: freeformAttributes,
 				})
 		default:
 			return nil, fmt.Errorf("TestSuite spec type %T is not supported", spec)
@@ -1044,10 +1059,11 @@
 							Value: suiteInfo.critical,
 						},
 					},
-					RunViaCft:   suiteInfo.runViaCft,
-					TagCriteria: suiteInfo.tagCriteria,
-					TotalShards: suiteInfo.totalShards,
-					Companions:  suiteInfo.companions,
+					RunViaCft:          suiteInfo.runViaCft,
+					TagCriteria:        suiteInfo.tagCriteria,
+					TotalShards:        suiteInfo.totalShards,
+					Companions:         suiteInfo.companions,
+					FreeformAttributes: suiteInfo.freeformAttributes,
 				}
 
 				if _, found := hwTests[displayName]; found {
diff --git a/src/go.chromium.org/chromiumos/test/plan/internal/compatibility/convertv1_test.go b/src/go.chromium.org/chromiumos/test/plan/internal/compatibility/convertv1_test.go
index 967b21d..2b15a75 100644
--- a/src/go.chromium.org/chromiumos/test/plan/internal/compatibility/convertv1_test.go
+++ b/src/go.chromium.org/chromiumos/test/plan/internal/compatibility/convertv1_test.go
@@ -8,15 +8,15 @@
 	"regexp"
 	"testing"
 
-	"go.chromium.org/chromiumos/test/plan/internal/compatibility"
-
 	"github.com/golang/protobuf/proto"
 	"github.com/google/go-cmp/cmp"
 	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/test_platform"
 	"go.chromium.org/chromiumos/infra/proto/go/testplans"
+	"go.chromium.org/chromiumos/test/plan/internal/compatibility"
 	bbpb "go.chromium.org/luci/buildbucket/proto"
 	"google.golang.org/protobuf/testing/protocmp"
 	"google.golang.org/protobuf/types/known/structpb"
@@ -88,6 +88,12 @@
 								},
 								Values: []string{"DUT_POOL_QUOTA"},
 							},
+							{
+								AttributeId: &testpb.DutAttribute_Id{
+									Value: "extradim",
+								},
+								Values: []string{"extraval"},
+							},
 						},
 					},
 				},
@@ -914,6 +920,9 @@
 							Suite:       "suite1",
 							SkylabBoard: "boardA",
 							Pool:        "DUT_POOL_QUOTA",
+							FreeformAttributes: &test_platform.Request_Params_FreeformAttributes{
+								SwarmingDimensions: []string{"extradim:extraval"},
+							},
 						},
 						{
 							Common: &testplans.TestSuiteCommon{
@@ -923,6 +932,9 @@
 							Suite:       "suite2",
 							SkylabBoard: "boardA",
 							Pool:        "DUT_POOL_QUOTA",
+							FreeformAttributes: &test_platform.Request_Params_FreeformAttributes{
+								SwarmingDimensions: []string{"extradim:extraval"},
+							},
 						},
 						{
 							Common: &testplans.TestSuiteCommon{
@@ -934,9 +946,10 @@
 								Tags:        []string{`"group:somegroup"`},
 								TagExcludes: []string{"informational"},
 							},
-							TotalShards: 1,
-							SkylabBoard: "boardA",
-							Pool:        "DUT_POOL_MULTI_DUT",
+							TotalShards:        1,
+							SkylabBoard:        "boardA",
+							Pool:               "DUT_POOL_MULTI_DUT",
+							FreeformAttributes: &test_platform.Request_Params_FreeformAttributes{},
 							Companions: []*testplans.TestCompanion{
 								{
 									Board: "boardCompanionA",
@@ -970,7 +983,8 @@
 								lab.LicenseType_LICENSE_TYPE_WINDOWS_10_PRO,
 								lab.LicenseType_LICENSE_TYPE_MS_OFFICE_STANDARD,
 							},
-							Pool: "DUT_POOL_QUOTA",
+							Pool:               "DUT_POOL_QUOTA",
+							FreeformAttributes: &test_platform.Request_Params_FreeformAttributes{},
 						},
 					},
 				},
@@ -996,11 +1010,12 @@
 								DisplayName: "cq-builderA-kernelnext.model1.hw.suite-with-board-variant",
 								Critical:    wrapperspb.Bool(false),
 							},
-							Suite:       "suite-with-board-variant",
-							SkylabBoard: "boardA",
-							SkylabModel: "model1",
-							Pool:        "DUT_POOL_QUOTA",
-							RunViaCft:   true,
+							Suite:              "suite-with-board-variant",
+							SkylabBoard:        "boardA",
+							SkylabModel:        "model1",
+							Pool:               "DUT_POOL_QUOTA",
+							RunViaCft:          true,
+							FreeformAttributes: &test_platform.Request_Params_FreeformAttributes{},
 						},
 					},
 				},
@@ -1026,10 +1041,11 @@
 								DisplayName: "cq-builderA-asan.hw.asan-suite",
 								Critical:    wrapperspb.Bool(true),
 							},
-							Suite:       "asan-suite",
-							SkylabBoard: "boardA",
-							Pool:        "DUT_POOL_QUOTA",
-							RunViaCft:   true,
+							Suite:              "asan-suite",
+							SkylabBoard:        "boardA",
+							Pool:               "DUT_POOL_QUOTA",
+							RunViaCft:          true,
+							FreeformAttributes: &test_platform.Request_Params_FreeformAttributes{},
 						},
 					},
 				},
@@ -1292,50 +1308,6 @@
 			errRegexp:        "only DutCriterion with at least one value supported",
 		},
 		{
-			name:        "invalid DUT attribute",
-			vmTestPlans: vmTestPlans,
-			hwTestPlans: []*test_api_v1.HWTestPlan{
-				{
-					CoverageRules: []*testpb.CoverageRule{
-						{
-							DutTargets: []*testpb.DutTarget{
-								{
-									Criteria: []*testpb.DutCriterion{
-										{
-											AttributeId: &testpb.DutAttribute_Id{
-												Value: "attr-program",
-											},
-											Values: []string{"programA"},
-										},
-										{
-											AttributeId: &testpb.DutAttribute_Id{
-												Value: "swarming-pool",
-											},
-											Values: []string{"DUT_POOL_QUOTA"},
-										},
-										{
-											AttributeId: &testpb.DutAttribute_Id{
-												Value: "attr-design",
-											},
-											Values: []string{"model1"},
-										},
-										{
-											AttributeId: &testpb.DutAttribute_Id{
-												Value: "fp",
-											},
-											Values: []string{"fp1"},
-										},
-									},
-								},
-							},
-						},
-					},
-				},
-			},
-			dutAttributeList: dutAttributeList,
-			errRegexp:        "criterion .+ doesn't match any valid attributes",
-		},
-		{
 			name:        "multiple pool values",
 			vmTestPlans: vmTestPlans,
 			hwTestPlans: []*test_api_v1.HWTestPlan{