blob: 83140c4338a48dd6b56349a41481a78db0f0ab37 [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 processor
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"go.chromium.org/tast/core/internal/logging"
"go.chromium.org/tast/core/internal/protocol"
"go.chromium.org/tast/core/internal/run/reporting"
)
const testOutputTimeFmt = "15:04:05.000" // format for timestamps attached to test output
// loggingHandler emits logs for test execution events.
type loggingHandler struct {
baseHandler
resDir string
multiplexer *logging.MultiLogger
client *reporting.RPCClient
loggers []*entityLogger
}
type entityLogger struct {
Logger *logging.SinkLogger
File *os.File
}
var _ Handler = &loggingHandler{}
// NewLoggingHandler creates a new loggingHandler.
// multiplexer should be a MultiLogger all logs from the processor (including
// the preprocessor and all handlers) are sent to; in other words, multiplexer
// should be attached to the context passed to Processor method calls.
// loggingHandler will add/remove additional loggers to/from multiplexer to save
// per-entity logs.
func NewLoggingHandler(resDir string, multiplexer *logging.MultiLogger, client *reporting.RPCClient) *loggingHandler {
return &loggingHandler{
resDir: resDir,
multiplexer: multiplexer,
client: client,
}
}
func (h *loggingHandler) EntityStart(ctx context.Context, ei *entityInfo) error {
const BLUE = "\033[1;34m"
const RESET = "\033[0m"
t := time.Now()
timeStr := t.UTC().Format("2006-01-02T15:04:05.000000Z")
f, err := os.Create(filepath.Join(ei.FinalOutDir, "log.txt"))
if err != nil {
return err
}
writers := []io.Writer{f}
if ei.Entity.GetType() == protocol.EntityType_TEST {
relPath, err := filepath.Rel(h.resDir, f.Name())
if err != nil {
return err
}
writers = append(writers, h.client.NewTestLogWriter(ei.Entity.GetName(), relPath))
}
logger := &entityLogger{
Logger: logging.NewSinkLogger(logging.LevelDebug, true, logging.NewWriterSink(io.MultiWriter(writers...))),
File: f,
}
h.loggers = append(h.loggers, logger)
h.multiplexer.AddLogger(logger.Logger)
logging.Debugf(ctx, "Started %s %s", entityTypeName(ei.Entity.GetType()), ei.Entity.GetName())
fmt.Printf("%v%v Started %s %s %v\n", timeStr, BLUE, entityTypeName(ei.Entity.GetType()), ei.Entity.GetName(), RESET)
return nil
}
func (h *loggingHandler) EntityLog(ctx context.Context, ei *entityInfo, l *logEntry) error {
logging.Infof(ctx, "[%s] %s", l.Time.Format(testOutputTimeFmt), l.Text)
return nil
}
func (h *loggingHandler) EntityError(ctx context.Context, ei *entityInfo, e *errorEntry) error {
ts := e.Time.Format(testOutputTimeFmt)
loc := e.Error.GetLocation()
if loc == nil {
logging.Infof(ctx, "[%s] Error: %s", ts, e.Error.GetReason())
} else {
logging.Infof(ctx, "[%s] Error at %s:%d: %s", ts, filepath.Base(loc.GetFile()), loc.GetLine(), e.Error.GetReason())
}
if stack := loc.GetStack(); stack != "" {
logging.Infof(ctx, "[%s] Stack trace:\n%s", ts, stack)
}
return nil
}
func (h *loggingHandler) EntityEnd(ctx context.Context, ei *entityInfo, r *entityResult) error {
const BLUE = "\033[1;34m"
const RESET = "\033[0m"
t := time.Now()
timeStr := t.UTC().Format("2006-01-02T15:04:05.000000Z")
if reasons := r.Skip.GetReasons(); len(reasons) > 0 {
logging.Debugf(ctx, "Skipped test %s due to missing dependencies: %s", ei.Entity.GetName(), strings.Join(reasons, ", "))
fmt.Printf("%v%v Skipped test %s%v due to missing dependencies: %s\n", timeStr, BLUE, ei.Entity.GetName(), RESET, strings.Join(reasons, ", "))
return nil
}
logging.Debugf(ctx,
"Completed %s %s in %v with %d error(s)",
entityTypeName(ei.Entity.GetType()),
ei.Entity.GetName(),
r.End.Sub(r.Start).Round(time.Millisecond),
len(r.Errors))
fmt.Printf("%v%v Completed %s %s %v in %v with %d error(s)\n",
timeStr, BLUE,
entityTypeName(ei.Entity.GetType()),
ei.Entity.GetName(), RESET,
r.End.Sub(r.Start).Round(time.Millisecond),
len(r.Errors))
logger := h.loggers[len(h.loggers)-1]
h.multiplexer.RemoveLogger(logger.Logger)
logger.File.Close()
h.loggers = h.loggers[:len(h.loggers)-1]
return nil
}
func (h *loggingHandler) RunLog(ctx context.Context, l *logEntry) error {
logging.Infof(ctx, "[%s] %s", l.Time.Format(testOutputTimeFmt), l.Text)
return nil
}
func entityTypeName(t protocol.EntityType) string {
switch t {
case protocol.EntityType_TEST:
return "test"
case protocol.EntityType_FIXTURE:
return "fixture"
default:
return fmt.Sprintf("unknown entity type (%d)", int(t))
}
}