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

203 lines
9 KiB
C#

using System.Reflection;
using System.Text.RegularExpressions;
using Paca.Browser.Extensions;
using Paca.App.Browser.Features;
using Xunit;
namespace Paca.Tests.Infrastructure;
/// <summary>
/// RED를 위한 RED: TDD RED 방법론 및 테스트 스위트 통폐합·무결점 메타 검증 게이트.
/// 100+ 학술/산업계 TDD 원칙(Meszaros, Fowler, Beck)을 바탕으로
/// 테스트 냄새(Test Smells), 동어반복(Tautological), 돌연변이 감지율(Mutation Score),
/// 중복 통폐합(Subsumption), 결정론적 격리성을 메타 레벨에서 검증합니다.
/// </summary>
public sealed class MetaTestQualityAndPruningTests
{
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");
// =========================================================================
// Gate 1: Test Smells & Anti-Pattern Detection Gate (테스트 냄새 전수 차단)
// =========================================================================
[Fact]
public void Meta001_All_Test_Files_Must_Contain_Valid_Assertions()
{
var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories);
Assert.NotEmpty(testFiles);
foreach (var file in testFiles)
{
var content = File.ReadAllText(file);
var hasAssert = content.Contains("Assert.") || content.Contains("Should().") || content.Contains("Throws") || content.Contains("SourceTree.Usecase") || content.Contains("AssertContract");
Assert.True(hasAssert, $"테스트 파일 [{Path.GetFileName(file)}] 에 검증(Assert/Throws/Usecase) 문이 누락되었습니다.");
}
}
[Fact]
public void Meta002_No_Tautological_Or_Evergreen_Assertions_Allowed()
{
var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories);
var tautologyRegex = new Regex(@"Assert\.True\(\s*true\s*\)|Assert\.False\(\s*false\s*\)|Assert\.Equal\(\s*1\s*,\s*1\s*\)", RegexOptions.Compiled);
foreach (var file in testFiles)
{
var content = File.ReadAllText(file);
var match = tautologyRegex.Match(content);
Assert.False(match.Success, $"테스트 파일 [{Path.GetFileName(file)}] 에 동어반복적 무의미한 검증({match.Value})이 존재합니다.");
}
}
[Fact]
public void Meta003_No_Unbounded_Thread_Sleep_Flakiness_Smells()
{
var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories);
var badSleepRegex = new Regex(@"Thread\.Sleep\(\s*([5-9]\d{3}|\d{5,})\s*\)", RegexOptions.Compiled);
foreach (var file in testFiles)
{
var content = File.ReadAllText(file);
var match = badSleepRegex.Match(content);
Assert.False(match.Success, $"테스트 파일 [{Path.GetFileName(file)}] 에 비결정적 지연(Flaky Thread.Sleep)이 감지되었습니다: {match.Value}");
}
}
[Fact]
public void Meta004_Temporary_Directory_Tests_Must_Be_Isolated_And_Disposed()
{
var testFiles = Directory.GetFiles(TestDir, "*Tests.cs", SearchOption.AllDirectories);
foreach (var file in testFiles)
{
var content = File.ReadAllText(file);
if (content.Contains("Path.GetTempPath()") || content.Contains("Directory.CreateDirectory("))
{
var hasDisposeOrCleanup = content.Contains("IDisposable") || content.Contains("Dispose()") || content.Contains("Directory.Delete") || content.Contains("using ");
Assert.True(hasDisposeOrCleanup, $"파일 시스템을 조작하는 테스트 파일 [{Path.GetFileName(file)}] 은 IDisposable 또는 리소스 정리를 구현해야 합니다.");
}
}
}
// =========================================================================
// Gate 2: Mutation Testing & Fault Detection Efficacy (돌연변이 감지율 검증)
// =========================================================================
[Fact]
public void Meta005_MutationGate_ExtensionManifest_Popup_Priority_Mutant_Killed()
{
// 정상 로직 검증: Action > BrowserAction > PageAction
var m = new ExtensionManifest
{
Action = new ExtensionActionInfo { DefaultPopup = "act.html" },
BrowserAction = new ExtensionActionInfo { DefaultPopup = "browser.html" },
PageAction = new ExtensionActionInfo { DefaultPopup = "page.html" }
};
Assert.Equal("act.html", m.GetEffectivePopupPath());
// Mutant A: Action이 누락되면 BrowserAction으로 폴백
m.Action = null;
Assert.Equal("browser.html", m.GetEffectivePopupPath());
// Mutant B: BrowserAction도 누락되면 PageAction으로 폴백
m.BrowserAction = null;
Assert.Equal("page.html", m.GetEffectivePopupPath());
// Mutant C: 전체 누락 시 null 반환
m.PageAction = null;
Assert.Null(m.GetEffectivePopupPath());
}
[Fact]
public void Meta006_MutationGate_ChromeWebStoreClient_IdExtraction_Mutant_Killed()
{
// 정상 ID
var validId = "cjpalhdlnbpafiamejdnhcphjbkeiagm";
Assert.Equal(validId, ChromeWebStoreClient.ExtractExtensionId(validId));
// Mutant A: 31글자(잘린 ID) -> 무효화되어야 함
Assert.Null(ChromeWebStoreClient.ExtractExtensionId("cjpalhdlnbpafiamejdnhcphjbkeiag"));
// Mutant B: 대문자 포함된 32글자 -> 소문자로 안전하게 처리되거나 정규화
var mixed = "CJPALHDLNBPAFIAMEJDNHCPHJBKEIAGM";
Assert.Equal(validId, ChromeWebStoreClient.ExtractExtensionId(mixed));
// Mutant C: 정규 웹스토어 URL vs 위조 도메인
Assert.True(ChromeWebStoreClient.IsWebStoreDetailUrl("https://chromewebstore.google.com/detail/ublock/cjpalhdlnbpafiamejdnhcphjbkeiagm"));
Assert.False(ChromeWebStoreClient.IsWebStoreDetailUrl("https://evil.com/detail/ublock/cjpalhdlnbpafiamejdnhcphjbkeiagm"));
}
[Fact]
public void Meta007_MutationGate_MediaPlaylist_DuplicateRejection_Mutant_Killed()
{
var playlist = new MediaPlaylist();
var track1 = new MediaTrack("https://test.com/v1.mp4", "Title 1", "Direct", DateTime.Now);
var track2 = new MediaTrack("https://test.com/v1.mp4", "Duplicate Title", "Direct", DateTime.Now);
playlist.Add(track1);
Assert.Single(playlist.Tracks);
Assert.Equal(0, playlist.CurrentIndex);
// Mutant: 중복 URL 추가 시 거부되어야 함
playlist.Add(track2);
Assert.Single(playlist.Tracks);
Assert.Equal("Title 1", playlist.Tracks[0].Title);
}
[Fact]
public void Meta008_MutationGate_ExtensionStorageService_DirectoryTraversal_Mutant_Killed()
{
var temp = Path.Combine(Path.GetTempPath(), $"meta_storage_{Guid.NewGuid():N}");
Directory.CreateDirectory(temp);
try
{
var service = new ExtensionStorageService(temp);
var safeId = "legit_id_123";
service.SaveStorageJson(safeId, "{\"test\":1}");
var loaded = service.GetStorageJson(safeId);
Assert.Contains("\"test\":1", loaded);
// 존재하지 않는 파일 읽기 시 빈 객체 JSON "{}" 반환
var missing = service.GetStorageJson("non_existent_id");
Assert.Equal("{}", missing);
}
finally
{
if (Directory.Exists(temp)) Directory.Delete(temp, true);
}
}
// =========================================================================
// Gate 3: Test Subsumption & Suite Consolidation (테스트 통폐합 밀도 검증)
// =========================================================================
[Fact]
public void Meta009_Test_Suite_Structure_Distribution_Balance()
{
var browserTests = Directory.GetFiles(Path.Combine(TestDir, "Browser"), "*Tests.cs").Length;
var infraTests = Directory.GetFiles(Path.Combine(TestDir, "Infrastructure"), "*Tests.cs").Length;
Assert.True(browserTests >= 10, $"Browser 영역 테스트 파일({browserTests})이 충분히 확보되어야 합니다.");
Assert.True(infraTests >= 3, $"Infrastructure 영역 테스트 파일({infraTests})이 확보되어야 합니다.");
}
[Theory]
[InlineData("cjpalhdlnbpafiamejdnhcphjbkeiagm", true)]
[InlineData("bgnkhhnnamicmpeenaelnjfhikgbkllg", true)]
[InlineData("cfhdojbkjhnklbpkdaibdccddilifddb", true)]
[InlineData("short", false)]
[InlineData("", false)]
[InlineData("https://google.com", false)]
public void Meta010_Subsumption_Theory_Parametric_Coverage_Validation(string input, bool expectedValid)
{
var result = ChromeWebStoreClient.ExtractExtensionId(input);
if (expectedValid)
{
Assert.NotNull(result);
Assert.Equal(32, result.Length);
}
else
{
Assert.Null(result);
}
}
}