using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; using Google.Protobuf.Reflection; using ZB.MOM.WW.ScadaBridge.Communication.Grpc; namespace ZB.MOM.WW.ScadaBridge.Communication.Tests.Grpc; /// Compiles source descriptors and compares the complete vendored wire contract. public class ProtoDescriptorContractTests { /// Verifies that the source descriptors match the vendored messages and services. /// A task that represents the asynchronous operation. [Fact] public async Task XC4_SourceDescriptorsMatchVendoredMessagesAndServices() { var compiled = await CompileAsync(); foreach (var generated in Generated()) Assert.Equal(CanonicalGenerated(generated), compiled.File.Single(f => f.Name == generated.Name)); } /// Verifies that the guard detects drift in names, types and services. /// The descriptor text before the change. /// The descriptor text after the change. /// A task that represents the asynchronous operation. [Theory] [InlineData("string site_identifier = 1;", "string renamed_site = 1;")] [InlineData("string site_identifier = 1;", "bytes site_identifier = 1;")] [InlineData("rpc Heartbeat(HeartbeatDto)", "rpc RenamedHeartbeat(HeartbeatDto)")] public async Task GuardDetectsNameTypeAndServiceDrift(string before, string after) { var compiled = await CompileAsync(before, after); var generated = CentralControlReflection.Descriptor; Assert.NotEqual(CanonicalGenerated(generated), compiled.File.Single(f => f.Name == generated.Name)); } // C# embeds descriptors with implicit JSON names omitted; descriptor_set_out expands // them. Compare effective names so explicit json_name drift remains detectable too. private static FileDescriptorProto CanonicalGenerated(FileDescriptor file) { var proto = file.ToProto(); foreach (var message in proto.MessageType) FillJsonNames(message, file.MessageTypes.Single(m => m.Name == message.Name)); return proto; } private static void FillJsonNames(DescriptorProto proto, MessageDescriptor descriptor) { foreach (var field in proto.Field) field.JsonName = descriptor.FindFieldByNumber(field.Number).JsonName; foreach (var nested in proto.NestedType) FillJsonNames(nested, descriptor.NestedTypes.Single(m => m.Name == nested.Name)); } private static FileDescriptor[] Generated() => [SitestreamReflection.Descriptor, SiteCommandReflection.Descriptor, CentralControlReflection.Descriptor]; private static async Task CompileAsync(string? before = null, string? after = null) { var tools = typeof(ProtoDescriptorContractTests).Assembly.GetCustomAttributes() .Single(a => a.Key == "GrpcToolsRoot").Value!; var platform = OperatingSystem.IsMacOS() ? "macosx_x64" : OperatingSystem.IsWindows() ? (RuntimeInformation.ProcessArchitecture == Architecture.X86 ? "windows_x86" : "windows_x64") : RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "linux_arm64" : RuntimeInformation.ProcessArchitecture == Architecture.X86 ? "linux_x86" : "linux_x64"; var executable = Path.Combine(tools, "tools", platform, OperatingSystem.IsWindows() ? "protoc.exe" : "protoc"); var directory = Path.Combine(Path.GetTempPath(), "scadabridge-proto-contract-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(Path.Combine(directory, "Protos")); try { foreach (var file in Directory.GetFiles(Path.Combine(AppContext.BaseDirectory, "ProtoSources", "Protos"), "*.proto")) { var source = await File.ReadAllTextAsync(file); if (before is not null && Path.GetFileName(file) == "central_control.proto") { Assert.Contains(before, source); source = source.Replace(before, after, StringComparison.Ordinal); } await File.WriteAllTextAsync(Path.Combine(directory, "Protos", Path.GetFileName(file)), source); } var output = Path.Combine(directory, "descriptor.bin"); var start = new ProcessStartInfo(executable) { WorkingDirectory = directory, RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; start.ArgumentList.Add("--proto_path=" + directory); start.ArgumentList.Add("--proto_path=" + Path.Combine(tools, "build", "native", "include")); start.ArgumentList.Add("--descriptor_set_out=" + output); foreach (var file in Directory.GetFiles(Path.Combine(directory, "Protos"), "*.proto").Order()) start.ArgumentList.Add("Protos/" + Path.GetFileName(file)); using var process = Process.Start(start)!; var stderr = process.StandardError.ReadToEndAsync(); var stdout = process.StandardOutput.ReadToEndAsync(); try { await process.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(30)); } catch { process.Kill(entireProcessTree: true); await process.WaitForExitAsync(); throw; } Assert.True(process.ExitCode == 0, await stderr + await stdout); return FileDescriptorSet.Parser.ParseFrom(await File.ReadAllBytesAsync(output)); } finally { Directory.Delete(directory, recursive: true); } } }