SeibroMarkersTests.cs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. using Application.Helpers;
  2. using Infrastructure.StockData;
  3. using Microsoft.EntityFrameworkCore;
  4. namespace Application.Tests;
  5. /// <summary>
  6. /// SEIBro 수집 마커(SeibroMarkers/SeibroFetchMarker/SeibroSweptDate) 검증 — 리뷰 결함 #1 해소 배선의 핵심.
  7. /// rolling: 조회 시도(0행 포함) 마커로 대상 정렬(미조회 최우선→오래된 순) → starvation 방지.
  8. /// date-sweep: "데이터 존재 OR 마커 존재" 판정으로 조용한 날 재조회 제거. 실제 SQL Server(LocalDB). 고유 JobKey 로 공유 DB 오염 방지.
  9. /// </summary>
  10. [TestClass]
  11. public sealed class SeibroMarkersTests
  12. {
  13. private static string NewJob() => $"test-{Guid.NewGuid():N}"[..30];
  14. [TestMethod]
  15. public async Task FetchMarker_TouchThenLoad_UpsertsAndBumpsTime()
  16. {
  17. // 기대 행위: TouchFetchMarker — 없으면 생성, 있으면 시각 갱신. LoadFetchMarkers 로 (키→시각) 사전 반환.
  18. var job = NewJob();
  19. using (var db = TestDb.Create())
  20. {
  21. await SeibroMarkers.TouchFetchMarkerAsync(db, job, "ISIN_A", CancellationToken.None);
  22. await SeibroMarkers.TouchFetchMarkerAsync(db, job, "ISIN_B", CancellationToken.None);
  23. }
  24. DateTime firstA;
  25. using (var db = TestDb.Create())
  26. {
  27. var markers = await SeibroMarkers.LoadFetchMarkersAsync(db, job, CancellationToken.None);
  28. Assert.AreEqual(2, markers.Count);
  29. Assert.IsTrue(markers.ContainsKey("ISIN_A"));
  30. firstA = markers["ISIN_A"];
  31. }
  32. await Task.Delay(15);
  33. using (var db = TestDb.Create())
  34. {
  35. await SeibroMarkers.TouchFetchMarkerAsync(db, job, "ISIN_A", CancellationToken.None); // 재조회 → 시각 갱신
  36. }
  37. using (var db = TestDb.Create())
  38. {
  39. var markers = await SeibroMarkers.LoadFetchMarkersAsync(db, job, CancellationToken.None);
  40. Assert.AreEqual(2, markers.Count, "재Touch 는 새 행 생성 없이 upsert");
  41. Assert.IsTrue(markers["ISIN_A"] > firstA, "재조회로 LastCheckedAt 갱신");
  42. }
  43. }
  44. [TestMethod]
  45. public async Task FetchMarker_Ordering_UncheckedFirstThenOldest()
  46. {
  47. // 기대 행위(#1 핵심): rolling 대상 정렬 = 미조회(마커 없음, null) 최우선 → 이미 조회한 것 중 오래된 순.
  48. // 정상 0행 키가 마커를 남겨 매 실행 최우선 재조회되던 문제 해소를 SeibroRollingSweep 정렬로 검증.
  49. var job = NewJob();
  50. using (var db = TestDb.Create())
  51. {
  52. await SeibroMarkers.TouchFetchMarkerAsync(db, job, "OLD", CancellationToken.None); // 오래됨
  53. await Task.Delay(15);
  54. await SeibroMarkers.TouchFetchMarkerAsync(db, job, "RECENT", CancellationToken.None); // 최근
  55. }
  56. using var dbLoad = TestDb.Create();
  57. var markers = await SeibroMarkers.LoadFetchMarkersAsync(dbLoad, job, CancellationToken.None);
  58. var universe = new[] { "RECENT", "OLD", "NEW" };
  59. var targets = universe.Select(k => (Key: k, LastUpdatedAt: markers.TryGetValue(k, out var t) ? (DateTime?)t : null)).ToList();
  60. var order = new List<string>();
  61. await SeibroRollingSweep.RunAsync(targets, (k, _) => { order.Add(k); return Task.CompletedTask; }, maxPerRun: 3, delayMs: 0, ct: CancellationToken.None);
  62. Assert.AreEqual("NEW", order[0], "미조회(마커 없음) 최우선");
  63. Assert.AreEqual("OLD", order[1], "그다음 오래된 조회");
  64. Assert.AreEqual("RECENT", order[2], "최근 조회는 마지막");
  65. }
  66. [TestMethod]
  67. public async Task SweptDate_MarkThenIsSwept_SkipsQuietDay()
  68. {
  69. // 기대 행위(#1 핵심): 0행 조회 후 MarkSwept → IsSwept 가 true → 다음 실행 재조회 스킵. 미기록 날은 false.
  70. var job = NewJob();
  71. var swept = new DateOnly(2020, 3, 14);
  72. var notSwept = new DateOnly(2020, 3, 15);
  73. using (var db = TestDb.Create())
  74. {
  75. Assert.IsFalse(await SeibroMarkers.IsSweptAsync(db, job, swept, null, CancellationToken.None), "초기엔 미조회");
  76. await SeibroMarkers.MarkSweptAsync(db, job, swept, null, CancellationToken.None); // 0행이어도 조회완료 기록
  77. await SeibroMarkers.MarkSweptAsync(db, job, swept, null, CancellationToken.None); // 재기록 idempotent
  78. }
  79. using (var db = TestDb.Create())
  80. {
  81. Assert.IsTrue(await SeibroMarkers.IsSweptAsync(db, job, swept, null, CancellationToken.None), "조회완료 → 재조회 스킵");
  82. Assert.IsFalse(await SeibroMarkers.IsSweptAsync(db, job, notSwept, null, CancellationToken.None), "미기록 날은 여전히 조회 대상");
  83. var count = await db.SeibroSweptDate.AsNoTracking().Where(c => c.JobKey == job && c.Date == swept).CountAsync();
  84. Assert.AreEqual(1, count, "재기록해도 단일 행(idempotent)");
  85. }
  86. }
  87. [TestMethod]
  88. public async Task SweptDate_Discriminator_IsolatesAxes()
  89. {
  90. // 기대 행위: 축(tpcd/kacd/nation) 구분 마커는 독립. 같은 날짜라도 축이 다르면 별도 조회 대상.
  91. var job = NewJob();
  92. var day = new DateOnly(2020, 5, 1);
  93. using (var db = TestDb.Create())
  94. {
  95. await SeibroMarkers.MarkSweptAsync(db, job, day, "12", CancellationToken.None); // 구분 12 만 조회완료
  96. }
  97. using (var db = TestDb.Create())
  98. {
  99. Assert.IsTrue(await SeibroMarkers.IsSweptAsync(db, job, day, "12", CancellationToken.None), "구분 12 조회완료");
  100. Assert.IsFalse(await SeibroMarkers.IsSweptAsync(db, job, day, "13", CancellationToken.None), "구분 13 은 여전히 대상");
  101. Assert.IsFalse(await SeibroMarkers.IsSweptAsync(db, job, day, null, CancellationToken.None), "축 없는(빈) 판정도 별개");
  102. }
  103. }
  104. }