Line data Source code
1 : import 'dart:async';
2 : import 'dart:developer' as developer;
3 : import 'package:flutter/foundation.dart';
4 : import 'package:lanebow/utils/test_utils.dart';
5 : import 'package:lanebow/utils/test_config.dart';
6 :
7 : // 이벤트 클래스들 - 클래스 외부로 이동
8 : class ClubChangedEvent {
9 : final String clubId;
10 :
11 4 : ClubChangedEvent(this.clubId);
12 :
13 4 : @override
14 8 : String toString() => 'ClubChangedEvent(clubId: $clubId)';
15 :
16 0 : @override
17 : bool operator ==(Object other) {
18 : if (identical(this, other)) return true;
19 0 : return other is ClubChangedEvent && other.clubId == clubId;
20 : }
21 :
22 0 : @override
23 0 : int get hashCode => clubId.hashCode;
24 : }
25 :
26 : class EventBus {
27 : // 테스트 환경에서 사용할 별도의 인스턴스 맵
28 36 : static final Map<String, EventBus> _testInstances = {};
29 :
30 : // 기본 싱글톤 인스턴스
31 42 : static final EventBus _instance = EventBus._internal();
32 :
33 : // 테스트 ID를 저장하는 변수 (테스트 격리용)
34 : static String? _currentTestId;
35 :
36 : // 테스트 환경 초기화 상태 추적
37 : static bool _isTestEnvironmentReady = false;
38 :
39 : // 테스트 환경 초기화 관련 로깅
40 12 : static void _logTestEnvironmentInit() {
41 : _isTestEnvironmentReady = true;
42 : if (kDebugMode) {
43 24 : debugPrint('EventBus: 테스트 환경 초기화 완료');
44 : }
45 : }
46 :
47 : // 활성 구독 수 추적 (디버깅 및 메모리 누수 감지용)
48 : static int _activeSubscriptionCount = 0;
49 :
50 : /// 현재 활성화된 테스트 인스턴스 수 반환
51 3 : static int get testInstanceCount => _testInstances.length;
52 :
53 : // 테스트 재시도 횟수 추적 및 관리
54 : static int _testRetryCount = 0;
55 :
56 : // 테스트 재시도 횟수 가져오기
57 0 : static int get testRetryCount => _testRetryCount;
58 :
59 : // 테스트 재시도 횟수 증가
60 0 : static void incrementRetryCount() {
61 0 : _testRetryCount++;
62 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
63 0 : debugPrint('EventBus: 테스트 재시도 횟수 증가 - $_testRetryCount');
64 : }
65 : }
66 :
67 : // 활성 구독 수 접근 게터
68 2 : static int get activeSubscriptionCount => _activeSubscriptionCount;
69 :
70 : // 현재 테스트 ID 설정 메서드
71 12 : static void setCurrentTestId(String testId) {
72 : // 테스트 설정 로드 확인 및 로깅
73 12 : final config = TestConfig.instance;
74 12 : if (config.isVerboseLogging) {
75 0 : debugPrint('EventBus: 테스트 설정 로드 - asyncTimeout=${config.asyncTimeout}ms, testDelay=${config.testDelay}ms');
76 : }
77 : // 이전 테스트 ID가 있으면 해당 인스턴스 정리
78 1 : if (_currentTestId != null && _currentTestId != testId) {
79 2 : if (_testInstances.containsKey(_currentTestId!)) {
80 : try {
81 3 : _testInstances[_currentTestId!]!.dispose();
82 : } catch (e) {
83 : if (kDebugMode) {
84 0 : debugPrint('EventBus: 이전 인스턴스 dispose 중 오류 - $e');
85 : }
86 : } finally {
87 2 : _testInstances.remove(_currentTestId!);
88 : }
89 : }
90 : }
91 :
92 : // 새 테스트 ID 설정
93 : _currentTestId = testId;
94 :
95 : // 새 테스트 ID에 대한 인스턴스가 없으면 생성
96 24 : if (!_testInstances.containsKey(testId)) {
97 36 : _testInstances[testId] = EventBus._internal();
98 : } else {
99 : // 이미 존재하는 인스턴스라도 초기화 수행
100 3 : _testInstances[testId]!.reset();
101 : }
102 :
103 : // 테스트 환경 초기화 로깅
104 12 : _logTestEnvironmentInit();
105 :
106 : // 활성 구독 수 초기화
107 : _activeSubscriptionCount = 0;
108 :
109 : // 테스트 재시도 횟수 초기화
110 : _testRetryCount = 0;
111 :
112 : if (kDebugMode) {
113 36 : debugPrint('EventBus: 테스트 ID 설정됨 - $testId');
114 36 : developer.log('EventBus 메모리 상태: $_testInstances', name: 'EventBus');
115 : }
116 : }
117 :
118 : // 현재 테스트 ID 초기화 메서드
119 11 : static Future<void> tearDownTestEnvironment() async {
120 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
121 22 : debugPrint('EventBus: 테스트 환경 초기화 해제 시작');
122 : }
123 :
124 : // 테스트 환경 해제 전 비동기 리소스 상태 로깅
125 11 : await _logAsyncResourceState('테스트 환경 해제 전');
126 :
127 : // 테스트 종료 전 남아있는 타이머가 완료될 시간 제공
128 11 : await _ensureAllTimersComplete();
129 :
130 : // 현재 테스트 ID에 해당하는 인스턴스가 있는 경우 먼저 dispose 호출
131 20 : if (_currentTestId != null && _testInstances.containsKey(_currentTestId!)) {
132 : try {
133 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
134 30 : debugPrint('EventBus: 현재 테스트 ID($_currentTestId) 인스턴스 dispose 시도');
135 : }
136 30 : await _testInstances[_currentTestId!]!.dispose();
137 : } catch (e) {
138 : if (kDebugMode) {
139 0 : debugPrint('EventBus: 현재 테스트 인스턴스 dispose 중 오류 - $e');
140 : }
141 : }
142 : }
143 :
144 : _currentTestId = null;
145 : // 테스트 환경 초기화 해제 로깅
146 : _isTestEnvironmentReady = false;
147 : if (kDebugMode) {
148 22 : debugPrint('EventBus: 테스트 환경 초기화 해제');
149 : }
150 : _activeSubscriptionCount = 0;
151 :
152 : // 테스트 환경 해제 후 비동기 리소스 상태 로깅
153 11 : await _logAsyncResourceState('테스트 환경 해제 후');
154 :
155 : // 메모리 누수 방지를 위해 GC 힌트 추가
156 11 : if (TestUtils.isInTest) {
157 2 : developer.log('EventBus 메모리 정리 요청', name: 'EventBus');
158 : }
159 :
160 : // 추가 마이크로태스크 실행으로 비동기 작업 완료 보장
161 22 : await Future.microtask(() {});
162 22 : await Future.microtask(() {});
163 : }
164 :
165 : // 테스트 ID 초기화 (후방 호환성을 위해 유지)
166 10 : static Future<void> clearCurrentTestId() async {
167 : // tearDownTestEnvironment로 대체되었지만 후방 호환성을 위해 유지
168 10 : await tearDownTestEnvironment();
169 : }
170 :
171 : // 모든 테스트 인스턴스를 정리하는 정적 메서드
172 1 : static Future<void> resetAllTestInstances() async {
173 2 : if (_testInstances.isEmpty) {
174 : return;
175 : }
176 :
177 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
178 5 : debugPrint('EventBus: 모든 테스트 인스턴스 초기화 시작 (인스턴스 수: ${_testInstances.length})');
179 : }
180 :
181 : // 초기화 전 비동기 리소스 상태 로깅
182 1 : await _logAsyncResourceState('모든 테스트 인스턴스 초기화 전');
183 :
184 : // 기존 인스턴스 모두 정리
185 3 : for (var entry in _testInstances.entries) {
186 : try {
187 : if (kDebugMode) {
188 4 : debugPrint('EventBus: 인스턴스 정리 중 - ${entry.key}');
189 : }
190 2 : entry.value.dispose(); // 완전히 dispose 호출
191 :
192 : // 각 인스턴스 dispose 후 마이크로태스크 실행으로 비동기 작업 완료 보장
193 2 : await Future.microtask(() {});
194 : } catch (e) {
195 : // dispose 중 오류 무시
196 : if (kDebugMode) {
197 0 : debugPrint('EventBus: 인스턴스 정리 중 오류 - $e');
198 : }
199 : }
200 : }
201 :
202 : // 인스턴스 정리 후 중간 상태 로깅
203 1 : await _logAsyncResourceState('테스트 인스턴스 dispose 후, 초기화 전');
204 :
205 : // 모든 테스트 인스턴스 제거
206 2 : _testInstances.clear();
207 :
208 : // 기본 인스턴스도 초기화
209 : try {
210 2 : _instance.reset();
211 2 : await Future.microtask(() {}); // 기본 인스턴스 초기화 후 마이크로태스크 실행
212 : } catch (e) {
213 : // 기본 인스턴스 초기화 중 오류 무시
214 : if (kDebugMode) {
215 0 : debugPrint('EventBus: 기본 인스턴스 초기화 중 오류 - $e');
216 : }
217 : }
218 :
219 : // 현재 테스트 ID도 초기화
220 : _currentTestId = null;
221 : // 테스트 환경 초기화 해제 로깅
222 : if (_isTestEnvironmentReady) {
223 : _isTestEnvironmentReady = false;
224 : if (kDebugMode) {
225 2 : debugPrint('EventBus: 모든 테스트 환경 초기화 해제');
226 : }
227 : }
228 : _activeSubscriptionCount = 0;
229 :
230 : // 비동기 작업이 완료될 시간을 제공하되, 타이머 생성 없이 동기화
231 : // 타이머 생성 없이 마이크로태스크 큐 완료 보장
232 2 : await Future.microtask(() {});
233 2 : await Future.microtask(() {});
234 :
235 : // 테스트 종료 전 남아있는 타이머가 완료될 시간 제공
236 : // 테스트 환경에서 !timersPending assertion 실패 방지
237 1 : await _ensureAllTimersComplete();
238 :
239 : // 초기화 완료 후 비동기 리소스 상태 로깅
240 1 : await _logAsyncResourceState('모든 테스트 인스턴스 초기화 후');
241 :
242 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
243 2 : debugPrint('EventBus: 모든 테스트 인스턴스 초기화됨');
244 1 : developer.log('EventBus 메모리 상태 초기화 완료', name: 'EventBus');
245 : }
246 : }
247 :
248 : // 비동기 리소스 상태 진단 및 로깅
249 11 : static Future<Map<String, dynamic>> _diagnoseAsyncResources() async {
250 11 : final diagnosticInfo = <String, dynamic>{
251 22 : 'timestamp': DateTime.now().toIso8601String(),
252 : 'activeSubscriptions': _activeSubscriptionCount,
253 22 : 'testInstances': _testInstances.length,
254 : 'currentTestId': _currentTestId,
255 : 'isTestEnvironmentReady': _isTestEnvironmentReady,
256 : 'testRetryCount': _testRetryCount,
257 : };
258 :
259 : // 인스턴스별 상태 수집
260 11 : final instanceStates = <String, Map<String, dynamic>>{};
261 33 : for (final entry in _testInstances.entries) {
262 33 : instanceStates[entry.key] = {
263 22 : 'isDisposed': entry.value._isDisposed,
264 33 : 'isStreamControllerClosed': entry.value._streamController.isClosed,
265 : };
266 : }
267 11 : diagnosticInfo['instanceStates'] = instanceStates;
268 :
269 : // 기본 인스턴스 상태
270 22 : diagnosticInfo['defaultInstance'] = {
271 22 : 'isDisposed': _instance._isDisposed,
272 33 : 'isStreamControllerClosed': _instance._streamController.isClosed,
273 : };
274 :
275 : return diagnosticInfo;
276 : }
277 :
278 : // 비동기 리소스 상태 로깅
279 11 : static Future<void> _logAsyncResourceState(String context) async {
280 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
281 11 : final diagnosticInfo = await _diagnoseAsyncResources();
282 33 : debugPrint('===== EventBus 비동기 리소스 상태 ($context) =====');
283 44 : debugPrint('시간: ${diagnosticInfo['timestamp']}');
284 44 : debugPrint('활성 구독 수: ${diagnosticInfo['activeSubscriptions']}');
285 44 : debugPrint('테스트 인스턴스 수: ${diagnosticInfo['testInstances']}');
286 44 : debugPrint('현재 테스트 ID: ${diagnosticInfo['currentTestId']}');
287 44 : debugPrint('테스트 환경 준비 상태: ${diagnosticInfo['isTestEnvironmentReady']}');
288 44 : debugPrint('테스트 재시도 횟수: ${diagnosticInfo['testRetryCount']}');
289 :
290 : // 인스턴스별 상태 출력
291 11 : final instanceStates = diagnosticInfo['instanceStates'] as Map<String, Map<String, dynamic>>;
292 11 : if (instanceStates.isNotEmpty) {
293 22 : debugPrint('--- 테스트 인스턴스 상태 ---');
294 22 : instanceStates.forEach((key, value) {
295 55 : debugPrint(' $key: disposed=${value['isDisposed']}, streamClosed=${value['isStreamControllerClosed']}');
296 : });
297 : }
298 :
299 : // 기본 인스턴스 상태
300 11 : final defaultInstance = diagnosticInfo['defaultInstance'] as Map<String, dynamic>;
301 22 : debugPrint('--- 기본 인스턴스 상태 ---');
302 55 : debugPrint(' disposed=${defaultInstance['isDisposed']}, streamClosed=${defaultInstance['isStreamControllerClosed']}');
303 :
304 22 : debugPrint('===========================================');
305 : }
306 : }
307 :
308 11 : static Future<void> _ensureAllTimersComplete() async {
309 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
310 22 : debugPrint('EventBus: 타이머 완료 대기 시작');
311 : }
312 :
313 : // 비동기 리소스 상태 로깅 (시작)
314 11 : await _logAsyncResourceState('타이머 완료 대기 시작');
315 :
316 : // 타이머를 새로 만들지 않기 위해 microtask만 사용
317 22 : await Future.microtask(() {});
318 22 : await Future.microtask(() {});
319 22 : await Future.microtask(() {});
320 :
321 : // 중간 상태 로깅
322 11 : await _logAsyncResourceState('타이머 완료 중간 상태');
323 :
324 : // 테스트 설정에서 정의된 타이머 완료 대기 시간을 적용
325 : // 추가 대기 시간이 설정되어 있어도 실제 Timer 생성은 하지 않음(테스트 타이머 경합 방지)
326 : // 필요한 경우 호출자에서 fakeAsync 등을 사용해 제어하도록 위임
327 :
328 : // 비동기 리소스 상태 로깅 (완료)
329 11 : await _logAsyncResourceState('타이머 완료 대기 완료');
330 :
331 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
332 22 : debugPrint('EventBus: 타이머 완료 대기 완료');
333 : }
334 : }
335 :
336 11 : factory EventBus() {
337 : // 테스트 환경에서는 테스트 ID 자동 생성 및 설정
338 11 : if (TestUtils.isInTest) {
339 : // 테스트 ID가 설정되지 않았다면 자동으로 생성하지 말고, 명시적 설정을 강제한다
340 : if (_currentTestId == null) {
341 0 : throw StateError('EventBus: 테스트 환경에서 currentTestId가 설정되지 않았습니다. setUp에서 EventBus.setCurrentTestId(...)를 호출하세요.');
342 : }
343 :
344 : // 테스트 ID가 있지만 인스턴스가 없는 경우 생성
345 16 : if (!_testInstances.containsKey(_currentTestId!)) {
346 : if (kDebugMode) {
347 0 : debugPrint('EventBus: 테스트 ID ${_currentTestId}에 대한 새 인스턴스 생성');
348 : }
349 0 : _testInstances[_currentTestId!] = EventBus._internal();
350 0 : _testInstances[_currentTestId!]!.reset();
351 : }
352 :
353 16 : return _testInstances[_currentTestId]!;
354 : }
355 :
356 : // 테스트 환경이 아닌 경우 기본 싱글톤 인스턴스 반환
357 3 : return _instance;
358 : }
359 :
360 15 : EventBus._internal();
361 :
362 : // 이벤트 버스 내부 상태
363 : StreamController _streamController = StreamController.broadcast();
364 : bool _isDisposed = false;
365 :
366 12 : Stream get stream => _streamController.stream;
367 :
368 4 : void fire(dynamic event) {
369 12 : if (!_isDisposed && !_streamController.isClosed) {
370 8 : _streamController.add(event);
371 : // 디버그 모드 또는 상세 로깅 설정 시 이벤트 발생 로그 추가
372 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
373 12 : debugPrint('EventBus: 이벤트 발생 - $event');
374 : }
375 0 : } else if (kDebugMode || TestConfig.instance.isVerboseLogging) {
376 0 : debugPrint('EventBus: 이벤트 발생 실패 (이미 dispose됨) - $event');
377 : }
378 : }
379 :
380 : // 특정 타입의 이벤트를 구독하는 메서드
381 4 : Stream<T> on<T>() {
382 4 : if (_isDisposed) {
383 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
384 0 : debugPrint('EventBus: dispose된 인스턴스에서 on<$T> 호출됨');
385 : }
386 : // 빈 스트림 반환
387 0 : return Stream<T>.empty();
388 : }
389 :
390 : // 타입 필터링 및 변환
391 20 : final filteredStream = stream.where((event) => event is T).cast<T>();
392 :
393 : // 구독 생성 시 카운터 증가 - 한 번만 증가
394 4 : _activeSubscriptionCount++;
395 :
396 4 : if (TestUtils.isInTest) {
397 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
398 12 : debugPrint('EventBus: 새 구독 추가됨 <$T> (현재 구독 수: $_activeSubscriptionCount)');
399 : }
400 :
401 : // 구독 취소 시 카운터 감소를 위한 처리
402 : // StreamController를 사용하여 구독 취소 시 카운터 감소 보장
403 4 : final controller = StreamController<T>();
404 :
405 : // 원본 스트림에서 데이터 수신 및 전달
406 4 : final subscription = filteredStream.listen(
407 8 : (data) => controller.add(data),
408 0 : onError: (error) => controller.addError(error),
409 2 : onDone: () => controller.close(),
410 : );
411 :
412 : // 컨트롤러 닫힐 때 구독 취소 및 카운터 감소
413 8 : controller.onCancel = () {
414 4 : subscription.cancel();
415 8 : _activeSubscriptionCount = (_activeSubscriptionCount > 0) ? _activeSubscriptionCount - 1 : 0;
416 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
417 12 : debugPrint('EventBus: 구독 취소됨 <$T> (남은 구독 수: $_activeSubscriptionCount)');
418 : }
419 : };
420 :
421 4 : return controller.stream;
422 : }
423 :
424 : return filteredStream;
425 : }
426 :
427 15 : Future<void> _logInstanceState() async {
428 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
429 30 : debugPrint('EventBus 인스턴스 상태:');
430 60 : debugPrint(' isDisposed: $_isDisposed');
431 75 : debugPrint(' streamController.isClosed: ${_streamController.isClosed}');
432 15 : if (TestUtils.isInTest) {
433 21 : debugPrint(' 현재 테스트 ID: $_currentTestId');
434 21 : debugPrint(' 활성 구독 수: $_activeSubscriptionCount');
435 : }
436 : }
437 : }
438 :
439 15 : Future<void> dispose() async {
440 15 : if (!_isDisposed) {
441 : // dispose 전 상태 로깅
442 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
443 30 : debugPrint('EventBus: dispose 시작');
444 15 : await _logInstanceState();
445 : }
446 :
447 15 : _isDisposed = true;
448 30 : if (!_streamController.isClosed) {
449 : try {
450 30 : await _streamController.close();
451 :
452 : // 마이크로태스크 실행으로 비동기 작업 완료 보장
453 30 : await Future.microtask(() {});
454 :
455 15 : if ((kDebugMode || TestConfig.instance.isVerboseLogging) && TestUtils.isInTest) {
456 21 : debugPrint('EventBus: 인스턴스 dispose 완료 (테스트 ID: $_currentTestId)');
457 : // dispose 후 상태 로깅
458 7 : await _logInstanceState();
459 : }
460 : } catch (e) {
461 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
462 0 : debugPrint('EventBus: dispose 중 오류 - $e');
463 : }
464 : }
465 : }
466 0 : } else if (kDebugMode || TestConfig.instance.isVerboseLogging) {
467 0 : debugPrint('EventBus: 이미 dispose된 인스턴스에 dispose 호출됨');
468 : }
469 : }
470 :
471 : /// 테스트 환경에서 EventBus를 초기화하는 메서드
472 : /// 기존 StreamController를 닫고 새로운 것으로 교체
473 9 : Future<void> reset() async {
474 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
475 18 : debugPrint('EventBus: reset 시작');
476 9 : await _logInstanceState();
477 : }
478 :
479 9 : if (!_isDisposed) {
480 : try {
481 8 : await dispose();
482 : } catch (e) {
483 : if (kDebugMode) {
484 0 : debugPrint('EventBus: reset 중 dispose 호출 오류 - $e');
485 : }
486 : }
487 : }
488 :
489 : // 새로운 StreamController 생성
490 18 : _streamController = StreamController.broadcast();
491 9 : _isDisposed = false;
492 :
493 : // 활성 구독 수 초기화 (인스턴스 레벨)
494 9 : if (TestUtils.isInTest) {
495 : _activeSubscriptionCount = 0;
496 : }
497 :
498 : // 마이크로태스크 실행으로 비동기 작업 완료 보장
499 18 : await Future.microtask(() {});
500 :
501 0 : if (kDebugMode || TestConfig.instance.isVerboseLogging) {
502 27 : debugPrint('EventBus: 인스턴스 reset 완료 (테스트 ID: $_currentTestId)');
503 9 : await _logInstanceState();
504 : }
505 : }
506 : }
|