go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/testverdicts/read_client.go (about)

     1  // Copyright 2024 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 testverdicts
    16  
    17  import (
    18  	"context"
    19  	"time"
    20  
    21  	"cloud.google.com/go/bigquery"
    22  	"google.golang.org/api/iterator"
    23  
    24  	"go.chromium.org/luci/common/errors"
    25  
    26  	"go.chromium.org/luci/analysis/internal/bqutil"
    27  )
    28  
    29  // NewReadClient creates a new client for reading test verdicts.
    30  func NewReadClient(ctx context.Context, gcpProject string) (*ReadClient, error) {
    31  	client, err := bqutil.Client(ctx, gcpProject)
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  	return &ReadClient{client: client}, nil
    36  }
    37  
    38  // ReadClient represents a client to read test verdicts from BigQuery.
    39  type ReadClient struct {
    40  	client *bigquery.Client
    41  }
    42  
    43  // Close releases any resources held by the client.
    44  func (c *ReadClient) Close() error {
    45  	return c.client.Close()
    46  }
    47  
    48  type ReadTestVerdictsPerSourcePositionOptions struct {
    49  	Project     string
    50  	TestID      string
    51  	VariantHash string
    52  	RefHash     string
    53  	// Only test verdicts with allowed invocation realms can be returned.
    54  	AllowedRealms []string
    55  	// All returned commits has source position greater than PositionMustGreater.
    56  	PositionMustGreater int64
    57  	// The maximum number of commits to return.
    58  	NumCommits int64
    59  }
    60  
    61  // CommitWithVerdicts represents a commit with test verdicts.
    62  type CommitWithVerdicts struct {
    63  	// Source position of this commit.
    64  	Position int64
    65  	// Commit hash of this commit.
    66  	CommitHash string
    67  	// Represent a branch in the source control.
    68  	Ref *Ref
    69  	// Returns at most 20 test verdicts at this commit.
    70  	TestVerdicts []*TestVerdict
    71  }
    72  type TestVerdict struct {
    73  	TestID                string
    74  	VariantHash           string
    75  	RefHash               string
    76  	InvocationID          string
    77  	Status                string
    78  	PartitionTime         time.Time
    79  	PassedAvgDurationUsec bigquery.NullFloat64
    80  	Changelists           []*Changelist
    81  	// Whether the caller has access to this test verdict.
    82  	HasAccess bool
    83  }
    84  
    85  type Ref struct {
    86  	Gitiles *Gitiles
    87  }
    88  type Gitiles struct {
    89  	Host    bigquery.NullString
    90  	Project bigquery.NullString
    91  	Ref     bigquery.NullString
    92  }
    93  
    94  type Changelist struct {
    95  	Host      bigquery.NullString
    96  	Change    bigquery.NullInt64
    97  	Patchset  bigquery.NullInt64
    98  	OwnerKind bigquery.NullString
    99  }
   100  
   101  // ReadTestVerdictsPerSourcePosition returns commits with test verdicts in source position ascending order.
   102  func (c *ReadClient) ReadTestVerdictsPerSourcePosition(ctx context.Context, options ReadTestVerdictsPerSourcePositionOptions) ([]*CommitWithVerdicts, error) {
   103  	query := `
   104  	SELECT
   105  		sources.gitiles_commit.position as Position,
   106  		ANY_VALUE(sources.gitiles_commit.commit_hash) as CommitHash,
   107  		ANY_VALUE(source_ref) as Ref,
   108  		ARRAY_AGG(STRUCT(test_id as TestID ,
   109  									variant_hash as VariantHash,
   110  									source_ref_hash as RefHash,
   111  									invocation.id as InvocationID,
   112  									status as Status,
   113  									(SELECT AVG(IF(r.status = "PASS",r.duration , NULL)) FROM UNNEST(results) as r) as PassedAvgDurationUsec,
   114  									(SELECT ARRAY_AGG(STRUCT(host as Host, change as Change, patchset as Patchset, owner_kind as OwnerKind)) FROM UNNEST(sources.changelists)) as Changelists,
   115  									invocation.realm IN UNNEST(@allowedRealms) as HasAccess,
   116  									partition_time as PartitionTime) ORDER BY partition_time DESC LIMIT 20) as TestVerdicts
   117  	FROM internal.test_verdicts
   118  	WHERE project = @project
   119  		AND test_id = @testID
   120  		AND variant_hash = @variantHash
   121  		AND source_ref_hash = @refHash
   122  		AND sources.gitiles_commit.position > @positionMustGreater
   123  	GROUP BY sources.gitiles_commit.position
   124  	ORDER BY sources.gitiles_commit.position
   125  	LIMIT @limit
   126  	`
   127  	q := c.client.Query(query)
   128  	q.DefaultDatasetID = "internal"
   129  	q.Parameters = []bigquery.QueryParameter{
   130  		{Name: "project", Value: options.Project},
   131  		{Name: "testID", Value: options.TestID},
   132  		{Name: "variantHash", Value: options.VariantHash},
   133  		{Name: "refHash", Value: options.RefHash},
   134  		{Name: "positionMustGreater", Value: options.PositionMustGreater},
   135  		{Name: "limit", Value: options.NumCommits},
   136  		{Name: "allowedRealms", Value: options.AllowedRealms},
   137  	}
   138  	job, err := q.Run(ctx)
   139  	if err != nil {
   140  		return nil, errors.Annotate(err, "running query").Err()
   141  	}
   142  	it, err := job.Read(ctx)
   143  	if err != nil {
   144  		return nil, errors.Annotate(err, "read").Err()
   145  	}
   146  	results := []*CommitWithVerdicts{}
   147  	for {
   148  		row := &CommitWithVerdicts{}
   149  		err := it.Next(row)
   150  		if errors.Is(err, iterator.Done) {
   151  			break
   152  		}
   153  		if err != nil {
   154  			return nil, errors.Annotate(err, "obtain next commit with test verdicts row").Err()
   155  		}
   156  		results = append(results, row)
   157  	}
   158  	return results, nil
   159  }