Webiant Logo Webiant Logo
  1. No results found.

    Try your search with a different keyword or use * as a wildcard.

ConcurrentTrieTests.cs

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
}