Add LibraryDebugCollector and enhance ChannelDebugCollector for drift analysis
ci / build-backend (push) Successful in 1m18s
ci / build-frontend (push) Successful in 50s
ci / tests (push) Successful in 1m25s
ci / sonar (push) Successful in 3m47s

Introduced the LibraryDebugCollector to gather library-related data for channel exports. Updated the ChannelDebugCollector to calculate and include drift information, comparing scheduled and actual slot times. Enhanced the ExportChannelDebugCommandHandler to integrate library data into the export process. Added tests to verify the correct functionality of the new drift analysis and library export features, ensuring comprehensive debug data for channel analysis.
This commit is contained in:
Leonid Pershin
2026-07-31 03:18:24 +03:00
parent 9cd364b174
commit 789357481d
7 changed files with 622 additions and 6 deletions
@@ -124,6 +124,7 @@ public class ChannelDebugExportTests
generator,
Options.Create(new SchedulerOptions { HorizonDays = 1, RetentionDays = 90 })
),
new LibraryDebugCollector(db, GroupServices.Dynamic(db), new GroupElementResolver(db)),
store
);
}
@@ -165,10 +166,17 @@ public class ChannelDebugExportTests
Assert.Equal(
[
"README.md",
"bumpers.json",
"channel.json",
"drift.json",
"effective-grid.json",
"groups-composition.json",
"groups.json",
"junctions.json",
"library-collections.json",
"library-interstitials.json",
"library-media.json",
"library-shows.json",
"plan-preview.json",
"schedule.json",
"slot-states.json",
@@ -195,6 +203,80 @@ public class ChannelDebugExportTests
Assert.Equal("Симпсоны", units[0].GetProperty("show").GetString());
}
[Fact]
public async Task Export_DescribesTheLibraryItWasBuiltFrom()
{
// Вторая половина разбора: когда в эфир вышло не то, смотрят не на ленту, а на то,
// из чего её собирали — сколько у шоу серий, готовы ли ассеты, что дало правило группы.
var (fixture, channelId) = await SeedAsync();
await using var db = fixture.New();
var result = await Handler(db, Substitute.For<IDebugExportStore>())
.Handle(new ExportChannelDebugCommand(channelId), CancellationToken.None);
Assert.True(result.IsSuccess);
var files = Unpack(result.Value.Content);
using var shows = JsonDocument.Parse(files["library-shows.json"]);
var show = shows.RootElement.EnumerateArray().Single();
Assert.Equal("Симпсоны", show.GetProperty("name").GetString());
Assert.Equal(2, show.GetProperty("episodes").GetInt32());
Assert.Equal(2, show.GetProperty("readyEpisodes").GetInt32());
// Серии перечислены по порядку показа — по нему планировщик их и ставит.
var items = show.GetProperty("items").EnumerateArray().ToList();
Assert.Equal(2, items.Count);
Assert.Equal("s01e01.mkv", items[0].GetProperty("file").GetString());
Assert.Equal("Ready", items[0].GetProperty("status").GetString());
using var groups = JsonDocument.Parse(files["groups-composition.json"]);
var group = groups.RootElement.EnumerateArray().Single();
Assert.Equal("Мультсериалы", group.GetProperty("name").GetString());
// Состав вычислен, а не переписан из позиций: у динамической группы он только так и виден.
var composition = group.GetProperty("composition").EnumerateArray().Single();
Assert.Equal("Симпсоны", composition.GetProperty("name").GetString());
Assert.Equal(2, composition.GetProperty("unitCount").GetInt32());
using var media = JsonDocument.Parse(files["library-media.json"]);
Assert.Equal(2, media.RootElement.GetProperty("total").GetInt32());
Assert.Empty(media.RootElement.GetProperty("problems").EnumerateArray());
}
[Fact]
public async Task Export_MeasuresDriftAgainstTheGrid()
{
// «Слот уехал» — первый вопрос к любой сетке, и сводить целевые времена с фактическими
// вручную по двум файлам приходилось каждый раз.
var (fixture, channelId) = await SeedAsync();
var store = Substitute.For<IDebugExportStore>();
await using var db = fixture.New();
var result = await Handler(db, store)
.Handle(new ExportChannelDebugCommand(channelId), CancellationToken.None);
Assert.True(result.IsSuccess);
var files = Unpack(result.Value.Content);
using var drift = JsonDocument.Parse(files["drift.json"]);
var summary = drift.RootElement.GetProperty("summary");
var instances = drift.RootElement.GetProperty("instances").EnumerateArray().ToList();
Assert.NotEmpty(instances);
Assert.Equal(instances.Count, summary.GetProperty("instances").GetInt32());
// Ленты ещё нет — значит и дрейф мерить не по чему: это «без записей», а не нулевой сдвиг.
Assert.Equal(instances.Count, summary.GetProperty("withoutEntries").GetInt32());
Assert.All(
instances,
i =>
{
Assert.Equal(JsonValueKind.Null, i.GetProperty("actualStartUtc").ValueKind);
Assert.Equal(JsonValueKind.Null, i.GetProperty("driftMinutes").ValueKind);
Assert.False(i.GetProperty("exceedsTolerance").GetBoolean());
Assert.True(i.GetProperty("maxDriftMinutes").GetInt32() > 0);
}
);
}
[Fact]
public async Task Export_DryRunGoesIntoTheDump()
{