| // Copyright 2020 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 runtimeprobe provides utilities for runtime_probe tests. |
| package runtimeprobe |
| |
| import ( |
| "context" |
| "encoding/json" |
| "regexp" |
| "sort" |
| "strings" |
| |
| "github.com/godbus/dbus" |
| "github.com/golang/protobuf/proto" |
| |
| rppb "chromiumos/system_api/runtime_probe_proto" |
| "chromiumos/tast/errors" |
| "chromiumos/tast/local/dbusutil" |
| "chromiumos/tast/local/sysutil" |
| "chromiumos/tast/local/upstart" |
| "chromiumos/tast/testing" |
| ) |
| |
| // Component represents runtime_probe component interface. |
| type Component interface { |
| GetName() string |
| GetInformation() *rppb.Information |
| } |
| |
| // GetComponentsFunc represents the function to get specific category of |
| // components from rppb.ProbeResult. |
| type GetComponentsFunc func(result *rppb.ProbeResult, category string) ([]Component, error) |
| |
| // Skip the known concurrent D-Bus call at boot. |
| const defaultTryCount = 2 |
| |
| // dbusCall invokes runtime_probe methods via D-Bus with given input protobuf |
| // |in|. If the method called successfully, |out| will be set to the replied |
| // message and return without errors. Otherwise an error will be returned. |
| func dbusCall(ctx context.Context, method string, in, out proto.Message) error { |
| const ( |
| jobName = "runtime_probe" |
| dbusName = "org.chromium.RuntimeProbe" |
| dbusPath = "/org/chromium/RuntimeProbe" |
| dbusInterface = "org.chromium.RuntimeProbe" |
| ) |
| var dbusMethod = dbusInterface + "." + method |
| |
| if err := upstart.EnsureJobRunning(ctx, jobName); err != nil { |
| return errors.Wrap(err, "runtime probe is not running") |
| } |
| defer upstart.StopJob(ctx, jobName) |
| |
| conn, obj, err := dbusutil.ConnectPrivateWithAuth(ctx, sysutil.ChronosUID, dbusName, dbus.ObjectPath(dbusPath)) |
| if err != nil { |
| return err |
| } |
| defer conn.Close() |
| |
| if err := dbusutil.CallProtoMethod(ctx, obj, dbusMethod, in, out); err != nil { |
| return errors.Wrapf(err, "failed to call method %s", dbusMethod) |
| } |
| return nil |
| } |
| |
| // dbusCallWithRetry wraps dbusCall with retries which try to skip known error |
| // caused by concurrent D-Bus calls. |
| func dbusCallWithRetry(ctx context.Context, method string, in, out proto.Message, tryCount int) error { |
| var err error |
| for i := 0; i < tryCount; i++ { |
| if err = dbusCall(ctx, method, in, out); err == nil { |
| return nil |
| } |
| } |
| return errors.Wrapf(err, "retry failed for %d times", tryCount) |
| } |
| |
| // probe uses D-Bus call to get result from runtime_probe with given request. |
| // Currently only users chronos and debugd are allowed to call this D-Bus function. |
| func probe(ctx context.Context, request *rppb.ProbeRequest) (*rppb.ProbeResult, error) { |
| result := &rppb.ProbeResult{} |
| err := dbusCallWithRetry(ctx, "ProbeCategories", request, result, defaultTryCount) |
| return result, err |
| } |
| |
| // hostInfoLabels get host info labels for tast tests. If the tast variable |
| // is not found or is an invalid JSON string, this function returns an |
| // error. |
| func hostInfoLabels(s *testing.State) ([]string, error) { |
| labelsStr, ok := s.Var("autotest_host_info_labels") |
| if !ok { |
| return nil, errors.New("no labels") |
| } |
| |
| var labels []string |
| if err := json.Unmarshal([]byte(labelsStr), &labels); err != nil { |
| return nil, err |
| } |
| return labels, nil |
| } |
| |
| // modelName returns the model name from |labels|. |
| func modelName(labels []string) (string, error) { |
| const modelPrefix = "model:" |
| |
| for _, label := range labels { |
| if strings.HasPrefix(label, modelPrefix) { |
| model := strings.TrimPrefix(label, modelPrefix) |
| return model, nil |
| } |
| } |
| return "", errors.New("no model label found") |
| } |
| |
| // categoryAliases returns the aliases of given category. The category |
| // "video" is a legacy usage of "camera" category and both follow the |
| // {category}_{cid}_{qid} name policy. |
| func categoryAliases(category string) []string { |
| if category == "camera" { |
| return []string{"camera", "video"} |
| } |
| return []string{category} |
| } |
| |
| // tryTrimQid tries to trim the "_{qid}" suffix in the component name and |
| // append a fixed string "_{Any}" because tast tests do not care about the |
| // mutable fields (e.g. firmware) which usually differ in qid but just make |
| // sure hardware components are probed by probe configs. |
| func tryTrimQid(model, category, compName string) string { |
| aliases := categoryAliases(category) |
| aliasPatterns := make([]string, 0, len(aliases)) |
| for _, alias := range aliases { |
| aliasPatterns = append(aliasPatterns, regexp.QuoteMeta(alias)) |
| } |
| pattern := regexp.MustCompile("^(" + regexp.QuoteMeta(model) + "_" + "(?:" + strings.Join(aliasPatterns, "|") + ")" + `_\d+)_\d+(?:#.*)?$`) |
| if matches := pattern.FindStringSubmatch(compName); len(matches) > 0 { |
| return matches[1] + "_{Any}" |
| } |
| return compName |
| } |
| |
| // collectKnownComponents uses D-Bus call to get known components with category |
| // |category|. |
| func collectKnownComponents(ctx context.Context, model, category string, tryCount int) (map[string]struct{}, error) { |
| categoryValue, found := rppb.ProbeRequest_SupportCategory_value[category] |
| if !found { |
| return nil, errors.Errorf("invalid category %q", category) |
| } |
| request := rppb.GetKnownComponentsRequest{ |
| Category: rppb.ProbeRequest_SupportCategory(categoryValue), |
| } |
| result := rppb.GetKnownComponentsResult{} |
| err := dbusCallWithRetry(ctx, "GetKnownComponents", &request, &result, tryCount) |
| if err != nil { |
| return nil, err |
| } |
| var components = make(map[string]struct{}) |
| for _, name := range result.GetComponentNames() { |
| trimmedName := tryTrimQid(model, category, name) |
| components[trimmedName] = struct{}{} |
| } |
| return components, nil |
| } |
| |
| // countComponents returns a counter counting each component label which is |
| // available on device |model|. |
| func countComponents(labels []string, category, model string, knownComponents map[string]struct{}) map[string]int { |
| count := make(map[string]int) |
| // Filter labels with prefix "hwid_component:<component type>/" and trim them. |
| for _, alias := range categoryAliases(category) { |
| categoryPrefix := "hwid_component:" + alias + "/" |
| for _, label := range labels { |
| if !strings.HasPrefix(label, categoryPrefix) { |
| continue |
| } |
| label := strings.TrimPrefix(label, categoryPrefix) |
| key := tryTrimQid(model, category, model+"_"+label) |
| if _, found := knownComponents[key]; found { |
| count[key]++ |
| } |
| } |
| } |
| return count |
| } |
| |
| // decreaseComponentCount decreases the count of given component by 1. If the |
| // count of given component is decreased to 0, it will be removed from |count|. |
| // The first returned value will be false on failure. The second returned |
| // value is the display name of |component|. |
| func decreaseComponentCount(count map[string]int, model, category string, component Component) (bool, string) { |
| name := component.GetName() |
| info := component.GetInformation() |
| if info != nil { |
| if compGroup := info.GetCompGroup(); compGroup != "" { |
| name = model + "_" + compGroup |
| } |
| } |
| if name == "generic" { |
| return false, name |
| } |
| trimmedName := tryTrimQid(model, category, name) |
| if _, exists := count[trimmedName]; !exists { |
| return false, name |
| } |
| count[trimmedName]-- |
| if count[trimmedName] == 0 { |
| delete(count, trimmedName) |
| } |
| return true, name |
| } |
| |
| // GenericTest probes components with category |categories| on a device using |
| // Runtime Probe D-Bus call and checks if the result matches the host info |
| // labels. |
| // If there is not a valid component label, the test will be skipped (pass). |
| // If a valid component in the host info labels is not probed, the test will |
| // fail. If a probed component is not in the host info labels, the test will |
| // fail if |allowExtraComponents| is false. Otherwise, the test will pass. |
| func GenericTest(ctx context.Context, s *testing.State, categories []string, getComponents GetComponentsFunc, allowExtraComponents bool) { |
| hostInfoLabels, err := hostInfoLabels(s) |
| if err != nil { |
| s.Fatal("hostInfoLabels failed: ", err) |
| } |
| |
| model, err := modelName(hostInfoLabels) |
| if err != nil { |
| s.Fatal("modelName failed: ", err) |
| } |
| |
| mapping := make(map[string]map[string]int) |
| var requestCategories []rppb.ProbeRequest_SupportCategory |
| for _, category := range categories { |
| categoryValue, found := rppb.ProbeRequest_SupportCategory_value[category] |
| if !found { |
| s.Fatalf("Invalid category %q", category) |
| } |
| |
| knownComponents, err := collectKnownComponents(ctx, model, category, defaultTryCount) |
| if err != nil { |
| s.Fatal("collectKnownComponents failed: ", err) |
| } |
| // The probe config on the DUT doesn't have any components with |
| // category |category|. |
| if len(knownComponents) == 0 { |
| s.Logf("%q components are not found in the probe config. Skipped", category) |
| continue |
| } |
| |
| count := countComponents(hostInfoLabels, category, model, knownComponents) |
| // Waived components are defined in |waived_comp_categories| on the |
| // HWID service: |
| // platform/factory-private/config/hwid/service/appengine/configurations.yaml |
| // If a component in the dut labels is not found in the probe config on |
| // the DUT, countComponents will also ignore it. |
| if len(count) == 0 { |
| s.Logf("No %q labels or known components in the labels. Skipped", category) |
| continue |
| } |
| mapping[category] = count |
| requestCategories = append(requestCategories, rppb.ProbeRequest_SupportCategory(categoryValue)) |
| } |
| |
| request := &rppb.ProbeRequest{ |
| Categories: requestCategories, |
| } |
| result, err := probe(ctx, request) |
| if err != nil { |
| s.Fatal("probe failed: ", err) |
| } |
| |
| for category, compCounts := range mapping { |
| probedComponents, err := getComponents(result, category) |
| var extraComponents []string |
| if err != nil { |
| s.Error("getComponents failed: ", err) |
| continue |
| } |
| for _, component := range probedComponents { |
| result, name := decreaseComponentCount(compCounts, model, category, component) |
| s.Logf("Probed %s component: %s", category, name) |
| if !result && name != "generic" { |
| extraComponents = append(extraComponents, category+"/"+name) |
| } |
| } |
| |
| var unprobedComponents []string |
| for name := range compCounts { |
| unprobedComponents = append(unprobedComponents, category+"/"+name) |
| } |
| if len(unprobedComponents) > 0 { |
| sort.Strings(unprobedComponents) |
| s.Fatalf("Some expected %s components are not probed: %v", category, unprobedComponents) |
| } |
| |
| if len(extraComponents) > 0 { |
| sort.Strings(extraComponents) |
| if allowExtraComponents { |
| s.Logf("Some extra %s components are probed: %v", category, extraComponents) |
| } else { |
| s.Fatalf("Some extra %s components are probed: %v", category, extraComponents) |
| } |
| } |
| } |
| } |