using System.Diagnostics; using System.Linq; using System.Text.Json; using Jellyfin.Plugin.JRay.Models; using Jellyfin.Plugin.JRay.Services; using Xunit; namespace Jellyfin.Plugin.JRay.Tests; /// /// JR-004 (windows are scene-membership claims, served verbatim), JR-005 (query /// semantics and inclusive bounds) and JR-006 (numerous windows). /// /// These pin the semantics SR-002 sets. The failure they exist to prevent is a /// well-meaning "tidy-up" — merging adjacent windows, trimming a zero-length /// one, or collapsing overlaps — each of which silently answers a different /// question from the one the truth file asked. /// /// TRACES: UT-016, UT-017, UT-018, UT-019, UT-020, UT-021, UT-022, UT-023 | JR-004, JR-005, JR-006 /// public class PresenceLookupTests { private static TruthActor Actor(params double[][] windows) { var actor = new TruthActor { Name = "Steve Buscemi", TmdbId = "884" }; foreach (var w in windows) { actor.Scenes.Add(w); } return actor; } // UT-016 [Theory] [InlineData(12.0)] // exactly the start [InlineData(30.0)] // inside [InlineData(45.0)] // exactly the end public void IsPresentAt_WithinInclusiveBounds_IsPresent(double t) { // Both ends inclusive: a window is [start, end], not [start, end). Assert.True(PresenceLookup.IsPresentAt(Actor([12.0, 45.0]), t)); } // UT-017 [Theory] [InlineData(11.999)] [InlineData(45.001)] public void IsPresentAt_OutsideBounds_IsAbsent(double t) { Assert.False(PresenceLookup.IsPresentAt(Actor([12.0, 45.0]), t)); } // UT-018 [Fact] public void IsPresentAt_ZeroLengthWindow_IsPresentAtThatInstant() { // A single sighting is a legitimate window. Discarding it as degenerate // would drop the actor from a scene they are demonstrably in. Assert.True(PresenceLookup.IsPresentAt(Actor([30.0, 30.0]), 30.0)); } // UT-019 [Fact] public void IsPresentAt_OverlappingWindows_IsPresentInsideTheEnclosingOne() { // [0,100] encloses [50,60]. A lookup that assumed non-overlapping, // sorted windows and stopped at the first start > t would miss t = 80. Assert.True(PresenceLookup.IsPresentAt(Actor([0.0, 100.0], [50.0, 60.0]), 80.0)); } // UT-020 [Fact] public void IsPresentAt_UnsortedWindows_StillFindsPresence() { // Sortedness is a producer guarantee, not something correctness may // depend on. A file that violates it must still be read correctly. var actor = Actor([100.0, 110.0], [10.0, 20.0]); Assert.True(PresenceLookup.IsPresentAt(actor, 15.0)); Assert.False(PresenceLookup.WindowsAreSorted(actor)); } // UT-021 [Fact] public void ActorsPresentAt_AdjacentWindowsAreNeverMerged() { // [0,10] and [10,20] look mergeable. They must not be merged: two // windows mean a genuine departure and return, and the plugin does not // reinterpret that claim. The actor is reported once, from two windows. var truth = new TruthFile(); truth.Actors.Add(Actor([0.0, 10.0], [10.0, 20.0])); Assert.Single(PresenceLookup.ActorsPresentAt(truth, 10.0)); Assert.Equal(2, truth.Actors[0].Scenes.Count); } // UT-022 [Fact] public void TruthFile_RoundTrips_WithWindowsByteIdentical() { // JR-004: served exactly as given. A round trip through the serializer // is where a silent normalisation would show up. const string Json = """ {"schema_version":1,"movie":"/m.mkv","sample_fps":1,"anneal_sec":2, "actors":[{"name":"A","imdb_id":"","tmdb_id":"884","jellyfin_id":"", "scenes":[[0.0,10.0],[10.0,20.0],[30.0,30.0]]}]} """; var parsed = JsonSerializer.Deserialize(Json, new JsonSerializerOptions(JsonSerializerDefaults.Web))!; var windows = parsed.Actors[0].Scenes; Assert.Equal(3, windows.Count); Assert.Equal([0.0, 10.0], windows[0]); Assert.Equal([10.0, 20.0], windows[1]); Assert.Equal([30.0, 30.0], windows[2]); } // UT-023 [Fact] public void ActorsPresentAt_WithManyWindows_StaysCheapAndBoundsTheResponse() { // SR-002: windows may be numerous; consumers must not assume a handful // of long ones. Track-extent presence with a short re-acquisition // timeout produces many short windows per actor. var truth = new TruthFile(); for (var a = 0; a < 50; a++) { var actor = Actor(); for (var w = 0; w < 1000; w++) { actor.Scenes.Add([w * 10.0, (w * 10.0) + 4.0]); } truth.Actors.Add(actor); } var sw = Stopwatch.StartNew(); var present = PresenceLookup.ActorsPresentAt(truth, 5002.0).ToList(); sw.Stop(); // The response is bounded by actor count, never by window count — which // is what keeps `jray?t=` small however finely presence is sliced. Assert.Equal(50, present.Count); // 50 000 windows scanned. Generous bound: this asserts the read path is // not accidentally quadratic, not a precise budget on a shared runner. Assert.True(sw.ElapsedMilliseconds < 250, $"lookup took {sw.ElapsedMilliseconds} ms"); } }