using System.IO.MemoryMappedFiles; using System.Text; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Supervisor; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Tests; /// /// The Proxy-side must read the Host's MMF format /// (magic 'OFPC', 256-byte entries). This test writes a hand-crafted file that mimics /// the Host's layout exactly + asserts the reader decodes it correctly. Keeps the two /// codebases in lockstep on the wire format without needing to reference the net48 /// Host assembly from the net10 test project. /// [Trait("Category", "Unit")] public sealed class PostMortemReaderCompatibilityTests : IDisposable { private readonly string _tempPath = Path.Combine(Path.GetTempPath(), $"focas-mmf-compat-{Guid.NewGuid():N}.bin"); public void Dispose() { if (File.Exists(_tempPath)) File.Delete(_tempPath); } [Fact] public void Reader_parses_host_format_and_returns_entries_in_oldest_first_order() { const int magic = 0x4F465043; const int capacity = 5; const int headerBytes = 16; const int entryBytes = 256; const int messageOffset = 16; var fileBytes = headerBytes + capacity * entryBytes; using (var fs = new FileStream(_tempPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read)) { fs.SetLength(fileBytes); using var mmf = MemoryMappedFile.CreateFromFile(fs, null, fileBytes, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, leaveOpen: false); using var acc = mmf.CreateViewAccessor(0, fileBytes, MemoryMappedFileAccess.ReadWrite); acc.Write(0, magic); acc.Write(4, 1); acc.Write(8, capacity); acc.Write(12, 2); // writeIndex — next write would land at slot 2 void WriteEntry(int slot, long ts, long op, string msg) { var offset = headerBytes + slot * entryBytes; acc.Write(offset + 0, ts); acc.Write(offset + 8, op); var bytes = Encoding.UTF8.GetBytes(msg); acc.WriteArray(offset + messageOffset, bytes, 0, bytes.Length); acc.Write(offset + messageOffset + bytes.Length, (byte)0); } WriteEntry(0, 100, 1, "op-a"); WriteEntry(1, 200, 2, "op-b"); // Slots 2,3 unwritten (ts=0) — reader must skip. WriteEntry(4, 50, 9, "old-wrapped"); } var entries = new PostMortemReader(_tempPath).ReadAll(); entries.Length.ShouldBe(3); // writeIndex=2 means the ring walk starts at slot 2, so iteration order is 2→3→4→0→1. // Slots 2 and 3 are empty; 4 yields "old-wrapped"; then 0="op-a", 1="op-b". entries[0].Message.ShouldBe("old-wrapped"); entries[1].Message.ShouldBe("op-a"); entries[2].Message.ShouldBe("op-b"); } [Fact] public void Reader_returns_empty_when_file_missing() { new PostMortemReader(_tempPath + "-does-not-exist").ReadAll().ShouldBeEmpty(); } [Fact] public void Reader_returns_empty_when_magic_mismatches() { File.WriteAllBytes(_tempPath, new byte[1024]); new PostMortemReader(_tempPath).ReadAll().ShouldBeEmpty(); } }