Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 31 additions & 21 deletions Models/IedBrowserPresentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -603,35 +603,45 @@ void Member(string name, string value = "Not read", int depth = 0, string source
rows.Add(new("DataHeader", "Name", "Value"));
if (model is not null)
{
// Group by real model DO; never concatenate ObjectReference and
// AttributePath, which can produce duplicate setVal spelling.
// Active SG values use the existing lazy IEC DA projector.
// Never collapse all SG/SE attributes into an unbound DO.
var settings = model.LogicalDevices
.Where(ld => string.Equals(ld.MmsDomain, block.Domain, StringComparison.Ordinal))
.Where(ld => string.Equals(ld.MmsDomain, block.Domain,
StringComparison.Ordinal))
.SelectMany(ld => ld.LogicalNodes)
.SelectMany(ln => ln.DataObjects.Select(obj => (Ln: ln, Object: obj)))
.Select(x => (x.Ln, x.Object, Attributes: x.Object.Attributes
.Where(a => a.FunctionalConstraint is "SG" or "SE").ToArray()))
.Where(x => x.Attributes.Length > 0)
.ToArray();
var shown = 0;
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var setting in settings)
{
var attributes = setting.Attributes;
var sole = attributes.Length == 1 ? attributes[0] : null;
rows.Add(new IedBrowserDetailRow("Data",
$"{setting.Ln.Name}.{setting.Object.Name}", "Not read", 0,
"SG/SE declaration; effective setting group not verified")
var sg = setting.Object.Attributes.Where(a =>
string.Equals(a.FunctionalConstraint, "SG",
StringComparison.OrdinalIgnoreCase)).ToArray();
if (sg.Length == 0) continue;
foreach (var field in IedBrowserStructuredDetails.AttributeRows(
sg, setting.Object.Name))
{
// Never project a multi-leaf parent from an arbitrary child.
ValueReference = sole?.MmsReference ?? "",
FunctionalConstraint = sole?.FunctionalConstraint ?? "",
EngineeringUnitReference = sole?.MmsReference ?? "",
IsEngineeringValue = sole is not null,
LoadChildren = () => IedBrowserStructuredDetails.AttributeRows(
attributes, setting.Object.Name, 1)
});
// Prefer a unique scalar descendant when direct
// parent and child declarations coexist.
var refToRead = field.FallbackValueReference.Length > 0
? field.FallbackValueReference : field.ValueReference;
var identity = setting.Object.Reference + "|" + field.Name;
if (!seen.Add(identity)) continue;
rows.Add(field with
{
Name = $"{setting.Ln.Name}.{setting.Object.Name}.{field.Name}",
ValueReference = refToRead,
FallbackValueReference = refToRead == field.ValueReference
? "" : field.ValueReference,
Source = field.Source + " · active SG declaration"
});
shown++;
}
}
if (settings.Length == 0)
rows.Add(new("Message", "No SG/SE parameters available in this model."));
if (shown == 0)
rows.Add(new("Message",
"No active-group [SG] parameters in this model; [SE] edit values cannot substitute."));
}
else
rows.Add(new("Message", "No model is available for SG/SE setting attributes."));
Expand Down
10 changes: 6 additions & 4 deletions Models/IedBrowserReadPlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,17 @@ public static IReadOnlyList<IedBrowserReadTarget> Build(
LiveIedLogicalDeviceModel ld => ld.LogicalNodes.SelectMany(ln => ln.DataObjects)
.SelectMany(o => o.Attributes),
LiveIedDataSetModel set => ForDataSet(all, schema, set),
// SGCB page: model-proven SG/SE parameters. The control
// state's exact safe fields are appended separately below.
// The visible SGCB page is active SG, never an implicit
// selection/confirmation of edit-group [SE] attributes.
LiveIedControlBlockModel block when page.Kind == "SGCB" =>
model.LogicalDevices
.Where(ld => string.Equals(ld.MmsDomain, block.Domain, StringComparison.Ordinal))
.Where(ld => string.Equals(ld.MmsDomain, block.Domain,
StringComparison.Ordinal))
.SelectMany(ld => ld.LogicalNodes)
.SelectMany(ln => ln.DataObjects)
.SelectMany(obj => obj.Attributes)
.Where(attr => attr.FunctionalConstraint is "SG" or "SE"),
.Where(attr => string.Equals(attr.FunctionalConstraint, "SG",
StringComparison.OrdinalIgnoreCase)),
// GoCB DataSet member reads do not prove GoEna. Only exact
// safe control fields from discovery's FC inventory are legal.
LiveIedControlBlockModel when page.Kind == "GoCB" =>
Expand Down
102 changes: 102 additions & 0 deletions tests/ARSAS.Tests/IedBrowserActiveSettingProjectionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using AR.Iec61850.Discovery;
using ArIED61850Tester.Models;

namespace ARSAS.Tests;

public sealed class IedBrowserActiveSettingProjectionTests
{
private const string Owner = "LD/ATCC1.BndWid";
private static LiveIedDataAttributeModel Attr(string fc, string path) => new()
{
ObjectReference = Owner,
AttributePath = "BndWid." + path,
FunctionalConstraint = fc,
MmsReference = "LD/ATCC1$" + fc + "$BndWid$" + path.Replace('.', '$')
};
private static (LiveIedModelDiscoveryDocument Model, IedBrowserTreeRow Page)
Fixture(params LiveIedDataAttributeModel[] attrs)
{
var model = new LiveIedModelDiscoveryDocument
{
LogicalDevices = [new LiveIedLogicalDeviceModel
{
MmsDomain = "LD",
LogicalNodes = [new LiveIedLogicalNodeModel
{
Name = "ATCC1",
DataObjects = [new LiveIedDataObjectModel
{
Name = "BndWid", Reference = Owner, Attributes = attrs
}]
}]
}]
};
return (model, new IedBrowserTreeRow("sg/LD", "SGCB", "SGCB", 3,
new LiveIedControlBlockModel { Kind = "SettingGroupControl",
Domain = "LD", LogicalNode = "LLN0", Name = "SGCB",
Reference = "LD/LLN0.SP.SGCB" }));
}

[Fact]
public void MultiLeafSgExposesAlreadyReadScalarAtParentAndChild()
{
var (model, page) = Fixture(Attr("SG", "setMag.f"),
Attr("SG", "setVal"), Attr("SE", "setMag.f"), Attr("CF", "units.SIUnit"));
var rows = IedBrowserPresentation.Details(page, model, true)
.Where(r => r.Kind == "Data").ToArray();
Assert.Equal(2, rows.Length);
var mag = Assert.Single(rows.Where(r => r.Name == "ATCC1.BndWid.setMag [SG]"));

Check warning on line 48 in tests/ARSAS.Tests/IedBrowserActiveSettingProjectionTests.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Do not use a Where clause to filter before calling Assert.Single. Use the overload of Assert.Single that accepts a filtering function. (https://xunit.net/xunit.analyzers/rules/xUnit2031)
var scalar = Assert.Single(rows.Where(r => r.Name == "ATCC1.BndWid.setVal [SG]"));

Check warning on line 49 in tests/ARSAS.Tests/IedBrowserActiveSettingProjectionTests.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Do not use a Where clause to filter before calling Assert.Single. Use the overload of Assert.Single that accepts a filtering function. (https://xunit.net/xunit.analyzers/rules/xUnit2031)
Assert.Equal("LD/ATCC1.BndWid.setMag.f",
IedBrowserLiveValues.CanonicalReference(mag.ValueReference));
Assert.Equal("LD/ATCC1$SG$BndWid$setVal", scalar.ValueReference);
Assert.Equal("SG", mag.FunctionalConstraint);
var child = Assert.Single(mag.GetChildren());
Assert.Equal("f [SG]", child.Name);
Assert.Equal(IedBrowserLiveValues.CanonicalReference(mag.ValueReference),
IedBrowserLiveValues.CanonicalReference(child.ValueReference));
var values = new IedBrowserLiveValues();
var association = values.BeginAssociation();
Assert.True(values.PublishForAssociation(association,
mag.ValueReference, "SG", "3", "", "", "MMS Read"));
Assert.True(values.TryGet(mag.ValueReference, "SG", out var parent));
Assert.True(values.TryGet(child.ValueReference, "SG", out var leaf));
Assert.Equal("3", parent.Value);
Assert.Equal(parent.Value, leaf.Value);
}

[Fact]
public void ExplicitSgReadOmitsSeEditGroupAndCfTargets()
{
var (model, page) = Fixture(Attr("SG", "setMag.f"),
Attr("SG", "setVal"), Attr("SE", "setMag.f"),
Attr("CF", "units.SIUnit"));
var targets = IedBrowserReadPlan.Build(model, page, readAll: false);
Assert.Equal(2, targets.Count);
Assert.All(targets, t => Assert.Equal("SG", t.FunctionalConstraint));
Assert.Contains(targets, t => t.Reference == "LD/ATCC1$SG$BndWid$setMag$f");
Assert.Contains(targets, t => t.Reference == "LD/ATCC1$SG$BndWid$setVal");
}

[Fact]
public void SgParentWithDirectAndPrimitiveDeclarationsPrefersVerifiedF()
{
var (model, page) = Fixture(Attr("SG", "setMag"),
Attr("SG", "setMag.f"), Attr("SE", "setMag.f"));
var root = Assert.Single(IedBrowserPresentation.Details(page, model, true)

Check warning on line 86 in tests/ARSAS.Tests/IedBrowserActiveSettingProjectionTests.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Do not use a Where clause to filter before calling Assert.Single. Use the overload of Assert.Single that accepts a filtering function. (https://xunit.net/xunit.analyzers/rules/xUnit2031)
.Where(r => r.Kind == "Data"));
Assert.Equal("LD/ATCC1.BndWid.setMag.f",
IedBrowserLiveValues.CanonicalReference(root.ValueReference));
Assert.Equal("LD/ATCC1$SG$BndWid$setMag", root.FallbackValueReference);
}

[Fact]
public void EditOnlyDeclarationIsNotTreatedAsActiveSettingValue()
{
var (model, page) = Fixture(Attr("SE", "setMag.f"));
var rows = IedBrowserPresentation.Details(page, model, true);
Assert.Empty(rows.Where(r => r.Kind == "Data"));

Check warning on line 98 in tests/ARSAS.Tests/IedBrowserActiveSettingProjectionTests.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Do not use Assert.Empty to check if a value does not exist in a collection. Use Assert.DoesNotContain instead. (https://xunit.net/xunit.analyzers/rules/xUnit2029)
Assert.Contains(rows, r => r.Kind == "Message" &&
r.Name.Contains("No active-group [SG]", StringComparison.Ordinal));
}
}
21 changes: 10 additions & 11 deletions tests/ARSAS.Tests/IedBrowserPresentationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
Name = "Unbuffer01"
};
var roots = IedBrowserPresentation.Roots(model, [rcb]);
var modelRoot = Assert.Single(roots.Where(x => x.Key == "category/model"));

Check warning on line 121 in tests/ARSAS.Tests/IedBrowserPresentationTests.cs

View workflow job for this annotation

GitHub Actions / Build, test, validate and package Windows application

Do not use a Where clause to filter before calling Assert.Single. Use the overload of Assert.Single that accepts a filtering function. (https://xunit.net/xunit.analyzers/rules/xUnit2031)
Assert.Equal(["ADD", "Application"],
modelRoot.GetChildren().Select(x => x.Title).ToArray());
Assert.Equal("model/ld/" + ied + "Application",
Expand Down Expand Up @@ -401,11 +401,10 @@
new IedBrowserTreeRow("sg", "SGCB", "SGCB", 3, sg), model, true);
Assert.Contains(sgRows, r => r.Kind == "Section" && r.Name == "Settings");
var settingRoot = Assert.Single(sgRows.Where(r => r.Kind == "Data" &&
r.Name == "ATCC1.BndWid"));
r.Name == "ATCC1.BndWid.setMag [SG]"));
Assert.Equal("Not read", settingRoot.Value);
var settingLeaf = Assert.Single(settingRoot.GetChildren());
Assert.Equal("setMag [SG]", settingLeaf.Name);
Assert.Equal("LD/ATCC1$SG$BndWid$setMag", settingLeaf.ValueReference);
Assert.Equal("LD/ATCC1$SG$BndWid$setMag", settingRoot.ValueReference);
Assert.Equal("SG", settingRoot.FunctionalConstraint);
var goose = new LiveIedControlBlockModel { Kind = "GOOSE", Domain = "LD",
Reference = "LD/LLN0.GO.cb1", DataSetReference = "LD/LLN0.Signals" };
var gRows = IedBrowserPresentation.Details(
Expand Down Expand Up @@ -794,16 +793,16 @@
var node = new IedBrowserTreeRow("s", "SGCB", "SGCB", 3,
new LiveIedControlBlockModel { Reference = "LD/LLN0.SGCB", Domain = "LD" });
var targets = IedBrowserReadPlan.Build(model, node, readAll: false);
Assert.Equal(2, targets.Count);
Assert.Single(targets);
Assert.Contains(targets, v => v.Reference == sg.MmsReference && v.FunctionalConstraint == "SG");
Assert.Contains(targets, v => v.Reference == se.MmsReference && v.FunctionalConstraint == "SE");
Assert.DoesNotContain(targets, v => v.Reference == foreign.MmsReference || v.Reference == mx.MmsReference);
Assert.DoesNotContain(targets, v => v.Reference == se.MmsReference ||
v.Reference == foreign.MmsReference || v.Reference == mx.MmsReference);
var rows = IedBrowserPresentation.Details(node, model, connected: true);
var root = Assert.Single(rows.Where(v => v.Kind == "Data"));
Assert.Equal("ATCC1.BndWid", root.Name);
Assert.Equal("", root.ValueReference);
var sgGroup = Assert.Single(root.GetChildren().Where(v => v.Name == "setMag [SG]"));
var leaf = Assert.Single(sgGroup.GetChildren());
Assert.Equal("ATCC1.BndWid.setMag [SG]", root.Name);
Assert.Equal(IedBrowserLiveValues.CanonicalReference(sg.MmsReference),
IedBrowserLiveValues.CanonicalReference(root.ValueReference));
var leaf = Assert.Single(root.GetChildren());
Assert.Equal("f [SG]", leaf.Name);
Assert.Equal(sg.MmsReference, leaf.ValueReference);
}
Expand Down
Loading