extern alias engine; using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Reflection; using HarmonyLib; namespace SVSim.BattleEngine.Tests.Coverage; /// Harmony-prefix-based method-invocation logger over SVSim.BattleEngine.dll. /// Idempotent: Install is safe to call from every fixture's [OneTimeSetUp]; only the first /// call patches. public static class CoverageRecorder { private static readonly ConcurrentDictionary _hit = new(); private static readonly Harmony _h = new("svsim.engine.coverage"); private static bool _installed; private static readonly object _gate = new(); public static int HitCount => _hit.Count; public static void Reset() => _hit.Clear(); public static void Install() { lock (_gate) { if (_installed) return; _installed = true; } var asm = typeof(engine::BattleManagerBase).Assembly; var prefix = AccessTools.Method(typeof(CoverageRecorder), nameof(Hit)); var hm = new HarmonyMethod(prefix); foreach (var t in asm.GetTypes()) { var ns = t.FullName ?? string.Empty; // Only engine + shim types — skip framework/3rd-party that happens to be co-located. if (!ns.StartsWith("SVSim.BattleEngine.") && !ns.StartsWith("Wizard.") && !ns.StartsWith("Cute.") && !ns.StartsWith("Toolbox.") && !ns.StartsWith("UnityEngine.")) continue; if (t.IsGenericTypeDefinition) continue; var members = t.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); foreach (var m in members) { if (m.IsAbstract) continue; if (m.ContainsGenericParameters) continue; if (m.GetMethodBody() is null) continue; try { _h.Patch(m, prefix: hm); } catch { /* generic/inlined; skipped */ } } } } public static void Dump(string path) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); File.WriteAllLines(path, _hit.Keys.OrderBy(k => k)); } private static void Hit(MethodBase __originalMethod) { var t = __originalMethod.DeclaringType; if (t is null) return; _hit.TryAdd($"{t.FullName}.{__originalMethod.Name}", 0); } }