| // 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.Service |
| } |
| |
| // NewLogger is Visible for testing. Use `Start` and `Resume` instead. |
| func NewLogger(svc resultstore.Service, 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: 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{ |
| 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{ |
| ID: "test", |
| InvocationID: invocationID, |
| TestSuite: e.TestName, |
| 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, |
| } |
| } |