Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
71e1bc2
Add IO testing domain model
masarray Jul 28, 2026
0140ada
Add IO evidence state machine
masarray Jul 28, 2026
d9540e8
Add IO evidence transition tests
masarray Jul 28, 2026
34de3b9
Add IO import contract validation
masarray Jul 28, 2026
978dc96
Add IO import validation tests
masarray Jul 28, 2026
fe54fc4
Add dedicated IO testing window shell
masarray Jul 28, 2026
e74d476
Add IO testing window code behind
masarray Jul 28, 2026
5847f7f
Harden IO testing XAML bindings
masarray Jul 28, 2026
e5fc837
Harden IO transition assertions
masarray Jul 28, 2026
e1b5412
Add live IO test model state
masarray Jul 28, 2026
913f6ac
Expose live IO observations
masarray Jul 28, 2026
8b5928e
Strengthen IO import validation
masarray Jul 28, 2026
80a49f9
Add ARSAS IO List XLSX importer
masarray Jul 28, 2026
b87d42a
Add IO test live binding preview
masarray Jul 28, 2026
059bb57
Add Open XML workbook dependency
masarray Jul 28, 2026
142c468
Show imported IO binding preview
masarray Jul 28, 2026
755c185
Connect IO testing window model
masarray Jul 28, 2026
35efeea
Add IO test workbook importer
masarray Jul 28, 2026
8e28f86
Add dedicated IO testing launcher
masarray Jul 28, 2026
dd713a4
Add IO List Testing workspace launcher
masarray Jul 28, 2026
058d3fa
Install IO List Testing launcher
masarray Jul 28, 2026
4d7b378
Test IO List workbook import
masarray Jul 28, 2026
b4891b1
Expose import validation summaries
masarray Jul 28, 2026
c011339
Test IO live binding preview
masarray Jul 28, 2026
6b2b868
Add IO test workbook importer regressions
masarray Jul 28, 2026
0eab9b0
Remove redundant Open XML dependency
masarray Jul 28, 2026
d920a2d
Use MainWindow IO Testing integration
masarray Jul 28, 2026
1b9a6d5
Remove duplicate IO Testing launcher
masarray Jul 28, 2026
2d63ff7
Remove duplicate Open XML importer
masarray Jul 28, 2026
13364ed
Remove duplicate importer tests
masarray Jul 28, 2026
ce05ff5
Add focused IO testing validation
masarray Jul 28, 2026
eab9960
Align missing IED binding assertion
masarray Jul 28, 2026
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
96 changes: 96 additions & 0 deletions .github/workflows/validate-io-testing.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: Validate IO List Testing

on:
pull_request:
paths:
- "MainWindow.IoTesting.cs"
- "IoListTestingWindow.xaml"
- "IoListTestingWindow.xaml.cs"
- "Models/IoTesting/**"
- "Services/IoTesting/**"
- "tests/ARSAS.Tests/IoTest*"
- "tests/ARSAS.Tests/IoList*"
- "engines/ARIEC61850.lock.json"
- ".github/workflows/validate-io-testing.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
validate-io-testing:
name: Build and test IO List Testing
runs-on: windows-latest

steps:
- name: Checkout ARSAS application
uses: actions/checkout@v4
with:
path: ARSAS

- name: Resolve immutable ARIEC61850 engine lock
shell: powershell
run: |
$lock = Get-Content ".\ARSAS\engines\ARIEC61850.lock.json" -Raw | ConvertFrom-Json
if ($lock.repository -notmatch '^[^/]+/[^/]+$' -or $lock.commit -notmatch '^[0-9a-f]{40}$') {
throw "ARIEC61850 lock metadata is invalid."
}
"ARIEC61850_REPOSITORY=$($lock.repository)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"ARIEC61850_COMMIT=$($lock.commit)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

- name: Checkout immutable ARIEC61850 engine revision
shell: powershell
run: |
git clone --quiet --filter=blob:none --no-checkout "https://github.com/$env:ARIEC61850_REPOSITORY.git" ARIEC61850
git -C .\ARIEC61850 fetch --quiet --depth 1 origin $env:ARIEC61850_COMMIT
git -C .\ARIEC61850 checkout --quiet --detach $env:ARIEC61850_COMMIT
$actual = (git -C .\ARIEC61850 rev-parse HEAD).Trim()
if ($actual -ne $env:ARIEC61850_COMMIT) {
throw "ARIEC61850 pin mismatch. Expected $env:ARIEC61850_COMMIT, got $actual."
}

- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

- name: Restore focused test project
run: dotnet restore .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj

- name: Build focused IO testing project
shell: powershell
run: |
$log = ".\ARSAS\io-testing-build.log"
dotnet build .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-restore *> $log
$buildExitCode = $LASTEXITCODE
if ($buildExitCode -ne 0) {
Write-Host "=== IO Testing compiler errors ===" -ForegroundColor Red
$errors = @(Select-String -Path $log -Pattern ': error ')
if ($errors.Count -eq 0) {
Get-Content $log -Tail 100 | ForEach-Object { Write-Host $_ -ForegroundColor Red }
}
else {
$errors | ForEach-Object { Write-Host $_.Line -ForegroundColor Red }
}
exit $buildExitCode
}
Get-Content $log -Tail 12

- name: Upload IO testing compiler diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: ARSAS-io-testing-compiler-log
path: ARSAS/io-testing-build.log
if-no-files-found: error

- name: Run IO testing regression tests
run: dotnet test .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-build --no-restore --filter "FullyQualifiedName~IoTest|FullyQualifiedName~IoList" --logger "trx;LogFileName=io-testing-tests.trx" --results-directory .\ARSAS\TestResults

- name: Upload IO testing test results
if: always()
uses: actions/upload-artifact@v4
with:
name: ARSAS-io-testing-test-results
path: ARSAS/TestResults/*.trx
if-no-files-found: ignore
2 changes: 1 addition & 1 deletion ArIED61850Tester.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,4 @@
<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." />
</Target>
</Project>
</Project>
167 changes: 167 additions & 0 deletions IoListTestingWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
<Window x:Class="ArIED61850Tester.IoListTestingWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="ARSAS - IO List Testing"
Height="900" Width="1500" MinHeight="760" MinWidth="1180"
WindowStartupLocation="CenterOwner" WindowState="Maximized"
Background="{StaticResource AppBackgroundGradient}"
FontFamily="Aptos, Segoe UI Variable Text, Segoe UI, Calibri"
Icon="Assets/app-icon.ico">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="10"/>
<RowDefinition Height="*"/>
<RowDefinition Height="10"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<Border Grid.Row="0" Style="{StaticResource Card}" Padding="14,10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="IO LIST TESTING" Style="{StaticResource MicroLabel}" Foreground="{StaticResource Accent}"/>
<TextBlock Text="{Binding Project.ProjectName}" FontSize="18" FontWeight="SemiBold" Foreground="{StaticResource Ink}"/>
<TextBlock Text="Dedicated FAT workspace · imported IEDs and IO-list SDI signals only"
Style="{StaticResource Caption}" Margin="0,2,0,0"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Border Background="{StaticResource SoftBlue}" CornerRadius="14" Padding="12,7" Margin="0,0,8,0">
<TextBlock Text="{Binding ProjectSummary}" FontSize="12" Foreground="{StaticResource Ink}"/>
</Border>
<Button Content="Return to Engineering" Style="{StaticResource SoftButton}" Click="ReturnToEngineering_Click"/>
</StackPanel>
</Grid>
</Border>

<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="286"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

<Border Grid.Column="0" Style="{StaticResource Card}" Padding="10">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Margin="4,2,4,10">
<TextBlock Text="IED TEST PLAN" Style="{StaticResource MicroLabel}"/>
<TextBlock Text="Only IEDs imported from the workbook" Style="{StaticResource Caption}" Margin="0,2,0,0"/>
</StackPanel>
<ListBox ItemsSource="{Binding Project.Ieds}" SelectedItem="{Binding SelectedIed, Mode=TwoWay}"
BorderThickness="0" Background="Transparent" HorizontalContentAlignment="Stretch"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="0,0,0,7"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="IedCard" Background="{StaticResource IedCardSurface}"
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="1"
CornerRadius="14" Padding="10">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="IedCard" Property="Background" Value="{StaticResource IedCardHover}"/>
<Setter TargetName="IedCard" Property="BorderBrush" Value="#8EAFE3"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="IedCard" Property="Background" Value="{StaticResource IedCardSelected}"/>
<Setter TargetName="IedCard" Property="BorderBrush" Value="#4F86E5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding IedName}" FontSize="13" FontWeight="SemiBold" Foreground="{StaticResource Ink}"/>
<TextBlock Grid.Row="1" Text="{Binding IpAddress}" Style="{StaticResource Caption}" Margin="0,2,0,0"/>
<TextBlock Grid.Row="2" Text="{Binding IedRole}" Style="{StaticResource Caption}" TextTrimming="CharacterEllipsis"/>
<TextBlock Grid.Row="3" Margin="0,6,0,0" FontSize="11.3" Foreground="{StaticResource Muted}">
<Run Text="{Binding BoundCount}"/><Run Text=" bound · "/>
<Run Text="{Binding PassedCount}"/><Run Text=" passed · "/>
<Run Text="{Binding PendingCount}"/><Run Text=" pending"/>
</TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Border>

<Border Grid.Column="2" Style="{StaticResource Card}" Padding="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="8"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel>
<TextBlock Text="{Binding SelectedIed.IedName}" Style="{StaticResource SectionTitle}"/>
<TextBlock Text="{Binding SelectedIedSummary}" Style="{StaticResource Caption}" Margin="0,2,0,0"/>
</StackPanel>
<Border Grid.Column="1" Background="#F7FAFF" BorderBrush="{StaticResource BorderSubtle}"
BorderThickness="1" CornerRadius="13" Padding="11,6">
<TextBlock Text="Imported SDI scope · read-only preview" Style="{StaticResource Caption}"/>
</Border>
</Grid>

<DataGrid Grid.Row="2" ItemsSource="{Binding SelectedIed.TestPoints}"
AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False"
IsReadOnly="True" HeadersVisibility="Column" GridLinesVisibility="Horizontal"
BorderThickness="0" Background="Transparent" RowBackground="White"
AlternatingRowBackground="#F8FAFD" HorizontalGridLinesBrush="{StaticResource BorderSubtle}"
SelectionMode="Single" SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridCheckBoxColumn Header="Test" Binding="{Binding TestEnabled}" Width="52"/>
<DataGridTextColumn Header="Signal name" Binding="{Binding SignalName}" Width="2.1*"/>
<DataGridTextColumn Header="IEC reference" Binding="{Binding ObjectReference}" Width="2.4*"/>
<DataGridTextColumn Header="Live binding" Binding="{Binding LiveBindingText}" Width="112"/>
<DataGridTextColumn Header="Current" Binding="{Binding Runtime.CurrentValue}" Width="100"/>
<DataGridTextColumn Header="Quality" Binding="{Binding Runtime.CurrentQuality}" Width="96"/>
<DataGridTextColumn Header="Source" Binding="{Binding Runtime.CurrentSource}" Width="104"/>
<DataGridTextColumn Header="ON evidence" Binding="{Binding Runtime.OnEvidence.CapturedAt, StringFormat={}{0:yyyy-MM-dd HH:mm:ss.fff}}" Width="178"/>
<DataGridTextColumn Header="OFF evidence" Binding="{Binding Runtime.OffEvidence.CapturedAt, StringFormat={}{0:yyyy-MM-dd HH:mm:ss.fff}}" Width="178"/>
<DataGridTextColumn Header="Result" Binding="{Binding Runtime.State}" Width="100"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
</Grid>

<Border Grid.Row="4" Background="#F7FAFF" BorderBrush="{StaticResource BorderSubtle}"
BorderThickness="1" CornerRadius="14" Padding="12,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding WorkspaceStatus}" Style="{StaticResource Caption}"/>
<TextBlock Grid.Column="1" Text="Imported FAT workspace" Style="{StaticResource MicroLabel}" Foreground="{StaticResource Success}"/>
</Grid>
</Border>
</Grid>
</Window>
66 changes: 66 additions & 0 deletions IoListTestingWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using ArIED61850Tester.Models.IoTesting;

namespace ArIED61850Tester;

public partial class IoListTestingWindow : Window, INotifyPropertyChanged
{
private IoTestIedPlan? _selectedIed;

public IoListTestingWindow()
: this(CreateEmptyProject())
{
}

public IoListTestingWindow(IoTestProject project)
{
Project = project ?? throw new ArgumentNullException(nameof(project));
Project.InitializeRuntimeNotifications();
InitializeComponent();
DataContext = this;
SelectedIed = Project.Ieds.FirstOrDefault();
}

public IoTestProject Project { get; }

public IoTestIedPlan? SelectedIed
{
get => _selectedIed;
set
{
if (ReferenceEquals(_selectedIed, value))
return;
_selectedIed = value;
Raise();
Raise(nameof(SelectedIedSummary));
}
}

public string ProjectSummary =>
$"{Project.Ieds.Count} IED · {Project.SignalCount} SDI · {Project.ReadySignalCount} ready · {Project.LiveBoundSignalCount} live-bound";

public string SelectedIedSummary => SelectedIed == null
? "Select an imported IED"
: $"{SelectedIed.IpAddress} · {SelectedIed.BoundCount}/{SelectedIed.TestPoints.Count} bound · {SelectedIed.LiveStatusText}";

public string WorkspaceStatus => Project.LiveBoundSignalCount > 0
? $"{Project.LiveBoundSignalCount:N0} signal(s) already match the loaded ARSAS workspace. FAT execution remains locked until the session controller and evidence journal are enabled."
: "The IO test plan is imported. Load or discover an IED in the engineering workspace before starting live FAT execution.";

public event PropertyChangedEventHandler? PropertyChanged;

private void ReturnToEngineering_Click(object sender, RoutedEventArgs e)
=> Close();

private void Raise([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

private static IoTestProject CreateEmptyProject() => new()
{
ProjectId = "EMPTY",
SchemaVersion = "ARSAS-FAT-IO-1.0",
ProjectName = "No IO List project loaded"
};
}
Loading
Loading