using System.IO; using System.Reflection; using System.Text.RegularExpressions; using System.Xml.Linq; using Paca.App; using Paca.App.Browser.Features; using Paca.Browser; using Paca.Browser.Extensions; using Paca.Core; using Paca.Core.Config; using Paca.Core.Detection; using Paca.Core.Net; using Paca.Core.Platform; using Paca.Mobile; using Paca.Mobile.Core; using Paca.Tests.E2E; using Paca.Tests.Infrastructure; using Xunit; namespace Paca.Tests.Infrastructure; /// /// 검사체계를 검사하는 상위 메타 검증 게이트 (Meta-Verification & Anti-Cheating Harness). /// Robert C. Martin TDD 3대 원칙 및 10대 디자인 감사 게이트, 안티치팅 규칙(Skip/빈 catch/Async void 금지), /// 게이트 검증 함수들의 변이(Mutation) 감지력, XAML 파서 무결점, 계약 어설션의 비동어반복성을 기계적으로 전수 검증합니다. /// public class TestVerificationSystemGateTests { private static readonly string TestDir = Directory.Exists(Path.Combine(SourceTree.Root, "tests", "Paca.Tests")) ? Path.Combine(SourceTree.Root, "tests", "Paca.Tests") : Path.Combine(SourceTree.Root, "Paca.Tests"); private static readonly string SrcDir = Directory.Exists(Path.Combine(SourceTree.Root, "src")) ? Path.Combine(SourceTree.Root, "src") : SourceTree.Root; // ========================================================================= // 1. 안티치팅(Anti-Cheating) 헌법 메타 게이트 // ========================================================================= [Fact] public void Meta_AntiCheating_No_Tests_Have_Skip_Attribute() { var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories); var skipRegex = new Regex(@"\[(?:Fact|Theory)\s*\(\s*Skip\s*=", RegexOptions.Compiled); var violations = new List(); foreach (var file in testFiles) { var text = File.ReadAllText(file); var matches = skipRegex.Matches(text); if (matches.Count > 0) { violations.Add($"{Path.GetFileName(file)}: {matches.Count}건의 Skip 발견"); } } Assert.True(violations.Count == 0, $"[안티치팅 위반] 테스트 회피(Skip) 속성이 발견되었습니다:\n" + string.Join("\n", violations)); } [Fact] public void Meta_AntiCheating_No_Async_Void_Test_Methods() { var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories); var asyncVoidRegex = new Regex(@"\[(?:Fact|Theory)\]\s*(?:public\s+)?async\s+void\s+\w+", RegexOptions.Compiled); var violations = new List(); foreach (var file in testFiles) { var text = File.ReadAllText(file); var matches = asyncVoidRegex.Matches(text); foreach (Match m in matches) { violations.Add($"{Path.GetFileName(file)}: {m.Value}"); } } Assert.True(violations.Count == 0, $"[안티치팅 위반] xUnit에서 예외를 삼킬 수 있는 'async void' 테스트가 감지되었습니다:\n" + string.Join("\n", violations)); } [Fact] public void Meta_AntiCheating_No_Empty_Catch_In_Test_Logic() { var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories); var violations = new List(); foreach (var file in testFiles) { var lines = File.ReadAllLines(file); for (int i = 0; i < lines.Length; i++) { var line = lines[i]; if (Regex.IsMatch(line, @"catch\s*(?:\([^\)]*\))?\s*\{\s*\}")) { // 정리/해제(finally, Dispose, Delete, Stop)가 아닌 테스트 본문 로직 내의 빈 catch는 치팅으로 간주 var context = string.Join("\n", lines.Skip(Math.Max(0, i - 12)).Take(18)); bool isLegitimateTeardown = context.Contains("finally") || context.Contains("Dispose()") || context.Contains("Directory.Delete") || context.Contains("File.Delete") || context.Contains("Close()") || context.Contains("Stop()") || context.Contains("Mock") || context.Contains("WpfTestApp"); if (!isLegitimateTeardown) { violations.Add($"{Path.GetFileName(file)} (line {i + 1}): {line.Trim()}"); } } } } Assert.True(violations.Count == 0, $"[안티치팅 위반] 테스트 검증/실행 로직에서 예외를 은폐하는 빈 catch 블록이 발견되었습니다:\n" + string.Join("\n", violations)); } // ========================================================================= // 2. 검사체계 헬퍼의 변이 감지력(Mutation Killing) 검증 게이트 // ========================================================================= [Fact] public void Meta_GateHelper_ContrastRatio_Mathematical_Invariants() { // 1. 완전 흑백 명도 대비는 정확히 21.0:1 이어야 함 var maxRatio = SourceTree.ContrastRatio("#000000", "#FFFFFF"); Assert.InRange(maxRatio, 20.99, 21.01); // 2. 동일 색상 명도 대비는 정확히 1.0:1 이어야 함 var minRatio = SourceTree.ContrastRatio("#1E1E1E", "#1E1E1E"); Assert.InRange(minRatio, 0.99, 1.01); // 3. 교환 법칙 (Commutativity) var ratioAB = SourceTree.ContrastRatio("#123456", "#ABCDEF"); var ratioBA = SourceTree.ContrastRatio("#ABCDEF", "#123456"); Assert.Equal(ratioAB, ratioBA); // 4. WCAG AA 기준 4.5:1 경계값 판별 정확성 var textOnDark = SourceTree.ContrastRatio("#CCCCCC", "#121212"); Assert.True(textOnDark >= 4.5, "밝은 회색(#CCCCCC) on 어두운 배경(#121212)은 4.5:1 이상이어야 함"); var badTextOnDark = SourceTree.ContrastRatio("#333333", "#121212"); Assert.False(badTextOnDark >= 4.5, "어두운 회색(#333333) on 어두운 배경(#121212)은 4.5:1 미만이어야 함"); } [Fact] public void Meta_GateHelper_UiContractPipeline_Negative_Mutants_Must_Fail() { // 1. 존재하지 않는 x:Name 또는 Name 요소 검색 시 반드시 false 여야 함 var fakeName = $"NonExistentElement_{Guid.NewGuid():N}"; Assert.False(UiContractPipeline.HasElementNamed(UiContractPipeline.MainWindowDoc, fakeName)); // 2. 존재하지 않는 x:Key 검색 시 반드시 false 여야 함 var fakeKey = $"NonExistentKey_{Guid.NewGuid():N}"; Assert.False(UiContractPipeline.HasResourceKey(UiContractPipeline.MainWindowDoc, fakeKey)); // 3. 존재하지 않는 속성/메서드 리플렉션 시 반드시 false 여야 함 Assert.False(UiContractPipeline.HasProperty(typeof(MainWindow), $"FakeProperty_{Guid.NewGuid():N}")); Assert.False(UiContractPipeline.HasMethod(typeof(MainWindow), $"FakeMethod_{Guid.NewGuid():N}")); } [Fact] public void Meta_GateHelper_AssertContract_Actually_Throws_On_False() { // AssertContract가 false를 받았을 때 예외를 발생시키지 않는 버그(가짜 통과) 방지 Assert.Throws(() => { UiContractPipeline.AssertContract(false, "META-FAIL", "이 어설션은 의도된 실패여야 합니다."); }); } // ========================================================================= // 3. XAML 파싱 및 UI 계약 소스 무결성 메타 게이트 // ========================================================================= [Fact] public void Meta_All_App_Xaml_Files_Are_Syntactically_Valid_Xml() { var xamlFiles = Directory.GetFiles(Path.Combine(SrcDir, "Paca.App"), "*.xaml", SearchOption.AllDirectories); Assert.NotEmpty(xamlFiles); foreach (var file in xamlFiles) { var content = File.ReadAllText(file); var ex = Record.Exception(() => XDocument.Parse(content)); Assert.Null(ex); } } [Fact] public void Meta_Themes_All_Color_Hex_Codes_Are_Valid_Format() { var dark = SourceTree.ThemeDarkXaml; var light = SourceTree.ThemeLightXaml; var hc = File.ReadAllText(SourceTree.AppPath("Themes/HighContrast.xaml")); var colorRegex = new Regex(@"Color=""(#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{8})""", RegexOptions.Compiled); foreach (var (themeName, xaml) in new[] { ("Dark", dark), ("Light", light), ("HighContrast", hc) }) { var matches = colorRegex.Matches(xaml); Assert.True(matches.Count > 10, $"{themeName} 테마에 적어도 10개 이상의 유효한 색상 코드가 존재해야 합니다."); foreach (Match match in matches) { var hex = match.Groups[1].Value; Assert.Matches(@"^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$", hex); } } } // ========================================================================= // 4. Mobile 코어 및 스니퍼 변이 검증 게이트 // ========================================================================= [Fact] public void Meta_Mobile_VideoSnifferParser_Quality_Negotiation_Mutant_Killed() { var streams = new List { new() { StreamUrl = "https://paca.media/sd.mp4", Resolution = "480p", VideoType = VideoStreamType.Mp4 }, new() { StreamUrl = "https://paca.media/fhd.mp4", Resolution = "1080p", VideoType = VideoStreamType.Mp4 }, new() { StreamUrl = "https://paca.media/4k.mp4", Resolution = "4K", VideoType = VideoStreamType.Mp4 } }; // 1. 최고 화질 요청 ("Max") var best = VideoSnifferParser.NegotiateBestQuality(streams, "Max"); Assert.NotNull(best); Assert.Equal("https://paca.media/4k.mp4", best.StreamUrl); // 2. 특정 화질 매칭 ("1080p") var match1080 = VideoSnifferParser.NegotiateBestQuality(streams, "1080p"); Assert.NotNull(match1080); Assert.Equal("https://paca.media/fhd.mp4", match1080.StreamUrl); // Mutant A: 빈 목록 -> null 반환 Assert.Null(VideoSnifferParser.NegotiateBestQuality(new List(), "Max")); // Mutant B: null 목록 -> null 반환 Assert.Null(VideoSnifferParser.NegotiateBestQuality(null!, "Max")); // Mutant C: 720p 요청 시 720p 이하 최고인 480p 선택 var fallback = VideoSnifferParser.NegotiateBestQuality(streams, "720p"); Assert.NotNull(fallback); Assert.Equal("480p", fallback.Resolution); } // ========================================================================= // 5. 시각 디자인 및 아이콘 글리프 감사 게이트 변이 검증 (Design Audit Meta-Gates) // ========================================================================= [Fact] public void Meta_IconGlyphGate_Mixed_PUA_And_Text_Mutant_Killed() { // PUA 글리프와 일반 텍스트가 결합되어 tofu/notdef 위험이 있는 돌연변이 XAML var badXaml = """