feat(validation): semantic checks for List attributes (element type, default value, trigger operands)
This commit is contained in:
+213
@@ -1056,4 +1056,217 @@ public class SemanticValidatorTests
|
||||
var result = _sut.Validate(config);
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.ParameterMismatch);
|
||||
}
|
||||
|
||||
// ── MV-5 List attribute semantics ────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_ValidElementTypeAndDefault_NoError()
|
||||
{
|
||||
// List + String element + a well-formed JSON-array default — fully valid.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute
|
||||
{
|
||||
CanonicalName = "Tags",
|
||||
DataType = "List",
|
||||
ElementDataType = "String",
|
||||
Value = "[\"a\",\"b\"]"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.MissingMetadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_NullElementType_ReturnsError()
|
||||
{
|
||||
// List with no element type — rule 1 violation (cardinality).
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute { CanonicalName = "Tags", DataType = "List", ElementDataType = null }
|
||||
]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
Assert.Contains(result.Errors, e =>
|
||||
e.Category == ValidationCategory.MissingMetadata &&
|
||||
e.EntityName == "Tags" &&
|
||||
e.Message.Contains("element type", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ScalarAttribute_WithElementType_ReturnsError()
|
||||
{
|
||||
// A non-List attribute must not carry an element type — rule 1 violation.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute { CanonicalName = "Status", DataType = "String", ElementDataType = "String" }
|
||||
]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
Assert.Contains(result.Errors, e =>
|
||||
e.Category == ValidationCategory.MissingMetadata &&
|
||||
e.EntityName == "Status" &&
|
||||
e.Message.Contains("element type", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_NonScalarElementType_ReturnsError()
|
||||
{
|
||||
// Binary and List are not valid List element scalars — rule 1 violation.
|
||||
foreach (var bad in new[] { "Binary", "List" })
|
||||
{
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute { CanonicalName = "Tags", DataType = "List", ElementDataType = bad }
|
||||
]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
Assert.Contains(result.Errors, e =>
|
||||
e.Category == ValidationCategory.MissingMetadata &&
|
||||
e.EntityName == "Tags");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_MalformedJsonDefault_ReturnsError()
|
||||
{
|
||||
// Unterminated JSON array default — rule 2 (default parseability) violation.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute
|
||||
{
|
||||
CanonicalName = "Tags",
|
||||
DataType = "List",
|
||||
ElementDataType = "String",
|
||||
Value = "[\"a\""
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
Assert.Contains(result.Errors, e =>
|
||||
e.Category == ValidationCategory.MissingMetadata &&
|
||||
e.EntityName == "Tags" &&
|
||||
e.Message.Contains("default", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_DefaultElementNotParseable_ReturnsError()
|
||||
{
|
||||
// Element type Int32 but the default carries a non-integer element — rule 2 violation.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute
|
||||
{
|
||||
CanonicalName = "Counts",
|
||||
DataType = "List",
|
||||
ElementDataType = "Int32",
|
||||
Value = "[\"x\"]"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
Assert.Contains(result.Errors, e =>
|
||||
e.Category == ValidationCategory.MissingMetadata &&
|
||||
e.EntityName == "Counts" &&
|
||||
e.Message.Contains("Int32"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_EmptyDefault_NoDefaultError()
|
||||
{
|
||||
// No authored default → rule 2 is inert; only the (satisfied) cardinality rule applies.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute { CanonicalName = "Tags", DataType = "List", ElementDataType = "String", Value = "" }
|
||||
]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
Assert.DoesNotContain(result.Errors, e => e.Category == ValidationCategory.MissingMetadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_AsRangeViolationOperand_ReturnsError()
|
||||
{
|
||||
// Rule 3 confirmation: a List attribute is non-numeric, so a RangeViolation
|
||||
// trigger over it is rejected by the existing NumericDataTypes guard.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute { CanonicalName = "Tags", DataType = "List", ElementDataType = "Int32" }
|
||||
],
|
||||
Alarms =
|
||||
[
|
||||
new ResolvedAlarm
|
||||
{
|
||||
CanonicalName = "BadAlarm",
|
||||
TriggerType = "RangeViolation",
|
||||
TriggerConfiguration = "{\"attributeName\":\"Tags\"}"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
Assert.Contains(result.Errors, e =>
|
||||
e.Category == ValidationCategory.TriggerOperandType &&
|
||||
e.Message.Contains("non-numeric"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_ListAttribute_AsHiLoOperand_ReturnsError()
|
||||
{
|
||||
// Rule 3 confirmation for HiLo.
|
||||
var config = new FlattenedConfiguration
|
||||
{
|
||||
InstanceUniqueName = "Instance1",
|
||||
Attributes =
|
||||
[
|
||||
new ResolvedAttribute { CanonicalName = "Tags", DataType = "List", ElementDataType = "Double" }
|
||||
],
|
||||
Alarms =
|
||||
[
|
||||
new ResolvedAlarm
|
||||
{
|
||||
CanonicalName = "BadHiLo",
|
||||
TriggerType = "HiLo",
|
||||
TriggerConfiguration = "{\"attributeName\":\"Tags\",\"hi\":80}"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var result = _sut.Validate(config);
|
||||
Assert.Contains(result.Errors, e =>
|
||||
e.Category == ValidationCategory.TriggerOperandType &&
|
||||
e.Message.Contains("non-numeric"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user