using System; using System.Collections.Generic; using System.Linq; using AGVNavigationCore.Models; using AGVNavigationCore.PathFinding.Planning; namespace AGVPathTester { /// /// AGV 경로 탐색 및 방향전환 로직 테스트 클래스 /// public class PathTester { private readonly string _mapFilePath; private List _mapNodes; private AGVPathfinder _pathfinder; private DirectionChangePlanner _directionChangePlanner; public PathTester(string mapFilePath) { _mapFilePath = mapFilePath; _mapNodes = new List(); } /// /// PathTester 초기화 /// public bool Initialize() { try { // 맵 파일 로딩 var mapLoadResult = MapLoader.LoadMapFromFile(_mapFilePath); if (!mapLoadResult.Success) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"❌ 맵 로딩 실패: {mapLoadResult.ErrorMessage}"); Console.ResetColor(); return false; } _mapNodes = mapLoadResult.Nodes; // PathFinder 초기화 _pathfinder = new AGVPathfinder(_mapNodes); // DirectionChangePlanner 초기화 _directionChangePlanner = new DirectionChangePlanner(_mapNodes); return true; } catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"❌ 초기화 중 오류: {ex.Message}"); Console.ResetColor(); return false; } } /// /// 로드된 노드 수 반환 /// public int GetNodeCount() { return _mapNodes?.Count ?? 0; } /// /// 기본 경로 탐색 테스트 실행 /// public void RunBasicPathTests() { Console.WriteLine("🔍 기본 경로 탐색 테스트 시작..."); var testCases = TestCases.GetBasicPathTestCases(); int passCount = 0; int totalCount = testCases.Count; foreach (var testCase in testCases) { Console.WriteLine($"\n--- 테스트: {testCase.StartNodeId} → {testCase.TargetNodeId} ---"); var result = _pathfinder.FindPath( GetNodeById(testCase.StartNodeId), GetNodeById(testCase.TargetNodeId), testCase.CurrentDirection ); bool passed = EvaluateBasicPathResult(result, testCase); if (passed) passCount++; DisplayPathResult(result, testCase.Description); } // 결과 요약 Console.ForegroundColor = ConsoleColor.White; Console.WriteLine($"\n📊 기본 경로 테스트 결과: {passCount}/{totalCount} 통과"); if (passCount == totalCount) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("🎉 모든 기본 경로 테스트 통과!"); } else { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"⚠️ {totalCount - passCount}개 테스트 실패"); } Console.ResetColor(); } /// /// 방향 전환 경로 테스트 실행 /// public void RunDirectionChangeTests() { Console.WriteLine("🔄 방향 전환 경로 테스트 시작..."); var testCases = TestCases.GetDirectionChangeTestCases(); int passCount = 0; int totalCount = testCases.Count; foreach (var testCase in testCases) { Console.WriteLine($"\n--- 방향전환 테스트: {testCase.StartNodeId} → {testCase.TargetNodeId} ---"); Console.WriteLine($"현재방향: {testCase.CurrentDirection}, 요구방향: {testCase.RequiredDirection}"); var plan = _directionChangePlanner.PlanDirectionChange( testCase.StartNodeId, testCase.TargetNodeId, testCase.CurrentDirection, testCase.RequiredDirection ); bool passed = EvaluateDirectionChangeResult(plan, testCase); if (passed) passCount++; DisplayDirectionChangePlan(plan, testCase.Description); } // 결과 요약 Console.ForegroundColor = ConsoleColor.White; Console.WriteLine($"\n📊 방향전환 테스트 결과: {passCount}/{totalCount} 통과"); if (passCount == totalCount) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("🎉 모든 방향전환 테스트 통과!"); } else { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"⚠️ {totalCount - passCount}개 테스트 실패"); } Console.ResetColor(); } /// /// 단일 테스트 실행 /// public void RunSingleTest(string startNodeId, string targetNodeId, AgvDirection currentDirection) { Console.WriteLine($"\n🎯 단일 테스트: {startNodeId} → {targetNodeId} (방향: {currentDirection})"); var startNode = GetNodeById(startNodeId); var targetNode = GetNodeById(targetNodeId); if (startNode == null) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"❌ 시작 노드를 찾을 수 없습니다: {startNodeId}"); Console.ResetColor(); return; } if (targetNode == null) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"❌ 목표 노드를 찾을 수 없습니다: {targetNodeId}"); Console.ResetColor(); return; } // 1. 기본 경로 탐색 Console.WriteLine("\n📍 기본 경로 탐색:"); var basicResult = _pathfinder.FindPath(startNode, targetNode, currentDirection); DisplayPathResult(basicResult, "사용자 정의 테스트"); // 2. 방향 전환이 필요한지 확인 var requiredDirection = GetRequiredDockingDirection(targetNode); if (requiredDirection.HasValue && requiredDirection.Value != currentDirection) { Console.WriteLine($"\n🔄 방향 전환 필요: {currentDirection} → {requiredDirection.Value}"); var plan = _directionChangePlanner.PlanDirectionChange( startNodeId, targetNodeId, currentDirection, requiredDirection.Value); DisplayDirectionChangePlan(plan, "방향전환 경로"); } else { Console.WriteLine("\n✅ 방향 전환 불필요"); } } /// /// 배치 테스트 실행 /// public void RunBatchTests() { Console.WriteLine("📦 배치 테스트 실행 중...\n"); RunBasicPathTests(); Console.WriteLine("\n" + new string('=', 50) + "\n"); RunDirectionChangeTests(); } /// /// 맵 정보 표시 /// public void ShowMapInfo() { Console.WriteLine($"📊 맵 파일: {_mapFilePath}"); Console.WriteLine($"🔢 총 노드 수: {_mapNodes.Count}"); // 노드 타입별 통계 var nodeStats = _mapNodes.GroupBy(n => n.Type) .ToDictionary(g => g.Key, g => g.Count()); Console.WriteLine("\n📈 노드 타입별 통계:"); foreach (var stat in nodeStats) { Console.WriteLine($" {stat.Key}: {stat.Value}개"); } // 도킹 방향별 통계 var dockingStats = _mapNodes.GroupBy(n => n.DockDirection) .ToDictionary(g => g.Key, g => g.Count()); Console.WriteLine("\n🚢 도킹 방향별 통계:"); foreach (var stat in dockingStats) { Console.WriteLine($" {stat.Key}: {stat.Value}개"); } // 연결 정보 var totalConnections = _mapNodes.Sum(n => n.ConnectedNodes.Count); Console.WriteLine($"\n🔗 총 연결 수: {totalConnections}"); // 갈림길 정보 분석 ShowJunctionInfo(); // 샘플 노드 정보 Console.WriteLine("\n📋 샘플 노드 (처음 5개):"); foreach (var node in _mapNodes.Take(5)) { var rfidInfo = string.IsNullOrEmpty(node.RfidId) ? "RFID 없음" : node.RfidId; Console.WriteLine($" {node.NodeId}: {node.Type}, {node.DockDirection}, {rfidInfo}, 연결:{node.ConnectedNodes.Count}개"); } } /// /// 갈림길 정보 분석 및 표시 /// public void ShowJunctionInfo() { Console.WriteLine("\n🛤️ 갈림길 분석:"); var junctions = _mapNodes.Where(n => n.ConnectedNodes.Count > 2).ToList(); Console.WriteLine($"갈림길 노드 수: {junctions.Count}개"); foreach (var node in junctions) { Console.WriteLine($" {node.NodeId}: {node.ConnectedNodes.Count}개 연결 - {string.Join(", ", node.ConnectedNodes)}"); // DirectionChangePlanner의 JunctionAnalyzer에서 실제로 갈림길로 인식되는지 확인 var junctionInfo = _directionChangePlanner.GetType() .GetField("_junctionAnalyzer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)? .GetValue(_directionChangePlanner); if (junctionInfo != null) { var getJunctionInfoMethod = junctionInfo.GetType().GetMethod("GetJunctionInfo"); var info = getJunctionInfoMethod?.Invoke(junctionInfo, new object[] { node.NodeId }); if (info != null) { Console.WriteLine($" -> JunctionAnalyzer 인식: {info}"); } else { Console.WriteLine($" -> JunctionAnalyzer 인식: null (인식 실패)"); } } } if (junctions.Count == 0) { Console.WriteLine(" ⚠️ 갈림길 노드가 없습니다! 이것이 방향전환 실패의 원인일 수 있습니다."); } } #region Private Helper Methods private MapNode GetNodeById(string nodeId) { return _mapNodes.FirstOrDefault(n => n.NodeId == nodeId); } private AgvDirection? GetRequiredDockingDirection(MapNode targetNode) { switch (targetNode.DockDirection) { case DockingDirection.Forward: return AgvDirection.Forward; case DockingDirection.Backward: return AgvDirection.Backward; case DockingDirection.DontCare: default: return null; } } private bool EvaluateBasicPathResult(AGVNavigationCore.PathFinding.Core.AGVPathResult result, TestCases.BasicPathTestCase testCase) { // 기본 평가: 성공 여부와 경로 존재 여부 bool basicSuccess = result.Success && result.Path != null && result.Path.Count > 0; if (!basicSuccess) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"❌ 기본 조건 실패: Success={result.Success}, PathCount={result.Path?.Count ?? 0}"); Console.ResetColor(); return false; } // 되돌아가기 패턴 체크 if (HasBacktrackingPattern(result.Path)) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("❌ 되돌아가기 패턴 발견!"); Console.ResetColor(); return false; } Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("✅ 기본 경로 테스트 통과"); Console.ResetColor(); return true; } private bool EvaluateDirectionChangeResult(DirectionChangePlanner.DirectionChangePlan plan, TestCases.DirectionChangeTestCase testCase) { if (!plan.Success) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"❌ 방향전환 계획 실패: {plan.ErrorMessage}"); Console.ResetColor(); return false; } // 되돌아가기 패턴 체크 if (HasBacktrackingPattern(plan.DirectionChangePath)) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("❌ 방향전환 경로에서 되돌아가기 패턴 발견!"); Console.ResetColor(); return false; } // 기대 경로와 비교 (기대 경로가 정의된 경우) if (testCase.ExpectedPath != null && testCase.ExpectedPath.Count > 0) { bool pathMatches = ComparePathsWithTolerance(plan.DirectionChangePath, testCase.ExpectedPath); if (!pathMatches) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("⚠️ 경로가 기대 결과와 다릅니다:"); Console.WriteLine($" 기대: {string.Join(" → ", testCase.ExpectedPath)}"); Console.WriteLine($" 실제: {string.Join(" → ", plan.DirectionChangePath)}"); Console.ResetColor(); // 경로가 다르더라도 되돌아가기가 없으면 부분 성공으로 처리 return true; } else { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("✅ 경로가 기대 결과와 일치합니다!"); Console.ResetColor(); } } Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("✅ 방향전환 테스트 통과"); Console.ResetColor(); return true; } private bool HasBacktrackingPattern(List path) { if (path == null || path.Count < 3) return false; for (int i = 0; i < path.Count - 2; i++) { if (path[i] == path[i + 2] && path[i] != path[i + 1]) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($"⚠️ 되돌아가기 패턴: {path[i]} → {path[i + 1]} → {path[i + 2]}"); Console.ResetColor(); return true; } } return false; } private void DisplayPathResult(AGVNavigationCore.PathFinding.Core.AGVPathResult result, string description) { Console.WriteLine($"📝 {description}"); if (result.Success) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"✅ 성공 - 경로: {string.Join(" → ", result.Path)}"); Console.WriteLine($"📏 거리: {result.TotalDistance:F1}, 단계: {result.Path.Count}"); Console.ResetColor(); } else { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"❌ 실패 - {result.ErrorMessage}"); Console.ResetColor(); } } private void DisplayDirectionChangePlan(DirectionChangePlanner.DirectionChangePlan plan, string description) { Console.WriteLine($"📝 {description}"); if (plan.Success) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"✅ 성공 - 경로: {string.Join(" → ", plan.DirectionChangePath)}"); Console.WriteLine($"🔄 방향전환 노드: {plan.DirectionChangeNode}"); Console.WriteLine($"📋 설명: {plan.PlanDescription}"); Console.ResetColor(); } else { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"❌ 실패 - {plan.ErrorMessage}"); Console.ResetColor(); } } private bool ComparePathsWithTolerance(List actualPath, List expectedPath) { if (actualPath == null || expectedPath == null) return false; if (actualPath.Count != expectedPath.Count) return false; for (int i = 0; i < actualPath.Count; i++) { if (actualPath[i] != expectedPath[i]) return false; } return true; } #endregion } }