diff --git a/MainWindow.IedBrowser.cs b/MainWindow.IedBrowser.cs
index 9a1bebda9..0f35d44a8 100644
--- a/MainWindow.IedBrowser.cs
+++ b/MainWindow.IedBrowser.cs
@@ -311,28 +311,15 @@ private void RefreshIedBrowserVisibleValues()
row.Value = "Not read";
continue;
}
- if (row.EvidenceField.Length == 0)
- {
+ // The parent q and its inline children share one IEC Quality
+ // presentation path. Unknown flags stay unavailable, never false.
+ if (row.EvidenceKind == "Quality")
+ row.Value = Iec61850QualityPresentation.FormatObserved(
+ live.Value, row.EvidenceField);
+ else if (row.EvidenceField.Length == 0)
row.Value = live.Value;
- continue;
- }
- if (row.EvidenceField == "Validity")
- {
- // A normalized q of Good/Invalid/Questionable is proof of
- // validity, not proof of the remaining independent q bits.
- var quality = live.Value.Trim();
- row.Value = quality.Equals("good", StringComparison.OrdinalIgnoreCase) ||
- quality.Equals("invalid", StringComparison.OrdinalIgnoreCase) ||
- quality.Equals("questionable", StringComparison.OrdinalIgnoreCase)
- ? quality.ToLowerInvariant() : "Not exposed";
- }
else
- {
- // The current report value formatter exposes normalized q/t
- // without raw bits. Do not paint default false/true for bits
- // that were not included in the model's actual value evidence.
row.Value = "Not exposed";
- }
}
}
diff --git a/Models/Iec61850ValueStatePresentation.cs b/Models/Iec61850ValueStatePresentation.cs
index dba01b57a..012cf4842 100644
--- a/Models/Iec61850ValueStatePresentation.cs
+++ b/Models/Iec61850ValueStatePresentation.cs
@@ -1,3 +1,5 @@
+using ArIED61850Tester.Protocol.Mms;
+
namespace ArIED61850Tester.Models;
///
@@ -255,6 +257,33 @@ private static bool LooksLikeAnalogAggregate(string text)
///
public static class Iec61850QualityPresentation
{
+ public static string FormatObserved(string? observed, string evidenceField = "")
+ {
+ if (MmsQualityDecoder.TryDecodeRendered(observed, out var typed))
+ return typed.FormatField(evidenceField);
+
+ var value = (observed ?? "").Trim();
+ // Legacy report projection may retain only the validity word. It is
+ // not evidence for any other Quality bit (including a zero bit).
+ var separator = value.IndexOf(" / ", StringComparison.Ordinal);
+ var validity = separator < 0 ? value : value[..separator];
+ if (validity.Equals("Good", StringComparison.OrdinalIgnoreCase) ||
+ validity.Equals("Invalid", StringComparison.OrdinalIgnoreCase) ||
+ validity.Equals("Reserved", StringComparison.OrdinalIgnoreCase) ||
+ validity.Equals("Questionable", StringComparison.OrdinalIgnoreCase))
+ {
+ if (evidenceField == "Validity")
+ return validity.ToLowerInvariant();
+ return evidenceField.Length == 0 ? value : "Not exposed";
+ }
+
+ // The malformed renderer text is still preserved for diagnostics,
+ // never presented as a trusted live IEC state.
+ return evidenceField.Length == 0 &&
+ value.Length > 0 && !value.StartsWith("bits(", StringComparison.OrdinalIgnoreCase)
+ ? value : "Not exposed";
+ }
+
public const string Good = "Good";
public const string Attention = "Attention";
public const string Bad = "Bad";
diff --git a/Models/IedBrowserPresentation.cs b/Models/IedBrowserPresentation.cs
index 85a85a0cf..121643f85 100644
--- a/Models/IedBrowserPresentation.cs
+++ b/Models/IedBrowserPresentation.cs
@@ -179,6 +179,8 @@ public string Value
// A typed child of a received q/t field; never treat the entire parent
// value text as this individual Boolean/time-quality bit.
public string EvidenceField { get; init; } = "";
+ // Schema-qualified presentation type; never infer a q/t bit from a DO name.
+ public string EvidenceKind { get; init; } = "";
public string FunctionalConstraint { get; init; } = "";
private IReadOnlyList? _children;
public IedBrowserTreeRow? NavigateTo { get; init; }
diff --git a/Models/IedBrowserStructuredDetails.cs b/Models/IedBrowserStructuredDetails.cs
index 8c885ff48..1eb4d7292 100644
--- a/Models/IedBrowserStructuredDetails.cs
+++ b/Models/IedBrowserStructuredDetails.cs
@@ -95,6 +95,8 @@ private static IReadOnlyList BuildAt(
? attr.MmsReference
: attr.ObjectReference + "." +
string.Join(".", groupArray[0].parts.Take(pathIndex + 1)),
+ EvidenceKind = hasDirect && name == "q" ? "Quality" :
+ hasDirect && name == "t" ? "Timestamp" : "",
FunctionalConstraint = groupArray.All(x =>
string.Equals(x.attr.FunctionalConstraint, attr.FunctionalConstraint,
StringComparison.OrdinalIgnoreCase))
@@ -119,7 +121,8 @@ IedBrowserDetailRow QualityField(string name, string evidence)
{
ValueReference = qualityReference,
FunctionalConstraint = fc,
- EvidenceField = evidence
+ EvidenceField = evidence,
+ EvidenceKind = "Quality"
};
var details = new[] { "Overflow", "OutOfRange", "BadReference",
"Oscillatory", "Failure", "OldData", "Inconsistent", "Inaccurate" };
@@ -133,7 +136,8 @@ IedBrowserDetailRow QualityField(string name, string evidence)
{
ValueReference = qualityReference,
FunctionalConstraint = fc,
- EvidenceField = name
+ EvidenceField = name,
+ EvidenceKind = "Quality"
}).ToArray()
},
QualityField("Source", "Source"),
@@ -150,6 +154,7 @@ private static IReadOnlyList TimeChildren(
{
ValueReference = timestampReference,
FunctionalConstraint = fc,
- EvidenceField = name
+ EvidenceField = name,
+ EvidenceKind = "Timestamp"
}).ToArray();
}
diff --git a/Protocol/Mms/MmsQualityDecoder.cs b/Protocol/Mms/MmsQualityDecoder.cs
index 14504c54f..e82ebafd0 100644
--- a/Protocol/Mms/MmsQualityDecoder.cs
+++ b/Protocol/Mms/MmsQualityDecoder.cs
@@ -1,57 +1,130 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
namespace ArIED61850Tester.Protocol.Mms;
-public static class MmsQualityDecoder
+/// Thirteen verified IEC 61850 Quality bits, not inferred defaults.
+public sealed record MmsQualityEvidence(
+ string Validity, bool Overflow, bool OutOfRange, bool BadReference,
+ bool Oscillatory, bool Failure, bool OldData, bool Inconsistent,
+ bool Inaccurate, bool Substituted, bool Test, bool OperatorBlocked)
{
- public static string Decode(ReadOnlySpan bitStringPayload)
- {
- if (bitStringPayload.Length < 2)
- return "Quality: empty";
+ public string Source => Substituted ? "substituted" : "process";
- var bits = ExpandBits(bitStringPayload[1..]);
- var validity = bits.Count >= 2
- ? ((bits[0] ? 2 : 0) | (bits[1] ? 1 : 0))
- : 0;
+ public string FormatField(string field) => field switch
+ {
+ "" => Validity,
+ "Validity" => Validity.ToLowerInvariant(),
+ "Source" => Source,
+ "Overflow" => Flag(Overflow),
+ "OutOfRange" => Flag(OutOfRange),
+ "BadReference" => Flag(BadReference),
+ "Oscillatory" => Flag(Oscillatory),
+ "Failure" => Flag(Failure),
+ "OldData" => Flag(OldData),
+ "Inconsistent" => Flag(Inconsistent),
+ "Inaccurate" => Flag(Inaccurate),
+ "Test" => Flag(Test),
+ "OperatorBlocked" => Flag(OperatorBlocked),
+ _ => "Not exposed"
+ };
- var validityText = validity switch
+ public string Summary
+ {
+ get
{
- 0 => "Good",
- 1 => "Invalid",
- 2 => "Reserved",
- 3 => "Questionable",
- _ => "Unknown"
- };
-
- var details = new List { validityText };
- Add(details, bits, 2, "Overflow");
- Add(details, bits, 3, "OutOfRange");
- Add(details, bits, 4, "BadReference");
- Add(details, bits, 5, "Oscillatory");
- Add(details, bits, 6, "Failure");
- Add(details, bits, 7, "OldData");
- Add(details, bits, 8, "Inconsistent");
- Add(details, bits, 9, "Inaccurate");
- Add(details, bits, 10, "Substituted");
- Add(details, bits, 11, "Test");
- Add(details, bits, 12, "OperatorBlocked");
- return string.Join(" / ", details);
+ var parts = new List { Validity };
+ Add(parts, Overflow, "Overflow");
+ Add(parts, OutOfRange, "OutOfRange");
+ Add(parts, BadReference, "BadReference");
+ Add(parts, Oscillatory, "Oscillatory");
+ Add(parts, Failure, "Failure");
+ Add(parts, OldData, "OldData");
+ Add(parts, Inconsistent, "Inconsistent");
+ Add(parts, Inaccurate, "Inaccurate");
+ Add(parts, Substituted, "Substituted");
+ Add(parts, Test, "Test");
+ Add(parts, OperatorBlocked, "OperatorBlocked");
+ return string.Join(" / ", parts);
+ }
}
- private static List ExpandBits(ReadOnlySpan bytes)
+ private static string Flag(bool value) => value ? "true" : "false";
+ private static void Add(List parts, bool enabled, string name)
{
- var result = new List(bytes.Length * 8);
- foreach (var b in bytes)
- {
- for (var bit = 7; bit >= 0; bit--)
- result.Add(((b >> bit) & 0x01) != 0);
- }
- return result;
+ if (enabled) parts.Add(name);
+ }
+}
+
+///
+/// Existing MMS Quality decoder, now fail-closed on width and unused-bit
+/// metadata. The rendered-value adapter is used only for schema-verified q.
+///
+public static class MmsQualityDecoder
+{
+ public static string Decode(ReadOnlySpan bitStringPayload)
+ => TryDecode(bitStringPayload, out var quality)
+ ? quality.Summary : "Quality: unavailable";
+
+ public static bool TryDecode(ReadOnlySpan payload, out MmsQualityEvidence quality)
+ {
+ quality = null!;
+ // Quality has exactly 13 significant bits. Reject nonzero padding.
+ if (payload.Length != 3 || payload[0] != 3 || (payload[2] & 0x07) != 0)
+ return false;
+
+ var validity = (Bit(payload, 0) ? 2 : 0) | (Bit(payload, 1) ? 1 : 0);
+ quality = new MmsQualityEvidence(
+ validity switch
+ {
+ 0 => "Good",
+ 1 => "Invalid",
+ 2 => "Reserved",
+ _ => "Questionable"
+ },
+ Bit(payload, 2), Bit(payload, 3), Bit(payload, 4),
+ Bit(payload, 5), Bit(payload, 6), Bit(payload, 7),
+ Bit(payload, 8), Bit(payload, 9), Bit(payload, 10),
+ Bit(payload, 11), Bit(payload, 12));
+ return true;
}
- private static void Add(List details, List bits, int index, string label)
+ private static bool Bit(ReadOnlySpan payload, int index)
+ => (payload[1 + index / 8] & (0x80 >> (index % 8))) != 0;
+
+ ///
+ /// Adapter for the already existing MMS renderer's bits(0000,unused=3)
+ /// representation. It does not decide which references have Quality type.
+ ///
+ public static bool TryDecodeRendered(string? rendered, out MmsQualityEvidence quality)
{
- if (bits.Count > index && bits[index]) details.Add(label);
+ quality = null!;
+ if (string.IsNullOrWhiteSpace(rendered))
+ return false;
+
+ var text = rendered.AsSpan().Trim();
+ if (!text.StartsWith("bits(".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
+ text[^1] != ')')
+ return false;
+ var body = text[5..^1];
+ var comma = body.IndexOf(',');
+ if (comma < 0) return false;
+ var hex = body[..comma].Trim();
+ if (hex.StartsWith("0x".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ hex = hex[2..];
+ var suffix = body[(comma + 1)..].Trim();
+ if (!suffix.StartsWith("unused=".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
+ !byte.TryParse(suffix[7..].Trim(), NumberStyles.Integer,
+ CultureInfo.InvariantCulture, out var padding) ||
+ hex.Length != 4 ||
+ !byte.TryParse(hex[..2], NumberStyles.HexNumber,
+ CultureInfo.InvariantCulture, out var first) ||
+ !byte.TryParse(hex[2..], NumberStyles.HexNumber,
+ CultureInfo.InvariantCulture, out var second))
+ return false;
+
+ Span payload = stackalloc byte[] { padding, first, second };
+ return TryDecode(payload, out quality);
}
}
diff --git a/Protocol/Mms/MmsReadResponseDecoder.cs b/Protocol/Mms/MmsReadResponseDecoder.cs
index ba28d5dde..f3dfe8b35 100644
--- a/Protocol/Mms/MmsReadResponseDecoder.cs
+++ b/Protocol/Mms/MmsReadResponseDecoder.cs
@@ -47,6 +47,12 @@ public static MmsReadDecodeResult DecodeSingleVariable(byte[] presentationPayloa
if (TryDecodeByHint(values, hint, out var hinted))
return Ok(hinted, hex);
+ // A declared Quality/Dbpos BIT STRING of the wrong width is not
+ // another valid process type. Never reinterpret it via generic fallback.
+ if ((hint.Contains("quality") || hint == "q" || hint.Contains("dbpos")) &&
+ values.Any(v => v.Tag == 0x84))
+ return Fail("MMS BIT STRING does not match the declared IEC value width.", hex);
+
if (TryDecodeAny(values, out var value))
return Ok(value, hex);
@@ -288,24 +294,21 @@ private static bool TryDecodeInteger(List values, out object? value)
private static bool TryDecodeDbpos(List values, out object? value)
{
- var tlv = values.LastOrDefault(v => v.Tag == 0x84 && v.Length >= 2);
- if (tlv.Tag == 0x84)
+ var tlv = values.LastOrDefault(v => v.Tag == 0x84);
+ // IEC 61850 Dbpos is a two-bit MMS BIT STRING. Quality and other
+ // BIT STRINGs must never be interpreted as a process position.
+ if (tlv.Tag == 0x84 && tlv.Length == 2 &&
+ tlv.Value.Span[0] == 6 && (tlv.Value.Span[1] & 0x3F) == 0)
{
- var span = tlv.Value.Span;
- var bytes = span[1..];
- if (bytes.Length > 0)
+ var code = tlv.Value.Span[1] >> 6;
+ value = code switch
{
- var code = (bytes[0] >> 6) & 0x03;
- value = code switch
- {
- 0 => "Intermediate [00]",
- 1 => "Open [01]",
- 2 => "Close [10]",
- 3 => "Invalid [11]",
- _ => code.ToString(CultureInfo.InvariantCulture)
- };
- return true;
- }
+ 0 => "Intermediate [00]",
+ 1 => "Open [01]",
+ 2 => "Close [10]",
+ _ => "Bad state [11]"
+ };
+ return true;
}
value = null;
return false;
@@ -314,9 +317,10 @@ private static bool TryDecodeDbpos(List values, out object? value)
private static bool TryDecodeQuality(List values, out object? value)
{
var tlv = values.LastOrDefault(v => v.Tag == 0x84 && v.Length >= 2);
- if (tlv.Tag == 0x84)
+ if (tlv.Tag == 0x84 &&
+ MmsQualityDecoder.TryDecode(tlv.Value.Span, out var quality))
{
- value = MmsQualityDecoder.Decode(tlv.Value.Span);
+ value = quality.Summary;
return true;
}
value = null;
diff --git a/tests/ARSAS.Tests/M2TypedQualityEvidenceTests.cs b/tests/ARSAS.Tests/M2TypedQualityEvidenceTests.cs
new file mode 100644
index 000000000..d490d5dab
--- /dev/null
+++ b/tests/ARSAS.Tests/M2TypedQualityEvidenceTests.cs
@@ -0,0 +1,152 @@
+using ArIED61850Tester.Protocol.Mms;
+using AR.Iec61850.Discovery;
+using ArIED61850Tester.Models;
+using ArIED61850Tester.Services;
+
+namespace ARSAS.Tests;
+
+public sealed class M2TypedQualityEvidenceTests
+{
+ [Theory]
+ [InlineData(0x00, "Good")]
+ [InlineData(0x40, "Invalid")]
+ [InlineData(0x80, "Reserved")]
+ [InlineData(0xC0, "Questionable")]
+ public void WireValidity_UsesOnlyThirteenProvenBits(int first, string expected)
+ {
+ var raw = new byte[] { 3, (byte)first, 0 };
+ Assert.True(MmsQualityDecoder.TryDecode(raw, out var evidence));
+ Assert.Equal(expected, evidence.Validity);
+ Assert.Equal(expected, MmsQualityDecoder.Decode(raw));
+ Assert.Equal("process", evidence.Source);
+ Assert.Equal("false", evidence.FormatField("OldData"));
+ }
+
+ [Fact]
+ public void QualityFlags_AreTypedAndReadableByEveryDetailField()
+ {
+ // Validity=Good; all eleven independent detail/source flags set.
+ var raw = new byte[] { 3, 0x3F, 0xF8 };
+ Assert.True(MmsQualityDecoder.TryDecode(raw, out var quality));
+ Assert.Equal("Good", quality.Validity);
+ Assert.Equal("substituted", quality.Source);
+ foreach (var field in new[] {
+ "Overflow", "OutOfRange", "BadReference", "Oscillatory",
+ "Failure", "OldData", "Inconsistent", "Inaccurate",
+ "Test", "OperatorBlocked" })
+ Assert.Equal("true", quality.FormatField(field));
+ Assert.Equal("Good", Iec61850QualityPresentation.FormatObserved("bits(3FF8,unused=3)"));
+ Assert.Equal("good", Iec61850QualityPresentation.FormatObserved("bits(3FF8,unused=3)", "Validity"));
+ Assert.Equal("substituted", Iec61850QualityPresentation.FormatObserved("bits(3FF8,unused=3)", "Source"));
+ Assert.Equal("true", Iec61850QualityPresentation.FormatObserved("bits(3FF8,unused=3)", "OperatorBlocked"));
+ Assert.Contains("OperatorBlocked", quality.Summary);
+ }
+
+ [Theory]
+ [InlineData("bits(0000,unused=3)", "Good")]
+ [InlineData("bits(4000, unused=3)", "Invalid")]
+ [InlineData("bits(8000,unused=3)", "Reserved")]
+ [InlineData("bits(C000,unused=3)", "Questionable")]
+ public void ExistingEngineRendererText_AdaptsToTypedEvidence(string rendered, string validity)
+ {
+ Assert.True(MmsQualityDecoder.TryDecodeRendered(rendered, out var quality));
+ Assert.Equal(validity, quality.Validity);
+ Assert.Equal(validity, Iec61850QualityPresentation.FormatObserved(rendered));
+ }
+
+ [Theory]
+ [InlineData("bits(40,unused=6)")]
+ [InlineData("bits(0000,unused=0)")]
+ [InlineData("bits(0001,unused=3)")]
+ [InlineData("bits(0000,unused=8)")]
+ [InlineData("bits(000000,unused=3)")]
+ [InlineData("bits(FFFF,unused=3)")]
+ [InlineData("bits(nonsense,unused=3)")]
+ public void WrongWidthOrNonzeroPadding_NeverCreatesSyntheticQuality(string rendered)
+ {
+ Assert.False(MmsQualityDecoder.TryDecodeRendered(rendered, out _));
+ Assert.Equal("Not exposed", Iec61850QualityPresentation.FormatObserved(rendered, "Validity"));
+ }
+
+ [Fact]
+ public void NormalizedReportValidity_DoesNotProveMissingIndependentFlags()
+ {
+ Assert.Equal("Good", Iec61850QualityPresentation.FormatObserved("Good"));
+ Assert.Equal("good", Iec61850QualityPresentation.FormatObserved("Good", "Validity"));
+ Assert.Equal("Not exposed", Iec61850QualityPresentation.FormatObserved("Good", "Failure"));
+ Assert.Equal("Not exposed", Iec61850QualityPresentation.FormatObserved("Invalid / Overflow", "Overflow"));
+ Assert.Equal("invalid", Iec61850QualityPresentation.FormatObserved("Invalid / Overflow", "Validity"));
+ Assert.Equal("Quality: unavailable", MmsQualityDecoder.Decode(new byte[] { 3, 0, 1 }));
+ Assert.False(MmsQualityDecoder.TryDecode(new byte[] { 6, 0x40 }, out _));
+ }
+
+ [Theory]
+ [InlineData("bits(00,unused=6)", "Intermediate [00]")]
+ [InlineData("bits(40,unused=6)", "Open [01]")]
+ [InlineData("bits(80,unused=6)", "Close [10]")]
+ [InlineData("bits(C0,unused=6)", "Bad state [11]")]
+ public void ExistingSharedDbposFormatter_RendersAllFourCodesWithoutQualityLeak(
+ string raw, string expected)
+ {
+ Assert.Equal(expected, Iec61850ValueFormatter.FormatReportProcessValue(
+ raw, "Dbpos", "", "Position", "LD/CSWI1.Pos.stVal"));
+ Assert.Equal(raw, Iec61850ValueFormatter.FormatReportProcessValue(
+ raw, "BitString", "", "Measurement", "LD/GGIO1.RawBits"));
+ Assert.False(MmsQualityDecoder.TryDecodeRendered(raw, out _));
+ }
+ [Fact]
+ public void MmsRead_EnforcesDeclaredTypeInsteadOfReinterpretingWrongWidth()
+ {
+ // Minimal confirmed-response envelope; the decoder sees the exact
+ // MMS Data BIT STRING encoded at the normal constructed response seam.
+ var position = MmsReadResponseDecoder.DecodeSingleVariable(
+ new byte[] { 0xA1, 0x07, 0x02, 0x01, 0x01, 0x84, 0x02, 0x06, 0x40 }, "Dbpos");
+ Assert.True(position.IsSuccess, position.Message);
+ Assert.Equal("Open [01]", position.Value);
+
+ var quality = MmsReadResponseDecoder.DecodeSingleVariable(
+ new byte[] { 0xA1, 0x08, 0x02, 0x01, 0x01, 0x84, 0x03, 0x03, 0x00, 0x00 }, "Quality");
+ Assert.True(quality.IsSuccess, quality.Message);
+ Assert.Equal("Good", quality.Value);
+
+ var wrongDbpos = MmsReadResponseDecoder.DecodeSingleVariable(
+ new byte[] { 0xA1, 0x08, 0x02, 0x01, 0x01, 0x84, 0x03, 0x03, 0x00, 0x00 }, "Dbpos");
+ Assert.False(wrongDbpos.IsSuccess);
+ Assert.Contains("width", wrongDbpos.Message, StringComparison.OrdinalIgnoreCase);
+
+ var wrongQuality = MmsReadResponseDecoder.DecodeSingleVariable(
+ new byte[] { 0xA1, 0x07, 0x02, 0x01, 0x01, 0x84, 0x02, 0x06, 0x40 }, "Quality");
+ Assert.False(wrongQuality.IsSuccess);
+ }
+
+ [Fact]
+ public void BrowserAttributeRows_CarrySchemaBasedQualityEvidenceIdentity()
+ {
+ var data = new[] {
+ new LiveIedDataAttributeModel {
+ AttributePath = "Pos.q", ObjectReference = "LD/CSWI1.Pos",
+ MmsReference = "LD/CSWI1$ST$Pos$q", FunctionalConstraint = "ST"
+ },
+ new LiveIedDataAttributeModel {
+ AttributePath = "Pos.t", ObjectReference = "LD/CSWI1.Pos",
+ MmsReference = "LD/CSWI1$ST$Pos$t", FunctionalConstraint = "ST"
+ },
+ new LiveIedDataAttributeModel {
+ AttributePath = "Pos.stVal", ObjectReference = "LD/CSWI1.Pos",
+ MmsReference = "LD/CSWI1$ST$Pos$stVal", FunctionalConstraint = "ST"
+ }
+ };
+ var rows = IedBrowserStructuredDetails.AttributeRows(data, "Pos");
+ var q = Assert.Single(rows.Where(row => row.Name == "q [ST]"));
+ var t = Assert.Single(rows.Where(row => row.Name == "t [ST]"));
+ var process = Assert.Single(rows.Where(row => row.Name == "stVal [ST]"));
+ Assert.Equal("Quality", q.EvidenceKind);
+ Assert.Equal("Timestamp", t.EvidenceKind);
+ Assert.Empty(process.EvidenceKind);
+ var children = q.GetChildren();
+ Assert.Contains(children, row => row.EvidenceKind == "Quality" && row.EvidenceField == "Validity");
+ Assert.Contains(children, row => row.EvidenceKind == "Quality" && row.EvidenceField == "Source");
+ Assert.All(t.GetChildren(), row => Assert.Equal("Timestamp", row.EvidenceKind));
+ }
+
+}