package main import ( "bufio" "bytes" "context" "flag" "fmt" "net/http" "os" "os/exec" "runtime" "strings" "sync" "time" ) // Конфигурация type Config struct { ServerURL string ServerPort string TestTimeout time.Duration Verbose bool Parallel bool Coverage bool TestSuite string } // Цвета для вывода type Colors struct { Reset string Red string Green string Yellow string Blue string Cyan string } var colors Colors func init() { // Определяем поддержку цветов if runtime.GOOS == "windows" { // На Windows включаем ANSI colors для новых версий colors = Colors{ Reset: "\033[0m", Red: "\033[31m", Green: "\033[32m", Yellow: "\033[33m", Blue: "\033[34m", Cyan: "\033[36m", } } else { colors = Colors{ Reset: "\033[0m", Red: "\033[31m", Green: "\033[32m", Yellow: "\033[33m", Blue: "\033[34m", Cyan: "\033[36m", } } } // Тестовый набор type TestSuite struct { Name string Pattern string Timeout time.Duration } // Результат теста type TestResult struct { Suite string Passed bool Duration time.Duration Output string Error error Coverage float64 } func main() { config := parseFlags() printBanner() // Проверяем сервер if !checkServer(config) { os.Exit(1) } // Очищаем кэш тестов cleanTestCache() // Запускаем тесты results := runTests(config) // Выводим результаты printResults(results) // Завершаем с соответствующим кодом if hasFailures(results) { os.Exit(1) } os.Exit(0) } func parseFlags() *Config { config := &Config{} flag.StringVar(&config.ServerURL, "server-url", "http://localhost:8088", "API server URL") flag.StringVar(&config.ServerPort, "server-port", "8088", "API server port") flag.DurationVar(&config.TestTimeout, "timeout", 30*time.Minute, "Test timeout") flag.BoolVar(&config.Verbose, "verbose", false, "Verbose output") flag.BoolVar(&config.Parallel, "parallel", false, "Run tests in parallel") flag.BoolVar(&config.Coverage, "coverage", false, "Generate coverage report") flag.StringVar(&config.TestSuite, "suite", "all", "Test suite to run (all, auth, account, object, feedback, comment, rating, appeal)") flag.Parse() return config } func printBanner() { fmt.Printf("%s╔════════════════════════════════════════════════════════════════╗%s\n", colors.Cyan, colors.Reset) fmt.Printf("%s║ Go Test Runner for API ║%s\n", colors.Cyan, colors.Reset) fmt.Printf("%s║ Version 1.0.0 ║%s\n", colors.Cyan, colors.Reset) fmt.Printf("%s╚════════════════════════════════════════════════════════════════╝%s\n", colors.Cyan, colors.Reset) fmt.Println() } func checkServer(config *Config) bool { fmt.Printf("%s🔍 Checking server status...%s\n", colors.Yellow, colors.Reset) client := &http.Client{Timeout: 5 * time.Second} resp, err := client.Get(fmt.Sprintf("%s/health", config.ServerURL)) if err != nil { fmt.Printf("%s❌ Server is not running on %s%s\n", colors.Red, config.ServerURL, colors.Reset) fmt.Printf("%s💡 Please start the server first: go run cmd/main.go%s\n", colors.Yellow, colors.Reset) return false } defer resp.Body.Close() if resp.StatusCode == http.StatusOK { fmt.Printf("%s✅ Server is running on %s%s\n\n", colors.Green, config.ServerURL, colors.Reset) return true } fmt.Printf("%s❌ Server returned status: %d%s\n", colors.Red, resp.StatusCode, colors.Reset) return false } func cleanTestCache() { fmt.Printf("%s🧹 Cleaning test cache...%s\n", colors.Yellow, colors.Reset) cmd := exec.Command("go", "clean", "-testcache") if err := cmd.Run(); err != nil { fmt.Printf("%s⚠️ Warning: failed to clean test cache: %v%s\n", colors.Yellow, err, colors.Reset) } fmt.Println() } func getTestSuites() []TestSuite { return []TestSuite{ {Name: "All", Pattern: "", Timeout: 30 * time.Minute}, {Name: "Auth", Pattern: "TestAuthFlow", Timeout: 5 * time.Minute}, {Name: "Account", Pattern: "TestAccountEndpoints", Timeout: 5 * time.Minute}, {Name: "Object", Pattern: "TestObjectEndpoints", Timeout: 5 * time.Minute}, {Name: "Feedback", Pattern: "TestFeedbackEndpoints", Timeout: 5 * time.Minute}, {Name: "Comment", Pattern: "TestCommentEndpoints", Timeout: 5 * time.Minute}, {Name: "Rating", Pattern: "TestRatingEndpoints", Timeout: 5 * time.Minute}, {Name: "Appeal", Pattern: "TestAppealEndpoints", Timeout: 5 * time.Minute}, } } func runTests(config *Config) []TestResult { suites := getTestSuites() var selectedSuites []TestSuite if config.TestSuite == "all" { selectedSuites = suites } else { for _, suite := range suites { if strings.EqualFold(suite.Name, config.TestSuite) { selectedSuites = []TestSuite{suite} break } } if len(selectedSuites) == 0 { fmt.Printf("%s❌ Unknown test suite: %s%s\n", colors.Red, config.TestSuite, colors.Reset) fmt.Printf("%s📚 Available suites: all, auth, account, object, feedback, comment, rating, appeal%s\n", colors.Yellow, colors.Reset) os.Exit(1) } } var results []TestResult if config.Parallel && len(selectedSuites) > 1 { results = runTestsParallel(selectedSuites, config) } else { for _, suite := range selectedSuites { if suite.Name == "All" { result := runTestSuite(suite, config) results = append(results, result) } else { result := runTestSuite(suite, config) results = append(results, result) } } } return results } func runTestsParallel(suites []TestSuite, config *Config) []TestResult { fmt.Printf("%s🚀 Running tests in parallel mode...%s\n\n", colors.Cyan, colors.Reset) var wg sync.WaitGroup results := make([]TestResult, len(suites)) // Пропускаем "All" suite в параллельном режиме var filteredSuites []TestSuite for _, suite := range suites { if suite.Name != "All" { filteredSuites = append(filteredSuites, suite) } } for i, suite := range filteredSuites { wg.Add(1) go func(idx int, s TestSuite) { defer wg.Done() results[idx] = runTestSuite(s, config) }(i, suite) } wg.Wait() return results } func runTestSuite(suite TestSuite, config *Config) TestResult { startTime := time.Now() fmt.Printf("%s📦 Running %s tests...%s\n", colors.Blue, suite.Name, colors.Reset) cmd := exec.Command("go", "test") if config.Verbose { cmd.Args = append(cmd.Args, "-v") } cmd.Args = append(cmd.Args, "-timeout", suite.Timeout.String()) if suite.Pattern != "" { cmd.Args = append(cmd.Args, "-run", suite.Pattern) } if config.Coverage && suite.Name == "All" { cmd.Args = append(cmd.Args, "-coverprofile=coverage.out") } cmd.Args = append(cmd.Args, "./tests/integration/...") // Захват вывода var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr // Запуск с таймаутом _, cancel := context.WithTimeout(context.Background(), suite.Timeout) defer cancel() err := cmd.Run() duration := time.Since(startTime) result := TestResult{ Suite: suite.Name, Duration: duration, Output: stdout.String(), } if err != nil { result.Passed = false result.Error = err if stderr.Len() > 0 { result.Output += "\n" + stderr.String() } } else { result.Passed = true } // Извлечение покрытия, если нужно if config.Coverage && suite.Name == "All" && result.Passed { result.Coverage = extractCoverage() } // Вывод результата if result.Passed { fmt.Printf("%s✅ %s tests passed %s(%v)%s\n", colors.Green, suite.Name, colors.Green, duration, colors.Reset) } else { fmt.Printf("%s❌ %s tests failed %s(%v)%s\n", colors.Red, suite.Name, colors.Red, duration, colors.Reset) if config.Verbose { fmt.Println(result.Output) } } return result } func extractCoverage() float64 { // Проверяем наличие файла покрытия if _, err := os.Stat("coverage.out"); os.IsNotExist(err) { return 0 } // Запускаем go tool cover для получения процента покрытия cmd := exec.Command("go", "tool", "cover", "-func=coverage.out") output, err := cmd.Output() if err != nil { return 0 } // Парсим вывод для получения total lines := strings.Split(string(output), "\n") for _, line := range lines { if strings.Contains(line, "total:") { // Извлекаем процент parts := strings.Fields(line) if len(parts) >= 3 { percentage := strings.TrimSuffix(parts[2], "%") var coverage float64 fmt.Sscanf(percentage, "%f", &coverage) return coverage } } } return 0 } func printResults(results []TestResult) { fmt.Println() fmt.Printf("%s╔════════════════════════════════════════════════════════════════╗%s\n", colors.Cyan, colors.Reset) fmt.Printf("%s║ Test Results ║%s\n", colors.Cyan, colors.Reset) fmt.Printf("%s╚════════════════════════════════════════════════════════════════╝%s\n", colors.Cyan, colors.Reset) fmt.Println() // Таблица результатов fmt.Printf("%-15s %-10s %-15s %s\n", "Suite", "Status", "Duration", "Coverage") fmt.Println(strings.Repeat("-", 60)) var totalTests int var passedTests int var totalDuration time.Duration for _, result := range results { status := fmt.Sprintf("%s✓ PASSED%s", colors.Green, colors.Reset) if !result.Passed { status = fmt.Sprintf("%s✗ FAILED%s", colors.Red, colors.Reset) } coverage := "-" if result.Coverage > 0 { coverage = fmt.Sprintf("%.1f%%", result.Coverage) } fmt.Printf("%-15s %-10s %-15v %s\n", result.Suite, status, result.Duration.Round(time.Millisecond), coverage) totalTests++ if result.Passed { passedTests++ } totalDuration += result.Duration } fmt.Println(strings.Repeat("-", 60)) fmt.Printf("%-15s %d/%d passed\n", "TOTAL:", passedTests, totalTests) fmt.Printf("%-15s %v\n", "TIME:", totalDuration.Round(time.Millisecond)) // Генерация HTML отчета при покрытии if len(results) > 0 && results[0].Coverage > 0 { generateHTMLReport() } fmt.Println() } func generateHTMLReport() { fmt.Printf("%s📊 Generating HTML coverage report...%s\n", colors.Yellow, colors.Reset) cmd := exec.Command("go", "tool", "cover", "-html=coverage.out", "-o", "coverage.html") if err := cmd.Run(); err != nil { fmt.Printf("%s⚠️ Failed to generate HTML report: %v%s\n", colors.Yellow, err, colors.Reset) return } fmt.Printf("%s✅ Coverage report generated: coverage.html%s\n", colors.Green, colors.Reset) // Открываем отчет в браузере var openCmd *exec.Cmd switch runtime.GOOS { case "windows": openCmd = exec.Command("cmd", "/c", "start", "coverage.html") case "darwin": openCmd = exec.Command("open", "coverage.html") default: openCmd = exec.Command("xdg-open", "coverage.html") } if err := openCmd.Run(); err != nil { fmt.Printf("%s⚠️ Could not open browser: %v%s\n", colors.Yellow, err, colors.Reset) } } func hasFailures(results []TestResult) bool { for _, result := range results { if !result.Passed { return true } } return false } // Интерактивный режим func interactiveMode() { reader := bufio.NewReader(os.Stdin) for { printMenu() fmt.Print("Enter your choice: ") input, _ := reader.ReadString('\n') input = strings.TrimSpace(input) switch input { case "1": runWithConfig(&Config{TestSuite: "all", Verbose: true}) case "2": runWithConfig(&Config{TestSuite: "auth", Verbose: true}) case "3": runWithConfig(&Config{TestSuite: "account", Verbose: true}) case "4": runWithConfig(&Config{TestSuite: "object", Verbose: true}) case "5": runWithConfig(&Config{TestSuite: "feedback", Verbose: true}) case "6": runWithConfig(&Config{TestSuite: "comment", Verbose: true}) case "7": runWithConfig(&Config{TestSuite: "rating", Verbose: true}) case "8": runWithConfig(&Config{TestSuite: "appeal", Verbose: true}) case "9": runWithConfig(&Config{TestSuite: "all", Coverage: true, Verbose: false}) case "10": runWithConfig(&Config{TestSuite: "all", Parallel: true, Verbose: false}) case "0": fmt.Printf("%s👋 Goodbye!%s\n", colors.Yellow, colors.Reset) os.Exit(0) default: fmt.Printf("%s❌ Invalid choice! Please try again.%s\n", colors.Red, colors.Reset) } fmt.Print("\nPress Enter to continue...") reader.ReadString('\n') } } func printMenu() { fmt.Printf("\n%s╔════════════════════════════════════════════════════════════════╗%s\n", colors.Cyan, colors.Reset) fmt.Printf("%s║ Interactive Menu ║%s\n", colors.Cyan, colors.Reset) fmt.Printf("%s╚════════════════════════════════════════════════════════════════╝%s\n", colors.Cyan, colors.Reset) fmt.Println() fmt.Printf("%s1.%s Run all tests\n", colors.Green, colors.Reset) fmt.Printf("%s2.%s Run auth tests only\n", colors.Green, colors.Reset) fmt.Printf("%s3.%s Run account tests only\n", colors.Green, colors.Reset) fmt.Printf("%s4.%s Run object tests only\n", colors.Green, colors.Reset) fmt.Printf("%s5.%s Run feedback tests only\n", colors.Green, colors.Reset) fmt.Printf("%s6.%s Run comment tests only\n", colors.Green, colors.Reset) fmt.Printf("%s7.%s Run rating tests only\n", colors.Green, colors.Reset) fmt.Printf("%s8.%s Run appeal tests only\n", colors.Green, colors.Reset) fmt.Printf("%s9.%s Run all tests with coverage\n", colors.Green, colors.Reset) fmt.Printf("%s10.%s Run all tests in parallel\n", colors.Green, colors.Reset) fmt.Printf("%s0.%s Exit\n", colors.Red, colors.Reset) fmt.Println() } func runWithConfig(config *Config) { config.ServerURL = "http://localhost:8088" config.TestTimeout = 30 * time.Minute printBanner() if !checkServer(config) { return } cleanTestCache() results := runTests(config) printResults(results) } // Функция для мониторинга сервера func watchServer(config *Config) { fmt.Printf("%s👁️ Starting server health monitor...%s\n", colors.Yellow, colors.Reset) ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() for range ticker.C { if !checkServer(config) { fmt.Printf("%s⚠️ Server is down!%s\n", colors.Red, colors.Reset) } } } // Структура для бенчмарков type BenchmarkResult struct { Name string Ops int64 NsPerOp time.Duration Allocs int64 Bytes int64 } func runBenchmarks() { fmt.Printf("%s🏃 Running benchmarks...%s\n\n", colors.Yellow, colors.Reset) cmd := exec.Command("go", "test", "-bench=.", "-benchmem", "./tests/integration/...") output, err := cmd.Output() if err != nil { fmt.Printf("%s❌ Benchmarks failed: %v%s\n", colors.Red, err, colors.Reset) return } fmt.Println(string(output)) } // Точка входа с поддержкой аргументов командной строки func mainWithArgs() { if len(os.Args) > 1 && os.Args[1] == "interactive" { interactiveMode() return } if len(os.Args) > 1 && os.Args[1] == "bench" { runBenchmarks() return } if len(os.Args) > 1 && os.Args[1] == "watch" { config := parseFlags() watchServer(config) return } // Обычный режим с флагами config := parseFlags() printBanner() if !checkServer(config) { os.Exit(1) } cleanTestCache() results := runTests(config) printResults(results) if hasFailures(results) { os.Exit(1) } }