blob: eb4ceb17562fb9c06176f10e5f6e5706907288e7 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package tilo
import (
"context"
"errors"
"fmt"
"time"
"fuchsia.googlesource.com/infra/infra/fuchsia/testexec"
"fuchsia.googlesource.com/infra/infra/tilo/resultstore"
"github.com/google/uuid"
)
// DummyStartTime is a placeholder start time to use when creating Targets. ResultStore
// requires that a Target has some start time at creation, but Tilo supports creating and
// starting a test target as two separate actions, so start time is not relevant for
// Target creation. This is used instead and is overwritten with the actual start time
// when a test is started.
var dummyStartTime = time.Date(2018, time.December, 30, 0, 0, 0, 0, time.UTC)
// Start creates a new Invocation in ResultStore and returns new Logger for modifying the
// Invocation. Host is the hostname of the ResultStore backend.
//
// TODO(IN-699): Define constants for the ResultStore backends and document them here.
func Start(ctx context.Context, environment resultstore.Environment, projectID string) (Logger, error) {
// Connect to ResultStore.
svc, err := resultstore.Connect(ctx, environment)
if err != nil {
return nil, err
}
// Create the Invocation.
authToken := uuid.New().String()
ctx, err = resultstore.SetAuthToken(ctx, authToken)
if err != nil {
return nil, err
}
invocation, err := svc.CreateInvocation(ctx, &resultstore.Invocation{
ProjectID: projectID,
ID: uuid.New().String(),
StartTime: time.Now(),
})
if err != nil {
return nil, err
}
// Initialize the resultstore.Context.
con := resultstore.NewContext()
con.SetEnvironment(environment)
con.SetAuthToken(authToken)
con.SetInvocationID(invocation.ID)
con.SetInvocationName(invocation.Name)
return &logger{con: con, svc: svc}, nil
}
// Resume creates a new Logger for the Invocation identified by a resultstore.Context.
func Resume(ctx context.Context, con resultstore.Context) (Logger, error) {
svc, err := resultstore.Connect(ctx, con.Environment())
if err != nil {
return nil, err
}
return &logger{con: con, svc: svc}, nil
}
// Logger is an interface for recording Fuchsia test events in ResultStore.
type Logger interface {
// Context returns the context associated with the current invocation. One Logger can
// continue editing this Logger's Invocation by inheriting its Context.
Context() resultstore.Context
// LogEnvironment reports that a test Environment has been discovered. The
// environment is logged in ResultStore as a Configuration.
LogEnvironment(context.Context, EnvironmentFoundEvent) error
// LogTestFound reports that a test has been discovered. The Test is logged in
// ResultStore as a Target.
LogTestFound(context.Context, TestFoundEvent) error
// LogTestStarted reports that a test has started. The corresponding ResultStore
// target recieves a ConfiguredTarget and Action.
LogTestStarted(context.Context, TestStartedEvent) error
// LogTestFinished reports that a test has finished. The corresponding ResultStore
// Action, ConfiguredTarget, and Target are updated and finished.
LogTestFinished(context.Context, TestFinishedEvent) error
// End finishes the Invocation.
End(context.Context) error
}
// The default Logger implementation
type logger struct {
con resultstore.Context
svc resultstore.UploadClient
}
// NewLogger is Visible for testing. Use `Start` and `Resume` instead.
func NewLogger(svc resultstore.UploadClient, con resultstore.Context) Logger {
return &logger{con: con, svc: svc}
}
func (l *logger) Context() resultstore.Context {
return l.con
}
func (l *logger) LogEnvironment(ctx context.Context, e EnvironmentFoundEvent) error {
ctx, err := l.setAuthToken(ctx)
if err != nil {
return err
}
return l.createConfiguration(ctx, e)
}
func (l *logger) LogTestFound(ctx context.Context, e TestFoundEvent) error {
ctx, err := l.setAuthToken(ctx)
if err != nil {
return err
}
return l.createTarget(ctx, e)
}
func (l *logger) LogTestStarted(ctx context.Context, e TestStartedEvent) error {
ctx, err := l.setAuthToken(ctx)
if err != nil {
return err
}
if err := l.createConfiguredTarget(ctx, e); err != nil {
return err
}
if err := l.createTestAction(ctx, e); err != nil {
return err
}
return l.startTarget(ctx, e)
}
func (l *logger) LogTestFinished(ctx context.Context, e TestFinishedEvent) error {
ctx, err := l.setAuthToken(ctx)
if err != nil {
return err
}
if err := l.updateTestAction(ctx, e); err != nil {
return err
}
if err := l.updateConfiguredTarget(ctx, e); err != nil {
return err
}
if err := l.finishConfiguredTarget(ctx, e); err != nil {
return err
}
if err := l.updateTarget(ctx, e); err != nil {
return err
}
return l.finishTarget(ctx, e)
}
func (l *logger) End(ctx context.Context) error {
ctx, err := l.setAuthToken(ctx)
if err != nil {
return err
}
return l.finishInvocation(ctx)
}
func (l *logger) updateTestAction(ctx context.Context, e TestFinishedEvent) error {
actionName, err := l.testAction(e.TestName, e.EnvName)
if err != nil {
return err
}
action := &resultstore.TestAction{
Name: actionName,
StartTime: e.StartTime,
Duration: e.EndTime.Sub(e.StartTime),
Status: resultstore.Status(e.TestStatus),
TestLogURI: e.LogFileURI,
}
if _, err := l.svc.UpdateTestAction(ctx, action); err != nil {
return err
}
return nil
}
func (l *logger) updateConfiguredTarget(ctx context.Context, e TestFinishedEvent) error {
configuredTargetName, err := l.configuredTargetName(e.TestName, e.EnvName)
if err != nil {
return err
}
configuredTarget := &resultstore.ConfiguredTarget{
Name: configuredTargetName,
StartTime: e.StartTime,
Duration: e.EndTime.Sub(e.StartTime),
Status: resultstore.Status(e.TestStatus),
}
if _, err := l.svc.UpdateConfiguredTarget(ctx, configuredTarget); err != nil {
return err
}
return nil
}
func (l *logger) finishConfiguredTarget(ctx context.Context, e TestFinishedEvent) error {
configuredTargetName, err := l.configuredTargetName(e.TestName, e.EnvName)
if err != nil {
return err
}
return l.svc.FinishConfiguredTarget(ctx, configuredTargetName)
}
func (l *logger) finishTarget(ctx context.Context, e TestFinishedEvent) error {
targetName, err := l.targetName(e.TestName)
if err != nil {
return err
}
return l.svc.FinishTarget(ctx, targetName)
}
func (l *logger) updateTarget(ctx context.Context, e TestFinishedEvent) error {
targetName, err := l.targetName(e.TestName)
if err != nil {
return err
}
target := &resultstore.Target{
Name: targetName,
StartTime: e.StartTime,
Duration: e.EndTime.Sub(e.StartTime),
Status: resultstore.Status(e.TestStatus),
}
if _, err := l.svc.UpdateTarget(ctx, target); err != nil {
return err
}
return nil
}
func (l *logger) startTarget(ctx context.Context, e TestStartedEvent) error {
targetName, err := l.targetName(e.TestName)
if err != nil {
return err
}
target := &resultstore.Target{
Name: targetName,
StartTime: e.StartTime,
}
_, err = l.svc.UpdateTarget(ctx, target)
return err
}
func (l *logger) createConfiguration(ctx context.Context, e EnvironmentFoundEvent) error {
invocationID, err := l.invocationID()
if err != nil {
return err
}
invocationName, err := l.invocationName()
if err != nil {
return err
}
config := &resultstore.Configuration{
ID: e.Name(),
InvocationID: invocationID,
Properties: l.dimensionSetToMap(e.Dimensions),
}
_, err = l.svc.CreateConfiguration(ctx, config, invocationName)
return err
}
func (l *logger) createTarget(ctx context.Context, e TestFoundEvent) error {
invocationID, err := l.invocationID()
if err != nil {
return err
}
invocationName, err := l.invocationName()
if err != nil {
return err
}
target := &resultstore.Target{
ID: &resultstore.TargetID{
ID: e.Name,
InvocationID: invocationID,
},
StartTime: dummyStartTime,
}
target, err = l.svc.CreateTarget(ctx, target, invocationName)
if err != nil {
return err
}
if !l.con.SetTarget(e.Name, target.Name) {
return fmt.Errorf("Target already exists for env %s", e.Name)
}
return nil
}
func (l *logger) createConfiguredTarget(ctx context.Context, e TestStartedEvent) error {
invocationID, err := l.invocationID()
if err != nil {
return err
}
targetName, err := l.targetName(e.TestName)
if err != nil {
return err
}
configuredTarget := &resultstore.ConfiguredTarget{
ID: &resultstore.ConfiguredTargetID{
InvocationID: invocationID,
ConfigID: e.EnvName,
TargetID: e.TestName,
},
StartTime: e.StartTime,
}
configuredTarget, err = l.svc.CreateConfiguredTarget(ctx, configuredTarget, targetName)
if err != nil {
return err
}
if !l.con.SetConfiguredTarget(e.TestName, e.EnvName, configuredTarget.Name) {
return fmt.Errorf("ConfiguredTarget already exists for env %s", e.EnvName)
}
return nil
}
func (l *logger) createTestAction(ctx context.Context, e TestStartedEvent) error {
invocationID, err := l.invocationID()
if err != nil {
return err
}
configuredTargetName, err := l.configuredTargetName(e.TestName, e.EnvName)
if err != nil {
return err
}
action := &resultstore.TestAction{
TestSuite: e.TestName,
ID: &resultstore.TestActionID{
ID: "test",
InvocationID: invocationID,
TargetID: e.TestName,
ConfigID: e.EnvName,
},
StartTime: e.StartTime,
}
action, err = l.svc.CreateTestAction(ctx, action, configuredTargetName)
if err != nil {
return err
}
if !l.con.SetTestAction(e.TestName, e.EnvName, action.Name) {
return fmt.Errorf("Test Action already exists for test %s", e.TestName)
}
return nil
}
func (l *logger) setAuthToken(ctx context.Context) (context.Context, error) {
token := l.con.AuthToken()
if token == "" {
return nil, errors.New("context is missing auth token")
}
ctx, err := resultstore.SetAuthToken(ctx, token)
if err != nil {
return nil, err
}
return ctx, nil
}
func (l *logger) finishInvocation(ctx context.Context) error {
invocationName, err := l.invocationName()
if err != nil {
return err
}
return l.svc.FinishInvocation(ctx, invocationName)
}
func (l *logger) invocationID() (string, error) {
invocationID := l.con.InvocationID()
if invocationID == "" {
return "", errors.New("context is missing invocation id")
}
return invocationID, nil
}
func (l *logger) invocationName() (string, error) {
invocationName := l.con.InvocationName()
if invocationName == "" {
return "", errors.New("context is missing invocation name")
}
return invocationName, nil
}
func (l *logger) targetName(testName string) (string, error) {
targetName := l.con.Target(testName)
if targetName == "" {
return "", fmt.Errorf("no target for test '%s'", testName)
}
return targetName, nil
}
func (l *logger) configuredTargetName(testName, environmentName string) (string, error) {
configuredTargetName := l.con.ConfiguredTarget(testName, environmentName)
if configuredTargetName == "" {
return "", fmt.Errorf("no ConfiguredTarget for test '%s' in environment '%s'", testName, environmentName)
}
return configuredTargetName, nil
}
func (l *logger) testAction(testName, environmentName string) (string, error) {
actionName := l.con.TestAction(testName, environmentName)
if actionName == "" {
return "", fmt.Errorf("no TestAction for test '%s' in environment '%s'", testName, environmentName)
}
return actionName, nil
}
func (l *logger) dimensionSetToMap(set testexec.DimensionSet) map[string]string {
return map[string]string{
"device_type": set.DeviceType,
}
}