Skip to content
Merged
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
14 changes: 14 additions & 0 deletions ArIED61850Tester.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
<ArIec61850Project Condition="'$(ArIec61850Project)' == ''">..\ARIEC61850\src\AR.Iec61850\AR.Iec61850.csproj</ArIec61850Project>
<ArIec61850NpcapProject Condition="'$(ArIec61850NpcapProject)' == '' and '$(ARIEC61850_NPCAP_PROJECT)' != ''">$(ARIEC61850_NPCAP_PROJECT)</ArIec61850NpcapProject>
<ArIec61850NpcapProject Condition="'$(ArIec61850NpcapProject)' == ''">..\ARIEC61850\src\AR.Iec61850.Transports.Npcap\AR.Iec61850.Transports.Npcap.csproj</ArIec61850NpcapProject>
<ArIec61850LockPath>$(MSBuildProjectDirectory)\engines\ARIEC61850.lock.json</ArIec61850LockPath>
<ArIec61850LockValidator>$(MSBuildProjectDirectory)\scripts\validate-ariec61850-lock.ps1</ArIec61850LockValidator>
<ArIec61850PowerShell Condition="'$(OS)' == 'Windows_NT'">powershell</ArIec61850PowerShell>
<ArIec61850PowerShell Condition="'$(OS)' != 'Windows_NT'">pwsh</ArIec61850PowerShell>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -68,5 +72,15 @@
Text="ARIEC61850 engine project was not found at '$(ArIec61850Project)'. Place this folder beside the ARIEC61850 repository, or build with -p:ArIec61850Project=FULL_PATH_TO_AR.Iec61850.csproj." />
<Error Condition="!Exists('$(ArIec61850NpcapProject)')"
Text="ARIEC61850 Npcap transport project was not found at '$(ArIec61850NpcapProject)'. Place this folder beside the ARIEC61850 repository, or build with -p:ArIec61850NpcapProject=FULL_PATH_TO_AR.Iec61850.Transports.Npcap.csproj." />
<Error Condition="!Exists('$(ArIec61850LockValidator)')"
Text="ARIEC61850 lock validator was not found at '$(ArIec61850LockValidator)'." />
<Error Condition="!Exists('$(ArIec61850LockPath)')"
Text="ARIEC61850 integration lock was not found at '$(ArIec61850LockPath)'." />
</Target>

<Target Name="ValidateArIec61850Revision"
BeforeTargets="ResolveProjectReferences"
DependsOnTargets="ValidateArIec61850Project">
<Exec Command="$(ArIec61850PowerShell) -NoProfile -ExecutionPolicy Bypass -File &quot;$(ArIec61850LockValidator)&quot; -LockPath &quot;$(ArIec61850LockPath)&quot; -EngineProject &quot;$(ArIec61850Project)&quot; -ProjectRoot &quot;$(MSBuildProjectDirectory)&quot;" />
</Target>
</Project>
9 changes: 8 additions & 1 deletion MainWindow.IoTesting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,14 @@ private void InstallFirstRunTestingChoices()
if (emptyState?.Child is not Grid heroGrid)
return;

var generalTestingCard = heroGrid.Children.OfType<Border>().SingleOrDefault();
// The hero can contain visual-only Borders (for example P2IndustrialHeroTint).
// Launcher discovery must identify the operational card by content contract rather
// than asserting that the visual tree contains exactly one Border.
var generalTestingCard = heroGrid.Children
.OfType<Border>()
.FirstOrDefault(border =>
!Equals(border.Tag, "P2IndustrialHeroTint") &&
border.Child is StackPanel);
if (generalTestingCard?.Child is not StackPanel generalContent)
return;

Expand Down
8 changes: 7 additions & 1 deletion SignalSelectionWizardWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,13 @@ public SignalSelectionWizardWindow(Iec61850MonitorDevice device, int restoredSel

foreach (var signal in device.Signals)
{
signal.DisplayReference = Iec61850MonitorPoint.StripIedNamePrefix(signal.ObjectReference, device.Name);
// Static DataSet inventory owns DisplayReference. Only legacy/non-DataSet rows
// that have no display identity may fall back to a shortened runtime reference.
if (string.IsNullOrWhiteSpace(signal.DisplayReference))
{
signal.DisplayReference =
Iec61850MonitorPoint.StripIedNamePrefix(signal.ObjectReference, device.Name);
}
signal.PropertyChanged += Signal_PropertyChanged;
}

Expand Down
79 changes: 79 additions & 0 deletions scripts/validate-ariec61850-lock.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
param(
[Parameter(Mandatory = $true)]
[string]$LockPath,

[Parameter(Mandatory = $true)]
[string]$EngineProject,

[Parameter(Mandatory = $true)]
[string]$ProjectRoot
)

$ErrorActionPreference = 'Stop'

function Resolve-FullPath {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$BasePath
)

if ([System.IO.Path]::IsPathRooted($Path)) {
return [System.IO.Path]::GetFullPath($Path)
}

return [System.IO.Path]::GetFullPath((Join-Path $BasePath $Path))
}

$projectRootFull = Resolve-FullPath -Path $ProjectRoot -BasePath (Get-Location).Path
$lockFull = Resolve-FullPath -Path $LockPath -BasePath $projectRootFull
$engineProjectFull = Resolve-FullPath -Path $EngineProject -BasePath $projectRootFull

if (-not (Test-Path -LiteralPath $lockFull -PathType Leaf)) {
throw "ARIEC61850 integration lock was not found at '$lockFull'."
}

if (-not (Test-Path -LiteralPath $engineProjectFull -PathType Leaf)) {
throw "ARIEC61850 engine project was not found at '$engineProjectFull'."
}

$lock = Get-Content -LiteralPath $lockFull -Raw | ConvertFrom-Json
$expected = ([string]$lock.commit).Trim().ToLowerInvariant()
if ([string]::IsNullOrWhiteSpace($expected) -or $expected.Length -lt 7) {
throw "ARIEC61850 integration lock '$lockFull' does not contain a valid commit SHA."
}

$engineProjectDirectory = Split-Path -Parent $engineProjectFull
$repoRoot = (& git -C $engineProjectDirectory rev-parse --show-toplevel 2>$null)
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoRoot)) {
throw "ARIEC61850 project '$engineProjectFull' is not inside a readable Git checkout; ARSAS cannot prove which engine source is being compiled."
}
$repoRoot = $repoRoot.Trim()

$actual = (& git -C $repoRoot rev-parse HEAD 2>$null)
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($actual)) {
throw "ARSAS could not read the ARIEC61850 Git revision from '$repoRoot'."
}
$actual = $actual.Trim().ToLowerInvariant()

if ($actual -ne $expected) {
$message = @"
ARSAS refused to compile against an unpinned ARIEC61850 engine.
Expected by: $lockFull
Expected SHA: $expected
Actual SHA : $actual
Engine repo : $repoRoot

Synchronize the engine checkout first, for example:
git -C "$repoRoot" fetch origin
git -C "$repoRoot" switch main
git -C "$repoRoot" pull --ff-only
git -C "$repoRoot" rev-parse HEAD

If main intentionally moved beyond the ARSAS lock, update engines/ARIEC61850.lock.json through a reviewed ARSAS change instead of compiling an arbitrary engine revision.
"@
throw $message
}

Write-Host "ARIEC61850 lock verified: $actual"
54 changes: 54 additions & 0 deletions tests/ARSAS.Tests/ArIec61850PinRegressionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Reflection;
using System.Text.Json;
using AR.Iec61850.Discovery;

namespace ARSAS.Tests;

public sealed class ArIec61850PinRegressionTests
{
[Fact]
public void LoadedEngineAssembly_MatchesArsasIntegrationLock()
{
var lockPath = FindRepoFile("engines/ARIEC61850.lock.json");
using var document = JsonDocument.Parse(File.ReadAllText(lockPath));
var expected = document.RootElement.GetProperty("commit").GetString();

Assert.False(string.IsNullOrWhiteSpace(expected));

var informationalVersion = typeof(Iec61850DataSetSignalInventoryProjection)
.Assembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
.InformationalVersion ?? string.Empty;

Assert.Contains(expected!, informationalVersion, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void ApplicationBuild_ValidatesSiblingEngineGitHeadBeforeProjectReferences()
{
var project = File.ReadAllText(FindRepoFile("ArIED61850Tester.csproj"));
var validator = File.ReadAllText(FindRepoFile("scripts/validate-ariec61850-lock.ps1"));

Assert.Contains("ValidateArIec61850Revision", project, StringComparison.Ordinal);
Assert.Contains("BeforeTargets=\"ResolveProjectReferences\"", project, StringComparison.Ordinal);
Assert.Contains("validate-ariec61850-lock.ps1", project, StringComparison.OrdinalIgnoreCase);
Assert.Contains("rev-parse HEAD", validator, StringComparison.Ordinal);
Assert.Contains("$actual -ne $expected", validator, StringComparison.Ordinal);
Assert.Contains("refused to compile against an unpinned ARIEC61850 engine", validator, StringComparison.Ordinal);
}

private static string FindRepoFile(string relativePath)
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory != null)
{
var candidate = Path.Combine(directory.FullName, relativePath);
if (File.Exists(candidate))
return candidate;
directory = directory.Parent;
}

throw new FileNotFoundException(
$"Could not locate repository file '{relativePath}' from '{AppContext.BaseDirectory}'.");
}
}
16 changes: 6 additions & 10 deletions tests/ARSAS.Tests/FirstRunLauncherRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@ namespace ARSAS.Tests;
public sealed class FirstRunLauncherRegressionTests
{
[Fact]
public void FirstRunLauncher_RemovesDecorativeBorderSynchronouslyBeforeQueuedDiscovery()
public void FirstRunLauncher_SelectsOperationalCardWithoutAssumingSingleBorder()
{
var source = File.ReadAllText(FindRepoFile("MainWindow.FirstRunLauncherRepair.cs"));

var loadedHandler = source.IndexOf("private static void FirstRunLauncherRepair_Loaded", StringComparison.Ordinal);
var removeBeforeQueue = source.IndexOf("window.RemoveFirstRunHeroTintBeforeLauncherDiscovery();", loadedHandler, StringComparison.Ordinal);
var queueContextIdle = source.IndexOf("window.Dispatcher.BeginInvoke", loadedHandler, StringComparison.Ordinal);
var source = File.ReadAllText(FindRepoFile("MainWindow.IoTesting.cs"));

Assert.True(loadedHandler >= 0);
Assert.True(removeBeforeQueue > loadedHandler);
Assert.True(queueContextIdle > removeBeforeQueue);
Assert.Contains("before that queued callback can execute SingleOrDefault", source, StringComparison.Ordinal);
Assert.DoesNotContain("heroGrid.Children.OfType<Border>().SingleOrDefault()", source, StringComparison.Ordinal);
Assert.Contains("P2IndustrialHeroTint", source, StringComparison.Ordinal);
Assert.Contains("border.Child is StackPanel", source, StringComparison.Ordinal);
Assert.Contains("FirstOrDefault(border =>", source, StringComparison.Ordinal);
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,25 @@ public void DeviceInventoryMerge_FallsBackToOfflineSclDesignModel()
StringComparison.Ordinal);
}

[Fact]
public void SignalSelectionConstructor_PreservesExistingStaticFcdaDisplayIdentity()
{
var source = File.ReadAllText(FindRepoFile("SignalSelectionWizardWindow.xaml.cs"));

Assert.Contains(
"if (string.IsNullOrWhiteSpace(signal.DisplayReference))",
source,
StringComparison.Ordinal);
Assert.DoesNotContain(
"signal.DisplayReference = Iec61850MonitorPoint.StripIedNamePrefix(signal.ObjectReference, device.Name);\n signal.PropertyChanged",
source,
StringComparison.Ordinal);
Assert.Contains(
"Static DataSet inventory owns DisplayReference",
source,
StringComparison.Ordinal);
}

[Fact]
public void SignalSelectionRecovery_DoesNotOverwriteStaticFcdaDisplayIdentity()
{
Expand Down
146 changes: 146 additions & 0 deletions tests/ARSAS.Tests/SiemensStaticDataSetSelectionRegressionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System.Xml.Linq;
using AR.Iec61850.Scl.Engineering;
using ArIED61850Tester.Models;
using ArIED61850Tester.Services;

namespace ARSAS.Tests;

public sealed class SiemensStaticDataSetSelectionRegressionTests
{
[Fact]
public void SiemensLike_58Member_CrossLd_StaticDataSets_AreRepresented58Of58()
{
var model = SclLiveModelProjectionBuilder.Build(BuildFixture(), "Siprotec_58_member.cid");
var signals = new List<SignalDefinition>();

var merge = Iec61850DataSetSignalInventoryService.EnsureMandatorySignals(signals, model);
var snapshot = Iec61850DataSetCompletenessDiagnostic.Evaluate(model, signals);

Assert.Equal(2, model.DataSets.Count);
Assert.Equal(58, model.DataSets.Sum(dataSet => dataSet.Members.Count));
Assert.Equal(58, merge.MandatoryCatalogCount);
Assert.Equal(58, signals.Count);
Assert.Equal(58, snapshot.StaticMemberCount);
Assert.Equal(58, snapshot.MandatoryInventoryCount);
Assert.Equal(58, snapshot.RepresentedCount);
Assert.Equal(0, snapshot.MissingCount);
Assert.True(snapshot.IsComplete);

Assert.Contains(signals, signal => signal.DisplayReference == "AA1C1F13R4ADD/GGIO1.Dig01");
Assert.Contains(signals, signal => signal.DisplayReference == "AA1C1F13R4MEAS/MMXU1.Ana22");
Assert.All(signals, signal =>
{
Assert.DoesNotContain("[ST]", signal.DisplayReference, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("[MX]", signal.DisplayReference, StringComparison.OrdinalIgnoreCase);
});
}

private static XDocument BuildFixture()
{
XNamespace ns = "http://www.iec.ch/61850/2003/SCL";

var digitalDataSet = new XElement(ns + "DataSet", new XAttribute("name", "Digital"));
foreach (var index in Enumerable.Range(1, 36))
{
digitalDataSet.Add(new XElement(ns + "FCDA",
new XAttribute("ldInst", "ADD"),
new XAttribute("lnClass", "GGIO"),
new XAttribute("lnInst", "1"),
new XAttribute("doName", $"Dig{index:00}"),
new XAttribute("fc", "ST")));
}

var analogDataSet = new XElement(ns + "DataSet", new XAttribute("name", "Analog"));
foreach (var index in Enumerable.Range(1, 22))
{
analogDataSet.Add(new XElement(ns + "FCDA",
new XAttribute("ldInst", "MEAS"),
new XAttribute("lnClass", "MMXU"),
new XAttribute("lnInst", "1"),
new XAttribute("doName", $"Ana{index:00}"),
new XAttribute("fc", "MX")));
}

var ggioType = new XElement(ns + "LNodeType",
new XAttribute("id", "GGIOType"),
new XAttribute("lnClass", "GGIO"));
foreach (var index in Enumerable.Range(1, 36))
{
ggioType.Add(new XElement(ns + "DO",
new XAttribute("name", $"Dig{index:00}"),
new XAttribute("type", "SpsType")));
}

var mmxuType = new XElement(ns + "LNodeType",
new XAttribute("id", "MMXUType"),
new XAttribute("lnClass", "MMXU"));
foreach (var index in Enumerable.Range(1, 22))
{
mmxuType.Add(new XElement(ns + "DO",
new XAttribute("name", $"Ana{index:00}"),
new XAttribute("type", "MvType")));
}

return new XDocument(
new XElement(ns + "SCL",
new XAttribute("version", "2007"),
new XAttribute("revision", "B"),
new XElement(ns + "Header", new XAttribute("id", "SIEMENS_58")),
new XElement(ns + "IED",
new XAttribute("name", "AA1C1F13R4"),
new XElement(ns + "AccessPoint",
new XAttribute("name", "E"),
new XElement(ns + "Server",
new XElement(ns + "LDevice",
new XAttribute("inst", "Application"),
new XElement(ns + "LN0",
new XAttribute("lnClass", "LLN0"),
new XAttribute("lnType", "LLN0Type"),
analogDataSet,
digitalDataSet)),
new XElement(ns + "LDevice",
new XAttribute("inst", "ADD"),
new XElement(ns + "LN0",
new XAttribute("lnClass", "LLN0"),
new XAttribute("lnType", "LLN0Type")),
new XElement(ns + "LN",
new XAttribute("lnClass", "GGIO"),
new XAttribute("inst", "1"),
new XAttribute("lnType", "GGIOType"))),
new XElement(ns + "LDevice",
new XAttribute("inst", "MEAS"),
new XElement(ns + "LN0",
new XAttribute("lnClass", "LLN0"),
new XAttribute("lnType", "LLN0Type")),
new XElement(ns + "LN",
new XAttribute("lnClass", "MMXU"),
new XAttribute("inst", "1"),
new XAttribute("lnType", "MMXUType")))))),
new XElement(ns + "DataTypeTemplates",
new XElement(ns + "LNodeType",
new XAttribute("id", "LLN0Type"),
new XAttribute("lnClass", "LLN0")),
ggioType,
mmxuType,
new XElement(ns + "DOType",
new XAttribute("id", "SpsType"),
new XAttribute("cdc", "SPS"),
new XElement(ns + "DA",
new XAttribute("name", "stVal"),
new XAttribute("bType", "BOOLEAN"),
new XAttribute("fc", "ST"))),
new XElement(ns + "DOType",
new XAttribute("id", "MvType"),
new XAttribute("cdc", "MV"),
new XElement(ns + "DA",
new XAttribute("name", "mag"),
new XAttribute("bType", "Struct"),
new XAttribute("type", "AnalogueValue"),
new XAttribute("fc", "MX"))),
new XElement(ns + "DAType",
new XAttribute("id", "AnalogueValue"),
new XElement(ns + "BDA",
new XAttribute("name", "f"),
new XAttribute("bType", "FLOAT32"))))));
}
}
Loading