Skip to content

Commit

Permalink
Merge pull request #1251 from sultan-duisenbay/resultstore_custom_hea…
Browse files Browse the repository at this point in the history
…ders

add functionality to extract custom headers
  • Loading branch information
google-oss-prow[bot] authored Aug 31, 2023
2 parents 1fb8fde + 7cc07e6 commit a2c5d56
Show file tree
Hide file tree
Showing 2 changed files with 298 additions and 0 deletions.
109 changes: 109 additions & 0 deletions pkg/updater/resultstore/resultstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"context"
"fmt"
"sort"
"strings"
"time"

"github.com/GoogleCloudPlatform/testgrid/pkg/updater"
Expand Down Expand Up @@ -69,6 +70,25 @@ func (sar *singleActionResult) TargetStatus() statuspb.TestStatus {
return status
}

func (sar *singleActionResult) extractHeaders(headerConf *configpb.TestGroup_ColumnHeader) []string {
if sar == nil {
return nil
}

var headers []string

if key := headerConf.GetProperty(); key != "" {
tr := &testResult{sar.ActionProto.GetTestAction().GetTestSuite(), nil}
for _, p := range tr.properties() {
if p.GetKey() == key {
headers = append(headers, p.GetValue())
}
}
}

return headers
}

type multiActionResult struct {
TargetProto *resultstorepb.Target
ConfiguredTargetProto *resultstorepb.ConfiguredTarget
Expand All @@ -82,6 +102,29 @@ type invocation struct {
TargetResults map[string][]*singleActionResult
}

func (inv *invocation) extractHeaders(headerConf *configpb.TestGroup_ColumnHeader) []string {
if inv == nil {
return nil
}

var headers []string

if key := headerConf.GetConfigurationValue(); key != "" {
for _, prop := range inv.InvocationProto.GetProperties() {
if prop.GetKey() == key {
headers = append(headers, prop.GetValue())
}
}
} else if prefix := headerConf.GetLabel(); prefix != "" {
for _, label := range inv.InvocationProto.GetInvocationAttributes().GetLabels() {
if strings.HasPrefix(label, prefix) {
headers = append(headers, label[len(prefix):])
}
}
}
return headers
}

// extractGroupID extracts grouping ID for a results based on the testgroup grouping configuration
// Returns an empty string for no config or incorrect config
func extractGroupID(tg *configpb.TestGroup, inv *invocation) string {
Expand Down Expand Up @@ -294,6 +337,30 @@ func includeStatus(tg *configpb.TestGroup, sar *singleActionResult) bool {
return true
}

// testResult is a convenient representation of resultstore Test proto
// only one of those fields are set at any time for a testResult instance
type testResult struct {
suiteProto *resultstorepb.TestSuite
caseProto *resultstorepb.TestCase
}

// properties return the recursive list of properties for a particular testResult
func (t *testResult) properties() []*resultstorepb.Property {
var properties []*resultstorepb.Property
for _, p := range t.suiteProto.GetProperties() {
properties = append(properties, p)
}
for _, p := range t.caseProto.GetProperties() {
properties = append(properties, p)
}

for _, t := range t.suiteProto.GetTests() {
newTestResult := &testResult{t.GetTestSuite(), t.GetTestCase()}
properties = append(properties, newTestResult.properties()...)
}
return properties
}

// processGroup will convert grouped invocations into columns
func processGroup(tg *configpb.TestGroup, group *invocationGroup) *updater.InflatedColumn {
if group == nil || group.Invocations == nil {
Expand All @@ -310,6 +377,7 @@ func processGroup(tg *configpb.TestGroup, group *invocationGroup) *updater.Infla
groupedCells := make(map[string][]updater.Cell)

hintTime := time.Unix(0, 0)
headers := make([][]string, len(tg.GetColumnHeader()))

// extract info from underlying invocations and target results
for _, invocation := range group.Invocations {
Expand All @@ -330,6 +398,12 @@ func processGroup(tg *configpb.TestGroup, group *invocationGroup) *updater.Infla
hintTime = started.AsTime()
}

for i, headerConf := range tg.GetColumnHeader() {
if invHeaders := invocation.extractHeaders(headerConf); invHeaders != nil {
headers[i] = append(headers[i], invHeaders...)
}
}

for targetID, singleActionResults := range invocation.TargetResults {
for _, sar := range singleActionResults {
if !includeStatus(tg, sar) {
Expand All @@ -351,6 +425,12 @@ func processGroup(tg *configpb.TestGroup, group *invocationGroup) *updater.Infla
cell.Result = *cr
}
groupedCells[targetID] = append(groupedCells[targetID], cell)

for i, headerConf := range tg.GetColumnHeader() {
if targetHeaders := sar.extractHeaders(headerConf); targetHeaders != nil {
headers[i] = append(headers[i], targetHeaders...)
}
}
}
}

Expand All @@ -368,10 +448,39 @@ func processGroup(tg *configpb.TestGroup, group *invocationGroup) *updater.Infla
}

col.Column.Hint = string(hint)
col.Column.Extra = compileHeaders(tg.GetColumnHeader(), headers)

return col
}

// compileHeaders reduces all seen header values down to the final string value.
// Separates multiple values with || when configured, otherwise the value becomes *
func compileHeaders(columnHeader []*configpb.TestGroup_ColumnHeader, headers [][]string) []string {
if len(columnHeader) == 0 {
return nil
}

var compiledHeaders []string
for i, headerList := range headers {
switch {
case len(headerList) == 0:
compiledHeaders = append(compiledHeaders, "")
case len(headerList) == 1:
compiledHeaders = append(compiledHeaders, headerList[0])
case columnHeader[i].GetListAllValues():
var values []string
for _, value := range headerList {
values = append(values, value)
}
sort.Strings(values)
compiledHeaders = append(compiledHeaders, strings.Join(values, "||"))
default:
compiledHeaders = append(compiledHeaders, "*")
}
}
return compiledHeaders
}

// identifyBuild applies build override configurations and assigns a build
// Returns an empty string if no configurations are present or no configs are correctly set.
// i.e. no key is found in properties.
Expand Down
189 changes: 189 additions & 0 deletions pkg/updater/resultstore/resultstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1758,3 +1758,192 @@ func TestGroupInvocations(t *testing.T) {
})
}
}

func TestExtractHeaders(t *testing.T) {
cases := []struct {
name string
isInv bool
inv *invocation
sar *singleActionResult
headerConf *configpb.TestGroup_ColumnHeader
want []string
}{
{
name: "empty invocation",
isInv: true,
want: nil,
},
{
name: "empty target results",
isInv: false,
want: nil,
},
{
name: "invocation has a config value present",
isInv: true,
inv: &invocation{
InvocationProto: &resultstore.Invocation{
Properties: []*resultstore.Property{
{Key: "field", Value: "green"},
{Key: "os", Value: "linux"},
},
},
},
headerConf: &configpb.TestGroup_ColumnHeader{
ConfigurationValue: "os",
},
want: []string{"linux"},
},
{
name: "invocation doesn't have a config value present",
isInv: true,
inv: &invocation{
InvocationProto: &resultstore.Invocation{
Properties: []*resultstore.Property{
{Key: "radio", Value: "head"},
},
},
},
headerConf: &configpb.TestGroup_ColumnHeader{
ConfigurationValue: "rainbows",
},
want: nil,
},
{
name: "invocation has labels with prefixes",
isInv: true,
inv: &invocation{
InvocationProto: &resultstore.Invocation{
InvocationAttributes: &resultstore.InvocationAttributes{
Labels: []string{"os=linux", "env=prod", "test=fast", "test=hermetic"},
},
},
},
headerConf: &configpb.TestGroup_ColumnHeader{
Label: "test=",
},
want: []string{"fast", "hermetic"},
},
{
name: "target result has existing properties",
sar: &singleActionResult{
ActionProto: &resultstore.Action{
ActionType: &resultstore.Action_TestAction{
TestAction: &resultstore.TestAction{
TestSuite: &resultstore.TestSuite{
Properties: []*resultstore.Property{
{Key: "test-property", Value: "fast"},
},
Tests: []*resultstore.Test{
{
TestType: &resultstore.Test_TestCase{
TestCase: &resultstore.TestCase{
Properties: []*resultstore.Property{
{Key: "test-property", Value: "hermetic"},
},
},
},
},
},
},
},
},
},
},
headerConf: &configpb.TestGroup_ColumnHeader{
Property: "test-property",
},
want: []string{"fast", "hermetic"},
},
{
name: "target results doesn't have properties",
sar: &singleActionResult{
ActionProto: &resultstore.Action{},
},
want: nil,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var got []string
switch {
case tc.isInv:
got = tc.inv.extractHeaders(tc.headerConf)
default:
got = tc.sar.extractHeaders(tc.headerConf)
}
if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
t.Errorf("extractHeaders(...) differed (-want, +got): %s", diff)
}
})
}
}

func TestCompileHeaders(t *testing.T) {
cases := []struct {
name string
columnHeaders []*configpb.TestGroup_ColumnHeader
headers [][]string
want []string
}{
{
name: "no custom headers configured",
want: nil,
},
{
name: "single custom header set with no values fetched",
columnHeaders: []*configpb.TestGroup_ColumnHeader{
{Label: "rapid="},
},
headers: make([][]string, 1),
want: []string{""},
},
{
name: "single custom header set with one value fetched",
columnHeaders: []*configpb.TestGroup_ColumnHeader{
{ConfigurationValue: "os"},
},
headers: [][]string{
{"linux"},
},
want: []string{"linux"},
},
{
name: "multiple custom headers set, don't list all",
columnHeaders: []*configpb.TestGroup_ColumnHeader{
{Label: "os="},
{ConfigurationValue: "test-duration"},
},
headers: [][]string{
{"linux", "ubuntu"},
{"30m"},
},
want: []string{"*", "30m"},
},
{
name: "multiple custom headers, list 'em all",
columnHeaders: []*configpb.TestGroup_ColumnHeader{
{Property: "type", ListAllValues: true},
{Label: "test=", ListAllValues: true},
},
headers: [][]string{
{"grass", "flying"},
{"fast", "unit", "hermetic"},
},
want: []string{
"flying||grass",
"fast||hermetic||unit",
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got := compileHeaders(tc.columnHeaders, tc.headers)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Fatalf("compileHeaders(...) differed (-want,+got): %s", diff)
}
})
}
}

0 comments on commit a2c5d56

Please sign in to comment.