From a811f7119d3787cbe708501c3df38ad400265dd8 Mon Sep 17 00:00:00 2001 From: Shark Date: Fri, 17 Oct 2025 21:24:41 +0200 Subject: [PATCH 1/3] initial test262 scheduler --- crates/yavashark_test262/runner/go.mod | 13 +- crates/yavashark_test262/runner/go.sum | 2 + .../runner/progress/progress.go | 151 +++++++++++++++ .../runner/results/results.go | 124 +++++++++++++ crates/yavashark_test262/runner/run/run.go | 66 +++++-- .../runner/scheduler/scheduler.go | 174 ++++++++++++++++++ 6 files changed, 514 insertions(+), 16 deletions(-) create mode 100644 crates/yavashark_test262/runner/go.sum create mode 100644 crates/yavashark_test262/runner/progress/progress.go create mode 100644 crates/yavashark_test262/runner/scheduler/scheduler.go diff --git a/crates/yavashark_test262/runner/go.mod b/crates/yavashark_test262/runner/go.mod index 45a456b22..4237b9df2 100644 --- a/crates/yavashark_test262/runner/go.mod +++ b/crates/yavashark_test262/runner/go.mod @@ -2,4 +2,15 @@ module yavashark_test262_runner go 1.23.2 -require github.com/BurntSushi/toml v1.5.0 // indirect +require ( + github.com/BurntSushi/toml v1.5.0 + github.com/schollz/progressbar/v3 v3.14.1 +) + +require ( + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 // indirect +) diff --git a/crates/yavashark_test262/runner/go.sum b/crates/yavashark_test262/runner/go.sum new file mode 100644 index 000000000..ff7fd092f --- /dev/null +++ b/crates/yavashark_test262/runner/go.sum @@ -0,0 +1,2 @@ +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= diff --git a/crates/yavashark_test262/runner/progress/progress.go b/crates/yavashark_test262/runner/progress/progress.go new file mode 100644 index 000000000..84191fe75 --- /dev/null +++ b/crates/yavashark_test262/runner/progress/progress.go @@ -0,0 +1,151 @@ +package progress + +import ( + "fmt" + "sync" + "sync/atomic" + "yavashark_test262_runner/status" +) + +const ( + ColorGreen = "\033[32m" + ColorRed = "\033[31m" + ColorYellow = "\033[33m" + ColorBlue = "\033[34m" + ColorMagenta = "\033[35m" + ColorCyan = "\033[36m" + ColorReset = "\033[0m" + ColorGray = "\033[90m" +) + +type ProgressTracker struct { + mu sync.Mutex + + passed atomic.Uint32 + failed atomic.Uint32 + skipped atomic.Uint32 + timeout atomic.Uint32 + crashed atomic.Uint32 + parseError atomic.Uint32 + parseSuccessError atomic.Uint32 + notImplemented atomic.Uint32 + runnerError atomic.Uint32 + total atomic.Uint32 + lastPrintedProgress atomic.Uint32 + + totalTests uint32 +} + +func NewProgressTracker(totalTests uint32) *ProgressTracker { + return &ProgressTracker{ + totalTests: totalTests, + } +} + +func (pt *ProgressTracker) Add(s status.Status) { + switch s { + case status.PASS: + pt.passed.Add(1) + case status.FAIL: + pt.failed.Add(1) + case status.SKIP: + pt.skipped.Add(1) + case status.TIMEOUT: + pt.timeout.Add(1) + case status.CRASH: + pt.crashed.Add(1) + case status.PARSE_ERROR: + pt.parseError.Add(1) + case status.PARSE_SUCCESS_ERROR: + pt.parseSuccessError.Add(1) + case status.NOT_IMPLEMENTED: + pt.notImplemented.Add(1) + case status.RUNNER_ERROR: + pt.runnerError.Add(1) + } + + current := pt.total.Add(1) + pt.updateProgress(current) +} + +func (pt *ProgressTracker) updateProgress(current uint32) { + lastPrinted := pt.lastPrintedProgress.Load() + + threshold := uint32(100) + if pt.totalTests > 0 { + percentThreshold := pt.totalTests / 50 // 2% + if percentThreshold > threshold { + threshold = percentThreshold + } + } + + if current-lastPrinted >= threshold || current == pt.totalTests { + if pt.lastPrintedProgress.CompareAndSwap(lastPrinted, current) { + pt.printProgressBar(current) + } + } +} + +func (pt *ProgressTracker) printProgressBar(current uint32) { + passed := pt.passed.Load() + failed := pt.failed.Load() + skipped := pt.skipped.Load() + timeout := pt.timeout.Load() + crashed := pt.crashed.Load() + + barWidth := 50 + passedWidth := int(float64(passed) / float64(current) * float64(barWidth)) + failedWidth := int(float64(failed) / float64(current) * float64(barWidth)) + skippedWidth := int(float64(skipped) / float64(current) * float64(barWidth)) + timeoutWidth := int(float64(timeout) / float64(current) * float64(barWidth)) + crashedWidth := int(float64(crashed) / float64(current) * float64(barWidth)) + otherWidth := barWidth - passedWidth - failedWidth - skippedWidth - timeoutWidth - crashedWidth + + bar := "" + if passedWidth > 0 { + bar += ColorGreen + repeatChar("█", passedWidth) + ColorReset + } + if failedWidth > 0 { + bar += ColorRed + repeatChar("█", failedWidth) + ColorReset + } + if timeoutWidth > 0 { + bar += ColorYellow + repeatChar("█", timeoutWidth) + ColorReset + } + if crashedWidth > 0 { + bar += ColorMagenta + repeatChar("█", crashedWidth) + ColorReset + } + if skippedWidth > 0 { + bar += ColorCyan + repeatChar("█", skippedWidth) + ColorReset + } + if otherWidth > 0 { + bar += ColorGray + repeatChar("░", otherWidth) + ColorReset + } + + percentage := float64(current) / float64(pt.totalTests) * 100 + + fmt.Printf("\r[%s] %3d%% (%d/%d) | %sP:%d%s %sF:%d%s %sT:%d%s %sC:%d%s %sS:%d%s", + bar, + int(percentage), + current, + pt.totalTests, + ColorGreen, passed, ColorReset, + ColorRed, failed, ColorReset, + ColorYellow, timeout, ColorReset, + ColorMagenta, crashed, ColorReset, + ColorCyan, skipped, ColorReset, + ) +} + +func (pt *ProgressTracker) GetStats() (passed, failed, skipped, timeout, crashed, parseError, parseSuccessError, notImplemented, runnerError, total uint32) { + return pt.passed.Load(), pt.failed.Load(), pt.skipped.Load(), pt.timeout.Load(), pt.crashed.Load(), + pt.parseError.Load(), pt.parseSuccessError.Load(), pt.notImplemented.Load(), pt.runnerError.Load(), + pt.total.Load() +} + +func repeatChar(char string, count int) string { + result := "" + for i := 0; i < count; i++ { + result += char + } + return result +} diff --git a/crates/yavashark_test262/runner/results/results.go b/crates/yavashark_test262/runner/results/results.go index ca18fc23e..6a869e73e 100644 --- a/crates/yavashark_test262/runner/results/results.go +++ b/crates/yavashark_test262/runner/results/results.go @@ -8,6 +8,18 @@ import ( "yavashark_test262_runner/status" ) +const ( + ColorGreen = "\033[32m" + ColorRed = "\033[31m" + ColorYellow = "\033[33m" + ColorBlue = "\033[34m" + ColorMagenta = "\033[35m" + ColorCyan = "\033[36m" + ColorReset = "\033[0m" + ColorBold = "\033[1m" + ColorDim = "\033[2m" +) + type TestResults struct { TestResults []Result Passed uint32 @@ -212,7 +224,119 @@ func (tr *TestResults) Compare(other *TestResults) { printDiff("Timeout", tr.Timeout, other.Timeout, tr.Total) printDiff("Parse Error", tr.ParseError, other.ParseError, tr.Total) printDiff("Total", tr.Total, other.Total, tr.Total) +} + +func (tr *TestResults) PrintResultsWithDiff(other *TestResults) { + fmt.Printf("\n%s=== Test Results Summary ===%s\n\n", ColorBold, ColorReset) + + fmt.Printf("%sCurrent Run:%s\n", ColorBold, ColorReset) + tr.printResultsLine("Passed", tr.Passed, tr.Total, other.Passed) + tr.printResultsLine("Failed", tr.Failed, tr.Total, other.Failed) + tr.printResultsLine("Timeout", tr.Timeout, tr.Total, other.Timeout) + tr.printResultsLine("Crashed", tr.Crashed, tr.Total, other.Crashed) + tr.printResultsLine("Skipped", tr.Skipped, tr.Total, other.Skipped) + tr.printResultsLine("Not Implemented", tr.NotImplemented, tr.Total, other.NotImplemented) + tr.printResultsLine("Runner Error", tr.RunnerError, tr.Total, other.RunnerError) + tr.printResultsLine("Parse Error", tr.ParseError, tr.Total, other.ParseError) + tr.printResultsLine("Parse Success Error", tr.ParseSuccessError, tr.Total, other.ParseSuccessError) + + fmt.Printf("\n%sTotal: %d%s\n", ColorBold, tr.Total, ColorReset) + + fmt.Printf("\n%s=== Net Changes ===%s\n", ColorBold, ColorReset) + netPassed := int32(tr.Passed) - int32(other.Passed) + netFailed := int32(tr.Failed) - int32(other.Failed) + netTimeout := int32(tr.Timeout) - int32(other.Timeout) + netCrashed := int32(tr.Crashed) - int32(other.Crashed) + + if netPassed != 0 { + if netPassed > 0 { + fmt.Printf("%s✓ Passed: +%d%s (gained)\n", ColorGreen, netPassed, ColorReset) + } else { + fmt.Printf("%s✗ Passed: %d%s (lost)\n", ColorRed, netPassed, ColorReset) + } + } + + if netFailed != 0 { + if netFailed > 0 { + fmt.Printf("%s✗ Failed: +%d%s (gained)\n", ColorRed, netFailed, ColorReset) + } else { + fmt.Printf("%s✓ Failed: %d%s (improved)\n", ColorGreen, netFailed, ColorReset) + } + } + + if netTimeout != 0 { + if netTimeout > 0 { + fmt.Printf("%s⏱ Timeout: +%d%s (gained)\n", ColorYellow, netTimeout, ColorReset) + } else { + fmt.Printf("%s✓ Timeout: %d%s (improved)\n", ColorGreen, netTimeout, ColorReset) + } + } + + if netCrashed != 0 { + if netCrashed > 0 { + fmt.Printf("%s💥 Crashed: +%d%s (gained)\n", ColorMagenta, netCrashed, ColorReset) + } else { + fmt.Printf("%s✓ Crashed: %d%s (improved)\n", ColorGreen, netCrashed, ColorReset) + } + } + + // Overall summary + fmt.Printf("\n%s=== Overall Summary ===%s\n", ColorBold, ColorReset) + totalChanges := abs(netPassed) + abs(netFailed) + abs(netTimeout) + abs(netCrashed) + if totalChanges > 0 { + fmt.Printf("Total test status changes: %d\n", totalChanges) + + passedGained := 0 + if netPassed > 0 { + passedGained = int(netPassed) + } + failedLost := 0 + if netFailed < 0 { + failedLost = int(-netFailed) + } + improvements := passedGained + failedLost + if improvements > 0 { + fmt.Printf("%s↑ Improvements: %d%s\n", ColorGreen, improvements, ColorReset) + } + + failedGained := 0 + if netFailed > 0 { + failedGained = int(netFailed) + } + passedLost := 0 + if netPassed < 0 { + passedLost = int(-netPassed) + } + regressions := failedGained + passedLost + + if regressions > 0 { + fmt.Printf("%s↓ Regressions: %d%s\n", ColorRed, regressions, ColorReset) + } + } else { + fmt.Printf("%sNo changes from previous run%s\n", ColorDim, ColorReset) + } +} + +func (tr *TestResults) printResultsLine(name string, current, total, previous uint32) { + percentage := float64(current) / float64(total) * 100 + diff := int32(current) - int32(previous) + + var diffStr string + if diff > 0 { + diffStr = fmt.Sprintf(" %s(+%d)%s", ColorGreen, diff, ColorReset) + } else if diff < 0 { + diffStr = fmt.Sprintf(" %s(%d)%s", ColorRed, diff, ColorReset) + } + + fmt.Printf(" %s: %d (%.2f%%)%s\n", name, current, percentage, diffStr) +} + +func abs(n int32) int32 { + if n < 0 { + return -n + } + return n } func printDiff(name string, n1 uint32, n2 uint32, total uint32) { diff --git a/crates/yavashark_test262/runner/run/run.go b/crates/yavashark_test262/runner/run/run.go index 664c047ee..c853575ed 100644 --- a/crates/yavashark_test262/runner/run/run.go +++ b/crates/yavashark_test262/runner/run/run.go @@ -1,13 +1,16 @@ package run import ( + "fmt" "log" "os" "path/filepath" "strings" "sync" "time" + "yavashark_test262_runner/progress" "yavashark_test262_runner/results" + "yavashark_test262_runner/scheduler" "yavashark_test262_runner/status" "yavashark_test262_runner/worker" ) @@ -26,24 +29,28 @@ func TestsInDir(testRoot string, workers int, skips bool, timings bool) *results wg.Add(workers) + num := countTests(testRoot) + progressTracker := progress.NewProgressTracker(num) + for i := range workers { go worker.Worker(i, jobs, resultsChan, wg, timings) } - num := countTests(testRoot) - testResults := results.New(num) + // Goroutine to process results and update progress go func() { for res := range resultsChan { testResults.Add(res) + progressTracker.Add(res.Status) } }() - now := time.Now() + var testPaths []string + var skippedPaths []string + _ = filepath.Walk(testRoot, func(path string, info os.FileInfo, err error) error { if err != nil { - //log.Printf("Failed to get file info for %s: %v", path, err) return nil } @@ -61,30 +68,59 @@ func TestsInDir(testRoot string, workers int, skips bool, timings bool) *results return nil } + shouldSkip := false if skips { for _, skip := range SKIP { if strings.HasPrefix(p, skip) { - resultsChan <- results.Result{ - Status: status.SKIP, - Msg: "skip", - Path: path, - MemoryKB: 0, - Duration: 0, - } - - return nil + skippedPaths = append(skippedPaths, path) + shouldSkip = true + break } } } - jobs <- path + if !shouldSkip { + testPaths = append(testPaths, path) + } return nil }) - close(jobs) + timingData := scheduler.LoadTestTimings("results.json") + + scheduler.EnrichTimingsWithFallback(timingData, testPaths) + + min, max, avg, fastCount, mediumCount, slowCount, riskCount := scheduler.GetStatistics(timingData) + log.Printf("Timing statistics - Min: %v, Max: %v, Avg: %v", min, max, avg) + log.Printf("Test distribution - Fast: %d, Medium: %d, Slow: %d, Risky: %d", + fastCount, mediumCount, slowCount, riskCount) + + scheduledJobs := scheduler.ScheduleTests(testPaths, timingData) + + now := time.Now() + + go func() { + for _, job := range scheduledJobs { + jobs <- job.Path + } + + for _, path := range skippedPaths { + resultsChan <- results.Result{ + Status: status.SKIP, + Msg: "skip", + Path: path, + MemoryKB: 0, + Duration: 0, + } + } + + close(jobs) + }() wg.Wait() + + fmt.Printf("\n") + log.Printf("Finished running %d tests in %s", num, time.Since(now).String()) close(resultsChan) diff --git a/crates/yavashark_test262/runner/scheduler/scheduler.go b/crates/yavashark_test262/runner/scheduler/scheduler.go new file mode 100644 index 000000000..acc255c86 --- /dev/null +++ b/crates/yavashark_test262/runner/scheduler/scheduler.go @@ -0,0 +1,174 @@ +package scheduler + +import ( + "encoding/json" + "log" + "os" + "sort" + "time" +) + +type TestJob struct { + Path string + EstimatedTime time.Duration + Priority int +} + +type StoredResult struct { + Status string `json:"status"` + Msg string `json:"msg"` + Path string `json:"path"` + MemoryKB uint64 `json:"memory_kb"` + Duration time.Duration `json:"duration"` +} + +const ( + // Priority levels + PRIORITY_FAST = 0 // Tests < 100ms based on history + PRIORITY_MEDIUM = 1 // Tests 100ms - 1s + PRIORITY_SLOW = 2 // Tests 1s - 5s + PRIORITY_SLOW_RISK = 3 // Tests > 5s or timeout/crash +) + +// ScheduleTests sorts tests intelligently using historical timing data +func ScheduleTests(testPaths []string, timings map[string]time.Duration) []TestJob { + jobs := make([]TestJob, len(testPaths)) + + for i, path := range testPaths { + estimatedTime := timings[path] + priority := calculatePriorityFromTiming(estimatedTime) + + jobs[i] = TestJob{ + Path: path, + EstimatedTime: estimatedTime, + Priority: priority, + } + } + + sort.Slice(jobs, func(i, j int) bool { + if jobs[i].Priority != jobs[j].Priority { + return jobs[i].Priority < jobs[j].Priority + } + if jobs[i].EstimatedTime != jobs[j].EstimatedTime { + return jobs[i].EstimatedTime < jobs[j].EstimatedTime + } + return jobs[i].Path < jobs[j].Path + }) + + return jobs +} + +func calculatePriorityFromTiming(duration time.Duration) int { + if duration == 0 { + return PRIORITY_MEDIUM + } + + if duration > 5*time.Second { + return PRIORITY_SLOW_RISK + } else if duration > 1*time.Second { + return PRIORITY_SLOW + } else if duration > 100*time.Millisecond { + return PRIORITY_MEDIUM + } + return PRIORITY_FAST +} + +func LoadTestTimings(resultsPath string) map[string]time.Duration { + timings := make(map[string]time.Duration) + + contents, err := os.ReadFile(resultsPath) + if err != nil { + if os.IsNotExist(err) { + log.Printf("No previous results found at %s, using default priorities", resultsPath) + return timings + } + log.Printf("Failed to read results file %s: %v", resultsPath, err) + return timings + } + + var results []StoredResult + err = json.Unmarshal(contents, &results) + if err != nil { + log.Printf("Failed to parse results JSON: %v", err) + return timings + } + + for _, result := range results { + path := result.Path + + if result.Status == "TIMEOUT" { + timings[path] = 30 * time.Second + } else if result.Status == "CRASH" { + timings[path] = 20 * time.Second + } else if result.Duration > 0 { + timings[path] = result.Duration + } else { + timings[path] = 500 * time.Millisecond + } + } + + log.Printf("Loaded timing data for %d tests from %s", len(timings), resultsPath) + return timings +} + +func EstimateTimingFromFileSize(path string) time.Duration { + fileInfo, err := os.Stat(path) + if err != nil { + return 500 * time.Millisecond + } + + sizeKB := fileInfo.Size() / 1024 + + if sizeKB > 100 { + return 10 * time.Second + } else if sizeKB > 50 { + return 5 * time.Second + } else if sizeKB > 20 { + return 1 * time.Second + } else if sizeKB > 5 { + return 200 * time.Millisecond + } + return 50 * time.Millisecond +} + +func EnrichTimingsWithFallback(timings map[string]time.Duration, testPaths []string) { + for _, path := range testPaths { + if _, exists := timings[path]; !exists { + timings[path] = EstimateTimingFromFileSize(path) + } + } +} + +func GetStatistics(timings map[string]time.Duration) (min, max, avg time.Duration, fastCount, mediumCount, slowCount, riskCount int) { + if len(timings) == 0 { + return + } + + var total time.Duration + + for _, duration := range timings { + total += duration + + priority := calculatePriorityFromTiming(duration) + switch priority { + case PRIORITY_FAST: + fastCount++ + case PRIORITY_MEDIUM: + mediumCount++ + case PRIORITY_SLOW: + slowCount++ + case PRIORITY_SLOW_RISK: + riskCount++ + } + + if min == 0 || duration < min { + min = duration + } + if duration > max { + max = duration + } + } + + avg = total / time.Duration(len(timings)) + return +} From 6c16fb6a8cbedbf49efa5f592efa362acb63b0bb Mon Sep 17 00:00:00 2001 From: Shark Date: Sun, 14 Dec 2025 20:33:47 +0100 Subject: [PATCH 2/3] make test runner better --- crates/yavashark_test262/runner/ci/ci.go | 4 +- crates/yavashark_test262/runner/config.go | 151 ++++- crates/yavashark_test262/runner/go.mod | 17 +- crates/yavashark_test262/runner/go.sum | 6 +- crates/yavashark_test262/runner/main.go | 14 +- .../runner/progress/progress.go | 586 ++++++++++++++++-- .../runner/results/results.go | 7 +- crates/yavashark_test262/runner/run/run.go | 55 +- .../runner/scheduler/scheduler.go | 32 +- crates/yavashark_test262/runner/test/test.go | 10 +- .../viewer/router/handler.go | 21 +- 11 files changed, 783 insertions(+), 120 deletions(-) diff --git a/crates/yavashark_test262/runner/ci/ci.go b/crates/yavashark_test262/runner/ci/ci.go index d841e5001..96806c10c 100644 --- a/crates/yavashark_test262/runner/ci/ci.go +++ b/crates/yavashark_test262/runner/ci/ci.go @@ -75,7 +75,7 @@ func runCI(testResults *results.TestResults, overall Summary, repo string, histo if diff { tooManyFailures = printCiDiff(filepath.Join(repo, "results.json"), testResults, root) } else { - testResults.PrintResults() + testResults.PrintResults(false) } if err := results.WriteCIResultsPath(testResults.TestResults, filepath.Join(repo, "results.json"), root); err != nil { @@ -271,7 +271,7 @@ func printCiDiff(path string, testResults *results.TestResults, root string) boo fmt.Println() fmt.Println("<=== Test Results ===>") - testResults.PrintResults() + testResults.PrintResults(false) fmt.Println() fmt.Println("<=== Comparison ===>") diff --git a/crates/yavashark_test262/runner/config.go b/crates/yavashark_test262/runner/config.go index a55cb2c8c..c5fd4ed88 100644 --- a/crates/yavashark_test262/runner/config.go +++ b/crates/yavashark_test262/runner/config.go @@ -1,24 +1,54 @@ package main import ( + "encoding/json" "flag" + "fmt" "log" "os" + "time" +) - "github.com/BurntSushi/toml" +const ( + DEFAULT_PROFILE_FILE = "profiles.json" ) type Config struct { - CI bool `toml:"ci"` - RepoPath string `toml:"repo_path"` - HistoryOnly bool `toml:"history_only"` - Workers int `toml:"workers"` - TestRootDir string `toml:"test_root_dir"` - Diff bool `toml:"diff"` - DiffFilter string `toml:"diff_filter"` - TestDir string `toml:"test_dir"` - Skips bool `toml:"skips"` - Timings bool `toml:"timings"` + CI bool `json:"ci"` + RepoPath string `json:"repo_path"` + HistoryOnly bool `json:"history_only"` + Workers int `json:"workers"` + TestRootDir string `json:"test_root_dir"` + Diff bool `json:"diff"` + DiffFilter string `json:"diff_filter"` + TestDir string `json:"test_dir"` + Skips bool `json:"skips"` + Timings bool `json:"timings"` + Timeout time.Duration `json:"timeout"` + Interactive bool `json:"interactive"` + ShowStats bool `json:"show_stats"` + Verbose bool `json:"verbose"` +} + +type ProfileConfig struct { + Profiles map[string]Profile `json:"profiles"` +} + +type Profile struct { + CI *bool `json:"ci,omitempty"` + RepoPath *string `json:"repo_path,omitempty"` + HistoryOnly *bool `json:"history_only,omitempty"` + Workers *int `json:"workers,omitempty"` + TestRootDir *string `json:"test_root,omitempty"` + Diff *bool `json:"diff,omitempty"` + DiffFilter *string `json:"diff_filter,omitempty"` + TestDir *string `json:"test_dir,omitempty"` + NoSkip *bool `json:"noskip,omitempty"` + Timings *bool `json:"timings,omitempty"` + Timeout *string `json:"timeout,omitempty"` + Interactive *bool `json:"interactive,omitempty"` + ShowStats *bool `json:"show_stats,omitempty"` + Verbose *bool `json:"verbose,omitempty"` } func NewConfig() *Config { @@ -33,13 +63,18 @@ func NewConfig() *Config { TestDir: "", Skips: true, Timings: false, + Timeout: 30 * time.Second, + Interactive: false, + ShowStats: false, + Verbose: false, } } func LoadConfig() *Config { config := NewConfig() - configFile := flag.String("config", "config.toml", "Path to TOML config file") + profileFile := flag.String("profiles", DEFAULT_PROFILE_FILE, "Path to JSON profiles file") + profile := flag.String("p", "", "Profile name to load from profiles file") ciEnabled := flag.Bool("ci", config.CI, "Enable CI mode to commit results") repoPath := flag.String("repo", config.RepoPath, "Path to external repository for CI results") historyOnly := flag.Bool("history-only", config.HistoryOnly, "Only generate the history file (skip git commit)") @@ -48,19 +83,23 @@ func LoadConfig() *Config { diff := flag.Bool("diff", config.Diff, "Diff to use for CI results") diffFilter := flag.String("dfilter", config.DiffFilter, "Diff filter to use for CI results") testdir := flag.String("testdir", config.TestDir, "Path in the test directory") - noskip := flag.Bool("noskip", false, "Path in the test directory") + noskip := flag.Bool("noskip", false, "Disable skipping of certain test directories") timings := flag.Bool("timings", false, "Attempt to parse timings from test output (if enabled)") + timeout := flag.Duration("timeout", config.Timeout, "Timeout for each test (e.g., 30s, 1m)") + interactive := flag.Bool("i", false, "Enable interactive TUI mode") + showStats := flag.Bool("stats", false, "Show memory and timing statistics") + verbose := flag.Bool("v", false, "Show verbose output (detailed results)") flag.Parse() - if *configFile != "" { - if err := loadConfigFile(*configFile, config); err != nil { - if !os.IsNotExist(err) { - log.Fatalf("Failed to load config file %s: %v", *configFile, err) - } + // Load profile if specified + if *profile != "" { + if err := loadProfile(*profileFile, *profile, config); err != nil { + log.Printf("Warning: Failed to load profile '%s': %v", *profile, err) } } + // Override with command-line flags (flags take precedence over profile) flag.Visit(func(f *flag.Flag) { switch f.Name { case "ci": @@ -83,13 +122,83 @@ func LoadConfig() *Config { config.Skips = !*noskip case "timings": config.Timings = *timings + case "timeout": + config.Timeout = *timeout + case "i": + config.Interactive = *interactive + case "stats": + config.ShowStats = *showStats + case "v": + config.Verbose = *verbose } }) return config } -func loadConfigFile(filename string, config *Config) error { - _, err := toml.DecodeFile(filename, config) - return err +func loadProfile(filename string, profileName string, config *Config) error { + contents, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read profiles file: %w", err) + } + + var profileConfig ProfileConfig + if err := json.Unmarshal(contents, &profileConfig); err != nil { + return fmt.Errorf("failed to parse profiles file: %w", err) + } + + profile, exists := profileConfig.Profiles[profileName] + if !exists { + return fmt.Errorf("profile '%s' not found in profiles file", profileName) + } + + // Apply profile settings to config + if profile.CI != nil { + config.CI = *profile.CI + } + if profile.RepoPath != nil { + config.RepoPath = *profile.RepoPath + } + if profile.HistoryOnly != nil { + config.HistoryOnly = *profile.HistoryOnly + } + if profile.Workers != nil { + config.Workers = *profile.Workers + } + if profile.TestRootDir != nil { + config.TestRootDir = *profile.TestRootDir + } + if profile.Diff != nil { + config.Diff = *profile.Diff + } + if profile.DiffFilter != nil { + config.DiffFilter = *profile.DiffFilter + } + if profile.TestDir != nil { + config.TestDir = *profile.TestDir + } + if profile.NoSkip != nil { + config.Skips = !*profile.NoSkip + } + if profile.Timings != nil { + config.Timings = *profile.Timings + } + if profile.Timeout != nil { + duration, err := time.ParseDuration(*profile.Timeout) + if err != nil { + return fmt.Errorf("invalid timeout in profile: %w", err) + } + config.Timeout = duration + } + if profile.Interactive != nil { + config.Interactive = *profile.Interactive + } + if profile.ShowStats != nil { + config.ShowStats = *profile.ShowStats + } + if profile.Verbose != nil { + config.Verbose = *profile.Verbose + } + + return nil } diff --git a/crates/yavashark_test262/runner/go.mod b/crates/yavashark_test262/runner/go.mod index 4237b9df2..88fc53537 100644 --- a/crates/yavashark_test262/runner/go.mod +++ b/crates/yavashark_test262/runner/go.mod @@ -1,16 +1,9 @@ module yavashark_test262_runner -go 1.23.2 +go 1.24.0 -require ( - github.com/BurntSushi/toml v1.5.0 - github.com/schollz/progressbar/v3 v3.14.1 -) +toolchain go1.24.6 -require ( - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect - github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect -) +require golang.org/x/term v0.38.0 + +require golang.org/x/sys v0.39.0 // indirect diff --git a/crates/yavashark_test262/runner/go.sum b/crates/yavashark_test262/runner/go.sum index ff7fd092f..08d079ac9 100644 --- a/crates/yavashark_test262/runner/go.sum +++ b/crates/yavashark_test262/runner/go.sum @@ -1,2 +1,4 @@ -github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= -github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= diff --git a/crates/yavashark_test262/runner/main.go b/crates/yavashark_test262/runner/main.go index b8e9518a2..9cb2c9bf5 100644 --- a/crates/yavashark_test262/runner/main.go +++ b/crates/yavashark_test262/runner/main.go @@ -19,7 +19,15 @@ func main() { testRoot := filepath.Join(config.TestRootDir, config.TestDir) - testResults := run.TestsInDir(testRoot, config.Workers, config.Skips, config.Timings) + runConfig := run.RunConfig{ + Workers: config.Workers, + Skips: config.Skips, + Timings: config.Timings, + Timeout: config.Timeout, + Interactive: config.Interactive, + } + + testResults := run.TestsInDir(testRoot, runConfig) if config.Diff && !config.CI { printDiff(testResults, config.DiffFilter) @@ -27,8 +35,8 @@ func main() { if config.CI { ci.RunCi(testResults, config.RepoPath, config.HistoryOnly, config.Diff, testRoot) - } else { - testResults.PrintResults() + } else if config.Verbose { + testResults.PrintResults(config.ShowStats) print("\n\n\n") _ = testResults.ComparePrev() diff --git a/crates/yavashark_test262/runner/progress/progress.go b/crates/yavashark_test262/runner/progress/progress.go index 84191fe75..aeed7f602 100644 --- a/crates/yavashark_test262/runner/progress/progress.go +++ b/crates/yavashark_test262/runner/progress/progress.go @@ -2,22 +2,115 @@ package progress import ( "fmt" + "os" + "strings" "sync" "sync/atomic" "yavashark_test262_runner/status" + + "golang.org/x/term" +) + +const ( + ColorReset = "\033[0m" + ColorBold = "\033[1m" + + // Foreground colors + FgGray = "\033[90m" + FgBrightGreen = "\033[92m" + FgBrightRed = "\033[91m" ) const ( - ColorGreen = "\033[32m" - ColorRed = "\033[31m" - ColorYellow = "\033[33m" - ColorBlue = "\033[34m" - ColorMagenta = "\033[35m" - ColorCyan = "\033[36m" - ColorReset = "\033[0m" - ColorGray = "\033[90m" + BlockFull = "█" + Block7_8 = "▉" + Block6_8 = "▊" + Block5_8 = "▋" + Block4_8 = "▌" + Block3_8 = "▍" + Block2_8 = "▎" + Block1_8 = "▏" +) + +// Unicode arrows +const ( + ArrowUp = "↑" + ArrowDown = "↓" + ArrowRight = "→" +) + +// RGB color type +type RGB struct { + R, G, B int +} + +// Status colors for progress bar +var ( + colorPass = RGB{76, 175, 80} // Green + colorFail = RGB{244, 67, 54} // Red + colorTimeout = RGB{255, 193, 7} // Yellow/Amber + colorCrash = RGB{156, 39, 176} // Purple + colorSkip = RGB{0, 188, 212} // Cyan + colorOther = RGB{33, 150, 243} // Blue + colorEmpty = RGB{60, 60, 60} // Dark gray +) + +type PillStyle struct { + Left RGB + Right RGB + Text RGB +} + +var ( + pillStylePass = PillStyle{ + Left: RGB{67, 160, 71}, + Right: RGB{129, 199, 132}, + Text: RGB{27, 94, 32}, + } + pillStyleFail = PillStyle{ + Left: RGB{229, 57, 53}, + Right: RGB{239, 154, 154}, + Text: RGB{255, 255, 255}, + } + pillStyleSkip = PillStyle{ + Left: RGB{0, 172, 193}, + Right: RGB{128, 222, 234}, + Text: RGB{0, 77, 64}, + } + pillStyleTimeout = PillStyle{ + Left: RGB{255, 179, 0}, + Right: RGB{255, 224, 130}, + Text: RGB{62, 39, 35}, + } + pillStyleCrash = PillStyle{ + Left: RGB{142, 36, 170}, + Right: RGB{206, 147, 216}, + Text: RGB{255, 255, 255}, + } + pillStyleParseError = PillStyle{ + Left: RGB{30, 136, 229}, + Right: RGB{144, 202, 249}, + Text: RGB{13, 71, 161}, + } + pillStyleNotImpl = PillStyle{ + Left: RGB{117, 117, 117}, + Right: RGB{189, 189, 189}, + Text: RGB{33, 33, 33}, + } + pillStyleRunnerError = PillStyle{ + Left: RGB{97, 97, 97}, + Right: RGB{158, 158, 158}, + Text: RGB{250, 250, 250}, + } ) +type RecentChange struct { + Path string + Status status.Status + PrevStatus status.Status + IsNew bool +} + type ProgressTracker struct { mu sync.Mutex @@ -33,16 +126,47 @@ type ProgressTracker struct { total atomic.Uint32 lastPrintedProgress atomic.Uint32 - totalTests uint32 + totalTests uint32 + interactive bool + + prevResults map[string]status.Status + + passGained atomic.Int32 + passLost atomic.Int32 + failGained atomic.Int32 + failLost atomic.Int32 + + recentChanges []RecentChange + changesMu sync.Mutex } -func NewProgressTracker(totalTests uint32) *ProgressTracker { - return &ProgressTracker{ - totalTests: totalTests, +func NewProgressTracker(totalTests uint32, interactive bool, prevResults map[string]status.Status) *ProgressTracker { + pt := &ProgressTracker{ + totalTests: totalTests, + interactive: interactive, + prevResults: prevResults, + recentChanges: make([]RecentChange, 0), } + + if interactive { + pt.enterInteractiveMode() + } + + return pt } -func (pt *ProgressTracker) Add(s status.Status) { +func (pt *ProgressTracker) enterInteractiveMode() { + fmt.Print("\033[?1049h") + fmt.Print("\033[?25l") + fmt.Print("\033[2J") +} + +func (pt *ProgressTracker) exitInteractiveMode() { + fmt.Print("\033[?25h") + fmt.Print("\033[?1049l") +} + +func (pt *ProgressTracker) Add(s status.Status, path string) { switch s { case status.PASS: pt.passed.Add(1) @@ -64,6 +188,40 @@ func (pt *ProgressTracker) Add(s status.Status) { pt.runnerError.Add(1) } + if pt.prevResults != nil { + prevStatus, existed := pt.prevResults[path] + + if existed && prevStatus != s { + if prevStatus == status.PASS && s != status.PASS { + pt.passLost.Add(1) + } + if prevStatus != status.PASS && s == status.PASS { + pt.passGained.Add(1) + } + if prevStatus == status.FAIL && s != status.FAIL { + pt.failLost.Add(1) + } + if prevStatus != status.FAIL && s == status.FAIL { + pt.failGained.Add(1) + } + + if pt.interactive && (s == status.PASS || s == status.FAIL || prevStatus == status.PASS || prevStatus == status.FAIL) { + change := RecentChange{ + Path: path, + Status: s, + PrevStatus: prevStatus, + IsNew: !existed, + } + pt.changesMu.Lock() + pt.recentChanges = append(pt.recentChanges, change) + if len(pt.recentChanges) > 20 { + pt.recentChanges = pt.recentChanges[len(pt.recentChanges)-20:] + } + pt.changesMu.Unlock() + } + } + } + current := pt.total.Add(1) pt.updateProgress(current) } @@ -73,67 +231,389 @@ func (pt *ProgressTracker) updateProgress(current uint32) { threshold := uint32(100) if pt.totalTests > 0 { - percentThreshold := pt.totalTests / 50 // 2% - if percentThreshold > threshold { - threshold = percentThreshold + if pt.interactive { + percentThreshold := pt.totalTests / 200 + if percentThreshold > threshold { + threshold = percentThreshold + } + } else { + percentThreshold := pt.totalTests / 50 + if percentThreshold > threshold { + threshold = percentThreshold + } } } if current-lastPrinted >= threshold || current == pt.totalTests { if pt.lastPrintedProgress.CompareAndSwap(lastPrinted, current) { - pt.printProgressBar(current) + if pt.interactive { + pt.renderInteractive(current) + } else { + pt.printProgressBar(current) + } } } } -func (pt *ProgressTracker) printProgressBar(current uint32) { +func (pt *ProgressTracker) Finish() { + if !pt.interactive { + pt.printProgressBar(pt.total.Load()) + fmt.Println() + } + + if pt.interactive { + pt.renderInteractive(pt.total.Load()) + pt.exitInteractiveMode() + } + + pt.printFinalSummary() +} + +func (pt *ProgressTracker) getTerminalWidth() int { + width, _, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil || width < 40 { + return 80 + } + return width +} + +func (pt *ProgressTracker) getTerminalHeight() int { + _, height, err := term.GetSize(int(os.Stdout.Fd())) + if err != nil || height < 10 { + return 24 + } + return height +} + +func formatPill(label string, style PillStyle) string { + paddedLabel := fmt.Sprintf(" %s ", label) + halfLen := len(paddedLabel) / 2 + leftPart := paddedLabel[:halfLen] + rightPart := paddedLabel[halfLen:] + + leftBg := fmt.Sprintf("\033[48;2;%d;%d;%dm", style.Left.R, style.Left.G, style.Left.B) + rightBg := fmt.Sprintf("\033[48;2;%d;%d;%dm", style.Right.R, style.Right.G, style.Right.B) + textColor := fmt.Sprintf("\033[38;2;%d;%d;%dm", style.Text.R, style.Text.G, style.Text.B) + + return fmt.Sprintf("%s%s%s%s%s%s%s", leftBg, textColor, leftPart, rightBg, rightPart, ColorReset, "") +} + +func formatStatusPill(s status.Status) string { + switch s { + case status.PASS: + return formatPill("PASS", pillStylePass) + case status.FAIL: + return formatPill("FAIL", pillStyleFail) + case status.SKIP: + return formatPill("SKIP", pillStyleSkip) + case status.TIMEOUT: + return formatPill("TIMEOUT", pillStyleTimeout) + case status.CRASH: + return formatPill("CRASH", pillStyleCrash) + case status.PARSE_ERROR: + return formatPill("PARSE_ERR", pillStyleParseError) + case status.PARSE_SUCCESS_ERROR: + return formatPill("PARSE_SUC", pillStyleParseError) + case status.NOT_IMPLEMENTED: + return formatPill("NOT_IMPL", pillStyleNotImpl) + case status.RUNNER_ERROR: + return formatPill("RUN_ERR", pillStyleRunnerError) + default: + return formatPill("???", pillStyleRunnerError) + } +} + +func formatDelta(gained, lost int32) string { + total := gained - lost + + var totalStr string + if total > 0 { + totalStr = fmt.Sprintf("%s+%d%s", FgBrightGreen, total, ColorReset) + } else if total < 0 { + totalStr = fmt.Sprintf("%s%d%s", FgBrightRed, total, ColorReset) + } else { + totalStr = fmt.Sprintf("%s0%s", FgGray, ColorReset) + } + + var breakdown string + if gained > 0 || lost > 0 { + breakdown = fmt.Sprintf(" (%s%s%d%s %s%s%d%s)", + FgBrightGreen, ArrowUp, gained, ColorReset, + FgBrightRed, ArrowDown, lost, ColorReset) + } + + return totalStr + breakdown +} + +func (pt *ProgressTracker) renderInteractive(current uint32) { + width := pt.getTerminalWidth() + height := pt.getTerminalHeight() + + fmt.Print("\033[H") + passed := pt.passed.Load() failed := pt.failed.Load() skipped := pt.skipped.Load() timeout := pt.timeout.Load() crashed := pt.crashed.Load() + parseError := pt.parseError.Load() + parseSuccessError := pt.parseSuccessError.Load() + notImplemented := pt.notImplemented.Load() + runnerError := pt.runnerError.Load() - barWidth := 50 - passedWidth := int(float64(passed) / float64(current) * float64(barWidth)) - failedWidth := int(float64(failed) / float64(current) * float64(barWidth)) - skippedWidth := int(float64(skipped) / float64(current) * float64(barWidth)) - timeoutWidth := int(float64(timeout) / float64(current) * float64(barWidth)) - crashedWidth := int(float64(crashed) / float64(current) * float64(barWidth)) - otherWidth := barWidth - passedWidth - failedWidth - skippedWidth - timeoutWidth - crashedWidth + fmt.Printf("%s Test262 Runner %s\n\n", ColorBold, ColorReset) + + pt.renderFullWidthProgressBar(current, width) + fmt.Println() + + pt.renderStatusLine("PASS", passed, pt.totalTests, pt.passGained.Load(), pt.passLost.Load(), pillStylePass) + pt.renderStatusLine("FAIL", failed, pt.totalTests, pt.failGained.Load(), pt.failLost.Load(), pillStyleFail) + pt.renderStatusLineSimple("SKIP", skipped, pt.totalTests, pillStyleSkip) + pt.renderStatusLineSimple("TIMEOUT", timeout, pt.totalTests, pillStyleTimeout) + pt.renderStatusLineSimple("CRASH", crashed, pt.totalTests, pillStyleCrash) + pt.renderStatusLineSimple("PARSE_ERR", parseError, pt.totalTests, pillStyleParseError) + pt.renderStatusLineSimple("PARSE_SUC", parseSuccessError, pt.totalTests, pillStyleParseError) + pt.renderStatusLineSimple("NOT_IMPL", notImplemented, pt.totalTests, pillStyleNotImpl) + pt.renderStatusLineSimple("RUN_ERR", runnerError, pt.totalTests, pillStyleRunnerError) - bar := "" - if passedWidth > 0 { - bar += ColorGreen + repeatChar("█", passedWidth) + ColorReset + fmt.Println() + + pt.changesMu.Lock() + changes := make([]RecentChange, len(pt.recentChanges)) + copy(changes, pt.recentChanges) + pt.changesMu.Unlock() + + headerLines := 13 + availableLines := height - headerLines - 2 + if availableLines < 3 { + availableLines = 3 + } + + fmt.Printf("%s Recent Changes: %s\n", ColorBold, ColorReset) + + startIdx := 0 + if len(changes) > availableLines { + startIdx = len(changes) - availableLines } - if failedWidth > 0 { - bar += ColorRed + repeatChar("█", failedWidth) + ColorReset + + for i := startIdx; i < len(changes); i++ { + change := changes[i] + shortPath := change.Path + maxPathLen := width - 60 + if maxPathLen < 20 { + maxPathLen = 20 + } + if len(shortPath) > maxPathLen { + shortPath = "..." + shortPath[len(shortPath)-(maxPathLen-3):] + } + + arrow := ArrowRight + if change.Status == status.PASS && change.PrevStatus != status.PASS { + arrow = fmt.Sprintf("%s%s%s", FgBrightGreen, ArrowUp, ColorReset) + } else if change.PrevStatus == status.PASS && change.Status != status.PASS { + arrow = fmt.Sprintf("%s%s%s", FgBrightRed, ArrowDown, ColorReset) + } + + fmt.Printf(" %s %s %s %s\033[K\n", + formatStatusPill(change.PrevStatus), + arrow, + formatStatusPill(change.Status), + shortPath) } - if timeoutWidth > 0 { - bar += ColorYellow + repeatChar("█", timeoutWidth) + ColorReset + + for i := len(changes) - startIdx; i < availableLines; i++ { + fmt.Printf("\033[K\n") + } +} + +// Segment for progress bar +type Segment struct { + Width float64 + Color RGB +} + +func (pt *ProgressTracker) renderFullWidthProgressBar(current uint32, width int) { + barWidth := width - 25 + if barWidth < 20 { + barWidth = 20 + } + + passed := pt.passed.Load() + failed := pt.failed.Load() + skipped := pt.skipped.Load() + timeout := pt.timeout.Load() + crashed := pt.crashed.Load() + other := current - passed - failed - skipped - timeout - crashed + remaining := pt.totalTests - current + + total := float64(pt.totalTests) + if total == 0 { + total = 1 + } + + segments := []Segment{ + {float64(passed) / total * float64(barWidth), colorPass}, + {float64(failed) / total * float64(barWidth), colorFail}, + {float64(timeout) / total * float64(barWidth), colorTimeout}, + {float64(crashed) / total * float64(barWidth), colorCrash}, + {float64(skipped) / total * float64(barWidth), colorSkip}, + {float64(other) / total * float64(barWidth), colorOther}, + {float64(remaining) / total * float64(barWidth), colorEmpty}, + } + + bar := renderSegmentedBar(segments) + percentage := float64(current) / float64(pt.totalTests) * 100 + + fmt.Printf("[%s] %5.1f%% (%d/%d)\033[K\n", bar, percentage, current, pt.totalTests) +} + +func renderSegmentedBar(segments []Segment) string { + var result strings.Builder + + for i, seg := range segments { + if seg.Width <= 0 { + continue + } + + fullBlocks := int(seg.Width) + fractional := seg.Width - float64(fullBlocks) + + fg := fmt.Sprintf("\033[38;2;%d;%d;%dm", seg.Color.R, seg.Color.G, seg.Color.B) + + if fullBlocks > 0 { + result.WriteString(fg) + result.WriteString(strings.Repeat(BlockFull, fullBlocks)) + result.WriteString(ColorReset) + } + + if fractional >= 0.125 { + nextColor := colorEmpty + for j := i + 1; j < len(segments); j++ { + if segments[j].Width > 0 { + nextColor = segments[j].Color + break + } + } + + fgColor := fmt.Sprintf("\033[38;2;%d;%d;%dm", seg.Color.R, seg.Color.G, seg.Color.B) + bgColor := fmt.Sprintf("\033[48;2;%d;%d;%dm", nextColor.R, nextColor.G, nextColor.B) + + result.WriteString(fgColor) + result.WriteString(bgColor) + result.WriteString(getFractionalBlock(fractional)) + result.WriteString(ColorReset) + } } - if crashedWidth > 0 { - bar += ColorMagenta + repeatChar("█", crashedWidth) + ColorReset + + return result.String() +} + +func getFractionalBlock(fraction float64) string { + switch { + case fraction >= 0.875: + return Block7_8 + case fraction >= 0.75: + return Block6_8 + case fraction >= 0.625: + return Block5_8 + case fraction >= 0.5: + return Block4_8 + case fraction >= 0.375: + return Block3_8 + case fraction >= 0.25: + return Block2_8 + case fraction >= 0.125: + return Block1_8 + default: + return "" } - if skippedWidth > 0 { - bar += ColorCyan + repeatChar("█", skippedWidth) + ColorReset +} + +func (pt *ProgressTracker) renderStatusLine(label string, count uint32, total uint32, gained, lost int32, style PillStyle) { + percentage := float64(count) / float64(total) * 100 + pill := formatPill(label, style) + delta := formatDelta(gained, lost) + + fmt.Printf("%s %6d (%5.2f%%) %s\033[K\n", pill, count, percentage, delta) +} + +func (pt *ProgressTracker) renderStatusLineSimple(label string, count uint32, total uint32, style PillStyle) { + percentage := float64(count) / float64(total) * 100 + pill := formatPill(label, style) + + fmt.Printf("%s %6d (%5.2f%%)\033[K\n", pill, count, percentage) +} + +func (pt *ProgressTracker) printProgressBar(current uint32) { + passed := pt.passed.Load() + failed := pt.failed.Load() + skipped := pt.skipped.Load() + timeout := pt.timeout.Load() + crashed := pt.crashed.Load() + other := current - passed - failed - skipped - timeout - crashed + + barWidth := 50 + total := float64(pt.totalTests) + if total == 0 { + total = 1 } - if otherWidth > 0 { - bar += ColorGray + repeatChar("░", otherWidth) + ColorReset + + segments := []Segment{ + {float64(passed) / total * float64(barWidth), colorPass}, + {float64(failed) / total * float64(barWidth), colorFail}, + {float64(timeout) / total * float64(barWidth), colorTimeout}, + {float64(crashed) / total * float64(barWidth), colorCrash}, + {float64(skipped) / total * float64(barWidth), colorSkip}, + {float64(other) / total * float64(barWidth), colorOther}, + {float64(pt.totalTests-current) / total * float64(barWidth), colorEmpty}, } + bar := renderSegmentedBar(segments) percentage := float64(current) / float64(pt.totalTests) * 100 - fmt.Printf("\r[%s] %3d%% (%d/%d) | %sP:%d%s %sF:%d%s %sT:%d%s %sC:%d%s %sS:%d%s", - bar, - int(percentage), - current, - pt.totalTests, - ColorGreen, passed, ColorReset, - ColorRed, failed, ColorReset, - ColorYellow, timeout, ColorReset, - ColorMagenta, crashed, ColorReset, - ColorCyan, skipped, ColorReset, - ) + fmt.Printf("\r[%s] %3d%% (%d/%d)", bar, int(percentage), current, pt.totalTests) +} + +func (pt *ProgressTracker) printFinalSummary() { + passed := pt.passed.Load() + failed := pt.failed.Load() + skipped := pt.skipped.Load() + timeout := pt.timeout.Load() + crashed := pt.crashed.Load() + parseError := pt.parseError.Load() + parseSuccessError := pt.parseSuccessError.Load() + notImplemented := pt.notImplemented.Load() + runnerError := pt.runnerError.Load() + total := pt.total.Load() + + fmt.Printf("\n%s=== Final Results ===%s\n\n", ColorBold, ColorReset) + + pt.printFinalStatusLine("PASS", passed, total, pt.passGained.Load(), pt.passLost.Load(), pillStylePass) + pt.printFinalStatusLine("FAIL", failed, total, pt.failGained.Load(), pt.failLost.Load(), pillStyleFail) + pt.printFinalStatusLineSimple("SKIP", skipped, total, pillStyleSkip) + pt.printFinalStatusLineSimple("TIMEOUT", timeout, total, pillStyleTimeout) + pt.printFinalStatusLineSimple("CRASH", crashed, total, pillStyleCrash) + pt.printFinalStatusLineSimple("PARSE_ERR", parseError, total, pillStyleParseError) + pt.printFinalStatusLineSimple("PARSE_SUC", parseSuccessError, total, pillStyleParseError) + pt.printFinalStatusLineSimple("NOT_IMPL", notImplemented, total, pillStyleNotImpl) + pt.printFinalStatusLineSimple("RUN_ERR", runnerError, total, pillStyleRunnerError) + + fmt.Printf("\n%sTotal: %d%s\n", ColorBold, total, ColorReset) +} + +func (pt *ProgressTracker) printFinalStatusLine(label string, count uint32, total uint32, gained, lost int32, style PillStyle) { + percentage := float64(count) / float64(total) * 100 + pill := formatPill(label, style) + delta := formatDelta(gained, lost) + + fmt.Printf("%s %6d (%5.2f%%) %s\n", pill, count, percentage, delta) +} + +func (pt *ProgressTracker) printFinalStatusLineSimple(label string, count uint32, total uint32, style PillStyle) { + percentage := float64(count) / float64(total) * 100 + pill := formatPill(label, style) + + fmt.Printf("%s %6d (%5.2f%%)\n", pill, count, percentage) } func (pt *ProgressTracker) GetStats() (passed, failed, skipped, timeout, crashed, parseError, parseSuccessError, notImplemented, runnerError, total uint32) { @@ -141,11 +621,3 @@ func (pt *ProgressTracker) GetStats() (passed, failed, skipped, timeout, crashed pt.parseError.Load(), pt.parseSuccessError.Load(), pt.notImplemented.Load(), pt.runnerError.Load(), pt.total.Load() } - -func repeatChar(char string, count int) string { - result := "" - for i := 0; i < count; i++ { - result += char - } - return result -} diff --git a/crates/yavashark_test262/runner/results/results.go b/crates/yavashark_test262/runner/results/results.go index 6a869e73e..bd5e4cd02 100644 --- a/crates/yavashark_test262/runner/results/results.go +++ b/crates/yavashark_test262/runner/results/results.go @@ -102,7 +102,7 @@ func (tr *TestResults) Add(res Result) { tr.TestResults = append(tr.TestResults, res) } -func (tr *TestResults) PrintResults() { +func (tr *TestResults) PrintResults(showStats bool) { printRes("Passed", tr.Passed, tr.Total) printRes("Failed", tr.Failed, tr.Total) printRes("Skipped", tr.Skipped, tr.Total) @@ -123,8 +123,9 @@ func (tr *TestResults) PrintResults() { printRes("Passed (skip, no-parse)", tr.Passed, tr.Total-(tr.Skipped+tr.ParseError+tr.ParseSuccessError)) fmt.Printf("Total (skip, no-parse): %d\n", tr.Total-(tr.Skipped+tr.ParseError+tr.ParseSuccessError)) - // Print memory usage statistics - tr.PrintMemoryStats() + if showStats { + tr.PrintMemoryStats() + } } func formatMemory(kb uint64) string { diff --git a/crates/yavashark_test262/runner/run/run.go b/crates/yavashark_test262/runner/run/run.go index c853575ed..5774cbb86 100644 --- a/crates/yavashark_test262/runner/run/run.go +++ b/crates/yavashark_test262/runner/run/run.go @@ -12,6 +12,7 @@ import ( "yavashark_test262_runner/results" "yavashark_test262_runner/scheduler" "yavashark_test262_runner/status" + "yavashark_test262_runner/test" "yavashark_test262_runner/worker" ) @@ -20,30 +21,56 @@ var SKIP = []string{ "staging", } -func TestsInDir(testRoot string, workers int, skips bool, timings bool) *results.TestResults { - jobs := make(chan string, workers*8) +type RunConfig struct { + Workers int + Skips bool + Timings bool + Timeout time.Duration + Interactive bool +} + +func TestsInDir(testRoot string, config RunConfig) *results.TestResults { + // Set the timeout for tests + test.SetTimeout(config.Timeout) - resultsChan := make(chan results.Result, workers*8) + jobs := make(chan string, config.Workers*8) + + resultsChan := make(chan results.Result, config.Workers*8) wg := &sync.WaitGroup{} - wg.Add(workers) + wg.Add(config.Workers) num := countTests(testRoot) - progressTracker := progress.NewProgressTracker(num) - for i := range workers { - go worker.Worker(i, jobs, resultsChan, wg, timings) + // Load previous results for delta calculation + prevResults, _ := results.LoadResults() + var prevResultsMap map[string]status.Status + if prevResults != nil { + prevResultsMap = make(map[string]status.Status) + for _, r := range prevResults { + prevResultsMap[r.Path] = r.Status + } + } + + progressTracker := progress.NewProgressTracker(num, config.Interactive, prevResultsMap) + + for i := range config.Workers { + go worker.Worker(i, jobs, resultsChan, wg, config.Timings) } testResults := results.New(num) + // WaitGroup for result processing + resultsDone := make(chan struct{}) + // Goroutine to process results and update progress go func() { for res := range resultsChan { testResults.Add(res) - progressTracker.Add(res.Status) + progressTracker.Add(res.Status, res.Path) } + close(resultsDone) }() var testPaths []string @@ -69,7 +96,7 @@ func TestsInDir(testRoot string, workers int, skips bool, timings bool) *results } shouldSkip := false - if skips { + if config.Skips { for _, skip := range SKIP { if strings.HasPrefix(p, skip) { skippedPaths = append(skippedPaths, path) @@ -90,7 +117,8 @@ func TestsInDir(testRoot string, workers int, skips bool, timings bool) *results scheduler.EnrichTimingsWithFallback(timingData, testPaths) - min, max, avg, fastCount, mediumCount, slowCount, riskCount := scheduler.GetStatistics(timingData) + // Get statistics only for tests we're actually running + min, max, avg, fastCount, mediumCount, slowCount, riskCount := scheduler.GetStatisticsForTests(timingData, testPaths) log.Printf("Timing statistics - Min: %v, Max: %v, Avg: %v", min, max, avg) log.Printf("Test distribution - Fast: %d, Medium: %d, Slow: %d, Risky: %d", fastCount, mediumCount, slowCount, riskCount) @@ -118,13 +146,16 @@ func TestsInDir(testRoot string, workers int, skips bool, timings bool) *results }() wg.Wait() + close(resultsChan) + + <-resultsDone + + progressTracker.Finish() fmt.Printf("\n") log.Printf("Finished running %d tests in %s", num, time.Since(now).String()) - close(resultsChan) - return testResults } diff --git a/crates/yavashark_test262/runner/scheduler/scheduler.go b/crates/yavashark_test262/runner/scheduler/scheduler.go index acc255c86..675bc500c 100644 --- a/crates/yavashark_test262/runner/scheduler/scheduler.go +++ b/crates/yavashark_test262/runner/scheduler/scheduler.go @@ -131,6 +131,21 @@ func EstimateTimingFromFileSize(path string) time.Duration { return 50 * time.Millisecond } +func FilterTimingsForTests(timings map[string]time.Duration, testPaths []string) map[string]time.Duration { + testSet := make(map[string]struct{}, len(testPaths)) + for _, path := range testPaths { + testSet[path] = struct{}{} + } + + filtered := make(map[string]time.Duration, len(testPaths)) + for path, duration := range timings { + if _, exists := testSet[path]; exists { + filtered[path] = duration + } + } + return filtered +} + func EnrichTimingsWithFallback(timings map[string]time.Duration, testPaths []string) { for _, path := range testPaths { if _, exists := timings[path]; !exists { @@ -139,14 +154,21 @@ func EnrichTimingsWithFallback(timings map[string]time.Duration, testPaths []str } } -func GetStatistics(timings map[string]time.Duration) (min, max, avg time.Duration, fastCount, mediumCount, slowCount, riskCount int) { - if len(timings) == 0 { +func GetStatisticsForTests(timings map[string]time.Duration, testPaths []string) (min, max, avg time.Duration, fastCount, mediumCount, slowCount, riskCount int) { + if len(testPaths) == 0 { return } var total time.Duration + count := 0 - for _, duration := range timings { + for _, path := range testPaths { + duration, exists := timings[path] + if !exists { + duration = 500 * time.Millisecond + } + + count++ total += duration priority := calculatePriorityFromTiming(duration) @@ -169,6 +191,8 @@ func GetStatistics(timings map[string]time.Duration) (min, max, avg time.Duratio } } - avg = total / time.Duration(len(timings)) + if count > 0 { + avg = total / time.Duration(count) + } return } diff --git a/crates/yavashark_test262/runner/test/test.go b/crates/yavashark_test262/runner/test/test.go index 5fabdd5c1..c3e88a0bf 100644 --- a/crates/yavashark_test262/runner/test/test.go +++ b/crates/yavashark_test262/runner/test/test.go @@ -18,13 +18,19 @@ import ( const ( ENGINE_LOCATION = "../../target/release/yavashark_test262" - TIMEOUT = 30 * time.Second + DEFAULT_TIMEOUT = 30 * time.Second ) +var TestTimeout = DEFAULT_TIMEOUT + +func SetTimeout(timeout time.Duration) { + TestTimeout = timeout +} + func RunTest(path string, timings bool) results.Result { startTime := time.Now() - ctx, cancel := context.WithTimeout(context.Background(), TIMEOUT) + ctx, cancel := context.WithTimeout(context.Background(), TestTimeout) defer cancel() cmd := exec.CommandContext(ctx, ENGINE_LOCATION, path) diff --git a/crates/yavashark_test262/viewer/router/handler.go b/crates/yavashark_test262/viewer/router/handler.go index 661bd3db8..0f1318993 100644 --- a/crates/yavashark_test262/viewer/router/handler.go +++ b/crates/yavashark_test262/viewer/router/handler.go @@ -4,6 +4,7 @@ import ( "path/filepath" "strings" "sync" + "time" "viewer/cache" "viewer/conf" "yavashark_test262_runner/results" @@ -37,7 +38,15 @@ func rerunAll(c *fiber.Ctx) error { defer testRun.Unlock() - res := run.TestsInDir(conf.TestRoot, conf.Workers, true, false) + runConfig := run.RunConfig{ + Workers: conf.Workers, + Skips: true, + Timings: false, + Timeout: 30 * time.Second, + Interactive: false, + } + + res := run.TestsInDir(conf.TestRoot, runConfig) res.Write() resCi := results.ConvertResultsToCI(res.TestResults, conf.TestRoot) @@ -63,7 +72,15 @@ func rerun(c *fiber.Ctx) error { fullPath := filepath.Join(conf.TestRoot, path) - run.TestsInDir(fullPath, conf.Workers, true, false) + runConfig := run.RunConfig{ + Workers: conf.Workers, + Skips: true, + Timings: false, + Timeout: 30 * time.Second, + Interactive: false, + } + + run.TestsInDir(fullPath, runConfig) return current(c) } From 6fc243a6a1d6fbc65cbfe46b265c974f937bbc97 Mon Sep 17 00:00:00 2001 From: Shark Date: Sun, 14 Dec 2025 22:23:27 +0100 Subject: [PATCH 3/3] sleeker look --- crates/yavashark_test262/runner/main.go | 7 +- .../runner/progress/progress.go | 384 ++++++++++++++---- .../yavashark_test262/runner/results/diff.go | 11 +- crates/yavashark_test262/runner/run/run.go | 6 +- 4 files changed, 320 insertions(+), 88 deletions(-) diff --git a/crates/yavashark_test262/runner/main.go b/crates/yavashark_test262/runner/main.go index 9cb2c9bf5..26322b929 100644 --- a/crates/yavashark_test262/runner/main.go +++ b/crates/yavashark_test262/runner/main.go @@ -4,6 +4,7 @@ import ( "log" "path/filepath" "yavashark_test262_runner/ci" + "yavashark_test262_runner/progress" "yavashark_test262_runner/results" "yavashark_test262_runner/run" "yavashark_test262_runner/timing" @@ -27,12 +28,16 @@ func main() { Interactive: config.Interactive, } - testResults := run.TestsInDir(testRoot, runConfig) + testResults, summary := run.TestsInDir(testRoot, runConfig) if config.Diff && !config.CI { printDiff(testResults, config.DiffFilter) } + if !config.CI { + progress.PrintSummary(summary) + } + if config.CI { ci.RunCi(testResults, config.RepoPath, config.HistoryOnly, config.Diff, testRoot) } else if config.Verbose { diff --git a/crates/yavashark_test262/runner/progress/progress.go b/crates/yavashark_test262/runner/progress/progress.go index aeed7f602..8485ccfbd 100644 --- a/crates/yavashark_test262/runner/progress/progress.go +++ b/crates/yavashark_test262/runner/progress/progress.go @@ -2,6 +2,7 @@ package progress import ( "fmt" + "math" "os" "strings" "sync" @@ -44,6 +45,119 @@ type RGB struct { R, G, B int } +type HSV struct { + H, S, V float64 +} + +func rgbToHSV(c RGB) HSV { + r := float64(c.R) / 255.0 + g := float64(c.G) / 255.0 + b := float64(c.B) / 255.0 + + max := math.Max(r, math.Max(g, b)) + min := math.Min(r, math.Min(g, b)) + delta := max - min + + var h float64 + s := 0.0 + v := max + + if max > 0 { + s = delta / max + } + + if delta != 0 { + switch max { + case r: + h = (g - b) / delta + if g < b { + h += 6 + } + case g: + h = (b-r)/delta + 2 + case b: + h = (r-g)/delta + 4 + } + h *= 60 + } + + return HSV{H: h, S: s, V: v} +} + +func hsvToRGB(hsv HSV) RGB { + h := hsv.H + s := hsv.S + v := hsv.V + + c := v * s + x := c * (1 - math.Abs(math.Mod(h/60.0, 2)-1)) + m := v - c + + var r1, g1, b1 float64 + switch { + case h >= 0 && h < 60: + r1, g1, b1 = c, x, 0 + case h >= 60 && h < 120: + r1, g1, b1 = x, c, 0 + case h >= 120 && h < 180: + r1, g1, b1 = 0, c, x + case h >= 180 && h < 240: + r1, g1, b1 = 0, x, c + case h >= 240 && h < 300: + r1, g1, b1 = x, 0, c + default: + r1, g1, b1 = c, 0, x + } + + return RGB{ + R: int(math.Round((r1 + m) * 255)), + G: int(math.Round((g1 + m) * 255)), + B: int(math.Round((b1 + m) * 255)), + } +} + +func clamp(x, lo, hi float64) float64 { + if x < lo { + return lo + } + if x > hi { + return hi + } + return x +} + +func blendHSV(a, b HSV, t float64) HSV { + t = clamp(t, 0, 1) + + ha := a.H + hb := b.H + d := hb - ha + if d > 180 { + d -= 360 + } else if d < -180 { + d += 360 + } + + h := ha + d*t + if h < 0 { + h += 360 + } else if h >= 360 { + h -= 360 + } + + return HSV{ + H: h, + S: a.S + (b.S-a.S)*t, + V: a.V + (b.V-a.V)*t, + } +} + +func shadeRGB(c RGB, vScale float64) RGB { + hsv := rgbToHSV(c) + hsv.V = clamp(hsv.V*vScale, 0, 1) + return hsvToRGB(hsv) +} + // Status colors for progress bar var ( colorPass = RGB{76, 175, 80} // Green @@ -64,43 +178,43 @@ type PillStyle struct { var ( pillStylePass = PillStyle{ Left: RGB{67, 160, 71}, - Right: RGB{129, 199, 132}, - Text: RGB{27, 94, 32}, + Right: RGB{46, 125, 50}, + Text: RGB{255, 255, 255}, } pillStyleFail = PillStyle{ Left: RGB{229, 57, 53}, - Right: RGB{239, 154, 154}, + Right: RGB{198, 40, 40}, Text: RGB{255, 255, 255}, } pillStyleSkip = PillStyle{ Left: RGB{0, 172, 193}, - Right: RGB{128, 222, 234}, - Text: RGB{0, 77, 64}, + Right: RGB{0, 131, 143}, + Text: RGB{255, 255, 255}, } pillStyleTimeout = PillStyle{ Left: RGB{255, 179, 0}, - Right: RGB{255, 224, 130}, - Text: RGB{62, 39, 35}, + Right: RGB{255, 143, 0}, + Text: RGB{33, 33, 33}, } pillStyleCrash = PillStyle{ Left: RGB{142, 36, 170}, - Right: RGB{206, 147, 216}, + Right: RGB{106, 27, 154}, Text: RGB{255, 255, 255}, } pillStyleParseError = PillStyle{ Left: RGB{30, 136, 229}, - Right: RGB{144, 202, 249}, - Text: RGB{13, 71, 161}, + Right: RGB{21, 101, 192}, + Text: RGB{255, 255, 255}, } pillStyleNotImpl = PillStyle{ Left: RGB{117, 117, 117}, - Right: RGB{189, 189, 189}, - Text: RGB{33, 33, 33}, + Right: RGB{66, 66, 66}, + Text: RGB{255, 255, 255}, } pillStyleRunnerError = PillStyle{ Left: RGB{97, 97, 97}, - Right: RGB{158, 158, 158}, - Text: RGB{250, 250, 250}, + Right: RGB{55, 71, 79}, + Text: RGB{255, 255, 255}, } ) @@ -111,6 +225,39 @@ type RecentChange struct { IsNew bool } +type Summary struct { + Passed uint32 + Failed uint32 + Skipped uint32 + Timeout uint32 + Crashed uint32 + ParseError uint32 + ParseSuccessError uint32 + NotImplemented uint32 + RunnerError uint32 + Total uint32 + + PassGained int32 + PassLost int32 + FailGained int32 + FailLost int32 + + SkipGained int32 + SkipLost int32 + TimeoutGained int32 + TimeoutLost int32 + CrashGained int32 + CrashLost int32 + ParseErrGained int32 + ParseErrLost int32 + ParseSucGained int32 + ParseSucLost int32 + NotImplGained int32 + NotImplLost int32 + RunErrGained int32 + RunErrLost int32 +} + type ProgressTracker struct { mu sync.Mutex @@ -126,6 +273,21 @@ type ProgressTracker struct { total atomic.Uint32 lastPrintedProgress atomic.Uint32 + skippedGained atomic.Int32 + skippedLost atomic.Int32 + timeoutGained atomic.Int32 + timeoutLost atomic.Int32 + crashedGained atomic.Int32 + crashedLost atomic.Int32 + parseErrorGained atomic.Int32 + parseErrorLost atomic.Int32 + parseSuccessErrorGained atomic.Int32 + parseSuccessErrorLost atomic.Int32 + notImplementedGained atomic.Int32 + notImplementedLost atomic.Int32 + runnerErrorGained atomic.Int32 + runnerErrorLost atomic.Int32 + totalTests uint32 interactive bool @@ -136,6 +298,9 @@ type ProgressTracker struct { failGained atomic.Int32 failLost atomic.Int32 + gainedByStatus map[status.Status]*atomic.Int32 + lostByStatus map[status.Status]*atomic.Int32 + recentChanges []RecentChange changesMu sync.Mutex } @@ -148,6 +313,25 @@ func NewProgressTracker(totalTests uint32, interactive bool, prevResults map[str recentChanges: make([]RecentChange, 0), } + pt.gainedByStatus = map[status.Status]*atomic.Int32{ + status.SKIP: &pt.skippedGained, + status.TIMEOUT: &pt.timeoutGained, + status.CRASH: &pt.crashedGained, + status.PARSE_ERROR: &pt.parseErrorGained, + status.PARSE_SUCCESS_ERROR: &pt.parseSuccessErrorGained, + status.NOT_IMPLEMENTED: &pt.notImplementedGained, + status.RUNNER_ERROR: &pt.runnerErrorGained, + } + pt.lostByStatus = map[status.Status]*atomic.Int32{ + status.SKIP: &pt.skippedLost, + status.TIMEOUT: &pt.timeoutLost, + status.CRASH: &pt.crashedLost, + status.PARSE_ERROR: &pt.parseErrorLost, + status.PARSE_SUCCESS_ERROR: &pt.parseSuccessErrorLost, + status.NOT_IMPLEMENTED: &pt.notImplementedLost, + status.RUNNER_ERROR: &pt.runnerErrorLost, + } + if interactive { pt.enterInteractiveMode() } @@ -156,13 +340,17 @@ func NewProgressTracker(totalTests uint32, interactive bool, prevResults map[str } func (pt *ProgressTracker) enterInteractiveMode() { - fmt.Print("\033[?1049h") - fmt.Print("\033[?25l") - fmt.Print("\033[2J") + fmt.Print("\033[?1049h") // alternate screen + fmt.Print("\033[?25l") // hide cursor + fmt.Print("\033[?7l") // disable line wrap + fmt.Print("\033[2J\033[H") + + pt.renderInteractive(0) } func (pt *ProgressTracker) exitInteractiveMode() { - fmt.Print("\033[?25h") + fmt.Print("\033[?7h") // enable line wrap + fmt.Print("\033[?25h") // show cursor fmt.Print("\033[?1049l") } @@ -205,7 +393,14 @@ func (pt *ProgressTracker) Add(s status.Status, path string) { pt.failGained.Add(1) } - if pt.interactive && (s == status.PASS || s == status.FAIL || prevStatus == status.PASS || prevStatus == status.FAIL) { + if gained, ok := pt.gainedByStatus[s]; ok { + gained.Add(1) + } + if lost, ok := pt.lostByStatus[prevStatus]; ok { + lost.Add(1) + } + + if pt.interactive { change := RecentChange{ Path: path, Status: s, @@ -232,10 +427,7 @@ func (pt *ProgressTracker) updateProgress(current uint32) { threshold := uint32(100) if pt.totalTests > 0 { if pt.interactive { - percentThreshold := pt.totalTests / 200 - if percentThreshold > threshold { - threshold = percentThreshold - } + threshold = 1 } else { percentThreshold := pt.totalTests / 50 if percentThreshold > threshold { @@ -255,7 +447,7 @@ func (pt *ProgressTracker) updateProgress(current uint32) { } } -func (pt *ProgressTracker) Finish() { +func (pt *ProgressTracker) Finish() Summary { if !pt.interactive { pt.printProgressBar(pt.total.Load()) fmt.Println() @@ -266,7 +458,41 @@ func (pt *ProgressTracker) Finish() { pt.exitInteractiveMode() } - pt.printFinalSummary() + s := pt.Summary() + return s +} + +func (pt *ProgressTracker) Summary() Summary { + return Summary{ + Passed: pt.passed.Load(), + Failed: pt.failed.Load(), + Skipped: pt.skipped.Load(), + Timeout: pt.timeout.Load(), + Crashed: pt.crashed.Load(), + ParseError: pt.parseError.Load(), + ParseSuccessError: pt.parseSuccessError.Load(), + NotImplemented: pt.notImplemented.Load(), + RunnerError: pt.runnerError.Load(), + Total: pt.total.Load(), + PassGained: pt.passGained.Load(), + PassLost: pt.passLost.Load(), + FailGained: pt.failGained.Load(), + FailLost: pt.failLost.Load(), + SkipGained: pt.skippedGained.Load(), + SkipLost: pt.skippedLost.Load(), + TimeoutGained: pt.timeoutGained.Load(), + TimeoutLost: pt.timeoutLost.Load(), + CrashGained: pt.crashedGained.Load(), + CrashLost: pt.crashedLost.Load(), + ParseErrGained: pt.parseErrorGained.Load(), + ParseErrLost: pt.parseErrorLost.Load(), + ParseSucGained: pt.parseSuccessErrorGained.Load(), + ParseSucLost: pt.parseSuccessErrorLost.Load(), + NotImplGained: pt.notImplementedGained.Load(), + NotImplLost: pt.notImplementedLost.Load(), + RunErrGained: pt.runnerErrorGained.Load(), + RunErrLost: pt.runnerErrorLost.Load(), + } } func (pt *ProgressTracker) getTerminalWidth() int { @@ -286,19 +512,19 @@ func (pt *ProgressTracker) getTerminalHeight() int { } func formatPill(label string, style PillStyle) string { - paddedLabel := fmt.Sprintf(" %s ", label) - halfLen := len(paddedLabel) / 2 - leftPart := paddedLabel[:halfLen] - rightPart := paddedLabel[halfLen:] + // Rounded Powerline pill: 