Files
tp/main_dc/yalarba/api_yal/cmd/testrunner/main.go
T
valitovgaziz d1e45c7686 On branch main
modified:   main_dc/yalarba/api_yal/cmd/testrunner/main.go
	modified:   main_dc/yalarba/api_yal/cmd/testrunner/runner.go
	modified:   main_dc/yalarba/api_yal/tests/integration/account_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/appeal_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/auth_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/comment_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/feedback_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/object_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/rating_test.go
	deleted:    main_dc/yalarba/api_yal/tests/testutils/client.go
	modified:   main_dc/yalarba/api_yal/tests/testutils/fixtures.go
	modified:   main_dc/yalarba/api_yal/tests/testutils/setup.go
write comments for and into test's functions
2026-06-08 01:58:04 +05:00

629 lines
22 KiB
Go

package main
import (
"bufio"
"bytes"
"context"
"flag"
"fmt"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"sync"
"time"
)
// Config хранит конфигурацию тестового раннера
type Config struct {
ServerURL string // URL API сервера
ServerPort string // Порт API сервера
TestTimeout time.Duration // Таймаут выполнения тестов
Verbose bool // Флаг подробного вывода
Parallel bool // Флаг параллельного выполнения
Coverage bool // Флаг генерации отчета о покрытии
TestSuite string // Выбранный набор тестов
}
// Colors хранит ANSI коды цветов для вывода в консоль
type Colors struct {
Reset string // Сброс цвета
Red string // Красный цвет (ошибки)
Green string // Зеленый цвет (успех)
Yellow string // Желтый цвет (предупреждения)
Blue string // Синий цвет (информация)
Cyan string // Бирюзовый цвет (заголовки)
}
// Глобальная переменная с цветами
var colors Colors
// init инициализирует цвета для консольного вывода
// Определяет поддержку цветов для разных ОС
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",
}
}
}
// TestSuite представляет набор тестов
type TestSuite struct {
Name string // Название набора
Pattern string // Регулярное выражение для фильтрации тестов
Timeout time.Duration // Таймаут для этого набора
}
// TestResult хранит результат выполнения тестового набора
type TestResult struct {
Suite string // Название набора
Passed bool // Флаг успешности
Duration time.Duration // Длительность выполнения
Output string // Вывод тестов
Error error // Ошибка выполнения
Coverage float64 // Процент покрытия кода
}
// main - точка входа в программу
// Обрабатывает аргументы командной строки и запускает соответствующий режим
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)
}
// parseFlags парсит аргументы командной строки
// Возвращает указатель на Config с заполненными значениями
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
}
// printBanner выводит графический баннер программы
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()
}
// checkServer проверяет доступность API сервера
// Отправляет GET запрос на /health эндпоинт
// Возвращает true если сервер доступен
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
}
// cleanTestCache очищает кэш тестов Go
// Выполняет команду 'go clean -testcache'
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()
}
// getTestSuites возвращает список доступных наборов тестов
// Возвращает массив TestSuite с предопределенными настройками
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},
}
}
// runTests запускает выбранные наборы тестов
// Параметры:
// - config: конфигурация запуска
// Возвращает: массив результатов тестов
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
}
// runTestsParallel запускает тесты параллельно
// Использует горутины и WaitGroup для синхронизации
// Параметры:
// - suites: список наборов тестов
// - config: конфигурация запуска
// Возвращает: массив результатов тестов
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
}
// runTestSuite запускает один набор тестов
// Выполняет команду 'go test' с соответствующими параметрами
// Параметры:
// - suite: набор тестов для запуска
// - config: конфигурация запуска
// Возвращает: результат выполнения тестов
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
}
// extractCoverage извлекает процент покрытия кода из файла coverage.out
// Выполняет команду 'go tool cover -func=coverage.out'
// Возвращает: процент покрытия (0 если не удалось извлечь)
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
}
// printResults выводит сводную таблицу результатов тестов
// Параметры:
// - results: массив результатов тестов
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()
}
// generateHTMLReport генерирует HTML отчет о покрытии кода
// Создает файл coverage.html и открывает его в браузере
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)
}
}
// hasFailures проверяет, есть ли неудачные тесты
// Параметры:
// - results: массив результатов тестов
// Возвращает: true если есть хотя бы один проваленный тест
func hasFailures(results []TestResult) bool {
for _, result := range results {
if !result.Passed {
return true
}
}
return false
}
// interactiveMode запускает интерактивный режим с меню выбора
// Позволяет пользователю выбирать тесты через консольное меню
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')
}
}
// printMenu выводит интерактивное меню выбора тестов
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()
}
// runWithConfig запускает тесты с заданной конфигурацией
// Используется в интерактивном режиме
// Параметры:
// - config: конфигурация запуска тестов
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)
}
// watchServer запускает мониторинг состояния сервера
// Периодически проверяет доступность сервера через health check
// Параметры:
// - config: конфигурация с адресом сервера
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)
}
}
}
// BenchmarkResult хранит результат бенчмарк теста
type BenchmarkResult struct {
Name string // Название бенчмарка
Ops int64 // Количество операций
NsPerOp time.Duration // Наносекунд на операцию
Allocs int64 // Количество аллокаций
Bytes int64 // Количество байт
}
// runBenchmarks запускает бенчмарк тесты
// Выполняет команду 'go test -bench=. -benchmem'
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))
}
// mainWithArgs - альтернативная точка входа с поддержкой режимов
// Обрабатывает специальные команды: interactive, bench, watch
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)
}
}