/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ // IDE0230: byte-array literals below are opaque binary test vectors, not text; a // UTF-8 "..."u8 literal would misrepresent their intent, so keep the explicit byte arrays. #pragma warning disable IDE0230 // Use UTF-8 string literal // CA2007: tests run without a SynchronizationContext; ConfigureAwait(false) // adds noise without a behavioural benefit. Disabled file-level for the suite. #pragma warning disable CA2007 using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using Opc.Ua.Redundancy; namespace Opc.Ua.Server.Tests.Redundancy { /// /// Unit tests for . /// [TestFixture] [Category("Distributed")] [Parallelizable(ParallelScope.All)] public class InMemorySharedKeyValueStoreTests { /// /// Verifies that an in-memory shared-store value can be retrieved after insertion. /// [Test] public async Task SetAndTryGetReturnsStoredValueAsync() { using var store = new InMemorySharedKeyValueStore(); var payload = ByteString.From(new byte[] { 1, 2, 3 }); await store.SetAsync("k1", payload).ConfigureAwait(false); (bool found, ByteString value) = await store.TryGetAsync("k1").ConfigureAwait(false); Assert.That(found, Is.True); Assert.That(value.ToArray(), Is.EqualTo(payload.ToArray())); } /// /// Verifies that reading a missing shared-store key returns false. /// [Test] public async Task TryGetMissingKeyReturnsFalseAsync() { using var store = new InMemorySharedKeyValueStore(); (bool found, ByteString value) = await store.TryGetAsync("missing").ConfigureAwait(false); Assert.That(found, Is.False); Assert.That(value.IsNull, Is.True); } /// /// Verifies that compare-and-swap creates an entry when the key is absent. /// [Test] public async Task CompareAndSwapCreatesWhenAbsentAsync() { using var store = new InMemorySharedKeyValueStore(); var value = ByteString.From(new byte[] { 9 }); bool created = await store.CompareAndSwapAsync("k", default, value).ConfigureAwait(false); bool createdAgain = await store.CompareAndSwapAsync("k", default, value).ConfigureAwait(false); Assert.That(created, Is.True); Assert.That(createdAgain, Is.False, "second create-if-absent must fail because the key now exists"); } /// /// Verifies that compare-and-swap replaces an entry when the expected value matches. /// [Test] public async Task CompareAndSwapSwapsWhenValueMatchesAsync() { using var store = new InMemorySharedKeyValueStore(); var first = ByteString.From(new byte[] { 1 }); var second = ByteString.From(new byte[] { 2 }); await store.SetAsync("k", first).ConfigureAwait(false); bool swapped = await store.CompareAndSwapAsync("k", first, second).ConfigureAwait(false); (bool found, ByteString value) = await store.TryGetAsync("k").ConfigureAwait(false); Assert.That(swapped, Is.True); Assert.That(found, Is.True); Assert.That(value.ToArray(), Is.EqualTo(second.ToArray())); } /// /// Verifies that compare-and-swap deletes a matching entry when the replacement is null. /// [Test] public async Task CompareAndSwapDeletesWhenReplacementIsNullAsync() { using var store = new InMemorySharedKeyValueStore(); var value = ByteString.From(new byte[] { 1 }); await store.SetAsync("k", value).ConfigureAwait(false); bool deleted = await store.CompareAndSwapAsync( "k", value, default).ConfigureAwait(false); (bool found, _) = await store.TryGetAsync("k") .ConfigureAwait(false); Assert.That(deleted, Is.True); Assert.That(found, Is.False); } /// /// Verifies that compare-and-swap fails when the expected value does not match. /// [Test] public async Task CompareAndSwapFailsWhenValueMismatchAsync() { using var store = new InMemorySharedKeyValueStore(); var actual = ByteString.From(new byte[] { 1 }); var wrongExpected = ByteString.From(new byte[] { 7 }); var desired = ByteString.From(new byte[] { 2 }); await store.SetAsync("k", actual).ConfigureAwait(false); bool swapped = await store.CompareAndSwapAsync("k", wrongExpected, desired).ConfigureAwait(false); (bool found, ByteString value) = await store.TryGetAsync("k").ConfigureAwait(false); Assert.That(swapped, Is.False); Assert.That(found, Is.True); Assert.That(value.ToArray(), Is.EqualTo(actual.ToArray()), "value must be unchanged on a failed CAS"); } /// /// Verifies that deletion removes the shared-store entry. /// [Test] public async Task DeleteRemovesKeyAsync() { using var store = new InMemorySharedKeyValueStore(); await store.SetAsync("k", ByteString.From(new byte[] { 1 })).ConfigureAwait(false); bool deleted = await store.DeleteAsync("k").ConfigureAwait(false); bool deletedAgain = await store.DeleteAsync("k").ConfigureAwait(false); (bool found, _) = await store.TryGetAsync("k").ConfigureAwait(false); Assert.That(deleted, Is.True); Assert.That(deletedAgain, Is.False); Assert.That(found, Is.False); } /// /// Verifies that scanning returns only shared-store keys matching the requested prefix. /// [Test] public async Task ScanReturnsMatchingPrefixOnlyAsync() { using var store = new InMemorySharedKeyValueStore(); await store.SetAsync("a/1", ByteString.From(new byte[] { 1 })).ConfigureAwait(false); await store.SetAsync("a/2", ByteString.From(new byte[] { 2 })).ConfigureAwait(false); await store.SetAsync("b/1", ByteString.From(new byte[] { 3 })).ConfigureAwait(false); var keys = new List(); await foreach (KeyValuePair entry in store.ScanAsync("a/")) { keys.Add(entry.Key); } Assert.That(keys, Is.EquivalentTo(["a/1", "a/2"])); } /// /// Verifies that a prefix watcher observes set and delete operations. /// [Test] public async Task WatchObservesSetAndDeleteForPrefixAsync() { using var store = new InMemorySharedKeyValueStore(); using var cts = new CancellationTokenSource(); // Driving the enumerator manually guarantees the watcher is // registered (synchronous prefix of the iterator runs on the // first MoveNextAsync) before any mutation is published — no // delays, no flakiness. await using IAsyncEnumerator enumerator = store.WatchAsync("a/", cts.Token).GetAsyncEnumerator(); ValueTask first = enumerator.MoveNextAsync(); await store.SetAsync("b/ignored", ByteString.From(new byte[] { 0 })).ConfigureAwait(false); await store.SetAsync("a/1", ByteString.From(new byte[] { 1 })).ConfigureAwait(false); Assert.That(await first.ConfigureAwait(false), Is.True); Assert.That(enumerator.Current.Kind, Is.EqualTo(KeyValueChangeKind.Set)); Assert.That(enumerator.Current.Key, Is.EqualTo("a/1")); ValueTask second = enumerator.MoveNextAsync(); await store.DeleteAsync("a/1").ConfigureAwait(false); Assert.That(await second.ConfigureAwait(false), Is.True); Assert.That(enumerator.Current.Kind, Is.EqualTo(KeyValueChangeKind.Delete)); Assert.That(enumerator.Current.Key, Is.EqualTo("a/1")); cts.Cancel(); } } }