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
36 changes: 23 additions & 13 deletions MainWindow.FirstRunLauncherRepair.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,38 @@ private static void FirstRunLauncherRepair_Loaded(object sender, RoutedEventArgs
if (sender is not MainWindow window)
return;

// MainWindow.IoTesting queues its normal launcher at Loaded priority. Run after
// that attempt so this path is only a compatibility repair, never a replacement.
// MainWindow.IoTesting queued InstallFirstRunTestingChoices at Loaded priority
// during OnInitialized. Remove the decorative Border synchronously while handling
// Loaded, before that queued callback can execute SingleOrDefault over hero Borders.
// This prevents the startup exception instead of merely repairing the launcher
// after the exception was swallowed by the dispatcher error handler.
window.RemoveFirstRunHeroTintBeforeLauncherDiscovery();

// Run the compatibility audit after the normal launcher attempt. At this point it
// only restores actions/Z-order or retries if some unrelated condition prevented
// the normal launcher from materializing.
window.Dispatcher.BeginInvoke(
new Action(window.RestoreFirstRunLauncherContract),
DispatcherPriority.ContextIdle);
}

private void RemoveFirstRunHeroTintBeforeLauncherDiscovery()
{
var heroGrid = FindFirstRunHeroGrid();
var tint = heroGrid?.Children
.OfType<Border>()
.FirstOrDefault(border => Equals(border.Tag, "P2IndustrialHeroTint"));
if (heroGrid != null && tint != null)
heroGrid.Children.Remove(tint);
}

private void RestoreFirstRunLauncherContract()
{
if (_ioListTestingLauncherCard == null)
{
var heroGrid = FindFirstRunHeroGrid();
var tint = heroGrid?.Children
.OfType<Border>()
.FirstOrDefault(border => Equals(border.Tag, "P2IndustrialHeroTint"));

// P2.1 originally inserted its tint as a Border. The legacy launcher
// intentionally looked for the single Border that represented the general
// testing card, so the decorative Border made SingleOrDefault fail.
if (heroGrid != null && tint != null)
heroGrid.Children.Remove(tint);

// Defensive retry: remove a tint that may have been re-applied between Loaded
// and ContextIdle, then invoke the legacy launcher against one operational card.
RemoveFirstRunHeroTintBeforeLauncherDiscovery();
InstallFirstRunTestingChoices();

if (_ioListTestingLauncherCard == null)
Expand Down
30 changes: 21 additions & 9 deletions SignalSelectionWizardWindow.DataSetAuthority.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,47 @@ namespace ArIED61850Tester;
public partial class SignalSelectionWizardWindow
{
protected override void OnInitialized(EventArgs e)
{
RestoreAuthoritativeDataSetInventory();
base.OnInitialized(e);
}

protected override void OnContentRendered(EventArgs e)
{
// The legacy selector constructor prepares display text before WPF finishes
// initialization. Run authority restoration again after construction so no
// runtime primary leaf can survive as a replacement for a static FCDA/FCD
// identity. The merge is idempotent and does not alter user selection.
RestoreAuthoritativeDataSetInventory();
SignalsView.Refresh();
RefreshViewState();
base.OnContentRendered(e);
}

private void RestoreAuthoritativeDataSetInventory()
{
var merge = Iec61850DataSetSignalInventoryService.EnsureMandatorySignals(_device);
foreach (var signal in merge.AddedSignals)
{
// DisplayReference is the engine-authoritative static FCDA/FCD identity.
// Do not rewrite it from ObjectReference: ObjectReference may point to the
// resolved runtime leaf (for example .stVal) while Signal Selection must
// continue to show the exact DataSet member.
// ObjectReference may point to a resolved runtime leaf such as .stVal.
if (string.IsNullOrWhiteSpace(signal.DisplayReference))
{
signal.DisplayReference = Iec61850MonitorPoint.StripIedNamePrefix(
signal.ObjectReference,
_device.Name);
}

signal.PropertyChanged -= Signal_PropertyChanged;
signal.PropertyChanged += Signal_PropertyChanged;
}

// Window initialization can run inside InitializeComponent(), before the caller's
// object initializer assigns Owner. Register recovered rows through the actual
// application MainWindow so they receive the same owner/property-change lifecycle
// as rows produced by the normal discovery pipeline.
if (Application.Current?.MainWindow is MainWindow mainWindow)
mainWindow.RegisterRecoveredDataSetSignals(_device, merge);
else
{
_device.RecountSelectedSignals();
_device.RefreshComputed();
}

base.OnInitialized(e);
}
}
6 changes: 3 additions & 3 deletions engines/ARIEC61850.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"schemaVersion": 1,
"repository": "masarray/ARIEC61850",
"ref": "main",
"commit": "4fc3119be2bb97e0ea3a8d31fb990e6a1a089e60",
"sourcePullRequest": 77,
"purpose": "Pins the ARIEC61850 engine used by ARSAS. PR #76 preserves every mandatory static DataSet member, including unresolved object-level FCD members without inventing .stVal. PR #77 canonicalizes SCL DataSet member identity across logical devices so parser display references such as IED/LD/LN.DO [FC] resolve to the same IEDLD/LN.DO identity used by the live model."
"commit": "761fa0df9ca84fbe15352d2670f03fddbe8784ba",
"sourcePullRequest": 78,
"purpose": "Pins the ARIEC61850 engine used by ARSAS. PR #76 preserves unresolved static DataSet members, PR #77 canonicalizes cross-logical-device SCL references, and PR #78 makes mandatory Signal Selection inventory protocol-member-centric: exactly one descriptor per static DataSet member while keeping resolved runtime primary leaves separate from the original FCDA/FCD identity."
}
15 changes: 15 additions & 0 deletions tests/ARSAS.Tests/FirstRunLauncherRegressionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ namespace ARSAS.Tests;

public sealed class FirstRunLauncherRegressionTests
{
[Fact]
public void FirstRunLauncher_RemovesDecorativeBorderSynchronouslyBeforeQueuedDiscovery()
{
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);

Assert.True(loadedHandler >= 0);
Assert.True(removeBeforeQueue > loadedHandler);
Assert.True(queueContextIdle > removeBeforeQueue);
Assert.Contains("before that queued callback can execute SingleOrDefault", source, StringComparison.Ordinal);
}

[Fact]
public void FirstRunLauncher_RepairsP2OverlayBeforeRebuildingOperationalCards()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,30 @@ public void SignalSelectionRecovery_DoesNotOverwriteStaticFcdaDisplayIdentity()
StringComparison.Ordinal);
}

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

Assert.Contains("protected override void OnContentRendered(EventArgs e)", source, StringComparison.Ordinal);
Assert.Contains("RestoreAuthoritativeDataSetInventory();", source, StringComparison.Ordinal);
Assert.Contains("SignalsView.Refresh();", source, StringComparison.Ordinal);
Assert.Contains(
"runtime primary leaf can survive as a replacement for a static FCDA/FCD",
source,
StringComparison.Ordinal);
}

[Fact]
public void EngineLock_PinsMemberCentricStaticDataSetInventory()
{
var source = File.ReadAllText(FindRepoFile("engines/ARIEC61850.lock.json"));

Assert.Contains("761fa0df9ca84fbe15352d2670f03fddbe8784ba", source, StringComparison.OrdinalIgnoreCase);
Assert.Contains("\"sourcePullRequest\": 78", source, StringComparison.Ordinal);
Assert.Contains("one descriptor per static DataSet member", source, StringComparison.OrdinalIgnoreCase);
}

[Fact]
public void CompletenessReport_SamplesEveryFailingDataSetAndSeparatesSemanticDescriptors()
{
Expand Down
Loading