Enhance GridScheduleGenerator and related components for improved slot handling and cursor management
ci / build-backend (push) Successful in 1m21s
ci / build-frontend (push) Successful in 51s
ci / tests (push) Successful in 1m40s
ci / sonar (push) Successful in 3m46s

Updated the GridScheduleGenerator to support rebuilding future schedules while correctly managing aired positions. Introduced a new AiredPosition record to track the last aired state of slots, ensuring that the cursor rewinds to the correct position during rebuilds. Refactored the BuildInputAsync method to accept aired positions, and modified the LoadAiredPositionsAsync method for accurate retrieval of past entries. Enhanced the SchedulePlanner to utilize shared cursor states between main and background loops, preventing duplicate series plays. Updated documentation to reflect these changes and added integration tests to verify the correct behavior of the new functionality.
This commit is contained in:
Leonid Pershin
2026-07-29 08:57:46 +03:00
parent c67d1f7752
commit fbfc6e677e
11 changed files with 568 additions and 117 deletions
@@ -368,6 +368,22 @@ public class SchedulePlannerTests
);
}
/// <summary>Источник нашёлся, но слот короче — это другая беда, и молчать о ней нельзя.</summary>
[Fact]
public void RepeatSlot_WithSourceTooLongForSlot_Warns()
{
var evening = Slot(T0, 60, [Element(1, 60)]);
var morning = Slot(T0.AddHours(2), 30, []) with
{
SlotKind = SlotKind.Repeat,
RepeatWindow = new PlanningRepeatWindow(T0, T0.AddHours(1)),
};
var result = Run(Input([evening, morning], horizonHours: 4));
Assert.Contains(result.Warnings, w => w.Kind == PlanningWarningKind.RepeatSourceEmpty);
}
/// <summary>
/// Старты эластичные: фильм законно начинается раньше объявленного времени, и окно повтора
/// обязано ловить его по пересечению, а не по попаданию старта внутрь.
@@ -391,6 +407,96 @@ public class SchedulePlannerTests
);
}
/// <summary>
/// Слот приходит по экземпляру на каждые свои сутки, и курсор у всех этих экземпляров один —
/// снимок из базы. Без общей позиции вторник переигрывал бы понедельник.
/// </summary>
[Fact]
public void SameSlot_ContinuesAcrossItsDays_WithinOneRun()
{
var element = Element(4, 30);
var monday = Slot(T0, 60, [element]);
var tuesday = monday with { TargetStartUtc = T0.AddHours(3) };
var result = Run(Input([monday, tuesday], horizonHours: 6));
var played = result
.Items.Where(i => i.Kind == PlannedItemKind.Program && i.SlotId == monday.SlotId)
.Select(i => i.MediaAssetId)
.ToList();
Assert.Equal(4, played.Count);
Assert.Equal(4, played.Distinct().Count());
}
/// <summary>
/// Тот же слот бывает перекрыт в одни сутки и свободен в другие: фон и основной цикл ведут его
/// вместе, и второй счётчик позиции переигрывал бы уже поставленные серии.
/// </summary>
[Fact]
public void Background_AndMainLoop_ShareOneSlotPosition()
{
var element = Element(8, 30);
var background = Slot(T0, 360, [element]);
var main = background with { TargetStartUtc = T0.AddHours(2), TargetDurationMinutes = 60 };
var result = Run(Input([main], horizonHours: 4, fallback: [], background: [background]));
var played = result
.Items.Where(i => i.Kind == PlannedItemKind.Program)
.Select(i => i.MediaAssetId)
.ToList();
Assert.Equal(8, played.Count);
Assert.Equal(8, played.Distinct().Count());
}
/// <summary>Остывание обязано видеть то, что прогон поставил сам: горизонт строится разом.</summary>
[Fact]
public void Cooldown_CountsPlaysFromTheSameRun()
{
var first = Slot(
T0,
60,
[Element(1, 60, position: 0), Element(1, 60, position: 1)],
strategy: SlotStrategyKind.RandomWithCooldown,
cooldownDays: 14
);
var second = first with { SlotId = Guid.NewGuid(), TargetStartUtc = T0.AddHours(2) };
var result = Run(Input([first, second], horizonHours: 4));
var shows = result
.Items.Where(i => i.Kind == PlannedItemKind.Program)
.Select(i => i.ShowId)
.ToList();
Assert.Equal(2, shows.Count);
Assert.Equal(2, shows.Distinct().Count());
}
/// <summary>Потолок повторов — тот же случай: показы прогона считаются наравне с записанными.</summary>
[Fact]
public void RepeatLimit_CountsPlaysFromTheSameRun()
{
var first = Slot(
T0,
60,
[Element(1, 60, position: 0), Element(1, 60, position: 1)],
strategy: SlotStrategyKind.RandomWithCooldown
) with
{
RepeatLimit = new RepeatLimit(WindowDays: 7, Max: 1),
};
var second = first with { SlotId = Guid.NewGuid(), TargetStartUtc = T0.AddHours(2) };
var result = Run(Input([first, second], horizonHours: 4));
var shows = result
.Items.Where(i => i.Kind == PlannedItemKind.Program)
.Select(i => i.ShowId)
.ToList();
Assert.Equal(2, shows.Count);
Assert.Equal(2, shows.Distinct().Count());
}
[Fact]
public void Trace_IsWrittenWithSlotAndStrategy()
{