Репозиторий хранил фронтенд в CRLF, а часть бэкенда — вперемешку, хотя CI и Docker-сборка работают под Linux. Прибиваем LF атрибутом `* text=auto eol=lf` и разово нормализуем дерево, чтобы форматтеры не переписывали файлы целиком на каждом прогоне. Коммит чисто механический: изменений содержимого нет, только концы строк. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
151 lines
6.0 KiB
C#
151 lines
6.0 KiB
C#
using TeleWave.Application.Programming.Planning;
|
|
using TeleWave.Application.Programming.Templates;
|
|
using TeleWave.Domain.Programming;
|
|
using Xunit;
|
|
|
|
namespace TeleWave.Application.Tests.Programming;
|
|
|
|
/// <summary>
|
|
/// Применимость слоёв (см. 3.4) и разрешение перекрытий: какие слоты реально действуют в сутки.
|
|
/// </summary>
|
|
public class LayerApplicabilityTests
|
|
{
|
|
private static readonly TimeOnly DayStart = new(6, 0);
|
|
|
|
[Fact]
|
|
public void Empty_CoversEveryDate()
|
|
{
|
|
var applicability = new LayerApplicability();
|
|
|
|
Assert.True(applicability.IsEmpty);
|
|
Assert.True(applicability.Covers(new DateOnly(2026, 3, 17)));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(2026, 12, 25, true)] // внутри, до Нового года
|
|
[InlineData(2026, 1, 3, true)] // внутри, после Нового года
|
|
[InlineData(2026, 12, 20, true)] // ровно начало
|
|
[InlineData(2026, 1, 8, true)] // ровно конец
|
|
[InlineData(2026, 12, 19, false)] // за день до начала
|
|
[InlineData(2026, 1, 9, false)] // на следующий день после конца
|
|
[InlineData(2026, 6, 15, false)] // середина года
|
|
public void AnnualRange_CrossingNewYear_IsInclusiveOnBothEnds(
|
|
int year,
|
|
int month,
|
|
int day,
|
|
bool expected
|
|
)
|
|
{
|
|
// «20 декабря — 8 января» задаётся один раз и работает в любом году, поэтому сравнение идёт
|
|
// по паре (месяц, день), а не по датам.
|
|
var applicability = new LayerApplicability(AnnualRanges: [new AnnualRange(12, 20, 1, 8)]);
|
|
|
|
Assert.Equal(expected, applicability.Covers(new DateOnly(year, month, day)));
|
|
}
|
|
|
|
[Fact]
|
|
public void AnnualRange_WithinOneYear_DoesNotWrap()
|
|
{
|
|
var applicability = new LayerApplicability(AnnualRanges: [new AnnualRange(6, 1, 8, 31)]);
|
|
|
|
Assert.True(applicability.Covers(new DateOnly(2026, 7, 4)));
|
|
Assert.False(applicability.Covers(new DateOnly(2026, 1, 4)));
|
|
}
|
|
|
|
[Fact]
|
|
public void Sections_AreCombinedWithOr()
|
|
{
|
|
// Понедельник ИЛИ конкретная дата: суббота из списка дат проходит, обычная суббота — нет.
|
|
var applicability = new LayerApplicability(
|
|
Weekdays: [1],
|
|
SpecificDates: [new DateOnly(2026, 3, 21)]
|
|
);
|
|
|
|
Assert.True(applicability.Covers(new DateOnly(2026, 3, 16))); // понедельник
|
|
Assert.True(applicability.Covers(new DateOnly(2026, 3, 21))); // суббота из списка
|
|
Assert.False(applicability.Covers(new DateOnly(2026, 3, 14))); // другая суббота
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_SkipsSlotsCoveredByHigherPriorityLayer()
|
|
{
|
|
// Слот младшего слоя пропускается целиком, а не обрезается: половина слота означала бы
|
|
// половину настройки — своей группы и стратегии у половинки нет.
|
|
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
|
|
var top = template.AddLayer("Прайм", 100);
|
|
var bottom = template.AddLayer("Обычный", 50);
|
|
|
|
AddSlot(top, "Кино", new TimeOnly(20, 0), 120);
|
|
AddSlot(bottom, "Сериал", new TimeOnly(20, 30), 60);
|
|
AddSlot(bottom, "Ночь", new TimeOnly(23, 0), 60);
|
|
|
|
var titles = Build(template).Select(s => s.Slot.Title).Distinct().ToList();
|
|
|
|
Assert.Contains("Кино", titles);
|
|
Assert.Contains("Ночь", titles);
|
|
Assert.DoesNotContain("Сериал", titles);
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_DropsLayersThatDoNotApplyOnTheDate()
|
|
{
|
|
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
|
|
var newYear = template.AddLayer("Новогодний", 100);
|
|
newYear.Update(
|
|
newYear.Name,
|
|
newYear.Priority,
|
|
new LayerApplicability(AnnualRanges: [new AnnualRange(12, 20, 1, 8)]).ToJson(),
|
|
isEnabled: true
|
|
);
|
|
AddSlot(newYear, "Ирония судьбы", new TimeOnly(20, 0), 180);
|
|
|
|
var usual = template.AddLayer("Обычный", 50);
|
|
AddSlot(usual, "Вечерний сериал", new TimeOnly(20, 0), 60);
|
|
|
|
var march = Build(template, new DateTimeOffset(2026, 3, 17, 12, 0, 0, TimeSpan.Zero))
|
|
.Select(s => s.Slot.Title)
|
|
.ToList();
|
|
var december = Build(template, new DateTimeOffset(2026, 12, 25, 12, 0, 0, TimeSpan.Zero))
|
|
.Select(s => s.Slot.Title)
|
|
.ToList();
|
|
|
|
Assert.Contains("Вечерний сериал", march);
|
|
Assert.DoesNotContain("Ирония судьбы", march);
|
|
Assert.Contains("Ирония судьбы", december);
|
|
Assert.DoesNotContain("Вечерний сериал", december);
|
|
}
|
|
|
|
[Fact]
|
|
public void Build_DisabledLayerIsIgnored()
|
|
{
|
|
var template = ScheduleTemplate.Create(Guid.NewGuid(), "Сетка");
|
|
var layer = template.AddLayer("Выключённый", 100);
|
|
AddSlot(layer, "Ничего", new TimeOnly(20, 0), 60);
|
|
layer.Update(layer.Name, layer.Priority, null, isEnabled: false);
|
|
|
|
Assert.Empty(Build(template));
|
|
}
|
|
|
|
private static void AddSlot(
|
|
GridLayer layer,
|
|
string title,
|
|
TimeOnly start,
|
|
int durationMinutes
|
|
) => layer.AddSlot(title, start, durationMinutes);
|
|
|
|
private static IReadOnlyList<ScheduledSlot> Build(
|
|
ScheduleTemplate template,
|
|
DateTimeOffset? from = null
|
|
)
|
|
{
|
|
var start = from ?? new DateTimeOffset(2026, 3, 17, 12, 0, 0, TimeSpan.Zero);
|
|
return EffectiveGridBuilder.Build(
|
|
template,
|
|
utcOffsetMinutes: 180,
|
|
DayStart,
|
|
start,
|
|
start.AddHours(18)
|
|
);
|
|
}
|
|
}
|