Files
PLib/tests/PLib.Tests/Library/DuplicateDetectionTests.cs

100 lines
3.2 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
using PLib.Application.Abstractions;
using PLib.Application.Library;
using PLib.Domain.Videos;
using Shouldly;
namespace PLib.Tests.Library;
public sealed class DuplicateDetectionTests
{
private readonly InMemoryVideoRepository _videos = new();
[Fact]
public void Distance_counts_the_bits_that_differ()
{
var left = Video("a", 0b1111);
var right = Video("b", 0b1010);
left.DistanceTo(right).ShouldBe(2);
}
[Fact]
public void A_video_without_a_hash_has_no_distance_to_anything()
{
var hashed = Video("a", 1);
var bare = new VideoItem(@"C:\videos\b.mp4", "b", 1, DateTimeOffset.UnixEpoch);
hashed.DistanceTo(bare).ShouldBeNull();
bare.DistanceTo(hashed).ShouldBeNull();
}
[Fact]
public async Task Near_identical_videos_are_grouped_and_unrelated_ones_are_not()
{
_videos.Seed(
Video("original", 0b0000_0000),
Video("re-encode", 0b0000_0011),
Video("unrelated", 0b1111_1111));
var groups = await CreateService().FindDuplicatesAsync(maxDistance: 4, Token);
groups.ShouldHaveSingleItem();
groups[0].Select(x => x.Title).OrderBy(x => x).ShouldBe(["original", "re-encode"]);
}
[Fact]
public async Task A_chain_of_near_matches_ends_up_in_one_group()
{
// Ends are 4 bits apart — further than the threshold — but the middle copy links
// them, and all three are the same film.
_videos.Seed(
Video("a", 0b0000_0000),
Video("b", 0b0000_0011),
Video("c", 0b0000_1111));
var groups = await CreateService().FindDuplicatesAsync(maxDistance: 2, Token);
groups.ShouldHaveSingleItem();
groups[0].Count.ShouldBe(3);
}
[Fact]
public async Task Videos_that_were_never_hashed_are_left_out_entirely()
{
_videos.Seed(
new VideoItem(@"C:\videos\x.mp4", "x", 1, DateTimeOffset.UnixEpoch),
new VideoItem(@"C:\videos\y.mp4", "y", 1, DateTimeOffset.UnixEpoch));
var groups = await CreateService().FindDuplicatesAsync(maxDistance: 64, Token);
// Two hashless videos are not evidence of anything, however wide the threshold.
groups.ShouldBeEmpty();
}
private static CancellationToken Token => TestContext.Current.CancellationToken;
private static VideoItem Video(string title, ulong hash)
{
var item = new VideoItem($@"C:\videos\{title}.mp4", title, 1, DateTimeOffset.UnixEpoch);
item.ApplyPerceptualHash(hash);
return item;
}
private LibraryService CreateService() => new(
_videos,
new InMemoryLabelRepository(),
Substitute.For<IVideoFileScanner>(),
Substitute.For<IMediaProbe>(),
Substitute.For<IThumbnailGenerator>(),
Substitute.For<IAnimatedPreviewGenerator>(),
Substitute.For<IVideoPerceptualHasher>(),
Substitute.For<IMetadataProvider>(),
Substitute.For<IRemoteImageCache>(),
Options.Create(new LibraryOptions()),
MetadataMonitor.Empty,
NullLogger<LibraryService>.Instance);
}