using Google.Protobuf; using Google.Protobuf.Reflection; using RetainedGalaxyReflection = ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy.GalaxyRepositoryReflection; using ServedGalaxyReflection = ZB.MOM.WW.GalaxyRepository.Grpc.GalaxyRepositoryReflection; namespace ZB.MOM.WW.MxGateway.Tests.Contracts; public sealed class GalaxyContractParityTests { /// Guards the retained client proto against drift from the descriptor served by the gateway. [Fact] public void RetainedClientDescriptor_MatchesServedGatewayDescriptor() { FileDescriptorProto retained = Normalize(RetainedGalaxyReflection.Descriptor); FileDescriptorProto served = Normalize(ServedGalaxyReflection.Descriptor); AssertEquivalentDescriptors(retained, served); } /// Proves descriptor drift diagnostics name the first differing protobuf message. [Fact] public void DescriptorComparison_WhenMessageDiffers_ReportsDifferingMessage() { FileDescriptorProto retained = Normalize(RetainedGalaxyReflection.Descriptor); FileDescriptorProto changed = retained.Clone(); DescriptorProto changedMessage = changed.MessageType.First(message => message.Field.Count > 0); changedMessage.Field[0].Name += "_changed"; Xunit.Sdk.XunitException exception = Assert.Throws( () => AssertEquivalentDescriptors(retained, changed)); Assert.Contains(changedMessage.Name, exception.Message, StringComparison.Ordinal); } private static FileDescriptorProto Normalize(FileDescriptor descriptor) { FileDescriptorProto proto = descriptor.ToProto(); proto.SourceCodeInfo = null; proto.Name = string.Empty; if (proto.Options is not null) { proto.Options.CsharpNamespace = string.Empty; } return proto; } private static void AssertEquivalentDescriptors(FileDescriptorProto expected, FileDescriptorProto actual) { byte[] expectedBytes = SerializeDeterministically(expected); byte[] actualBytes = SerializeDeterministically(actual); if (expectedBytes.AsSpan().SequenceEqual(actualBytes)) { return; } throw new Xunit.Sdk.XunitException( $"Galaxy Repository descriptors differ at {DescribeFirstDifference(expected, actual)}."); } private static string DescribeFirstDifference(FileDescriptorProto expected, FileDescriptorProto actual) { int comparableMessageCount = Math.Min(expected.MessageType.Count, actual.MessageType.Count); for (int index = 0; index < comparableMessageCount; index++) { DescriptorProto expectedMessage = expected.MessageType[index]; DescriptorProto actualMessage = actual.MessageType[index]; if (!SerializeDeterministically(expectedMessage).AsSpan() .SequenceEqual(SerializeDeterministically(actualMessage))) { string messageName = string.IsNullOrEmpty(expectedMessage.Name) ? actualMessage.Name : expectedMessage.Name; return $"message '{Qualify(expected.Package, messageName)}'"; } } if (expected.MessageType.Count != actual.MessageType.Count) { return "the top-level message list"; } return "the normalized file descriptor"; } private static string Qualify(string package, string symbol) => string.IsNullOrEmpty(package) ? symbol : package + "." + symbol; private static byte[] SerializeDeterministically(IMessage message) { using MemoryStream stream = new(); using CodedOutputStream output = new(stream, leaveOpen: true) { Deterministic = true, }; message.WriteTo(output); output.Flush(); return stream.ToArray(); } }