diff --git a/MainWindow.GooseSubscriber.cs b/MainWindow.GooseSubscriber.cs
index ccab206d3..0c78504a8 100644
--- a/MainWindow.GooseSubscriber.cs
+++ b/MainWindow.GooseSubscriber.cs
@@ -31,6 +31,7 @@ public partial class MainWindow
private string _gooseStatusText = "Select a network adapter, then start the read-only GOOSE subscriber.";
private string _gooseBindingText = "No GOOSE model evaluated yet.";
private string _gooseCaptureFilter = GooseSubscriberRuntime.DefaultCaptureFilter;
+ private string _gooseCaptureContextHint = "If no frames arrive, verify publisher, adapter and VLAN.";
private long _gooseCapturedFrames;
private long _gooseFrames;
private long _gooseOtherFrames;
@@ -189,7 +190,7 @@ await _gooseSubscriberRuntime.StartAsync(
GooseCaptureFilter,
_applicationCancellation.Token);
IsGooseCapturing = true;
- GooseStatusText = $"Listening on {SelectedGooseAdapter.DisplayText}. Waiting for GOOSE frames…";
+ GooseStatusText = $"Waiting for actual GOOSE frames on {SelectedGooseAdapter.DisplayText}. {_gooseCaptureContextHint}";
SetStatus("GOOSE Subscriber started in read-only capture mode.");
AddLog("INFO", "GOOSE", $"Subscriber started on adapter {SelectedGooseAdapter.Index}: {SelectedGooseAdapter.Description}. Binding: {_gooseBindingCatalog.Summary}");
}
@@ -275,12 +276,10 @@ private void RefreshGooseAdapters()
var previousName = SelectedGooseAdapter?.Name;
var adapters = _gooseSubscriberRuntime.ListAdapters();
GooseAdapters.ReplaceAll(adapters);
- var usableAdapters = adapters.Where(adapter => !LooksLikeLoopback(adapter)).ToArray();
- // Never silently choose the first of several network interfaces: an
- // unrelated LAN/Wi-Fi adapter can miss all IEC 61850 GOOSE frames.
+ // Preserve only an explicitly selected capture-device identity.
+ // Never silently choose the sole unrelated NIC (especially on same-host simulators).
SelectedGooseAdapter = adapters.FirstOrDefault(adapter =>
- adapter.Name.Equals(previousName, StringComparison.OrdinalIgnoreCase))
- ?? (usableAdapters.Length == 1 ? usableAdapters[0] : null);
+ previousName is not null && adapter.Name.Equals(previousName, StringComparison.OrdinalIgnoreCase));
GooseStatusText = adapters.Count == 0
? "No Npcap/WinPcap adapters found. Install Npcap, then refresh adapters."
: $"{adapters.Count:N0} capture adapter(s) available. Choose the station Ethernet adapter and press Start.";
diff --git a/MainWindow.IedGooseQuickStart.cs b/MainWindow.IedGooseQuickStart.cs
index 38a793eb2..02270805a 100644
--- a/MainWindow.IedGooseQuickStart.cs
+++ b/MainWindow.IedGooseQuickStart.cs
@@ -5,6 +5,7 @@
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Text.RegularExpressions;
+using ArIED61850Tester.Services;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
@@ -124,23 +125,27 @@ internal void OpenIedGooseSubscriber(Iec61850MonitorDevice device)
if (GooseAdapters.Count == 0)
RefreshGooseAdapters();
- // MMS route is an initial NIC recommendation, not proof of the
- // station GOOSE VLAN. Only auto-start when one non-loopback Npcap
- // adapter uniquely matches the actual Windows-routed interface.
+ // Exact OS/Npcap interface identity selects the candidate, not an
+ // adapter's display name or the presence of a single remaining NIC.
+ // The selected NIC is an observation point, NOT proof of GOOSE emission.
var proposed = ResolveGooseAdapterForIed(device, out var route);
- if (proposed is not null && !LooksLikeLoopback(proposed))
+ _gooseCaptureContextHint = route.Contains("Same-PC", StringComparison.Ordinal)
+ ? "Same-PC simulator: verify it actually publishes Ethernet GOOSE on this adapter."
+ : "If no frames arrive, check the publisher, station NIC and VLAN.";
+ AddLog("INFO", "GOOSE", $"Adapter selection: {route}");
+ if (proposed is not null)
{
SelectedGooseAdapter = proposed;
- GooseStatusText = $"Listening on suggested adapter {proposed.DisplayText}. Change adapter if GOOSE uses a separate port/VLAN.";
- SetStatus($"GOOSE: starting on {proposed.DisplayText}.");
+ GooseStatusText = $"Matched capture NIC {proposed.DisplayText}. GOOSE frame reception not yet verified.";
+ SetStatus($"GOOSE: observing on {proposed.DisplayText}.");
StartGooseSubscriber_Click(this, new RoutedEventArgs());
return;
}
SelectedGooseAdapter = null;
_pendingIedGooseAutoStart = device;
GooseStatusText = GooseAdapters.Count == 0
- ? "No Ethernet capture adapter found. Check Npcap and refresh adapters."
- : $"Choose the GOOSE station adapter for {device.Name}; capture begins on selection. {route}";
+ ? "No Npcap capture adapter found. Check installation and refresh."
+ : $"Choose GOOSE adapter for {device.Name}; capture starts on confirmation. {route}";
SetStatus($"GOOSE: select network adapter for {device.Name}.");
}));
}
@@ -157,46 +162,29 @@ internal void OpenIedGooseSubscriber(Iec61850MonitorDevice device)
return null;
}
- var localAddress = ResolveLocalIpv4ForTarget(target);
- if (localAddress is not null)
+ try
{
- var networkInterface = NetworkInterface.GetAllNetworkInterfaces()
- .FirstOrDefault(adapter => adapter.GetIPProperties().UnicastAddresses.Any(unicast =>
- unicast.Address.AddressFamily == AddressFamily.InterNetwork &&
- unicast.Address.Equals(localAddress)));
-
- if (networkInterface is not null)
- {
- var matches = GooseAdapters
- .Where(adapter => CaptureAdapterMatchesNetworkInterface(adapter, networkInterface))
- .ToList();
- if (matches.Count == 1)
- {
- routeDetail =
- $"Windows route {localAddress} → {target} via {networkInterface.Name} ({networkInterface.Description})";
- return matches[0];
- }
-
- routeDetail = matches.Count == 0
- ? $"Windows selected {networkInterface.Name} ({localAddress}), but no Npcap adapter matched its ID/MAC."
- : $"Windows selected {networkInterface.Name} ({localAddress}), but {matches.Count} Npcap adapters matched.";
- }
- else
- {
- routeDetail = $"Windows selected local address {localAddress}, but its network interface was not found.";
- }
+ var route = ResolveLocalIpv4ForTarget(target);
+ var interfaces = NetworkInterface.GetAllNetworkInterfaces()
+ .Select(nic => new GooseWindowsInterfaceSnapshot(
+ nic.Id,
+ nic.Name,
+ nic.Description,
+ nic.GetPhysicalAddress().ToString(),
+ nic.GetIPProperties().UnicastAddresses
+ .Where(unicast => unicast.Address.AddressFamily == AddressFamily.InterNetwork)
+ .Select(unicast => unicast.Address)
+ .ToArray()))
+ .ToArray();
+ var selection = GooseAdapterSelectionPolicy.Resolve(target, route, interfaces, GooseAdapters.ToArray());
+ routeDetail = selection.Evidence;
+ return selection.Adapter;
}
-
- var usable = GooseAdapters
- .Where(adapter => !LooksLikeLoopback(adapter))
- .ToList();
- if (usable.Count == 1)
+ catch (Exception ex) when (ex is NetworkInformationException or SocketException or InvalidOperationException)
{
- routeDetail += $" Falling back to the only non-loopback capture adapter: {usable[0].DisplayText}.";
- return usable[0];
+ routeDetail = $"Network adapter identity unavailable: {ex.Message}. Select the capture adapter manually.";
+ return null;
}
-
- return null;
}
private static IPAddress? ResolveLocalIpv4ForTarget(IPAddress target)
@@ -236,9 +224,5 @@ private static string NormalizeGooseAdapterMac(string? value)
=> Regex.Replace(value ?? string.Empty, "[^0-9A-Fa-f]", string.Empty).ToUpperInvariant();
private static bool LooksLikeLoopback(GooseAdapterOption adapter)
- {
- var text = $"{adapter.Name} {adapter.Description} {adapter.FriendlyName}";
- return text.Contains("loopback", StringComparison.OrdinalIgnoreCase) ||
- text.Contains("npcap loopback", StringComparison.OrdinalIgnoreCase);
- }
+ => GooseAdapterSelectionPolicy.IsNpcapPseudoLoopback(adapter);
}
diff --git a/Services/GooseAdapterSelectionPolicy.cs b/Services/GooseAdapterSelectionPolicy.cs
new file mode 100644
index 000000000..e2c7cbaff
--- /dev/null
+++ b/Services/GooseAdapterSelectionPolicy.cs
@@ -0,0 +1,133 @@
+using System.Net;
+using ArIED61850Tester.Models;
+
+namespace ArIED61850Tester.Services;
+
+/// A snapshot of Windows interface identity; deliberately independent of live OS APIs for replay tests.
+public sealed record GooseWindowsInterfaceSnapshot(
+ string Id,
+ string Name,
+ string Description,
+ string MacAddress,
+ IReadOnlyList Ipv4Addresses);
+
+public sealed record GooseAdapterRouteDecision(
+ GooseAdapterOption? Adapter,
+ bool IsSameHost,
+ string Evidence)
+{
+ public bool HasAuthoritativeMatch => Adapter is not null;
+}
+
+///
+/// Pure identity-based GOOSE capture candidate selection. A successful MMS/IP route
+/// is NOT evidence that an Ethernet GOOSE publisher exists on the capture interface.
+/// The calling UI may start read-only observation only on an exact, unique match.
+///
+public static class GooseAdapterSelectionPolicy
+{
+ public static GooseAdapterRouteDecision Resolve(
+ IPAddress target,
+ IPAddress? routedLocalAddress,
+ IReadOnlyList windowsInterfaces,
+ IReadOnlyList captureAdapters)
+ {
+ ArgumentNullException.ThrowIfNull(target);
+ ArgumentNullException.ThrowIfNull(windowsInterfaces);
+ ArgumentNullException.ThrowIfNull(captureAdapters);
+
+ var ownsTarget = windowsInterfaces.Where(nic => HasAddress(nic, target)).ToArray();
+ var sameHost = IPAddress.IsLoopback(target) || ownsTarget.Length != 0;
+ if (sameHost)
+ {
+ if (ownsTarget.Length != 1)
+ return new(null, true,
+ "Same-PC MMS endpoint: no unique Windows adapter owns the target IP. Select a capture adapter manually; IP loopback does not prove Ethernet GOOSE.");
+ return Match(ownsTarget[0], captureAdapters, true);
+ }
+
+ if (routedLocalAddress is null)
+ return new(null, false, "MMS route unavailable. Select the actual GOOSE station adapter; a sole NIC is not sufficient evidence.");
+
+ var routedInterfaces = windowsInterfaces.Where(nic => HasAddress(nic, routedLocalAddress)).ToArray();
+ if (routedInterfaces.Length != 1)
+ return new(null, false,
+ "Windows MMS route did not resolve to one interface. Choose the GOOSE capture adapter manually.");
+
+ return Match(routedInterfaces[0], captureAdapters, false);
+ }
+
+ /// The dedicated Npcap software pseudo-device, NOT a Windows KM-TEST virtual Ethernet NIC.
+ public static bool IsNpcapPseudoLoopback(GooseAdapterOption adapter)
+ {
+ var name = adapter.Name.Trim();
+ return name.Equals(@"\Device\NPF_Loopback", StringComparison.OrdinalIgnoreCase) ||
+ name.Equals("NPF_Loopback", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static GooseAdapterRouteDecision Match(
+ GooseWindowsInterfaceSnapshot nic,
+ IReadOnlyList adapters,
+ bool sameHost)
+ {
+ var context = sameHost ? "Same-PC MMS simulator" : "Windows MMS route";
+ var candidates = adapters.Where(adapter => !IsNpcapPseudoLoopback(adapter)).ToArray();
+ var winGuid = ParseGuid(nic.Id);
+ if (winGuid is not null)
+ {
+ var guidMatches = candidates.Where(adapter => ParseGuid(adapter.Name) == winGuid).ToArray();
+ if (guidMatches.Length == 1)
+ return new(guidMatches[0], sameHost,
+ $"{context}: exact Npcap/Windows interface GUID for {nic.Name}. GOOSE frames still unverified.");
+ if (guidMatches.Length > 1)
+ return new(null, sameHost,
+ $"{context}: multiple Npcap capture devices share the interface GUID. Choose manually.");
+ }
+
+ // MAC fallback is safe only when BOTH MACs are valid, the match is
+ // unique, and a contradictory explicit interface GUID is absent.
+ var mac = NormalizeValidMac(nic.MacAddress);
+ if (mac is not null)
+ {
+ var macMatches = candidates.Where(adapter =>
+ string.Equals(NormalizeValidMac(adapter.MacAddress), mac, StringComparison.Ordinal) &&
+ (winGuid is null || ParseGuid(adapter.Name) is null)).ToArray();
+ if (macMatches.Length == 1)
+ return new(macMatches[0], sameHost,
+ $"{context}: one non-conflicting Npcap adapter matched the Windows interface MAC for {nic.Name}. GOOSE frames still unverified.");
+ if (macMatches.Length > 1)
+ return new(null, sameHost,
+ $"{context}: several capture adapters share the MAC. Choose the GOOSE adapter manually.");
+ }
+
+ return new(null, sameHost,
+ $"{context}: no unique GUID/MAC-matched Npcap adapter for {nic.Name}. " +
+ (sameHost
+ ? "KM-TEST is eligible when Npcap exposes its real Ethernet interface, but the Npcap IP-loopback pseudo-device is not equivalent."
+ : "The IP route is only a hint; choose the actual station Ethernet adapter."));
+ }
+
+ private static bool HasAddress(GooseWindowsInterfaceSnapshot nic, IPAddress address)
+ => nic.Ipv4Addresses.Any(candidate => candidate.Equals(address));
+
+ private static Guid? ParseGuid(string? source)
+ {
+ if (string.IsNullOrWhiteSpace(source))
+ return null;
+ var first = source.IndexOf('{');
+ if (first < 0)
+ return Guid.TryParse(source, out var standalone) ? standalone : null;
+ var last = source.IndexOf('}', first + 1);
+ return last > first && Guid.TryParse(source.AsSpan(first, last - first + 1), out var guid)
+ ? guid : null;
+ }
+
+ private static string? NormalizeValidMac(string? input)
+ {
+ if (string.IsNullOrWhiteSpace(input))
+ return null;
+ var mac = new string(input.Where(Uri.IsHexDigit).Select(char.ToUpperInvariant).ToArray());
+ return mac.Length is 12 or 16 && mac.Any(ch => ch != '0') && mac.Any(ch => ch != 'F')
+ ? mac : null;
+ }
+}
diff --git a/Views/GooseSubscriberLiteView.xaml b/Views/GooseSubscriberLiteView.xaml
index b0892f12c..533925280 100644
--- a/Views/GooseSubscriberLiteView.xaml
+++ b/Views/GooseSubscriberLiteView.xaml
@@ -146,7 +146,7 @@
+ TextTrimming="CharacterEllipsis" ToolTip="{Binding GooseStatusText}"/>
diff --git a/docs/GOOSE_SUBSCRIBER.md b/docs/GOOSE_SUBSCRIBER.md
index 5e90bc357..1996f7435 100644
--- a/docs/GOOSE_SUBSCRIBER.md
+++ b/docs/GOOSE_SUBSCRIBER.md
@@ -94,3 +94,10 @@ Validate the feature with a known publisher and SCL before relying on names in p
- Vendor-specific structured DataSet members are rendered compactly as one `allData` item. The grid does not invent nested field names beyond metadata provided by ARIEC61850.
- Npcap capture visibility depends on Windows driver installation, adapter binding, VLAN offload behavior, and switch port mirroring/network topology.
- This workspace provides engineering evidence, not an IEC 61850 conformance certificate or cybersecurity assurance.
+
+
+## Same-PC C264 adapter selection (P8, draft field candidate)
+
+The IED-card GOOSE entry now resolves the capture NIC from authoritative Windows/Npcap interface GUID, then a unique non-conflicting valid MAC. For an MMS endpoint hosted on this computer, the **interface owning the target IPv4** takes precedence over the Windows socket route. A KM-TEST virtual Ethernet adapter is not rejected just because its friendly name says "loopback"; only the dedicated Npcap `NPF_Loopback` pseudo-interface is excluded from automatic Ethernet capture. If identity is missing or ambiguous, the operator selects the adapter manually; no random one-NIC fallback occurs. The manual picker still supports explicit Npcap loopback investigation.
+
+Clicking the IED-card GOOSE action may begin *read-only observation* when an exact unique NIC match exists; it is **not** evidence that a GOOSE publisher emitted a frame. The top bar explains that actual Ethernet GOOSE reception remains pending and suggests checking whether the simulator publishes and whether that adapter receives multicast/VLAN traffic. These conditions must be verified on the operator's PC before claiming capture parity. No new native transport, background adapter scan, per-packet UI timer, or control/write traffic is introduced.
diff --git a/tests/ARSAS.Tests/GooseAdapterSelectionPolicyTests.cs b/tests/ARSAS.Tests/GooseAdapterSelectionPolicyTests.cs
new file mode 100644
index 000000000..077569478
--- /dev/null
+++ b/tests/ARSAS.Tests/GooseAdapterSelectionPolicyTests.cs
@@ -0,0 +1,136 @@
+using System.Net;
+using ArIED61850Tester.Models;
+using ArIED61850Tester.Services;
+
+namespace ARSAS.Tests;
+
+public sealed class GooseAdapterSelectionPolicyTests
+{
+ private const string KmGuid = "89AAB1E2-3A41-48AC-B32F-5DD8A341BB11";
+ private const string OtherGuid = "77AC42AA-5920-421E-80E0-A19BCE61B727";
+ private static IPAddress Ip(string address) => IPAddress.Parse(address);
+
+ private static GooseWindowsInterfaceSnapshot Nic(
+ string id, string name, string mac, params string[] addresses) =>
+ new(id, name, name, mac, addresses.Select(Ip).ToArray());
+
+ private static GooseAdapterOption Capture(
+ string name, string friendly, string mac = "", int index = 0) =>
+ new() { Index = index, Name = name, FriendlyName = friendly, Description = friendly, MacAddress = mac };
+
+ [Fact]
+ public void SameHostKmTest_UsesOwnedTargetIpAndExactGuid_EvenWhenNameContainsLoopback()
+ {
+ var km = Nic(KmGuid, "KM_TEST Microsoft KM-TEST Loopback Adapter #2",
+ "02-14-20-33-44-55", "192.16.1.13", "192.16.1.33");
+ var other = Nic(OtherGuid, "Wi-Fi", "12-22-33-44-55-66", "192.168.1.4");
+ var observedKm = Capture(@"\Device\NPF_{" + KmGuid + "}", "KM_TEST Loopback Adapter", index: 3);
+ var loopback = Capture(@"\Device\NPF_Loopback", "Npcap Loopback Adapter", index: 0);
+
+ var result = GooseAdapterSelectionPolicy.Resolve(
+ Ip("192.16.1.13"), Ip("192.168.1.4"),
+ new[] { km, other }, new[] { loopback, observedKm });
+ Assert.True(result.IsSameHost);
+ Assert.Same(observedKm, result.Adapter);
+ Assert.True(result.HasAuthoritativeMatch);
+ Assert.Contains("GUID", result.Evidence);
+ Assert.Contains("unverified", result.Evidence);
+ Assert.False(GooseAdapterSelectionPolicy.IsNpcapPseudoLoopback(observedKm));
+ Assert.True(GooseAdapterSelectionPolicy.IsNpcapPseudoLoopback(loopback));
+ }
+
+ [Fact]
+ public void UnmatchedKmTest_NeverFallsBackToOnlyUnrelatedPhysicalNicOrPseudoLoopback()
+ {
+ var km = Nic(KmGuid, "KM_TEST Loopback", "02-14-20-33-44-55", "192.16.1.13");
+ var wlan = Capture(@"\Device\NPF_{" + OtherGuid + "}", "Wi-Fi", "AA-BB-CC-DD-EE-22");
+ var result = GooseAdapterSelectionPolicy.Resolve(
+ Ip("192.16.1.13"), Ip("192.16.1.13"), new[] { km },
+ new[] { wlan, Capture(@"\Device\NPF_Loopback", "Npcap Loopback Adapter") });
+ Assert.Null(result.Adapter);
+ Assert.True(result.IsSameHost);
+ Assert.Contains("no unique", result.Evidence);
+ }
+
+ [Fact]
+ public void SameHostMultipleAliasesOnOneInterface_RemainsUnambiguousButDuplicateOwnersFailClosed()
+ {
+ var primary = Nic(KmGuid, "KM_TEST", "02-14-20-33-44-55", "192.16.1.13", "192.16.1.33");
+ var device = Capture(@"\Device\NPF_{" + KmGuid + "}", "KM_TEST");
+ Assert.Same(device, GooseAdapterSelectionPolicy.Resolve(
+ Ip("192.16.1.33"), null, new[] { primary }, new[] { device }).Adapter);
+
+ var clone = Nic(OtherGuid, "Conflicting assigned alias", "22-33-44-55-66-77", "192.16.1.33");
+ var result = GooseAdapterSelectionPolicy.Resolve(
+ Ip("192.16.1.33"), null, new[] { primary, clone }, new[] { device });
+ Assert.Null(result.Adapter);
+ Assert.Contains("no unique Windows adapter", result.Evidence);
+ }
+
+ [Fact]
+ public void RemoteIED_UniqueWindowsRouteMayIdentifyAdapter_ButNoRouteMustNotGuess()
+ {
+ var ethernet = Nic(KmGuid, "Ethernet", "02:14:20:33:44:55", "10.0.0.20");
+ var adapter = Capture(@"\Device\NPF_{" + KmGuid + "}", "Ethernet");
+ var matched = GooseAdapterSelectionPolicy.Resolve(
+ Ip("10.0.0.50"), Ip("10.0.0.20"), new[] { ethernet }, new[] { adapter });
+ Assert.Same(adapter, matched.Adapter);
+ Assert.False(matched.IsSameHost);
+
+ var noRoute = GooseAdapterSelectionPolicy.Resolve(
+ Ip("10.0.0.50"), null, new[] { ethernet }, new[] { adapter });
+ Assert.Null(noRoute.Adapter);
+ }
+
+ [Fact]
+ public void UniqueMacFallback_AcceptsUnknownCaptureGuidOnly_RejectsContradictoryGuid()
+ {
+ var nic = Nic(KmGuid, "KM_TEST", "02-14-20-33-44-55", "192.16.1.13");
+ var noGuid = Capture("capture-opaque", "KM_TEST", "02:14:20:33:44:55");
+ Assert.Same(noGuid, GooseAdapterSelectionPolicy.Resolve(
+ Ip("192.16.1.13"), null, new[] { nic }, new[] { noGuid }).Adapter);
+
+ var wrongGuid = Capture(@"\Device\NPF_{" + OtherGuid + "}", "KM_TEST", "02:14:20:33:44:55");
+ Assert.Null(GooseAdapterSelectionPolicy.Resolve(
+ Ip("192.16.1.13"), null, new[] { nic }, new[] { wrongGuid }).Adapter);
+ }
+
+ [Fact]
+ public void DuplicateCaptureGuidAndIpLoopback_FailClosed()
+ {
+ var nic = Nic(KmGuid, "KM_TEST", "02-14-20-33-44-55", "192.16.1.13");
+ var first = Capture(@"\Device\NPF_{" + KmGuid + "}", "KM_TEST", index: 1);
+ var second = Capture(@"\Device\NPF_{" + KmGuid + "}", "KM_TEST #2", index: 2);
+ Assert.Null(GooseAdapterSelectionPolicy.Resolve(
+ Ip("192.16.1.13"), null, new[] { nic }, new[] { first, second }).Adapter);
+ Assert.Null(GooseAdapterSelectionPolicy.Resolve(
+ Ip("127.0.0.1"), Ip("127.0.0.1"), new[] { nic }, new[] { first }).Adapter);
+ }
+
+ [Fact]
+ public void CardEntryKeepsReadOnlyCaptureAndNeverEquatesMmsToObservedGoose()
+ {
+ var source = Read("MainWindow.IedGooseQuickStart.cs");
+ var subscriber = Read("MainWindow.GooseSubscriber.cs");
+ Assert.Contains("GooseAdapterSelectionPolicy.Resolve", source);
+ Assert.Contains("GooseAdapterSelectionPolicy.IsNpcapPseudoLoopback", source);
+ Assert.DoesNotContain("Where(adapter => !LooksLikeLoopback(adapter))", subscriber);
+ Assert.Contains("StartGooseSubscriber_Click(this, new RoutedEventArgs())", source);
+ Assert.Contains("_pendingIedGooseAutoStart = device", source);
+ Assert.Contains("GooseSubscriberRuntime.DefaultCaptureFilter", subscriber);
+ Assert.Contains("Waiting for actual GOOSE frames", subscriber);
+ Assert.DoesNotContain("SetTime", source);
+ }
+
+ private static string Read(string path)
+ {
+ DirectoryInfo? folder = new(AppContext.BaseDirectory);
+ while (folder is not null)
+ {
+ var file = Path.Combine(folder.FullName, path);
+ if (File.Exists(file)) return File.ReadAllText(file);
+ folder = folder.Parent;
+ }
+ throw new FileNotFoundException(path);
+ }
+}