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": µs, 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 }