using Moq; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using XP.Common.Logging.Interfaces; using XplorePlane.Models; using XplorePlane.Models.Inspection; using XplorePlane.Services.Inspection; using Xunit; namespace XplorePlane.Tests.Inspection { public sealed class InspectionDefinitionRepositoryTests : IDisposable { private readonly string _directory; private readonly JsonInspectionDefinitionRepository _repository; public InspectionDefinitionRepositoryTests() { _directory = Path.Combine(Path.GetTempPath(), "XplorePlane.Tests", Guid.NewGuid().ToString("N")); var logger = new Mock(); logger.Setup(x => x.ForModule()).Returns(logger.Object); _repository = new JsonInspectionDefinitionRepository(_directory, logger.Object); } [Fact] public async Task SaveAndLoad_PreservesChineseAndDefinitionData() { var definition = CreateDefinition("焊球空洞检测"); await _repository.SaveAsync(definition); var loaded = await _repository.LoadAsync(definition.Id); Assert.Equal("焊球空洞检测", loaded.Name); Assert.Equal(InspectionKind.Bga, loaded.Kind); Assert.Equal(120, loaded.Acquisition.VoltageKv); Assert.Equal(4, loaded.Geometry.BgaGrid.Rows); Assert.Equal("BgaVoidRate", loaded.Analysis.AdvancedDetectorType); var json = await File.ReadAllTextAsync(Path.Combine(_directory, $"{definition.Id:D}.xppool")); Assert.Contains("焊球空洞检测", json); Assert.DoesNotContain("\\u710a", json, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task ListCloneDelete_ImplementsSimpleCrud() { var source = CreateDefinition("原始项目"); await _repository.SaveAsync(source); var clone = await _repository.CloneAsync(source.Id, "复制项目"); var listed = await _repository.ListAsync(); Assert.Equal(2, listed.Count); Assert.NotEqual(source.Id, clone.Id); Assert.Equal("复制项目", clone.Name); Assert.Equal(source.Geometry.BgaGrid.Rows, clone.Geometry.BgaGrid.Rows); Assert.Equal(source.Geometry.BgaGrid.Columns, clone.Geometry.BgaGrid.Columns); Assert.Equal(source.Geometry.Regions.Select(r => r.Id), clone.Geometry.Regions.Select(r => r.Id)); await _repository.DeleteAsync(source.Id); listed = await _repository.ListAsync(); Assert.Single(listed); Assert.Equal(clone.Id, listed[0].Id); } [Fact] public async Task ReferenceAsset_PersistsClonesAndDeletesWithDefinition() { var assets = new InspectionReferenceAssetService(_directory); var source = CreateDefinition("参考图项目"); var image = System.Windows.Media.Imaging.BitmapSource.Create( 2, 2, 96, 96, System.Windows.Media.PixelFormats.Gray8, null, new byte[] { 1, 2, 3, 4 }, 2); image.Freeze(); var reference = await assets.SaveAsync(source.Id, image, DateTime.UtcNow); source = source with { ReferenceImage = reference }; await _repository.SaveAsync(source); var loadedImage = await assets.LoadAsync((await _repository.LoadAsync(source.Id)).ReferenceImage); var clone = await _repository.CloneAsync(source.Id, "参考图副本"); Assert.Equal(2, loadedImage.PixelWidth); Assert.NotEqual(source.ReferenceImage.RelativePath, clone.ReferenceImage.RelativePath); Assert.True(File.Exists(Path.Combine(_directory, clone.ReferenceImage.RelativePath))); await _repository.DeleteAsync(clone.Id); Assert.False(Directory.Exists(Path.Combine(_directory, "References", clone.Id.ToString("D")))); } [Fact] public async Task List_RemovesOrphanReferenceAssetDirectories() { var orphanId = Guid.NewGuid(); var orphanDirectory = Path.Combine(_directory, "References", orphanId.ToString("D")); Directory.CreateDirectory(orphanDirectory); await File.WriteAllTextAsync(Path.Combine(orphanDirectory, "reference.png"), "orphan"); await _repository.ListAsync(); Assert.False(Directory.Exists(orphanDirectory)); } [Fact] public async Task Save_InvalidDefinition_IsRejectedBeforeWritingFile() { var invalid = CreateDefinition(" ") with { Id = Guid.Empty }; await Assert.ThrowsAsync(() => _repository.SaveAsync(invalid)); Assert.Empty(Directory.EnumerateFiles(_directory)); } [Fact] public void Validator_DoesNotRequireRegionCountToEqualBgaRowsTimesColumns() { var definition = CreateDefinition("BGA") with { Geometry = new InspectionGeometry( new[] { CreateRectangle("BGA-ROI") }, new BgaGridGeometry(20, 20, 30, 30, Array.Empty())) }; var result = InspectionDefinitionValidator.Validate(definition); Assert.True(result.IsValid, string.Join("; ", result.Errors)); } [Fact] public void InspectionPoolTaskNode_SerializesCompleteSnapshot() { var definition = CreateDefinition("CNC 检测项目"); CncNode node = new InspectionPoolTaskNode( Guid.NewGuid(), 0, "检测_CNC 检测项目", definition.Id, definition); var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }; var json = JsonSerializer.Serialize(node, options); var restored = Assert.IsType(JsonSerializer.Deserialize(json, options)); Assert.Equal(definition.Id, restored.DefinitionId); Assert.Equal("CNC 检测项目", restored.DefinitionSnapshot.Name); Assert.Equal(120, restored.DefinitionSnapshot.Acquisition.VoltageKv); Assert.Equal("BgaVoidRate", restored.DefinitionSnapshot.Analysis.AdvancedDetectorType); } [Fact] public void SnapshotClone_IsIndependentFromMutablePipelineSource() { var pipeline = new PipelineModel { Name = "原流水线" }; pipeline.Nodes.Add(new PipelineNodeModel { OperatorKey = "Threshold", Order = 0 }); var source = CreateDefinition("Pipeline") with { Kind = InspectionKind.Pipeline, Geometry = new InspectionGeometry(new[] { CreateRectangle("ROI") }), Analysis = new AnalysisParameters(pipeline, string.Empty, new Dictionary()) }; var snapshot = InspectionDefinitionSnapshot.Clone(source); source.Analysis.PipelineSnapshot.Name = "已修改"; source.Analysis.PipelineSnapshot.Nodes.Clear(); Assert.Equal("原流水线", snapshot.Analysis.PipelineSnapshot.Name); Assert.Single(snapshot.Analysis.PipelineSnapshot.Nodes); } public void Dispose() { if (Directory.Exists(_directory)) Directory.Delete(_directory, recursive: true); } private static InspectionDefinition CreateDefinition(string name) { return new InspectionDefinition( Guid.NewGuid(), name, InspectionKind.Bga, new MachineParameters(1, 2, 3, 4, 5, 6, 7, 100, 200, 2), new AcquisitionParameters(120, 80, 100, 4, "HighResolution", false, false), new InspectionGeometry( new[] { CreateRectangle("ROI-1") }, new BgaGridGeometry(4, 4, 20, 20, Array.Empty())), new AnalysisParameters( null, "BgaVoidRate", new Dictionary { ["Threshold"] = 0.5 }), new DecisionRules( new[] { new MetricDecisionRule("VoidRatio", 0, 25) }, 0, 0), DateTime.UtcNow); } private static InspectionRegion CreateRectangle(string id) => new(id, RoiShapeKind.Rectangle, new[] { new RoiPoint(10, 20), new RoiPoint(110, 120) }); } }