diff --git a/Tests/FrameworkTests/Utils/IndexEncodingTests.cs b/Tests/FrameworkTests/Utils/IndexEncodingTests.cs deleted file mode 100644 index e726a73..0000000 --- a/Tests/FrameworkTests/Utils/IndexEncodingTests.cs +++ /dev/null @@ -1,189 +0,0 @@ -using Logging; -using NuGet.Frameworks; -using NUnit.Framework; -using System.Collections.Concurrent; -using System.Numerics; -using Utils; - -namespace FrameworkTests.Utils -{ - [TestFixture] - public class IndexEncodingTests - { - private readonly Random random = new Random(); - - [Test] - public void IndexRanging() - { - var log = new FileLog(Path.Combine(Environment.CurrentDirectory, nameof(IndexRanging) + ".log")); - - var reruns = 1; - var tests = CreateTests().ToArray(); - log.Log($"Tests: {tests.Length}"); - foreach (var test in tests) - { - log.Log($"Running {test.GetName()}"); - Parallel.For(0, reruns, i => - { - RunTest(log, test); - }); - } - - log.Log("Results:"); - foreach (var test in tests) - { - test.PrintResult(log); - } - } - - private void RunTest(FileLog log, IndexTest test) - { - var blockPresence = new BlockPresence(log, test.NumIndices, test.PresenceFactor); - var presentIndices = blockPresence.Present.ToArray(); - - //Stopwatch.Measure(log, nameof(RunLengthEncode), () => - //{ - var run = RunLengthEncode(presentIndices); - test.RunLengthEncodingLengths.Add(run.Length); - //}); - - //Stopwatch.Measure(log, nameof(FlipMapEncode), () => - //{ - var flipMap = FlipMapEncode(presentIndices, test.NumIndices); - test.FlipMapLengths.Add(flipMap.Length); - //}); - } - - private int[] RunLengthEncode(int[] indices) - { - var result = new List(); - if (indices.Length == 0) return result.ToArray(); - - var runValue = indices[0]; - var runStart = runValue; - var runLength = 1; - for (var i = 1; i < indices.Length; i++) - { - if (i >= indices.Length) - { - result.Add(runStart); - result.Add(runLength); - } - else - { - var nextValue = indices[i]; - if (nextValue == runValue + 1) - { - runLength++; - } - else - { - result.Add(runStart); - result.Add(runLength); - - runLength = 1; - runStart = nextValue; - runValue = nextValue; - } - } - } - - return result.ToArray(); - } - - private int[] FlipMapEncode(int[] presentIndices, int numIndices) - { - var flips = new List(); - if (presentIndices.Length == 0) return flips.ToArray(); - - var current = false; - for (var i = 0; i < numIndices; i++) - { - var isPresent = presentIndices.Contains(i); - if (current != isPresent) - { - flips.Add(i); - current = isPresent; - } - } - - return flips.ToArray(); - } - - private IEnumerable CreateTests() - { - //// 10,000,000 indices * 64k = 610 GB dataset - //for (var numIndices = 1000; numIndices < 10000000; numIndices *= 100) - //{ - // for (float factor = 0.0f; factor < 1.0f; factor += 0.1f) - // { - // yield return new IndexTest(numIndices, factor); - // } - // yield return new IndexTest(numIndices, 1.0f); - //} - - yield return new IndexTest(100000, 0.5f); - } - - public class IndexTest - { - public IndexTest(int numIndices, float presenceFactor) - { - NumIndices = numIndices; - PresenceFactor = presenceFactor; - } - - public int NumIndices { get; } - public float PresenceFactor { get; } - - public int BitMapLength => Convert.ToInt32(Math.Ceiling(NumIndices / 8.0)); - public int PresenceArrayLength => Convert.ToInt32(Math.Ceiling(NumIndices * PresenceFactor)); - public ConcurrentBag RunLengthEncodingLengths { get; } = new ConcurrentBag(); - public ConcurrentBag FlipMapLengths { get; } = new ConcurrentBag(); - - public void PrintResult(ILog log) - { - log.Log(GetName()); - log.Log($"BitmapLength: {BitMapLength}"); - log.Log($"PresenceArrayLength: {PresenceArrayLength}"); - log.Log($"RunLength: {RunLengthEncodingLengths.Average()}"); - log.Log($"FlipMap: {FlipMapLengths.Average()}"); - log.Log(""); - } - - public string GetName() - { - return $"Test: {NumIndices} indices, {PresenceFactor * 100.0f}% present."; - } - } - - public class BlockPresence - { - public BlockPresence(ILog log, int length, float factor) - { - //Stopwatch.Measure(log, "Factoring", () => - //{ - var all = Enumerable.Range(0, length).ToList(); - - float l = length; - var numPresent = Convert.ToInt32(Math.Round(l * factor)); - if (numPresent >= length) - { - Present = all.ToArray(); - return; - } - - var present = new List(); - while (present.Count < numPresent) - { - present.Add(all.PickOneRandom()); - } - present.Sort(); - Present = present.ToArray(); - //}); - } - - public int[] Present { get; private set; } = Array.Empty(); - } - } -} diff --git a/Tests/FrameworkTests/Utils/RunLengthEncodingRunTests.cs b/Tests/FrameworkTests/Utils/RunLengthEncodingRunTests.cs new file mode 100644 index 0000000..d3544c1 --- /dev/null +++ b/Tests/FrameworkTests/Utils/RunLengthEncodingRunTests.cs @@ -0,0 +1,193 @@ +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using static FrameworkTests.Utils.RunLengthEncodingTests; + +namespace FrameworkTests.Utils +{ + [TestFixture] + public class RunLengthEncodingRunTests + { + [Test] + [Combinatorial] + public void RunIncludes( + [Values(0, 1, 2, 3)] int start, + [Values(1, 2, 3, 4)] int length) + { + var run = new Run(start, length); + + var shouldInclude = Enumerable.Range(start, length).ToArray(); + var shouldExclude = new int[] + { + shouldInclude.Min() - 1, + shouldInclude.Max() + 1 + }; + + foreach (var incl in shouldInclude) + { + Assert.That(run.Includes(incl)); + } + foreach (var excl in shouldExclude) + { + Assert.That(!run.Includes(excl)); + } + } + + [Test] + public void RunExpandToInclude() + { + var run = new Run(2, 3); + Assert.That(run.Includes(2)); + Assert.That(run.Includes(4)); + Assert.That(!run.Includes(5)); + + Assert.That(run.ExpandToInclude(1), Is.False); + Assert.That(run.ExpandToInclude(2), Is.False); + Assert.That(run.ExpandToInclude(4), Is.False); + Assert.That(run.ExpandToInclude(6), Is.False); + + Assert.That(run.ExpandToInclude(5), Is.True); + Assert.That(run.Includes(5)); + Assert.That(!run.Includes(6)); + } + + [Test] + public void RunCanUnsetLastIndex() + { + var run = new Run(0, 3); + Assert.That(run.Includes(2)); + var update = run.Unset(2); + Assert.That(!run.Includes(2)); + + Assert.That(update.NewRuns.Length, Is.EqualTo(0)); + Assert.That(update.RemoveRuns.Length, Is.EqualTo(0)); + } + + [Test] + public void RunCanSplit() + { + var run = new Run(0, 6); // 0, 1, 2, 3, 4, 5 + var update = run.Unset(2); + + Assert.That(run.Start, Is.EqualTo(0)); + Assert.That(run.Length, Is.EqualTo(2)); // 0, 1 + Assert.That(!run.Includes(2)); + + Assert.That(update.NewRuns.Length, Is.EqualTo(1)); + Assert.That(update.RemoveRuns.Length, Is.EqualTo(0)); + + Assert.That(!update.NewRuns[0].Includes(2)); + Assert.That(update.NewRuns[0].Start, Is.EqualTo(3)); + Assert.That(update.NewRuns[0].Length, Is.EqualTo(3)); // 3, 4, 5 + Assert.That(!update.NewRuns[0].Includes(6)); + } + + [Test] + public void RunReplacesSelfWhenUnsetFirstIndex() + { + var run = new Run(0, 5); + var update = run.Unset(0); + + Assert.That(update.NewRuns.Length, Is.EqualTo(1)); + Assert.That(update.RemoveRuns.Length, Is.EqualTo(1)); + + Assert.That(update.RemoveRuns[0], Is.SameAs(run)); + Assert.That(update.NewRuns[0].Start, Is.EqualTo(1)); + Assert.That(update.NewRuns[0].Length, Is.EqualTo(4)); + } + + [Test] + public void CanIterateIndices() + { + var run = new Run(2, 4); + var seen = new List(); + run.Iterate(i => seen.Add(i)); + + CollectionAssert.AreEqual(new[] { 2, 3, 4, 5 }, seen); + } + } + + public class Run + { + public Run(int start, int length) + { + Start = start; + Length = length; + } + + public int Start { get; } + public int Length { get; private set; } + + public bool Includes(int index) + { + return index >= Start && index < (Start + Length); + } + + public bool ExpandToInclude(int index) + { + if (index == (Start + Length)) + { + Length++; + return true; + } + return false; + } + + public RunUpdate Unset(int index) + { + if (!Includes(index)) + { + return new RunUpdate(); + } + + if (index == Start) + { + // First index: Replace self with new run at next index, unless empty. + if (Length == 1) + { + return new RunUpdate(Array.Empty(), new[] { this }); + } + return new RunUpdate( + newRuns: new[] { new Run(Start + 1, Length - 1) }, + removeRuns: new[] { this } + ); + } + + if (index == (Start + Length - 1)) + { + // Last index: Become one smaller. + Length--; + return new RunUpdate(); + } + + // Split: + var newRunLength = (Start + Length - 1) - index; + Length = index - Start; + return new RunUpdate(new[] { new Run(index + 1, newRunLength) }, Array.Empty()); + } + + public void Iterate(Action action) + { + for (var i = 0; i < Length; i++) + { + action(Start + i); + } + } + } + + public class RunUpdate + { + public RunUpdate() + : this(Array.Empty(), Array.Empty()) + { + } + + public RunUpdate(Run[] newRuns, Run[] removeRuns) + { + NewRuns = newRuns; + RemoveRuns = removeRuns; + } + + public Run[] NewRuns { get; } + public Run[] RemoveRuns { get; } + } +} diff --git a/Tests/FrameworkTests/Utils/RunLengthEncodingTests.cs b/Tests/FrameworkTests/Utils/RunLengthEncodingTests.cs new file mode 100644 index 0000000..aaddbcb --- /dev/null +++ b/Tests/FrameworkTests/Utils/RunLengthEncodingTests.cs @@ -0,0 +1,320 @@ +using Logging; +using Microsoft.VisualStudio.TestPlatform.Common; +using NuGet.Frameworks; +using NUnit.Framework; +using System.Collections.Concurrent; +using System.Numerics; +using Utils; + +namespace FrameworkTests.Utils +{ + [TestFixture] + public class RunLengthEncodingTests + { + private readonly Random random = new Random(); + + [Test] + public void EmptySet() + { + var set = new IndexSet(); + for (var i = 0; i < 1000; i++) + { + Assert.That(set.IsSet(i), Is.False); + } + + var calls = 0; + set.Iterate(i => calls++); + Assert.That(calls, Is.EqualTo(0)); + } + + [Test] + public void SetsIndex() + { + var set = new IndexSet(); + var index = 1234; + set.Set(index); + + Assert.That(set.IsSet(index), Is.True); + } + + [Test] + public void UnsetsIndex() + { + var set = new IndexSet(); + var index = 1234; + set.Set(index); + set.Unset(index); + + Assert.That(set.IsSet(index), Is.False); + } + + [Test] + public void RandomIndices() + { + var indices = GenerateRandomIndices(); + var set = new IndexSet(indices); + + AssertEqual(set, indices); + } + + [Test] + public void RandomRunLengthEncoding() + { + var indices = GenerateRandomIndices(); + var set = new IndexSet(indices); + + var encoded = set.RunLengthEncoded(); + var decoded = IndexSet.FromRunLengthEncoded(encoded); + + AssertEqual(decoded, indices); + } + + [Test] + public void RunLengthEncoding() + { + var indices = new[] { 0, 1, 2, 4, 6, 7 }; + var set = new IndexSet(indices); + var encoded = set.RunLengthEncoded(); + + CollectionAssert.AreEqual(new[] + { + 0, 3, + 4, 1, + 6, 2 + }, encoded); + } + + [Test] + public void RunLengthDecoding() + { + var encoded = new[] + { + 2, 4, // 2, 3, 4, 5 + 7, 1, // 7 + 9, 2 // 9, 10 + }; + + var set = IndexSet.FromRunLengthEncoded(encoded); + var seen = new List(); + set.Iterate(i => seen.Add(i)); + + CollectionAssert.AreEqual(new[] + { + 2, 3, 4, 5, + 7, + 9, 10 + }, seen); + } + + [Test] + public void SetIndexBeforeRun() + { + var set = new IndexSet(new[] { 12, 13, 14 }); + set.Set(11); + var encoded = set.RunLengthEncoded(); + + CollectionAssert.AreEqual(new[] + { + 11, 4 + }, encoded); + } + + [Test] + public void SetIndexAfterRun() + { + var set = new IndexSet(new[] { 12, 13, 14 }); + set.Set(15); + var encoded = set.RunLengthEncoded(); + + CollectionAssert.AreEqual(new[] + { + 12, 4 + }, encoded); + } + + [Test] + public void UnsetIndexAtStartOfRun() + { + var set = new IndexSet(new[] { 11, 12, 13, 14 }); + set.Unset(11); + var encoded = set.RunLengthEncoded(); + + CollectionAssert.AreEqual(new[] + { + 12, 3 + }, encoded); + } + + [Test] + public void UnsetIndexAtEndOfRun() + { + var set = new IndexSet(new[] { 11, 12, 13, 14 }); + set.Unset(14); + var encoded = set.RunLengthEncoded(); + + CollectionAssert.AreEqual(new[] + { + 11, 3 + }, encoded); + } + + [Test] + public void UnsetIndexInRun() + { + var set = new IndexSet(new[] { 11, 12, 13, 14 }); + set.Unset(12); + var encoded = set.RunLengthEncoded(); + + CollectionAssert.AreEqual(new[] + { + 11, 1, + 13, 2 + }, encoded); + } + + private void AssertEqual(IndexSet set, int[] indices) + { + var max = indices.Max() + 1; + for (var i = 0; i < max; i++) + { + Assert.That(set.IsSet(i), Is.EqualTo(indices.Contains(i))); + } + + var seen = new List(); + set.Iterate(i => seen.Add(i)); + + CollectionAssert.AreEqual(indices, seen); + } + + private int[] GenerateRandomIndices() + { + var number = 1000; + var max = 2000; + var all = Enumerable.Range(0, max).ToList(); + var result = new List(); + + while (all.Any() && result.Count < number) + { + result.Add(all.PickOneRandom()); + } + + all.Sort(); + return all.ToArray(); + } + + public class IndexSet + { + private readonly SortedList runs = new SortedList(); + + public IndexSet() + { + } + + public IndexSet(int[] indices) + { + foreach (var i in indices) Set(i); + } + + public static IndexSet FromRunLengthEncoded(int[] rle) + { + var set = new IndexSet(); + for (var i = 0; i < rle.Length; i += 2) + { + var start = rle[i]; + var length = rle[i + 1]; + set.runs.Add(start, new Run(start, length)); + } + + return set; + } + + public bool IsSet(int index) + { + if (runs.ContainsKey(index)) return true; + + var run = GetRunBefore(index); + if (run == null) return false; + + return run.Includes(index); + } + + public void Set(int index) + { + if (runs.ContainsKey(index)) return; + + var run = GetRunBefore(index); + if (run == null || !run.ExpandToInclude(index)) + { + CreateNewRun(index); + } + } + + public void Unset(int index) + { + if (runs.ContainsKey(index)) + { + HandleUpdate(runs[index].Unset(index)); + } + else + { + var run = GetRunBefore(index); + if (run == null) return; + HandleUpdate(run.Unset(index)); + } + } + + public void Iterate(Action onIndex) + { + foreach (var run in runs.Values) + { + run.Iterate(onIndex); + } + } + + public int[] RunLengthEncoded() + { + return Encode().ToArray(); + } + + private IEnumerable Encode() + { + foreach (var pair in runs) + { + yield return pair.Value.Start; + yield return pair.Value.Length; + } + } + + private Run? GetRunBefore(int index) + { + Run? result = null; + foreach (var pair in runs) + { + if (pair.Key < index) result = pair.Value; + else return result; + } + return result; + } + + private void HandleUpdate(RunUpdate runUpdate) + { + foreach (var newRun in runUpdate.NewRuns) runs.Add(newRun.Start, newRun); + foreach (var removeRun in runUpdate.RemoveRuns) runs.Remove(removeRun.Start); + } + + private void CreateNewRun(int index) + { + if (runs.ContainsKey(index + 1)) + { + var length = runs[index + 1].Length + 1; + runs.Add(index, new Run(index, length)); + runs.Remove(index + 1); + } + else + { + runs.Add(index, new Run(index, 1)); + } + } + } + } +}