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
{
[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));
}
[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); }
}
}