using System.Runtime.CompilerServices; using Google.Protobuf; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests; /// /// Guards the committed Golden/*.bin Sparkplug-B wire vectors: they must still be exactly /// what produces, and they must actually reach the test /// binary's output directory. /// /// /// /// Without the drift guard the vectors would be write-once decoration — a re-vendored proto, /// a protoc upgrade, or an edited builder could change the encoding while every decode test /// kept passing against stale bytes. With it, the committed files are a genuine pin on the /// wire format the driver is expected to read. /// /// /// To regenerate (only after a deliberate, reviewed change to the vectors): /// OTOPCUA_SPARKPLUG_GOLDEN_REGEN=1 dotnet test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests /// --filter "FullyQualifiedName~SparkplugGoldenVectorTests", then review and commit the /// resulting .bin diff. /// /// public sealed class SparkplugGoldenVectorTests { private const string RegenEnvVar = "OTOPCUA_SPARKPLUG_GOLDEN_REGEN"; /// The committed NBIRTH vector is byte-identical to its builder's output. [Fact] public void Nbirth_CommittedBytes_MatchTheBuilder() => AssertMatchesBuilder(SparkplugGoldenPayloads.NbirthFile, SparkplugGoldenPayloads.BuildNbirth().ToByteArray()); /// The committed NDATA vector is byte-identical to its builder's output. [Fact] public void Ndata_CommittedBytes_MatchTheBuilder() => AssertMatchesBuilder(SparkplugGoldenPayloads.NdataFile, SparkplugGoldenPayloads.BuildNdata().ToByteArray()); /// /// Both vectors are present in the test binary's output directory — i.e. the csproj really does /// copy Golden/**. A missing copy item is invisible in source and turns every decode test /// into a at runtime. /// [Fact] public void GoldenVectors_AreCopiedToTheOutputDirectory() { File.Exists(SparkplugGoldenPayloads.PathTo(SparkplugGoldenPayloads.NbirthFile)).ShouldBeTrue(); File.Exists(SparkplugGoldenPayloads.PathTo(SparkplugGoldenPayloads.NdataFile)).ShouldBeTrue(); } /// Compares a committed vector to freshly built bytes, regenerating first when asked to. /// The vector's file name. /// The bytes the builder produces now. private static void AssertMatchesBuilder(string fileName, byte[] built) { if (Environment.GetEnvironmentVariable(RegenEnvVar) == "1") { // Write to BOTH the source tree (so the change can be committed) and the output directory // (so the assertion below reads what was just written rather than a stale copied file). var source = Path.Combine(SourceDirectory(), SparkplugGoldenPayloads.Directory, fileName); System.IO.Directory.CreateDirectory(Path.GetDirectoryName(source)!); File.WriteAllBytes(source, built); var output = SparkplugGoldenPayloads.PathTo(fileName); System.IO.Directory.CreateDirectory(Path.GetDirectoryName(output)!); File.WriteAllBytes(output, built); } var path = SparkplugGoldenPayloads.PathTo(fileName); File.Exists(path).ShouldBeTrue( $"Golden vector '{fileName}' is missing from the output directory. If the csproj copy item is " + $"intact, regenerate with {RegenEnvVar}=1."); File.ReadAllBytes(path).ShouldBe( built, $"Golden vector '{fileName}' no longer matches SparkplugGoldenPayloads. Either the builder or " + $"the vendored proto changed. If the change is intended, regenerate with {RegenEnvVar}=1 and " + "commit the .bin diff."); } /// This file's own source directory, resolved at compile time. /// Supplied by the compiler; never pass it. /// The directory holding the test sources. private static string SourceDirectory([CallerFilePath] string path = "") => Path.GetDirectoryName(path)!; }