using ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared; using System.Buffers.Binary; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential; /// Runs the differential alone: no other suite may hold a session to the target meanwhile. [CollectionDefinition(Name, DisableParallelization = true)] public sealed class DifferentialCollection : ICollectionFixture { public const string Name = "AbCip transport differential"; } /// Performs the one run all differential tests read from, on first use. public sealed class DifferentialRunFixture { private static readonly TimeSpan RunBudget = TimeSpan.FromMinutes(15); private readonly Lazy> _run = new(RunAsync); internal Task GetAsync() => _run.Value; private static async Task RunAsync() { var target = DifferentialTarget.FromEnvironment() ?? throw new InvalidOperationException(DifferentialTarget.SkipReason); using var cts = new CancellationTokenSource(RunBudget); return await DifferentialRun.ExecuteAsync(target, cts.Token).ConfigureAwait(false); } } /// /// Read-only differential (Task 18): the same tags through libplctag and the native transport against one /// endpoint — browse listings, UDT templates and tag reads must agree byte for byte and value for value. /// Task 21 runs this against a production press, so the suite is read-only by construction (see /// ), libplctag and native are never connected at the /// same time, and the native side runs on one connection held open by an anchor lease. /// [Collection(DifferentialCollection.Name)] [Trait("Category", "LiveIntegration")] public sealed class TransportDifferentialTests(DifferentialRunFixture fixture) { [Fact] public async Task Browse_listings_decode_to_identical_tag_sets_through_both_enumerators() { var r = await RunOrSkipAsync(); Log($"## Browse — {r.Target}"); Log($"libplctag: {r.Libplctag.Browse.Count} tag(s){ErrorSuffix(r.Libplctag.BrowseError)}; " + $"native: {r.Native.Browse.Count} tag(s){ErrorSuffix(r.Native.BrowseError)}"); var onlyLib = DifferentialRun.Missing(r.Libplctag.Browse, r.Native.Browse); var onlyNative = DifferentialRun.Missing(r.Native.Browse, r.Libplctag.Browse); foreach (var t in onlyLib) Log($"- only libplctag: {t}"); foreach (var t in onlyNative) Log($"- only native: {t}"); r.Libplctag.BrowseError.ShouldBeNull(); r.Native.BrowseError.ShouldBeNull(); r.Libplctag.Browse.ShouldNotBeEmpty("a browse that returns nothing proves nothing"); onlyLib.ShouldBeEmpty(); onlyNative.ShouldBeEmpty(); } [Fact] public async Task Every_template_decodes_to_an_identical_shape_through_both_readers() { var r = await RunOrSkipAsync(); var ids = r.Libplctag.Shapes.Keys.Concat(r.Libplctag.TemplateErrors.Keys) .Concat(r.Native.Shapes.Keys).Concat(r.Native.TemplateErrors.Keys) .Distinct().Order().ToList(); Log($"## Templates — {ids.Count} id(s); truncated by {DifferentialTarget.MaxTemplatesVar}: " + $"libplctag {r.Libplctag.TemplatesTruncated}, native {r.Native.TemplatesTruncated}"); if (ids.Count > 0) { Log("| Template | libplctag | native | Equal? |"); Log("|---|---|---|---|"); } var refused = DifferentialRun.EqualRefusals(r.Libplctag, r.Native).ToHashSet(); foreach (var id in ids) { var equal = DifferentialRun.ShapesEqual(r.Libplctag.Shapes.GetValueOrDefault(id), r.Native.Shapes.GetValueOrDefault(id)); var verdict = refused.Contains(id) ? "equal refusal" : equal ? "yes" : "**NO**"; Log($"| {id} | {Shape(r.Libplctag, id)} | {Shape(r.Native, id)} | {verdict} |"); } if (refused.Count > 0) Log($"- EQUAL REFUSAL (not a divergence; both transports refuse with the same status): {string.Join(", ", refused)}"); var problems = DifferentialRun.TemplateProblems(r.Libplctag, r.Native); foreach (var p in problems) Log($"- PROBLEM: {p}"); problems.ShouldBeEmpty(); } [Fact] public async Task Every_selected_tag_reads_identically_through_both_transports() { var r = await RunOrSkipAsync(); Log($"## Reads — {r.Selection.Count} tag(s), {r.Target}"); Log("Selection by origin: " + string.Join(", ", r.Selection.GroupBy(t => t.Origin).Select(g => $"{g.Key} {g.Count()}"))); Log("| Tag | Type | libplctag status | native status | libplctag probe | Equal? |"); Log("|---|---|---|---|---|---|"); var unexpected = new List(); var expected = new List(); var live = new List(); foreach (var tag in r.Selection) { var lib = r.Libplctag.Reads[tag.Name]; var nat = r.Native.Reads[tag.Name]; var probe = r.Libplctag.ProbeReads.GetValueOrDefault(tag.Name); var verdict = Compare(tag, lib, nat, probe); switch (Classify(verdict)) { case Verdict.Expected: expected.Add($"{tag.Name}: {verdict}"); break; case Verdict.Live: live.Add($"{tag.Name} (lib {Detail(lib, full: true)}; lib probe {Detail(probe!, full: true)}; native {Detail(nat, full: true)})"); break; case Verdict.Unexpected when DifferentialRun.IsLive(lib, probe): unexpected.Add($"{tag.Name}: {verdict} (live; lib {Detail(lib, full: true)}; lib probe {Detail(probe!, full: true)}; native {Detail(nat, full: true)})"); break; case Verdict.Unexpected: unexpected.Add($"{tag.Name}: {verdict} (lib {Detail(lib)}; native {Detail(nat)})"); break; } Log($"| `{tag.Name}` | {tag.Type?.ToString() ?? "?"}{(tag.IsArray ? $"[{tag.ElementCount}]" : "")} | " + $"{StatusName(lib.Status)} | {StatusName(nat.Status)} | {ProbeColumn(lib, probe)} | {verdict} |"); } Log(""); Log($"Unexpected divergences: {unexpected.Count}; expected divergences: {expected.Count}; " + $"live tags (value moved between the two libplctag reads; compared by status, length, type code, shape and every " + $"element that held still): {live.Count} of {r.Selection.Count}."); foreach (var e in expected) Log($"- expected: {e}"); foreach (var l in live) Log($"- live: {l}"); foreach (var u in unexpected) Log($"- UNEXPECTED: {u}"); var coverage = DifferentialRun.SelectionCoverageProblems(r.Target, r.Libplctag, r.Selection); foreach (var c in coverage) Log($"- COVERAGE: {c}"); r.Selection.ShouldNotBeEmpty("a run that reads nothing proves nothing"); unexpected.ShouldBeEmpty(); coverage.ShouldBeEmpty(); } [Fact] public async Task Libplctag_and_native_are_never_connected_at_the_same_time() { var r = await RunOrSkipAsync(); Log($"TCP connections to the target: baseline {r.ConnectionsBaseline?.ToString(CultureInfo.InvariantCulture) ?? "unobservable"}, " + $"before native {r.ConnectionsBeforeNative?.ToString(CultureInfo.InvariantCulture) ?? "unobservable"}, " + $"libplctag drain wait {r.LibplctagDrainWait.TotalMilliseconds:0} ms."); if (r.ConnectionsBaseline is null) Assert.Skip("This OS cannot enumerate TCP connections; the run fell back to a fixed settle before the native phase."); // The run itself refuses to start the native phase while libplctag's session is open; this pins it. r.ConnectionsBeforeNative.ShouldNotBeNull(); r.ConnectionsBeforeNative.Value.ShouldBeLessThanOrEqualTo(r.ConnectionsBaseline.Value); } /// /// T21 press pass 1 ended in "A callback was made on a garbage collected delegate": libplctag tags whose /// initialisation failed (NotFound tags, refused latency setups) were left native-side with a live callback. /// Every libplctag tag this harness creates goes through the driver's adapters, which now destroy such a tag /// (); this pins that none had to fall back to being rooted instead. /// [Fact] public async Task No_failed_libplctag_tag_is_left_with_a_live_native_callback() { await RunOrSkipAsync(); Log($"libplctag orphans destroyed so far: {LibplctagOrphanRelease.Released}; rooted (unreleasable): {LibplctagOrphanRelease.RootedCount}."); LibplctagOrphanRelease.CanReadHandles.ShouldBeTrue(); LibplctagOrphanRelease.RootedCount.ShouldBe(0); } [Fact] public async Task The_native_side_makes_exactly_one_forward_open_over_the_whole_run() { var r = await RunOrSkipAsync(); if (!r.Target.CountOpens) Assert.Skip($"{DifferentialTarget.CountOpensVar} is not 1 — the process-wide Forward Open counter is only " + "trusted when the differential runs alone."); Log($"Native Forward Opens over the run: {r.NativeForwardOpens}."); r.NativeForwardOpens.ShouldBe(1); } /// /// T21 review r1 M9: the native session is closed properly — exactly one Forward Close on the wire (a skipped /// close, e.g. on a socket already dropped, would otherwise be visible only in a capture), and no connection to /// the target left once the anchor is released. /// [Fact] public async Task The_native_side_closes_its_one_connection_with_exactly_one_forward_close() { var r = await RunOrSkipAsync(); Log($"Native Forward Closes sent/acked: {r.NativeForwardCloses.Sent}/{r.NativeForwardCloses.Acked}; " + $"connections after the native phase: {r.ConnectionsAfterNative?.ToString(CultureInfo.InvariantCulture) ?? "unobservable"}."); if (r.ConnectionsAfterNative is { } after && r.ConnectionsBaseline is { } baseline) after.ShouldBeLessThanOrEqualTo(baseline); if (!r.Target.CountOpens) Assert.Skip($"{DifferentialTarget.CountOpensVar} is not 1 — the process-wide Forward Close counter is only " + "trusted when the differential runs alone."); r.NativeForwardCloses.Sent.ShouldBe(1); } /// /// Offline guard — deliberately NOT gated on the endpoint, so it runs in every build. Fails if any /// source file of this harness names a write-capable member, and, independently of the source text, /// if any compiled method of a harness type calls one. /// [Fact] public void The_harness_is_read_only_by_construction() { // Built by concatenation so this file's own text never matches itself. string[] forbidden = [ "Write" + "Async", "Encode" + "Value", "WriteBit" + "InDInt", "Send" + "Async", "AbCip" + "Driver", ]; // IL half (review r1 M1): method names match by prefix; the driver type matches by declaring type, // since the driver type's own members are named .ctor / ReadAsync / ..., never the type name. var forbiddenType = "AbCip" + "Driver"; var dir = Path.GetDirectoryName(ThisFile())!; var sources = Directory.GetFiles(dir, "*.cs"); sources.ShouldContain(f => Path.GetFileName(f) == nameof(TransportDifferentialTests) + ".cs"); // Task 21's latency comparison and its run helper are part of the harness the press sees: pinned in scope, // so moving either out of this folder (or namespace) fails the guard instead of silently escaping it. sources.ShouldContain(f => Path.GetFileName(f) == nameof(PollLatencyComparisonTests) + ".cs"); sources.ShouldContain(f => Path.GetFileName(f) == nameof(PollLatencyRun) + ".cs"); var textHits = (from f in sources let text = File.ReadAllText(f) from token in forbidden where text.Contains(token, StringComparison.Ordinal) select $"{Path.GetFileName(f)} names {token}").ToList(); textHits.ShouldBeEmpty(); var harnessTypes = typeof(TransportDifferentialTests).Assembly.GetTypes() .Where(t => t.Namespace == typeof(TransportDifferentialTests).Namespace).ToList(); harnessTypes.ShouldContain(typeof(TransportDifferentialTests)); harnessTypes.ShouldContain(typeof(PollLatencyComparisonTests)); harnessTypes.ShouldContain(typeof(PollLatencyRun)); var callHits = (from t in harnessTypes from m in MethodsOf(t) from callee in CalleesOf(m) where forbidden.Any(f => callee.Name.StartsWith(f, StringComparison.Ordinal)) || callee.DeclaringType?.Name.StartsWith(forbiddenType, StringComparison.Ordinal) == true select $"{t.Name}.{m.Name} calls {callee.DeclaringType?.Name}.{callee.Name}").ToList(); callHits.ShouldBeEmpty(); } /// /// Offline falsifiability check of the verdicts: a harness whose comparison could never say NO would /// pass every live run vacuously. /// [Fact] public void The_comparison_flags_every_divergence_and_excuses_only_a_native_type_mismatch() { var dint = new SelectedTag("T", AbCipDataType.DInt, 1, false, "scalar"); var ok = new ReadObservation((int)AbStatus.Ok, null, [1, 0, 0, 0], 1); Compare(dint, ok, ok with { }).ShouldBe("yes"); Compare(dint, ok, ok with { Raw = [2, 0, 0, 0] }).ShouldBe("**NO: raw**"); Compare(dint, ok, ok with { Raw = null }).ShouldBe("**NO: raw**"); Compare(dint, ok, ok with { Decoded = 2 }).ShouldBe("**NO: value**"); Compare(dint, ok, new ReadObservation((int)AbStatus.NotFound, "NotFound", null, null)).ShouldBe("**NO: status**"); Compare(dint, ok, ok with { Error = "boom" }).ShouldBe("**NO: error**"); var notFound = new ReadObservation((int)AbStatus.NotFound, "NotFound", null, null); Compare(dint, notFound, notFound with { }).ShouldBe("yes"); Compare(dint, ok, new ReadObservation((int)AbStatus.TypeMismatch, null, [1, 0, 0, 0], null)) .ShouldStartWith("expected"); // Only that direction is excused: libplctag refusing what native accepts is a real divergence. Compare(dint, new ReadObservation((int)AbStatus.TypeMismatch, null, [1, 0, 0, 0], null), ok) .ShouldBe("**NO: status**"); var array = new SelectedTag("A", AbCipDataType.Real, 2, true, "array"); var arr = new ReadObservation((int)AbStatus.Ok, null, new byte[8], new[] { float.NaN, 1f }); Compare(array, arr, arr with { Decoded = new[] { float.NaN, 1f } }).ShouldBe("yes"); Compare(array, arr, arr with { Decoded = new[] { float.NaN, 2f } }).ShouldBe("**NO: value**"); } /// /// Offline pin of the live-drift handling (T21 press pass 1): a value that moved between the two libplctag /// reads is compared structurally, while a value libplctag read stably and native read differently is still /// an unexpected divergence. Removing the live handling turns the first assertion into "**NO: raw**". /// [Fact] public void A_live_value_is_compared_structurally_and_a_stable_one_by_value() { var real = new SelectedTag("WW_M4ActualTemp", AbCipDataType.Real, 1, false, "scalar"); ReadObservation Obs(float v) => new((int)AbStatus.Ok, null, BitConverter.GetBytes(v), v); var lib = Obs(384.88684f); var probe = Obs(384.95f); var nat = Obs(385.13782f) with { TypeCode = 0xCA }; // Live: the libplctag value moved by itself, so a different native value is not a divergence. DifferentialRun.IsLive(lib, probe).ShouldBeTrue(); Compare(real, lib, nat, probe).ShouldBe("live"); Classify(Compare(real, lib, nat, probe)).ShouldBe(Verdict.Live); // ...but everything structural still has to match. Compare(real, lib, nat with { Status = (int)AbStatus.NotFound, Error = "NotFound" }, probe).ShouldBe("**NO: status**"); Compare(real, lib, nat with { Raw = new byte[8] }, probe).ShouldBe("**NO: raw length**"); Compare(real, lib, nat with { Raw = null }, probe).ShouldBe("**NO: raw length**"); Compare(real, lib with { TypeCode = 0xC4 }, nat, probe).ShouldBe("**NO: type code**"); Compare(real, lib, nat with { Decoded = 385 }, probe).ShouldBe("**NO: shape**"); Compare(real, lib, nat with { Decoded = new[] { 385f } }, probe).ShouldBe("**NO: shape**"); Compare(real, lib, nat with { DecodeError = "boom" }, probe).ShouldBe("**NO: decode**"); Classify("**NO: raw length**").ShouldBe(Verdict.Unexpected); var array = new SelectedTag("A", AbCipDataType.Real, 2, true, "array"); var arrLib = new ReadObservation((int)AbStatus.Ok, null, new byte[8], new[] { 1f, 2f }); var arrProbe = arrLib with { Raw = [1, 0, 0, 0, 0, 0, 0, 0], Decoded = new[] { 1.1f, 2f } }; // Element 0 moved (1 → 1.1), element 1 held at 2: native may read anything finite for 0, but must read 2 for 1. Compare(array, arrLib, arrLib with { Raw = [2, 0, 0, 0, 0, 0, 0, 0], Decoded = new[] { 3f, 2f } }, arrProbe).ShouldBe("live"); Compare(array, arrLib, arrLib with { Raw = [2, 0, 0, 0, 0, 0, 0, 0], Decoded = new[] { 3f, 4f } }, arrProbe) .ShouldBe("**NO: stable element 1**"); Compare(array, arrLib, arrLib with { Decoded = new[] { 1f, 2f, 3f } }, arrProbe).ShouldBe("**NO: shape**"); // Review r1 I1, the pass-1 shape: Real[5] where element 0 (400000) holds still and 1..4 move. A native defect // that shifts the stable element must FAIL even though the tag is live. var fout = new SelectedTag("Program:EM01_PowerUnit.__fout9FB1561FA00D73F5", AbCipDataType.Real, 5, true, "udt member"); ReadObservation Arr(params float[] v) => new((int)AbStatus.Ok, null, v.SelectMany(BitConverter.GetBytes).ToArray(), v); var foutLib = Arr(400000f, 4.2464733f, 5.2464733f, -6.7535267f, 4.2464733f); var foutProbe = Arr(400000f, 4.2461f, 5.2461f, -6.7539f, 4.2461f); Compare(fout, foutLib, Arr(400000f, 4.2457356f, 5.2457356f, -6.7542644f, 4.2457356f), foutProbe).ShouldBe("live"); var shifted = Compare(fout, foutLib, Arr(4.2457356f, 5.2457356f, -6.7542644f, 4.2457356f, 400000f), foutProbe); shifted.ShouldBe("**NO: stable element 0**"); Classify(shifted).ShouldBe(Verdict.Unexpected); Compare(fout, foutLib, Arr(400000f, float.NaN, 5.2457356f, -6.7542644f, 4.2457356f), foutProbe) .ShouldBe("**NO: non-finite element 1**"); Compare(real, lib, nat with { Decoded = float.PositiveInfinity }, probe).ShouldBe("**NO: non-finite element 0**"); // Stable: the two libplctag reads agree, so native reading anything else is a real decoding divergence. var stableProbe = lib with { }; DifferentialRun.IsLive(lib, stableProbe).ShouldBeFalse(); Compare(real, lib, nat, stableProbe).ShouldBe("**NO: raw**"); Classify(Compare(real, lib, nat, stableProbe)).ShouldBe(Verdict.Unexpected); Compare(real, lib, lib with { Decoded = 1f }, stableProbe).ShouldBe("**NO: value**"); Compare(real, lib, lib with { }, stableProbe).ShouldBe("yes"); Classify("yes").ShouldBe(Verdict.Equal); // A missing or failed probe is never live: the full value comparison applies. Compare(real, lib, nat, null).ShouldBe("**NO: raw**"); Compare(real, lib, nat, new ReadObservation((int)AbStatus.Timeout, "Timeout", null, null)).ShouldBe("**NO: raw**"); } private async Task RunOrSkipAsync() { if (DifferentialTarget.FromEnvironment() is null) Assert.Skip(DifferentialTarget.SkipReason); return await fixture.GetAsync(); } internal enum Verdict { Equal, Expected, Live, Unexpected } /// Which list a verdict string belongs to. Live tags are neither expected nor unexpected divergences. internal static Verdict Classify(string verdict) => verdict == "yes" ? Verdict.Equal : verdict == "live" ? Verdict.Live : verdict.StartsWith("expected", StringComparison.Ordinal) ? Verdict.Expected : Verdict.Unexpected; /// libplctag's second (volatility-probe) read of the tag; null ⇒ treated as stable. internal static string Compare(SelectedTag tag, ReadObservation lib, ReadObservation nat, ReadObservation? libProbe = null) { // Spec §5: native refuses a returned type that disagrees with the authored one; libplctag decodes it anyway. if (nat.Status == (int)AbStatus.TypeMismatch && lib.Status == (int)AbStatus.Ok) return "expected (native TypeMismatch, libplctag Ok)"; if (lib.Status != nat.Status) return "**NO: status**"; if (!lib.Succeeded || !nat.Succeeded) return lib.Succeeded == nat.Succeeded ? "yes" : "**NO: error**"; if (lib.DecodeError is not null || nat.DecodeError is not null) return "**NO: decode**"; if (DifferentialRun.IsLive(lib, libProbe)) { // The value moved between libplctag's own two reads, so native reading a third value proves nothing. // Everything that does not depend on the value must still agree. if (nat.Raw is null || nat.Raw.Length != lib.Raw!.Length) return "**NO: raw length**"; if (lib.TypeCode is { } lt && nat.TypeCode is { } nt && lt != nt) return "**NO: type code**"; if (tag.Type is null) return "live"; // raw-only (type unknown): nothing decoded to compare element-wise if (!DifferentialRun.SameShape(lib.Decoded, nat.Decoded)) return "**NO: shape**"; // Only what MOVED between libplctag's two reads is excused (review r1 I1): every element that held still // must read the same through native, so a native offset/stride/byte-order defect cannot hide as "live". return DifferentialRun.LiveElementMismatch(lib.Decoded, libProbe!.Decoded, nat.Decoded) ?? "live"; } if (lib.Raw is null || nat.Raw is null || !lib.Raw.AsSpan().SequenceEqual(nat.Raw)) return "**NO: raw**"; if (tag.Type is not null && !DifferentialRun.ValuesEqual(lib.Decoded, nat.Decoded)) return "**NO: value**"; return "yes"; } /// Every raw byte and every element (live tags: the evidence must be re-checkable by hand). private static string Detail(ReadObservation o, bool full = false) => o.Error ?? o.DecodeError ?? $"{o.Raw?.Length.ToString(CultureInfo.InvariantCulture) ?? "-"} byte(s) {Hex(o.Raw, full)} = {Show(o.Decoded, full)}"; private static string ProbeColumn(ReadObservation lib, ReadObservation? probe) => probe is null ? "-" : DifferentialRun.IsLive(lib, probe) ? "**live**" : probe.Succeeded && lib.Succeeded ? "stable" : StatusName(probe.Status); private static string Hex(byte[]? raw, bool full = false) => raw is null ? "-" : full || raw.Length <= 32 ? Convert.ToHexString(raw) : Convert.ToHexString(raw.AsSpan(0, 32)) + "…"; private static string Show(object? v, bool full = false) => v switch { null => "null", Array a => "[" + string.Join(",", a.Cast().Take(full ? int.MaxValue : 8).Select(x => Show(x))) + (!full && a.Length > 8 ? ",…" : "") + "]", IFormattable f => f.ToString(null, CultureInfo.InvariantCulture), _ => v.ToString() ?? "", }; private static string Shape(TransportObservations o, uint id) => o.TemplateErrors.TryGetValue(id, out var err) ? $"error {err}" : o.Shapes.TryGetValue(id, out var s) ? s is null ? "undecodable" : $"{s.TypeName} ({s.Members.Count} members, {s.TotalSize} B)" : "not read"; private static string StatusName(int status) => Enum.IsDefined(typeof(AbStatus), status) ? ((AbStatus)status).ToString() : status.ToString(CultureInfo.InvariantCulture); private static string ErrorSuffix(string? error) => error is null ? "" : $" (error: {error})"; private static void Log(string line) => TestContext.Current.TestOutputHelper?.WriteLine(line); private static string ThisFile([CallerFilePath] string path = "") => path; private static IEnumerable MethodsOf(Type t) { const BindingFlags all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; return t.GetMethods(all).Cast().Concat(t.GetConstructors(all)); } /// /// Every method a body's IL references by token (call / callvirt / newobj / ldftn). Scans every offset /// for a MethodDef / MemberRef / MethodSpec token — over-inclusive by design: a misaligned hit that /// happens to resolve can only add a false alarm, never hide a real call. /// private static IEnumerable CalleesOf(MethodBase m) { var il = m.GetMethodBody()?.GetILAsByteArray(); if (il is null) yield break; var typeArgs = m.DeclaringType is { IsGenericType: true } dt ? dt.GetGenericArguments() : null; var methodArgs = m.IsGenericMethod ? m.GetGenericArguments() : null; for (var i = 0; i + 4 <= il.Length; i++) { var token = BinaryPrimitives.ReadInt32LittleEndian(il.AsSpan(i, 4)); if ((token >> 24) is not (0x06 or 0x0A or 0x2B)) continue; MethodBase? callee; try { callee = m.Module.ResolveMethod(token, typeArgs, methodArgs); } catch (Exception) { continue; } if (callee is not null) yield return callee; } } }