From 9380af2486d71cc621270f8415765b7091a18127 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Fri, 12 Dec 2025 11:54:07 +0000 Subject: [PATCH 1/5] Proof of concept for public message types - Expose RemoteConfigMessage on the public API - Add supporting types for remote config - Update unshipped public API --- .../.publicApi/PublicAPI.Unshipped.txt | 6 ++++ .../Internal/Messages/RemoteConfigMessage.cs | 17 ----------- .../AgentConfigDictionary.cs | 14 ++++++++++ .../RemoteConfiguration/AgentConfigFile.cs | 28 +++++++++++++++++++ .../RemoteConfigMessage.cs | 24 ++++++++++++++++ 5 files changed, 72 insertions(+), 17 deletions(-) delete mode 100644 src/OpenTelemetry.OpAmp.Client/Internal/Messages/RemoteConfigMessage.cs create mode 100644 src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigDictionary.cs create mode 100644 src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs create mode 100644 src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs diff --git a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt index 1780095e3a..0dbdd24710 100644 --- a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt @@ -1,5 +1,11 @@ +OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage +OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.AgentConfigMap.get -> OpenTelemetry.OpAmp.Client.Messages.AgentConfigDictionary! OpenTelemetry.OpAmp.Client.Listeners.IOpAmpListener OpenTelemetry.OpAmp.Client.Listeners.IOpAmpListener.HandleMessage(TMessage! message) -> void +OpenTelemetry.OpAmp.Client.Messages.AgentConfigDictionary +OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile +OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.Body.get -> byte[]! +OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.ContentType.get -> string? OpenTelemetry.OpAmp.Client.Messages.OpAmpMessage OpenTelemetry.OpAmp.Client.Messages.OpAmpMessage.OpAmpMessage() -> void OpenTelemetry.OpAmp.Client.OpAmpClient diff --git a/src/OpenTelemetry.OpAmp.Client/Internal/Messages/RemoteConfigMessage.cs b/src/OpenTelemetry.OpAmp.Client/Internal/Messages/RemoteConfigMessage.cs deleted file mode 100644 index 92d4eaa627..0000000000 --- a/src/OpenTelemetry.OpAmp.Client/Internal/Messages/RemoteConfigMessage.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using OpAmp.Proto.V1; -using OpenTelemetry.OpAmp.Client.Messages; - -namespace OpenTelemetry.OpAmp.Client.Internal.Listeners.Messages; - -internal class RemoteConfigMessage : OpAmpMessage -{ - public RemoteConfigMessage(AgentRemoteConfig agentRemoteConfig) - { - this.RemoteConfig = agentRemoteConfig; - } - - public AgentRemoteConfig RemoteConfig { get; set; } -} diff --git a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigDictionary.cs b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigDictionary.cs new file mode 100644 index 0000000000..8bf46caad4 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigDictionary.cs @@ -0,0 +1,14 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.OpAmp.Client.Messages; + +/// +/// Represents a collection of agent configuration files, indexed by agent name. +/// +public class AgentConfigDictionary : Dictionary +{ + internal AgentConfigDictionary() + { + } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs new file mode 100644 index 0000000000..9b9318ec12 --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs @@ -0,0 +1,28 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.OpAmp.Client.Messages; + +/// +/// Represents an agent configuration file. +/// +public class AgentConfigFile +{ + internal AgentConfigFile(global::OpAmp.Proto.V1.AgentConfigFile agentConfigFile) + { + this.Body = agentConfigFile.Body?.ToByteArray() ?? []; + this.ContentType = agentConfigFile.ContentType; + } + + /// + /// Gets the raw bytes of the configuration file. + /// +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Body { get; } +#pragma warning restore CA1819 // Properties should not return arrays + + /// + /// Gets the MIME Content-Type that describes the data contained in the ". + /// + public string? ContentType { get; } +} diff --git a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs new file mode 100644 index 0000000000..a052e03a5f --- /dev/null +++ b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs @@ -0,0 +1,24 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using OpAmp.Proto.V1; + +namespace OpenTelemetry.OpAmp.Client.Messages; + +/// +/// Represents a server-to-agent remote configuration message. +/// +public class RemoteConfigMessage : OpAmpMessage +{ + internal RemoteConfigMessage(AgentRemoteConfig agentRemoteConfig) + { + foreach (var config in agentRemoteConfig.Config.ConfigMap) + { + this.AgentConfigMap ??= []; + this.AgentConfigMap[config.Key] = new AgentConfigFile(config.Value); + } + } + + /// + public AgentConfigDictionary AgentConfigMap { get; } = []; +} From f9f79c3e36df827821beb5dc005780482b9532ea Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 15 Dec 2025 09:51:08 +0000 Subject: [PATCH 2/5] Refine public API for remote config message --- .../.publicApi/PublicAPI.Unshipped.txt | 10 ++-- .../AgentConfigDictionary.cs | 14 ----- .../RemoteConfiguration/AgentConfigFile.cs | 58 ++++++++++++++++--- .../RemoteConfigMessage.cs | 15 +++-- 4 files changed, 67 insertions(+), 30 deletions(-) delete mode 100644 src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigDictionary.cs diff --git a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt index 0dbdd24710..fbb6124e10 100644 --- a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt @@ -1,13 +1,15 @@ -OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage -OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.AgentConfigMap.get -> OpenTelemetry.OpAmp.Client.Messages.AgentConfigDictionary! OpenTelemetry.OpAmp.Client.Listeners.IOpAmpListener OpenTelemetry.OpAmp.Client.Listeners.IOpAmpListener.HandleMessage(TMessage! message) -> void -OpenTelemetry.OpAmp.Client.Messages.AgentConfigDictionary OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile -OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.Body.get -> byte[]! +OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.BodyLength.get -> int OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.ContentType.get -> string? +OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.GetBodyBytes() -> byte[]! +OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.Name.get -> string! +OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.TryGetBody(System.Span destination, out int bytesWritten) -> bool OpenTelemetry.OpAmp.Client.Messages.OpAmpMessage OpenTelemetry.OpAmp.Client.Messages.OpAmpMessage.OpAmpMessage() -> void +OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage +OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.AgentConfigMap.get -> System.Collections.Generic.IReadOnlyDictionary! OpenTelemetry.OpAmp.Client.OpAmpClient OpenTelemetry.OpAmp.Client.OpAmpClient.Dispose() -> void OpenTelemetry.OpAmp.Client.OpAmpClient.OpAmpClient(System.Action? configure = null) -> void diff --git a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigDictionary.cs b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigDictionary.cs deleted file mode 100644 index 8bf46caad4..0000000000 --- a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigDictionary.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -namespace OpenTelemetry.OpAmp.Client.Messages; - -/// -/// Represents a collection of agent configuration files, indexed by agent name. -/// -public class AgentConfigDictionary : Dictionary -{ - internal AgentConfigDictionary() - { - } -} diff --git a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs index 9b9318ec12..c637eee630 100644 --- a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs +++ b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs @@ -1,6 +1,8 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using Google.Protobuf; + namespace OpenTelemetry.OpAmp.Client.Messages; /// @@ -8,21 +10,63 @@ namespace OpenTelemetry.OpAmp.Client.Messages; /// public class AgentConfigFile { - internal AgentConfigFile(global::OpAmp.Proto.V1.AgentConfigFile agentConfigFile) + private readonly ByteString body; + + internal AgentConfigFile(string name, global::OpAmp.Proto.V1.AgentConfigFile agentConfigFile) { - this.Body = agentConfigFile.Body?.ToByteArray() ?? []; + this.body = agentConfigFile.Body ?? ByteString.Empty; this.ContentType = agentConfigFile.ContentType; + this.Name = name; } /// - /// Gets the raw bytes of the configuration file. + /// Gets the length, in bytes, of the configuration file body. /// -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Body { get; } -#pragma warning restore CA1819 // Properties should not return arrays + public int BodyLength => this.body.Length; /// - /// Gets the MIME Content-Type that describes the data contained in the ". + /// Gets the MIME Content-Type that describes the data contained in the body of the remote configuration file. /// public string? ContentType { get; } + + /// + /// Gets the name of this configuration file. + /// + public string Name { get; } + + /// + /// Returns the configuration file body as a byte array. + /// + /// A byte array containing the contents of the message body. The array is empty if the body has no content. + public byte[] GetBodyBytes() => this.body.ToByteArray() ?? []; + + /// + /// Attempts to copy the configuration file body to the specified destination buffer. + /// + /// If the body is empty, no data is written and the method returns true with set to + /// 0. If the destination buffer is too small to hold the body content, no data is written, is set to + /// 0, and the method returns false. + /// The buffer that receives the body bytes. Must be large enough to hold the entire body content. + /// When this method returns, contains the number of bytes successfully written to the destination buffer. + /// true if the body was successfully copied to the destination buffer or if the body is empty; otherwise, false. + public bool TryGetBody(Span destination, out int bytesWritten) + { + if (this.body.IsEmpty) + { + bytesWritten = 0; + return true; + } + + try + { + this.body.Span.CopyTo(destination); + bytesWritten = this.body.Length; + return true; + } + catch (ArgumentException) + { + bytesWritten = 0; + return false; + } + } } diff --git a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs index a052e03a5f..dfcc21fd65 100644 --- a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs +++ b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs @@ -6,19 +6,24 @@ namespace OpenTelemetry.OpAmp.Client.Messages; /// -/// Represents a server-to-agent remote configuration message. +/// Represents an OpAMP server-to-agent remote configuration message. /// public class RemoteConfigMessage : OpAmpMessage { + private readonly Dictionary agentConfigMap; + internal RemoteConfigMessage(AgentRemoteConfig agentRemoteConfig) { + this.agentConfigMap = new Dictionary(agentRemoteConfig.Config.ConfigMap.Count, StringComparer.Ordinal); + foreach (var config in agentRemoteConfig.Config.ConfigMap) { - this.AgentConfigMap ??= []; - this.AgentConfigMap[config.Key] = new AgentConfigFile(config.Value); + this.agentConfigMap[config.Key] = new AgentConfigFile(config.Key, config.Value); } } - /// - public AgentConfigDictionary AgentConfigMap { get; } = []; + /// + /// Gets a dictionary of agent configuration files, keyed by the name of the configuration file. + /// + public IReadOnlyDictionary AgentConfigMap => this.agentConfigMap; } From 30b794c1b60f689c343174ccc62d78c989927e60 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 15 Dec 2025 13:54:20 +0000 Subject: [PATCH 3/5] Support config hash, PR feedback and add tests --- .../.publicApi/PublicAPI.Unshipped.txt | 4 + .../RemoteConfiguration/AgentConfigFile.cs | 12 +- .../RemoteConfigMessage.cs | 60 ++++- .../Messages/RemoteConfigMessageTests.cs | 245 ++++++++++++++++++ 4 files changed, 313 insertions(+), 8 deletions(-) create mode 100644 test/OpenTelemetry.OpAmp.Client.Tests/Messages/RemoteConfigMessageTests.cs diff --git a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt index fbb6124e10..29279c6d1e 100644 --- a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt @@ -10,6 +10,10 @@ OpenTelemetry.OpAmp.Client.Messages.OpAmpMessage OpenTelemetry.OpAmp.Client.Messages.OpAmpMessage.OpAmpMessage() -> void OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.AgentConfigMap.get -> System.Collections.Generic.IReadOnlyDictionary! +OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.GetConfigHashBytes() -> byte[]! +OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.GetConfigHashUtf8String() -> string! +OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.HashLength.get -> int +OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.TryGetConfigHash(System.Span destination, out int bytesWritten) -> bool OpenTelemetry.OpAmp.Client.OpAmpClient OpenTelemetry.OpAmp.Client.OpAmpClient.Dispose() -> void OpenTelemetry.OpAmp.Client.OpAmpClient.OpAmpClient(System.Action? configure = null) -> void diff --git a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs index c637eee630..de0b8f7d42 100644 --- a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs +++ b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs @@ -57,16 +57,14 @@ public bool TryGetBody(Span destination, out int bytesWritten) return true; } - try - { - this.body.Span.CopyTo(destination); - bytesWritten = this.body.Length; - return true; - } - catch (ArgumentException) + if (destination.Length < this.body.Length) { bytesWritten = 0; return false; } + + this.body.Span.CopyTo(destination); + bytesWritten = this.body.Length; + return true; } } diff --git a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs index dfcc21fd65..16086189e6 100644 --- a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs +++ b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using Google.Protobuf; using OpAmp.Proto.V1; namespace OpenTelemetry.OpAmp.Client.Messages; @@ -11,6 +12,7 @@ namespace OpenTelemetry.OpAmp.Client.Messages; public class RemoteConfigMessage : OpAmpMessage { private readonly Dictionary agentConfigMap; + private readonly ByteString configHash; internal RemoteConfigMessage(AgentRemoteConfig agentRemoteConfig) { @@ -18,12 +20,68 @@ internal RemoteConfigMessage(AgentRemoteConfig agentRemoteConfig) foreach (var config in agentRemoteConfig.Config.ConfigMap) { - this.agentConfigMap[config.Key] = new AgentConfigFile(config.Key, config.Value); + if (config.Value is not null) + { + this.agentConfigMap[config.Key] = new AgentConfigFile(config.Key, config.Value); + } } + + this.configHash = agentRemoteConfig.ConfigHash; } /// /// Gets a dictionary of agent configuration files, keyed by the name of the configuration file. /// public IReadOnlyDictionary AgentConfigMap => this.agentConfigMap; + + /// + /// Gets the length, in bytes, of the configuration hash. + /// + public int HashLength => this.configHash.Length; + + /// + /// Returns the configuration hash as a byte array. + /// + /// A array containing the hash of the remote configuration. + public byte[] GetConfigHashBytes() => this.configHash.ToByteArray(); + + /// + /// Returns the configuration hash as a UTF-8 string. + /// + /// A representing the UTF-8 encoded configuration hash. + public string GetConfigHashUtf8String() => this.configHash.ToStringUtf8(); + + /// + /// Attempts to copy the configuration hash to the specified destination buffer. + /// + /// If the hash is empty, no data is written and the method returns true with set to + /// 0. If the destination buffer is too small to hold the hash, no data is written, is set to + /// 0, and the method returns false. + /// The buffer that receives the hash bytes. Must be large enough to hold the entire hash content. + /// When this method returns, contains the number of bytes successfully written to the destination buffer. + /// true if the hash was successfully copied to the destination buffer or if the hash is empty; otherwise, false. + public bool TryGetConfigHash(Span destination, out int bytesWritten) + { + if (this.configHash is null) + { + bytesWritten = 0; + return false; + } + + if (this.configHash.IsEmpty) + { + bytesWritten = 0; + return true; + } + + if (destination.Length < this.configHash.Length) + { + bytesWritten = 0; + return false; + } + + this.configHash.Span.CopyTo(destination); + bytesWritten = this.configHash.Length; + return true; + } } diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/Messages/RemoteConfigMessageTests.cs b/test/OpenTelemetry.OpAmp.Client.Tests/Messages/RemoteConfigMessageTests.cs new file mode 100644 index 0000000000..2612bb96d5 --- /dev/null +++ b/test/OpenTelemetry.OpAmp.Client.Tests/Messages/RemoteConfigMessageTests.cs @@ -0,0 +1,245 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Text; +using Google.Protobuf; +using Xunit; + +namespace OpenTelemetry.OpAmp.Client.Tests.Messages; + +public class RemoteConfigMessageTests +{ + private const string JsonString = "{ \"myKey\": \"this is a value\" }"; + private const string YamlString = "enabled: true"; + private const string HashString = "dummy-hash"; + private const string Config1Name = "config1"; + private const string Config2Name = "config2"; + private const string JsonContentType = "application/json"; + private const string YamlContentType = "application/yaml"; + + [Fact] + public void Constructor_WithValidConfig_InitializesConfigMap() + { + // Arrange + var agentRemoteConfig = this.CreateAgentRemoteConfig(); + + // Act + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + + // Assert + Assert.Equal(2, remoteConfigMessage.AgentConfigMap.Count); + Assert.True(remoteConfigMessage.AgentConfigMap.ContainsKey(Config1Name)); + Assert.True(remoteConfigMessage.AgentConfigMap.ContainsKey(Config2Name)); + } + + [Fact] + public void Constructor_WithEmptyConfigMap_InitializesEmptyDictionary() + { + // Arrange + var agentRemoteConfig = new global::OpAmp.Proto.V1.AgentRemoteConfig + { + Config = new global::OpAmp.Proto.V1.AgentConfigMap(), + ConfigHash = ByteString.CopyFromUtf8(HashString), + }; + + // Act + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + + // Assert + Assert.Empty(remoteConfigMessage.AgentConfigMap); + } + + [Fact] + public void GetConfigHashBytes_WithValidHash_ReturnsExpectedBytes() + { + // Arrange + var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + + // Act + var hashBytes = remoteConfigMessage.GetConfigHashBytes(); + + // Assert + Assert.Equal(Encoding.UTF8.GetByteCount(HashString), hashBytes.Length); + Assert.Equal(HashString, Encoding.UTF8.GetString(hashBytes)); + } + + [Fact] + public void GetConfigHashUtf8String_WithValidHash_ReturnsExpectedString() + { + // Arrange + var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + + // Act + var hashString = remoteConfigMessage.GetConfigHashUtf8String(); + + // Assert + Assert.Equal(HashString, hashString); + } + + [Fact] + public void HashLength_WithValidHash_ReturnsExpectedLength() + { + // Arrange + var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + + // Act & Assert + Assert.Equal(Encoding.UTF8.GetByteCount(HashString), remoteConfigMessage.HashLength); + Assert.Equal(remoteConfigMessage.GetConfigHashBytes().Length, remoteConfigMessage.HashLength); + } + + [Fact] + public void TryGetConfigHash_WithSufficientBuffer_ReturnsTrueAndWritesBytes() + { + // Arrange + var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + Span hashSpan = stackalloc byte[remoteConfigMessage.HashLength]; + + // Act + var result = remoteConfigMessage.TryGetConfigHash(hashSpan, out int bytesWritten); + + // Assert + Assert.True(result); + Assert.Equal(remoteConfigMessage.HashLength, bytesWritten); + Assert.Equal(HashString, Encoding.UTF8.GetString(hashSpan.ToArray())); + } + + [Fact] + public void TryGetConfigHash_WithInsufficientBuffer_ReturnsFalse() + { + // Arrange + var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + Span hashSpan = stackalloc byte[remoteConfigMessage.HashLength - 1]; + + // Act + var result = remoteConfigMessage.TryGetConfigHash(hashSpan, out int bytesWritten); + + // Assert + Assert.False(result); + Assert.Equal(0, bytesWritten); + } + + [Theory] + [InlineData(Config1Name, JsonContentType, JsonString)] + [InlineData(Config2Name, YamlContentType, YamlString)] + public void AgentConfigFile_Properties_ReturnExpectedValues(string configName, string contentType, string bodyContent) + { + // Arrange + var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + + // Act + var configFile = remoteConfigMessage.AgentConfigMap[configName]; + + // Assert + Assert.Equal(configName, configFile.Name); + Assert.Equal(contentType, configFile.ContentType); + Assert.Equal(Encoding.UTF8.GetByteCount(bodyContent), configFile.BodyLength); + } + + [Fact] + public void AgentConfigFile_GetBodyBytes_ReturnsExpectedContent() + { + // Arrange + var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + + // Act + var config1 = remoteConfigMessage.AgentConfigMap[Config1Name]; + var bodyBytes = config1.GetBodyBytes(); + + // Assert + Assert.Equal(bodyBytes.Length, config1.BodyLength); + Assert.Equal(JsonString, Encoding.UTF8.GetString(bodyBytes)); + } + + [Fact] + public void AgentConfigFile_TryGetBody_WithSufficientBuffer_ReturnsTrueAndWritesBytes() + { + // Arrange + var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + var config1 = remoteConfigMessage.AgentConfigMap[Config1Name]; + Span bodySpan = stackalloc byte[config1.BodyLength]; + + // Act + var result = config1.TryGetBody(bodySpan, out int bytesWritten); + + // Assert + Assert.True(result); + Assert.Equal(config1.BodyLength, bytesWritten); + Assert.Equal(JsonString, Encoding.UTF8.GetString(bodySpan.ToArray())); + } + + [Fact] + public void AgentConfigFile_TryGetBody_WithInsufficientBuffer_ReturnsFalse() + { + // Arrange + var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + var config1 = remoteConfigMessage.AgentConfigMap[Config1Name]; + Span bodySpan = stackalloc byte[config1.BodyLength - 1]; + + // Act + var result = config1.TryGetBody(bodySpan, out int bytesWritten); + + // Assert + Assert.False(result); + Assert.Equal(0, bytesWritten); + } + +#if NET + [Fact] + public void AgentConfigFile_JsonContent_CanBeDeserialized() + { + // Arrange + var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + var config1 = remoteConfigMessage.AgentConfigMap[Config1Name]; + Span bodySpan = stackalloc byte[config1.BodyLength]; + Assert.True(config1.TryGetBody(bodySpan, out _)); + + // Act + var config = System.Text.Json.JsonSerializer.Deserialize(bodySpan); + + // Assert + Assert.NotNull(config); + Assert.Equal("this is a value", config!.MyKey); + } +#endif + + private global::OpAmp.Proto.V1.AgentRemoteConfig CreateAgentRemoteConfig() + { + var configMap = new global::OpAmp.Proto.V1.AgentConfigMap(); + + configMap.ConfigMap.Add(Config1Name, new global::OpAmp.Proto.V1.AgentConfigFile + { + Body = ByteString.CopyFromUtf8(JsonString), + ContentType = JsonContentType, + }); + + configMap.ConfigMap.Add(Config2Name, new global::OpAmp.Proto.V1.AgentConfigFile + { + Body = ByteString.CopyFromUtf8(YamlString), + ContentType = YamlContentType, + }); + + return new global::OpAmp.Proto.V1.AgentRemoteConfig + { + Config = configMap, + ConfigHash = ByteString.CopyFromUtf8(HashString), + }; + } + +#if NET + private class Config + { + [System.Text.Json.Serialization.JsonPropertyName("myKey")] + public string? MyKey { get; set; } + } +#endif +} From d42f72fba2df059fd9c9351c162dbdc04be3618e Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 15 Dec 2025 13:57:08 +0000 Subject: [PATCH 4/5] Update changelog --- src/OpenTelemetry.OpAmp.Client/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md index 63805db847..b2e9297081 100644 --- a/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md +++ b/src/OpenTelemetry.OpAmp.Client/CHANGELOG.md @@ -5,11 +5,16 @@ * Add setting to configure the factory used to create `HttpClient` instances used for the OpAMP Plain HTTP transport. ([#3589](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3589)) + * Add support for subscribing and unsubscribing to messages from the OpAMP server. ([#3593](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3593)) + * Clean up directories and namespaces for public API. ([#3612](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3612)) +* Expose public `RemoteConfigMessage`. + ([#3614](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/3614)) + ## 0.1.0-alpha.3 Released 2025-Nov-13 From 727de3e3da24bbb8e1f79cfa122057e81ba6b516 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 15 Dec 2025 15:05:25 +0000 Subject: [PATCH 5/5] Simplify the public API and update tests --- .../.publicApi/PublicAPI.Unshipped.txt | 9 +- .../RemoteConfiguration/AgentConfigFile.cs | 38 +---- .../RemoteConfigMessage.cs | 51 +----- .../Messages/RemoteConfigMessageTests.cs | 152 +++++++++++------- 4 files changed, 97 insertions(+), 153 deletions(-) diff --git a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt index 29279c6d1e..c6ffd9adba 100644 --- a/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.OpAmp.Client/.publicApi/PublicAPI.Unshipped.txt @@ -1,19 +1,14 @@ OpenTelemetry.OpAmp.Client.Listeners.IOpAmpListener OpenTelemetry.OpAmp.Client.Listeners.IOpAmpListener.HandleMessage(TMessage! message) -> void OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile -OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.BodyLength.get -> int +OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.Body.get -> System.ReadOnlySpan OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.ContentType.get -> string? -OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.GetBodyBytes() -> byte[]! OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.Name.get -> string! -OpenTelemetry.OpAmp.Client.Messages.AgentConfigFile.TryGetBody(System.Span destination, out int bytesWritten) -> bool OpenTelemetry.OpAmp.Client.Messages.OpAmpMessage OpenTelemetry.OpAmp.Client.Messages.OpAmpMessage.OpAmpMessage() -> void OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.AgentConfigMap.get -> System.Collections.Generic.IReadOnlyDictionary! -OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.GetConfigHashBytes() -> byte[]! -OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.GetConfigHashUtf8String() -> string! -OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.HashLength.get -> int -OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.TryGetConfigHash(System.Span destination, out int bytesWritten) -> bool +OpenTelemetry.OpAmp.Client.Messages.RemoteConfigMessage.ConfigHash.get -> System.ReadOnlySpan OpenTelemetry.OpAmp.Client.OpAmpClient OpenTelemetry.OpAmp.Client.OpAmpClient.Dispose() -> void OpenTelemetry.OpAmp.Client.OpAmpClient.OpAmpClient(System.Action? configure = null) -> void diff --git a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs index de0b8f7d42..43f9f93a13 100644 --- a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs +++ b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/AgentConfigFile.cs @@ -20,9 +20,9 @@ internal AgentConfigFile(string name, global::OpAmp.Proto.V1.AgentConfigFile age } /// - /// Gets the length, in bytes, of the configuration file body. + /// Gets the byte content of the configuration file. /// - public int BodyLength => this.body.Length; + public ReadOnlySpan Body => this.body.Span; /// /// Gets the MIME Content-Type that describes the data contained in the body of the remote configuration file. @@ -33,38 +33,4 @@ internal AgentConfigFile(string name, global::OpAmp.Proto.V1.AgentConfigFile age /// Gets the name of this configuration file. /// public string Name { get; } - - /// - /// Returns the configuration file body as a byte array. - /// - /// A byte array containing the contents of the message body. The array is empty if the body has no content. - public byte[] GetBodyBytes() => this.body.ToByteArray() ?? []; - - /// - /// Attempts to copy the configuration file body to the specified destination buffer. - /// - /// If the body is empty, no data is written and the method returns true with set to - /// 0. If the destination buffer is too small to hold the body content, no data is written, is set to - /// 0, and the method returns false. - /// The buffer that receives the body bytes. Must be large enough to hold the entire body content. - /// When this method returns, contains the number of bytes successfully written to the destination buffer. - /// true if the body was successfully copied to the destination buffer or if the body is empty; otherwise, false. - public bool TryGetBody(Span destination, out int bytesWritten) - { - if (this.body.IsEmpty) - { - bytesWritten = 0; - return true; - } - - if (destination.Length < this.body.Length) - { - bytesWritten = 0; - return false; - } - - this.body.Span.CopyTo(destination); - bytesWritten = this.body.Length; - return true; - } } diff --git a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs index 16086189e6..917e9f58fb 100644 --- a/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs +++ b/src/OpenTelemetry.OpAmp.Client/Messages/RemoteConfiguration/RemoteConfigMessage.cs @@ -20,6 +20,7 @@ internal RemoteConfigMessage(AgentRemoteConfig agentRemoteConfig) foreach (var config in agentRemoteConfig.Config.ConfigMap) { + // The value should never be null, but just in case... if (config.Value is not null) { this.agentConfigMap[config.Key] = new AgentConfigFile(config.Key, config.Value); @@ -35,53 +36,7 @@ internal RemoteConfigMessage(AgentRemoteConfig agentRemoteConfig) public IReadOnlyDictionary AgentConfigMap => this.agentConfigMap; /// - /// Gets the length, in bytes, of the configuration hash. + /// Gets the hash value representing the current remote configuration. /// - public int HashLength => this.configHash.Length; - - /// - /// Returns the configuration hash as a byte array. - /// - /// A array containing the hash of the remote configuration. - public byte[] GetConfigHashBytes() => this.configHash.ToByteArray(); - - /// - /// Returns the configuration hash as a UTF-8 string. - /// - /// A representing the UTF-8 encoded configuration hash. - public string GetConfigHashUtf8String() => this.configHash.ToStringUtf8(); - - /// - /// Attempts to copy the configuration hash to the specified destination buffer. - /// - /// If the hash is empty, no data is written and the method returns true with set to - /// 0. If the destination buffer is too small to hold the hash, no data is written, is set to - /// 0, and the method returns false. - /// The buffer that receives the hash bytes. Must be large enough to hold the entire hash content. - /// When this method returns, contains the number of bytes successfully written to the destination buffer. - /// true if the hash was successfully copied to the destination buffer or if the hash is empty; otherwise, false. - public bool TryGetConfigHash(Span destination, out int bytesWritten) - { - if (this.configHash is null) - { - bytesWritten = 0; - return false; - } - - if (this.configHash.IsEmpty) - { - bytesWritten = 0; - return true; - } - - if (destination.Length < this.configHash.Length) - { - bytesWritten = 0; - return false; - } - - this.configHash.Span.CopyTo(destination); - bytesWritten = this.configHash.Length; - return true; - } + public ReadOnlySpan ConfigHash => this.configHash.Span; } diff --git a/test/OpenTelemetry.OpAmp.Client.Tests/Messages/RemoteConfigMessageTests.cs b/test/OpenTelemetry.OpAmp.Client.Tests/Messages/RemoteConfigMessageTests.cs index 2612bb96d5..45e16f19a7 100644 --- a/test/OpenTelemetry.OpAmp.Client.Tests/Messages/RemoteConfigMessageTests.cs +++ b/test/OpenTelemetry.OpAmp.Client.Tests/Messages/RemoteConfigMessageTests.cs @@ -50,146 +50,176 @@ public void Constructor_WithEmptyConfigMap_InitializesEmptyDictionary() } [Fact] - public void GetConfigHashBytes_WithValidHash_ReturnsExpectedBytes() + public void ConfigHash_WithValidHash_ReturnsExpectedBytes() { // Arrange var agentRemoteConfig = this.CreateAgentRemoteConfig(); var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); // Act - var hashBytes = remoteConfigMessage.GetConfigHashBytes(); + var hash = remoteConfigMessage.ConfigHash; // Assert - Assert.Equal(Encoding.UTF8.GetByteCount(HashString), hashBytes.Length); - Assert.Equal(HashString, Encoding.UTF8.GetString(hashBytes)); + Assert.Equal(Encoding.UTF8.GetByteCount(HashString), hash.Length); + Assert.Equal(HashString, Encoding.UTF8.GetString(hash.ToArray())); } [Fact] - public void GetConfigHashUtf8String_WithValidHash_ReturnsExpectedString() + public void ConfigHash_WithEmptyHash_ReturnsEmptySpan() { // Arrange - var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var agentRemoteConfig = new global::OpAmp.Proto.V1.AgentRemoteConfig + { + Config = new global::OpAmp.Proto.V1.AgentConfigMap(), + ConfigHash = ByteString.Empty, + }; var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); // Act - var hashString = remoteConfigMessage.GetConfigHashUtf8String(); + var hashSpan = remoteConfigMessage.ConfigHash; // Assert - Assert.Equal(HashString, hashString); + Assert.Equal(0, hashSpan.Length); + Assert.True(hashSpan.IsEmpty); } - [Fact] - public void HashLength_WithValidHash_ReturnsExpectedLength() + [Theory] + [InlineData(Config1Name, JsonContentType, JsonString)] + [InlineData(Config2Name, YamlContentType, YamlString)] + public void AgentConfigFile_Properties_ReturnExpectedValues(string configName, string contentType, string bodyContent) { // Arrange var agentRemoteConfig = this.CreateAgentRemoteConfig(); var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); - // Act & Assert - Assert.Equal(Encoding.UTF8.GetByteCount(HashString), remoteConfigMessage.HashLength); - Assert.Equal(remoteConfigMessage.GetConfigHashBytes().Length, remoteConfigMessage.HashLength); + // Act + var configFile = remoteConfigMessage.AgentConfigMap[configName]; + + // Assert + Assert.Equal(configName, configFile.Name); + Assert.Equal(contentType, configFile.ContentType); + Assert.Equal(bodyContent, Encoding.UTF8.GetString(configFile.Body.ToArray())); } [Fact] - public void TryGetConfigHash_WithSufficientBuffer_ReturnsTrueAndWritesBytes() + public void AgentConfigFile_Body_WithEmptyBody_ReturnsEmptySpan() { // Arrange - var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var configMap = new global::OpAmp.Proto.V1.AgentConfigMap(); + configMap.ConfigMap.Add("empty-config", new global::OpAmp.Proto.V1.AgentConfigFile + { + Body = ByteString.Empty, + ContentType = JsonContentType, + }); + + var agentRemoteConfig = new global::OpAmp.Proto.V1.AgentRemoteConfig + { + Config = configMap, + ConfigHash = ByteString.CopyFromUtf8(HashString), + }; + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); - Span hashSpan = stackalloc byte[remoteConfigMessage.HashLength]; + var configFile = remoteConfigMessage.AgentConfigMap["empty-config"]; // Act - var result = remoteConfigMessage.TryGetConfigHash(hashSpan, out int bytesWritten); + var bodySpan = configFile.Body; // Assert - Assert.True(result); - Assert.Equal(remoteConfigMessage.HashLength, bytesWritten); - Assert.Equal(HashString, Encoding.UTF8.GetString(hashSpan.ToArray())); + Assert.Equal(0, bodySpan.Length); + Assert.True(bodySpan.IsEmpty); } [Fact] - public void TryGetConfigHash_WithInsufficientBuffer_ReturnsFalse() + public void AgentConfigFile_WithEmptyContentType_ReturnsEmptyString() { // Arrange - var agentRemoteConfig = this.CreateAgentRemoteConfig(); + var configMap = new global::OpAmp.Proto.V1.AgentConfigMap(); + configMap.ConfigMap.Add("empty-content-type", new global::OpAmp.Proto.V1.AgentConfigFile + { + Body = ByteString.CopyFromUtf8(JsonString), + ContentType = string.Empty, + }); + + var agentRemoteConfig = new global::OpAmp.Proto.V1.AgentRemoteConfig + { + Config = configMap, + ConfigHash = ByteString.CopyFromUtf8(HashString), + }; + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); - Span hashSpan = stackalloc byte[remoteConfigMessage.HashLength - 1]; // Act - var result = remoteConfigMessage.TryGetConfigHash(hashSpan, out int bytesWritten); + var configFile = remoteConfigMessage.AgentConfigMap["empty-content-type"]; // Assert - Assert.False(result); - Assert.Equal(0, bytesWritten); + Assert.Equal(string.Empty, configFile.ContentType); } - [Theory] - [InlineData(Config1Name, JsonContentType, JsonString)] - [InlineData(Config2Name, YamlContentType, YamlString)] - public void AgentConfigFile_Properties_ReturnExpectedValues(string configName, string contentType, string bodyContent) + [Fact] + public void AgentConfigMap_IsReadOnly() { // Arrange var agentRemoteConfig = this.CreateAgentRemoteConfig(); var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); - // Act - var configFile = remoteConfigMessage.AgentConfigMap[configName]; - - // Assert - Assert.Equal(configName, configFile.Name); - Assert.Equal(contentType, configFile.ContentType); - Assert.Equal(Encoding.UTF8.GetByteCount(bodyContent), configFile.BodyLength); + // Act & Assert + Assert.IsType>( + remoteConfigMessage.AgentConfigMap, exactMatch: false); } [Fact] - public void AgentConfigFile_GetBodyBytes_ReturnsExpectedContent() + public void AgentConfigMap_UsesOrdinalComparison() { // Arrange - var agentRemoteConfig = this.CreateAgentRemoteConfig(); - var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + var configMap = new global::OpAmp.Proto.V1.AgentConfigMap(); + configMap.ConfigMap.Add("Config", new global::OpAmp.Proto.V1.AgentConfigFile + { + Body = ByteString.CopyFromUtf8(JsonString), + ContentType = JsonContentType, + }); - // Act - var config1 = remoteConfigMessage.AgentConfigMap[Config1Name]; - var bodyBytes = config1.GetBodyBytes(); + var agentRemoteConfig = new global::OpAmp.Proto.V1.AgentRemoteConfig + { + Config = configMap, + ConfigHash = ByteString.CopyFromUtf8(HashString), + }; - // Assert - Assert.Equal(bodyBytes.Length, config1.BodyLength); - Assert.Equal(JsonString, Encoding.UTF8.GetString(bodyBytes)); + var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); + + // Act & Assert - case-sensitive check + Assert.True(remoteConfigMessage.AgentConfigMap.ContainsKey("Config")); + Assert.False(remoteConfigMessage.AgentConfigMap.ContainsKey("config")); } [Fact] - public void AgentConfigFile_TryGetBody_WithSufficientBuffer_ReturnsTrueAndWritesBytes() + public void ConfigHash_MultipleAccess_ReturnsSameSpan() { // Arrange var agentRemoteConfig = this.CreateAgentRemoteConfig(); var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); - var config1 = remoteConfigMessage.AgentConfigMap[Config1Name]; - Span bodySpan = stackalloc byte[config1.BodyLength]; // Act - var result = config1.TryGetBody(bodySpan, out int bytesWritten); + var hash1 = remoteConfigMessage.ConfigHash; + var hash2 = remoteConfigMessage.ConfigHash; // Assert - Assert.True(result); - Assert.Equal(config1.BodyLength, bytesWritten); - Assert.Equal(JsonString, Encoding.UTF8.GetString(bodySpan.ToArray())); + Assert.True(hash1.SequenceEqual(hash2)); } [Fact] - public void AgentConfigFile_TryGetBody_WithInsufficientBuffer_ReturnsFalse() + public void AgentConfigFile_Body_MultipleAccess_ReturnsSameSpan() { // Arrange var agentRemoteConfig = this.CreateAgentRemoteConfig(); var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); - var config1 = remoteConfigMessage.AgentConfigMap[Config1Name]; - Span bodySpan = stackalloc byte[config1.BodyLength - 1]; + var configFile = remoteConfigMessage.AgentConfigMap[Config1Name]; // Act - var result = config1.TryGetBody(bodySpan, out int bytesWritten); + var body1 = configFile.Body; + var body2 = configFile.Body; // Assert - Assert.False(result); - Assert.Equal(0, bytesWritten); + Assert.True(body1.SequenceEqual(body2)); } #if NET @@ -200,11 +230,9 @@ public void AgentConfigFile_JsonContent_CanBeDeserialized() var agentRemoteConfig = this.CreateAgentRemoteConfig(); var remoteConfigMessage = new Client.Messages.RemoteConfigMessage(agentRemoteConfig); var config1 = remoteConfigMessage.AgentConfigMap[Config1Name]; - Span bodySpan = stackalloc byte[config1.BodyLength]; - Assert.True(config1.TryGetBody(bodySpan, out _)); // Act - var config = System.Text.Json.JsonSerializer.Deserialize(bodySpan); + var config = System.Text.Json.JsonSerializer.Deserialize(config1.Body); // Assert Assert.NotNull(config);