Enhance EffectiveGridBuilder to support background slots and improve scheduling logic
Updated the EffectiveGridBuilder to return an EffectiveGrid containing both scheduled and background slots, allowing for better management of overlapping content. Refactored the ResolveDay method to handle background slots separately, ensuring they fill gaps in the schedule appropriately. Adjusted related components, including GridScheduleGenerator and SchedulePlanner, to accommodate the new structure and improve overall scheduling accuracy. Enhanced documentation and comments for clarity.
This commit is contained in:
@@ -74,7 +74,8 @@ public class SchedulePlannerTests
|
||||
IReadOnlyList<PlanningSlot> slots,
|
||||
DateTimeOffset? start = null,
|
||||
int horizonHours = 6,
|
||||
IReadOnlyList<PlanningUnit>? fallback = null
|
||||
IReadOnlyList<PlanningUnit>? fallback = null,
|
||||
IReadOnlyList<PlanningSlot>? background = null
|
||||
) =>
|
||||
new(
|
||||
Guid.NewGuid(),
|
||||
@@ -82,7 +83,9 @@ public class SchedulePlannerTests
|
||||
(start ?? T0).AddHours(horizonHours),
|
||||
slots,
|
||||
fallback ?? [Unit(1, Guid.NewGuid(), 0)],
|
||||
SegmentSeconds: 2
|
||||
SegmentSeconds: 2,
|
||||
UtcOffsetMinutes: 0,
|
||||
background ?? []
|
||||
);
|
||||
|
||||
private static PlanningResult Run(PlanningInput input) =>
|
||||
@@ -347,6 +350,184 @@ public class SchedulePlannerTests
|
||||
Assert.Equal(element.ElementId, program.Trace.ElementId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Background_PlaysInLeftoverInsteadOfEmergencyFiller()
|
||||
{
|
||||
// Слот на полчаса при часовом горизонте: остаток по замыслу принадлежит фоновому слою.
|
||||
var backgroundElement = Element(4, 10);
|
||||
var background = Slot(T0, 24 * 60, [backgroundElement]);
|
||||
|
||||
var result = Run(
|
||||
Input([Slot(T0, 30, [Element(1, 30)])], horizonHours: 1, background: [background])
|
||||
);
|
||||
|
||||
var filled = result
|
||||
.Items.Where(i => i.SlotId == background.SlotId)
|
||||
.OrderBy(i => i.StartsAtUtc)
|
||||
.ToList();
|
||||
Assert.Equal(3, filled.Count);
|
||||
// Фон — это программа своего слота, а не аварийный запас.
|
||||
Assert.All(filled, item => Assert.Equal(PlannedItemKind.Program, item.Kind));
|
||||
Assert.All(
|
||||
filled,
|
||||
item => Assert.Equal(backgroundElement.ElementId, item.Trace!.ElementId)
|
||||
);
|
||||
Assert.DoesNotContain(result.Items, i => i.Kind == PlannedItemKind.Fallback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Background_ContinuesAcrossGaps()
|
||||
{
|
||||
var backgroundElement = Element(6, 10);
|
||||
var background = Slot(T0, 24 * 60, [backgroundElement]);
|
||||
|
||||
var result = Run(
|
||||
Input(
|
||||
[Slot(T0, 20, [Element(1, 20)]), Slot(T0.AddMinutes(40), 20, [Element(1, 20)])],
|
||||
horizonHours: 2,
|
||||
background: [background]
|
||||
)
|
||||
);
|
||||
|
||||
var filled = result
|
||||
.Items.Where(i => i.SlotId == background.SlotId)
|
||||
.OrderBy(i => i.StartsAtUtc)
|
||||
.Select(i => i.UnitIndex)
|
||||
.ToList();
|
||||
// Вторая пауза продолжает сериал с той серии, на которой остановилась первая; исчерпав
|
||||
// элемент, последовательная стратегия начинает его заново — это её штатное поведение.
|
||||
Assert.Equal([0, 1, 2, 3, 4, 5], filled.Take(6));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Background_FallsBackToEmergencyWhenEpisodeDoesNotFit()
|
||||
{
|
||||
var background = Slot(T0, 24 * 60, [Element(4, 30)]);
|
||||
|
||||
var result = Run(
|
||||
Input(
|
||||
[Slot(T0, 50, [Element(1, 50)])],
|
||||
horizonHours: 1,
|
||||
fallback: [Unit(2, Guid.Empty, 0)],
|
||||
background: [background]
|
||||
)
|
||||
);
|
||||
|
||||
// Остаток — 10 минут, серия фона длиннее: хвост добирает аварийный запас, а не дыра.
|
||||
Assert.DoesNotContain(result.Items, i => i.SlotId == background.SlotId);
|
||||
Assert.Equal(5, result.Items.Count(i => i.Kind == PlannedItemKind.Fallback));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Background_FillsEmptyRepeatSlot()
|
||||
{
|
||||
var backgroundElement = Element(6, 10);
|
||||
var background = Slot(T0, 24 * 60, [backgroundElement]);
|
||||
var repeat = Slot(T0, 60, []) with { SlotKind = SlotKind.Repeat, RepeatUnits = [] };
|
||||
|
||||
var result = Run(Input([repeat], horizonHours: 1, background: [background]));
|
||||
|
||||
// Свежему каналу повторять нечего — по замыслу это место отдаётся фоновому слою.
|
||||
Assert.Contains(result.Warnings, w => w.Kind == PlanningWarningKind.RepeatSourceEmpty);
|
||||
Assert.Equal(6, result.Items.Count(i => i.SlotId == background.SlotId));
|
||||
Assert.DoesNotContain(result.Items, i => i.Kind == PlannedItemKind.Fallback);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Background_CursorIsSavedOnlyForOverlappedSlots()
|
||||
{
|
||||
var backgroundElement = Element(6, 10);
|
||||
var background = Slot(T0, 24 * 60, [backgroundElement]);
|
||||
|
||||
var result = Run(
|
||||
Input([Slot(T0, 30, [Element(1, 30)])], horizonHours: 1, background: [background])
|
||||
);
|
||||
|
||||
var cursor = Assert.Single(result.Cursors, c => c.SlotId == background.SlotId);
|
||||
Assert.Equal(backgroundElement.ElementId, cursor.ElementId);
|
||||
Assert.Equal(3, cursor.NextUnitIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fallback_KeepsShowOfItsUnits()
|
||||
{
|
||||
var showId = Guid.NewGuid();
|
||||
// Слот на 30 минут при часовом горизонте: остаток закрывает фон.
|
||||
var result = Run(
|
||||
Input([Slot(T0, 30, [Element(1, 30)])], horizonHours: 1, fallback: [Unit(5, showId, 3)])
|
||||
);
|
||||
|
||||
var fallback = result.Items.Where(i => i.Kind == PlannedItemKind.Fallback).ToList();
|
||||
Assert.NotEmpty(fallback);
|
||||
// Фон — это программы из группы: без шоу в ленте на их месте прочерк.
|
||||
Assert.All(fallback, item => Assert.Equal(showId, item.ShowId));
|
||||
Assert.All(fallback, item => Assert.Equal(3, item.UnitIndex));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fallback_AnonymousFillerHasNoShow()
|
||||
{
|
||||
// Аварийный филлер канала приходит без шоу — подписывать его нечем и не нужно.
|
||||
var result = Run(
|
||||
Input(
|
||||
[Slot(T0, 30, [Element(1, 30)])],
|
||||
horizonHours: 1,
|
||||
fallback: [Unit(5, Guid.Empty, 0)]
|
||||
)
|
||||
);
|
||||
|
||||
Assert.All(
|
||||
result.Items.Where(i => i.Kind == PlannedItemKind.Fallback),
|
||||
item => Assert.Null(item.ShowId)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fallback_RotatesUnitsBetweenGaps()
|
||||
{
|
||||
var first = Unit(5, Guid.NewGuid(), 0);
|
||||
var second = Unit(5, Guid.NewGuid(), 0);
|
||||
|
||||
var result = Run(
|
||||
Input(
|
||||
[Slot(T0, 30, [Element(1, 30)]), Slot(T0.AddMinutes(40), 30, [Element(1, 30)])],
|
||||
horizonHours: 2,
|
||||
fallback: [first, second]
|
||||
)
|
||||
);
|
||||
|
||||
var fallback = result
|
||||
.Items.Where(i => i.Kind == PlannedItemKind.Fallback)
|
||||
.OrderBy(i => i.StartsAtUtc)
|
||||
.ToList();
|
||||
// Пауза продолжает перебор с того места, где остановилась прошлая: иначе ночь выглядит
|
||||
// одним зациклённым куском.
|
||||
Assert.Equal(first.MediaAssetId, fallback[0].MediaAssetId);
|
||||
Assert.Equal(second.MediaAssetId, fallback[1].MediaAssetId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fallback_TakesShorterUnitWhenNextDoesNotFit()
|
||||
{
|
||||
var showId = Guid.NewGuid();
|
||||
// Список отсортирован по возрастанию, как его готовит оркестратор.
|
||||
var result = Run(
|
||||
Input(
|
||||
[Slot(T0, 50, [Element(1, 50)])],
|
||||
horizonHours: 1,
|
||||
fallback: [Unit(2, showId, 0), Unit(30, showId, 1)]
|
||||
)
|
||||
);
|
||||
|
||||
// Остаток — 10 минут: длинная единица не влезает, и вместо дыры в ленте идут короткие.
|
||||
var fallback = result.Items.Where(i => i.Kind == PlannedItemKind.Fallback).ToList();
|
||||
Assert.Equal(5, fallback.Count);
|
||||
Assert.All(
|
||||
fallback,
|
||||
item => Assert.Equal(TimeSpan.FromMinutes(2), item.EndsAtUtc - item.StartsAtUtc)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoFallbackUnits_ProducesWarning()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user