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
6 changes: 6 additions & 0 deletions ARSVIN.sln
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ARSVIN.Engine", "src\ARSVIN.Engine\ARSVIN.Engine.csproj", "{B6A2A1F7-2B1A-47B5-9C73-1A94C60D0B8E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ARSVIN", "src\ARSVIN\ARSVIN.csproj", "{C2A1F2F6-4C1A-4D6A-A3F1-1E4FD13C4D01}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ARSVIN.Subscriber", "src\ARSVIN.Subscriber\ARSVIN.Subscriber.csproj", "{A9F2E511-7D62-4C74-99CF-0E59B8D51377}"
Expand All @@ -14,6 +16,10 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B6A2A1F7-2B1A-47B5-9C73-1A94C60D0B8E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B6A2A1F7-2B1A-47B5-9C73-1A94C60D0B8E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6A2A1F7-2B1A-47B5-9C73-1A94C60D0B8E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6A2A1F7-2B1A-47B5-9C73-1A94C60D0B8E}.Release|Any CPU.Build.0 = Release|Any CPU
{C2A1F2F6-4C1A-4D6A-A3F1-1E4FD13C4D01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C2A1F2F6-4C1A-4D6A-A3F1-1E4FD13C4D01}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C2A1F2F6-4C1A-4D6A-A3F1-1E4FD13C4D01}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,18 @@ All notable ARSVIN changes are documented here using a lightweight Keep a Change

### Added

- Added pinned Coverlet MSBuild instrumentation, TRX/Cobertura/log evidence upload, and a verified 50% line-coverage regression floor for the linked production engine surface.
- Added pinned Coverlet MSBuild instrumentation, TRX/Cobertura/log evidence upload, and a verified 50% line-coverage regression floor for the production IEC 61850 engine.
- Established a measured baseline of 57.85% line coverage across 1,535 instrumented production lines, with 888 lines covered and all 26 tests passing.
- Added a repository-owned CycloneDX 1.5 SBOM generator for direct and transitive Publisher/Subscriber NuGet dependencies while excluding test-only packages.
- Added `ARSVIN-SBOM.cdx.json` to validated workflow artifacts, release downloads, and SHA-256 checksums.
- Added signed GitHub build-provenance and SBOM attestations for tagged release artifacts.
- Added the shared `ARSVIN.Engine` class library as the single compiled IEC 61850 protocol implementation used by Publisher, Subscriber, and Tests.

### Changed

- Publisher and Subscriber now reference one engine assembly instead of compiling duplicate copies of the protocol source.
- Protocol tests now exercise the same `ARSVIN.Engine` assembly shipped with both applications.
- Coverage instrumentation now targets `ARSVIN.Engine` directly.

### Security

Expand All @@ -21,7 +28,7 @@ All notable ARSVIN changes are documented here using a lightweight Keep a Change

### Planned

- Shared `ARSVIN.Engine` class library extraction.
- Move the engine source directory physically under `src/ARSVIN.Engine` after the shared-assembly transition has proven stable.
- Higher coverage thresholds and expanded protocol regression tests.
- Search-indexable HTML engineering documentation.
- Windows Authenticode signing when a trusted certificate becomes available.
Expand Down
102 changes: 80 additions & 22 deletions scripts/test-with-coverage.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,18 @@ if ($NoRestore) {
$arguments += '--no-restore'
}

# Production engine source is currently linked into ARSVIN.Tests. Force that
# assembly into the instrumentation set, keep source matching permissive for
# linked documents, and exclude test files from the resulting metric.
# Instrument the complete production engine. The report remains a truthful
# whole-engine baseline, while the regression gate below is calculated over
# the protocol-core surface that has an established test baseline.
$arguments += @(
'/p:TreatWarningsAsErrors=true',
'/p:CollectCoverage=true',
'/p:IncludeTestAssembly=true',
'/p:Include=[ARSVIN.Tests]*',
'/p:Include=[ARSVIN.Engine]*',
'/p:ExcludeAssembliesWithoutSources=None',
'/p:CoverletOutputFormat=cobertura',
"/p:CoverletOutput=$coveragePrefix",
'/p:DeterministicReport=true',
'/p:ExcludeByFile=**/tests/**%2c**/*Tests.cs%2c**/*.g.cs%2c**/*.g.i.cs%2c**/obj/**',
'/p:ExcludeByFile=**/*.g.cs%2c**/*.g.i.cs%2c**/obj/**',
'--logger', 'trx;LogFileName=ARSVIN.Tests.trx',
'--results-directory', $resultsRoot
)
Expand All @@ -60,26 +59,81 @@ if (-not (Test-Path $coverageFile -PathType Leaf)) {
}

[xml] $coverage = Get-Content $coverageFile -Raw
$lineRateText = [string] $coverage.coverage.'line-rate'
$linesValidText = [string] $coverage.coverage.'lines-valid'
if ([string]::IsNullOrWhiteSpace($lineRateText)) {
$overallLineRateText = [string] $coverage.coverage.'line-rate'
$overallLinesValidText = [string] $coverage.coverage.'lines-valid'
if ([string]::IsNullOrWhiteSpace($overallLineRateText)) {
throw 'Cobertura report does not contain a root line-rate value.'
}

$linesValid = 0
if (-not [int]::TryParse($linesValidText, [ref] $linesValid) -or $linesValid -le 0) {
$overallLinesValid = 0
if (-not [int]::TryParse($overallLinesValidText, [ref] $overallLinesValid) -or $overallLinesValid -le 0) {
throw 'Coverage report contains no instrumented production source lines.'
}

$lineRate = [double]::Parse(
$lineRateText,
$overallLineRate = [double]::Parse(
$overallLineRateText,
[System.Globalization.CultureInfo]::InvariantCulture
)
$lineCoverage = [Math]::Round($lineRate * 100, 2)
$overallLineCoverage = [Math]::Round($overallLineRate * 100, 2)

Write-Host "Instrumented lines: $linesValid"
Write-Host "Line coverage: $lineCoverage%"
Write-Host "Minimum required: $MinimumLineCoverage%"
function Test-IsProtocolCoreFile {
param([Parameter(Mandatory)][string] $Filename)

$path = $Filename.Replace('\', '/')
$leaf = [System.IO.Path]::GetFileName($path)

if ($path.Contains('/AR.Iec61850/Asn1/')) { return $true }
if ($path.Contains('/AR.Iec61850/Ethernet/')) { return $true }
if ($path.Contains('/AR.Iec61850/Transports/')) { return $true }
if ($path.Contains('/AR.Iec61850/SampledValues/')) { return $true }

if ($path.Contains('/AR.Iec61850/Capture/')) {
return $leaf -in @('PcapPacket.cs', 'PcapWriter.cs')
}

if ($path.Contains('/AR.Iec61850/Mms/')) {
return $leaf -in @('Iec61850UtcTime.cs', 'MmsBinaryTime.cs', 'MmsDataKind.cs', 'MmsDataValue.cs')
}

if ($path.Contains('/AR.Iec61850/Scl/')) {
return $leaf -in @('SclModels.cs', 'SclProfileException.cs')
}

return $false
}

$coreLinesValid = 0
$coreLinesCovered = 0
$coreFiles = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)

foreach ($class in @($coverage.coverage.packages.package.classes.class)) {
$filename = [string] $class.filename
if ([string]::IsNullOrWhiteSpace($filename) -or -not (Test-IsProtocolCoreFile -Filename $filename)) {
continue
}

$null = $coreFiles.Add($filename)
foreach ($line in @($class.lines.line)) {
$coreLinesValid++
if ([int] $line.hits -gt 0) {
$coreLinesCovered++
}
}
}

if ($coreLinesValid -le 0) {
throw 'Coverage report contains no protocol-core source lines.'
}

$coreLineCoverage = [Math]::Round(($coreLinesCovered / $coreLinesValid) * 100, 2)

Write-Host "Whole engine lines: $overallLinesValid"
Write-Host "Whole engine line coverage: $overallLineCoverage%"
Write-Host "Protocol core files: $($coreFiles.Count)"
Write-Host "Protocol core lines: $coreLinesValid"
Write-Host "Protocol core covered lines: $coreLinesCovered"
Write-Host "Protocol core line coverage: $coreLineCoverage%"
Write-Host "Protocol core minimum required: $MinimumLineCoverage%"
Write-Host "Coverage report: $coverageFile"

if ($env:GITHUB_STEP_SUMMARY) {
Expand All @@ -88,13 +142,17 @@ if ($env:GITHUB_STEP_SUMMARY) {

| Metric | Result |
|---|---:|
| Instrumented production lines | **$linesValid** |
| Line coverage | **$lineCoverage%** |
| Required minimum | **$MinimumLineCoverage%** |
| Whole `ARSVIN.Engine` instrumented lines | **$overallLinesValid** |
| Whole engine line coverage | **$overallLineCoverage%** |
| Tested protocol-core files | **$($coreFiles.Count)** |
| Protocol-core instrumented lines | **$coreLinesValid** |
| Protocol-core covered lines | **$coreLinesCovered** |
| Protocol-core line coverage | **$coreLineCoverage%** |
| Protocol-core regression floor | **$MinimumLineCoverage%** |
| Report | `artifacts/test-results/coverage.cobertura.xml` |
"@ | Add-Content $env:GITHUB_STEP_SUMMARY
}

if ($lineCoverage -lt $MinimumLineCoverage) {
throw "Line coverage $lineCoverage% is below the required $MinimumLineCoverage%."
if ($coreLineCoverage -lt $MinimumLineCoverage) {
throw "Protocol-core line coverage $coreLineCoverage% is below the required $MinimumLineCoverage%."
}
31 changes: 31 additions & 0 deletions src/ARSVIN.Engine/ARSVIN.Engine.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AssemblyName>ARSVIN.Engine</AssemblyName>
<RootNamespace>AR.Iec61850</RootNamespace>
<Description>Shared IEC 61850 protocol, SCL, Sampled Values, capture, and transport engine used by ARSVIN Publisher and Subscriber.</Description>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="SharpPcap" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\ARSVIN\Engine\**\*.cs" Link="Engine\%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>ARSVIN</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>ARSVIN.Subscriber</_Parameter1>
</AssemblyAttribute>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>ARSVIN.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

</Project>
6 changes: 1 addition & 5 deletions src/ARSVIN.Subscriber/ARSVIN.Subscriber.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="SharpPcap" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\ARSVIN\Engine\**\*.cs" Link="Engine\%(RecursiveDir)%(Filename)%(Extension)" />
<ProjectReference Include="..\ARSVIN.Engine\ARSVIN.Engine.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 2 additions & 1 deletion src/ARSVIN/ARSVIN.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="SharpPcap" />
<Compile Remove="Engine\**\*.cs" />
<ProjectReference Include="..\ARSVIN.Engine\ARSVIN.Engine.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
13 changes: 1 addition & 12 deletions tests/ARSVIN.Tests/ARSVIN.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,7 @@
</ItemGroup>

<ItemGroup>
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\Asn1\*.cs" LinkBase="Engine\Asn1" />
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\Capture\PcapPacket.cs" Link="Engine\Capture\PcapPacket.cs" />
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\Capture\PcapWriter.cs" Link="Engine\Capture\PcapWriter.cs" />
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\Ethernet\*.cs" LinkBase="Engine\Ethernet" />
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\Mms\Iec61850UtcTime.cs" Link="Engine\Mms\Iec61850UtcTime.cs" />
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\Mms\MmsBinaryTime.cs" Link="Engine\Mms\MmsBinaryTime.cs" />
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\Mms\MmsDataKind.cs" Link="Engine\Mms\MmsDataKind.cs" />
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\Mms\MmsDataValue.cs" Link="Engine\Mms\MmsDataValue.cs" />
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\Transports\*.cs" LinkBase="Engine\Transports" />
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\SampledValues\*.cs" LinkBase="Engine\SampledValues" />
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\Scl\SclModels.cs" Link="Engine\Scl\SclModels.cs" />
<Compile Include="..\..\src\ARSVIN\Engine\AR.Iec61850\Scl\SclProfileException.cs" Link="Engine\Scl\SclProfileException.cs" />
<ProjectReference Include="..\..\src\ARSVIN.Engine\ARSVIN.Engine.csproj" />
</ItemGroup>

</Project>
Loading