Files
Explorer-Workbench/tests/Explorer.Analysis.Tests/AnalysisTests.cs
netquick e9aba73552 Add Explorer Workbench with hierarchical, off-UI Storage analysis.
Storage queries run in the background with cancellation and covering indexes so switching views no longer freezes the UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 12:43:05 +02:00

140 lines
6.2 KiB
C#

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<SqliteIndexStore>.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<SqliteIndexStore>.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<StorageProviderRegistry>.Instance)),
NullLogger<DuplicateHashWorker>.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);
}
}