Refactor GridScheduleGenerator and related components to improve cursor management and group handling
Updated the GridScheduleGenerator to reset cursors for slots with no aired positions, ensuring proper state management during rebuilds. Introduced a new method, ResetUnairedCursorsAsync, to handle cursor resets effectively. Enhanced the handling of interstitial groups in the scheduling process, preventing them from being included in slots or fallback groups. Updated related tests to verify the correct behavior of these changes, ensuring robust scheduling logic and accurate group categorization.
This commit is contained in:
@@ -228,6 +228,44 @@ public class GenerateGridTests
|
||||
Assert.Equal("Сериалы", morning.GroupName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InterstitialGroup_GoesNeitherIntoSlots_NorIntoFallback()
|
||||
{
|
||||
// Группа рекламы — материал для стыков. По размеру она бьёт любую библиотеку, поэтому
|
||||
// раньше выигрывала каждую полосу без предпочтения по типу и становилась аварийной:
|
||||
// в эфир уходила полоса роликов, подписанная как программа.
|
||||
var fixture = new TestDb();
|
||||
var series = Playable("Сериал", ShowKind.Series, 40, 25);
|
||||
var films = Films(8, 100);
|
||||
var ads = Enumerable
|
||||
.Range(0, 60)
|
||||
.Select(i => Playable($"Ролик {i}", ShowKind.Interstitial, 1, 1))
|
||||
.ToList();
|
||||
|
||||
var adsGroup = GroupOf("Реклама", [.. ads.Select(a => a.Show.Id)]);
|
||||
var (channelId, _) = await SeedChannelAsync(
|
||||
fixture,
|
||||
[series.Show, .. films.Shows, .. ads.Select(a => a.Show)],
|
||||
[.. series.Assets, .. films.Assets, .. ads.SelectMany(a => a.Assets)],
|
||||
[
|
||||
GroupOf("Сериалы", series.Show.Id),
|
||||
GroupOf("Фильмы", [.. films.Shows.Select(s => s.Id)]),
|
||||
adsGroup,
|
||||
]
|
||||
);
|
||||
|
||||
var plan = await PlanAsync(
|
||||
fixture,
|
||||
channelId,
|
||||
GridProfileKind.Mixed,
|
||||
GridGenerationMode.Rebuild
|
||||
);
|
||||
|
||||
Assert.DoesNotContain(plan.Slots, s => s.GroupId == adsGroup.Id);
|
||||
Assert.NotNull(plan.FallbackGroupId);
|
||||
Assert.NotEqual(adsGroup.Id, plan.FallbackGroupId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ChildrenBand_SkipsAdultGroup()
|
||||
{
|
||||
|
||||
@@ -559,6 +559,37 @@ public class SchedulePlannerTests
|
||||
Assert.Contains(result.Items, i => i.SlotId == background.SlotId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pad_CapCountsWholeSeam_WhenBackgroundHasNothingToPlay()
|
||||
{
|
||||
// Часовая дыра и пустой фоновый слой: добор до якоря и остаток перед ним сходятся встык,
|
||||
// и без общего потолка в эфир ушло бы двадцать минут рекламы подряд вместо десяти.
|
||||
var result = Run(
|
||||
Input(
|
||||
[Slot(T0.AddHours(1), 30, [Element(1, 30)], isAnchor: true)],
|
||||
horizonHours: 2,
|
||||
pad: [Ad(5)],
|
||||
maxPad: TimeSpan.FromMinutes(10)
|
||||
)
|
||||
);
|
||||
|
||||
// Считается самый длинный рекламный блок подряд: два добора по разные стороны от фона —
|
||||
// это два шва, а сошедшиеся встык — один, и он потолком и ограничен.
|
||||
var longest = TimeSpan.Zero;
|
||||
var current = TimeSpan.Zero;
|
||||
foreach (var item in result.Items.OrderBy(i => i.StartsAtUtc))
|
||||
{
|
||||
current =
|
||||
item.Kind == PlannedItemKind.Ad
|
||||
? current + (item.EndsAtUtc - item.StartsAtUtc)
|
||||
: TimeSpan.Zero;
|
||||
if (current > longest)
|
||||
longest = current;
|
||||
}
|
||||
|
||||
Assert.Equal(TimeSpan.FromMinutes(10), longest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pad_ClosesTailWhenProgrammeDoesNotFit()
|
||||
{
|
||||
|
||||
@@ -163,6 +163,46 @@ public sealed class GridScheduleGeneratorIntegrationTests(PostgresFixture fixtur
|
||||
Assert.Equal(aired.EpisodeIndex + 1, firstAfterRebuild.EpisodeIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Канал, который ещё не вышел в эфир: применять сетку можно сколько угодно раз, и каждый раз
|
||||
/// эфир обязан начинаться с начала. Отматывать здесь не к чему — отыгранного нет вовсе, — и без
|
||||
/// сброса курсора второе применение начинало бы сериал с той серии, до которой курсор домотал
|
||||
/// по только что снесённому хвосту.
|
||||
/// </summary>
|
||||
[SkippableFact]
|
||||
public async Task Generate_Rebuild_StartsOver_WhenNothingHasAiredYet()
|
||||
{
|
||||
Skip.IfNot(fixture.Available, "Docker недоступен");
|
||||
|
||||
await using var seedDb = fixture.CreateContext();
|
||||
var world = await SeedAsync(seedDb, episodes: 100);
|
||||
|
||||
await using var first = fixture.CreateContext();
|
||||
await Generator(first).GenerateAsync(world.ChannelId, Now, false, default);
|
||||
|
||||
await using var beforeDb = fixture.CreateContext();
|
||||
// Ничего не отыграно: вся построенная лента — будущее.
|
||||
Assert.Empty(
|
||||
beforeDb.ScheduleEntries.Where(e =>
|
||||
e.ChannelId == world.ChannelId && e.StartsAtUtc < Now
|
||||
)
|
||||
);
|
||||
Assert.True(beforeDb.SlotStates.Single(s => s.SlotId == world.SlotId).NextUnitIndex > 0);
|
||||
|
||||
await using var rebuild = fixture.CreateContext();
|
||||
await Generator(rebuild).GenerateAsync(world.ChannelId, Now, true, default);
|
||||
|
||||
await using var verify = fixture.CreateContext();
|
||||
var firstProgram = verify
|
||||
.ScheduleEntries.Where(e =>
|
||||
e.ChannelId == world.ChannelId && e.Kind == ScheduleEntryKind.Program
|
||||
)
|
||||
.OrderBy(e => e.StartsAtUtc)
|
||||
.First();
|
||||
|
||||
Assert.Equal(0, firstProgram.EpisodeIndex);
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public async Task Generate_CollectionInGroup_StampsCollectionOnEntries()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user