blob: a544ac0446ff8445f600a915c30b23b8a07a105e [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package starlark
import (
"context"
"fmt"
"path/filepath"
buildpb "go.chromium.org/chromiumos/config/go/build/api"
"go.chromium.org/chromiumos/config/go/payload"
test_api_v1 "go.chromium.org/chromiumos/config/go/test/api/v1"
"go.chromium.org/chromiumos/config/go/test/plan"
"go.chromium.org/luci/starlark/interpreter"
"go.chromium.org/luci/starlark/starlarkproto"
"go.starlark.net/starlark"
"go.starlark.net/starlarkstruct"
"google.golang.org/protobuf/proto"
)
// protoAccessorBuiltin returns a Builtin that provides access to Message m.
func protoAccessorBuiltin(
protoLoader *starlarkproto.Loader,
name string,
m proto.Message,
) *starlark.Builtin {
return accessorBuiltin(name, protoLoader.MessageType(m.ProtoReflect().Descriptor()).MessageFromProto(m))
}
// accessorBuiltin returns a Builtin that provides access to v.
func accessorBuiltin(name string, v starlark.Value) *starlark.Builtin {
return starlark.NewBuiltin(
name,
func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
// Check that no args are passed.
err := starlark.UnpackArgs(fn.Name(), args, kwargs)
if err != nil {
return nil, err
}
return v, nil
},
)
}
// starlarkValueToProto converts value to proto Message m. An error is returned
// if value is not a starlarkproto.Message, with a protoreflect.FullName that is
// exactly the same as the protoreflect.FullName of m.
func starlarkValueToProto(value starlark.Value, m proto.Message) error {
mName := m.ProtoReflect().Descriptor().FullName()
// Assert value is a starlarkproto.Message.
starlarkMessage, ok := value.(*starlarkproto.Message)
if !ok {
return fmt.Errorf("arg must be a %s, got %q", mName, value)
}
// It is not possible to use type assertions to convert the
// starlarkproto.Message to the concrete proto type, so marshal it to bytes
// and then unmarshal as the concrete proto type.
//
// First check that the full name of the message passed in exactly
// matches the full name of m, to avoid confusing errors
// from unmarshalling.
starlarkProto := starlarkMessage.ToProto()
if mName != starlarkProto.ProtoReflect().Descriptor().FullName() {
return fmt.Errorf("arg must be a %s, got %q", mName, starlarkProto)
}
bytes, err := proto.Marshal(starlarkMessage.ToProto())
if err != nil {
return err
}
if err := proto.Unmarshal(bytes, m); err != nil {
return err
}
return nil
}
// buildAddHWTestPlanBuiltin returns a Builtin that takes a single HWTestPlan
// and adds it to result.
func buildAddHWTestPlanBuiltin(result *[]*test_api_v1.HWTestPlan) *starlark.Builtin {
return starlark.NewBuiltin(
"add_hw_test_plan",
func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var starlarkValue starlark.Value
if err := starlark.UnpackArgs(fn.Name(), args, kwargs, "hw_test_plan", &starlarkValue); err != nil {
return nil, err
}
hwTestPlan := &test_api_v1.HWTestPlan{}
if err := starlarkValueToProto(starlarkValue, hwTestPlan); err != nil {
return nil, fmt.Errorf("%s: %w", fn.Name(), err)
}
*result = append(*result, hwTestPlan)
return starlark.None, nil
},
)
}
// buildAddVMTestPlanBuiltin returns a Builtin that takes a single HWTestPlan
// and adds it to result.
func buildAddVMTestPlanBuiltin(result *[]*test_api_v1.VMTestPlan) *starlark.Builtin {
return starlark.NewBuiltin(
"add_vm_test_plan",
func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var starlarkValue starlark.Value
if err := starlark.UnpackArgs(fn.Name(), args, kwargs, "vm_test_plan", &starlarkValue); err != nil {
return nil, err
}
vmTestPlan := &test_api_v1.VMTestPlan{}
if err := starlarkValueToProto(starlarkValue, vmTestPlan); err != nil {
return nil, fmt.Errorf("%s: %w", fn.Name(), err)
}
*result = append(*result, vmTestPlan)
return starlark.None, nil
},
)
}
// ExecTestPlan executes the Starlark file planFilename.
//
// Builtins are provided to planFilename to access buildMetadataList and
// configBundleList, and add [VM,HW]TestPlans to the output.
//
// A loader is provided to load proto constructors.
//
// If templateParameters is non-nil, builtins are provided to access its fields.
// This function does not check that the suite_name field is used properly to
// prevent name collisions, this is the responsibility of the caller when there
// are multiple templateParameters for the same file.
func ExecTestPlan(
ctx context.Context,
planFilename string,
buildMetadataList *buildpb.SystemImage_BuildMetadataList,
configBundleList *payload.ConfigBundleList,
templateParameters *plan.SourceTestPlan_TestPlanStarlarkFile_TemplateParameters,
) ([]*test_api_v1.HWTestPlan, []*test_api_v1.VMTestPlan, error) {
protoLoader, err := buildProtoLoader()
if err != nil {
return nil, nil, err
}
getBuildMetadataBuiltin := protoAccessorBuiltin(
protoLoader, "get_build_metadata", buildMetadataList,
)
getFlatConfigListBuiltin := protoAccessorBuiltin(
protoLoader, "get_config_bundle_list", configBundleList,
)
// If templateParameters is non-nil, provide builtins to access tagCriteria
// and suiteName. Otherwise, the builtins will return errors.
var getTagCriteriaBuiltin *starlark.Builtin
var getSuiteNameBuiltin *starlark.Builtin
var getProgramBuiltin *starlark.Builtin
getTagCriteriaBuiltinName := "get_tag_criteria"
getSuiteNameBuiltinName := "get_suite_name"
getProgramBuiltinName := "get_program"
if templateParameters != nil {
getTagCriteriaBuiltin = protoAccessorBuiltin(
protoLoader, getTagCriteriaBuiltinName, templateParameters.GetTagCriteria(),
)
getSuiteNameBuiltin = accessorBuiltin(
getSuiteNameBuiltinName, starlark.String(templateParameters.GetSuiteName()),
)
getProgramBuiltin = accessorBuiltin(getProgramBuiltinName, starlark.String(templateParameters.GetProgram()))
} else {
getTagCriteriaBuiltin = starlark.NewBuiltin(
getTagCriteriaBuiltinName,
func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return nil, fmt.Errorf(
"%s: no TestCaseTagCriteria available in this Starlark execution, was it specified on the interpreter command line?",
getTagCriteriaBuiltinName,
)
},
)
getSuiteNameBuiltin = starlark.NewBuiltin(
getSuiteNameBuiltinName,
func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return nil, fmt.Errorf(
"%s: no test suite name available in this Starlark execution, was it specified on the interpreter command line?",
getSuiteNameBuiltinName,
)
},
)
getProgramBuiltin = starlark.NewBuiltin(
getProgramBuiltinName,
func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
return nil, fmt.Errorf(
"%s: no program available in this Starlark execution, was it specified on the interpreter command line?",
getProgramBuiltinName,
)
},
)
}
var hw_test_plans []*test_api_v1.HWTestPlan
addHWTestPlanBuiltin := buildAddHWTestPlanBuiltin(&hw_test_plans)
var vm_test_plans []*test_api_v1.VMTestPlan
addVMTestPlanBuiltin := buildAddVMTestPlanBuiltin(&vm_test_plans)
testplanModule := &starlarkstruct.Module{
Name: "testplan",
Members: starlark.StringDict{
getBuildMetadataBuiltin.Name(): getBuildMetadataBuiltin,
getFlatConfigListBuiltin.Name(): getFlatConfigListBuiltin,
getTagCriteriaBuiltin.Name(): getTagCriteriaBuiltin,
getSuiteNameBuiltin.Name(): getSuiteNameBuiltin,
getProgramBuiltin.Name(): getProgramBuiltin,
addHWTestPlanBuiltin.Name(): addHWTestPlanBuiltin,
addVMTestPlanBuiltin.Name(): addVMTestPlanBuiltin,
},
}
// The directory of planFilename is set as the main package for the
// interpreter to run.
planDir, planBasename := filepath.Split(planFilename)
pkgs := map[string]interpreter.Loader{
interpreter.MainPkg: interpreter.FileSystemLoader(planDir),
}
// Create a loader for proto constructors, using protoLoader. The paths are
// based on the descriptors in protoLoader, i.e. the Starlark code will look
// like
// `load('@proto//chromiumos/test/api/v1/plan.proto', plan_pb = 'chromiumos.test.api.v1')`
pkgs["proto"] = func(path string) (dict starlark.StringDict, src string, err error) {
mod, err := protoLoader.Module(path)
if err != nil {
return nil, "", err
}
return starlark.StringDict{mod.Name: mod}, "", nil
}
// Init the interpreter and execute it on planBasename.
intr := interpreter.Interpreter{
Predeclared: starlark.StringDict{
testplanModule.Name: testplanModule,
// Add the "struct" builtin as a convenience.
starlarkstruct.Default.GoString(): starlark.NewBuiltin(
starlarkstruct.Default.GoString(), starlarkstruct.Make,
),
},
Packages: pkgs,
}
if err := intr.Init(ctx); err != nil {
return nil, nil, err
}
if _, err := intr.ExecModule(ctx, interpreter.MainPkg, planBasename); err != nil {
return nil, nil, fmt.Errorf("failed executing Starlark file %q: %w", planFilename, err)
}
return hw_test_plans, vm_test_plans, nil
}