Catalog/MaddoShared.Tests/DataModelCharacterizationTests.cs

311 lines
10 KiB
C#

using System;
using System.IO;
using System.Threading.Tasks;
using ImageCatalog_2;
using ImageCatalog_2.Services;
using MaddoShared;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NSubstitute;
using Shouldly;
namespace MaddoShared.Tests;
[TestClass]
public class DataModelCharacterizationTests
{
[TestMethod]
public void SelectSourceFolderCommand_RaisesEvent()
{
var model = CreateModel();
var raised = false;
model.SelectSourceFolderRequested += (_, _) => raised = true;
model.SelectSourceFolderCommand.Execute(null);
raised.ShouldBeTrue();
}
[TestMethod]
public async Task SaveSettingsToFileAsync_DelegatesToSettingsService()
{
var settingsService = Substitute.For<ISettingsService>();
settingsService
.SaveSettingsAsync(Arg.Any<string>(), Arg.Any<object>())
.Returns(Task.CompletedTask);
var model = CreateModel(settingsService: settingsService);
await model.SaveSettingsToFileAsync("settings.xml");
await settingsService.Received(1)
.SaveSettingsAsync("settings.xml", model);
}
[TestMethod]
public async Task LoadSettingsFromFileAsync_DelegatesToSettingsService()
{
var settingsService = Substitute.For<ISettingsService>();
settingsService
.LoadSettingsAsync(Arg.Any<string>(), Arg.Any<object>())
.Returns(Task.CompletedTask);
var model = CreateModel(settingsService: settingsService);
await model.LoadSettingsFromFileAsync("settings.xml");
await settingsService.Received(1)
.LoadSettingsAsync("settings.xml", model);
}
[TestMethod]
public void ThumbnailOptionIndex_UpdatesAuthoritativeThumbnailState()
{
var model = CreateModel();
model.ThumbnailOptionIndex = (int)DataModel.ThumbnailOptionEnum.RaceTime;
model.ThumbnailOption.ShouldBe(DataModel.ThumbnailOptionEnum.RaceTime);
model.AddRaceTimeToThumbnails.ShouldBeTrue();
model.ThumbnailMode.ShouldBe("RaceTime");
}
[TestMethod]
public void ProcessingChildChange_RaisesDataModelPropertyChanged()
{
var model = CreateModel();
string? changed = null;
model.PropertyChanged += (_, args) => changed = args.PropertyName;
model.Processing.SpeedCounter = "12.00 f/s";
changed.ShouldBe(nameof(DataModel.SpeedCounter));
model.SpeedCounter.ShouldBe("12.00 f/s");
}
[TestMethod]
public void PathsNormalize_UpdatesFlattenedSourceAndDestination()
{
var model = CreateModel();
model.SourcePath = "\"C:/input\"";
model.DestinationPath = "C:/output";
model.Paths.NormalizePaths();
model.SourcePath.ShouldBe($"C:{System.IO.Path.DirectorySeparatorChar}input{System.IO.Path.DirectorySeparatorChar}");
model.DestinationPath.ShouldBe($"C:{System.IO.Path.DirectorySeparatorChar}output{System.IO.Path.DirectorySeparatorChar}");
}
[TestMethod]
public void AiChildChange_RaisesDataModelPropertyChanged()
{
var model = CreateModel();
string? changed = null;
model.PropertyChanged += (_, args) => changed = args.PropertyName;
model.Ai.ModelsFolderPath = "K:/models";
changed.ShouldBe(nameof(DataModel.ModelsFolderPath));
model.ModelsFolderPath.ShouldBe("K:/models");
}
[TestMethod]
public void NumberAiGpuChildChange_RaisesDataModelPropertyChanged()
{
var model = CreateModel();
string? changed = null;
model.PropertyChanged += (_, args) => changed = args.PropertyName;
model.Ai.UseNumberAiGpu = true;
changed.ShouldBe(nameof(DataModel.UseNumberAiGpu));
model.UseNumberAiGpu.ShouldBeTrue();
}
[TestMethod]
public void NumberAiThumbnails_DefaultsOffAndRaisesDataModelPropertyChanged()
{
var model = CreateModel();
model.IncludeNumberAiThumbnails.ShouldBeFalse();
string? changed = null;
model.PropertyChanged += (_, args) => changed = args.PropertyName;
model.Ai.IncludeNumberAiThumbnails = true;
changed.ShouldBe(nameof(DataModel.IncludeNumberAiThumbnails));
model.IncludeNumberAiThumbnails.ShouldBeTrue();
}
[TestMethod]
public void NumberAiWorkload_DefaultsToThreeAndClampsToFive()
{
var model = CreateModel();
model.NumberAiWorkloadLevel.ShouldBe(3);
string? changed = null;
model.PropertyChanged += (_, args) => changed = args.PropertyName;
model.NumberAiWorkloadLevel = 99;
changed.ShouldBe(nameof(DataModel.NumberAiWorkloadLevel));
model.NumberAiWorkloadLevel.ShouldBe(3);
model.Ai.NumberAiWorkloadLevel = 5;
model.NumberAiWorkloadLevel.ShouldBe(5);
}
[TestMethod]
public void CommandLineOperationRunner_DetectsHeadlessRequest()
{
CommandLineOperationRunner.IsHeadlessRequest(["--config", "settings.xml", "--operation", "number-ai"]).ShouldBeTrue();
CommandLineOperationRunner.IsHeadlessRequest(["--config=settings.xml", "--operation=number-ai"]).ShouldBeTrue();
CommandLineOperationRunner.IsHeadlessRequest([]).ShouldBeFalse();
}
[TestMethod]
public void RaceUploadChildChange_RaisesDataModelPropertyChanged()
{
var model = CreateModel();
string? changed = null;
model.PropertyChanged += (_, args) => changed = args.PropertyName;
model.RaceUpload.ApiLogin = "admin";
changed.ShouldBe(nameof(DataModel.ApiLogin));
model.ApiLogin.ShouldBe("admin");
}
[TestMethod]
public void VisualChildChange_RaisesDataModelPropertyChanged()
{
var model = CreateModel();
string? changed = null;
model.PropertyChanged += (_, args) => changed = args.PropertyName;
model.Visual.FontSize = 42;
changed.ShouldBe(nameof(DataModel.FontSize));
model.FontSize.ShouldBe(42);
}
[TestMethod]
public void FaceExecutableFolder_EnablesGpuToggleWhenBothVariantsExist()
{
using var root = new TemporaryDirectory();
CreateFaceEncoderExecutable(root.Path, "cpu");
CreateFaceEncoderExecutable(root.Path, "gpu");
var model = CreateModel();
model.FaceExecutablePath = root.Path;
model.FaceGpuOptionEnabled.ShouldBeTrue();
model.UseFaceGpu.ShouldBeFalse();
}
[TestMethod]
public void UseFaceGpu_UpdatesUpsampleWhenUsingRecommendedDefault()
{
using var root = new TemporaryDirectory();
CreateFaceEncoderExecutable(root.Path, "cpu");
CreateFaceEncoderExecutable(root.Path, "gpu");
var model = CreateModel();
model.FaceExecutablePath = root.Path;
model.FaceUpsample.ShouldBeTrue();
model.UseFaceGpu = true;
model.FaceUpsample.ShouldBeFalse();
}
[TestMethod]
public void ResolveConfiguredFaceEncoderExecutablePath_UsesFolderLayoutFromPowerShellScript()
{
using var root = new TemporaryDirectory();
var cpuExecutable = CreateFaceEncoderExecutable(root.Path, "cpu");
var gpuExecutable = CreateFaceEncoderExecutable(root.Path, "gpu");
DataModel.ResolveConfiguredFaceEncoderExecutablePath(root.Path, useGpu: false).ShouldBe(cpuExecutable);
DataModel.ResolveConfiguredFaceEncoderExecutablePath(root.Path, useGpu: true).ShouldBe(gpuExecutable);
}
[TestMethod]
public void BuildFaceEncoderOutputPaths_UsesTimestampAndSanitizedFolderName()
{
var timestamp = new DateTime(2026, 5, 9, 14, 30, 45);
var output = DataModel.BuildFaceEncoderOutputPaths(
@"C:\out",
@"C:\images\04 APRILE: gara?",
timestamp);
output.OutputFilePath.ShouldBe(@"C:\out\face_encodings_20260509_143045_04_APRILE_gara.pkl");
output.LogFilePath.ShouldBe(@"C:\out\encoder_log_20260509_143045_04_APRILE_gara.txt");
}
private static DataModel CreateModel(
ISettingsService? settingsService = null,
ITestService? testService = null)
{
var mapper = Substitute.For<AutoMapper.IMapper>();
var picSettings = new PicSettings();
var imageCreator = Substitute.For<IImageCreator>();
imageCreator
.CreateImageAsync(Arg.Any<ImageState>(), Arg.Any<byte[]?>())
.Returns(Task.CompletedTask);
var imageCreationService = new ImageCreationService(
Substitute.For<ILogger<ImageCreationService>>(),
picSettings,
imageCreator);
var imageProcessingCoordinator = new ImageProcessingCoordinator(
imageCreationService,
Substitute.For<ILogger<ImageProcessingCoordinator>>());
var aiExtractionService = new AiExtractionService(
Substitute.For<ILogger<AiExtractionService>>());
return new DataModel(
testService ?? Substitute.For<ITestService>(),
settingsService ?? Substitute.For<ISettingsService>(),
imageCreationService,
aiExtractionService,
imageProcessingCoordinator,
picSettings,
mapper,
Substitute.For<ILogger<DataModel>>(),
versionProvider: null);
}
private static string CreateFaceEncoderExecutable(string rootPath, string variant)
{
var variantDirectory = Path.Combine(rootPath, $"face_encoder_{variant}");
Directory.CreateDirectory(variantDirectory);
var executablePath = Path.Combine(variantDirectory, $"face_encoder_{variant}.exe");
File.WriteAllText(executablePath, "stub");
return executablePath;
}
private sealed class TemporaryDirectory : IDisposable
{
public TemporaryDirectory()
{
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName());
Directory.CreateDirectory(Path);
}
public string Path { get; }
public void Dispose()
{
if (Directory.Exists(Path))
{
Directory.Delete(Path, recursive: true);
}
}
}
}