| // 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)) |
| } |
| } |