版本260406

This commit is contained in:
2026-04-06 22:04:05 +08:00
parent 7dc5e73af7
commit 0b150470be
216 changed files with 98993 additions and 33 deletions

View File

@@ -0,0 +1,645 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using OrpaonVision.Core.LayerRecognition;
using OrpaonVision.Core.PartRecognition;
using OrpaonVision.Core.QuantityValidation;
using OrpaonVision.Core.Results;
using OrpaonVision.SiteApp.Runtime.Options;
using OrpaonVision.SiteApp.Runtime.Services;
namespace OrpaonVision.SiteApp.Tests.Runtime.Services;
/// <summary>
/// 数量校验服务单元测试。
/// </summary>
[TestClass]
public class QuantityValidationServiceTests
{
private QuantityValidationService _service = null!;
private Mock<ILogger<QuantityValidationService>> _loggerMock = null!;
private Mock<IOptions<RuntimeOptions>> _optionsMock = null!;
private RuntimeOptions _options = null!;
[TestInitialize]
public void TestInitialize()
{
_loggerMock = new Mock<ILogger<QuantityValidationService>>();
_options = new RuntimeOptions();
_optionsMock = new Mock<IOptions<RuntimeOptions>>();
_optionsMock.Setup(x => x.Value).Returns(_options);
_service = new QuantityValidationService(_loggerMock.Object, _optionsMock.Object);
}
[TestMethod]
public async Task ValidateExactQuantityAsync_ExactMatch_ReturnsSuccess()
{
// Arrange
var parts = CreateTestParts(3);
const int expectedCount = 3;
// Act
var result = await _service.ValidateExactQuantityAsync(parts, expectedCount);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsTrue(result.Data.IsValid);
Assert.AreEqual(expectedCount, result.Data.ActualCount);
Assert.AreEqual(expectedCount, result.Data.ExpectedCount);
Assert.AreEqual(0, result.Data.Deviation);
}
[TestMethod]
public async Task ValidateExactQuantityAsync_NotExactMatch_ReturnsFailure()
{
// Arrange
var parts = CreateTestParts(2);
const int expectedCount = 3;
// Act
var result = await _service.ValidateExactQuantityAsync(parts, expectedCount);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsFalse(result.Data.IsValid);
Assert.AreEqual(2, result.Data.ActualCount);
Assert.AreEqual(expectedCount, result.Data.ExpectedCount);
Assert.AreEqual(-1, result.Data.Deviation);
}
[TestMethod]
public async Task ValidateExactQuantityAsync_WithPartType_FiltersCorrectly()
{
// Arrange
var parts = CreateTestPartsWithTypes();
const int expectedCount = 2;
const PartType partType = PartType.Required;
// Act
var result = await _service.ValidateExactQuantityAsync(parts, expectedCount, partType);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsTrue(result.Data.IsValid);
Assert.AreEqual(partType, result.Data.PartType);
Assert.AreEqual(expectedCount, result.Data.ActualCount);
}
[TestMethod]
public async Task ValidateMinimumQuantityAsync_AboveMinimum_ReturnsSuccess()
{
// Arrange
var parts = CreateTestParts(5);
const int minCount = 3;
// Act
var result = await _service.ValidateMinimumQuantityAsync(parts, minCount);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsTrue(result.Data.IsValid);
Assert.AreEqual(5, result.Data.ActualCount);
Assert.AreEqual(minCount, result.Data.MinimumCount);
Assert.AreEqual(2, result.Data.ExcessCount);
}
[TestMethod]
public async Task ValidateMinimumQuantityAsync_BelowMinimum_ReturnsFailure()
{
// Arrange
var parts = CreateTestParts(2);
const int minCount = 3;
// Act
var result = await _service.ValidateMinimumQuantityAsync(parts, minCount);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsFalse(result.Data.IsValid);
Assert.AreEqual(2, result.Data.ActualCount);
Assert.AreEqual(minCount, result.Data.MinimumCount);
Assert.AreEqual(0, result.Data.ExcessCount);
}
[TestMethod]
public async Task ValidateRangeQuantityAsync_WithinRange_ReturnsSuccess()
{
// Arrange
var parts = CreateTestParts(4);
const int minCount = 3;
const int maxCount = 5;
// Act
var result = await _service.ValidateRangeQuantityAsync(parts, minCount, maxCount);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsTrue(result.Data.IsValid);
Assert.AreEqual(4, result.Data.ActualCount);
Assert.AreEqual(minCount, result.Data.MinimumCount);
Assert.AreEqual(maxCount, result.Data.MaximumCount);
Assert.IsFalse(result.Data.IsBelowMinimum);
Assert.IsFalse(result.Data.IsAboveMaximum);
}
[TestMethod]
public async Task ValidateRangeQuantityAsync_BelowMinimum_ReturnsFailure()
{
// Arrange
var parts = CreateTestParts(2);
const int minCount = 3;
const int maxCount = 5;
// Act
var result = await _service.ValidateRangeQuantityAsync(parts, minCount, maxCount);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsFalse(result.Data.IsValid);
Assert.AreEqual(2, result.Data.ActualCount);
Assert.AreEqual(minCount, result.Data.MinimumCount);
Assert.AreEqual(maxCount, result.Data.MaximumCount);
Assert.IsTrue(result.Data.IsBelowMinimum);
Assert.IsFalse(result.Data.IsAboveMaximum);
}
[TestMethod]
public async Task ValidateRangeQuantityAsync_AboveMaximum_ReturnsFailure()
{
// Arrange
var parts = CreateTestParts(6);
const int minCount = 3;
const int maxCount = 5;
// Act
var result = await _service.ValidateRangeQuantityAsync(parts, minCount, maxCount);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsFalse(result.Data.IsValid);
Assert.AreEqual(6, result.Data.ActualCount);
Assert.AreEqual(minCount, result.Data.MinimumCount);
Assert.AreEqual(maxCount, result.Data.MaximumCount);
Assert.IsFalse(result.Data.IsBelowMinimum);
Assert.IsTrue(result.Data.IsAboveMaximum);
}
[TestMethod]
public async Task ValidateUniqueQuantityAsync_ExactUniqueCount_ReturnsSuccess()
{
// Arrange
var parts = CreateTestPartsWithUniqueProperties("serial_number", 3);
const int expectedUniqueCount = 3;
const string uniqueProperty = "serial_number";
// Act
var result = await _service.ValidateUniqueQuantityAsync(parts, uniqueProperty, expectedUniqueCount);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsTrue(result.Data.IsValid);
Assert.AreEqual(expectedUniqueCount, result.Data.ActualUniqueCount);
Assert.AreEqual(expectedUniqueCount, result.Data.ExpectedUniqueCount);
Assert.AreEqual(uniqueProperty, result.Data.UniqueProperty);
Assert.AreEqual(0, result.Data.DuplicateValues.Count);
}
[TestMethod]
public async Task ValidateUniqueQuantityAsync_WithDuplicates_ReturnsFailure()
{
// Arrange
var parts = CreateTestPartsWithDuplicates("serial_number", 3);
const int expectedUniqueCount = 3;
const string uniqueProperty = "serial_number";
// Act
var result = await _service.ValidateUniqueQuantityAsync(parts, uniqueProperty, expectedUniqueCount);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsFalse(result.Data.IsValid);
Assert.AreEqual(2, result.Data.ActualUniqueCount); // 3 parts, but 2 unique values
Assert.AreEqual(expectedUniqueCount, result.Data.ExpectedUniqueCount);
Assert.AreEqual(uniqueProperty, result.Data.UniqueProperty);
Assert.AreEqual(1, result.Data.DuplicateValues.Count);
}
[TestMethod]
public async Task ValidateUniqueQuantityAsync_PropertyNotExists_ReturnsZeroUnique()
{
// Arrange
var parts = CreateTestParts(3);
const int expectedUniqueCount = 3;
const string uniqueProperty = "nonexistent_property";
// Act
var result = await _service.ValidateUniqueQuantityAsync(parts, uniqueProperty, expectedUniqueCount);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsFalse(result.Data.IsValid);
Assert.AreEqual(0, result.Data.ActualUniqueCount);
Assert.AreEqual(expectedUniqueCount, result.Data.ExpectedUniqueCount);
Assert.AreEqual(uniqueProperty, result.Data.UniqueProperty);
}
[TestMethod]
public async Task BatchValidateQuantityAsync_AllRulesPass_ReturnsSuccess()
{
// Arrange
var parts = CreateTestParts(3);
var rules = new List<QuantityValidationRule>
{
CreateExactQuantityRule("Rule1", 3),
CreateMinimumQuantityRule("Rule2", 2),
CreateRangeQuantityRule("Rule3", 2, 4)
};
// Act
var result = await _service.BatchValidateQuantityAsync(parts, rules);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.AreEqual(3, result.Data.TotalRules);
Assert.AreEqual(3, result.Data.PassedRules);
Assert.AreEqual(0, result.Data.FailedRules);
Assert.AreEqual(100.0, result.Data.OverallPassRate);
}
[TestMethod]
public async Task BatchValidateQuantityAsync_SomeRulesFail_ReturnsPartialSuccess()
{
// Arrange
var parts = CreateTestParts(3);
var rules = new List<QuantityValidationRule>
{
CreateExactQuantityRule("Rule1", 3), // Pass
CreateExactQuantityRule("Rule2", 5), // Fail
CreateMinimumQuantityRule("Rule3", 2) // Pass
};
// Act
var result = await _service.BatchValidateQuantityAsync(parts, rules);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.AreEqual(3, result.Data.TotalRules);
Assert.AreEqual(2, result.Data.PassedRules);
Assert.AreEqual(1, result.Data.FailedRules);
Assert.AreEqual(66.7, Math.Round(result.Data.OverallPassRate, 1));
}
[TestMethod]
public async Task CreateValidationRuleAsync_ValidRule_ReturnsSuccess()
{
// Arrange
var rule = CreateExactQuantityRule("TestRule", 5);
// Act
var result = await _service.CreateValidationRuleAsync(rule);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsNotNull(result.Data.RuleId);
Assert.AreEqual("TestRule", result.Data.RuleName);
Assert.AreEqual(QuantityValidationType.Exact, result.Data.ValidationType);
Assert.IsTrue(result.Data.IsEnabled);
}
[TestMethod]
public async Task UpdateValidationRuleAsync_ExistingRule_ReturnsSuccess()
{
// Arrange
var rule = CreateExactQuantityRule("TestRule", 5);
var createResult = await _service.CreateValidationRuleAsync(rule);
var updatedRule = createResult.Data;
updatedRule.ExpectedCount = 10;
updatedRule.RuleDescription = "Updated description";
// Act
var result = await _service.UpdateValidationRuleAsync(updatedRule);
// Assert
Assert.IsTrue(result.Succeeded);
}
[TestMethod]
public async Task UpdateValidationRuleAsync_NonExistingRule_ReturnsFailure()
{
// Arrange
var rule = CreateExactQuantityRule("TestRule", 5);
rule.RuleId = Guid.NewGuid();
// Act
var result = await _service.UpdateValidationRuleAsync(rule);
// Assert
Assert.IsFalse(result.Succeeded);
Assert.AreEqual("RULE_NOT_FOUND", result.Code);
}
[TestMethod]
public async Task DeleteValidationRuleAsync_ExistingRule_ReturnsSuccess()
{
// Arrange
var rule = CreateExactQuantityRule("TestRule", 5);
var createResult = await _service.CreateValidationRuleAsync(rule);
// Act
var result = await _service.DeleteValidationRuleAsync(createResult.Data.RuleId);
// Assert
Assert.IsTrue(result.Succeeded);
}
[TestMethod]
public async Task DeleteValidationRuleAsync_NonExistingRule_ReturnsFailure()
{
// Arrange
var nonExistingRuleId = Guid.NewGuid();
// Act
var result = await _service.DeleteValidationRuleAsync(nonExistingRuleId);
// Assert
Assert.IsFalse(result.Succeeded);
Assert.AreEqual("RULE_NOT_FOUND", result.Code);
}
[TestMethod]
public async Task GetValidationRulesAsync_NoFilters_ReturnsAllRules()
{
// Arrange
await _service.CreateValidationRuleAsync(CreateExactQuantityRule("Rule1", 5));
await _service.CreateValidationRuleAsync(CreateMinimumQuantityRule("Rule2", 3));
await _service.CreateValidationRuleAsync(CreateRangeQuantityRule("Rule3", 2, 4));
// Act
var result = await _service.GetValidationRulesAsync();
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsTrue(result.Data.Count >= 3); // At least the 3 rules we created plus default rules
}
[TestMethod]
public async Task GetValidationRulesAsync_WithFilters_ReturnsFilteredRules()
{
// Arrange
await _service.CreateValidationRuleAsync(CreateExactQuantityRule("Rule1", 5, "ProductA", 1));
await _service.CreateValidationRuleAsync(CreateMinimumQuantityRule("Rule2", 3, "ProductB", 2));
await _service.CreateValidationRuleAsync(CreateRangeQuantityRule("Rule3", 2, 4, "ProductA", 2));
// Act
var result = await _service.GetValidationRulesAsync("ProductA", 2);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.AreEqual(1, result.Data.Count); // Only Rule3 matches both filters
Assert.AreEqual("Rule3", result.Data.First().RuleName);
}
[TestMethod]
public async Task GetValidationStatisticsAsync_WithHistory_ReturnsStatistics()
{
// Arrange
var parts = CreateTestParts(3);
var startTime = DateTime.UtcNow.AddHours(-1);
var endTime = DateTime.UtcNow;
// Perform some validations to generate history
await _service.ValidateExactQuantityAsync(parts, 3);
await _service.ValidateMinimumQuantityAsync(parts, 2);
await _service.ValidateRangeQuantityAsync(parts, 2, 4);
// Act
var result = await _service.GetValidationStatisticsAsync(startTime, endTime);
// Assert
Assert.IsTrue(result.Succeeded);
Assert.IsTrue(result.Data.TotalValidations >= 3);
Assert.IsTrue(result.Data.ByValidationType.Count > 0);
Assert.IsTrue(result.Data.OverallSuccessRate >= 0);
}
#region Helper Methods
private static List<MappedPart> CreateTestParts(int count)
{
var parts = new List<MappedPart>();
for (int i = 0; i < count; i++)
{
parts.Add(new MappedPart
{
PartId = Guid.NewGuid(),
PartCode = $"PART-{i:D3}",
PartName = $"Test Part {i}",
PartType = PartType.Required,
OriginalClass = "component_a",
MappingConfidence = 0.9,
DetectionConfidence = 0.85,
OverallConfidence = 0.875,
BoundingBox = new BoundingBox
{
X = i * 10,
Y = i * 10,
Width = 50,
Height = 50,
Confidence = 0.85,
Class = "component_a"
},
PartAttributes = new Dictionary<string, object>(),
IsValid = true,
MappingTimeUtc = DateTime.UtcNow
});
}
return parts;
}
private static List<MappedPart> CreateTestPartsWithTypes()
{
return new List<MappedPart>
{
new MappedPart
{
PartId = Guid.NewGuid(),
PartCode = "PART-001",
PartName = "Required Part 1",
PartType = PartType.Required,
OriginalClass = "component_a",
MappingConfidence = 0.9,
DetectionConfidence = 0.85,
OverallConfidence = 0.875,
BoundingBox = new BoundingBox { X = 0, Y = 0, Width = 50, Height = 50, Confidence = 0.85, Class = "component_a" },
PartAttributes = new Dictionary<string, object>(),
IsValid = true,
MappingTimeUtc = DateTime.UtcNow
},
new MappedPart
{
PartId = Guid.NewGuid(),
PartCode = "PART-002",
PartName = "Required Part 2",
PartType = PartType.Required,
OriginalClass = "component_a",
MappingConfidence = 0.9,
DetectionConfidence = 0.85,
OverallConfidence = 0.875,
BoundingBox = new BoundingBox { X = 60, Y = 0, Width = 50, Height = 50, Confidence = 0.85, Class = "component_a" },
PartAttributes = new Dictionary<string, object>(),
IsValid = true,
MappingTimeUtc = DateTime.UtcNow
},
new MappedPart
{
PartId = Guid.NewGuid(),
PartCode = "PART-003",
PartName = "Optional Part 1",
PartType = PartType.Optional,
OriginalClass = "component_b",
MappingConfidence = 0.8,
DetectionConfidence = 0.75,
OverallConfidence = 0.775,
BoundingBox = new BoundingBox { X = 120, Y = 0, Width = 50, Height = 50, Confidence = 0.75, Class = "component_b" },
PartAttributes = new Dictionary<string, object>(),
IsValid = true,
MappingTimeUtc = DateTime.UtcNow
}
};
}
private static List<MappedPart> CreateTestPartsWithUniqueProperties(string propertyName, int count)
{
var parts = new List<MappedPart>();
for (int i = 0; i < count; i++)
{
parts.Add(new MappedPart
{
PartId = Guid.NewGuid(),
PartCode = $"PART-{i:D3}",
PartName = $"Test Part {i}",
PartType = PartType.Required,
OriginalClass = "component_a",
MappingConfidence = 0.9,
DetectionConfidence = 0.85,
OverallConfidence = 0.875,
BoundingBox = new BoundingBox
{
X = i * 10,
Y = i * 10,
Width = 50,
Height = 50,
Confidence = 0.85,
Class = "component_a"
},
PartAttributes = new Dictionary<string, object>
{
[propertyName] = $"UNIQUE-{i:D3}"
},
IsValid = true,
MappingTimeUtc = DateTime.UtcNow
});
}
return parts;
}
private static List<MappedPart> CreateTestPartsWithDuplicates(string propertyName, int count)
{
var parts = new List<MappedPart>();
for (int i = 0; i < count; i++)
{
parts.Add(new MappedPart
{
PartId = Guid.NewGuid(),
PartCode = $"PART-{i:D3}",
PartName = $"Test Part {i}",
PartType = PartType.Required,
OriginalClass = "component_a",
MappingConfidence = 0.9,
DetectionConfidence = 0.85,
OverallConfidence = 0.875,
BoundingBox = new BoundingBox
{
X = i * 10,
Y = i * 10,
Width = 50,
Height = 50,
Confidence = 0.85,
Class = "component_a"
},
PartAttributes = new Dictionary<string, object>
{
[propertyName] = i < 2 ? "DUPLICATE-001" : $"UNIQUE-{i:D3}" // First two have same value
},
IsValid = true,
MappingTimeUtc = DateTime.UtcNow
});
}
return parts;
}
private static QuantityValidationRule CreateExactQuantityRule(string name, int expectedCount, string productType = "default", int layerNumber = 0)
{
return new QuantityValidationRule
{
RuleId = Guid.NewGuid(),
RuleName = name,
RuleDescription = $"Exact quantity rule for {name}",
ProductTypeCode = productType,
LayerNumber = layerNumber,
ValidationType = QuantityValidationType.Exact,
ExpectedCount = expectedCount,
MinimumCount = expectedCount,
MaximumCount = expectedCount,
IsEnabled = true,
Priority = 1,
CreatedAtUtc = DateTime.UtcNow,
UpdatedAtUtc = DateTime.UtcNow,
Version = "1.0"
};
}
private static QuantityValidationRule CreateMinimumQuantityRule(string name, int minCount, string productType = "default", int layerNumber = 0)
{
return new QuantityValidationRule
{
RuleId = Guid.NewGuid(),
RuleName = name,
RuleDescription = $"Minimum quantity rule for {name}",
ProductTypeCode = productType,
LayerNumber = layerNumber,
ValidationType = QuantityValidationType.Minimum,
MinimumCount = minCount,
ExpectedCount = minCount,
IsEnabled = true,
Priority = 2,
CreatedAtUtc = DateTime.UtcNow,
UpdatedAtUtc = DateTime.UtcNow,
Version = "1.0"
};
}
private static QuantityValidationRule CreateRangeQuantityRule(string name, int minCount, int maxCount, string productType = "default", int layerNumber = 0)
{
return new QuantityValidationRule
{
RuleId = Guid.NewGuid(),
RuleName = name,
RuleDescription = $"Range quantity rule for {name}",
ProductTypeCode = productType,
LayerNumber = layerNumber,
ValidationType = QuantityValidationType.Range,
MinimumCount = minCount,
MaximumCount = maxCount,
ExpectedCount = (minCount + maxCount) / 2,
IsEnabled = true,
Priority = 3,
CreatedAtUtc = DateTime.UtcNow,
UpdatedAtUtc = DateTime.UtcNow,
Version = "1.0"
};
}
#endregion
}