using Explorer.Analysis; using Explorer.Application; using Explorer.Domain; using Explorer.Presentation.ViewModels; using Explorer.Storage.Sqlite; using Microsoft.Extensions.Logging.Abstractions; namespace Explorer.Analysis.Tests; public class AnalysisTests { [Fact] public async Task Largest_dirs_files_and_types() { var db = Path.Combine(Path.GetTempPath(), "ew-an", Guid.NewGuid().ToString("N"), "index.db"); await using var store = new SqliteIndexStore(db, NullLogger.Instance); await store.OpenAsync(); var source = new Source { StableKey = "k", DisplayName = "Media", Kind = SourceKind.NtfsLocal, LastRootPath = @"C:\m", Status = SourceStatus.Online }; source.Id = await store.Sources.UpsertAsync(source); var root = new IndexEntry { SourceId = source.Id, Name = "m", NameNorm = "m", IsDirectory = true, PathRel = "", LastSeenUtc = DateTimeOffset.UtcNow, AggregateSize = 300 }; root.Id = await store.Entries.UpsertAsync(root); var movies = new IndexEntry { SourceId = source.Id, ParentId = root.Id, Name = "Movies", NameNorm = "movies", IsDirectory = true, PathRel = "Movies", LastSeenUtc = DateTimeOffset.UtcNow, AggregateSize = 200 }; movies.Id = await store.Entries.UpsertAsync(movies); await store.Entries.UpsertAsync(new IndexEntry { SourceId = source.Id, ParentId = movies.Id, Name = "a.mkv", NameNorm = "a.mkv", Extension = "mkv", SizeBytes = 200, PathRel = @"Movies\a.mkv", LastSeenUtc = DateTimeOffset.UtcNow }); await store.Entries.UpsertAsync(new IndexEntry { SourceId = source.Id, ParentId = root.Id, Name = "b.txt", NameNorm = "b.txt", Extension = "txt", SizeBytes = 100, PathRel = "b.txt", LastSeenUtc = DateTimeOffset.UtcNow }); var analysis = new AnalysisService(store); var dirs = await analysis.LargestDirectoriesAsync(source.Id, root.Id); Assert.Equal("Movies", dirs[0].Name); var files = await analysis.LargestFilesAsync(source.Id, null); Assert.Equal("a.mkv", files[0].Name); var types = await analysis.UsageByExtensionAsync(source.Id, null); Assert.Contains(types, t => t.Extension == "mkv" && t.TotalSize == 200); var drill = await analysis.DrilldownAsync(root.Id); Assert.Contains(drill, e => e.Name == "Movies"); Assert.Equal("Movies", dirs[0].PathRel); var global = await analysis.LargestDirectoriesAsync(source.Id, null); Assert.Contains(global, d => d.PathRel == "Movies"); Assert.DoesNotContain(global, d => d.ParentId is null); var first = await analysis.GetIndexStampAsync(); await store.Sources.UpdateIndexedAsync(source.Id, DateTimeOffset.UtcNow, source.ScanGeneration + 1); var second = await analysis.GetIndexStampAsync(); Assert.NotEqual(first, second); } [Fact] public void Sibling_size_bars_scale_to_the_largest_child() { var siblings = new[] { new StorageNodeViewModel { Name = "A", FullPath = @"C:\A", PathRel = "A", SourceId = 1, Size = 80 }, new StorageNodeViewModel { Name = "B", FullPath = @"C:\B", PathRel = "B", SourceId = 1, Size = 40 }, new StorageNodeViewModel { Name = "C", FullPath = @"C:\C", PathRel = "C", SourceId = 1, Size = 0 } }; AnalysisViewModel.ApplySiblingFractions(siblings); Assert.Equal(1, siblings[0].Fraction); Assert.Equal(0.5, siblings[1].Fraction); Assert.Equal(0, siblings[2].Fraction); } [Fact] public async Task Hash_worker_skips_online_only_without_opening() { var db = Path.Combine(Path.GetTempPath(), "ew-hash", Guid.NewGuid().ToString("N"), "index.db"); var rootPath = Path.Combine(Path.GetTempPath(), "ew-hash-fs", Guid.NewGuid().ToString("N")); Directory.CreateDirectory(rootPath); File.WriteAllBytes(Path.Combine(rootPath, "local.bin"), new byte[64]); var missingOnline = Path.Combine(rootPath, "online-only.bin"); await using var store = new SqliteIndexStore(db, NullLogger.Instance); await store.OpenAsync(); var source = new Source { StableKey = "h", DisplayName = "H", Kind = SourceKind.NtfsLocal, LastRootPath = rootPath, Status = SourceStatus.Online }; source.Id = await store.Sources.UpsertAsync(source); var root = new IndexEntry { SourceId = source.Id, Name = "H", NameNorm = "h", IsDirectory = true, PathRel = "", LastSeenUtc = DateTimeOffset.UtcNow }; root.Id = await store.Entries.UpsertAsync(root); await store.Entries.UpsertAsync(new IndexEntry { SourceId = source.Id, ParentId = root.Id, Name = "local.bin", NameNorm = "local.bin", SizeBytes = 64, PathRel = "local.bin", LastSeenUtc = DateTimeOffset.UtcNow }); var online = new IndexEntry { SourceId = source.Id, ParentId = root.Id, Name = "online-only.bin", NameNorm = "online-only.bin", SizeBytes = 64, PathRel = "online-only.bin", LastSeenUtc = DateTimeOffset.UtcNow, Attributes = AttributeFlags.RecallOnDataAccess, CloudAvailability = CloudAvailability.OnlineOnly }; online.Id = await store.Entries.UpsertAsync(online); await store.Hashes.EnqueueSizeCollisionsAsync(source.Id); var worker = new DuplicateHashWorker( store, new HydrationGuard(new StorageProviderRegistry([], NullLogger.Instance)), NullLogger.Instance); await worker.ProcessPendingAsync(CancellationToken.None); var skipped = await store.Entries.GetByPathAsync(source.Id, "online-only.bin"); Assert.Equal(HashState.Skipped, skipped!.HashState); Assert.False(File.Exists(missingOnline)); var local = await store.Entries.GetByPathAsync(source.Id, "local.bin"); Assert.Equal(HashState.Partial, local!.HashState); } }