Update scheduling parameters and refactor channel endpoints: extend HorizonDays to 7 and RetentionDays to 90 in appsettings.json. Consolidate channel-related endpoint logic by removing obsolete files and enhancing the ShowEndpoints with audience and genre management capabilities. Improve error handling and streamline command handlers for channel operations.
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
using TeleWave.Domain.Broadcast.Scheduling;
|
||||
using TeleWave.Domain.Programming;
|
||||
using TeleWave.Domain.Programming.Planning;
|
||||
using Xunit;
|
||||
using GridPlanner = TeleWave.Domain.Programming.Planning.SchedulePlanner;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Programming;
|
||||
|
||||
public class JunctionFillerTests
|
||||
{
|
||||
private static readonly DateTimeOffset T0 = new(2026, 4, 6, 18, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private sealed class FirstAlways : IRandomSource
|
||||
{
|
||||
public int Next(int maxExclusive) => 0;
|
||||
}
|
||||
|
||||
private static PlanningUnit Unit(double minutes, Guid? showId = null, int index = 0) =>
|
||||
new(Guid.NewGuid(), TimeSpan.FromMinutes(minutes), showId ?? Guid.NewGuid(), index);
|
||||
|
||||
private static PlanningElement Series(int episodes, int minutes, out Guid showId)
|
||||
{
|
||||
showId = Guid.NewGuid();
|
||||
var id = showId;
|
||||
var units = Enumerable
|
||||
.Range(0, episodes)
|
||||
.Select(i => Unit(minutes, id, i))
|
||||
.ToList();
|
||||
return new PlanningElement(GroupElementKind.Show, Guid.NewGuid(), 1, 0, units);
|
||||
}
|
||||
|
||||
private static PlanningJunctionElement Ads(
|
||||
int count,
|
||||
double minutesEach = 0.5,
|
||||
bool required = false,
|
||||
int minMinutesBetween = 0,
|
||||
bool onlyOnElementChange = false
|
||||
) =>
|
||||
new(
|
||||
JunctionElementKind.Ad,
|
||||
Enumerable.Range(0, 8).Select(_ => Unit(minutesEach)).ToList(),
|
||||
JunctionAmountMode.Count,
|
||||
count,
|
||||
required,
|
||||
onlyOnElementChange,
|
||||
minMinutesBetween
|
||||
);
|
||||
|
||||
private static PlanningSlot Slot(
|
||||
PlanningElement element,
|
||||
int blockValue,
|
||||
PlanningJunction? between = null,
|
||||
PlanningJunction? after = null
|
||||
) =>
|
||||
new(
|
||||
Guid.NewGuid(),
|
||||
T0,
|
||||
240,
|
||||
SlotKind.Content,
|
||||
IsAnchor: false,
|
||||
MaxDriftMinutes: 30,
|
||||
SnapToMinutes: null,
|
||||
SlotBlockMode.Count,
|
||||
blockValue,
|
||||
OverflowPolicy.ContinueNext,
|
||||
new PlanningStrategy(SlotStrategyKind.Sequential),
|
||||
[element],
|
||||
Cursor: null,
|
||||
RepeatUnits: null,
|
||||
JunctionBetween: between,
|
||||
JunctionAfter: after
|
||||
);
|
||||
|
||||
private static PlanningResult Run(PlanningSlot slot) =>
|
||||
GridPlanner.Plan(
|
||||
new PlanningInput(
|
||||
Guid.NewGuid(),
|
||||
T0,
|
||||
T0.AddHours(5),
|
||||
[slot],
|
||||
[Unit(1)],
|
||||
SegmentSeconds: 2
|
||||
),
|
||||
new FirstAlways()
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public void BetweenJunction_PlacesAdsBetweenUnitsButNotBeforeFirst()
|
||||
{
|
||||
var element = Series(3, 20, out _);
|
||||
var result = Run(Slot(element, 3, between: new PlanningJunction(Guid.NewGuid(), [Ads(2)])));
|
||||
|
||||
var ordered = result.Items.OrderBy(i => i.StartsAtUtc).ToList();
|
||||
Assert.Equal(PlannedItemKind.Program, ordered[0].Kind);
|
||||
// Между тремя сериями два стыка по две врезки.
|
||||
Assert.Equal(4, result.Items.Count(i => i.Kind == PlannedItemKind.Ad));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AfterJunction_PlacesAdsAtBlockEnd()
|
||||
{
|
||||
var element = Series(2, 20, out _);
|
||||
var result = Run(Slot(element, 2, after: new PlanningJunction(Guid.NewGuid(), [Ads(3)])));
|
||||
|
||||
var last = result
|
||||
.Items.Where(i => i.Kind != PlannedItemKind.Fallback)
|
||||
.OrderBy(i => i.StartsAtUtc)
|
||||
.Last();
|
||||
Assert.Equal(PlannedItemKind.Ad, last.Kind);
|
||||
Assert.Equal(3, result.Items.Count(i => i.Kind == PlannedItemKind.Ad));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DurationMode_IncludesLastUnitWholly()
|
||||
{
|
||||
var element = Series(2, 20, out _);
|
||||
var ads = new PlanningJunctionElement(
|
||||
JunctionElementKind.Ad,
|
||||
Enumerable.Range(0, 6).Select(_ => Unit(0.5)).ToList(),
|
||||
JunctionAmountMode.Duration,
|
||||
1,
|
||||
IsRequired: false
|
||||
);
|
||||
|
||||
var result = Run(Slot(element, 2, between: new PlanningJunction(Guid.NewGuid(), [ads])));
|
||||
|
||||
// Бюджет 1 минута, ролики по 30 секунд: две штуки добирают ровно минуту, третья не нужна.
|
||||
Assert.Equal(2, result.Items.Count(i => i.Kind == PlannedItemKind.Ad));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinInterval_SkipsSecondJunction()
|
||||
{
|
||||
var element = Series(3, 20, out _);
|
||||
var result = Run(
|
||||
Slot(
|
||||
element,
|
||||
3,
|
||||
between: new PlanningJunction(Guid.NewGuid(), [Ads(1, minMinutesBetween: 60)])
|
||||
)
|
||||
);
|
||||
|
||||
// Серии по 20 минут: второй стык приходит через 20 минут после первого — раньше разрешённого.
|
||||
Assert.Equal(1, result.Items.Count(i => i.Kind == PlannedItemKind.Ad));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnlyOnElementChange_SkipsWithinSameShow()
|
||||
{
|
||||
var element = Series(3, 20, out _);
|
||||
var result = Run(
|
||||
Slot(
|
||||
element,
|
||||
3,
|
||||
between: new PlanningJunction(Guid.NewGuid(), [Ads(1, onlyOnElementChange: true)])
|
||||
)
|
||||
);
|
||||
|
||||
// Внутри одного сериала смены нет — врезка не ставится.
|
||||
Assert.DoesNotContain(result.Items, i => i.Kind == PlannedItemKind.Ad);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bumper_ReservesDurationWithEmptyAssetAndShowPair()
|
||||
{
|
||||
var element = Series(2, 20, out var showId);
|
||||
var bumper = new PlanningJunctionElement(
|
||||
JunctionElementKind.Bumper,
|
||||
[],
|
||||
JunctionAmountMode.Count,
|
||||
1,
|
||||
IsRequired: true,
|
||||
BumperTemplateId: Guid.NewGuid(),
|
||||
BumperDuration: TimeSpan.FromSeconds(10)
|
||||
);
|
||||
|
||||
var result = Run(Slot(element, 2, between: new PlanningJunction(Guid.NewGuid(), [bumper])));
|
||||
|
||||
var placed = Assert.Single(result.Items, i => i.Kind == PlannedItemKind.Bumper);
|
||||
// Ассет подставит рендер после сборки ленты — до неё пара соседей неизвестна.
|
||||
Assert.Equal(Guid.Empty, placed.MediaAssetId);
|
||||
Assert.Equal(showId, placed.ToShowId);
|
||||
Assert.Equal(TimeSpan.FromSeconds(10), placed.EndsAtUtc - placed.StartsAtUtc);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequiredElement_GoesBeforeOptional()
|
||||
{
|
||||
var element = Series(2, 20, out _);
|
||||
var optional = Ads(1, minutesEach: 1);
|
||||
var required = new PlanningJunctionElement(
|
||||
JunctionElementKind.Promo,
|
||||
[Unit(0.5)],
|
||||
JunctionAmountMode.Count,
|
||||
1,
|
||||
IsRequired: true
|
||||
);
|
||||
|
||||
var result = Run(
|
||||
Slot(element, 2, between: new PlanningJunction(Guid.NewGuid(), [optional, required]))
|
||||
);
|
||||
|
||||
var junctionItems = result
|
||||
.Items.Where(i => i.Kind is PlannedItemKind.Ad or PlannedItemKind.Promo)
|
||||
.OrderBy(i => i.StartsAtUtc)
|
||||
.ToList();
|
||||
Assert.Equal(PlannedItemKind.Promo, junctionItems[0].Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JunctionDoesNotCrossAnchor()
|
||||
{
|
||||
var element = Series(2, 20, out _);
|
||||
var slot = Slot(element, 2, after: new PlanningJunction(Guid.NewGuid(), [Ads(4, 5)])) with
|
||||
{
|
||||
TargetDurationMinutes = 40,
|
||||
};
|
||||
var anchor = new PlanningSlot(
|
||||
Guid.NewGuid(),
|
||||
T0.AddMinutes(45),
|
||||
60,
|
||||
SlotKind.Content,
|
||||
IsAnchor: true,
|
||||
MaxDriftMinutes: 5,
|
||||
SnapToMinutes: null,
|
||||
SlotBlockMode.Count,
|
||||
1,
|
||||
OverflowPolicy.ContinueNext,
|
||||
new PlanningStrategy(SlotStrategyKind.Sequential),
|
||||
[Series(1, 30, out _)],
|
||||
Cursor: null
|
||||
);
|
||||
|
||||
var result = GridPlanner.Plan(
|
||||
new PlanningInput(
|
||||
Guid.NewGuid(),
|
||||
T0,
|
||||
T0.AddHours(3),
|
||||
[slot, anchor],
|
||||
[Unit(1)],
|
||||
SegmentSeconds: 2
|
||||
),
|
||||
new FirstAlways()
|
||||
);
|
||||
|
||||
// До якоря остаётся пять минут: влезает одна пятиминутная врезка, остальные не начинают.
|
||||
Assert.All(
|
||||
result.Items.Where(i => i.Kind == PlannedItemKind.Ad),
|
||||
item => Assert.True(item.EndsAtUtc <= anchor.TargetStartUtc)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user