Added new methods for intersecting and subtracting time intervals in the GridCoverage class to enhance scheduling capabilities. Updated the GridPlan and GridPlanDto classes to replace string notes with a structured PlanNote type, improving note management. Enhanced the GridPlanner class to utilize these new features, ensuring better handling of scheduling conflicts and notes during grid generation. Adjusted related tests to validate the new functionality.
234 lines
8.7 KiB
C#
234 lines
8.7 KiB
C#
using TeleWave.Application.Programming.Groups;
|
|
using TeleWave.Application.Programming.Templates;
|
|
using TeleWave.Application.Programming.Templates.Generate;
|
|
using TeleWave.Application.Tests.Support;
|
|
using TeleWave.Domain.Broadcast;
|
|
using TeleWave.Domain.Library;
|
|
using TeleWave.Domain.Media;
|
|
using TeleWave.Domain.Programming;
|
|
using Xunit;
|
|
|
|
namespace TeleWave.Application.Tests.Programming;
|
|
|
|
/// <summary>
|
|
/// Витрина автосборки: каталог профилей, предпросмотр плана и его перевод в DTO. Предпросмотр —
|
|
/// то, по чему админ принимает решение, поэтому проверяется отдельно от самой раскладки.
|
|
/// </summary>
|
|
public class GridPlanPreviewTests
|
|
{
|
|
private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
|
|
|
private static PlannedSlot Slot(
|
|
string title,
|
|
SlotKind kind,
|
|
SlotBlockMode blockMode,
|
|
int blockValue,
|
|
int? weekday = null
|
|
) =>
|
|
new(
|
|
GridPlanLayer.Main,
|
|
weekday,
|
|
new TimeOnly(20, 0),
|
|
120,
|
|
title,
|
|
Daypart.Prime,
|
|
kind,
|
|
kind == SlotKind.Content ? Guid.NewGuid() : null,
|
|
kind == SlotKind.Content ? title : null,
|
|
blockMode,
|
|
blockValue,
|
|
null,
|
|
null,
|
|
IsAnchor: false
|
|
);
|
|
|
|
[Fact]
|
|
public async Task Profiles_AreListed_WithReferenceAndDescription()
|
|
{
|
|
var result = await new ListGridProfilesQueryHandler().Handle(
|
|
new ListGridProfilesQuery(),
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
// Каждый профиль должен объяснять себя: имя, на что похоже и для какой библиотеки годится.
|
|
Assert.Equal(GridProfiles.All.Count, result.Value.Count);
|
|
Assert.All(
|
|
result.Value,
|
|
profile =>
|
|
{
|
|
Assert.False(string.IsNullOrWhiteSpace(profile.Name));
|
|
Assert.False(string.IsNullOrWhiteSpace(profile.Reference));
|
|
Assert.False(string.IsNullOrWhiteSpace(profile.Description));
|
|
}
|
|
);
|
|
Assert.Contains(result.Value, p => p.Kind == GridProfileKind.Animation);
|
|
}
|
|
|
|
[Fact]
|
|
public void Mapper_DescribesEveryBlockKind()
|
|
{
|
|
var plan = new GridPlan(
|
|
GridProfiles.Get(GridProfileKind.Mixed),
|
|
GridGenerationMode.Rebuild,
|
|
[
|
|
Slot("Сериалы", SlotKind.Content, SlotBlockMode.Count, 3),
|
|
Slot("Кино", SlotKind.Content, SlotBlockMode.FillSlot, 1),
|
|
Slot("Полоса", SlotKind.Content, SlotBlockMode.Duration, 90, weekday: 6),
|
|
Slot("Повтор", SlotKind.Repeat, SlotBlockMode.FillSlot, 1),
|
|
Slot("Конец вещания", SlotKind.SignOff, SlotBlockMode.FillSlot, 1),
|
|
],
|
|
[new PlanNote(false, "замечание")],
|
|
SlotsToRemove: 4,
|
|
FallbackGroupId: Guid.NewGuid(),
|
|
FallbackGroupName: "Фон",
|
|
FreeMinutes: 10080,
|
|
CoveredMinutes: 5040
|
|
);
|
|
|
|
var dto = plan.ToDto();
|
|
|
|
Assert.Equal(GridProfileKind.Mixed, dto.Profile);
|
|
Assert.Equal(plan.Profile.Name, dto.ProfileName);
|
|
Assert.Equal(4, dto.SlotsToRemove);
|
|
Assert.Equal("Фон", dto.FallbackGroupName);
|
|
Assert.Equal(["замечание"], dto.Notes.Select(n => n.Text));
|
|
|
|
// Описание блока — то, что читает админ вместо режима и числа.
|
|
Assert.Equal(
|
|
["3 подряд", "до конца слота", "90 мин", "повтор", "конец вещания"],
|
|
dto.Slots.Select(s => s.Block)
|
|
);
|
|
// Время приезжает строкой «ЧЧ:ММ»: в предпросмотре оно только показывается.
|
|
Assert.All(dto.Slots, slot => Assert.Equal("20:00", slot.Start));
|
|
Assert.Equal(6, dto.Slots[2].Weekday);
|
|
Assert.Null(dto.Slots[0].Weekday);
|
|
// У неконтентных слотов группы нет — показывать в колонке нечего.
|
|
Assert.Null(dto.Slots[3].GroupName);
|
|
Assert.Equal("Сериалы", dto.Slots[0].GroupName);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Preview_ReturnsPlanForProfile()
|
|
{
|
|
var fixture = new TestDb();
|
|
var channelId = await SeedAsync(fixture);
|
|
|
|
await using var db = fixture.New();
|
|
var result = await new PreviewGeneratedGridQueryHandler(
|
|
new GridPlanner(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
|
|
).Handle(
|
|
new PreviewGeneratedGridQuery(
|
|
channelId,
|
|
GridProfileKind.Kids,
|
|
GridGenerationMode.Rebuild
|
|
),
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Equal(GridProfileKind.Kids, result.Value.Profile);
|
|
Assert.NotEmpty(result.Value.Slots);
|
|
// Пересборка сносит то, что было, и это видно до нажатия.
|
|
Assert.True(result.Value.SlotsToRemove >= 0);
|
|
Assert.Equal(7 * GridCoverage.MinutesInDay, result.Value.FreeMinutes);
|
|
// У детского профиля ночь — конец вещания, и в предпросмотре он подписан именно так.
|
|
Assert.Contains(result.Value.Slots, s => s.Block == "конец вещания");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Preview_FailsWhenChannelIsGone()
|
|
{
|
|
var fixture = new TestDb();
|
|
await using var db = fixture.New();
|
|
|
|
var result = await new PreviewGeneratedGridQueryHandler(
|
|
new GridPlanner(db, GroupServices.Dynamic(db), new GroupElementResolver(db))
|
|
).Handle(
|
|
new PreviewGeneratedGridQuery(
|
|
Guid.NewGuid(),
|
|
GridProfileKind.Mixed,
|
|
GridGenerationMode.FillGaps
|
|
),
|
|
CancellationToken.None
|
|
);
|
|
|
|
Assert.False(result.IsSuccess);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validator_RejectsUnknownProfileAndMode()
|
|
{
|
|
var validator = new GenerateGridCommandValidator();
|
|
|
|
Assert.True(
|
|
validator
|
|
.Validate(
|
|
new GenerateGridCommand(
|
|
Guid.NewGuid(),
|
|
GridProfileKind.Sitcom,
|
|
GridGenerationMode.Rebuild
|
|
)
|
|
)
|
|
.IsValid
|
|
);
|
|
// Значения enum приходят с клиента строкой — за пределами каталога они бессмысленны.
|
|
Assert.False(
|
|
validator
|
|
.Validate(
|
|
new GenerateGridCommand(
|
|
Guid.NewGuid(),
|
|
(GridProfileKind)42,
|
|
GridGenerationMode.FillGaps
|
|
)
|
|
)
|
|
.IsValid
|
|
);
|
|
}
|
|
|
|
/// <summary>Канал с одной группой мягкого рейтинга — чтобы детскому профилю было чем закрыться.</summary>
|
|
private static async Task<Guid> SeedAsync(TestDb fixture)
|
|
{
|
|
var show = Show.Create("Мультсериал", ShowKind.Series);
|
|
show.SetAudience(ShowAudience.G);
|
|
var assets = new List<MediaAsset>();
|
|
|
|
for (var i = 0; i < 20; i++)
|
|
{
|
|
var asset = MediaAsset.Register($"e{i}.mkv", ".mkv", MediaSource.Upload);
|
|
asset.MarkProcessing();
|
|
asset.MarkReady(
|
|
new MediaReadyInfo(
|
|
TimeSpan.FromMinutes(22),
|
|
6,
|
|
220,
|
|
1920,
|
|
1080,
|
|
"h264",
|
|
"aac",
|
|
$"assets/e{i}"
|
|
)
|
|
);
|
|
assets.Add(asset);
|
|
show.AddEpisode(asset.Id);
|
|
}
|
|
|
|
var group = Group.Create("Мультфильмы");
|
|
group.AddElement(GroupElementKind.Show, show.Id);
|
|
|
|
var channel = Channel.Create("Детский", "kids", T0);
|
|
var template = ScheduleTemplate.Create(channel.Id, "Сетка");
|
|
channel.SetTemplate(template.Id);
|
|
|
|
await using var seed = fixture.New();
|
|
seed.MediaAssets.AddRange(assets);
|
|
seed.Shows.Add(show);
|
|
seed.Groups.Add(group);
|
|
seed.Channels.Add(channel);
|
|
seed.ScheduleTemplates.Add(template);
|
|
await seed.SaveChangesAsync(CancellationToken.None);
|
|
|
|
return channel.Id;
|
|
}
|
|
}
|