// cmd/testrunner/runner.go - Дополнительные утилиты для тестового раннера package main import ( "encoding/json" "encoding/xml" "fmt" "os" "os/exec" "strings" "time" ) // saveResultsToJSON сохраняет результаты тестов в JSON файл // Параметры: // - results: массив результатов тестов // - filename: имя файла для сохранения // Возвращает: ошибку при сохранении func saveResultsToJSON(results []TestResult, filename string) error { type JSONResult struct { Timestamp string `json:"timestamp"` // Время сохранения отчета Results []TestResult `json:"results"` // Результаты тестов Summary struct { Total int `json:"total"` // Общее количество тестов Passed int `json:"passed"` // Количество пройденных Failed int `json:"failed"` // Количество проваленных PassRate float64 `json:"pass_rate"` // Процент прохождения } `json:"summary"` } var jsonResult JSONResult jsonResult.Timestamp = time.Now().Format(time.RFC3339) jsonResult.Results = results // Подсчет статистики for _, r := range results { jsonResult.Summary.Total++ if r.Passed { jsonResult.Summary.Passed++ } else { jsonResult.Summary.Failed++ } } // Расчет процента прохождения if jsonResult.Summary.Total > 0 { jsonResult.Summary.PassRate = float64(jsonResult.Summary.Passed) / float64(jsonResult.Summary.Total) * 100 } data, err := json.MarshalIndent(jsonResult, "", " ") if err != nil { return err } return os.WriteFile(filename, data, 0644) } // generateJUnitReport создает отчет в формате JUnit XML для CI/CD систем // Параметры: // - results: массив результатов тестов // - filename: имя файла для сохранения // Возвращает: ошибку при сохранении func generateJUnitReport(results []TestResult, filename string) error { // TestCase представляет один тестовый случай в JUnit формате type TestCase struct { Name string `xml:"name,attr"` // Название теста Classname string `xml:"classname,attr"` // Класс теста Time float64 `xml:"time,attr"` // Время выполнения Failure *string `xml:"failure,omitempty"` // Информация об ошибке (опционально) } // TestSuite представляет набор тестов в JUnit формате type TestSuite struct { Name string `xml:"name,attr"` // Название набора Tests int `xml:"tests,attr"` // Количество тестов Failures int `xml:"failures,attr"` // Количество провалов Time float64 `xml:"time,attr"` // Общее время TestCases []TestCase `xml:"testcase"` // Список тестов } // TestSuites корневой элемент JUnit отчета type TestSuites struct { XMLName string `xml:"testsuites"` // Имя корневого элемента TestSuites []TestSuite `xml:"testsuite"` // Список наборов тестов } var suites TestSuites // Конвертируем результаты в JUnit формат for _, result := range results { suite := TestSuite{ Name: result.Suite, Tests: 1, Time: result.Duration.Seconds(), } testCase := TestCase{ Name: fmt.Sprintf("%s Suite", result.Suite), Classname: "api.tests", Time: result.Duration.Seconds(), } // Добавляем информацию об ошибке если тест провален if !result.Passed { suite.Failures = 1 failureMsg := result.Error.Error() testCase.Failure = &failureMsg } suite.TestCases = append(suite.TestCases, testCase) suites.TestSuites = append(suites.TestSuites, suite) } // Маршалинг в XML и сохранение xmlData, err := xml.MarshalIndent(suites, "", " ") if err != nil { return err } return os.WriteFile(filename, append([]byte(xml.Header), xmlData...), 0644) } // runTestsByTag запускает тесты с определенным тегом (build tag) // Позволяет фильтровать тесты по тегам // Параметры: // - tag: тег для фильтрации тестов // - config: конфигурация запуска // Возвращает: массив результатов тестов func runTestsByTag(tag string, config *Config) []TestResult { fmt.Printf("%s🏷️ Running tests with tag: %s%s\n", colors.Yellow, tag, colors.Reset) cmd := exec.Command("go", "test", "-v", "-tags", tag, "./tests/integration/...") output, err := cmd.CombinedOutput() result := TestResult{ Suite: fmt.Sprintf("Tag: %s", tag), Output: string(output), Passed: err == nil, Error: err, } if result.Passed { fmt.Printf("%s✅ Tests with tag '%s' passed%s\n", colors.Green, tag, colors.Reset) } else { fmt.Printf("%s❌ Tests with tag '%s' failed%s\n", colors.Red, tag, colors.Reset) } return []TestResult{result} } // generateCIScript создает bash скрипт для запуска тестов в CI/CD // Скрипт автоматически запускает сервер, выполняет тесты и останавливает сервер func generateCIScript() { ciScript := `#!/bin/bash # CI/CD script for API tests set -e echo "Starting API Tests in CI environment" # Запуск сервера в фоне go run cmd/main.go & SERVER_PID=$! # Ждем запуска сервера sleep 5 # Запуск тестов go run cmd/testrunner/main.go -suite all -coverage # Остановка сервера kill $SERVER_PID echo "Tests completed" ` // Создаем директорию scripts если ее нет if err := os.MkdirAll("scripts", 0755); err != nil { fmt.Printf("Failed to create scripts directory: %v\n", err) return } // Сохраняем скрипт и делаем его исполняемым if err := os.WriteFile("scripts/ci_test.sh", []byte(ciScript), 0755); err != nil { fmt.Printf("Failed to generate CI script: %v\n", err) } } // compareResults сравнивает результаты текущего запуска с предыдущим // Показывает изменения в прохождении тестов и времени выполнения // Параметры: // - current: текущие результаты тестов // - previous: предыдущие результаты для сравнения func compareResults(current, previous []TestResult) { fmt.Printf("%s📊 Test Result Comparison%s\n", colors.Cyan, colors.Reset) fmt.Println(strings.Repeat("-", 50)) // Создаем карты для быстрого доступа к результатам currentMap := make(map[string]TestResult) for _, r := range current { currentMap[r.Suite] = r } prevMap := make(map[string]TestResult) for _, r := range previous { prevMap[r.Suite] = r } // Сравниваем каждый набор тестов for name, currentResult := range currentMap { prevResult, exists := prevMap[name] if !exists { // Новый набор тестов fmt.Printf("%s➕ New test suite: %s%s\n", colors.Green, name, colors.Reset) continue } // Сравниваем статус прохождения if currentResult.Passed != prevResult.Passed { if currentResult.Passed { fmt.Printf("%s✅ Fixed: %s (was failing)%s\n", colors.Green, name, colors.Reset) } else { fmt.Printf("%s❌ Regression: %s (was passing)%s\n", colors.Red, name, colors.Reset) } } else if currentResult.Passed { // Сравнение времени выполнения для успешных тестов diff := currentResult.Duration - prevResult.Duration if diff > 0 { fmt.Printf("%s⚠️ Slower: %s (+%v)%s\n", colors.Yellow, name, diff.Round(time.Millisecond), colors.Reset) } else if diff < 0 { fmt.Printf("%s⚡ Faster: %s (%v)%s\n", colors.Green, name, (-diff).Round(time.Millisecond), colors.Reset) } } } }