go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/testresults/test_result.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package testresults
    16  
    17  import (
    18  	"context"
    19  	"net/url"
    20  	"strings"
    21  	"time"
    22  
    23  	"cloud.google.com/go/spanner"
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/protobuf/proto"
    26  	"google.golang.org/protobuf/types/known/durationpb"
    27  	"google.golang.org/protobuf/types/known/structpb"
    28  
    29  	"go.chromium.org/luci/common/errors"
    30  	"go.chromium.org/luci/grpc/appstatus"
    31  	"go.chromium.org/luci/resultdb/internal/invocations"
    32  	"go.chromium.org/luci/resultdb/internal/spanutil"
    33  	pb "go.chromium.org/luci/resultdb/proto/v1"
    34  )
    35  
    36  // MustParseName retrieves the invocation ID, unescaped test id, and
    37  // result ID.
    38  //
    39  // Panics if the name is invalid. Should be used only with trusted data.
    40  //
    41  // MustParseName is faster than pbutil.ParseTestResultName.
    42  func MustParseName(name string) (invID invocations.ID, testID, resultID string) {
    43  	parts := strings.Split(name, "/")
    44  	if len(parts) != 6 || parts[0] != "invocations" || parts[2] != "tests" || parts[4] != "results" {
    45  		panic(errors.Reason("malformed test result name: %q", name).Err())
    46  	}
    47  
    48  	invID = invocations.ID(parts[1])
    49  	testID = parts[3]
    50  	resultID = parts[5]
    51  
    52  	unescaped, err := url.PathUnescape(testID)
    53  	if err != nil {
    54  		panic(errors.Annotate(err, "malformed test id %q", testID).Err())
    55  	}
    56  	testID = unescaped
    57  
    58  	return
    59  }
    60  
    61  // Read reads specified TestResult within the transaction.
    62  // If the TestResult does not exist, the returned error is annotated with
    63  // NotFound GRPC code.
    64  func Read(ctx context.Context, name string) (*pb.TestResult, error) {
    65  	invID, testID, resultID := MustParseName(name)
    66  	tr := &pb.TestResult{
    67  		Name:     name,
    68  		TestId:   testID,
    69  		ResultId: resultID,
    70  		Expected: true,
    71  	}
    72  
    73  	var maybeUnexpected spanner.NullBool
    74  	var micros spanner.NullInt64
    75  	var summaryHTML spanutil.Compressed
    76  	var tmd spanutil.Compressed
    77  	var fr spanutil.Compressed
    78  	var properties spanutil.Compressed
    79  	var skipReason spanner.NullInt64
    80  	err := spanutil.ReadRow(ctx, "TestResults", invID.Key(testID, resultID), map[string]any{
    81  		"Variant":         &tr.Variant,
    82  		"VariantHash":     &tr.VariantHash,
    83  		"IsUnexpected":    &maybeUnexpected,
    84  		"Status":          &tr.Status,
    85  		"SummaryHTML":     &summaryHTML,
    86  		"StartTime":       &tr.StartTime,
    87  		"RunDurationUsec": &micros,
    88  		"Tags":            &tr.Tags,
    89  		"TestMetadata":    &tmd,
    90  		"FailureReason":   &fr,
    91  		"Properties":      &properties,
    92  		"SkipReason":      &skipReason,
    93  	})
    94  	switch {
    95  	case spanner.ErrCode(err) == codes.NotFound:
    96  		return nil, appstatus.Attachf(err, codes.NotFound, "%s not found", name)
    97  
    98  	case err != nil:
    99  		return nil, errors.Annotate(err, "failed to fetch %q", name).Err()
   100  	}
   101  
   102  	tr.SummaryHtml = string(summaryHTML)
   103  	PopulateExpectedField(tr, maybeUnexpected)
   104  	PopulateDurationField(tr, micros)
   105  	PopulateSkipReasonField(tr, skipReason)
   106  	if err := populateTestMetadata(tr, tmd); err != nil {
   107  		return nil, errors.Annotate(err, "failed to unmarshal test metadata").Err()
   108  	}
   109  	if err := populateFailureReason(tr, fr); err != nil {
   110  		return nil, errors.Annotate(err, "failed to unmarshal failure reason").Err()
   111  	}
   112  	if err := populateProperties(tr, properties); err != nil {
   113  		return nil, errors.Annotate(err, "failed to unmarshal properties").Err()
   114  	}
   115  	return tr, nil
   116  }
   117  
   118  // PopulateDurationField populates tr.Duration from NullInt64.
   119  func PopulateDurationField(tr *pb.TestResult, micros spanner.NullInt64) {
   120  	tr.Duration = nil
   121  	if micros.Valid {
   122  		tr.Duration = durationpb.New(time.Duration(1000 * micros.Int64))
   123  	}
   124  }
   125  
   126  // PopulateSkipReasonField populates tr.SkipReason from NullInt64.
   127  func PopulateSkipReasonField(tr *pb.TestResult, skipReason spanner.NullInt64) {
   128  	tr.SkipReason = pb.SkipReason_SKIP_REASON_UNSPECIFIED
   129  	if skipReason.Valid {
   130  		tr.SkipReason = pb.SkipReason(skipReason.Int64)
   131  	}
   132  }
   133  
   134  // PopulateExpectedField populates tr.Expected from NullBool.
   135  func PopulateExpectedField(tr *pb.TestResult, maybeUnexpected spanner.NullBool) {
   136  	tr.Expected = !maybeUnexpected.Valid || !maybeUnexpected.Bool
   137  }
   138  
   139  func populateTestMetadata(tr *pb.TestResult, tmd spanutil.Compressed) error {
   140  	if len(tmd) == 0 {
   141  		return nil
   142  	}
   143  
   144  	tr.TestMetadata = &pb.TestMetadata{}
   145  	return proto.Unmarshal(tmd, tr.TestMetadata)
   146  }
   147  
   148  func populateFailureReason(tr *pb.TestResult, fr spanutil.Compressed) error {
   149  	if len(fr) == 0 {
   150  		return nil
   151  	}
   152  
   153  	tr.FailureReason = &pb.FailureReason{}
   154  	return proto.Unmarshal(fr, tr.FailureReason)
   155  }
   156  
   157  func populateProperties(tr *pb.TestResult, properties spanutil.Compressed) error {
   158  	if len(properties) == 0 {
   159  		return nil
   160  	}
   161  
   162  	tr.Properties = &structpb.Struct{}
   163  	return proto.Unmarshal(properties, tr.Properties)
   164  }