From 56f94f489a41997ad5d49de2d7163114bbf5e2f8 Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Tue, 30 Nov 2021 10:38:42 -0600 Subject: [PATCH] fix filler playout crash (#517) --- CHANGELOG.md | 1 + .../ShuffledMediaCollectionEnumeratorTests.cs | 90 +++++++++++++++++++ ErsatzTV.Core/Scheduling/CloneableRandom.cs | 41 +++++++++ .../ShuffledMediaCollectionEnumerator.cs | 30 +++---- 4 files changed, 146 insertions(+), 16 deletions(-) create mode 100644 ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs create mode 100644 ErsatzTV.Core/Scheduling/CloneableRandom.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index fbedbdb3b..ccfdb4b45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Properly split song genre tags - Properly display all songs that have an identical album and title - Fix channel logo and watermark uploads +- Fix regression introduced with `v0.2.4-alpha` that caused some filler edge cases to crash the playout builder ### Added - Add song genres to search index diff --git a/ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs b/ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs new file mode 100644 index 000000000..3f861006a --- /dev/null +++ b/ErsatzTV.Core.Tests/Scheduling/ShuffledMediaCollectionEnumeratorTests.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Scheduling; +using FluentAssertions; +using LanguageExt; +using LanguageExt.UnsafeValueAccess; +using NUnit.Framework; + +namespace ErsatzTV.Core.Tests.Scheduling +{ + [TestFixture] + public class ShuffledMediaCollectionEnumeratorTests + { + private readonly List _mediaItems = new() + { + new GroupedMediaItem(new MediaItem { Id = 1 }, new List()), + new GroupedMediaItem(new MediaItem { Id = 2 }, new List()), + new GroupedMediaItem(new MediaItem { Id = 3 }, new List()) + }; + + [Test] + public void Peek_Zero_Should_Match_Current() + { + var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; + var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); + + Option peek = enumerator.Peek(0); + Option current = enumerator.Current; + + peek.IsSome.Should().BeTrue(); + current.IsSome.Should().BeTrue(); + peek.ValueUnsafe().Id.Should().Be(1); + current.ValueUnsafe().Id.Should().Be(1); + } + + [Test] + public void Peek_One_Should_Match_Next() + { + var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; + var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); + + Option peek = enumerator.Peek(1); + + enumerator.MoveNext(); + Option next = enumerator.Current; + + peek.IsSome.Should().BeTrue(); + next.IsSome.Should().BeTrue(); + peek.ValueUnsafe().Id.Should().Be(2); + next.ValueUnsafe().Id.Should().Be(2); + } + + [Test] + public void Peek_Two_Should_Match_NextNext() + { + var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; + var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); + + Option peek = enumerator.Peek(2); + + enumerator.MoveNext(); + enumerator.MoveNext(); + Option next = enumerator.Current; + + peek.IsSome.Should().BeTrue(); + next.IsSome.Should().BeTrue(); + peek.ValueUnsafe().Id.Should().Be(3); + next.ValueUnsafe().Id.Should().Be(3); + } + + [Test] + public void Peek_Three_Should_Match_NextNextNext() + { + var state = new CollectionEnumeratorState { Index = 0, Seed = 0 }; + var enumerator = new ShuffledMediaCollectionEnumerator(_mediaItems, state); + + Option peek = enumerator.Peek(3); + + enumerator.MoveNext(); + enumerator.MoveNext(); + enumerator.MoveNext(); + Option next = enumerator.Current; + + peek.IsSome.Should().BeTrue(); + next.IsSome.Should().BeTrue(); + peek.ValueUnsafe().Id.Should().Be(2); + next.ValueUnsafe().Id.Should().Be(2); + } + } +} diff --git a/ErsatzTV.Core/Scheduling/CloneableRandom.cs b/ErsatzTV.Core/Scheduling/CloneableRandom.cs new file mode 100644 index 000000000..29ce97a90 --- /dev/null +++ b/ErsatzTV.Core/Scheduling/CloneableRandom.cs @@ -0,0 +1,41 @@ +using System; + +namespace ErsatzTV.Core.Scheduling +{ + public class CloneableRandom + { + private readonly int _seed; + private readonly Random _random; + private int _count; + + public CloneableRandom(int seed) + { + _seed = seed; + _random = new Random(_seed); + } + + public CloneableRandom Clone() + { + var clone = new CloneableRandom(_seed); + + for (var i = 0; i < _count; i++) + { + clone.Next(); + } + + return clone; + } + + public int Next() + { + _count++; + return _random.Next(); + } + + public int Next(int maxValue) + { + _count++; + return _random.Next(maxValue); + } + } +} diff --git a/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs b/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs index 1bee3a797..36c313886 100644 --- a/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs +++ b/ErsatzTV.Core/Scheduling/ShuffledMediaCollectionEnumerator.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Scheduling; using LanguageExt; @@ -13,7 +12,7 @@ namespace ErsatzTV.Core.Scheduling { private readonly int _mediaItemCount; private readonly IList _mediaItems; - private Random _random; + private CloneableRandom _random; private IList _shuffled; public ShuffledMediaCollectionEnumerator( @@ -29,7 +28,7 @@ namespace ErsatzTV.Core.Scheduling state.Seed = new Random(state.Seed).Next(); } - _random = new Random(state.Seed); + _random = new CloneableRandom(state.Seed); _shuffled = Shuffle(_mediaItems, _random); State = new CollectionEnumeratorState { Seed = state.Seed }; @@ -45,7 +44,7 @@ namespace ErsatzTV.Core.Scheduling public void MoveNext() { - if ((State.Index + 1) % _shuffled.Count == 0) + if ((State.Index + 1) % _mediaItemCount == 0) { Option tail = Current; @@ -53,7 +52,7 @@ namespace ErsatzTV.Core.Scheduling do { State.Seed = _random.Next(); - _random = new Random(State.Seed); + _random = new CloneableRandom(State.Seed); _shuffled = Shuffle(_mediaItems, _random); } while (_mediaItems.Count > 1 && Current == tail); } @@ -62,29 +61,28 @@ namespace ErsatzTV.Core.Scheduling State.Index++; } - State.Index %= _shuffled.Count; + State.Index %= _mediaItemCount; } public Option Peek(int offset) { + if (offset == 0) + { + return Current; + } + if ((State.Index + offset) % _mediaItemCount == 0) { IList shuffled; Option tail = Current; - + // clone the random - var randomCopy = new Random(); - FieldInfo seedArrayInfo = typeof(Random).GetField( - "_seedArray", - BindingFlags.NonPublic | BindingFlags.Instance); - var seedArray = seedArrayInfo.GetValue(_random) as int[]; - int[] seedArrayCopy = seedArray.ToArray(); - seedArrayInfo.SetValue(randomCopy, seedArrayCopy); + CloneableRandom randomCopy = _random.Clone(); do { int newSeed = randomCopy.Next(); - randomCopy = new Random(newSeed); + randomCopy = new CloneableRandom(newSeed); shuffled = Shuffle(_mediaItems, randomCopy); } while (_mediaItems.Count > 1 && shuffled[0] == tail); @@ -94,7 +92,7 @@ namespace ErsatzTV.Core.Scheduling return _shuffled.Any() ? _shuffled[(State.Index + offset) % _mediaItemCount] : None; } - private IList Shuffle(IEnumerable list, Random random) + private IList Shuffle(IEnumerable list, CloneableRandom random) { GroupedMediaItem[] copy = list.ToArray();