import 'dart:async'; import 'dart:developer' as developer; import 'package:flutter/foundation.dart'; import 'package:lanebow/utils/test_utils.dart'; import 'package:lanebow/utils/test_config.dart'; // 이벤트 클래스들 - 클래스 외부로 이동 class ClubChangedEvent { final String clubId; ClubChangedEvent(this.clubId); @override String toString() => 'ClubChangedEvent(clubId: $clubId)'; @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is ClubChangedEvent && other.clubId == clubId; } @override int get hashCode => clubId.hashCode; } class EventBus { // 테스트 환경에서 사용할 별도의 인스턴스 맵 static final Map _testInstances = {}; // 기본 싱글톤 인스턴스 static final EventBus _instance = EventBus._internal(); // 테스트 ID를 저장하는 변수 (테스트 격리용) static String? _currentTestId; // 테스트 환경 초기화 상태 추적 static bool _isTestEnvironmentReady = false; // 테스트 환경 초기화 관련 로깅 static void _logTestEnvironmentInit() { _isTestEnvironmentReady = true; if (kDebugMode) { debugPrint('EventBus: 테스트 환경 초기화 완료'); } } // 활성 구독 수 추적 (디버깅 및 메모리 누수 감지용) static int _activeSubscriptionCount = 0; /// 현재 활성화된 테스트 인스턴스 수 반환 static int get testInstanceCount => _testInstances.length; // 테스트 재시도 횟수 추적 및 관리 static int _testRetryCount = 0; // 테스트 재시도 횟수 가져오기 static int get testRetryCount => _testRetryCount; // 테스트 재시도 횟수 증가 static void incrementRetryCount() { _testRetryCount++; if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 테스트 재시도 횟수 증가 - $_testRetryCount'); } } // 활성 구독 수 접근 게터 static int get activeSubscriptionCount => _activeSubscriptionCount; // 현재 테스트 ID 설정 메서드 static void setCurrentTestId(String testId) { // 테스트 설정 로드 확인 및 로깅 final config = TestConfig.instance; if (config.isVerboseLogging) { debugPrint('EventBus: 테스트 설정 로드 - asyncTimeout=${config.asyncTimeout}ms, testDelay=${config.testDelay}ms'); } // 이전 테스트 ID가 있으면 해당 인스턴스 정리 if (_currentTestId != null && _currentTestId != testId) { if (_testInstances.containsKey(_currentTestId!)) { try { _testInstances[_currentTestId!]!.dispose(); } catch (e) { if (kDebugMode) { debugPrint('EventBus: 이전 인스턴스 dispose 중 오류 - $e'); } } finally { _testInstances.remove(_currentTestId!); } } } // 새 테스트 ID 설정 _currentTestId = testId; // 새 테스트 ID에 대한 인스턴스가 없으면 생성 if (!_testInstances.containsKey(testId)) { _testInstances[testId] = EventBus._internal(); } else { // 이미 존재하는 인스턴스라도 초기화 수행 _testInstances[testId]!.reset(); } // 테스트 환경 초기화 로깅 _logTestEnvironmentInit(); // 활성 구독 수 초기화 _activeSubscriptionCount = 0; // 테스트 재시도 횟수 초기화 _testRetryCount = 0; if (kDebugMode) { debugPrint('EventBus: 테스트 ID 설정됨 - $testId'); developer.log('EventBus 메모리 상태: $_testInstances', name: 'EventBus'); } } // 현재 테스트 ID 초기화 메서드 static Future tearDownTestEnvironment() async { if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 테스트 환경 초기화 해제 시작'); } // 테스트 환경 해제 전 비동기 리소스 상태 로깅 await _logAsyncResourceState('테스트 환경 해제 전'); // 테스트 종료 전 남아있는 타이머가 완료될 시간 제공 await _ensureAllTimersComplete(); // 현재 테스트 ID에 해당하는 인스턴스가 있는 경우 먼저 dispose 호출 if (_currentTestId != null && _testInstances.containsKey(_currentTestId!)) { try { if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 현재 테스트 ID($_currentTestId) 인스턴스 dispose 시도'); } await _testInstances[_currentTestId!]!.dispose(); } catch (e) { if (kDebugMode) { debugPrint('EventBus: 현재 테스트 인스턴스 dispose 중 오류 - $e'); } } } _currentTestId = null; // 테스트 환경 초기화 해제 로깅 _isTestEnvironmentReady = false; if (kDebugMode) { debugPrint('EventBus: 테스트 환경 초기화 해제'); } _activeSubscriptionCount = 0; // 테스트 환경 해제 후 비동기 리소스 상태 로깅 await _logAsyncResourceState('테스트 환경 해제 후'); // 메모리 누수 방지를 위해 GC 힌트 추가 if (TestUtils.isInTest) { developer.log('EventBus 메모리 정리 요청', name: 'EventBus'); } // 추가 마이크로태스크 실행으로 비동기 작업 완료 보장 await Future.microtask(() {}); await Future.microtask(() {}); } // 테스트 ID 초기화 (후방 호환성을 위해 유지) static Future clearCurrentTestId() async { // tearDownTestEnvironment로 대체되었지만 후방 호환성을 위해 유지 await tearDownTestEnvironment(); } // 모든 테스트 인스턴스를 정리하는 정적 메서드 static Future resetAllTestInstances() async { if (_testInstances.isEmpty) { return; } if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 모든 테스트 인스턴스 초기화 시작 (인스턴스 수: ${_testInstances.length})'); } // 초기화 전 비동기 리소스 상태 로깅 await _logAsyncResourceState('모든 테스트 인스턴스 초기화 전'); // 기존 인스턴스 모두 정리 for (var entry in _testInstances.entries) { try { if (kDebugMode) { debugPrint('EventBus: 인스턴스 정리 중 - ${entry.key}'); } entry.value.dispose(); // 완전히 dispose 호출 // 각 인스턴스 dispose 후 마이크로태스크 실행으로 비동기 작업 완료 보장 await Future.microtask(() {}); } catch (e) { // dispose 중 오류 무시 if (kDebugMode) { debugPrint('EventBus: 인스턴스 정리 중 오류 - $e'); } } } // 인스턴스 정리 후 중간 상태 로깅 await _logAsyncResourceState('테스트 인스턴스 dispose 후, 초기화 전'); // 모든 테스트 인스턴스 제거 _testInstances.clear(); // 기본 인스턴스도 초기화 try { _instance.reset(); await Future.microtask(() {}); // 기본 인스턴스 초기화 후 마이크로태스크 실행 } catch (e) { // 기본 인스턴스 초기화 중 오류 무시 if (kDebugMode) { debugPrint('EventBus: 기본 인스턴스 초기화 중 오류 - $e'); } } // 현재 테스트 ID도 초기화 _currentTestId = null; // 테스트 환경 초기화 해제 로깅 if (_isTestEnvironmentReady) { _isTestEnvironmentReady = false; if (kDebugMode) { debugPrint('EventBus: 모든 테스트 환경 초기화 해제'); } } _activeSubscriptionCount = 0; // 비동기 작업이 완료될 시간을 제공하되, 타이머 생성 없이 동기화 // 타이머 생성 없이 마이크로태스크 큐 완료 보장 await Future.microtask(() {}); await Future.microtask(() {}); // 테스트 종료 전 남아있는 타이머가 완료될 시간 제공 // 테스트 환경에서 !timersPending assertion 실패 방지 await _ensureAllTimersComplete(); // 초기화 완료 후 비동기 리소스 상태 로깅 await _logAsyncResourceState('모든 테스트 인스턴스 초기화 후'); if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 모든 테스트 인스턴스 초기화됨'); developer.log('EventBus 메모리 상태 초기화 완료', name: 'EventBus'); } } // 비동기 리소스 상태 진단 및 로깅 static Future> _diagnoseAsyncResources() async { final diagnosticInfo = { 'timestamp': DateTime.now().toIso8601String(), 'activeSubscriptions': _activeSubscriptionCount, 'testInstances': _testInstances.length, 'currentTestId': _currentTestId, 'isTestEnvironmentReady': _isTestEnvironmentReady, 'testRetryCount': _testRetryCount, }; // 인스턴스별 상태 수집 final instanceStates = >{}; for (final entry in _testInstances.entries) { instanceStates[entry.key] = { 'isDisposed': entry.value._isDisposed, 'isStreamControllerClosed': entry.value._streamController.isClosed, }; } diagnosticInfo['instanceStates'] = instanceStates; // 기본 인스턴스 상태 diagnosticInfo['defaultInstance'] = { 'isDisposed': _instance._isDisposed, 'isStreamControllerClosed': _instance._streamController.isClosed, }; return diagnosticInfo; } // 비동기 리소스 상태 로깅 static Future _logAsyncResourceState(String context) async { if (kDebugMode || TestConfig.instance.isVerboseLogging) { final diagnosticInfo = await _diagnoseAsyncResources(); debugPrint('===== EventBus 비동기 리소스 상태 ($context) ====='); debugPrint('시간: ${diagnosticInfo['timestamp']}'); debugPrint('활성 구독 수: ${diagnosticInfo['activeSubscriptions']}'); debugPrint('테스트 인스턴스 수: ${diagnosticInfo['testInstances']}'); debugPrint('현재 테스트 ID: ${diagnosticInfo['currentTestId']}'); debugPrint('테스트 환경 준비 상태: ${diagnosticInfo['isTestEnvironmentReady']}'); debugPrint('테스트 재시도 횟수: ${diagnosticInfo['testRetryCount']}'); // 인스턴스별 상태 출력 final instanceStates = diagnosticInfo['instanceStates'] as Map>; if (instanceStates.isNotEmpty) { debugPrint('--- 테스트 인스턴스 상태 ---'); instanceStates.forEach((key, value) { debugPrint(' $key: disposed=${value['isDisposed']}, streamClosed=${value['isStreamControllerClosed']}'); }); } // 기본 인스턴스 상태 final defaultInstance = diagnosticInfo['defaultInstance'] as Map; debugPrint('--- 기본 인스턴스 상태 ---'); debugPrint(' disposed=${defaultInstance['isDisposed']}, streamClosed=${defaultInstance['isStreamControllerClosed']}'); debugPrint('==========================================='); } } static Future _ensureAllTimersComplete() async { if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 타이머 완료 대기 시작'); } // 비동기 리소스 상태 로깅 (시작) await _logAsyncResourceState('타이머 완료 대기 시작'); // 타이머를 새로 만들지 않기 위해 microtask만 사용 await Future.microtask(() {}); await Future.microtask(() {}); await Future.microtask(() {}); // 중간 상태 로깅 await _logAsyncResourceState('타이머 완료 중간 상태'); // 테스트 설정에서 정의된 타이머 완료 대기 시간을 적용 // 추가 대기 시간이 설정되어 있어도 실제 Timer 생성은 하지 않음(테스트 타이머 경합 방지) // 필요한 경우 호출자에서 fakeAsync 등을 사용해 제어하도록 위임 // 비동기 리소스 상태 로깅 (완료) await _logAsyncResourceState('타이머 완료 대기 완료'); if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 타이머 완료 대기 완료'); } } factory EventBus() { // 테스트 환경에서는 테스트 ID 자동 생성 및 설정 if (TestUtils.isInTest) { // 테스트 ID가 설정되지 않았다면 자동으로 생성하지 말고, 명시적 설정을 강제한다 if (_currentTestId == null) { throw StateError('EventBus: 테스트 환경에서 currentTestId가 설정되지 않았습니다. setUp에서 EventBus.setCurrentTestId(...)를 호출하세요.'); } // 테스트 ID가 있지만 인스턴스가 없는 경우 생성 if (!_testInstances.containsKey(_currentTestId!)) { if (kDebugMode) { debugPrint('EventBus: 테스트 ID ${_currentTestId}에 대한 새 인스턴스 생성'); } _testInstances[_currentTestId!] = EventBus._internal(); _testInstances[_currentTestId!]!.reset(); } return _testInstances[_currentTestId]!; } // 테스트 환경이 아닌 경우 기본 싱글톤 인스턴스 반환 return _instance; } EventBus._internal(); // 이벤트 버스 내부 상태 StreamController _streamController = StreamController.broadcast(); bool _isDisposed = false; Stream get stream => _streamController.stream; void fire(dynamic event) { if (!_isDisposed && !_streamController.isClosed) { _streamController.add(event); // 디버그 모드 또는 상세 로깅 설정 시 이벤트 발생 로그 추가 if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 이벤트 발생 - $event'); } } else if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 이벤트 발생 실패 (이미 dispose됨) - $event'); } } // 특정 타입의 이벤트를 구독하는 메서드 Stream on() { if (_isDisposed) { if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: dispose된 인스턴스에서 on<$T> 호출됨'); } // 빈 스트림 반환 return Stream.empty(); } // 타입 필터링 및 변환 final filteredStream = stream.where((event) => event is T).cast(); // 구독 생성 시 카운터 증가 - 한 번만 증가 _activeSubscriptionCount++; if (TestUtils.isInTest) { if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 새 구독 추가됨 <$T> (현재 구독 수: $_activeSubscriptionCount)'); } // 구독 취소 시 카운터 감소를 위한 처리 // StreamController를 사용하여 구독 취소 시 카운터 감소 보장 final controller = StreamController(); // 원본 스트림에서 데이터 수신 및 전달 final subscription = filteredStream.listen( (data) => controller.add(data), onError: (error) => controller.addError(error), onDone: () => controller.close(), ); // 컨트롤러 닫힐 때 구독 취소 및 카운터 감소 controller.onCancel = () { subscription.cancel(); _activeSubscriptionCount = (_activeSubscriptionCount > 0) ? _activeSubscriptionCount - 1 : 0; if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 구독 취소됨 <$T> (남은 구독 수: $_activeSubscriptionCount)'); } }; return controller.stream; } return filteredStream; } Future _logInstanceState() async { if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus 인스턴스 상태:'); debugPrint(' isDisposed: $_isDisposed'); debugPrint(' streamController.isClosed: ${_streamController.isClosed}'); if (TestUtils.isInTest) { debugPrint(' 현재 테스트 ID: $_currentTestId'); debugPrint(' 활성 구독 수: $_activeSubscriptionCount'); } } } Future dispose() async { if (!_isDisposed) { // dispose 전 상태 로깅 if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: dispose 시작'); await _logInstanceState(); } _isDisposed = true; if (!_streamController.isClosed) { try { await _streamController.close(); // 마이크로태스크 실행으로 비동기 작업 완료 보장 await Future.microtask(() {}); if ((kDebugMode || TestConfig.instance.isVerboseLogging) && TestUtils.isInTest) { debugPrint('EventBus: 인스턴스 dispose 완료 (테스트 ID: $_currentTestId)'); // dispose 후 상태 로깅 await _logInstanceState(); } } catch (e) { if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: dispose 중 오류 - $e'); } } } } else if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 이미 dispose된 인스턴스에 dispose 호출됨'); } } /// 테스트 환경에서 EventBus를 초기화하는 메서드 /// 기존 StreamController를 닫고 새로운 것으로 교체 Future reset() async { if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: reset 시작'); await _logInstanceState(); } if (!_isDisposed) { try { await dispose(); } catch (e) { if (kDebugMode) { debugPrint('EventBus: reset 중 dispose 호출 오류 - $e'); } } } // 새로운 StreamController 생성 _streamController = StreamController.broadcast(); _isDisposed = false; // 활성 구독 수 초기화 (인스턴스 레벨) if (TestUtils.isInTest) { _activeSubscriptionCount = 0; } // 마이크로태스크 실행으로 비동기 작업 완료 보장 await Future.microtask(() {}); if (kDebugMode || TestConfig.instance.isVerboseLogging) { debugPrint('EventBus: 인스턴스 reset 완료 (테스트 ID: $_currentTestId)'); await _logInstanceState(); } } }