459 lines
12 KiB
Go
459 lines
12 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package app
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestTimingCollector_Basic(t *testing.T) {
|
|
tc := NewTimingCollector()
|
|
if tc == nil {
|
|
t.Fatal("NewTimingCollector returned nil")
|
|
}
|
|
|
|
// Record some timings
|
|
tc.Record("op1", 10*time.Millisecond)
|
|
tc.Record("op2", 20*time.Millisecond)
|
|
|
|
entries := tc.Entries()
|
|
if len(entries) != 2 {
|
|
t.Errorf("expected 2 entries, got %d", len(entries))
|
|
}
|
|
|
|
// Check ordering
|
|
if entries[0].Name != "op1" {
|
|
t.Errorf("expected first entry to be 'op1', got %q", entries[0].Name)
|
|
}
|
|
if entries[1].Name != "op2" {
|
|
t.Errorf("expected second entry to be 'op2', got %q", entries[1].Name)
|
|
}
|
|
}
|
|
|
|
func TestTimingCollector_StartTimer(t *testing.T) {
|
|
tc := NewTimingCollector()
|
|
|
|
stop := tc.StartTimer("timed_op")
|
|
time.Sleep(5 * time.Millisecond)
|
|
stop()
|
|
|
|
entries := tc.Entries()
|
|
if len(entries) != 1 {
|
|
t.Fatalf("expected 1 entry, got %d", len(entries))
|
|
}
|
|
if entries[0].Name != "timed_op" {
|
|
t.Errorf("expected entry name 'timed_op', got %q", entries[0].Name)
|
|
}
|
|
if entries[0].Duration < 5*time.Millisecond {
|
|
t.Errorf("expected duration >= 5ms, got %v", entries[0].Duration)
|
|
}
|
|
}
|
|
|
|
func TestTimingCollector_NilSafe(t *testing.T) {
|
|
var tc *TimingCollector
|
|
|
|
// Should not panic on nil collector
|
|
tc.Record("op", 10*time.Millisecond)
|
|
stop := tc.StartTimer("op")
|
|
stop()
|
|
_ = tc.Total()
|
|
_ = tc.Entries()
|
|
tc.Print(nil)
|
|
tc.PrintIfEnabled()
|
|
}
|
|
|
|
func TestTimingCollector_Print(t *testing.T) {
|
|
tc := NewTimingCollector()
|
|
tc.Record("auth_token", 44*time.Millisecond)
|
|
tc.Record("mcp_call", 150*time.Millisecond)
|
|
|
|
var buf bytes.Buffer
|
|
tc.Print(&buf)
|
|
|
|
output := buf.String()
|
|
if !strings.Contains(output, "[Perf]") {
|
|
t.Error("output should contain [Perf] header")
|
|
}
|
|
if !strings.Contains(output, "auth_token") {
|
|
t.Error("output should contain 'auth_token'")
|
|
}
|
|
if !strings.Contains(output, "mcp_call") {
|
|
t.Error("output should contain 'mcp_call'")
|
|
}
|
|
if !strings.Contains(output, "Total") {
|
|
t.Error("output should contain 'Total'")
|
|
}
|
|
}
|
|
|
|
func TestTimingCollector_PrintIfEnabled(t *testing.T) {
|
|
// Set environment variable
|
|
os.Setenv(PerfDebugEnv, "1")
|
|
defer os.Unsetenv(PerfDebugEnv)
|
|
|
|
tc := NewTimingCollector()
|
|
tc.Record("test_op", 10*time.Millisecond)
|
|
|
|
// This should not panic and should print to stderr
|
|
tc.PrintIfEnabled()
|
|
}
|
|
|
|
func TestTimingCollector_ContextIntegration(t *testing.T) {
|
|
tc := NewTimingCollector()
|
|
ctx := WithTimingCollector(context.Background(), tc)
|
|
|
|
// Retrieve from context
|
|
retrieved := TimingCollectorFromContext(ctx)
|
|
if retrieved != tc {
|
|
t.Error("TimingCollectorFromContext should return the same collector")
|
|
}
|
|
|
|
// Use convenience functions
|
|
RecordTiming(ctx, "ctx_op", 30*time.Millisecond)
|
|
stop := StartTiming(ctx, "ctx_timed")
|
|
time.Sleep(2 * time.Millisecond)
|
|
stop()
|
|
|
|
entries := tc.Entries()
|
|
if len(entries) != 2 {
|
|
t.Errorf("expected 2 entries, got %d", len(entries))
|
|
}
|
|
}
|
|
|
|
func TestTimingCollectorFromContext_NilContext(t *testing.T) {
|
|
//lint:ignore SA1012 Testing explicit nil-context guard in TimingCollectorFromContext.
|
|
tc := TimingCollectorFromContext(nil)
|
|
if tc != nil {
|
|
t.Error("TimingCollectorFromContext(nil) should return nil")
|
|
}
|
|
}
|
|
|
|
func TestTimingCollectorFromContext_NoCollector(t *testing.T) {
|
|
tc := TimingCollectorFromContext(context.Background())
|
|
if tc != nil {
|
|
t.Error("TimingCollectorFromContext with no collector should return nil")
|
|
}
|
|
}
|
|
|
|
func TestStartTiming_NoCollector(t *testing.T) {
|
|
ctx := context.Background()
|
|
stop := StartTiming(ctx, "no_collector")
|
|
// Should not panic
|
|
stop()
|
|
}
|
|
|
|
func TestIsPerfDebugEnabled(t *testing.T) {
|
|
// Clear the env var first
|
|
os.Unsetenv(PerfDebugEnv)
|
|
|
|
if IsPerfDebugEnabled() {
|
|
t.Error("IsPerfDebugEnabled should return false when env var is not set")
|
|
}
|
|
|
|
os.Setenv(PerfDebugEnv, "1")
|
|
defer os.Unsetenv(PerfDebugEnv)
|
|
|
|
if !IsPerfDebugEnabled() {
|
|
t.Error("IsPerfDebugEnabled should return true when env var is set")
|
|
}
|
|
}
|
|
|
|
// ── PerfReport tests ────────────────────────────────────────────────────
|
|
|
|
func TestBuildReport(t *testing.T) {
|
|
tc := NewTimingCollector()
|
|
tc.Record("cmd_init", 45*time.Millisecond)
|
|
tc.Record("auth_keychain", 72*time.Millisecond)
|
|
tc.Record("mcp_call", 620*time.Millisecond)
|
|
|
|
report := tc.BuildReport("v1.0.8", "dws aitable list-records")
|
|
|
|
if report.Kind != "perf_report" {
|
|
t.Errorf("expected kind 'perf_report', got %q", report.Kind)
|
|
}
|
|
if report.Version != "1" {
|
|
t.Errorf("expected version '1', got %q", report.Version)
|
|
}
|
|
if report.CLIVersion != "v1.0.8" {
|
|
t.Errorf("expected cli_version 'v1.0.8', got %q", report.CLIVersion)
|
|
}
|
|
if report.Command != "dws aitable list-records" {
|
|
t.Errorf("expected command 'dws aitable list-records', got %q", report.Command)
|
|
}
|
|
if len(report.Phases) != 3 {
|
|
t.Fatalf("expected 3 phases, got %d", len(report.Phases))
|
|
}
|
|
if report.Phases[0].Name != "cmd_init" || report.Phases[0].DurationMs != 45 {
|
|
t.Errorf("unexpected first phase: %+v", report.Phases[0])
|
|
}
|
|
if report.Slowest != "mcp_call" {
|
|
t.Errorf("expected slowest 'mcp_call', got %q", report.Slowest)
|
|
}
|
|
if report.TotalMs < 0 {
|
|
t.Errorf("total_ms should be >= 0, got %d", report.TotalMs)
|
|
}
|
|
if report.OverheadMs < 0 {
|
|
t.Errorf("overhead_ms should be >= 0, got %d", report.OverheadMs)
|
|
}
|
|
}
|
|
|
|
func TestBuildReportEmpty(t *testing.T) {
|
|
tc := NewTimingCollector()
|
|
report := tc.BuildReport("dev", "dws version")
|
|
|
|
if len(report.Phases) != 0 {
|
|
t.Errorf("expected 0 phases, got %d", len(report.Phases))
|
|
}
|
|
if report.Slowest != "" {
|
|
t.Errorf("expected empty slowest, got %q", report.Slowest)
|
|
}
|
|
}
|
|
|
|
func TestBuildReportJSON(t *testing.T) {
|
|
tc := NewTimingCollector()
|
|
tc.Record("cmd_init", 10*time.Millisecond)
|
|
|
|
report := tc.BuildReport("v1.0.0", "dws version")
|
|
data, err := json.Marshal(report)
|
|
if err != nil {
|
|
t.Fatalf("json.Marshal failed: %v", err)
|
|
}
|
|
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
t.Fatalf("json.Unmarshal failed: %v", err)
|
|
}
|
|
|
|
requiredKeys := []string{"kind", "version", "cli_version", "command", "timestamp", "total_ms", "phases", "slowest", "overhead_ms"}
|
|
for _, key := range requiredKeys {
|
|
if _, ok := parsed[key]; !ok {
|
|
t.Errorf("missing key %q in JSON output", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWriteReportIfEnabled(t *testing.T) {
|
|
dir := t.TempDir()
|
|
reportPath := filepath.Join(dir, "report.json")
|
|
|
|
t.Setenv(PerfReportEnv, reportPath)
|
|
|
|
tc := NewTimingCollector()
|
|
tc.Record("cmd_init", 50*time.Millisecond)
|
|
tc.Record("mcp_call", 200*time.Millisecond)
|
|
|
|
tc.WriteReportIfEnabled("v1.0.0", "dws version")
|
|
|
|
data, err := os.ReadFile(reportPath)
|
|
if err != nil {
|
|
t.Fatalf("report file not written: %v", err)
|
|
}
|
|
|
|
var report PerfReport
|
|
if err := json.Unmarshal(data, &report); err != nil {
|
|
t.Fatalf("invalid JSON in report: %v", err)
|
|
}
|
|
if report.Kind != "perf_report" {
|
|
t.Errorf("expected kind 'perf_report', got %q", report.Kind)
|
|
}
|
|
if len(report.Phases) != 2 {
|
|
t.Errorf("expected 2 phases, got %d", len(report.Phases))
|
|
}
|
|
}
|
|
|
|
func TestWriteReportIfEnabled_Auto(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
expected := filepath.Join(tmpHome, ".dws", "perf", "latest.json")
|
|
|
|
originalHome := timingUserHomeDir
|
|
timingUserHomeDir = func() (string, error) { return tmpHome, nil }
|
|
t.Cleanup(func() { timingUserHomeDir = originalHome })
|
|
t.Setenv(PerfReportEnv, "auto")
|
|
|
|
tc := NewTimingCollector()
|
|
tc.Record("cmd_init", 10*time.Millisecond)
|
|
tc.WriteReportIfEnabled("v1.0.0", "dws version")
|
|
|
|
if _, err := os.Stat(expected); err != nil {
|
|
t.Fatalf("expected report at %s: %v", expected, err)
|
|
}
|
|
}
|
|
|
|
func TestWriteReportIfEnabled_Disabled(t *testing.T) {
|
|
t.Setenv(PerfReportEnv, "")
|
|
|
|
tc := NewTimingCollector()
|
|
tc.Record("op", 10*time.Millisecond)
|
|
tc.WriteReportIfEnabled("v1.0.0", "dws version")
|
|
// No file should be written; no error expected
|
|
}
|
|
|
|
func TestWriteReportIfEnabled_NilCollector(t *testing.T) {
|
|
t.Setenv(PerfReportEnv, "/tmp/should-not-exist.json")
|
|
var tc *TimingCollector
|
|
tc.WriteReportIfEnabled("v1.0.0", "dws version")
|
|
}
|
|
|
|
func TestLoadLatestReport(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
originalHome := timingUserHomeDir
|
|
timingUserHomeDir = func() (string, error) { return tmpHome, nil }
|
|
t.Cleanup(func() { timingUserHomeDir = originalHome })
|
|
|
|
perfDir := filepath.Join(tmpHome, ".dws", "perf")
|
|
if err := os.MkdirAll(perfDir, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
report := PerfReport{
|
|
Kind: "perf_report",
|
|
Version: "1",
|
|
CLIVersion: "v1.0.0",
|
|
Command: "dws version",
|
|
TotalMs: 100,
|
|
Phases: []PerfPhase{{Name: "cmd_init", DurationMs: 50, Seq: 0}},
|
|
Slowest: "cmd_init",
|
|
OverheadMs: 50,
|
|
}
|
|
data, _ := json.MarshalIndent(report, "", " ")
|
|
if err := os.WriteFile(filepath.Join(perfDir, "latest.json"), data, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
loaded, err := LoadLatestReport()
|
|
if err != nil {
|
|
t.Fatalf("LoadLatestReport failed: %v", err)
|
|
}
|
|
if loaded.CLIVersion != "v1.0.0" {
|
|
t.Errorf("expected cli_version 'v1.0.0', got %q", loaded.CLIVersion)
|
|
}
|
|
if len(loaded.Phases) != 1 {
|
|
t.Errorf("expected 1 phase, got %d", len(loaded.Phases))
|
|
}
|
|
}
|
|
|
|
func TestLoadLatestReport_NotFound(t *testing.T) {
|
|
tmpHome := t.TempDir()
|
|
originalHome := timingUserHomeDir
|
|
timingUserHomeDir = func() (string, error) { return tmpHome, nil }
|
|
t.Cleanup(func() { timingUserHomeDir = originalHome })
|
|
|
|
_, err := LoadLatestReport()
|
|
if err == nil {
|
|
t.Error("expected error when report file does not exist")
|
|
}
|
|
}
|
|
|
|
func TestSanitizeCommand(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
}{
|
|
{
|
|
name: "no sensitive flags",
|
|
args: []string{"dws", "aitable", "list-records"},
|
|
want: "dws aitable list-records",
|
|
},
|
|
{
|
|
name: "token with space-separated value",
|
|
args: []string{"dws", "--token", "secret123", "version"},
|
|
want: "dws --token *** version",
|
|
},
|
|
{
|
|
name: "token with equals sign",
|
|
args: []string{"dws", "--token=secret123", "version"},
|
|
want: "dws --token=*** version",
|
|
},
|
|
{
|
|
name: "client-secret space-separated",
|
|
args: []string{"dws", "--client-secret", "mysecret", "--client-id", "myid", "auth"},
|
|
want: "dws --client-secret *** --client-id *** auth",
|
|
},
|
|
{
|
|
name: "client-id with equals",
|
|
args: []string{"dws", "--client-id=abc123"},
|
|
want: "dws --client-id=***",
|
|
},
|
|
{
|
|
name: "empty args",
|
|
args: []string{},
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := SanitizeCommand(tt.args)
|
|
if got != tt.want {
|
|
t.Errorf("SanitizeCommand(%v) = %q, want %q", tt.args, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolvePerfReportPath_Auto(t *testing.T) {
|
|
p := resolvePerfReportPath("auto")
|
|
if p == "" {
|
|
t.Skip("HOME not available")
|
|
}
|
|
if !strings.HasSuffix(p, filepath.Join("perf", "latest.json")) {
|
|
t.Errorf("expected path ending in perf/latest.json, got %q", p)
|
|
}
|
|
}
|
|
|
|
func TestResolvePerfReportPath_Custom(t *testing.T) {
|
|
p := resolvePerfReportPath("/tmp/my-report.json")
|
|
if p != "/tmp/my-report.json" {
|
|
t.Errorf("expected '/tmp/my-report.json', got %q", p)
|
|
}
|
|
}
|
|
|
|
func TestPrintPerfReportSummary(t *testing.T) {
|
|
report := &PerfReport{
|
|
Command: "dws version",
|
|
Timestamp: time.Now(),
|
|
TotalMs: 300,
|
|
Phases: []PerfPhase{{Name: "cmd_init", DurationMs: 50, Seq: 0}, {Name: "mcp_call", DurationMs: 200, Seq: 1}},
|
|
Slowest: "mcp_call",
|
|
OverheadMs: 50,
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
printPerfReportSummary(&buf, report)
|
|
out := buf.String()
|
|
|
|
if !strings.Contains(out, "cmd_init") {
|
|
t.Error("output should contain 'cmd_init'")
|
|
}
|
|
if !strings.Contains(out, "mcp_call") {
|
|
t.Error("output should contain 'mcp_call'")
|
|
}
|
|
if !strings.Contains(out, "← 最慢") {
|
|
t.Error("output should contain '← 最慢' marker")
|
|
}
|
|
if !strings.Contains(out, "总耗时") {
|
|
t.Error("output should contain '总耗时'")
|
|
}
|
|
if !strings.Contains(out, "框架开销") {
|
|
t.Error("output should contain '框架开销'")
|
|
}
|
|
}
|