From de5909896148e94e1758b702dfb1b2164cbcdb2f Mon Sep 17 00:00:00 2001 From: Andy Howard Date: Mon, 14 Sep 2026 17:28:18 +0100 Subject: [PATCH] Modernise test tooling and coverage validation --- SacsCoreUnitTests/SacsCoreUnitTests.csproj | 11 ++++--- .../Base/Data/SacsAesIvCrypter_Tester.cs | 15 ++-------- .../Base/Procs/SaProcess_Base_Tester.cs | 4 +-- .../Standard/Base/Utils/EnumUtil_Tester.cs | 5 ++-- .../Base/Utils/NewtonsoftJsonUtil_Tester.cs | 2 +- docs/component-reviews.md | 1 + docs/topics.md | 1 + eng/README.md | 4 ++- eng/validate.ps1 | 30 +++++++++++++++++-- 9 files changed, 47 insertions(+), 26 deletions(-) diff --git a/SacsCoreUnitTests/SacsCoreUnitTests.csproj b/SacsCoreUnitTests/SacsCoreUnitTests.csproj index dcce6c7..889cfd7 100644 --- a/SacsCoreUnitTests/SacsCoreUnitTests.csproj +++ b/SacsCoreUnitTests/SacsCoreUnitTests.csproj @@ -9,10 +9,13 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/SacsCoreUnitTests/Standard/Base/Data/SacsAesIvCrypter_Tester.cs b/SacsCoreUnitTests/Standard/Base/Data/SacsAesIvCrypter_Tester.cs index f5b4998..54167d4 100644 --- a/SacsCoreUnitTests/Standard/Base/Data/SacsAesIvCrypter_Tester.cs +++ b/SacsCoreUnitTests/Standard/Base/Data/SacsAesIvCrypter_Tester.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text; using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -14,7 +14,6 @@ namespace Sacs.UnitTests.Net.Standard.Base.Data //------------------------------------------------------------------- // Data members. private static Random random = new Random(); - private TestContext testContextInstance; //------------------------------------------------------------------- // Construction. @@ -27,17 +26,7 @@ namespace Sacs.UnitTests.Net.Standard.Base.Data ///Gets or sets the test context which provides ///information about and functionality for the current test run. /// - public TestContext TestContext - { - get - { - return testContextInstance; - } - set - { - testContextInstance = value; - } - } + public TestContext TestContext { get; set; } //------------------------------------------------------------------- #region Additional test attributes diff --git a/SacsCoreUnitTests/Standard/Base/Procs/SaProcess_Base_Tester.cs b/SacsCoreUnitTests/Standard/Base/Procs/SaProcess_Base_Tester.cs index 7764802..e5fb768 100644 --- a/SacsCoreUnitTests/Standard/Base/Procs/SaProcess_Base_Tester.cs +++ b/SacsCoreUnitTests/Standard/Base/Procs/SaProcess_Base_Tester.cs @@ -79,7 +79,7 @@ namespace Sacs.UnitTests.Net.Standard.Base.Procs Task runTask = process.RunAsync(); Assert.IsTrue(process.Started.Wait(1000), "Process did not start."); - await Assert.ThrowsExceptionAsync(() => process.RunAsync()); + await Assert.ThrowsExactlyAsync(() => process.RunAsync()); process.Release.Set(); await runTask; @@ -97,7 +97,7 @@ namespace Sacs.UnitTests.Net.Standard.Base.Procs Assert.IsFalse(await process.StopProcessAsync(40)); Assert.IsTrue(process.IsAsync, "A timed-out stop must retain the active process state."); - await Assert.ThrowsExceptionAsync(() => process.RunAsync()); + await Assert.ThrowsExactlyAsync(() => process.RunAsync()); process.Release.Set(); await runTask; diff --git a/SacsCoreUnitTests/Standard/Base/Utils/EnumUtil_Tester.cs b/SacsCoreUnitTests/Standard/Base/Utils/EnumUtil_Tester.cs index b0fec91..f1a0f85 100644 --- a/SacsCoreUnitTests/Standard/Base/Utils/EnumUtil_Tester.cs +++ b/SacsCoreUnitTests/Standard/Base/Utils/EnumUtil_Tester.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Microsoft.VisualStudio.TestTools.UnitTesting; using Sarand.Net.Standard.Base.Core; @@ -44,10 +44,9 @@ namespace Sarand.UnitTests.Core.Base.Utils Assert.AreEqual(SaMessageLevel.Debug, EnumUtil.ParseEnum("Debug")); } [TestMethod] - [ExpectedException(typeof(ArgumentException), "Requested value 'ABC' was not found.")] public void EnumUtil_ParseEnum_Exception () { - SaMessageLevel messageLevel = EnumUtil.ParseEnum("ABC"); + Assert.ThrowsExactly(() => EnumUtil.ParseEnum("ABC")); } //------------------------------------------------------------------- [TestMethod] diff --git a/SacsCoreUnitTests/Standard/Base/Utils/NewtonsoftJsonUtil_Tester.cs b/SacsCoreUnitTests/Standard/Base/Utils/NewtonsoftJsonUtil_Tester.cs index 86b0fad..4d6aea6 100644 --- a/SacsCoreUnitTests/Standard/Base/Utils/NewtonsoftJsonUtil_Tester.cs +++ b/SacsCoreUnitTests/Standard/Base/Utils/NewtonsoftJsonUtil_Tester.cs @@ -77,7 +77,7 @@ namespace Sacs.UnitTests.Net.Standard.Base.Utils Assert.IsNotNull(objectException); Assert.IsNull(NewtonsoftJsonUtil.ParseJsonArray("{}", out Exception arrayException)); Assert.IsNotNull(arrayException); - Assert.ThrowsException(() => NewtonsoftJsonUtil.ParseJsonToken("{")); + Assert.ThrowsExactly(() => NewtonsoftJsonUtil.ParseJsonToken("{")); } [TestMethod] diff --git a/docs/component-reviews.md b/docs/component-reviews.md index 51950c1..1d6ae37 100644 --- a/docs/component-reviews.md +++ b/docs/component-reviews.md @@ -15,6 +15,7 @@ Last updated: 2026-09-14 | `SacsStandardBase/Base/Utils/ImageUtils.cs` | ImageSharp wrapper | Covered for package upgrades | 2026-09-14 | Extend the suite when adding formats or changing resize policy. | Five in-memory tests cover PNG and JPEG metadata, invalid and truncated input, SVG recognition, format-preserving encoding, proportional landscape and portrait resizing, no-upscale behavior, and invalid constraints. | | `SacsStandardBase` JSON utilities and `SysId` converter | Newtonsoft.Json integration | Covered for package upgrades | 2026-09-14 | Retain the suite for future Newtonsoft.Json updates. | Six tests cover objects, arrays, nulls, serializer settings, valid and invalid parsing paths, dynamic access, `SysId` conversion, independent deep copies, nested collections, and constructor-value replacement. | | `SacsStandardDbas` provider and parameter creation | Microsoft.Data.SqlClient and registry integration | Initial package-upgrade coverage complete | 2026-09-14 | Expand into configured database integration tests before the planned SqlClient major upgrade. | Four database-free tests cover SQL versus forced ODBC connection selection, connection strings, values and nulls, explicit integer and binary types, SQL `DateTime2`, ODBC `DateTime`, precision normalization, and sentinel dates. The redundant Registry 5.0.0 package was removed; .NET 10 supplies the APIs and `NU1510` is gone. | +| `SacsCoreUnitTests` | Test SDK, MSTest, and coverage tooling | Current; coverage verified | 2026-09-14 | Treat MSTest 4 as a separate major-version review. | Uses Microsoft.NET.Test.Sdk 18.10.0, MSTest 3.10.1, and coverlet collector 10.0.1. New analyzer findings were corrected, all 126 tests pass, and the optional validation coverage mode verifies a non-empty Cobertura report. | | `SacsMobileXamarin` | Migration disposition | Isolated; retained for reference | 2026-09-10 | Keep outside the supported build unless a separately scoped resurrection or migration is approved. | Source is unchanged and available through `SaCommonVS.LegacyXamarin.sln`; see `docs/legacy-xamarin.md`. | | [`SacsMobileMaui`](../SacsMobileMaui/docs/component-reviews.md) | Project | .NET 10 migration complete; tracked in project docs | 2026-09-11 | Validate Apple targets on a Mac and continue API cleanup in the project tracker. | The shared project and both active mobile consumers target .NET 10. Windows runtime smoke tests pass, and Windows and Android Debug builds complete with zero errors. | | `SacsStandardBase/Base/Procs/SaProcess_Base.cs` | Class | In progress | 2026-09-09 | Complete the Process Base actions recorded in `topics.md`; review derived processes separately for cooperative polling. | Lifecycle state ownership and cancellation lock ordering were changed. Five focused tests pass. Remaining concerns include mixed sync/async execution, durable cancellation outcome, cancellation exception handling, disposal, and status synchronization. | diff --git a/docs/topics.md b/docs/topics.md index 6f7a887..19400f1 100644 --- a/docs/topics.md +++ b/docs/topics.md @@ -44,6 +44,7 @@ Last updated: 2026-09-14 | Topic | Date noted | Date started | Date completed | Outcome | |---|---|---|---|---| +| Modernise Common test tooling and coverage collection | 2026-09-14 | 2026-09-14 | 2026-09-14 | Updated Microsoft.NET.Test.Sdk from 16.5.0 to 18.10.0, MSTest adapter and framework from 2.1.0 to 3.10.1, and coverlet collector from 1.2.0 to 10.0.1 with private-assets metadata. Replaced legacy exception assertions and corrected the `TestContext` property exposed by the newer analyzers. All 126 Common tests, 11 BusinessMgr Control tests, and 13 BusinessMgr Portal tests pass. The validation scripts now offer `-CollectCoverage`, which verifies non-empty Cobertura reports across Common and both BusinessMgr suites. | | Remove redundant Microsoft.Win32.Registry package | 2026-09-10 | 2026-09-14 | 2026-09-14 | Removed the direct 5.0.0 package reference from `SacsStandardDbas`; .NET 10 supplies the required registry APIs. Common and BusinessMgr restore, build, and all 126 Common, 11 Control, and 13 Portal tests pass. The `NU1510` warning no longer appears. | | Apply low-risk Common package patches and regression coverage | 2026-09-14 | 2026-09-14 | 2026-09-14 | Updated Microsoft.Data.SqlClient from 5.2.2 to 5.2.3, Newtonsoft.Json from 13.0.3 to 13.0.4, and SixLabors.ImageSharp from 3.1.11 to 3.1.12. Added 15 in-memory tests covering image recognition, encoding and resizing; JSON serialization, parsing, dynamic access, `SysId` conversion and deep copying; and SQL/ODBC provider selection and command parameters. The Common solution builds with zero errors; all 126 Common tests, 11 BusinessMgr Control tests, and 13 BusinessMgr Portal tests pass. Existing warnings are unchanged. | | Make asynchronous Process Base execution awaitable and retain state after a stop timeout | 2026-09-08 | 2026-09-08 | 2026-09-08 | `RunAsync()` returns `Task`; `StopProcessAsync()` reports success and leaves active state owned by a worker that has not stopped. | diff --git a/eng/README.md b/eng/README.md index e701fb4..25dac90 100644 --- a/eng/README.md +++ b/eng/README.md @@ -8,4 +8,6 @@ Run the complete Common validation, including the downstream BusinessMgr build a Use `-SkipRestore` when packages are already restored. Use `-SkipDownstream` only for a deliberately scoped local check; shared behavior, public API, framework, and package changes require downstream validation. -The script suppresses .NET first-run setup and banners, builds serially to avoid excessive build-worker creation, stores test output under the ignored `TestResults/Validation` folder, audits the NPOI-owning project for vulnerable packages, checks the Git diff, and verifies working-tree line endings without modifying files. Its isolated `DOTNET_CLI_HOME` stores only CLI state; NuGet packages reuse `NUGET_PACKAGES` when already configured or the user's standard `.nuget/packages` cache. +Use `-CollectCoverage` after changing the test platform or coverage collector. It runs an additional Common coverage pass, validates that a non-empty Cobertura report was produced, and passes the same option to downstream BusinessMgr validation. + +The script suppresses .NET first-run setup and banners, builds serially to avoid excessive build-worker creation, stores test and optional coverage output under the ignored `TestResults/Validation` folder, audits the NPOI-owning project for vulnerable packages, checks the Git diff, and verifies working-tree line endings without modifying files. Its isolated `DOTNET_CLI_HOME` stores only CLI state; NuGet packages reuse `NUGET_PACKAGES` when already configured or the user's standard `.nuget/packages` cache. diff --git a/eng/validate.ps1 b/eng/validate.ps1 index 7805c9e..204fe3c 100644 --- a/eng/validate.ps1 +++ b/eng/validate.ps1 @@ -1,7 +1,8 @@ [CmdletBinding()] param( [switch] $SkipRestore, - [switch] $SkipDownstream + [switch] $SkipDownstream, + [switch] $CollectCoverage ) $ErrorActionPreference = 'Stop' @@ -73,6 +74,23 @@ function Test-LineEndings } } +function Test-CoverageReport +{ + param([Parameter(Mandatory)] [string] $ResultsPath) + + $report = Get-ChildItem -LiteralPath $ResultsPath -Filter 'coverage.cobertura.xml' -Recurse | Select-Object -First 1 + if (($null -eq $report) -or ($report.Length -eq 0)) + { + throw "A non-empty Cobertura coverage report was not found under $ResultsPath" + } + + [xml] $coverage = Get-Content -LiteralPath $report.FullName -Raw + if ($coverage.DocumentElement.Name -ne 'coverage') + { + throw "The coverage report at $($report.FullName) is not valid Cobertura XML." + } +} + Push-Location $repositoryRoot try { @@ -85,6 +103,12 @@ try Invoke-External dotnet @('build', 'SaCommonVS.sln', '--configuration', 'Debug', '--no-restore', '--disable-build-servers', '--verbosity', 'minimal', '--maxcpucount:1') Invoke-External dotnet @('test', 'SacsCoreUnitTests\SacsCoreUnitTests.csproj', '--configuration', 'Debug', '--no-build', '--no-restore', '--filter', 'FullyQualifiedName~SaMsSpreadsheet_POI_Tester', '--results-directory', $testResultsPath, '--verbosity', 'minimal') Invoke-External dotnet @('test', 'SacsCoreUnitTests\SacsCoreUnitTests.csproj', '--configuration', 'Debug', '--no-build', '--no-restore', '--results-directory', $testResultsPath, '--verbosity', 'minimal') + if ($CollectCoverage) + { + $coverageResultsPath = Join-Path $testResultsPath "Coverage\$([guid]::NewGuid())" + Invoke-External dotnet @('test', 'SacsCoreUnitTests\SacsCoreUnitTests.csproj', '--configuration', 'Debug', '--no-build', '--no-restore', '--collect', 'XPlat Code Coverage', '--results-directory', $coverageResultsPath, '--verbosity', 'minimal') + Test-CoverageReport $coverageResultsPath + } Invoke-External dotnet @('list', 'SacsStandardProc\SacsStandardProc.csproj', 'package', '--vulnerable', '--include-transitive') Invoke-External git @('diff', '--check') Test-LineEndings @@ -96,7 +120,9 @@ try { throw "BusinessMgr validation script was not found at $businessMgrValidation" } - & $businessMgrValidation + $businessMgrArguments = @{} + if ($CollectCoverage) { $businessMgrArguments.CollectCoverage = $true } + & $businessMgrValidation @businessMgrArguments if (-not $?) { throw 'BusinessMgr validation failed.' } }