blob: fb7e780591d3d0212b59bedac26c1b74057d6f9f [file] [log] [blame]
// Copyright 2021 The Chromium OS 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 useractions contains the definition of UserContext and UserAction.
// It also provides helper functions to use in test cases.
package useractions
import (
"context"
"encoding/csv"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"chromiumos/tast/common/action"
"chromiumos/tast/errors"
"chromiumos/tast/local/chrome"
"chromiumos/tast/testing"
)
// UserContext represents the user context in the test.
type UserContext struct {
testName string
cr *chrome.Chrome
tconn *chrome.TestConn
outputDir string
attributes map[string]string
tags map[ActionTag]struct{}
}
// UserActionCfg represents optional configurations of a user action.
type UserActionCfg struct {
Attributes map[string]string
Tags []ActionTag
ValidateResult action.Action // validateResult should only check the outcome of the user action.
Callback func(ctx context.Context, actionErr error) error // callback takes action error as input.
}
// UserAction represents the user action.
type UserAction struct {
name string
action action.Action
userActionCfg *UserActionCfg
userContext *UserContext
}
// NewUserContext returns a new user context.
func NewUserContext(testName string, cr *chrome.Chrome, tconn *chrome.TestConn, outputDir string, attributes map[string]string, tags []ActionTag) *UserContext {
tagsMap := make(map[ActionTag]struct{})
if tags != nil {
for _, tag := range tags {
tagsMap[tag] = struct{}{}
}
}
if attributes == nil {
attributes = make(map[string]string)
}
return &UserContext{
testName,
cr,
tconn,
outputDir,
attributes,
tagsMap,
}
}
// NewUserAction returns a new user action.
func NewUserAction(name string, action action.Action, uc *UserContext, uac *UserActionCfg) *UserAction {
return fromAction(name, action, uc, uac)
}
// NewUserAction returns a new user action.
func (uc *UserContext) NewUserAction(name string, action action.Action, uac *UserActionCfg) *UserAction {
return fromAction(name, action, uc, uac)
}
// InvalidUserAction creates a user action to return error directly.
func (uc *UserContext) InvalidUserAction(err error) *UserAction {
return fromAction("Invalid action", func(ctx context.Context) error {
return err
}, uc, nil)
}
// SetTestName sets the test name of the user context.
func (uc *UserContext) SetTestName(testName string) {
uc.testName = testName
}
// SetAttribute set the value of an attribute of the user context.
func (uc *UserContext) SetAttribute(name, value string) {
uc.attributes[name] = value
}
// RemoveAttribute removes an attribute of the user context.
func (uc *UserContext) RemoveAttribute(name string) {
delete(uc.attributes, name)
}
// Attributes returns all attributes of the user context.
func (uc *UserContext) Attributes() map[string]string {
return uc.attributes
}
// AddTags adds tags to the user context.
func (uc *UserContext) AddTags(actionTags []ActionTag) {
for _, newTag := range actionTags {
uc.tags[newTag] = struct{}{}
}
}
// RemoveTags removes tags from the user context.
func (uc *UserContext) RemoveTags(actionTags []ActionTag) {
for _, tag := range actionTags {
delete(uc.tags, tag)
}
}
// Chrome returns the Chrome instance from the user context.
func (uc *UserContext) Chrome() *chrome.Chrome {
return uc.cr
}
// TestAPIConn returns the test connection from the user context.
func (uc *UserContext) TestAPIConn() *chrome.TestConn {
return uc.tconn
}
func fromAction(name string, action action.Action, uc *UserContext, uac *UserActionCfg) *UserAction {
if uac == nil {
uac = &UserActionCfg{}
}
if uac.Attributes == nil {
uac.Attributes = make(map[string]string)
}
if uac.Tags == nil {
uac.Tags = []ActionTag{}
}
return &UserAction{
name: name,
action: action,
userContext: uc,
userActionCfg: uac,
}
}
// RunAction runs a action.Action as a user action and records detailed running information.
func (uc *UserContext) RunAction(ctx context.Context, name string, action action.Action, uac *UserActionCfg) error {
userAction := fromAction(name, action, uc, uac)
return userAction.Run(ctx)
}
// Run runs a user action and records detailed running information.
func (ua *UserAction) Run(ctx context.Context) (err error) {
// Combine context attributes and action attributes.
// Action attributes will replace context attributes if the same name.
combinedAttributes := make(map[string]string)
for k, v := range ua.userContext.attributes {
combinedAttributes[k] = v
}
for k, v := range ua.userActionCfg.Attributes {
combinedAttributes[k] = v
}
// Combine context tags and action tags.
// It should be tagged if either one equals True.
combinedTagsMap := make(map[ActionTag]struct{})
for k := range ua.userContext.tags {
combinedTagsMap[k] = struct{}{}
}
for _, tag := range ua.userActionCfg.Tags {
combinedTagsMap[tag] = struct{}{}
}
combinedTags := make([]ActionTag, 0, len(combinedTagsMap))
for k := range combinedTagsMap {
combinedTags = append(combinedTags, k)
}
startTime := time.Now()
defer func(ctx context.Context) {
endTime := time.Now()
result := &actionResult{
actionName: ua.name,
testName: ua.userContext.testName,
startTime: startTime,
endTime: endTime,
attributes: combinedAttributes,
tags: combinedTags,
pass: err == nil,
err: err,
}
if err := result.writeToFile(ua.userContext.outputDir); err != nil {
testing.ContextLog(ctx, "Warning: Failed to write user action result: ", err)
}
}(ctx)
err = ua.action(ctx)
// Only validate action result if the action finished without error.
if err == nil && ua.userActionCfg.ValidateResult != nil {
err = ua.userActionCfg.ValidateResult(ctx)
}
if ua.userActionCfg.Callback != nil {
if callbackError := ua.userActionCfg.Callback(ctx, err); callbackError != nil {
testing.ContextLogf(ctx, "callback failed in action %q: %v", ua.name, callbackError)
}
}
return err
}
// Name returns the name of the user action.
func (ua *UserAction) Name() string {
return ua.name
}
// UserContext returns the user context instance of the user action.
func (ua *UserAction) UserContext() *UserContext {
return ua.userContext
}
// SetAttribute set the value of an attribute of the user action.
func (ua *UserAction) SetAttribute(key, value string) {
ua.userActionCfg.Attributes[key] = value
}
// AddTags adds tags to the user action.
func (ua *UserAction) AddTags(actionTags []ActionTag) {
ua.userActionCfg.Tags = append(ua.userActionCfg.Tags, actionTags...)
}
type actionResult struct {
actionName string
testName string
attributes map[string]string
tags []ActionTag
startTime time.Time
endTime time.Time
pass bool
err error
}
const actionTimeFormat = "2006-01-02 15:04:05.000"
func (ar *actionResult) stringArray() ([]string, error) {
attrStr, err := json.Marshal(ar.attributes)
if err != nil {
return nil, errors.Wrapf(err, "failed to JSON encoding user action attributes: %v", ar.attributes)
}
errMessage := ""
if ar.err != nil {
errMessage = fmt.Sprintf("%v", ar.err)
}
var tags []string
for _, tag := range ar.tags {
tags = append(tags, string(tag))
}
return []string{
ar.actionName,
ar.testName,
fmt.Sprintf("%s", attrStr),
fmt.Sprintf("%s", strings.Join(tags, ", ")),
fmt.Sprintf("%s", ar.startTime.Format(actionTimeFormat)),
fmt.Sprintf("%s", ar.endTime.Format(actionTimeFormat)),
fmt.Sprintf("%s", strconv.FormatBool(ar.pass)),
fmt.Sprintf("%s", errMessage),
}, nil
}
// writeToFile writes action result into "actionLogFileName" in the outDir.
func (ar *actionResult) writeToFile(outDir string) error {
strArray, err := ar.stringArray()
if err != nil {
return err
}
const actionLogFileName = "user_action_log.csv"
filePath := filepath.Join(outDir, actionLogFileName)
f, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return errors.Wrapf(err, "failed to open file at %q", filePath)
}
defer f.Close()
w := csv.NewWriter(f)
defer w.Flush()
return w.Write(strArray)
}