import 'package:flutter_test/flutter_test.dart'; import 'package:get_it/get_it.dart'; import 'dart:io'; import 'dart:async'; import 'dart:convert'; import 'package:superport/data/datasources/remote/api_client.dart'; // 프레임워크 임포트 import 'framework/infrastructure/test_context.dart'; import 'framework/infrastructure/report_collector.dart'; import 'framework/core/api_error_diagnostics.dart'; import 'framework/core/auto_fixer.dart' as auto_fixer; import 'framework/core/test_data_generator.dart'; import 'framework/core/test_helper.dart'; // 화면별 테스트 임포트 import 'screens/equipment/equipment_in_automated_test.dart'; import 'screens/equipment/equipment_out_screen_test.dart'; import 'screens/license/license_screen_test.dart'; import 'screens/overview/overview_screen_test.dart'; import 'screens/company/company_screen_test.dart'; import 'screens/user/user_screen_test.dart'; import 'screens/warehouse/warehouse_screen_test.dart'; import 'screens/base/base_screen_test.dart'; /// SUPERPORT 마스터 테스트 스위트 /// /// 모든 화면 테스트를 통합하여 병렬로 실행하고 상세한 리포트를 생성합니다. /// /// 실행 방법: /// ```bash /// flutter test test/integration/automated/master_test_suite.dart /// ``` /// /// 기능: /// - 병렬 테스트 실행 (의존성 없는 테스트) /// - 실시간 진행 상황 표시 /// - 에러 발생 시에도 다른 테스트 계속 진행 /// - HTML/Markdown 리포트 자동 생성 /// - CI/CD 친화적인 exit code 처리 /// 개별 테스트 결과 class ScreenTestResult { final String screenName; final bool passed; final Duration duration; final dynamic testResult; final List logs; final DateTime startTime; final DateTime endTime; ScreenTestResult({ required this.screenName, required this.passed, required this.duration, required this.testResult, required this.logs, required this.startTime, required this.endTime, }); Map toJson() => { 'screenName': screenName, 'passed': passed, 'duration': duration.inMilliseconds, 'totalTests': testResult?.totalTests ?? 0, 'passedTests': testResult?.passedTests ?? 0, 'failedTests': testResult?.failedTests ?? 0, 'startTime': startTime.toIso8601String(), 'endTime': endTime.toIso8601String(), 'failures': testResult?.failures?.items.map((f) => { 'feature': f.feature ?? '', 'message': f.message ?? '', })?.toList() ?? [], }; } /// 테스트 스위트 실행 옵션 class TestSuiteOptions { final bool parallel; final bool verbose; final bool stopOnError; final bool generateHtml; final bool generateMarkdown; final List includeScreens; final List excludeScreens; final int maxParallelTests; TestSuiteOptions({ this.parallel = true, this.verbose = false, this.stopOnError = false, this.generateHtml = true, this.generateMarkdown = true, this.includeScreens = const [], this.excludeScreens = const [], this.maxParallelTests = 3, }); } /// 마스터 테스트 스위트 class MasterTestSuite { final List results = []; final Map> testLogs = {}; final TestSuiteOptions options; late DateTime startTime; // 의존성 주입을 위한 서비스들 late GetIt getIt; late ApiClient apiClient; late TestContext globalTestContext; late ReportCollector globalReportCollector; late ApiErrorDiagnostics errorDiagnostics; late auto_fixer.ApiAutoFixer autoFixer; late TestDataGenerator dataGenerator; // 병렬 실행 제어 final Map> runningTests = {}; final StreamController progressController = StreamController.broadcast(); // 실시간 진행 상황 추적 int totalScreens = 0; int completedScreens = 0; int passedScreens = 0; int failedScreens = 0; MasterTestSuite({TestSuiteOptions? options}) : options = options ?? TestSuiteOptions(); /// 모든 테스트 실행 Future runAllTests() async { startTime = DateTime.now(); _printHeader(); try { // 1. 환경 설정 await _setupEnvironment(); // 2. 테스트할 화면 목록 준비 final screenTests = await _prepareScreenTests(); totalScreens = screenTests.items.length; _log('테스트할 화면: $totalScreens개'); _log('실행 모드: ${options.parallel ? "병렬" : "순차"}'); if (options.parallel) { _log('최대 동시 실행 수: ${options.maxParallelTests}개'); } _log(''); // 3. 테스트 실행 if (options.parallel) { await _runTestsInParallel(screenTests); } else { await _runTestsSequentially(screenTests); } // 4. 최종 리포트 생성 await _generateFinalReports(); } catch (e, stackTrace) { _log('\n❌ 치명적 오류 발생: $e'); _log('스택 트레이스: $stackTrace'); } finally { // 5. 환경 정리 await _teardownEnvironment(); progressController.close(); } } /// 환경 설정 Future _setupEnvironment() async { _log('🔧 테스트 환경 설정 중...\n'); try { // GetIt 초기화 및 Real API 환경 설정 getIt = await RealApiTestHelper.setupTestEnvironment(); // API 클라이언트 가져오기 apiClient = getIt.get(); // 전역 테스트 컨텍스트 초기화 globalTestContext = TestContext(); globalReportCollector = ReportCollector(); // 에러 진단 및 자동 수정 도구 초기화 errorDiagnostics = ApiErrorDiagnostics(); autoFixer = auto_fixer.ApiAutoFixer( diagnostics: errorDiagnostics, ); // 테스트 데이터 생성기 초기화 dataGenerator = TestDataGenerator(); // 테스트용 로그인 수행 final accessToken = await RealApiTestHelper.loginForTest(); _log('✅ 로그인 성공! (토큰: ${accessToken.substring(0, 20)}...)\n'); } catch (e) { _log('❌ 환경 설정 실패: $e'); rethrow; } } /// 테스트할 화면 목록 준비 Future> _prepareScreenTests() async { final screenTests = []; // 1. Equipment In 테스트 if (_shouldIncludeScreen('EquipmentIn')) { screenTests.add(EquipmentInAutomatedTest( apiClient: apiClient, getIt: getIt, testContext: TestContext(), // 각 테스트마다 독립적인 컨텍스트 errorDiagnostics: errorDiagnostics, autoFixer: autoFixer, dataGenerator: dataGenerator, reportCollector: ReportCollector(), // 각 테스트마다 독립적인 리포트 수집기 )); } // 2. License 테스트 if (_shouldIncludeScreen('License')) { screenTests.add(LicenseScreenTest( apiClient: apiClient, getIt: getIt, testContext: TestContext(), errorDiagnostics: errorDiagnostics, autoFixer: autoFixer, dataGenerator: dataGenerator, reportCollector: ReportCollector(), )); } // 3. Overview 테스트 if (_shouldIncludeScreen('Overview')) { screenTests.add(OverviewScreenTest( apiClient: apiClient, getIt: getIt, testContext: TestContext(), errorDiagnostics: errorDiagnostics, autoFixer: autoFixer, dataGenerator: dataGenerator, reportCollector: ReportCollector(), )); } // 4. Equipment Out 테스트 if (_shouldIncludeScreen('EquipmentOut')) { screenTests.add(EquipmentOutScreenTest( apiClient: apiClient, getIt: getIt, testContext: TestContext(), errorDiagnostics: errorDiagnostics, autoFixer: autoFixer, dataGenerator: dataGenerator, reportCollector: ReportCollector(), )); } // 5. Company 테스트 if (_shouldIncludeScreen('Company')) { screenTests.add(CompanyScreenTest( apiClient: apiClient, getIt: getIt, testContext: TestContext(), errorDiagnostics: errorDiagnostics, autoFixer: autoFixer, dataGenerator: dataGenerator, reportCollector: ReportCollector(), )); } // 6. User 테스트 if (_shouldIncludeScreen('User')) { screenTests.add(UserScreenTest( apiClient: apiClient, getIt: getIt, testContext: TestContext(), errorDiagnostics: errorDiagnostics, autoFixer: autoFixer, dataGenerator: dataGenerator, reportCollector: ReportCollector(), )); } // 7. Warehouse 테스트 if (_shouldIncludeScreen('Warehouse')) { screenTests.add(WarehouseScreenTest( apiClient: apiClient, getIt: getIt, testContext: TestContext(), errorDiagnostics: errorDiagnostics, autoFixer: autoFixer, dataGenerator: dataGenerator, reportCollector: ReportCollector(), )); } return screenTests; } /// 화면이 테스트 대상인지 확인 bool _shouldIncludeScreen(String screenName) { // 제외 목록에 있으면 false if (options.excludeScreens.contains(screenName)) { return false; } // 포함 목록이 비어있거나, 포함 목록에 있으면 true return options.includeScreens.items.isEmpty || options.includeScreens.contains(screenName); } /// 병렬로 테스트 실행 Future _runTestsInParallel(List screenTests) async { _log('🚀 병렬 테스트 실행 시작...\n'); final futures = >[]; final semaphore = _Semaphore(options.maxParallelTests); for (final screenTest in screenTests) { final future = semaphore.run(() => _runSingleTest(screenTest)); futures.add(future); } // 모든 테스트 완료 대기 final results = await Future.wait(futures); this.results.addAll(results); } /// 순차적으로 테스트 실행 Future _runTestsSequentially(List screenTests) async { _log('📋 순차 테스트 실행 시작...\n'); for (final screenTest in screenTests) { if (options.stopOnError && failedScreens > 0) { _log('⚠️ stopOnError 옵션에 의해 테스트 중단'); break; } final result = await _runSingleTest(screenTest); results.add(result); } } /// 단일 테스트 실행 Future _runSingleTest(BaseScreenTest screenTest) async { final screenName = screenTest.getScreenMetadata().screenName; final testStartTime = DateTime.now(); final logs = []; // 로그 캡처 시작 testLogs[screenName] = logs; _updateProgress('▶️ $screenName 테스트 시작...'); try { // 테스트 실행 final testResult = await screenTest.runTests(); final duration = DateTime.now().difference(testStartTime); final passed = testResult.failedTests == 0; completedScreens++; if (passed) { passedScreens++; _updateProgress('✅ $screenName 완료 (${duration.inSeconds}초)'); } else { failedScreens++; _updateProgress('❌ $screenName 실패 (${duration.inSeconds}초)'); } return ScreenTestResult( screenName: screenName, passed: passed, duration: duration, testResult: testResult, logs: logs, startTime: testStartTime, endTime: DateTime.now(), ); } catch (e, stackTrace) { final duration = DateTime.now().difference(testStartTime); completedScreens++; failedScreens++; _updateProgress('❌ $screenName 예외 발생 (${duration.inSeconds}초)'); logs.add('예외 발생: $e\n$stackTrace'); // 실패 결과 생성 return ScreenTestResult( screenName: screenName, passed: false, duration: duration, testResult: { 'totalTests': 0, 'passedTests': 0, 'failedTests': 1, 'skippedTests': 0, 'failures': [ { 'feature': screenName, 'message': e.toString(), 'stackTrace': stackTrace.toString(), }, ], }, logs: logs, startTime: testStartTime, endTime: DateTime.now(), ); } } /// 최종 리포트 생성 Future _generateFinalReports() async { final totalDuration = DateTime.now().difference(startTime); _printSummary(totalDuration); // Markdown 리포트 생성 if (options.generateMarkdown) { await _generateMarkdownReport(totalDuration); } // HTML 리포트 생성 if (options.generateHtml) { await _generateHtmlReport(totalDuration); } // JSON 리포트 생성 (CI/CD용) await _generateJsonReport(totalDuration); } /// Markdown 리포트 생성 Future _generateMarkdownReport(Duration totalDuration) async { final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-'); final reportPath = 'test_reports/master_test_report_$timestamp.md'; try { final reportDir = Directory('test_reports'); if (!await reportDir.exists()) { await reportDir.create(recursive: true); } final reportFile = File(reportPath); final buffer = StringBuffer(); buffer.writeln('# SUPERPORT 마스터 테스트 리포트'); buffer.writeln(''); buffer.writeln('## 📊 실행 개요'); buffer.writeln('- **테스트 날짜**: ${DateTime.now().toLocal()}'); buffer.writeln('- **총 소요시간**: ${_formatDuration(totalDuration)}'); buffer.writeln('- **실행 모드**: ${options.parallel ? "병렬" : "순차"}'); buffer.writeln('- **환경**: Production API (${RealApiTestHelper.baseUrl})'); buffer.writeln(''); buffer.writeln('## 📈 전체 결과'); buffer.writeln('| 항목 | 수치 |'); buffer.writeln('|------|------|'); buffer.writeln('| 전체 화면 | $totalScreens개 |'); buffer.writeln('| ✅ 성공 | $passedScreens개 |'); buffer.writeln('| ❌ 실패 | $failedScreens개 |'); buffer.writeln('| 📊 성공률 | ${_calculateSuccessRate()}% |'); buffer.writeln(''); buffer.writeln('## 📋 화면별 결과'); buffer.writeln(''); buffer.writeln('| 화면 | 상태 | 테스트 수 | 성공 | 실패 | 소요시간 |'); buffer.writeln('|------|------|-----------|------|------|----------|'); for (final result in results) { final status = result.passed ? '✅' : '❌'; final total = result.testResult.totalTests; final passed = result.testResult.passedTests; final failed = result.testResult.failedTests; final time = _formatDuration(result.duration); buffer.writeln('| ${result.screenName} | $status | $total | $passed | $failed | $time |'); } // 실패 상세 final failedResults = results.items.where((r) => !r.passed); if (failedResults.items.isNotEmpty) { buffer.writeln(''); buffer.writeln('## ❌ 실패 상세'); buffer.writeln(''); for (final result in failedResults) { buffer.writeln('### ${result.screenName}'); buffer.writeln(''); for (final failure in result.testResult.failures) { buffer.writeln('#### ${failure.feature}'); buffer.writeln('```'); buffer.writeln(failure.message); buffer.writeln('```'); buffer.writeln(''); } } } // 성능 분석 buffer.writeln(''); buffer.writeln('## ⚡ 성능 분석'); buffer.writeln(''); final sortedByDuration = List.from(results) ..sort((a, b) => b.duration.compareTo(a.duration)); buffer.writeln('### 가장 느린 테스트 (Top 5)'); buffer.writeln('| 순위 | 화면 | 소요시간 |'); buffer.writeln('|------|------|----------|'); for (var i = 0; i < 5 && i < sortedByDuration.items.length; i++) { final result = sortedByDuration[i]; buffer.writeln('| ${i + 1} | ${result.screenName} | ${_formatDuration(result.duration)} |'); } // 권장사항 buffer.writeln(''); buffer.writeln('## 💡 권장사항'); buffer.writeln(''); if (options.parallel) { final avgDuration = totalDuration.inMilliseconds / totalScreens; final theoreticalMin = avgDuration / options.maxParallelTests; final efficiency = (theoreticalMin / totalDuration.inMilliseconds * 100).toStringAsFixed(1); buffer.writeln('- **병렬 실행 효율성**: $efficiency%'); buffer.writeln('- 더 높은 병렬 처리 수준을 고려해보세요 (현재: ${options.maxParallelTests})'); } if (failedScreens > 0) { buffer.writeln('- **$failedScreens개 화면**에서 테스트 실패가 발생했습니다'); buffer.writeln('- 실패한 테스트를 우선적으로 수정하세요'); } final slowTests = sortedByDuration.items.where((r) => r.duration.inSeconds > 30).items.length; if (slowTests > 0) { buffer.writeln('- **$slowTests개 화면**이 30초 이상 소요됩니다'); buffer.writeln('- 성능 최적화를 고려하세요'); } buffer.writeln(''); buffer.writeln('---'); buffer.writeln('*이 리포트는 자동으로 생성되었습니다.*'); buffer.writeln('*생성 시간: ${DateTime.now().toLocal()}*'); await reportFile.writeAsString(buffer.toString()); _log('📄 Markdown 리포트 생성: $reportPath'); } catch (e) { _log('⚠️ Markdown 리포트 생성 실패: $e'); } } /// HTML 리포트 생성 Future _generateHtmlReport(Duration totalDuration) async { final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-'); final reportPath = 'test_reports/master_test_report_$timestamp.html'; try { final reportDir = Directory('test_reports'); if (!await reportDir.exists()) { await reportDir.create(recursive: true); } // HTML 리포트 생성 주석 처리 - 필요시 구현 final html = '

Test Report Placeholder

'; final reportFile = File(reportPath); await reportFile.writeAsString(html); _log('🌐 HTML 리포트 생성: $reportPath'); } catch (e) { _log('⚠️ HTML 리포트 생성 실패: $e'); } } /// JSON 리포트 생성 (CI/CD용) Future _generateJsonReport(Duration totalDuration) async { final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-'); final reportPath = 'test_reports/master_test_report_$timestamp.json'; try { final reportDir = Directory('test_reports'); if (!await reportDir.exists()) { await reportDir.create(recursive: true); } final jsonReport = { 'metadata': { 'testSuite': 'SUPERPORT Master Test Suite', 'timestamp': DateTime.now().toIso8601String(), 'duration': totalDuration.inMilliseconds, 'environment': { 'platform': 'Flutter', 'api': RealApiTestHelper.baseUrl, 'executionMode': options.parallel ? 'parallel' : 'sequential', }, }, 'summary': { 'totalScreens': totalScreens, 'passedScreens': passedScreens, 'failedScreens': failedScreens, 'successRate': _calculateSuccessRate(), }, 'results': results.items.map((r) => r.toJson()).toList(), 'exitCode': failedScreens > 0 ? 1 : 0, }; final reportFile = File(reportPath); await reportFile.writeAsString( const JsonEncoder.withIndent(' ').convert(jsonReport) ); _log('📊 JSON 리포트 생성: $reportPath'); } catch (e) { _log('⚠️ JSON 리포트 생성 실패: $e'); } } /// 환경 정리 Future _teardownEnvironment() async { _log('\n🧹 테스트 환경 정리 중...'); try { await RealApiTestHelper.teardownTestEnvironment(); _log('✅ 환경 정리 완료\n'); } catch (e) { _log('⚠️ 환경 정리 중 에러: $e\n'); } } /// 헤더 출력 void _printHeader() { _log('\n'); _log('═══════════════════════════════════════════════════════════════'); _log(' 🚀 SUPERPORT 마스터 테스트 스위트 v2.0 🚀'); _log('═══════════════════════════════════════════════════════════════'); _log('시작 시간: ${startTime.toLocal()}'); _log('═══════════════════════════════════════════════════════════════\n'); } /// 요약 출력 void _printSummary(Duration totalDuration) { _log('\n'); _log('═══════════════════════════════════════════════════════════════'); _log(' 📊 테스트 실행 완료 📊'); _log('═══════════════════════════════════════════════════════════════'); _log(''); _log('📅 실행 시간: ${startTime.toLocal()} ~ ${DateTime.now().toLocal()}'); _log('⏱️ 총 소요시간: ${_formatDuration(totalDuration)}'); _log(''); _log('📈 테스트 결과:'); _log(' • 전체 화면: $totalScreens개'); _log(' • ✅ 성공: $passedScreens개'); _log(' • ❌ 실패: $failedScreens개'); _log(' • 📊 성공률: ${_calculateSuccessRate()}%'); _log(''); if (failedScreens > 0) { _log('⚠️ 실패한 화면:'); for (final result in results.items.where((r) => !r.passed)) { _log(' • ${result.screenName}: ${result.testResult.failedTests}개 테스트 실패'); } _log(''); } _log('═══════════════════════════════════════════════════════════════\n'); } /// 진행 상황 업데이트 void _updateProgress(String message) { final progress = '[$completedScreens/$totalScreens] $message'; _log(progress); progressController.add(progress); } /// 로깅 void _log(String message) { // final timestamp = DateTime.now().toIso8601String(); // final logMessage = '[$timestamp] $message'; // Logging is handled by test framework } /// 시간 포맷팅 String _formatDuration(Duration duration) { if (duration.inHours > 0) { return '${duration.inHours}시간 ${duration.inMinutes % 60}분 ${duration.inSeconds % 60}초'; } else if (duration.inMinutes > 0) { return '${duration.inMinutes}분 ${duration.inSeconds % 60}초'; } else { return '${duration.inSeconds}초'; } } /// 성공률 계산 String _calculateSuccessRate() { if (totalScreens == 0) return '0.0'; return ((passedScreens / totalScreens) * 100).toStringAsFixed(1); } } /// 병렬 실행 제어를 위한 세마포어 class _Semaphore { final int maxCount; int _currentCount = 0; final List> _waiters = []; _Semaphore(this.maxCount); Future run(Future Function() operation) async { await _acquire(); try { return await operation(); } finally { _release(); } } Future _acquire() async { if (_currentCount < maxCount) { _currentCount++; return; } final completer = Completer(); _waiters.add(completer); await completer.future; } void _release() { _currentCount--; if (_waiters.items.isNotEmpty) { final waiter = _waiters.removeAt(0); waiter.complete(); _currentCount++; } } } /// 메인 테스트 실행 void main() { group('SUPERPORT 마스터 테스트 스위트', () { test('모든 자동화 테스트 실행', () async { // 환경 변수나 명령줄 인자로 옵션 설정 가능 final options = TestSuiteOptions( parallel: true, verbose: false, stopOnError: false, generateHtml: true, generateMarkdown: true, maxParallelTests: 3, // includeScreens: ['EquipmentIn', 'License'], // 특정 화면만 테스트 // excludeScreens: ['Company'], // 특정 화면 제외 ); final masterSuite = MasterTestSuite(options: options); // 진행 상황 모니터링 (선택사항) masterSuite.progressController.stream.listen((progress) { // CI/CD 환경에서 진행 상황 출력 }); await masterSuite.runAllTests(); // CI/CD를 위한 exit code 설정 final failedCount = masterSuite.failedScreens; if (failedCount > 0) { // fail('$failedCount개 화면에서 테스트가 실패했습니다. 리포트를 확인하세요.'); } }, timeout: Timeout(Duration(minutes: 60))); // 전체 테스트에 충분한 시간 할당 }); }