1ae26675ac
Vendors the NORMATIVE (proto2) Eclipse Tahu sparkplug_b.proto and compiles it
in .Contracts with build-time Grpc.Tools, message-only (GrpcServices="None") --
Sparkplug rides MQTT, there is no gRPC service, so no Grpc.Core.Api is pulled in.
Provenance rides in the file header: repo, path, pinned commit
5736e404889d4b95910613040a99ba79589ffb13, permalink, git blob SHA-1
bf72ab5f..., content SHA-256 4432c5c4..., EPL-2.0, and a re-verify recipe. The
body below line 38 is byte-for-byte upstream; the blob SHA-1 matches the tree
entry at that commit, so the copy is provably genuine and not reconstructed.
Chose the proto2 file over upstream's sibling sparkplug_b_c_sharp.proto: the
sibling is a lossy proto3 restatement that drops explicit presence, and protoc
generates valid C# from proto2 into the identical namespace
(Org.Eclipse.Tahu.Protobuf, PascalCased from the package -- no
csharp_namespace option). Presence matters downstream: an NBIRTH legitimately
carries seq = 0 and a DATA metric legitimately omits `name`, so Has{Seq,Name,
Alias,IsNull,Datatype} is the difference between "absent" and "present, zero".
.Contracts stays transport-free: Google.Protobuf is a serialization dependency
with a framework-only graph, Grpc.Tools is PrivateAssets=all, and the resolved
graph is exactly {Google.Protobuf, Grpc.Tools, Core.Abstractions} -- no MQTTnet.
Codegen tests pin the round-trip, the namespace, the Sparkplug field numbers,
the DataType spec indices, and protoc's enum-name mangling (UInt64 -> Uint64,
UUID -> Uuid) that Task 17's datatype map has to spell correctly.
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
101 lines
4.3 KiB
C#
101 lines
4.3 KiB
C#
using Google.Protobuf;
|
|
using Org.Eclipse.Tahu.Protobuf;
|
|
using Shouldly;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Mqtt.Tests;
|
|
|
|
/// <summary>
|
|
/// Pins the vendored Eclipse Tahu <c>sparkplug_b</c> protobuf codegen: the generated types exist,
|
|
/// land in the namespace the rest of P2 is written against, and carry the Sparkplug-B field numbers
|
|
/// on the wire. Nothing here decodes a real payload — that is Task 16 (<c>SparkplugCodec</c> +
|
|
/// golden vectors); this suite exists so a broken/renamed/mangled vendored proto fails at the
|
|
/// codegen seam rather than as a mysterious decode failure three tasks later.
|
|
/// </summary>
|
|
public sealed class SparkplugProtoCodegenTests
|
|
{
|
|
[Fact]
|
|
public void GeneratedPayload_RoundTrips()
|
|
{
|
|
var p = new Payload { Seq = 3 };
|
|
p.Metrics.Add(new Payload.Types.Metric { Name = "Temperature", Alias = 5 });
|
|
|
|
var back = Payload.Parser.ParseFrom(p.ToByteArray());
|
|
|
|
back.Seq.ShouldBe(3ul);
|
|
back.Metrics[0].Name.ShouldBe("Temperature");
|
|
back.Metrics[0].Alias.ShouldBe(5ul);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tasks 16 / 20 / 25 name <c>Org.Eclipse.Tahu.Protobuf</c> explicitly, and the namespace is
|
|
/// derived (not declared) — protoc PascalCases the proto <c>package</c> because the vendored file
|
|
/// carries no <c>option csharp_namespace</c>. Editing the package line to "tidy" it would silently
|
|
/// rehome every generated type.
|
|
/// </summary>
|
|
[Fact]
|
|
public void GeneratedTypes_LiveInTheTahuPackageNamespace()
|
|
{
|
|
typeof(Payload).Namespace.ShouldBe("Org.Eclipse.Tahu.Protobuf");
|
|
Payload.Descriptor.File.Package.ShouldBe("org.eclipse.tahu.protobuf");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Field numbers are the wire contract with every Sparkplug edge node in the plant; a vendored
|
|
/// proto that was hand-edited or reconstructed rather than copied from upstream would most likely
|
|
/// drift here first.
|
|
/// </summary>
|
|
[Fact]
|
|
public void PayloadAndMetric_CarrySparkplugBFieldNumbers()
|
|
{
|
|
Payload.TimestampFieldNumber.ShouldBe(1);
|
|
Payload.MetricsFieldNumber.ShouldBe(2);
|
|
Payload.SeqFieldNumber.ShouldBe(3);
|
|
Payload.UuidFieldNumber.ShouldBe(4);
|
|
Payload.BodyFieldNumber.ShouldBe(5);
|
|
|
|
Payload.Types.Metric.NameFieldNumber.ShouldBe(1);
|
|
Payload.Types.Metric.AliasFieldNumber.ShouldBe(2);
|
|
Payload.Types.Metric.TimestampFieldNumber.ShouldBe(3);
|
|
Payload.Types.Metric.DatatypeFieldNumber.ShouldBe(4);
|
|
Payload.Types.Metric.IsNullFieldNumber.ShouldBe(7);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Task 17's datatype map keys off these indices. They are Sparkplug-spec constants, not protobuf
|
|
/// tags, so they are worth pinning independently of the field numbers above.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DataTypeEnum_MatchesSparkplugSpecIndices()
|
|
{
|
|
((int)DataType.Int8).ShouldBe(1);
|
|
((int)DataType.Int32).ShouldBe(3);
|
|
((int)DataType.Uint64).ShouldBe(8);
|
|
((int)DataType.Float).ShouldBe(9);
|
|
((int)DataType.Double).ShouldBe(10);
|
|
((int)DataType.Boolean).ShouldBe(11);
|
|
((int)DataType.String).ShouldBe(12);
|
|
((int)DataType.DateTime).ShouldBe(13);
|
|
((int)DataType.DataSet).ShouldBe(16);
|
|
((int)DataType.Template).ShouldBe(19);
|
|
((int)DataType.Int32Array).ShouldBe(24);
|
|
}
|
|
|
|
/// <summary>
|
|
/// protoc's C# generator RE-CASES enum member names: the proto's <c>UInt64</c> becomes
|
|
/// <c>Uint64</c> and <c>UUID</c> becomes <c>Uuid</c> (only the leading character of each
|
|
/// underscore-separated word survives as a capital). Task 17's datatype map has to spell them
|
|
/// the mangled way, so pin the mangling rather than letting it be rediscovered as a build error.
|
|
/// The wire-facing spelling is preserved on <c>OriginalName</c>, which is what
|
|
/// <c>DataType.Parser</c>-style JSON round-trips use.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DataTypeEnum_CSharpNamesAreProtocMangled_NotTheProtoSpelling()
|
|
{
|
|
Enum.GetNames<DataType>().ShouldContain("Uint64");
|
|
Enum.GetNames<DataType>().ShouldNotContain("UInt64");
|
|
Enum.GetNames<DataType>().ShouldContain("Uuid");
|
|
Enum.GetNames<DataType>().ShouldNotContain("UUID");
|
|
}
|
|
}
|