mirror of https://github.com/ErsatzTV/ErsatzTV.git
19 changed files with 2886 additions and 3176 deletions
@ -0,0 +1,731 @@
@@ -0,0 +1,731 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using ErsatzTV.Core.Tests.Fakes; |
||||
using NSubstitute; |
||||
using NUnit.Framework; |
||||
using Shouldly; |
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling; |
||||
|
||||
[TestFixture] |
||||
public class ContinuePlayoutTests : PlayoutBuilderTestBase |
||||
{ |
||||
[Test] |
||||
public async Task ChronologicalFlood_Should_AnchorAndMaintainExistingPlayout() |
||||
{ |
||||
var mediaItems = new List<MediaItem> |
||||
{ |
||||
TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), |
||||
TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) |
||||
}; |
||||
|
||||
(PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = |
||||
TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); |
||||
DateTimeOffset start = HoursAfterMidnight(0); |
||||
DateTimeOffset finish = start + TimeSpan.FromHours(6); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(1); |
||||
result.AddedItems.Head().MediaItemId.ShouldBe(1); |
||||
|
||||
playout.Anchor.NextStartOffset.ShouldBe(finish); |
||||
|
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(1); |
||||
playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(1); |
||||
|
||||
DateTimeOffset start2 = HoursAfterMidnight(1); |
||||
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); |
||||
|
||||
PlayoutBuildResult result2 = await builder.Build( |
||||
playout, |
||||
referenceData, |
||||
PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Continue, |
||||
start2, |
||||
finish2, |
||||
CancellationToken); |
||||
|
||||
result2.AddedItems.Count.ShouldBe(1); |
||||
result2.AddedItems[0].StartOffset.ShouldBe(finish); |
||||
result2.AddedItems[0].MediaItemId.ShouldBe(2); |
||||
|
||||
playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(12)); |
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(1); |
||||
playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(0); |
||||
} |
||||
|
||||
[Test] |
||||
public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems() |
||||
{ |
||||
var mediaItems = new List<MediaItem> |
||||
{ |
||||
TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), |
||||
TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) |
||||
}; |
||||
|
||||
(PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = |
||||
TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); |
||||
DateTimeOffset start = HoursAfterMidnight(0); |
||||
DateTimeOffset finish = start + TimeSpan.FromHours(6); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(1); |
||||
result.AddedItems.Head().MediaItemId.ShouldBe(1); |
||||
|
||||
playout.Anchor.NextStartOffset.ShouldBe(finish); |
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(1); |
||||
playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(1); |
||||
|
||||
DateTimeOffset start2 = HoursAfterMidnight(1); |
||||
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(12); |
||||
|
||||
PlayoutBuildResult result2 = await builder.Build( |
||||
playout, |
||||
referenceData, |
||||
PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Continue, |
||||
start2, |
||||
finish2, |
||||
CancellationToken); |
||||
|
||||
result2.AddedItems.Count.ShouldBe(2); |
||||
result2.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(6)); |
||||
result2.AddedItems[0].MediaItemId.ShouldBe(2); |
||||
result2.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(12)); |
||||
result2.AddedItems[1].MediaItemId.ShouldBe(1); |
||||
|
||||
playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(18)); |
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(1); |
||||
playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(1); |
||||
} |
||||
|
||||
[Test] |
||||
public async Task ChronologicalFlood_Should_AnchorAndReturnNewPlayoutItems_MultiDay() |
||||
{ |
||||
var mediaItems = new List<MediaItem> |
||||
{ |
||||
TestMovie(1, TimeSpan.FromHours(6), DateTime.Today), |
||||
TestMovie(2, TimeSpan.FromHours(6), DateTime.Today.AddHours(1)) |
||||
}; |
||||
|
||||
(PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = |
||||
TestDataFloodForItems(mediaItems, PlaybackOrder.Chronological); |
||||
DateTimeOffset start = HoursAfterMidnight(0); |
||||
DateTimeOffset finish = start + TimeSpan.FromDays(1); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(4); |
||||
result.AddedItems.Map(i => i.MediaItemId).ToList().ShouldBe([1, 2, 1, 2]); |
||||
|
||||
playout.Anchor.NextStartOffset.ShouldBe(finish); |
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(1); |
||||
playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(0); |
||||
|
||||
PlayoutProgramScheduleAnchor headAnchor = playout.ProgramScheduleAnchors.Head(); |
||||
|
||||
// throw in a detractor anchor - playout builder should prioritize the "continue" anchor
|
||||
playout.ProgramScheduleAnchors.Insert( |
||||
0, |
||||
new PlayoutProgramScheduleAnchor |
||||
{ |
||||
Id = headAnchor.Id + 1, |
||||
Collection = headAnchor.Collection, |
||||
CollectionId = headAnchor.CollectionId, |
||||
Playout = playout, |
||||
PlayoutId = playout.Id, |
||||
AnchorDate = DateTime.Today.ToUniversalTime(), |
||||
CollectionType = headAnchor.CollectionType, |
||||
EnumeratorState = new CollectionEnumeratorState |
||||
{ Index = headAnchor.EnumeratorState.Index + 1, Seed = headAnchor.EnumeratorState.Seed }, |
||||
MediaItem = headAnchor.MediaItem, |
||||
MediaItemId = headAnchor.MediaItemId, |
||||
MultiCollection = headAnchor.MultiCollection, |
||||
MultiCollectionId = headAnchor.MultiCollectionId, |
||||
SmartCollection = headAnchor.SmartCollection, |
||||
SmartCollectionId = headAnchor.SmartCollectionId |
||||
}); |
||||
|
||||
// continue 1h later
|
||||
DateTimeOffset start2 = HoursAfterMidnight(1); |
||||
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(1); |
||||
|
||||
result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start2, finish2, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(1); |
||||
result.AddedItems[0].StartOffset.ShouldBe(finish); |
||||
result.AddedItems[0].MediaItemId.ShouldBe(1); |
||||
|
||||
playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(30)); |
||||
|
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(2); |
||||
playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(1); |
||||
|
||||
// continue 1h later
|
||||
DateTimeOffset start3 = HoursAfterMidnight(2); |
||||
DateTimeOffset finish3 = start3 + TimeSpan.FromDays(1); |
||||
|
||||
result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Continue, start3, finish3, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(0); |
||||
|
||||
playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(30)); |
||||
|
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(2); |
||||
playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(1); |
||||
} |
||||
|
||||
[Test] |
||||
public async Task ShuffleFlood_Should_MaintainRandomSeed() |
||||
{ |
||||
var mediaItems = new List<MediaItem> |
||||
{ |
||||
TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), |
||||
TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), |
||||
TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)) |
||||
}; |
||||
|
||||
(PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = |
||||
TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); |
||||
DateTimeOffset start = HoursAfterMidnight(0); |
||||
DateTimeOffset finish = start + TimeSpan.FromHours(6); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Reset, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(6); |
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(1); |
||||
playout.ProgramScheduleAnchors.Head().EnumeratorState.Seed.ShouldBeGreaterThan(0); |
||||
|
||||
int firstSeedValue = playout.ProgramScheduleAnchors.Head().EnumeratorState.Seed; |
||||
|
||||
playout.Anchor.NextStartOffset.ShouldBe(finish); |
||||
|
||||
DateTimeOffset start2 = HoursAfterMidnight(0); |
||||
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); |
||||
|
||||
PlayoutBuildResult result2 = await builder.Build( |
||||
playout, |
||||
referenceData, |
||||
PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Continue, |
||||
start2, |
||||
finish2, |
||||
CancellationToken); |
||||
|
||||
int secondSeedValue = playout.ProgramScheduleAnchors.Head().EnumeratorState.Seed; |
||||
|
||||
firstSeedValue.ShouldBe(secondSeedValue); |
||||
|
||||
playout.Anchor.NextStartOffset.ShouldBe(finish); |
||||
} |
||||
|
||||
[Test] |
||||
public async Task ShuffleFlood_Should_MaintainRandomSeed_MultipleDays() |
||||
{ |
||||
var mediaItems = new List<MediaItem>(); |
||||
for (var i = 1; i <= 25; i++) |
||||
{ |
||||
mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i))); |
||||
} |
||||
|
||||
(PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); |
||||
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5); |
||||
DateTimeOffset finish = start + TimeSpan.FromDays(2); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(53); |
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(2); |
||||
|
||||
playout.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).ShouldBeTrue(); |
||||
PlayoutProgramScheduleAnchor lastCheckpoint = playout.ProgramScheduleAnchors |
||||
.OrderByDescending(a => a.AnchorDate ?? DateTime.MinValue) |
||||
.First(); |
||||
lastCheckpoint.EnumeratorState.Seed.ShouldBeGreaterThan(0); |
||||
lastCheckpoint.EnumeratorState.Index.ShouldBe(3); |
||||
|
||||
// we need to mess up the ordering to trigger the problematic behavior
|
||||
// this simulates the way the rows are loaded with EF
|
||||
PlayoutProgramScheduleAnchor oldest = playout.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate) |
||||
.Last(); |
||||
PlayoutProgramScheduleAnchor newest = playout.ProgramScheduleAnchors.OrderByDescending(a => a.AnchorDate) |
||||
.First(); |
||||
|
||||
playout.ProgramScheduleAnchors = |
||||
[ |
||||
oldest, |
||||
newest |
||||
]; |
||||
|
||||
int firstSeedValue = lastCheckpoint.EnumeratorState.Seed; |
||||
|
||||
DateTimeOffset start2 = start.AddHours(1); |
||||
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2); |
||||
|
||||
PlayoutBuildResult result2 = await builder.Build( |
||||
playout, |
||||
referenceData, |
||||
PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Continue, |
||||
start2, |
||||
finish2, |
||||
CancellationToken); |
||||
|
||||
PlayoutProgramScheduleAnchor continueAnchor = |
||||
playout.ProgramScheduleAnchors.First(x => x.AnchorDate is null); |
||||
int secondSeedValue = continueAnchor.EnumeratorState.Seed; |
||||
|
||||
// the continue anchor should have the same seed as the most recent (last) checkpoint from the first run
|
||||
firstSeedValue.ShouldBe(secondSeedValue); |
||||
} |
||||
|
||||
[Test] |
||||
public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed() |
||||
{ |
||||
var mediaItems = new List<MediaItem> |
||||
{ |
||||
TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), |
||||
TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), |
||||
TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)) |
||||
}; |
||||
|
||||
(PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = |
||||
TestDataFloodForSmartCollectionItems(mediaItems, PlaybackOrder.Shuffle); |
||||
DateTimeOffset start = HoursAfterMidnight(0); |
||||
DateTimeOffset finish = start + TimeSpan.FromHours(6); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(6); |
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(2); |
||||
PlayoutProgramScheduleAnchor primaryAnchor = |
||||
playout.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1); |
||||
primaryAnchor.EnumeratorState.Seed.ShouldBeGreaterThan(0); |
||||
primaryAnchor.EnumeratorState.Index.ShouldBe(0); |
||||
|
||||
int firstSeedValue = primaryAnchor.EnumeratorState.Seed; |
||||
|
||||
DateTimeOffset start2 = HoursAfterMidnight(0); |
||||
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); |
||||
|
||||
PlayoutBuildResult result2 = await builder.Build( |
||||
playout, |
||||
referenceData, |
||||
PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Continue, |
||||
start2, |
||||
finish2, |
||||
CancellationToken); |
||||
|
||||
primaryAnchor = playout.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1); |
||||
int secondSeedValue = primaryAnchor.EnumeratorState.Seed; |
||||
|
||||
firstSeedValue.ShouldBe(secondSeedValue); |
||||
|
||||
primaryAnchor.EnumeratorState.Index.ShouldBe(0); |
||||
} |
||||
|
||||
[Test] |
||||
public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed_MultipleDays() |
||||
{ |
||||
var mediaItems = new List<MediaItem>(); |
||||
for (var i = 1; i <= 100; i++) |
||||
{ |
||||
mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i))); |
||||
} |
||||
|
||||
(PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = |
||||
TestDataFloodForSmartCollectionItems(mediaItems, PlaybackOrder.Shuffle); |
||||
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5); |
||||
DateTimeOffset finish = start + TimeSpan.FromDays(2); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Reset, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(53); |
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(4); |
||||
|
||||
playout.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).ShouldBeTrue(); |
||||
PlayoutProgramScheduleAnchor lastCheckpoint = playout.ProgramScheduleAnchors |
||||
.Filter(psa => psa.SmartCollectionId == 1) |
||||
.OrderByDescending(a => a.AnchorDate ?? DateTime.MinValue) |
||||
.First(); |
||||
lastCheckpoint.EnumeratorState.Seed.ShouldBeGreaterThan(0); |
||||
lastCheckpoint.EnumeratorState.Index.ShouldBe(53); |
||||
|
||||
int firstSeedValue = lastCheckpoint.EnumeratorState.Seed; |
||||
|
||||
for (var i = 1; i < 20; i++) |
||||
{ |
||||
DateTimeOffset start2 = start.AddHours(i); |
||||
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2); |
||||
|
||||
PlayoutBuildResult result2 = await builder.Build( |
||||
playout, |
||||
referenceData, |
||||
PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Continue, |
||||
start2, |
||||
finish2, |
||||
CancellationToken); |
||||
|
||||
PlayoutProgramScheduleAnchor continueAnchor = |
||||
playout.ProgramScheduleAnchors |
||||
.Filter(psa => psa.SmartCollectionId == 1) |
||||
.First(x => x.AnchorDate is null); |
||||
int secondSeedValue = continueAnchor.EnumeratorState.Seed; |
||||
|
||||
// the continue anchor should have the same seed as the most recent (last) checkpoint from the first run
|
||||
firstSeedValue.ShouldBe(secondSeedValue); |
||||
} |
||||
} |
||||
|
||||
[Test] |
||||
public async Task FloodContent_Should_FloodWithFixedStartTime_FromAnchor() |
||||
{ |
||||
var floodCollection = new Collection |
||||
{ |
||||
Id = 1, |
||||
Name = "Flood Items", |
||||
MediaItems = |
||||
[ |
||||
TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1)), |
||||
TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 2, 1)) |
||||
] |
||||
}; |
||||
|
||||
var fixedCollection = new Collection |
||||
{ |
||||
Id = 2, |
||||
Name = "Fixed Items", |
||||
MediaItems = |
||||
[ |
||||
TestMovie(3, TimeSpan.FromHours(2), new DateTime(2020, 1, 1)), |
||||
TestMovie(4, TimeSpan.FromHours(1), new DateTime(2020, 1, 2)) |
||||
] |
||||
}; |
||||
|
||||
var fakeRepository = new FakeMediaCollectionRepository( |
||||
Map( |
||||
(floodCollection.Id, floodCollection.MediaItems.ToList()), |
||||
(fixedCollection.Id, fixedCollection.MediaItems.ToList()))); |
||||
|
||||
var items = new List<ProgramScheduleItem> |
||||
{ |
||||
new ProgramScheduleItemFlood |
||||
{ |
||||
Id = 1, |
||||
Index = 1, |
||||
Collection = floodCollection, |
||||
CollectionId = floodCollection.Id, |
||||
StartTime = TimeSpan.FromHours(7), |
||||
PlaybackOrder = PlaybackOrder.Chronological |
||||
}, |
||||
new ProgramScheduleItemOne |
||||
{ |
||||
Id = 2, |
||||
Index = 2, |
||||
Collection = fixedCollection, |
||||
CollectionId = fixedCollection.Id, |
||||
StartTime = TimeSpan.FromHours(12), |
||||
PlaybackOrder = PlaybackOrder.Chronological |
||||
} |
||||
}; |
||||
|
||||
var playout = new Playout |
||||
{ |
||||
ProgramSchedule = new ProgramSchedule |
||||
{ |
||||
Items = items |
||||
}, |
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, |
||||
Anchor = new PlayoutAnchor |
||||
{ |
||||
NextStart = HoursAfterMidnight(9).UtcDateTime, |
||||
ScheduleItemsEnumeratorState = new CollectionEnumeratorState |
||||
{ |
||||
Index = 0, |
||||
Seed = 1 |
||||
}, |
||||
InFlood = true |
||||
}, |
||||
ProgramScheduleAnchors = [], |
||||
Items = [], |
||||
ProgramScheduleAlternates = [], |
||||
FillGroupIndices = [] |
||||
}; |
||||
|
||||
var referenceData = |
||||
new PlayoutReferenceData(playout.Channel, Option<Deco>.None, [], [], playout.ProgramSchedule, [], []); |
||||
|
||||
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>(); |
||||
var televisionRepo = new FakeTelevisionRepository(); |
||||
IArtistRepository artistRepo = Substitute.For<IArtistRepository>(); |
||||
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = |
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); |
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); |
||||
var builder = new PlayoutBuilder( |
||||
configRepo, |
||||
fakeRepository, |
||||
televisionRepo, |
||||
artistRepo, |
||||
factory, |
||||
localFileSystem, |
||||
Logger); |
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(0); |
||||
DateTimeOffset finish = start + TimeSpan.FromHours(32); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Continue, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(5); |
||||
|
||||
result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(9)); |
||||
result.AddedItems[0].MediaItemId.ShouldBe(1); |
||||
result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(10)); |
||||
result.AddedItems[1].MediaItemId.ShouldBe(2); |
||||
result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(11)); |
||||
result.AddedItems[2].MediaItemId.ShouldBe(1); |
||||
|
||||
result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(12)); |
||||
result.AddedItems[3].MediaItemId.ShouldBe(3); |
||||
|
||||
result.AddedItems[4].StartOffset.ShouldBe(start + TimeSpan.FromHours(31)); |
||||
result.AddedItems[4].MediaItemId.ShouldBe(2); |
||||
|
||||
playout.Anchor.InFlood.ShouldBeTrue(); |
||||
|
||||
playout.Anchor.NextStartOffset.ShouldBe(finish); |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Alternating_MultipleContent_Should_Maintain_Counts() |
||||
{ |
||||
var collectionOne = new Collection |
||||
{ |
||||
Id = 1, |
||||
Name = "Multiple Items 1", |
||||
MediaItems = [TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))] |
||||
}; |
||||
|
||||
var collectionTwo = new Collection |
||||
{ |
||||
Id = 2, |
||||
Name = "Multiple Items 2", |
||||
MediaItems = [TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))] |
||||
}; |
||||
|
||||
var fakeRepository = new FakeMediaCollectionRepository( |
||||
Map( |
||||
(collectionOne.Id, collectionOne.MediaItems.ToList()), |
||||
(collectionTwo.Id, collectionTwo.MediaItems.ToList()))); |
||||
|
||||
var items = new List<ProgramScheduleItem> |
||||
{ |
||||
new ProgramScheduleItemMultiple |
||||
{ |
||||
Id = 1, |
||||
Index = 1, |
||||
Collection = collectionOne, |
||||
CollectionId = collectionOne.Id, |
||||
StartTime = null, |
||||
Count = 3, |
||||
PlaybackOrder = PlaybackOrder.Chronological |
||||
}, |
||||
new ProgramScheduleItemMultiple |
||||
{ |
||||
Id = 2, |
||||
Index = 2, |
||||
Collection = collectionTwo, |
||||
CollectionId = collectionTwo.Id, |
||||
StartTime = null, |
||||
Count = 3, |
||||
PlaybackOrder = PlaybackOrder.Chronological |
||||
} |
||||
}; |
||||
|
||||
var playout = new Playout |
||||
{ |
||||
ProgramSchedule = new ProgramSchedule |
||||
{ |
||||
Items = items |
||||
}, |
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, |
||||
Anchor = new PlayoutAnchor |
||||
{ |
||||
NextStart = HoursAfterMidnight(1).UtcDateTime, |
||||
ScheduleItemsEnumeratorState = new CollectionEnumeratorState |
||||
{ |
||||
Index = 0, |
||||
Seed = 1 |
||||
}, |
||||
MultipleRemaining = 2 |
||||
}, |
||||
ProgramScheduleAnchors = [], |
||||
Items = [], |
||||
ProgramScheduleAlternates = [], |
||||
FillGroupIndices = [] |
||||
}; |
||||
|
||||
var referenceData = |
||||
new PlayoutReferenceData(playout.Channel, Option<Deco>.None, [], [], playout.ProgramSchedule, [], []); |
||||
|
||||
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>(); |
||||
var televisionRepo = new FakeTelevisionRepository(); |
||||
IArtistRepository artistRepo = Substitute.For<IArtistRepository>(); |
||||
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = |
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); |
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); |
||||
var builder = new PlayoutBuilder( |
||||
configRepo, |
||||
fakeRepository, |
||||
televisionRepo, |
||||
artistRepo, |
||||
factory, |
||||
localFileSystem, |
||||
Logger); |
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(0); |
||||
DateTimeOffset finish = start + TimeSpan.FromHours(5); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Continue, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(4); |
||||
|
||||
result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(1)); |
||||
result.AddedItems[0].MediaItemId.ShouldBe(1); |
||||
result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(2)); |
||||
result.AddedItems[1].MediaItemId.ShouldBe(1); |
||||
|
||||
result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(3)); |
||||
result.AddedItems[2].MediaItemId.ShouldBe(2); |
||||
result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(4)); |
||||
result.AddedItems[3].MediaItemId.ShouldBe(2); |
||||
|
||||
playout.Anchor.ScheduleItemsEnumeratorState.Index.ShouldBe(1); |
||||
playout.Anchor.MultipleRemaining.ShouldBe(1); |
||||
playout.Anchor.NextStartOffset.ShouldBe(finish); |
||||
} |
||||
|
||||
[Test] |
||||
public async Task Alternating_Duration_Should_Complete_Duration() |
||||
{ |
||||
var collectionOne = new Collection |
||||
{ |
||||
Id = 1, |
||||
Name = "Duration Items 1", |
||||
MediaItems = [TestMovie(1, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))] |
||||
}; |
||||
|
||||
var collectionTwo = new Collection |
||||
{ |
||||
Id = 2, |
||||
Name = "Duration Items 2", |
||||
MediaItems = [TestMovie(2, TimeSpan.FromHours(1), new DateTime(2020, 1, 1))] |
||||
}; |
||||
|
||||
var fakeRepository = new FakeMediaCollectionRepository( |
||||
Map( |
||||
(collectionOne.Id, collectionOne.MediaItems.ToList()), |
||||
(collectionTwo.Id, collectionTwo.MediaItems.ToList()))); |
||||
|
||||
var items = new List<ProgramScheduleItem> |
||||
{ |
||||
new ProgramScheduleItemDuration |
||||
{ |
||||
Id = 1, |
||||
Index = 1, |
||||
Collection = collectionOne, |
||||
CollectionId = collectionOne.Id, |
||||
StartTime = null, |
||||
PlayoutDuration = TimeSpan.FromHours(3), |
||||
TailMode = TailMode.None, |
||||
PlaybackOrder = PlaybackOrder.Chronological |
||||
}, |
||||
new ProgramScheduleItemDuration |
||||
{ |
||||
Id = 2, |
||||
Index = 2, |
||||
Collection = collectionTwo, |
||||
CollectionId = collectionTwo.Id, |
||||
StartTime = null, |
||||
PlayoutDuration = TimeSpan.FromHours(3), |
||||
TailMode = TailMode.None, |
||||
PlaybackOrder = PlaybackOrder.Chronological |
||||
} |
||||
}; |
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(0); |
||||
DateTimeOffset finish = start + TimeSpan.FromHours(5); |
||||
|
||||
var playout = new Playout |
||||
{ |
||||
ProgramSchedule = new ProgramSchedule |
||||
{ |
||||
Items = items |
||||
}, |
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, |
||||
Anchor = new PlayoutAnchor |
||||
{ |
||||
NextStart = (start + TimeSpan.FromHours(1)).UtcDateTime, |
||||
ScheduleItemsEnumeratorState = new CollectionEnumeratorState |
||||
{ |
||||
Index = 0, |
||||
Seed = 1 |
||||
}, |
||||
DurationFinish = (start + TimeSpan.FromHours(3)).UtcDateTime |
||||
}, |
||||
ProgramScheduleAnchors = [], |
||||
Items = [], |
||||
ProgramScheduleAlternates = [], |
||||
FillGroupIndices = [] |
||||
}; |
||||
|
||||
var referenceData = |
||||
new PlayoutReferenceData(playout.Channel, Option<Deco>.None, [], [], playout.ProgramSchedule, [], []); |
||||
|
||||
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>(); |
||||
var televisionRepo = new FakeTelevisionRepository(); |
||||
IArtistRepository artistRepo = Substitute.For<IArtistRepository>(); |
||||
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = |
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); |
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); |
||||
var builder = new PlayoutBuilder( |
||||
configRepo, |
||||
fakeRepository, |
||||
televisionRepo, |
||||
artistRepo, |
||||
factory, |
||||
localFileSystem, |
||||
Logger); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Continue, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(5); |
||||
|
||||
result.AddedItems[0].StartOffset.ShouldBe(start + TimeSpan.FromHours(1)); |
||||
result.AddedItems[0].MediaItemId.ShouldBe(1); |
||||
result.AddedItems[1].StartOffset.ShouldBe(start + TimeSpan.FromHours(2)); |
||||
result.AddedItems[1].MediaItemId.ShouldBe(1); |
||||
|
||||
result.AddedItems[2].StartOffset.ShouldBe(start + TimeSpan.FromHours(3)); |
||||
result.AddedItems[2].MediaItemId.ShouldBe(2); |
||||
result.AddedItems[3].StartOffset.ShouldBe(start + TimeSpan.FromHours(4)); |
||||
result.AddedItems[3].MediaItemId.ShouldBe(2); |
||||
result.AddedItems[4].StartOffset.ShouldBe(start + TimeSpan.FromHours(5)); |
||||
result.AddedItems[4].MediaItemId.ShouldBe(2); |
||||
|
||||
playout.Anchor.ScheduleItemsEnumeratorState.Index.ShouldBe(0); |
||||
playout.Anchor.DurationFinish.ShouldBeNull(); |
||||
playout.Anchor.NextStartOffset.ShouldBe(start + TimeSpan.FromHours(6)); |
||||
} |
||||
} |
||||
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using Shouldly; |
||||
using NUnit.Framework; |
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling; |
||||
|
||||
public class GetStartTimeAfterTests |
||||
{ |
||||
[Test] |
||||
[Ignore("This test isn't ready to run yet")] |
||||
public void Should_Return_Correct_Time_On_Dst_Fall_Back() |
||||
{ |
||||
var scheduleItem = new ProgramScheduleItemOne |
||||
{ |
||||
StartTime = TimeSpan.FromHours(3) |
||||
}; |
||||
|
||||
var state = new PlayoutBuilderState( |
||||
0, |
||||
null, |
||||
Option<int>.None, |
||||
Option<DateTimeOffset>.None, |
||||
false, |
||||
false, |
||||
0, |
||||
DateTimeOffset.Parse("2025-11-02T00:00:00-05:00")); |
||||
|
||||
DateTimeOffset result = PlayoutModeSchedulerBase<ProgramScheduleItem>.GetStartTimeAfter(state, scheduleItem); |
||||
|
||||
result.ShouldBe(DateTimeOffset.Parse("2025-11-02T02:00:00-06:00")); |
||||
} |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,203 @@
@@ -0,0 +1,203 @@
|
||||
using Destructurama; |
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Filler; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using ErsatzTV.Core.Tests.Fakes; |
||||
using Microsoft.Extensions.Logging; |
||||
using NSubstitute; |
||||
using NUnit.Framework; |
||||
using Serilog; |
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling; |
||||
|
||||
public abstract class PlayoutBuilderTestBase |
||||
{ |
||||
[SetUp] |
||||
public void SetUp() => CancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; |
||||
|
||||
protected readonly ILogger<PlayoutBuilder> Logger; |
||||
protected CancellationToken CancellationToken; |
||||
|
||||
protected PlayoutBuilderTestBase() |
||||
{ |
||||
Log.Logger = new LoggerConfiguration() |
||||
.MinimumLevel.Debug() |
||||
.WriteTo.Console() |
||||
.Destructure.UsingAttributes() |
||||
.CreateLogger(); |
||||
|
||||
ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); |
||||
|
||||
Logger = loggerFactory.CreateLogger<PlayoutBuilder>(); |
||||
} |
||||
|
||||
protected static DateTimeOffset HoursAfterMidnight(int hours) |
||||
{ |
||||
DateTimeOffset now = DateTimeOffset.Now; |
||||
return now - now.TimeOfDay + TimeSpan.FromHours(hours); |
||||
|
||||
// // pick a timezone that has DST and a known offset on a specific date
|
||||
// TimeZoneInfo eastern = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
|
||||
// DateTime date = new DateTime(2025, 11, 2, 0, 0, 0, DateTimeKind.Unspecified);
|
||||
// DateTimeOffset now = new DateTimeOffset(date, eastern.GetUtcOffset(date));
|
||||
// return now.Date + TimeSpan.FromHours(hours);
|
||||
} |
||||
|
||||
protected TestData TestDataFloodForItems( |
||||
List<MediaItem> mediaItems, |
||||
PlaybackOrder playbackOrder, |
||||
IConfigElementRepository configMock = null) |
||||
{ |
||||
var mediaCollection = new Collection |
||||
{ |
||||
Id = 1, |
||||
MediaItems = mediaItems |
||||
}; |
||||
|
||||
IConfigElementRepository configRepo = configMock ?? Substitute.For<IConfigElementRepository>(); |
||||
|
||||
var collectionRepo = new FakeMediaCollectionRepository(Map((mediaCollection.Id, mediaItems))); |
||||
var televisionRepo = new FakeTelevisionRepository(); |
||||
IArtistRepository artistRepo = Substitute.For<IArtistRepository>(); |
||||
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = |
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); |
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); |
||||
var builder = new PlayoutBuilder( |
||||
configRepo, |
||||
collectionRepo, |
||||
televisionRepo, |
||||
artistRepo, |
||||
factory, |
||||
localFileSystem, |
||||
Logger); |
||||
|
||||
var items = new List<ProgramScheduleItem> { Flood(mediaCollection, playbackOrder) }; |
||||
|
||||
var playout = new Playout |
||||
{ |
||||
Id = 1, |
||||
ProgramSchedule = new ProgramSchedule { Items = items }, |
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, |
||||
Items = [], |
||||
ProgramScheduleAnchors = [], |
||||
ProgramScheduleAlternates = [], |
||||
FillGroupIndices = [] |
||||
}; |
||||
|
||||
var referenceData = new PlayoutReferenceData(playout.Channel, Option<Deco>.None, [], [], playout.ProgramSchedule, [], []); |
||||
|
||||
return new TestData(builder, playout, referenceData); |
||||
} |
||||
|
||||
protected static Movie TestMovie(int id, TimeSpan duration, DateTime aired) => |
||||
new() |
||||
{ |
||||
Id = id, |
||||
MovieMetadata = [new MovieMetadata { ReleaseDate = aired }], |
||||
MediaVersions = |
||||
[ |
||||
new MediaVersion |
||||
{ |
||||
Duration = duration, MediaFiles = [new MediaFile { Path = $"/fake/path/{id}" }] |
||||
} |
||||
] |
||||
}; |
||||
|
||||
private static ProgramScheduleItem Flood(Collection mediaCollection, PlaybackOrder playbackOrder) => |
||||
new ProgramScheduleItemFlood |
||||
{ |
||||
Id = 1, |
||||
Index = 1, |
||||
CollectionType = ProgramScheduleItemCollectionType.Collection, |
||||
Collection = mediaCollection, |
||||
CollectionId = mediaCollection.Id, |
||||
StartTime = null, |
||||
PlaybackOrder = playbackOrder |
||||
}; |
||||
|
||||
private static ProgramScheduleItem Flood( |
||||
SmartCollection smartCollection, |
||||
SmartCollection fillerCollection, |
||||
PlaybackOrder playbackOrder) => |
||||
new ProgramScheduleItemFlood |
||||
{ |
||||
Id = 1, |
||||
Index = 1, |
||||
CollectionType = ProgramScheduleItemCollectionType.SmartCollection, |
||||
SmartCollection = smartCollection, |
||||
SmartCollectionId = smartCollection.Id, |
||||
StartTime = null, |
||||
PlaybackOrder = playbackOrder, |
||||
FallbackFiller = new FillerPreset |
||||
{ |
||||
Id = 1, |
||||
CollectionType = ProgramScheduleItemCollectionType.SmartCollection, |
||||
SmartCollection = fillerCollection, |
||||
SmartCollectionId = fillerCollection.Id, |
||||
FillerKind = FillerKind.Fallback |
||||
} |
||||
}; |
||||
|
||||
protected TestData TestDataFloodForSmartCollectionItems( |
||||
List<MediaItem> mediaItems, |
||||
PlaybackOrder playbackOrder, |
||||
IConfigElementRepository configMock = null) |
||||
{ |
||||
var mediaCollection = new SmartCollection |
||||
{ |
||||
Id = 1, |
||||
Query = "asdf" |
||||
}; |
||||
|
||||
var fillerCollection = new SmartCollection |
||||
{ |
||||
Id = 2, |
||||
Query = "qwerty" |
||||
}; |
||||
|
||||
IConfigElementRepository configRepo = configMock ?? Substitute.For<IConfigElementRepository>(); |
||||
|
||||
var collectionRepo = new FakeMediaCollectionRepository( |
||||
Map( |
||||
(mediaCollection.Id, mediaItems), |
||||
(fillerCollection.Id, mediaItems.Take(1).ToList()) |
||||
) |
||||
); |
||||
var televisionRepo = new FakeTelevisionRepository(); |
||||
IArtistRepository artistRepo = Substitute.For<IArtistRepository>(); |
||||
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = |
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); |
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); |
||||
var builder = new PlayoutBuilder( |
||||
configRepo, |
||||
collectionRepo, |
||||
televisionRepo, |
||||
artistRepo, |
||||
factory, |
||||
localFileSystem, |
||||
Logger); |
||||
|
||||
var items = new List<ProgramScheduleItem> { Flood(mediaCollection, fillerCollection, playbackOrder) }; |
||||
|
||||
var playout = new Playout |
||||
{ |
||||
Id = 1, |
||||
ProgramSchedule = new ProgramSchedule { Items = items }, |
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, |
||||
Items = [], |
||||
ProgramScheduleAnchors = [], |
||||
ProgramScheduleAlternates = [], |
||||
FillGroupIndices = [] |
||||
}; |
||||
|
||||
var referenceData = new PlayoutReferenceData(playout.Channel, Option<Deco>.None, [], [], playout.ProgramSchedule, [], []); |
||||
|
||||
return new TestData(builder, playout, referenceData); |
||||
} |
||||
|
||||
protected record TestData(PlayoutBuilder Builder, Playout Playout, PlayoutReferenceData ReferenceData); |
||||
} |
||||
@ -0,0 +1,127 @@
@@ -0,0 +1,127 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Domain.Scheduling; |
||||
using ErsatzTV.Core.Interfaces.Metadata; |
||||
using ErsatzTV.Core.Interfaces.Repositories; |
||||
using ErsatzTV.Core.Interfaces.Scheduling; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using ErsatzTV.Core.Tests.Fakes; |
||||
using NSubstitute; |
||||
using NUnit.Framework; |
||||
using Shouldly; |
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling; |
||||
|
||||
[TestFixture] |
||||
public class RefreshPlayoutTests : PlayoutBuilderTestBase |
||||
{ |
||||
[Test] |
||||
public async Task Two_Day_Playout_Should_Refresh_From_Midnight_Anchor() |
||||
{ |
||||
var collectionOne = new Collection |
||||
{ |
||||
Id = 1, |
||||
Name = "Duration Items 1", |
||||
MediaItems = |
||||
[ |
||||
TestMovie(1, TimeSpan.FromHours(6), new DateTime(2002, 1, 1)), |
||||
TestMovie(2, TimeSpan.FromHours(6), new DateTime(2003, 1, 1)), |
||||
TestMovie(3, TimeSpan.FromHours(6), new DateTime(2004, 1, 1)) |
||||
] |
||||
}; |
||||
|
||||
var fakeRepository = |
||||
new FakeMediaCollectionRepository(Map((collectionOne.Id, collectionOne.MediaItems.ToList()))); |
||||
|
||||
var items = new List<ProgramScheduleItem> |
||||
{ |
||||
new ProgramScheduleItemFlood |
||||
{ |
||||
Id = 1, |
||||
Index = 1, |
||||
Collection = collectionOne, |
||||
CollectionId = collectionOne.Id, |
||||
StartTime = null, |
||||
PlaybackOrder = PlaybackOrder.Chronological |
||||
} |
||||
}; |
||||
|
||||
var playout = new Playout |
||||
{ |
||||
ProgramSchedule = new ProgramSchedule |
||||
{ |
||||
Items = items |
||||
}, |
||||
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" }, |
||||
|
||||
// this should be ignored
|
||||
Anchor = new PlayoutAnchor |
||||
{ |
||||
NextStart = HoursAfterMidnight(1).UtcDateTime, |
||||
ScheduleItemsEnumeratorState = new CollectionEnumeratorState |
||||
{ |
||||
Index = 0, |
||||
Seed = 1 |
||||
}, |
||||
DurationFinish = HoursAfterMidnight(3).UtcDateTime |
||||
}, |
||||
|
||||
ProgramScheduleAnchors = [], |
||||
Items = [], |
||||
ProgramScheduleAlternates = [], |
||||
FillGroupIndices = [] |
||||
}; |
||||
|
||||
playout.ProgramScheduleAnchors.Add( |
||||
new PlayoutProgramScheduleAnchor |
||||
{ |
||||
AnchorDate = HoursAfterMidnight(24).UtcDateTime, |
||||
Collection = collectionOne, |
||||
CollectionId = collectionOne.Id, |
||||
CollectionType = ProgramScheduleItemCollectionType.Collection, |
||||
EnumeratorState = new CollectionEnumeratorState |
||||
{ |
||||
Index = 1, |
||||
Seed = 12345 |
||||
}, |
||||
Playout = playout |
||||
}); |
||||
|
||||
var referenceData = new PlayoutReferenceData(playout.Channel, Option<Deco>.None, [], [], playout.ProgramSchedule, [], []); |
||||
|
||||
IConfigElementRepository configRepo = Substitute.For<IConfigElementRepository>(); |
||||
var televisionRepo = new FakeTelevisionRepository(); |
||||
IArtistRepository artistRepo = Substitute.For<IArtistRepository>(); |
||||
IMultiEpisodeShuffleCollectionEnumeratorFactory factory = |
||||
Substitute.For<IMultiEpisodeShuffleCollectionEnumeratorFactory>(); |
||||
ILocalFileSystem localFileSystem = Substitute.For<ILocalFileSystem>(); |
||||
var builder = new PlayoutBuilder( |
||||
configRepo, |
||||
fakeRepository, |
||||
televisionRepo, |
||||
artistRepo, |
||||
factory, |
||||
localFileSystem, |
||||
Logger); |
||||
|
||||
DateTimeOffset start = HoursAfterMidnight(24); |
||||
DateTimeOffset finish = start + TimeSpan.FromDays(1); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, PlayoutBuildMode.Refresh, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(4); |
||||
result.AddedItems[0].MediaItemId.ShouldBe(2); |
||||
result.AddedItems[0].StartOffset.TimeOfDay.ShouldBe(TimeSpan.Zero); |
||||
result.AddedItems[0].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(6)); |
||||
result.AddedItems[1].MediaItemId.ShouldBe(3); |
||||
result.AddedItems[1].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(6)); |
||||
result.AddedItems[1].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(12)); |
||||
result.AddedItems[2].MediaItemId.ShouldBe(1); |
||||
result.AddedItems[2].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(12)); |
||||
result.AddedItems[2].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(18)); |
||||
result.AddedItems[3].MediaItemId.ShouldBe(2); |
||||
result.AddedItems[3].StartOffset.TimeOfDay.ShouldBe(TimeSpan.FromHours(18)); |
||||
result.AddedItems[3].FinishOffset.TimeOfDay.ShouldBe(TimeSpan.Zero); |
||||
|
||||
playout.Anchor.NextStartOffset.ShouldBe(HoursAfterMidnight(48)); |
||||
} |
||||
} |
||||
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
using ErsatzTV.Core.Domain; |
||||
using ErsatzTV.Core.Scheduling; |
||||
using NUnit.Framework; |
||||
using Shouldly; |
||||
|
||||
namespace ErsatzTV.Core.Tests.Scheduling.ClassicScheduling; |
||||
|
||||
[TestFixture] |
||||
public class ResetPlayoutTests : PlayoutBuilderTestBase |
||||
{ |
||||
[Test] |
||||
public async Task ShuffleFlood_Should_IgnoreAnchors() |
||||
{ |
||||
var mediaItems = new List<MediaItem> |
||||
{ |
||||
TestMovie(1, TimeSpan.FromHours(1), DateTime.Today), |
||||
TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)), |
||||
TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(2)), |
||||
TestMovie(4, TimeSpan.FromHours(1), DateTime.Today.AddHours(3)), |
||||
TestMovie(5, TimeSpan.FromHours(1), DateTime.Today.AddHours(4)), |
||||
TestMovie(6, TimeSpan.FromHours(1), DateTime.Today.AddHours(5)) |
||||
}; |
||||
|
||||
(PlayoutBuilder builder, Playout playout, PlayoutReferenceData referenceData) = |
||||
TestDataFloodForItems(mediaItems, PlaybackOrder.Shuffle); |
||||
DateTimeOffset start = HoursAfterMidnight(0); |
||||
DateTimeOffset finish = start + TimeSpan.FromHours(6); |
||||
|
||||
PlayoutBuildResult result = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Reset, start, finish, CancellationToken); |
||||
|
||||
result.AddedItems.Count.ShouldBe(6); |
||||
playout.Anchor.NextStartOffset.ShouldBe(finish); |
||||
|
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(1); |
||||
playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(0); |
||||
|
||||
int firstSeedValue = playout.ProgramScheduleAnchors.Head().EnumeratorState.Seed; |
||||
|
||||
DateTimeOffset start2 = HoursAfterMidnight(0); |
||||
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6); |
||||
|
||||
PlayoutBuildResult result2 = await builder.Build(playout, referenceData, PlayoutBuildResult.Empty, |
||||
PlayoutBuildMode.Reset, start2, finish2, CancellationToken); |
||||
|
||||
result2.AddedItems.Count.ShouldBe(6); |
||||
playout.Anchor.NextStartOffset.ShouldBe(finish); |
||||
|
||||
playout.ProgramScheduleAnchors.Count.ShouldBe(1); |
||||
playout.ProgramScheduleAnchors.Head().EnumeratorState.Index.ShouldBe(0); |
||||
|
||||
int secondSeedValue = playout.ProgramScheduleAnchors.Head().EnumeratorState.Seed; |
||||
|
||||
firstSeedValue.ShouldNotBe(secondSeedValue); |
||||
} |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue