Add broadcast scheduling features: implement Show and Channel entities, enhance AppDbContext and DependencyInjection for broadcasting, and update API routing. Include migration for new database schema and update documentation for broadcast-related functionalities.
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
using TeleWave.Domain.Broadcast;
|
||||
using TeleWave.Domain.Broadcast.Scheduling;
|
||||
using Xunit;
|
||||
|
||||
namespace TeleWave.Domain.Tests.Broadcast;
|
||||
|
||||
public class SchedulePlannerTests
|
||||
{
|
||||
private static readonly DateTimeOffset Start = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
/// <summary>Источник случайности с фиксированной (циклической) последовательностью значений.</summary>
|
||||
private sealed class FixedRandom(params int[] values) : IRandomSource
|
||||
{
|
||||
private int _i;
|
||||
|
||||
public int Next(int maxExclusive) =>
|
||||
values.Length == 0 ? 0 : values[_i++ % values.Length] % maxExclusive;
|
||||
}
|
||||
|
||||
private static Dictionary<Guid, TimeSpan> Durations(params (Guid Id, int Minutes)[] items) =>
|
||||
items.ToDictionary(x => x.Id, x => TimeSpan.FromMinutes(x.Minutes));
|
||||
|
||||
[Fact]
|
||||
public void Count_Block_ProducesConsecutiveEpisodes_BackToBack()
|
||||
{
|
||||
var cs = Guid.NewGuid();
|
||||
var show = Guid.NewGuid();
|
||||
Guid[] eps = [Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid()];
|
||||
|
||||
var input = new PlannerInput(
|
||||
ChannelId: Guid.NewGuid(),
|
||||
AdInsertion: AdInsertion.BetweenBlocks,
|
||||
AdsPerBreak: 1,
|
||||
NextAdIndex: 0,
|
||||
Shows: [new PlannerShow(cs, show, 1, BlockMode.Count, 3, eps, 0)],
|
||||
AdPool: [],
|
||||
Durations: Durations((eps[0], 2), (eps[1], 2), (eps[2], 2), (eps[3], 2)),
|
||||
Overrides: [],
|
||||
StartTime: Start,
|
||||
HorizonEnd: Start.AddSeconds(1)
|
||||
);
|
||||
|
||||
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
|
||||
|
||||
Assert.Equal(3, result.Entries.Count);
|
||||
Assert.All(result.Entries, e => Assert.Equal(ScheduleEntryKind.Program, e.Kind));
|
||||
Assert.Equal([0, 1, 2], result.Entries.Select(e => e.EpisodeIndex));
|
||||
Assert.Equal(Start, result.Entries[0].StartsAtUtc);
|
||||
Assert.Equal(result.Entries[0].EndsAtUtc, result.Entries[1].StartsAtUtc);
|
||||
Assert.Equal(result.Entries[1].EndsAtUtc, result.Entries[2].StartsAtUtc);
|
||||
Assert.Equal(3, result.NextEpisodeIndexByChannelShow[cs]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Count_Block_WrapsAtEndOfSeries()
|
||||
{
|
||||
var cs = Guid.NewGuid();
|
||||
Guid[] eps = [Guid.NewGuid(), Guid.NewGuid()];
|
||||
|
||||
var input = BaseInput(
|
||||
[new PlannerShow(cs, Guid.NewGuid(), 1, BlockMode.Count, 3, eps, 0)],
|
||||
Durations((eps[0], 2), (eps[1], 2)),
|
||||
horizonEnd: Start.AddSeconds(1)
|
||||
);
|
||||
|
||||
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
|
||||
|
||||
Assert.Equal([0, 1, 0], result.Entries.Select(e => e.EpisodeIndex));
|
||||
Assert.Equal(1, result.NextEpisodeIndexByChannelShow[cs]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Duration_Block_TakesEpisodesUntilBudgetReached()
|
||||
{
|
||||
var cs = Guid.NewGuid();
|
||||
Guid[] eps = [Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid()];
|
||||
|
||||
var input = BaseInput(
|
||||
[new PlannerShow(cs, Guid.NewGuid(), 1, BlockMode.Duration, 25, eps, 0)],
|
||||
Durations((eps[0], 10), (eps[1], 10), (eps[2], 10), (eps[3], 10)),
|
||||
horizonEnd: Start.AddMinutes(30)
|
||||
);
|
||||
|
||||
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
|
||||
|
||||
// 10+10+10 = 30 >= 25 → три серии.
|
||||
Assert.Equal(3, result.Entries.Count);
|
||||
Assert.Equal([0, 1, 2], result.Entries.Select(e => e.EpisodeIndex));
|
||||
Assert.Equal(3, result.NextEpisodeIndexByChannelShow[cs]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdsBetweenBlocks_InsertsAdAfterBlock()
|
||||
{
|
||||
var cs = Guid.NewGuid();
|
||||
Guid ep = Guid.NewGuid(),
|
||||
ad0 = Guid.NewGuid(),
|
||||
ad1 = Guid.NewGuid();
|
||||
|
||||
var input = new PlannerInput(
|
||||
Guid.NewGuid(),
|
||||
AdInsertion.BetweenBlocks,
|
||||
AdsPerBreak: 1,
|
||||
NextAdIndex: 0,
|
||||
Shows: [new PlannerShow(cs, Guid.NewGuid(), 1, BlockMode.Count, 1, [ep], 0)],
|
||||
AdPool: [ad0, ad1],
|
||||
Durations: Durations((ep, 20), (ad0, 1), (ad1, 1)),
|
||||
Overrides: [],
|
||||
StartTime: Start,
|
||||
HorizonEnd: Start.AddSeconds(1)
|
||||
);
|
||||
|
||||
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
|
||||
|
||||
Assert.Equal(2, result.Entries.Count);
|
||||
Assert.Equal(ScheduleEntryKind.Program, result.Entries[0].Kind);
|
||||
Assert.Equal(ScheduleEntryKind.Ad, result.Entries[1].Kind);
|
||||
Assert.Equal(ad0, result.Entries[1].MediaAssetId);
|
||||
Assert.Equal(1, result.NextAdIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdsBetweenEpisodes_InsertsAdAfterEachEpisode()
|
||||
{
|
||||
var cs = Guid.NewGuid();
|
||||
Guid[] eps = [Guid.NewGuid(), Guid.NewGuid()];
|
||||
Guid ad = Guid.NewGuid();
|
||||
|
||||
var input = new PlannerInput(
|
||||
Guid.NewGuid(),
|
||||
AdInsertion.BetweenEpisodes,
|
||||
AdsPerBreak: 1,
|
||||
NextAdIndex: 0,
|
||||
Shows: [new PlannerShow(cs, Guid.NewGuid(), 1, BlockMode.Count, 2, eps, 0)],
|
||||
AdPool: [ad],
|
||||
Durations: Durations((eps[0], 20), (eps[1], 20), (ad, 1)),
|
||||
Overrides: [],
|
||||
StartTime: Start,
|
||||
HorizonEnd: Start.AddSeconds(1)
|
||||
);
|
||||
|
||||
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
ScheduleEntryKind.Program,
|
||||
ScheduleEntryKind.Ad,
|
||||
ScheduleEntryKind.Program,
|
||||
ScheduleEntryKind.Ad,
|
||||
],
|
||||
result.Entries.Select(e => e.Kind)
|
||||
);
|
||||
Assert.Equal(2, result.NextAdIndex);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WeightedPick_RespectsRoll()
|
||||
{
|
||||
var a = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 3, BlockMode.Count, 1, [Guid.NewGuid()], 0);
|
||||
var b = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
|
||||
var durations = Durations(
|
||||
(a.EpisodeAssetIds[0], 20),
|
||||
(b.EpisodeAssetIds[0], 20)
|
||||
);
|
||||
|
||||
var pickA = SchedulePlanner.Plan(BaseInput([a, b], durations, Start.AddSeconds(1)), new FixedRandom(0));
|
||||
var pickB = SchedulePlanner.Plan(BaseInput([a, b], durations, Start.AddSeconds(1)), new FixedRandom(3));
|
||||
|
||||
Assert.Equal(a.ShowId, pickA.Entries[0].ShowId);
|
||||
Assert.Equal(b.ShowId, pickB.Entries[0].ShowId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExclusiveOverride_ForcesSingleShow()
|
||||
{
|
||||
var a = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 10, BlockMode.Count, 1, [Guid.NewGuid()], 0);
|
||||
var b = new PlannerShow(Guid.NewGuid(), Guid.NewGuid(), 1, BlockMode.Count, 1, [Guid.NewGuid()], 0);
|
||||
var durations = Durations((a.EpisodeAssetIds[0], 20), (b.EpisodeAssetIds[0], 20));
|
||||
|
||||
var input = new PlannerInput(
|
||||
Guid.NewGuid(),
|
||||
AdInsertion.BetweenBlocks,
|
||||
0,
|
||||
0,
|
||||
[a, b],
|
||||
[],
|
||||
durations,
|
||||
Overrides:
|
||||
[
|
||||
new PlannerOverride(
|
||||
Start,
|
||||
Start.AddHours(1),
|
||||
OverrideMode.Exclusive,
|
||||
[new PlannerOverrideShow(b.ShowId, 1)]
|
||||
),
|
||||
],
|
||||
StartTime: Start,
|
||||
HorizonEnd: Start.AddSeconds(1)
|
||||
);
|
||||
|
||||
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
|
||||
|
||||
Assert.Equal(b.ShowId, result.Entries[0].ShowId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoPlayableShows_ReturnsEmpty()
|
||||
{
|
||||
var input = BaseInput([], new Dictionary<Guid, TimeSpan>(), Start.AddHours(1));
|
||||
|
||||
var result = SchedulePlanner.Plan(input, new FixedRandom(0));
|
||||
|
||||
Assert.Empty(result.Entries);
|
||||
}
|
||||
|
||||
private static PlannerInput BaseInput(
|
||||
IReadOnlyList<PlannerShow> shows,
|
||||
IReadOnlyDictionary<Guid, TimeSpan> durations,
|
||||
DateTimeOffset horizonEnd
|
||||
) =>
|
||||
new(
|
||||
ChannelId: Guid.NewGuid(),
|
||||
AdInsertion: AdInsertion.BetweenBlocks,
|
||||
AdsPerBreak: 0,
|
||||
NextAdIndex: 0,
|
||||
Shows: shows,
|
||||
AdPool: [],
|
||||
Durations: durations,
|
||||
Overrides: [],
|
||||
StartTime: Start,
|
||||
HorizonEnd: horizonEnd
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user