blob: 6398a7227cef626651431fb6b21c636dffabcec3 [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package tastrpc provides the Tast related RPC services by cros-test.
package tastrpc
import (
"context"
"fmt"
"io"
"net"
"path/filepath"
"strings"
"sync"
"github.com/golang/protobuf/ptypes/empty"
_go "go.chromium.org/chromiumos/config/go"
"go.chromium.org/chromiumos/config/go/test/api"
"google.golang.org/grpc"
"go.chromium.org/chromiumos/test/execution/errors"
"go.chromium.org/tast/core/framework/protocol"
)
// ReportsServer implements the tast.framework.protocol.ReportsServer.
type ReportsServer struct {
srv *grpc.Server // RPC server to receive reports from tast.
listenerAddr net.Addr // The address for the listener for gRPC service.
mu sync.Mutex // A mutex to protect reportedTests and testCaseResults.
tests []string // Tests to be run.
reportedTests map[string]struct{} // Tests that have received results.
testCaseResults []*api.TestCaseResult // Reported test results.
testResultsDir string // Parent directory for all test results.
testNamesToIds map[string]string // Mapping between test names and test ids.
testNamesToMetadata map[string]*api.TestCaseMetadata // Mapping between test names and test metadata.
allWarnings []string // All warnings that has been encountered.
allErrors []error // All errors that has been encountered.
}
var _ protocol.ReportsServer = (*ReportsServer)(nil)
// LogStream gets logs from tast and passes on to progress sink server.
func (s *ReportsServer) LogStream(stream protocol.Reports_LogStreamServer) error {
for {
_, err := stream.Recv()
if err == io.EOF {
return stream.SendAndClose(&empty.Empty{})
}
if err != nil {
return err
}
}
}
// ReportResult gets a report request from tast and passes on to progress sink.
func (s *ReportsServer) ReportResult(ctx context.Context, req *protocol.ReportResultRequest) (*protocol.ReportResultResponse, error) {
testID, ok := s.testNamesToIds[req.Test]
if !ok {
s.allErrors = append(s.allErrors, errors.NewStatusError(errors.InvalidArgument,
fmt.Errorf("failed to find test id for test %v", req.Test)))
return &protocol.ReportResultResponse{}, nil
}
testMetadata, ok := s.testNamesToMetadata[req.Test]
if !ok {
testMetadata = nil
s.allWarnings = append(s.allWarnings, fmt.Sprintf("failed to find test metadata for test %v", req.Test))
}
testResult := api.TestCaseResult{
TestCaseId: &api.TestCase_Id{Value: testID},
ResultDirPath: &_go.StoragePath{
HostType: _go.StoragePath_LOCAL,
Path: filepath.Join(s.testResultsDir, "tests", req.Test),
},
Verdict: &api.TestCaseResult_Pass_{Pass: &api.TestCaseResult_Pass{}},
TestHarness: &api.TestHarness{
TestHarnessType: &api.TestHarness_Tast_{
Tast: &api.TestHarness_Tast{},
},
},
StartTime: req.StartTime,
Duration: req.Duration,
TestCaseMetadata: testMetadata,
}
if len(req.Errors) > 0 {
testResult.Verdict = &api.TestCaseResult_Fail_{Fail: &api.TestCaseResult_Fail{}}
var reasons []string
for _, e := range req.Errors {
reasons = append(reasons, e.Reason)
}
testResult.Reason = strings.Join(reasons, "\n")
} else if req.SkipReason != "" {
testResult.Verdict = &api.TestCaseResult_Skip_{Skip: &api.TestCaseResult_Skip{}}
testResult.Reason = req.SkipReason
}
s.mu.Lock()
// Check the reported tests (which uses req.Test), to see if a result for the test has previously been reported.
if _, ok := s.reportedTests[req.Test]; ok {
// If it has, we will remove the duplicate result, in order to stub in the new proper retry result.
// testID is used for matching there... a bit odd...
for i, res := range s.testCaseResults {
if res.TestCaseId.Value == testID {
s.testCaseResults = append(s.testCaseResults[:i], s.testCaseResults[i+1:]...)
break
}
}
}
// Note, must be created AFTER the dup check; otherwise everything will be a dup
s.reportedTests[req.Test] = struct{}{}
s.testCaseResults = append(s.testCaseResults, &testResult)
s.mu.Unlock()
return &protocol.ReportResultResponse{}, nil
}
// MissingTestsReports return error results to all tests that have not reported results.
func (s *ReportsServer) MissingTestsReports(reason string) []*api.TestCaseResult {
var missingTestResults []*api.TestCaseResult
s.mu.Lock()
defer s.mu.Unlock()
if reason == "" {
reason = "Test did not run"
}
for _, t := range s.tests {
if _, ok := s.reportedTests[t]; ok {
continue
}
testID, ok := s.testNamesToIds[t]
if !ok {
continue
}
// We still should be able to map the missing tests' metadata.
testMetadata, ok := s.testNamesToMetadata[t]
if !ok {
testMetadata = nil
s.allWarnings = append(s.allWarnings, fmt.Sprintf("failed to find test metadata for missing test %v", t))
}
missingTestResults = append(missingTestResults, &api.TestCaseResult{
TestCaseId: &api.TestCase_Id{Value: testID},
Verdict: &api.TestCaseResult_NotRun_{NotRun: &api.TestCaseResult_NotRun{}},
Reason: reason,
TestHarness: &api.TestHarness{
TestHarnessType: &api.TestHarness_Tast_{
Tast: &api.TestHarness_Tast{},
},
},
TestCaseMetadata: testMetadata,
})
}
return missingTestResults
}
// TestsReports returns results to all tests that have reported results.
func (s *ReportsServer) TestsReports() []*api.TestCaseResult {
s.mu.Lock()
defer s.mu.Unlock()
return s.testCaseResults
}
// Stop stops the ReportsServer.
func (s *ReportsServer) Stop() {
s.srv.Stop()
}
// Address returns the network address of the ReportsServer.
func (s *ReportsServer) Address() string {
return s.listenerAddr.String()
}
// Errors returns errors encountered during test reporting.
func (s *ReportsServer) Errors() []error {
return s.allErrors
}
// Warnings returns warnings encountered during test reporting.
func (s *ReportsServer) Warnings() []string {
return s.allWarnings
}
// NewReportsServer starts a Reports gRPC service and returns a ReportsServer object when success.
// The caller is responsible for calling Stop() method.
func NewReportsServer(port int, tests []string, testNamesToIds map[string]string, testNamesToMetadata map[string]*api.TestCaseMetadata, resultDir string) (*ReportsServer, error) {
l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return nil, err
}
s := ReportsServer{
srv: grpc.NewServer(),
listenerAddr: l.Addr(),
reportedTests: make(map[string]struct{}),
tests: tests,
testResultsDir: resultDir,
testNamesToIds: testNamesToIds,
testNamesToMetadata: testNamesToMetadata,
}
protocol.RegisterReportsServer(s.srv, &s)
go s.srv.Serve(l)
return &s, nil
}