208 lines
8.8 KiB
C#
208 lines
8.8 KiB
C#
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<ILoggerService>();
|
|
logger.Setup(x => x.ForModule<JsonInspectionDefinitionRepository>()).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<InvalidDataException>(() => _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<string>()))
|
|
};
|
|
|
|
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<InspectionPoolTaskNode>(JsonSerializer.Deserialize<CncNode>(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<string, double>())
|
|
};
|
|
|
|
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<string>())),
|
|
new AnalysisParameters(
|
|
null,
|
|
"BgaVoidRate",
|
|
new Dictionary<string, double> { ["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)
|
|
});
|
|
}
|
|
}
|