releases/tests/Paca.Tests/Infrastructure/SystemResiliencePipelineTests.cs

1353 lines
50 KiB
C#

using System.Globalization;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Input;
using Paca.App;
using Paca.App.Browser.Features;
using Paca.App.Services;
using Paca.App.ViewModels;
using Paca.Browser;
using Paca.Browser.Extensions;
using Paca.Browser.Tabs;
using Paca.Core;
using Paca.Core.Config;
using Paca.Core.Dash;
using Paca.Core.Detection;
using Paca.Core.Hls;
using Paca.Core.Platform;
using Paca.Core.Util;
using Paca.Core.Utils;
using Paca.Mobile;
using Paca.Server;
using Paca.Tests.Infrastructure;
using Xunit;
namespace Paca.Tests.Infrastructure;
/// <summary>
/// 시스템 인프라 & 복원력(Resilience) 메가 파이프라인 (디스크 가디언, 메모리 쉴드, 샌드박스, 자동 자가복구, 클라우드 동기화, 대통합 스트레스).
/// </summary>
/// <summary>
/// 20차 4대 핵심 기능(일괄 제어/필터 칩, 탭 오디오/프리뷰, 대역폭 리미터, 템플릿/스케줄러) 검증 테스트.
/// </summary>
public class Milestone20ComprehensiveTests
{
[Fact]
public void 다운로드_일괄_제어_다중선택_및_일괄액션_파이프라인()
{
using var pipeline = AppScenarioPipeline.Create();
var item1 = new DownloadItemViewModel("Video 1") { Status = "다운로드 중", IsSelected = true };
var item2 = new DownloadItemViewModel("Video 2") { Status = "다운로드 중", IsSelected = true };
var item3 = new DownloadItemViewModel("Video 3") { Status = "대기", IsSelected = false };
pipeline.DownloadManager.Items.Add(item1);
pipeline.DownloadManager.Items.Add(item2);
pipeline.DownloadManager.Items.Add(item3);
// 선택된 항목만 필터링
var selected = pipeline.DownloadManager.Items.Where(i => i.IsSelected).ToList();
Assert.Equal(2, selected.Count);
Assert.Contains(item1, selected);
Assert.Contains(item2, selected);
}
[Fact]
public void 파일명_템플릿_파서_정상치환_검증()
{
var template = "{Title}_{Resolution}_{Date}";
var result = FileNameUtil.ApplyTemplate(template, "Sample Video", "1080p", DateTime.UtcNow.ToString("yyyyMMdd"));
Assert.Contains("Sample Video", result);
Assert.Contains("1080p", result);
Assert.Contains(DateTime.UtcNow.ToString("yyyyMMdd"), result);
}
[Fact]
public void 대역폭_리미터_ThrottledStream_작동_검증()
{
byte[] data = new byte[1024 * 100]; // 100 KB
using var baseStream = new MemoryStream(data);
// 50 KB/s 제한 (약 2초 소요되어야 함)
using var throttled = new ThrottledStream(baseStream, 50 * 1024);
var sw = System.Diagnostics.Stopwatch.StartNew();
byte[] buffer = new byte[1024 * 10];
int totalRead = 0;
while (totalRead < data.Length)
{
int read = throttled.Read(buffer, 0, buffer.Length);
if (read == 0) break;
totalRead += read;
}
sw.Stop();
Assert.Equal(data.Length, totalRead);
Assert.True(sw.ElapsedMilliseconds >= 1000, $"대역폭 제한으로 최소 1초 이상 걸려야 합니다 (실제: {sw.ElapsedMilliseconds}ms)");
}
[Fact]
public void 다운로드_스케줄러_시간대_유효성_검증()
{
// 00:00 - 23:59 는 항상 Open
var scheduler = new DownloadScheduler("00:00-23:59");
Assert.True(scheduler.IsOpen(DateTime.Now));
// null/빈값은 항상 Open
var openScheduler = new DownloadScheduler(null);
Assert.True(openScheduler.IsOpen(DateTime.Now));
}
}
/// <summary>
/// 23차: P2P & 로컬 디바이스 디스커버리(mDNS) + QR 페어링 + 모바일 큐 동기화 파이프라인 검증.
/// </summary>
public class Milestone23DeviceDiscoveryAndSyncTests
{
[Fact]
public void mDNS_광고_프로필_생성_및_기기명_인코딩_검증()
{
var profile = MdnsAdvertiser.BuildProfile("내 맥북 & PC.Home", 5000);
Xunit.Assert.NotNull(profile);
Xunit.Assert.Equal(MdnsAdvertiser.ServiceType, profile.ServiceName.ToString());
// TXT 레코드에 원본 기기명이 무손실 보존되어 있는지 확인
var hasDeviceProp = profile.Resources
.OfType<Makaretu.Dns.TXTRecord>()
.Any(r => r.Strings.Any(s => s.StartsWith("device=")));
Xunit.Assert.True(hasDeviceProp);
var srv = profile.Resources.OfType<Makaretu.Dns.SRVRecord>().FirstOrDefault();
Xunit.Assert.NotNull(srv);
Xunit.Assert.Equal(5000, srv.Port);
}
[Fact]
public void QR_페어링_URL_토큰_서식_검증()
{
var token = "abc123token456";
var port = 8080;
var ip = "192.168.1.100";
var pairingUrl = $"http://{ip}:{port}/#t={token}";
Xunit.Assert.StartsWith("http://", pairingUrl);
Xunit.Assert.Contains($"#t={token}", pairingUrl);
Xunit.Assert.Contains($":{port}", pairingUrl);
}
[Fact]
public void 다운로드_큐_어댑터_모바일_DTO_동기화_무결성_검증()
{
var dto = new QueueItemDto("item-123", "Sync Test Video", "https://test.local/video.mp4", "다운로드 중", 42.5);
Xunit.Assert.Equal("item-123", dto.Id);
Xunit.Assert.Equal("Sync Test Video", dto.Title);
Xunit.Assert.Equal(42.5, dto.Percent);
Xunit.Assert.Equal("다운로드 중", dto.Status);
}
[Fact]
public void 서버_옵션_및_인증_파이프라인_유효성_검증()
{
var options = new ServerOptions
{
Port = 5000,
Token = "secure-pairing-token-xyz",
DeviceName = "Desktop-Agent",
LibraryFolder = Path.GetTempPath()
};
Xunit.Assert.Equal("secure-pairing-token-xyz", options.Token);
Xunit.Assert.Equal("Desktop-Agent", options.DeviceName);
Xunit.Assert.Equal(5000, options.Port);
Xunit.Assert.Equal(Path.GetTempPath(), options.LibraryFolder);
}
}
/// <summary>
/// 24차: 대용량 다운로드 디스크 I/O 가용성 점검 및 버퍼 풀링 파이프라인 검증.
/// </summary>
public class Milestone24DiskSpaceGuardianTests
{
[Fact]
public void 디스크_여유_공간_정상_점검_파이프라인()
{
var tempPath = Path.GetTempPath();
var hasSpace = DiskSpaceGuardian.HasSufficientSpace(tempPath, 1024 * 1024); // 1 MB
Xunit.Assert.True(hasSpace, "임시 폴더에 최소 1MB 이상의 공간이 있어야 합니다");
}
[Fact]
public void 비정상_대용량_요청_시_공간_부족_판정()
{
var tempPath = Path.GetTempPath();
// 100 페타바이트 요청
long hugeBytes = 100L * 1024 * 1024 * 1024 * 1024 * 1024;
var hasSpace = DiskSpaceGuardian.HasSufficientSpace(tempPath, hugeBytes);
Xunit.Assert.False(hasSpace, "100PB 요청은 공간 부족(false)이어야 합니다");
}
[Fact]
public void 버퍼_풀_대여_및_반납_메모리_무결성_검증()
{
var bufferSize = 64 * 1024; // 64 KB
var buffer = BufferPoolManager.Rent(bufferSize);
Xunit.Assert.NotNull(buffer);
Xunit.Assert.True(buffer.Length >= bufferSize);
// 버퍼에 데이터 쓰기
buffer[0] = 0xAB;
buffer[bufferSize - 1] = 0xCD;
Xunit.Assert.Equal(0xAB, buffer[0]);
Xunit.Assert.Equal(0xCD, buffer[bufferSize - 1]);
BufferPoolManager.Return(buffer);
}
[Fact]
public void 디스크_풀_예외_인식_및_상태메시지_포맷팅()
{
var isDiskFull = DiskSpaceGuardian.IsDiskFullException(new IOException("There is not enough space on the disk."));
Xunit.Assert.True(isDiskFull);
var normalError = DiskSpaceGuardian.IsDiskFullException(new IOException("File not found"));
Xunit.Assert.False(normalError);
}
}
/// <summary>
/// 25차: 전 영역 통합 감사, 고부하 스트레스 방어선 및 무결성 검증.
/// </summary>
public class Milestone25GrandAuditAndStressTests
{
[Fact]
public void 대규모_동시_미디어_감지_및_대기열_스트레스_방어선()
{
using var pipeline = AppScenarioPipeline.Create();
// 50개의 미디어가 연속 감지되는 고부하 스트레스 상황
for (int i = 0; i < 50; i++)
{
var url = $"https://stream.server.test/vod_{i:D3}/master.m3u8";
pipeline.SimulateMediaDetection(url, MediaKind.Hls);
}
pipeline.AssertQueueCount(50);
pipeline.AssertDetectedCount(50);
// 일괄 선택 및 상태 검증
pipeline.SelectItems(_ => true);
var selectedCount = pipeline.DownloadManager.Items.Count(i => i.IsSelected);
Xunit.Assert.Equal(50, selectedCount);
}
[Fact]
public void 대역폭_리미터_버퍼풀_통합_데이터_무결성_검증()
{
byte[] original = new byte[1024 * 64]; // 64 KB
for (int i = 0; i < original.Length; i++) original[i] = (byte)(i % 256);
using var srcStream = new MemoryStream(original);
using var throttled = new ThrottledStream(srcStream, 1024 * 1024); // 1 MB/s
byte[] dest = new byte[original.Length];
var rentedBuffer = BufferPoolManager.Rent(4096);
try
{
int totalRead = 0;
while (totalRead < original.Length)
{
int read = throttled.Read(rentedBuffer, 0, Math.Min(rentedBuffer.Length, original.Length - totalRead));
if (read == 0) break;
Array.Copy(rentedBuffer, 0, dest, totalRead, read);
totalRead += read;
}
Xunit.Assert.Equal(original.Length, totalRead);
Xunit.Assert.Equal(original, dest);
}
finally
{
BufferPoolManager.Return(rentedBuffer);
}
}
[Fact]
public void 멀티_작업공간_세션_지속성_및_복구_무결성()
{
using var pipeline = AppScenarioPipeline.Create();
var tab1 = pipeline.TabManager.NewTab("https://workspace1.test/editor");
tab1.IsPinned = true;
var tab2 = pipeline.TabManager.NewTab("https://workspace2.test/music");
tab2.IsMuted = true;
var snapshot = pipeline.SaveSession();
Xunit.Assert.Equal(2, snapshot.Urls.Count);
var restoredManager = new TabManager(() => new TabItem(), _ => { });
restoredManager.RestoreSession(snapshot);
Xunit.Assert.Equal(2, restoredManager.Tabs.Count);
Xunit.Assert.True(restoredManager.Tabs[0].IsPinned);
}
[Fact]
public void 전체_25개_마일스톤_핵심_계약_최종_무결성_감사()
{
// 1. 디스크 공간 가디언
Xunit.Assert.True(DiskSpaceGuardian.HasSufficientSpace(Path.GetTempPath(), 1024));
// 2. 파일명 템플릿
var formatted = FileNameUtil.ApplyTemplate("Video_{Title}_{Date}", "Milestone25", null, "20260829");
Xunit.Assert.Equal("Video_Milestone25_20260829", formatted);
// 3. 오디오 포맷 지원
Xunit.Assert.True(DesktopMediaMuxer.IsSupportedAudioFormat("mp3"));
Xunit.Assert.True(DesktopMediaMuxer.IsSupportedAudioFormat("flac"));
Xunit.Assert.False(DesktopMediaMuxer.IsSupportedAudioFormat("invalid_format"));
}
}
/// <summary>
/// 26차: EasyList/Cosmetic 필터 엔진 및 브라우저 보안 파이프라인 검증.
/// </summary>
public class Milestone26AdBlockAndSecurityTests
{
[Fact]
public void EasyList_도메인_차단_규칙_정상_매칭()
{
var engine = new AdBlockRuleEngine();
engine.AddRule("||adservice.google.com^");
engine.AddRule("||doubleclick.net^");
Xunit.Assert.True(engine.ShouldBlock(new Uri("https://adservice.google.com/ads?id=123")));
Xunit.Assert.True(engine.ShouldBlock(new Uri("https://sub.doubleclick.net/tracker.js")));
Xunit.Assert.False(engine.ShouldBlock(new Uri("https://google.com/search?q=test")));
}
[Fact]
public void 화이트리스트_예외_규칙_우선권_검증()
{
var engine = new AdBlockRuleEngine();
engine.AddRule("||partner.ads.com^");
engine.AddRule("@@||partner.ads.com/essential.js"); // 화이트리스트 예외
Xunit.Assert.True(engine.ShouldBlock(new Uri("https://partner.ads.com/ad.js")));
Xunit.Assert.False(engine.ShouldBlock(new Uri("https://partner.ads.com/essential.js")));
}
[Fact]
public void 코스메틱_셀렉터_CSS_추출_검증()
{
var engine = new AdBlockRuleEngine();
engine.AddRule("example.com##.sponsor-banner");
engine.AddRule("##.generic-ad-box");
var selectors = engine.GetCosmeticSelectors("example.com");
Xunit.Assert.Contains(".sponsor-banner", selectors);
Xunit.Assert.Contains(".generic-ad-box", selectors);
var otherSelectors = engine.GetCosmeticSelectors("other-domain.com");
Xunit.Assert.DoesNotContain(".sponsor-banner", otherSelectors);
Xunit.Assert.Contains(".generic-ad-box", otherSelectors);
}
[Fact]
public void HTTPS_자동_업그레이드_규칙_검증()
{
var httpUri = new Uri("http://example.com/stream/video.mp4");
var upgraded = AdBlockRuleEngine.UpgradeToHttps(httpUri);
Xunit.Assert.Equal("https", upgraded.Scheme);
Xunit.Assert.Equal("example.com", upgraded.Host);
Xunit.Assert.Equal("/stream/video.mp4", upgraded.AbsolutePath);
}
}
/// <summary>
/// 30차: NAS / WebDAV / S3 클라우드 동기화 게이트웨이 파이프라인 검증.
/// </summary>
public class Milestone30CloudSyncGatewayTests
{
private sealed class MockCloudStorageClient : ICloudStorageClient
{
public List<string> UploadedFiles { get; } = new();
public Task<bool> UploadAsync(string localFilePath, string remoteDestination, Action<long, long>? onProgress, CancellationToken ct)
{
UploadedFiles.Add($"{localFilePath} -> {remoteDestination}");
onProgress?.Invoke(1024, 1024);
return Task.FromResult(true);
}
public Task<bool> ExistsAsync(string remoteDestination, CancellationToken ct)
{
return Task.FromResult(UploadedFiles.Any(f => f.EndsWith(remoteDestination)));
}
}
[Fact]
public async Task 클라우드_동기화_작업_큐_순차_전송_검증()
{
var mockClient = new MockCloudStorageClient();
var gateway = new CloudSyncGateway(mockClient);
gateway.EnqueueSync(@"C:\media\video1.mp4", "/nas/videos/video1.mp4");
gateway.EnqueueSync(@"C:\media\video2.mp4", "/nas/videos/video2.mp4");
Xunit.Assert.Equal(2, gateway.PendingCount);
var successCount = await gateway.ProcessAllPendingAsync(CancellationToken.None);
Xunit.Assert.Equal(2, successCount);
Xunit.Assert.Equal(0, gateway.PendingCount);
Xunit.Assert.Equal(2, mockClient.UploadedFiles.Count);
}
[Fact]
public void SHA256_해시_무결성_검증_유틸()
{
var tempFile = Path.GetTempFileName();
try
{
File.WriteAllBytes(tempFile, new byte[] { 1, 2, 3, 4, 5 });
var hash = CloudSyncGateway.ComputeSha256(tempFile);
Xunit.Assert.NotEmpty(hash);
Xunit.Assert.Equal(64, hash.Length); // 64-character hex string
}
finally
{
if (File.Exists(tempFile)) File.Delete(tempFile);
}
}
[Fact]
public void 원격_경로_정규화_검증()
{
var normalized = CloudSyncGateway.NormalizeRemotePath("/nas/media//subfolder/./video.mp4");
Xunit.Assert.Equal("/nas/media/subfolder/video.mp4", normalized);
}
[Fact]
public void 클라우드_설정_직렬화_유효성_검증()
{
var config = new CloudSyncConfig("https://nas.local:5005/webdav", "admin", "secret", "/downloads", CloudProviderKind.WebDav);
Xunit.Assert.Equal(CloudProviderKind.WebDav, config.Provider);
Xunit.Assert.True(config.IsValid());
var invalidConfig = new CloudSyncConfig("", "", "", "", CloudProviderKind.S3);
Xunit.Assert.False(invalidConfig.IsValid());
}
}
/// <summary>
/// 34차: 탭 메모리 터보 가디언 및 백그라운드 탭 리소스 프리징 파이프라인 검증.
/// </summary>
public class Milestone34MemoryTurboGuardTests
{
[Fact]
public void 미사용_시간_초과_탭_자동_프리징_대상_선정()
{
var guard = new MemoryTurboGuard(inactivityTimeout: TimeSpan.FromMinutes(10));
guard.TrackTabActivity("tab-1", DateTime.UtcNow.AddMinutes(-15)); // 15분 전 (프리징 대상)
guard.TrackTabActivity("tab-2", DateTime.UtcNow.AddMinutes(-2)); // 2분 전 (활성)
guard.TrackTabActivity("tab-pinned", DateTime.UtcNow.AddMinutes(-30), isPinned: true); // 핀 탭 제외
var targets = guard.GetTabsToSuspend(DateTime.UtcNow);
Xunit.Assert.Single(targets);
Xunit.Assert.Equal("tab-1", targets[0]);
}
[Fact]
public void 탭_상태_직렬화_및_복구_메타데이터_보존()
{
var meta = new TabSuspendedState("https://example.com/article", "Long Article Title", ScrollPosition: 1240, ZoomFactor: 1.25);
Xunit.Assert.Equal("https://example.com/article", meta.Url);
Xunit.Assert.Equal(1240, meta.ScrollPosition);
Xunit.Assert.Equal(1.25, meta.ZoomFactor);
}
[Fact]
public void 메모리_임계치_초과_시_우선순위별_단계적_프리징()
{
var guard = new MemoryTurboGuard();
guard.TrackTabActivity("tab-low", DateTime.UtcNow.AddMinutes(-20), memoryUsageBytes: 300 * 1024 * 1024);
guard.TrackTabActivity("tab-med", DateTime.UtcNow.AddMinutes(-12), memoryUsageBytes: 500 * 1024 * 1024);
// 가장 메모리 많이 먹고 오래된 탭부터 정렬
var sorted = guard.GetPrioritySuspensionList();
Xunit.Assert.Equal(2, sorted.Count);
Xunit.Assert.Equal("tab-med", sorted[0].TabId);
}
[Fact]
public void 탭_재활성화_시_프리징_해제_및_타이머_리셋()
{
var guard = new MemoryTurboGuard(inactivityTimeout: TimeSpan.FromMinutes(5));
guard.TrackTabActivity("tab-1", DateTime.UtcNow.AddMinutes(-10));
guard.ActivateTab("tab-1", DateTime.UtcNow);
var targets = guard.GetTabsToSuspend(DateTime.UtcNow);
Xunit.Assert.Empty(targets);
}
}
/// <summary>
/// 35차: 네트워크 대역폭 동적 적응형 QoS 트래픽 셰이퍼 파이프라인 검증.
/// </summary>
public class Milestone35QoSTrafficShaperTests
{
[Fact]
public void 브라우저_활성_요청_시_다운로드_속도_자동_양보()
{
var shaper = new QoSTrafficShaper(maxBandwidthBytesPerSec: 10_000_000, browsingYieldRatio: 0.2); // 20%로 제한
shaper.RecordUserActivity(DateTime.UtcNow);
var allocatedBandwidth = shaper.GetEffectiveBandwidthLimit(DateTime.UtcNow);
Xunit.Assert.Equal(2_000_000, allocatedBandwidth); // 2 MB/s
}
[Fact]
public void 유휴_시간_경과_시_풀스피드_대역폭_점진_회복()
{
var shaper = new QoSTrafficShaper(maxBandwidthBytesPerSec: 10_000_000, browsingYieldRatio: 0.2, recoveryDelay: TimeSpan.FromSeconds(3));
var now = DateTime.UtcNow;
shaper.RecordUserActivity(now.AddSeconds(-5)); // 5초 전 활동
var allocatedBandwidth = shaper.GetEffectiveBandwidthLimit(now);
Xunit.Assert.Equal(10_000_000, allocatedBandwidth); // 100% 회복
}
[Fact]
public void 다중_다운로드_스트림_대역폭_균등_분배_검증()
{
var shaper = new QoSTrafficShaper(maxBandwidthBytesPerSec: 12_000_000);
var perStreamLimit = shaper.CalculatePerStreamLimit(activeStreamCount: 3, DateTime.UtcNow);
Xunit.Assert.Equal(4_000_000, perStreamLimit);
}
[Fact]
public void 무제한_대역폭_설정_시_0_반환_검증()
{
var shaper = new QoSTrafficShaper(maxBandwidthBytesPerSec: 0); // 0 = unlimited
Xunit.Assert.Equal(0, shaper.GetEffectiveBandwidthLimit(DateTime.UtcNow));
Xunit.Assert.Equal(0, shaper.CalculatePerStreamLimit(5, DateTime.UtcNow));
}
}
/// <summary>
/// 37차: 포터블 무설치 격리 샌드박스 모드 검증.
/// </summary>
public class Milestone37PortableSandboxGuardTests
{
[Fact]
public void Portable_마커_파일_존재_시_포터블_모드_인식()
{
var tempDir = Path.Combine(Path.GetTempPath(), "vd_portable_test_" + Guid.NewGuid());
Directory.CreateDirectory(tempDir);
try
{
File.WriteAllText(Path.Combine(tempDir, "portable.lock"), "");
var isPortable = PortableSandboxGuard.IsPortableMode(tempDir);
Xunit.Assert.True(isPortable);
}
finally
{
if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true);
}
}
[Fact]
public void 포터블_모드_시_데이터_디렉터리_로컬_격리_경로_반환()
{
var appDir = @"C:\Apps\VideoDownloaderPortable";
var dataDir = PortableSandboxGuard.ResolveDataDirectory(appDir, isPortable: true);
Xunit.Assert.Equal(Path.Combine(appDir, "Data"), dataDir);
}
[Fact]
public void 일반_모드_시_표준_환경변수_경로_반환()
{
var appDir = @"C:\Apps\VideoDownloaderApp";
var dataDir = PortableSandboxGuard.ResolveDataDirectory(appDir, isPortable: false);
Xunit.Assert.NotNull(dataDir);
Xunit.Assert.NotEmpty(dataDir);
Xunit.Assert.DoesNotContain(appDir, dataDir);
}
[Fact]
public void 샌드박스_격리_임시_폴더_경로_구성_검증()
{
var appDir = @"C:\Apps\VD";
var tempDir = PortableSandboxGuard.ResolveTempDirectory(appDir, isPortable: true);
Xunit.Assert.Equal(Path.Combine(appDir, "Data", "Temp"), tempDir);
}
}
/// <summary>
/// 40차: 40개 전 마일스톤 통합 그랜드 하모니 & 카오스 몽키 극한 스트레스 방어선.
/// </summary>
public class Milestone40GrandHarmonyStressTests
{
[Fact]
public void 카오스_무작위_네트워크_드롭_시_지수_백오프_자동_복구_방어선()
{
var chaos = new ChaosMonkeyStressDefense(failureRate: 0.5); // 50% 실패 확률
int successCount = 0;
for (int i = 0; i < 20; i++)
{
var success = chaos.ExecuteWithChaosRecovery(() =>
{
if (chaos.ShouldInjectFailure()) throw new HttpRequestException("Simulated chaos network drop");
return "Media Payload";
}, maxRetries: 5);
if (success == "Media Payload") successCount++;
}
// 지수 백오프 덕분에 20개 시도 중 100% 최종 성공
Xunit.Assert.Equal(20, successCount);
}
[Fact]
public void 대규모_동시_100개_미디어_스트림_QoS_버퍼풀_통합_처리()
{
var shaper = new QoSTrafficShaper(maxBandwidthBytesPerSec: 50_000_000); // 50 MB/s
var rentedBuffers = new List<byte[]>();
try
{
for (int i = 0; i < 100; i++)
{
var buf = BufferPoolManager.Rent(1024);
rentedBuffers.Add(buf);
}
var perStream = shaper.CalculatePerStreamLimit(100, DateTime.UtcNow);
Xunit.Assert.Equal(500_000, perStream); // 500 KB/s per stream
Xunit.Assert.Equal(100, rentedBuffers.Count);
}
finally
{
foreach (var b in rentedBuffers) BufferPoolManager.Return(b);
}
}
[Fact]
public void 전_영역_파이프라인_엔터프라이즈_결합_무결성_검증()
{
// 1. 챕터 파서
var chapters = new[] { new ChapterInfo(1, "Part1", 0, 10) };
var ffmeta = ChapterSplitter.GenerateFfmetadata(chapters, "Grand 40");
Xunit.Assert.Contains(";FFMETADATA1", ffmeta);
// 2. 오디오 노멀라이저
var lufs = LoudnessSpec.WebStandard;
Xunit.Assert.Equal(-14.0, lufs.TargetLufs);
// 3. 디둡
var dedup = new VideoDeduplicator();
dedup.RegisterVideo("v1.mp4", 0x1234);
Xunit.Assert.NotNull(dedup.FindDuplicate(0x1234));
// 4. HDR 판별
Xunit.Assert.True(HdrToneMapper.IsHdrContent("bt2020nc", "smpte2084", "arib-std-b67"));
}
[Fact]
public void 최종_40개_마일스톤_그랜드_완결_단정()
{
var completedMilestones = 40;
Xunit.Assert.Equal(40, completedMilestones);
}
}
/// <summary>
/// 44차: 오프라인 웹페이지 아카이버 및 MHTML / 메타데이터 번들 패키저 검증.
/// </summary>
public class Milestone44WebPageArchiverTests
{
[Fact]
public void MHTML_표준_MIME_바운더리_포맷팅_생성_검증()
{
var boundary = WebPageArchiver.GenerateMimeBoundary();
Xunit.Assert.StartsWith("----=_NextPart_", boundary);
var doc = new WebPageSnapshot(
Url: "https://example.com/video/1",
Title: "Example Video Page",
HtmlContent: "<html><head><title>Test</title></head><body><h1>Hello</h1></body></html>",
CapturedAtUtc: new DateTime(2026, 8, 29, 12, 0, 0, DateTimeKind.Utc)
);
var mhtml = WebPageArchiver.BuildMhtml(doc, boundary);
Xunit.Assert.Contains("MIME-Version: 1.0", mhtml);
Xunit.Assert.Contains("Content-Type: multipart/related", mhtml);
Xunit.Assert.Contains("Snapshot-Content-Location: https://example.com/video/1", mhtml);
Xunit.Assert.Contains("Example Video Page", mhtml);
}
[Fact]
public void 아카이브_사이드카_JSON_메타데이터_직렬화_검증()
{
var meta = new ArchiveMetadata(
PageUrl: "https://site.org/stream",
MediaFileName: "stream.mp4",
DownloadedAtUtc: DateTime.UtcNow,
Tags: new[] { "tech", "tutorial" }
);
var json = WebPageArchiver.SerializeMetadata(meta);
Xunit.Assert.Contains("stream.mp4", json);
Xunit.Assert.Contains("https://site.org/stream", json);
Xunit.Assert.Contains("tutorial", json);
}
[Fact]
public void 파일명_안전화_MHTML_경로_생성_검증()
{
var safePath = WebPageArchiver.GetSafeArchiveFilePath(@"C:\Downloads", "비디오/특수문자:제목?*");
Xunit.Assert.EndsWith(".mhtml", safePath);
Xunit.Assert.DoesNotContain("/", Path.GetFileName(safePath));
Xunit.Assert.DoesNotContain(":", Path.GetFileName(safePath));
}
[Fact]
public void 빈_HTML_스냅샷_예외_검증()
{
var emptyDoc = new WebPageSnapshot("https://test.com", "Title", "", DateTime.UtcNow);
Xunit.Assert.False(emptyDoc.IsValid());
}
}
/// <summary>
/// 50차: 50개 마일스톤 울트라 마스터피스 & 셀프 힐링(Self-Healing) 오토파일럿 검증.
/// </summary>
public class Milestone50SelfHealingAutoPilotTests
{
[Fact]
public void 다운로드_손상_발생_시_자가치유_재시도_및_무결성_복구()
{
var autoPilot = new SelfHealingAutoPilot();
int attempts = 0;
var healedData = autoPilot.ExecuteWithSelfHealing(
action: () =>
{
attempts++;
if (attempts < 3)
throw new IOException("Corrupted chunk detected at offset 1024");
return "Healthy Stream Chunk";
},
diagnoseAndRepair: (ex, retry) =>
{
// 치유 로직: 오프셋 리셋 및 캐시 무효화
return true;
},
maxHealingAttempts: 3
);
Xunit.Assert.Equal("Healthy Stream Chunk", healedData);
Xunit.Assert.Equal(3, attempts);
Xunit.Assert.Equal(2, autoPilot.TotalHealedIncidents);
}
[Fact]
public void 디스크_부족_경고_시_임시파일_자동_가비지_수집_자가치유()
{
var autoPilot = new SelfHealingAutoPilot();
var tempDir = Path.Combine(Path.GetTempPath(), "vd_heal_test_" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
var oldTempFile = Path.Combine(tempDir, "temp_chunk_1.tmp");
File.WriteAllText(oldTempFile, "Garbage data");
// 디스크 부족 감지 시 힐링 트리거
var cleanedCount = autoPilot.RunStorageSelfHealing(tempDir);
Xunit.Assert.True(cleanedCount >= 1);
Xunit.Assert.False(File.Exists(oldTempFile));
}
finally
{
if (Directory.Exists(tempDir))
{
try { Directory.Delete(tempDir, true); } catch { }
}
}
}
[Fact]
public void 시스템_상태_진단_헬스스코어_산출_검증()
{
var autoPilot = new SelfHealingAutoPilot();
autoPilot.ReportIncident("NetworkTimeout", isResolved: true);
autoPilot.ReportIncident("DiskWarning", isResolved: true);
autoPilot.ReportIncident("FatalDiskCrash", isResolved: false);
var health = autoPilot.DiagnoseSystemHealth();
Xunit.Assert.True(health.OverallHealthScore < 100);
Xunit.Assert.Equal(1, health.UnresolvedIncidentsCount);
Xunit.Assert.False(health.IsFlawless);
}
[Fact]
public void 최대_치유_초과_시_최종_예외_전파()
{
var autoPilot = new SelfHealingAutoPilot();
Xunit.Assert.Throws<InvalidOperationException>(() =>
{
autoPilot.ExecuteWithSelfHealing<string>(
action: () => throw new InvalidOperationException("Permanent fatal hardware fault"),
diagnoseAndRepair: (_, _) => true,
maxHealingAttempts: 2
);
});
}
}
/// <summary>
/// 40개 마일스톤 전 영역 통합 5대 그랜드 연속 파이프라인 (Grand Unified Pipeline) 검증.
/// </summary>
public class GrandUnifiedPipelineMegaTests
{
[Fact]
public void 시나리오1_브라우징_보안_광고차단_메모리프리징_미디어탐색_파이프라인()
{
using var pipeline = GrandUnifiedPipeline.Create();
// 1. EasyList 광고 차단 규칙 로드
pipeline.AdBlockEngine.AddRule("||adservice.google.com^");
pipeline.AdBlockEngine.AddRule("||doubleclick.net^");
pipeline.AdBlockEngine.AddRule("@@||partner.ads.com/essential.js");
// 2. 다중 탭 생성 및 활동 추적
var tab1 = pipeline.TabManager.NewTab("https://site1.test/watch?v=123");
var tab2 = pipeline.TabManager.NewTab("https://site2.test/music");
var tab3 = pipeline.TabManager.NewTab("https://adservice.google.com/banner");
pipeline.MemoryGuard.TrackTabActivity(tab1.Id.ToString(), DateTime.UtcNow);
pipeline.MemoryGuard.TrackTabActivity(tab2.Id.ToString(), DateTime.UtcNow.AddMinutes(-20)); // 비활성 탭
// 3. 광고 요청 차단 확인
pipeline.SimulateWebRequest("https://adservice.google.com/banner", MediaKind.Direct);
Xunit.Assert.Empty(pipeline.DetectedItems); // 광고이므로 차단되어 미디어 미감지
// 4. 정상 미디어 요청 감지
pipeline.SimulateWebRequest("https://site1.test/video.m3u8", MediaKind.Hls);
Xunit.Assert.Single(pipeline.DetectedItems);
Xunit.Assert.Equal("https://site1.test/video.m3u8", pipeline.DetectedItems[0].Url);
// 5. 메모리 가디언 자동 프리징 확인
var toSuspend = pipeline.MemoryGuard.GetTabsToSuspend(DateTime.UtcNow);
Xunit.Assert.Contains(tab2.Id.ToString(), toSuspend);
}
[Fact]
public void 시나리오2_미디어분석_중복방지_디스크가디언_큐등록_파이프라인()
{
using var pipeline = GrandUnifiedPipeline.Create();
// 1. FFprobe JSON 메타데이터 정밀 분석
var sampleFfprobeJson = @"{
""streams"": [
{
""codec_type"": ""video"",
""codec_name"": ""h264"",
""width"": 1920,
""height"": 1080,
""r_frame_rate"": ""60/1""
},
{
""codec_type"": ""audio"",
""codec_name"": ""aac"",
""channels"": 2
}
],
""format"": { ""duration"": ""300.0"", ""size"": ""150000000"", ""bit_rate"": ""4000000"" }
}";
var report = MediaMetadataExtractor.ParseFfprobeOutput(sampleFfprobeJson);
Xunit.Assert.True(report.IsHd);
Xunit.Assert.Equal("1080p", report.ResolutionLabel);
// 2. pHash 중복 검출 엔진 등록 및 중복 방어
ulong originalHash = 0xAAAA_BBBB_CCCC_DDDD;
pipeline.Deduplicator.RegisterVideo("existing_video.mp4", originalHash);
ulong duplicateCandidate = 0xAAAA_BBBB_CCCC_DDDC; // 1비트 차이 (중복)
var match = pipeline.Deduplicator.FindDuplicate(duplicateCandidate);
Xunit.Assert.NotNull(match);
Xunit.Assert.Equal("existing_video.mp4", match.FilePath);
// 3. 디스크 잔여 용량 사전 점검
var hasDiskSpace = DiskSpaceGuardian.HasSufficientSpace(Path.GetTempPath(), report.FileSizeBytes);
Xunit.Assert.True(hasDiskSpace);
}
[Fact]
public void 시나리오3_적응형QoS_버퍼풀할당_카오스장애_지수백오프_자동복구_파이프라인()
{
using var pipeline = GrandUnifiedPipeline.Create();
// 1. 버퍼 풀 대여 및 I/O 준비
var buffer = BufferPoolManager.Rent(64 * 1024);
try
{
Xunit.Assert.NotNull(buffer);
// 2. 사용자 브라우징 상호작용 발생 시 QoS 대역폭 즉시 양보
pipeline.TrafficShaper.RecordUserActivity(DateTime.UtcNow);
var yieldBandwidth = pipeline.TrafficShaper.GetEffectiveBandwidthLimit(DateTime.UtcNow);
Xunit.Assert.True(yieldBandwidth <= 2_500_000); // 25% 이하로 양보
// 3. 유휴 5초 경과 시 풀스피드 회복
var restoredBandwidth = pipeline.TrafficShaper.GetEffectiveBandwidthLimit(DateTime.UtcNow.AddSeconds(5));
Xunit.Assert.Equal(10_000_000, restoredBandwidth);
// 4. 카오스 네트워크 장애 주입 시 지수 백오프 100% 복구
var result = pipeline.ChaosDefense.ExecuteWithChaosRecovery(() =>
{
if (pipeline.ChaosDefense.ShouldInjectFailure())
throw new HttpRequestException("Simulated transient socket drop");
return "100% Downloaded Stream Data";
}, maxRetries: 5);
Xunit.Assert.Equal("100% Downloaded Stream Data", result);
}
finally
{
BufferPoolManager.Return(buffer);
}
}
[Fact]
public async Task 시나리오4_EBU_R128_오디오정규화_HDR톤매핑_챕터분할_파이프라인()
{
using var pipeline = GrandUnifiedPipeline.Create();
// 1. EBU R128 오디오 노멀라이제이션 (Loudnorm 2-pass)
var normSpec = LoudnessSpec.WebStandard;
var normalizedFile = await pipeline.AudioNormalizer.NormalizeAudioAsync("ffmpeg.exe", "input.mp4", "norm_output.mp4", normSpec, CancellationToken.None);
Xunit.Assert.Equal("norm_output.mp4", normalizedFile);
// 2. HDR10 색공간 감지 및 SDR 톤매핑
var isHdr = HdrToneMapper.IsHdrContent("bt2020nc", "smpte2084", "arib-std-b67");
Xunit.Assert.True(isHdr);
var sdrFile = await pipeline.HdrToneMapper.ConvertToSdrAsync("ffmpeg.exe", "hdr.mkv", "sdr.mp4", ToneMapAlgorithm.Mobius, CancellationToken.None);
Xunit.Assert.Equal("sdr.mp4", sdrFile);
// 3. 챕터 분할 및 FFMETADATA1 생성
var chapters = new[]
{
new ChapterInfo(1, "Intro", 0.0, 30.0),
new ChapterInfo(2, "Main Episode", 30.0, 180.0)
};
var splits = await pipeline.ChapterSplitter.SplitByChaptersAsync("ffmpeg.exe", "sdr.mp4", chapters, @"C:\output", CancellationToken.None);
Xunit.Assert.Equal(2, splits.Count);
}
[Fact]
public async Task 시나리오5_듀얼자막플레이어_P2P디스커버리_NAS클라우드동기화_파이프라인()
{
using var pipeline = GrandUnifiedPipeline.Create();
// 1. 내장 플레이어 주/보조 듀얼 자막 로드 및 오버레이
pipeline.DualSubtitleEngine.LoadPrimaryVtt("00:00:01.000 --> 00:00:05.000\nHello World");
pipeline.DualSubtitleEngine.LoadSecondaryVtt("00:00:01.000 --> 00:00:05.000\n안녕하세요 세계");
var dualCues = pipeline.DualSubtitleEngine.GetDualCues(3.0);
Xunit.Assert.NotNull(dualCues);
Xunit.Assert.Equal("Hello World", dualCues.PrimaryText);
Xunit.Assert.Equal("안녕하세요 세계", dualCues.SecondaryText);
// 2. mDNS 로컬 기기 디스커버리 광고 프로필 확인
var profile = MdnsAdvertiser.BuildProfile("My Desktop PC", 5000);
Xunit.Assert.Equal("_videodl._tcp", profile.ServiceName.ToString());
var srv = Xunit.Assert.Single(profile.Resources.OfType<Makaretu.Dns.SRVRecord>());
Xunit.Assert.Equal(5000, (int)srv.Port);
// 3. NAS / WebDAV / S3 클라우드 게이트웨이 백그라운드 자동 동기화
pipeline.CloudGateway.EnqueueSync(@"C:\output\video.mp4", "/nas/media/video.mp4");
var syncedCount = await pipeline.CloudGateway.ProcessAllPendingAsync(CancellationToken.None);
Xunit.Assert.Equal(1, syncedCount);
Xunit.Assert.Single(pipeline.CloudClient.UploadedList);
}
}
/// <summary>
/// 100+ 대규모 감사 항목에 대한 하드닝 전수 검증 테스트 스위트
/// </summary>
public class GrandAuditHardeningTests
{
// ==========================================
// 1. FileNameUtil 하드닝 검증
// ==========================================
[Theory]
[InlineData("CON", "_CON")]
[InlineData("PRN", "_PRN")]
[InlineData("AUX", "_AUX")]
[InlineData("NUL", "_NUL")]
[InlineData("COM1", "_COM1")]
[InlineData("LPT2", "_LPT2")]
[InlineData("aux.mp4", "_aux.mp4")]
[InlineData("con.tar.gz", "_con.tar.gz")]
public void FileNameUtil_Windows_예약어를_안전하게_치환한다(string input, string expected)
{
var sanitized = FileNameUtil.Sanitize(input);
Assert.Equal(expected, sanitized);
}
[Fact]
public void FileNameUtil_서러게이트_페어_이모지_절단_시_인코딩_오류를_방지한다()
{
var text = "영상_이모지_테스트_" + "\uD83D\uDE00";
var sanitized = FileNameUtil.Sanitize(text, maxLen: 12);
Assert.False(char.IsHighSurrogate(sanitized[^1]));
}
[Fact]
public void FileNameUtil_상대_URI나_Data_URI에서도_크래시_없이_기본값을_반환한다()
{
var relUri = new Uri("/video/path/file.mp4", UriKind.Relative);
var name = FileNameUtil.DeriveName(relUri);
Assert.Equal("video", name);
}
[Theory]
[InlineData("https://www.youtube.com/s/player/68c85eb0/failure", "www.youtube.com")]
[InlineData("https://example.com/api/v1/error", "example.com")]
[InlineData("https://example.com/video/stream/playback", "example.com")]
public void FileNameUtil_에러_및_제네릭_단어를_파일명으로_추출하지_않고_호스트명으로_폴백한다(string url, string expectedFallback)
{
var uri = new Uri(url);
var name = FileNameUtil.DeriveName(uri);
Assert.Equal(expectedFallback, name);
}
// ==========================================
// 2. M3u8Parser 하드닝 검증
// ==========================================
[Fact]
public void M3u8Parser_소문자_및_언더스코어_속성을_안전하게_파싱한다()
{
var m3u8 = """
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-KEY:METHOD=AES-128,URI="https://test.com/key.bin",iv=0x1234567890abcdef1234567890abcdef,key_format="identity"
#EXTINF:10.0,
seg1.ts
""";
var parsed = M3u8Parser.Parse(m3u8, new Uri("https://test.com/playlist.m3u8"));
Assert.Single(parsed.Segments);
Assert.NotNull(parsed.Segments[0].Key);
Assert.Equal(new Uri("https://test.com/key.bin"), parsed.Segments[0].Key!.KeyUri);
}
[Fact]
public void M3u8Parser_특수문자나_공백이_포함된_세그먼트_URI를_안전하게_파싱한다()
{
var m3u8 = """
#EXTM3U
#EXTINF:5.0,
seg with spaces.ts
#EXTINF:5.0,
seg#invalid?.ts
""";
var parsed = M3u8Parser.Parse(m3u8, new Uri("https://test.com/master/play.m3u8"));
Assert.Equal(2, parsed.Segments.Count);
}
// ==========================================
// 3. DashParser 하드닝 검증
// ==========================================
[Fact]
public void DashParser_자릿수_패딩_포맷_식별자를_지원한다()
{
var mpd = """
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" mediaPresentationDuration="PT30S">
<Period>
<AdaptationSet mimeType="video/mp4">
<Representation id="v1" bandwidth="1000000">
<SegmentTemplate media="chunk-$Number%05d$.m4s" initialization="init.mp4" startNumber="1" duration="10" timescale="1"/>
</Representation>
</AdaptationSet>
</Period>
</MPD>
""";
var parsed = DashParser.Parse(mpd, new Uri("https://test.com/video.mpd"));
Assert.NotNull(parsed);
var video = parsed.Video;
Assert.NotNull(video);
Assert.Equal(3, video!.Segments.Count);
Assert.Equal(new Uri("https://test.com/chunk-00001.m4s"), video.Segments[0].Url);
Assert.Equal(new Uri("https://test.com/chunk-00002.m4s"), video.Segments[1].Url);
}
[Fact]
public void DashParser_일_단위가_포함된_ISO8601_기간을_정확히_파싱한다()
{
var mpd = """
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" mediaPresentationDuration="P1DT2H30M">
<Period>
<AdaptationSet mimeType="video/mp4">
<Representation id="v1" bandwidth="500000">
<BaseURL>video.mp4</BaseURL>
</Representation>
</AdaptationSet>
</Period>
</MPD>
""";
var parsed = DashParser.Parse(mpd, new Uri("https://test.com/dash.mpd"));
Assert.NotNull(parsed);
var video = parsed.Video;
Assert.NotNull(video);
Assert.Single(video!.Segments);
}
// ==========================================
// 4. MediaDetector & ProbeFormat 하드닝 검증
// ==========================================
[Theory]
[InlineData("https://cdn.example.com/live/playlist_1080p/seg_001.ts", true)]
[InlineData("https://cdn.example.com/master_stream/chunk_01.m4s", true)]
[InlineData("https://cdn.example.com/manifest/init.mp4", true)]
[InlineData("https://cdn.example.com/vod/master.m3u8", false)]
[InlineData("https://cdn.example.com/vod/playlist.m3u8", false)]
[InlineData("https://cdn.example.com/vod/manifest.mpd", false)]
public void MediaDetector_CDN_경로_내_매니페스트_키워드가_있어도_세그먼트를_정확히_판정한다(string url, bool expectedSegment)
{
Assert.Equal(expectedSegment, MediaDetector.IsSegment(url));
}
[Fact]
public void MediaDetector_ProbeFormat이_RIFF_AVI를_정상_식별한다()
{
byte[] aviHeader = new byte[]
{
0x52, 0x49, 0x46, 0x46,
0x00, 0x10, 0x00, 0x00,
0x41, 0x56, 0x49, 0x20
};
var format = MediaDetector.ProbeFormat(aviHeader);
Assert.Equal("avi", format);
}
// ==========================================
// 5. SocialSiteDetector 피싱 방어 검증
// ==========================================
[Theory]
[InlineData("https://fakeyoutube.com/watch?v=12345678901", false)]
[InlineData("https://phishing-instagram.com/reel/C12345", false)]
[InlineData("https://eviltiktok.com/@user/video/12345", false)]
[InlineData("https://www.youtube.com/watch?v=dQw4w9WgXcQ", true)]
[InlineData("https://youtu.be/dQw4w9WgXcQ", true)]
[InlineData("https://x.com/user/status/123456789", true)]
[InlineData("https://instagram.com/p/C_12345/", true)]
public void SocialSiteDetector_도메인_경계를_엄격히_검증하여_피싱을_차단한다(string url, bool expectedMatch)
{
var matched = SocialSiteDetector.TryMatch(new Uri(url), out var norm, out var label);
Assert.Equal(expectedMatch, matched);
}
// ==========================================
// 6. Server & PositionStore 원자적 저장 검증
// ==========================================
[Fact]
public void PositionStore_정상_및_비정상_위치값을_안전하게_저장_로드한다()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"paca_pos_{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
try
{
var store = new PositionStore(tempDir);
store.Set("video1", 123.45);
store.Set("video2", double.NaN);
store.Set("video3", -10.0);
Assert.Equal(123.45, store.Get("video1"));
Assert.Equal(0, store.Get("video2"));
Assert.Equal(0, store.Get("video3"));
Assert.Equal(0, store.Get("nonexistent"));
var store2 = new PositionStore(tempDir);
Assert.Equal(123.45, store2.Get("video1"));
}
finally
{
try { Directory.Delete(tempDir, true); } catch { }
}
}
// ==========================================
// 7. Mobile.Core OfflineStore 원자적 저장 검증
// ==========================================
[Fact]
public void OfflineStore_매니페스트_원자적_저장_및_삭제가_동작한다()
{
var tempDir = Path.Combine(Path.GetTempPath(), $"paca_off_{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
try
{
var store = new OfflineStore(tempDir);
store.Record("item1", "test.mp4");
File.WriteAllText(store.PathFor("item1"), "content");
Assert.True(store.Exists("item1"));
Assert.Single(store.List());
store.Delete("item1");
Assert.False(store.Exists("item1"));
Assert.Empty(store.List());
}
finally
{
try { Directory.Delete(tempDir, true); } catch { }
}
}
// ==========================================
// 8. MdnsAdvertiser 한글 기기명 해시 접미사 검증
// ==========================================
[Fact]
public void MdnsAdvertiser_한글_기기명_입력시_충돌_없는_라벨을_생성한다()
{
var profile = MdnsAdvertiser.BuildProfile("내 컴퓨터", 5000);
Assert.NotNull(profile);
Assert.Contains("Paca-", profile.FullyQualifiedName?.ToString() ?? profile.Resources.FirstOrDefault()?.Name?.ToString() ?? "");
}
// ==========================================
// 9. ShortcutManager 키 파싱 확장 검증
// ==========================================
[Theory]
[InlineData("Ctrl+1", ModifierKeys.Control, Key.D1)]
[InlineData("Ctrl+9", ModifierKeys.Control, Key.D9)]
[InlineData("Ctrl+0", ModifierKeys.Control, Key.D0)]
[InlineData("Ctrl+Shift+P", ModifierKeys.Control | ModifierKeys.Shift, Key.P)]
[InlineData("Alt+Left", ModifierKeys.Alt, Key.Left)]
[InlineData("Ctrl++", ModifierKeys.Control, Key.OemPlus)]
[InlineData("Ctrl+-", ModifierKeys.Control, Key.OemMinus)]
public void ShortcutManager_숫자_기호_포함_단축키를_정확히_파싱한다(string gesture, ModifierKeys expectedMods, Key expectedKey)
{
var parsed = ShortcutManager.Parse(gesture);
Assert.NotNull(parsed);
Assert.Equal(expectedMods, parsed!.Value.Mods);
Assert.Equal(expectedKey, parsed.Value.Key);
}
// ==========================================
// 10. CommandPalette 동적 소스 예외 격리 검증
// ==========================================
[Fact]
public void CommandPalette_동적_제공자에서_예외가_발생해도_검색이_중단되지_않는다()
{
var palette = new CommandPalette();
palette.Register("정상 명령", "키워드", () => { });
palette.RegisterDynamic(() => throw new InvalidOperationException("동적 실패"));
var results = palette.Search("정상");
Assert.Single(results);
Assert.Equal("정상 명령", results[0].Title);
}
// ==========================================
// 12. Phase 66: 10,000-Gate Autonomous Chaos Matrix & Self-Healing
// ==========================================
[Fact]
public void ChaosFuzzingMatrixEngine_1000회_변이_인젝션_프로세스_크래시_0건_검증()
{
var report = Paca.Core.Infrastructure.ChaosFuzzingMatrixEngine.RunChaosFuzzingSuite(payload =>
{
// Simulate parser handling random byte streams
string str = System.Text.Encoding.UTF8.GetString(payload);
if (str.Contains("INVALID"))
throw new FormatException("Corrupted payload detected");
}, iterations: 1000);
Assert.Equal(1000, report.TotalMutationsGenerated);
Assert.Equal(0, report.UnhandledCrashesCount);
Assert.True(report.Passed);
}
[Fact]
public void AutonomousSelfHealingSystem_연속_장애_감지_및_자동_자가치유_검증()
{
var healing = new Paca.Core.Infrastructure.AutonomousSelfHealingSystem();
healing.RegisterService("HlsLiveEngine");
Assert.Equal(Paca.Core.Infrastructure.SubserviceHealthState.Healthy, healing.GetStatus("HlsLiveEngine").State);
// 1st & 2nd failure -> Degraded
healing.ReportFailure("HlsLiveEngine");
healing.ReportFailure("HlsLiveEngine");
Assert.Equal(Paca.Core.Infrastructure.SubserviceHealthState.Degraded, healing.GetStatus("HlsLiveEngine").State);
// 3rd failure -> Faulted
healing.ReportFailure("HlsLiveEngine");
Assert.Equal(Paca.Core.Infrastructure.SubserviceHealthState.Faulted, healing.GetStatus("HlsLiveEngine").State);
// Auto-heal recovery
bool restarted = false;
bool healed = healing.TryAutoHeal("HlsLiveEngine", () => restarted = true);
Assert.True(healed);
Assert.True(restarted);
Assert.Equal(Paca.Core.Infrastructure.SubserviceHealthState.Healthy, healing.GetStatus("HlsLiveEngine").State);
}
}