using System.Collections.Concurrent; using System.Diagnostics; using FluentAssertions; using Nop.Core.Infrastructure; using NUnit.Framework; namespace Nop.Tests.Nop.Core.Tests.Infrastructure; [TestFixture] public class ConcurrentTrieTests { private IConcurrentCollection _sut; private static void Profile(Action action) { var sw = new Stopwatch(); var memory = GC.GetTotalMemory(true) / 1024.0 / 1024.0; sw.Start(); action.Invoke(); sw.Stop(); var delta = GC.GetTotalMemory(true) / 1024.0 / 1024.0 - memory; Console.WriteLine("Elapsed time: {0:F} s", sw.ElapsedMilliseconds / 1000.0); Console.WriteLine("Memory usage: {0:F} MB", delta); } [SetUp] public void SetUp() { _sut = new ConcurrentTrie(); } [Test] public void CanAddAndGetValue() { _sut.TryGetValue("a", out _).Should().BeFalse(); _sut.Add("a", 1); _sut.TryGetValue("a", out var value).Should().BeTrue(); value.Should().Be(1); _sut.Add("a", 2); _sut.TryGetValue("a", out value).Should().BeTrue(); value.Should().Be(2); } [Test] public void CanAddAndGetValues() { _sut.Add("a", 1); _sut.TryGetValue("ab", out _).Should().BeFalse(); _sut.Add("abc", 3); _sut.TryGetValue("ab", out _).Should().BeFalse(); _sut.TryGetValue("a", out var value).Should().BeTrue(); value.Should().Be(1); _sut.TryGetValue("abc", out value).Should().BeTrue(); value.Should().Be(3); _sut.Add("ab", 2); _sut.TryGetValue("ab", out value).Should().BeTrue(); value.Should().Be(2); } [Test] public void DoesNotBlockWhileEnumerating() { _sut.Add("a", 0); _sut.Add("ab", 0); foreach (var item in _sut.Keys) _sut.Remove(item); } [Test] public void CanRemoveValue() { _sut.Add("a", 1); _sut.Add("b", 1); _sut.Add("bbb", 1); _sut.Add("ab", 1); _sut.Add("aa", 1); _sut.Add("abc", 1); _sut.Add("abb", 1); _sut.Remove("ab"); _sut.TryGetValue("ab", out _).Should().BeFalse(); _sut.Keys.Should().BeEquivalentTo(["abc", "a", "b", "aa", "abb", "bbb"]); Assert.DoesNotThrow(() => _sut.Remove("ab")); _sut.Remove("bb"); _sut.TryGetValue("b", out _).Should().BeTrue(); _sut.TryGetValue("bbb", out _).Should().BeTrue(); _sut.Prune("b", out _sut); _sut.Keys.Should().BeEquivalentTo(["b", "bbb"]); _sut.Remove("b"); _sut.Keys.Should().BeEquivalentTo(["bbb"]); } [Test] public void CanGetKeys() { var keys = new[] { "a", "b", "abc" }; foreach (var key in keys) _sut.Add(key, 1); _sut.Keys.Should().BeEquivalentTo(keys); } [Test] public void CanPrune() { _sut.Add("a", 1); _sut.Add("b", 1); _sut.Add("bba", 1); _sut.Add("bbb", 1); _sut.Add("ab", 1); _sut.Add("abc", 1); _sut.Add("abd", 1); _sut.Prune("bc", out _).Should().BeFalse(); _sut.Prune("ab", out var subtree).Should().BeTrue(); subtree.Keys.Should().BeEquivalentTo(["ab", "abc", "abd"]); _sut.Keys.Should().BeEquivalentTo(["a", "b", "bba", "bbb"]); _sut.Prune("b", out subtree).Should().BeTrue(); subtree.Keys.Should().BeEquivalentTo(["b", "bba", "bbb"]); _sut.Keys.Should().BeEquivalentTo(["a"]); _sut = subtree; _sut.Prune("bb", out subtree).Should().BeTrue(); subtree.Keys.Should().BeEquivalentTo(["bba", "bbb"]); _sut.Keys.Should().BeEquivalentTo(["b"]); _sut = subtree; _sut.Prune("bba", out subtree); subtree.Keys.Should().BeEquivalentTo(["bba"]); _sut.Keys.Should().BeEquivalentTo(["bbb"]); _sut = new ConcurrentTrie(); _sut.Add("aaa", 1); _sut.Prune("a", out subtree).Should().BeTrue(); subtree.Keys.Should().BeEquivalentTo(["aaa"]); _sut.Keys.Should().BeEmpty(); _sut = subtree; _sut.Prune("aa", out subtree).Should().BeTrue(); _sut.Keys.Should().BeEmpty(); subtree.Keys.Should().BeEquivalentTo(["aaa"]); } [Test] public void CanSearch() { _sut.Add("a", 1); _sut.Add("b", 1); _sut.Add("abc", 1); _sut.Add("abcd", 2); var keys = _sut.Keys.ToList(); _sut.Search("ab").Should().BeEquivalentTo(new KeyValuePair[] { new("abc", 1), new("abcd", 2) }); _sut.Keys.Should().BeEquivalentTo(keys); } [Test] public void CanClear() { _sut.Add("a", 1); _sut.Add("ab", 1); _sut.Add("abc", 1); _sut.Clear(); _sut.Keys.Should().BeEmpty(); } [Test] [TestCase(typeof(NopConcurrentCollection))] [TestCase(typeof(ConcurrentTrie))] [Ignore("Not a test, used for profiling.")] public void Profile(Type oType) { var sut = Activator.CreateInstance(oType) as IConcurrentCollection; sut.Should().NotBeNull(); Profile(() => { for (var i = 0; i < 1000000; i++) sut.Add(Guid.NewGuid().ToString(), 0); }); } [Test] [TestCase(typeof(NopConcurrentCollection))] [TestCase(typeof(ConcurrentTrie))] [Ignore("Expensive concurrency test, enable manually.")] public void DoesNotBreakDuringParallelAddRemove(Type oType) { var sut = Activator.CreateInstance(oType) as IConcurrentCollection; sut.Should().NotBeNull(); Profile(() => { Parallel.For(0, 1000, new ParallelOptions { MaxDegreeOfParallelism = 8 }, j => { for (var i = 0; i < 1000; i++) { var s = $"{i}-{j}"; sut.Add(s, i); sut.TryGetValue(s, out var value).Should().BeTrue(); value.Should().Be(i); sut.Remove(s); sut.TryGetValue(s, out _).Should().BeFalse(); } }); }); sut.Keys.Count().Should().Be(0); } [Test] [TestCase(typeof(NopConcurrentCollection))] [TestCase(typeof(ConcurrentTrie))] [Ignore("Expensive concurrency test, enable manually.")] public void DoesNotBreakDuringParallelAddPrune(Type oType) { var sut = Activator.CreateInstance(oType) as IConcurrentCollection; sut.Should().NotBeNull(); Profile(() => { Parallel.For(0, 1000, new ParallelOptions { MaxDegreeOfParallelism = 8 }, j => { for (var i = 0; i < 1000; i++) { var s = $"{j}-{i}"; sut.Add(s, i); } sut.Prune($"{j}-", out var st); st.Keys.Count().Should().Be(1000); }); }); sut.Keys.Count().Should().Be(0); } [Test] [TestCase(typeof(NopConcurrentCollection))] [TestCase(typeof(ConcurrentTrie))] [Ignore("Not a test, used for profiling.")] public void ProfilePrune(Type oType) { var sut = Activator.CreateInstance(oType) as IConcurrentCollection; sut.Should().NotBeNull(); // insert for (var i = 0; i < 10000; i++) sut.Add(Guid.NewGuid().ToString(), 0); Profile(() => { Parallel.For(0, 1000, new ParallelOptions { MaxDegreeOfParallelism = 8 }, j => { for (var i = 0; i < 20; i++) { // insert sut.Add(Guid.NewGuid().ToString(), 0); // remove by prefix sut.Prune(Guid.NewGuid().ToString()[..5], out _); } }); }); } #region Nested class /// /// A thread-safe collection based on /// public partial class NopConcurrentCollection : IConcurrentCollection { #region Fields protected readonly ConcurrentDictionary _dictionary; #endregion #region Ctor /// /// Initializes a new instance of /// protected NopConcurrentCollection(IEnumerable> subCollection) { _dictionary = new ConcurrentDictionary(subCollection); } /// /// Initializes a new empty instance of /// public NopConcurrentCollection() { _dictionary = new ConcurrentDictionary(); } #endregion #region Methods /// /// Attempts to get the value associated with the specified key /// /// The key of the item to get (case-sensitive) /// The value associated with , if found /// /// True if the key was found, otherwise false /// public bool TryGetValue(string key, out TValue value) { return _dictionary.TryGetValue(key, out value); } /// /// Adds a key-value pair to the collection /// /// The key of the new item (case-sensitive) /// The value to be associated with public void Add(string key, TValue value) { _dictionary[key] = value; } /// /// Clears the collection /// public void Clear() { _dictionary.Clear(); } /// /// Gets all key-value pairs for keys starting with the given prefix /// /// The prefix (case-sensitive) to search for /// /// All key-value pairs for keys starting with /// public IEnumerable> Search(string prefix) { return _dictionary.Keys.Where(k => k.StartsWith(prefix)) .Select(key => new KeyValuePair(key, _dictionary[key])); } /// /// Removes the item with the given key, if present /// /// The key (case-sensitive) of the item to be removed public void Remove(string key) { _dictionary.Remove(key, out _); } /// /// Attempts to remove all items with keys starting with the specified prefix /// /// The prefix (case-sensitive) of the items to be deleted /// The sub-collection containing all deleted items, if found /// /// True if the prefix was successfully removed from the collection, otherwise false /// public bool Prune(string prefix, out IConcurrentCollection subCollection) { subCollection = new NopConcurrentCollection(Search(prefix)); try { foreach (var key in subCollection.Keys) Remove(key); } catch { return false; } return subCollection.Keys.Any(); } #endregion #region Properties /// /// Gets a collection that contains the keys in the /// public IEnumerable Keys => _dictionary.Keys; #endregion } #endregion }