yao/agent/test/output.go
Max d21f9c3769 Enhance Logging Functionality in RequestLogger
- Added a `noop` check in multiple logging methods (`LLMComplete`, `ToolStart`, `ToolComplete`, `HookStart`, `HookComplete`, and `HistoryLoad`) to prevent logging when the logger is in no-operation mode.
- Improved command handling in `root.go` by removing minimum argument requirements for commands and providing help output when no arguments are given.
- Introduced an `agent` command for better organization of agent-related functionalities in the CLI.
- Implemented automatic detection of the application root directory in `run.go` to streamline the application startup process.
- Cleaned up debug print statements in `config.go` to reduce clutter in the output.
2025-12-17 12:33:34 +08:00

316 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package test
import (
"fmt"
"strings"
"time"
"github.com/fatih/color"
jsoniter "github.com/json-iterator/go"
)
// OutputWriter handles colored console output for test execution
type OutputWriter struct {
verbose bool
}
// NewOutputWriter creates a new output writer
func NewOutputWriter(verbose bool) *OutputWriter {
return &OutputWriter{verbose: verbose}
}
// Header prints a header section
func (w *OutputWriter) Header(title string) {
fmt.Println()
color.New(color.FgCyan, color.Bold).Println("═══════════════════════════════════════════════════════════════")
color.New(color.FgCyan, color.Bold).Printf(" %s\n", title)
color.New(color.FgCyan, color.Bold).Println("═══════════════════════════════════════════════════════════════")
}
// SubHeader prints a sub-header
func (w *OutputWriter) SubHeader(title string) {
fmt.Println()
color.New(color.FgWhite, color.Bold).Println("───────────────────────────────────────────────────────────────")
color.New(color.FgWhite, color.Bold).Printf(" %s\n", title)
color.New(color.FgWhite, color.Bold).Println("───────────────────────────────────────────────────────────────")
}
// Info prints an info message
func (w *OutputWriter) Info(format string, args ...interface{}) {
color.New(color.FgBlue).Printf(" ")
fmt.Printf(format+"\n", args...)
}
// Success prints a success message
func (w *OutputWriter) Success(format string, args ...interface{}) {
color.New(color.FgGreen).Printf("✓ ")
fmt.Printf(format+"\n", args...)
}
// Error prints an error message
func (w *OutputWriter) Error(format string, args ...interface{}) {
color.New(color.FgRed).Printf("✗ ")
fmt.Printf(format+"\n", args...)
}
// Warning prints a warning message
func (w *OutputWriter) Warning(format string, args ...interface{}) {
color.New(color.FgYellow).Printf("⚠ ")
fmt.Printf(format+"\n", args...)
}
// Skip prints a skip message
func (w *OutputWriter) Skip(format string, args ...interface{}) {
color.New(color.FgYellow).Printf("○ ")
fmt.Printf(format+"\n", args...)
}
// Verbose prints a verbose message (only if verbose mode is enabled)
func (w *OutputWriter) Verbose(format string, args ...interface{}) {
if w.verbose {
color.New(color.FgHiBlack).Printf(" │ ")
fmt.Printf(format+"\n", args...)
}
}
// TestStart prints test case start
func (w *OutputWriter) TestStart(id string, input string, runNum int) {
inputPreview := truncateString(input, 50)
if runNum > 1 {
color.New(color.FgWhite).Printf("► [%s] Run %d: ", id, runNum)
} else {
color.New(color.FgWhite).Printf("► [%s] ", id)
}
color.New(color.FgHiBlack).Printf("%s", inputPreview)
fmt.Print(" ")
}
// TestResult prints test case result
func (w *OutputWriter) TestResult(status Status, duration time.Duration) {
switch status {
case StatusPassed:
color.New(color.FgGreen, color.Bold).Printf("PASSED")
case StatusFailed:
color.New(color.FgRed, color.Bold).Printf("FAILED")
case StatusSkipped:
color.New(color.FgYellow).Printf("SKIPPED")
case StatusError:
color.New(color.FgRed, color.Bold).Printf("ERROR")
case StatusTimeout:
color.New(color.FgRed).Printf("TIMEOUT")
}
color.New(color.FgHiBlack).Printf(" (%s)\n", formatDuration(duration))
}
// TestError prints test error details
func (w *OutputWriter) TestError(err string) {
color.New(color.FgRed).Printf(" └─ %s\n", err)
}
// TestOutput prints test output (verbose mode)
func (w *OutputWriter) TestOutput(output string) {
if w.verbose && output != "" {
outputPreview := truncateString(output, 100)
color.New(color.FgHiBlack).Printf(" └─ Output: %s\n", outputPreview)
}
}
// Progress prints progress information
func (w *OutputWriter) Progress(current, total int) {
percentage := float64(current) / float64(total) * 100
color.New(color.FgHiBlack).Printf("\r Progress: %d/%d (%.0f%%)", current, total, percentage)
}
// Summary prints the test summary
func (w *OutputWriter) Summary(summary *Summary, duration time.Duration) {
w.SubHeader("Summary")
// Agent info
color.New(color.FgWhite).Printf(" Agent: ")
color.New(color.FgCyan).Printf("%s\n", summary.AgentID)
if summary.Connector != "" {
color.New(color.FgWhite).Printf(" Connector: ")
color.New(color.FgCyan).Printf("%s\n", summary.Connector)
}
// Results
color.New(color.FgWhite).Printf(" Total: ")
fmt.Printf("%d\n", summary.Total)
color.New(color.FgWhite).Printf(" Passed: ")
if summary.Passed > 0 {
color.New(color.FgGreen).Printf("%d\n", summary.Passed)
} else {
fmt.Printf("%d\n", summary.Passed)
}
color.New(color.FgWhite).Printf(" Failed: ")
if summary.Failed > 0 {
color.New(color.FgRed).Printf("%d\n", summary.Failed)
} else {
fmt.Printf("%d\n", summary.Failed)
}
if summary.Skipped > 0 {
color.New(color.FgWhite).Printf(" Skipped: ")
color.New(color.FgYellow).Printf("%d\n", summary.Skipped)
}
if summary.Errors > 0 {
color.New(color.FgWhite).Printf(" Errors: ")
color.New(color.FgRed).Printf("%d\n", summary.Errors)
}
if summary.Timeouts > 0 {
color.New(color.FgWhite).Printf(" Timeouts: ")
color.New(color.FgRed).Printf("%d\n", summary.Timeouts)
}
// Pass rate
passRate := float64(0)
if summary.Total > 0 {
passRate = float64(summary.Passed) / float64(summary.Total) * 100
}
color.New(color.FgWhite).Printf(" Pass Rate: ")
if passRate == 100 {
color.New(color.FgGreen, color.Bold).Printf("%.1f%%\n", passRate)
} else if passRate >= 80 {
color.New(color.FgYellow).Printf("%.1f%%\n", passRate)
} else {
color.New(color.FgRed).Printf("%.1f%%\n", passRate)
}
// Duration
color.New(color.FgWhite).Printf(" Duration: ")
fmt.Printf("%s\n", formatDuration(duration))
// Stability info (if runs > 1)
if summary.RunsPerCase > 1 {
fmt.Println()
color.New(color.FgWhite, color.Bold).Println(" Stability Analysis:")
color.New(color.FgWhite).Printf(" Runs/Case: %d\n", summary.RunsPerCase)
color.New(color.FgWhite).Printf(" Total Runs: %d\n", summary.TotalRuns)
color.New(color.FgWhite).Printf(" Stable Cases: ")
if summary.StableCases == summary.Total {
color.New(color.FgGreen).Printf("%d\n", summary.StableCases)
} else {
color.New(color.FgYellow).Printf("%d\n", summary.StableCases)
}
color.New(color.FgWhite).Printf(" Unstable: ")
if summary.UnstableCases > 0 {
color.New(color.FgRed).Printf("%d\n", summary.UnstableCases)
} else {
fmt.Printf("%d\n", summary.UnstableCases)
}
}
}
// OutputFile prints the output file path
func (w *OutputWriter) OutputFile(path string) {
fmt.Println()
color.New(color.FgWhite).Printf(" Output: ")
color.New(color.FgCyan).Printf("%s\n", path)
}
// FinalResult prints the final result banner
func (w *OutputWriter) FinalResult(passed bool) {
fmt.Println()
if passed {
color.New(color.FgGreen, color.Bold).Println("═══════════════════════════════════════════════════════════════")
color.New(color.FgGreen, color.Bold).Println(" ✨ ALL TESTS PASSED ✨")
color.New(color.FgGreen, color.Bold).Println("═══════════════════════════════════════════════════════════════")
} else {
color.New(color.FgRed, color.Bold).Println("═══════════════════════════════════════════════════════════════")
color.New(color.FgRed, color.Bold).Println(" ❌ TESTS FAILED")
color.New(color.FgRed, color.Bold).Println("═══════════════════════════════════════════════════════════════")
}
fmt.Println()
}
// DirectOutput prints the agent output directly (for development mode)
func (w *OutputWriter) DirectOutput(output interface{}) {
if output == nil {
return
}
// Try to format as JSON if it's a complex type
switch v := output.(type) {
case string:
fmt.Println(v)
case map[string]interface{}, []interface{}:
// Pretty print JSON
jsonBytes, err := jsoniter.MarshalIndent(v, "", " ")
if err != nil {
fmt.Printf("%v\n", output)
} else {
fmt.Println(string(jsonBytes))
}
default:
// Try to marshal as JSON
jsonBytes, err := jsoniter.MarshalIndent(output, "", " ")
if err != nil {
fmt.Printf("%v\n", output)
} else {
fmt.Println(string(jsonBytes))
}
}
}
// StabilityResult prints stability analysis result for a test case
func (w *OutputWriter) StabilityResult(sr *StabilityResult) {
color.New(color.FgWhite).Printf(" [%s] ", sr.ID)
// Pass rate
if sr.PassRate == 100 {
color.New(color.FgGreen).Printf("%.0f%%", sr.PassRate)
} else if sr.PassRate >= 80 {
color.New(color.FgYellow).Printf("%.0f%%", sr.PassRate)
} else {
color.New(color.FgRed).Printf("%.0f%%", sr.PassRate)
}
// Classification
color.New(color.FgHiBlack).Printf(" (%d/%d) ", sr.Passed, sr.Runs)
switch sr.StabilityClass {
case StabilityStable:
color.New(color.FgGreen).Printf("Stable")
case StabilityMostlyStable:
color.New(color.FgYellow).Printf("Mostly Stable")
case StabilityUnstable:
color.New(color.FgRed).Printf("Unstable")
case StabilityHighlyUnstable:
color.New(color.FgRed, color.Bold).Printf("Highly Unstable")
}
// Timing
color.New(color.FgHiBlack).Printf(" avg:%.0fms\n", sr.AvgDurationMs)
}
// Helper functions
func truncateString(s string, maxLen int) string {
// Remove newlines and extra spaces
s = strings.ReplaceAll(s, "\n", " ")
s = strings.ReplaceAll(s, "\r", "")
s = strings.Join(strings.Fields(s), " ")
if len(s) <= maxLen {
return s
}
return s[:maxLen-3] + "..."
}
func formatDuration(d time.Duration) string {
if d < time.Millisecond {
return fmt.Sprintf("%dµs", d.Microseconds())
}
if d < time.Second {
return fmt.Sprintf("%dms", d.Milliseconds())
}
if d < time.Minute {
return fmt.Sprintf("%.1fs", d.Seconds())
}
return fmt.Sprintf("%.1fm", d.Minutes())
}