namespace LivingVillage.Desktop.Tests open System.Threading open System.Threading.Tasks open Microsoft.VisualStudio.TestTools.UnitTesting open LivingVillage.Headless /// M3-1 可观测性回归:批次输出格式与逐世界流式驱动。 [] type M3BatchOutputTests () = let sampleBase = "world=3 seed=45 chats_max=9 chats_avg=2.00 ratio=4.500 gini=0.100 relcnt_max=5 relcnt_min=1 relconn=6/6 nonfinite=0 check_a=PASS check_b=PASS check_c=PASS old_crit=PASS OK" [] member _.WorldLineKeepsPrefixAndAppendsElapsed () = let line = BatchOutput.worldLine sampleBase 123.4 Assert.IsTrue(line.StartsWith("world=3 seed=45 "), "world 行前缀必须保持不变") Assert.IsTrue(line.StartsWith(sampleBase), "判据字段必须原样保留") Assert.IsTrue(line.EndsWith("elapsed_s=123.4"), "耗时字段追加在行尾") // 旧判据 token 仍在(监督器据此判定 OK)。 Assert.AreEqual(sampleBase + " elapsed_s=123.4", line) [] member _.DoneLineReportsProgressAndElapsed () = Assert.AreEqual("[done 2/4] elapsed=12.3s world=1", BatchOutput.doneLine 2 4 12.3 1) Assert.AreEqual("[done 4/4] elapsed=600.0s world=3", BatchOutput.doneLine 4 4 600.0 3) [] member _.ProgressLineReportsSteppedDayWithinWorld () = Assert.AreEqual("[world 7] progress day=5/15 elapsed=61.2s", BatchOutput.progressLine 7 5L 15L 61.2) Assert.AreEqual("[world 0] progress day=15/15 elapsed=260.0s", BatchOutput.progressLine 0 15L 15L 260.0) [] member _.MultiworldLineReportsCountsGcRssAndElapsed () = let line = BatchOutput.multiworldLine 8 4L 20L 1600L 32000L 64L 5120L (1048576L * 96L) (1048576L * 240L) 31.5 Assert.AreEqual( "multiworld_sample worlds=8 day=4/20 events=1600 rumors=32000 annals=64 memory_entries=5120 gc_live_mb=96.00 working_set_rss_mb=240.00 elapsed_s=31.5", line) [] member _.SummaryLineKeepsOldFieldsAndAppendsWallClock () = let line = BatchOutput.summaryLine 4 3 1 4 1.100 9.900 0.010 0.400 4 610.5 // 监督器正则只读前三个字段,前缀必须逐字保持。 Assert.IsTrue(line.StartsWith("batch_summary worlds=4 passed=3 failed=1 old_passed=4/4 "), line) Assert.IsTrue(line.Contains("ratio_min=1.100 ratio_max=9.900 gini_min=0.010 gini_max=0.400"), line) Assert.IsTrue(line.Contains("elapsed_s=610.5"), line) Assert.IsTrue(line.EndsWith("workers=4 wall_s=610.5"), line) [] member _.CostLineDerivesThroughputFromMeasuredTicks () = let line = BatchOutput.costLine 10L 51840000L 254.0 12345678L 3L 1L 0L Assert.IsTrue(line.StartsWith("cost days=10 ticks=51840000 elapsed_s=254.000 "), line) Assert.IsTrue(line.Contains("ticks_per_s=204094.5"), line) Assert.IsTrue(line.EndsWith("allocated_bytes=12345678 gen0=3 gen1=1 gen2=0"), line) [] member _.DefaultCostLadderIsOneFiveTenTwentyFiftyHundred () = CollectionAssert.AreEqual([| 1L; 5L; 10L; 20L; 50L; 100L |], List.toArray BatchOutput.defaultCostLadder) [] member _.RunStreamingEmitsEachWorldBeforeTheNextStarts () = // workers=1:每次 work 后必须立刻 emit 该世界(证明不是收完再打印)。 let events = ResizeArray () let results = BatchOutput.runStreaming 3 1 (fun k -> lock events (fun () -> events.Add(sprintf "work%d" k)) k * k) (fun k _ _ -> lock events (fun () -> events.Add(sprintf "emit%d" k))) CollectionAssert.AreEqual([| 0; 1; 4 |], results) CollectionAssert.AreEqual( [| "work0"; "emit0"; "work1"; "emit1"; "work2"; "emit2" |], events.ToArray()) [] member _.RunStreamingIsCompleteUnderParallelism () = let total = 12 let emitted = System.Collections.Concurrent.ConcurrentDictionary () let completions = ResizeArray () let results = BatchOutput.runStreaming total 4 (fun k -> k + 100) (fun k result doneCount -> Assert.AreEqual(k + 100, result) emitted.[k] <- doneCount lock completions (fun () -> completions.Add doneCount)) Assert.AreEqual(total, results.Length) Assert.AreEqual(total, emitted.Count) for k in 0 .. total - 1 do Assert.IsTrue(emitted.ContainsKey k, sprintf "world=%d 未 emit" k) // 完成计数恰为 1..total 的一个排列(并发下不重不漏)。 CollectionAssert.AreEqual( [| 1 .. total |], completions |> Seq.sort |> Seq.toArray)