From c865b2fc1df6a3c5472be090d5834c0f4b86695f Mon Sep 17 00:00:00 2001
From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com>
Date: Fri, 9 Oct 2026 13:54:32 -0300
Subject: [PATCH 1/4] Validate installed SqlClient family versions from Logging
packages
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
BUILDGUIDE.md | 32 +++
...ft.Data.SqlClient.Internal.Logging.targets | 53 ++++
.../Logging/src/Logging.csproj | 2 +
.../Logging/src/PackageReadme.md | 27 ++
.../Logging/test/RunVersionChecks.ps1 | 230 ++++++++++++++++++
5 files changed, 344 insertions(+)
create mode 100644 src/Microsoft.Data.SqlClient.Internal/Logging/build/Microsoft.Data.SqlClient.Internal.Logging.targets
create mode 100644 src/Microsoft.Data.SqlClient.Internal/Logging/test/RunVersionChecks.ps1
diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md
index 0585d362bf..0313de22a3 100644
--- a/BUILDGUIDE.md
+++ b/BUILDGUIDE.md
@@ -253,6 +253,38 @@ dotnet build -t:TestSqlClientFunctional -p:TestFramework=net10.0
### Packaging Projects
+#### Consumer family-version check
+
+The Logging package owns `ValidateSqlClientFamilyPackageVersions` and ships it in
+both `build/` and `buildTransitive/`. NuGet imports it into direct consumers and
+transitive PackageReference consumers (unless build assets are excluded).
+Installed Logging, Abstractions, SqlClient, Extensions.Azure, and AKV packages
+must match the resolved Logging version exactly, including prerelease labels.
+Optional family packages are not required; independently versioned
+Microsoft.SqlServer.Server and native SNI packages are excluded. The target uses
+resolved compile/runtime package metadata or legacy `packages.config`, not
+assembly versions or declared version ranges, and skips design-time builds.
+Missing version metadata or a missing Logging anchor is not guessed.
+
+**SQLCLIENT001** means installed family versions differ. Upgrade them together;
+historical extension `1.0.0` versions are not automatically mapped to driver
+`7.0.0`/`7.0.1`. The emergency escape hatch is
+`-p:SqlClientEnforceFamilyVersions=false` (or that property in the consumer
+project). It suppresses this check only, not incompatibilities.
+Ordinary project references do not propagate NuGet buildTransitive assets:
+Project-mode repository builds are not validated by this consumer target.
+
+Run the focused synthetic and isolated NuGet consumer regression suite with:
+
+```powershell
+pwsh -NoProfile -File src/Microsoft.Data.SqlClient.Internal/Logging/test/RunVersionChecks.ps1
+```
+
+The suite packs the real Logging project, creates small family-package fixtures
+in a private local feed, and verifies automatic imports for both direct and
+transitive package consumers. It uses a unique directory under Logging/test and
+removes that directory on completion; no SQL Server is required.
+
Just like building and testing the various projects in this repository, packaging the projects into NuGet packages is
also handled by `build.proj`. From the root of your repository, run `dotnet build` against `build.proj` with a pack target,
following this pattern:
diff --git a/src/Microsoft.Data.SqlClient.Internal/Logging/build/Microsoft.Data.SqlClient.Internal.Logging.targets b/src/Microsoft.Data.SqlClient.Internal/Logging/build/Microsoft.Data.SqlClient.Internal.Logging.targets
new file mode 100644
index 0000000000..776947df5a
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient.Internal/Logging/build/Microsoft.Data.SqlClient.Internal.Logging.targets
@@ -0,0 +1,53 @@
+
+
+
+ true
+
+
+
+
+
+ <_SqlClientResolvedAsset Include="@(ResolvedCompileFileDefinitions);@(ReferencePath);@(RuntimeCopyLocalItems);@(RuntimeTargetsCopyLocalItems)" />
+ <_SqlClientFamilyAsset Include="@(_SqlClientResolvedAsset)"
+ Condition="'%(_SqlClientResolvedAsset.NuGetPackageId)' == 'Microsoft.Data.SqlClient' Or
+ '%(_SqlClientResolvedAsset.NuGetPackageId)' == 'Microsoft.Data.SqlClient.Extensions.Abstractions' Or
+ '%(_SqlClientResolvedAsset.NuGetPackageId)' == 'Microsoft.Data.SqlClient.Extensions.Azure' Or
+ '%(_SqlClientResolvedAsset.NuGetPackageId)' == 'Microsoft.Data.SqlClient.Internal.Logging' Or
+ '%(_SqlClientResolvedAsset.NuGetPackageId)' == 'Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider'">
+ %(_SqlClientResolvedAsset.NuGetPackageVersion)
+ %(_SqlClientResolvedAsset.NuGetPackageId)
+
+ <_SqlClientLoggingVersion Include="@(_SqlClientFamilyAsset)"
+ Condition="'%(_SqlClientFamilyAsset.FamilyId)' == 'Microsoft.Data.SqlClient.Internal.Logging' And '%(_SqlClientFamilyAsset.FamilyVersion)' != ''" />
+
+
+
+ <_SqlClientExpectedFamilyVersion>@(_SqlClientLoggingVersion->'%(FamilyVersion)'->Distinct())
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.Data.SqlClient.Internal/Logging/src/Logging.csproj b/src/Microsoft.Data.SqlClient.Internal/Logging/src/Logging.csproj
index d83b8ae577..af7b725643 100644
--- a/src/Microsoft.Data.SqlClient.Internal/Logging/src/Logging.csproj
+++ b/src/Microsoft.Data.SqlClient.Internal/Logging/src/Logging.csproj
@@ -75,6 +75,8 @@
+
diff --git a/src/Microsoft.Data.SqlClient.Internal/Logging/src/PackageReadme.md b/src/Microsoft.Data.SqlClient.Internal/Logging/src/PackageReadme.md
index 3ae2b2cd55..87813b4627 100644
--- a/src/Microsoft.Data.SqlClient.Internal/Logging/src/PackageReadme.md
+++ b/src/Microsoft.Data.SqlClient.Internal/Logging/src/PackageReadme.md
@@ -22,6 +22,33 @@ This package provides **ETW EventSource tracing and diagnostics** for [Microsoft
## Supportability
+### SqlClient family version validation
+
+Logging ships a consumer MSBuild target in `build/` and `buildTransitive/`.
+NuGet imports it for direct package consumers and transitive PackageReference
+consumers. After package/reference resolution it compares installed
+`Microsoft.Data.SqlClient`, `Microsoft.Data.SqlClient.Extensions.Abstractions`,
+`Microsoft.Data.SqlClient.Extensions.Azure`, and
+`Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` package versions
+against the **resolved Logging package version**. Optional packages are not required.
+`Microsoft.SqlServer.Server` and native SNI packages are excluded.
+
+Versions must match exactly, including prerelease labels; a mismatch fails the
+build with **SQLCLIENT001**. Upgrade all installed family packages together.
+Historical extensions numbered `1.0.0` are not mapped to driver `7.0.0`/`7.0.1`.
+The check uses resolved compile/runtime asset metadata for PackageReference and
+installed versions in `packages.config` for legacy consumers. It does not check
+assembly versions, restore ranges, or API compatibility. Assets missing version
+metadata and builds without a Logging version anchor are skipped. Design-time
+builds are skipped.
+
+For emergency recovery, set `SqlClientEnforceFamilyVersions=false` in the
+consumer project or pass `-p:SqlClientEnforceFamilyVersions=false`. This disables
+only this build check; it does not make mixed versions compatible. Excluding the
+package's build assets also prevents target import. Ordinary project references
+do not propagate NuGet buildTransitive targets, so this check does not enforce
+in-tree Project-mode versions.
+
This package supports:
- .NET Standard 2.0 (compatible with .NET Framework 4.6.1+, .NET Core 2.0+, and .NET 5+)
diff --git a/src/Microsoft.Data.SqlClient.Internal/Logging/test/RunVersionChecks.ps1 b/src/Microsoft.Data.SqlClient.Internal/Logging/test/RunVersionChecks.ps1
new file mode 100644
index 0000000000..6d9e195e8f
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient.Internal/Logging/test/RunVersionChecks.ps1
@@ -0,0 +1,230 @@
+# Licensed to the .NET Foundation under one or more agreements.
+# The .NET Foundation licenses this file to you under the MIT license.
+# See the LICENSE file in the project root for more information.
+
+<#
+.SYNOPSIS
+Checks exact family versions and automatic NuGet imports using the real Logging package.
+.DESCRIPTION
+Synthetic projects cover every asset group and packages.config without a database.
+Isolated SDK consumers restore from a private fixture feed/cache, never manually import
+the target, and prove direct and transitive package imports. Other family packages are
+small test fixtures, not production builds. No ordinary project-reference propagation
+is asserted. All scratch files stay under this test directory and are removed.
+#>
+[CmdletBinding()]
+param()
+
+$ErrorActionPreference = 'Stop'
+$targets = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../build/Microsoft.Data.SqlClient.Internal.Logging.targets'))
+$loggingProject = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../src/Logging.csproj'))
+$work = Join-Path $PSScriptRoot ("version-checks-" + [guid]::NewGuid())
+$previousTmp = $env:TMPDIR
+$previousPackages = $env:NUGET_PACKAGES
+$family = @('Microsoft.Data.SqlClient.Extensions.Abstractions', 'Microsoft.Data.SqlClient',
+ 'Microsoft.Data.SqlClient.Extensions.Azure', 'Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider')
+$logging = 'Microsoft.Data.SqlClient.Internal.Logging'
+$excluded = @('Microsoft.SqlServer.Server', 'Microsoft.Data.SqlClient.SNI', 'Microsoft.Data.SqlClient.SNI.runtime')
+$script:checks = 0
+
+# Execute one command, checking both the exit code and the expected diagnostic.
+function Invoke-CheckedDotnet {
+ param([string[]] $Arguments, [bool] $Mismatch = $false, [string] $Evidence = '')
+ $output = & dotnet @Arguments 2>&1
+ $exitCode = $LASTEXITCODE
+ $text = $output -join "`n"
+ if ($Mismatch) {
+ if ($exitCode -eq 0 -or $text -notmatch 'SQLCLIENT001' -or
+ ($Evidence -and -not $text.Contains($Evidence)) -or
+ $text -match 'MSB4096|error (?!SQLCLIENT001)[A-Z]+\d+') {
+ throw "Expected only SQLCLIENT001: dotnet $($Arguments -join ' ')`n$text"
+ }
+ } elseif ($exitCode -ne 0 -or ($Evidence -and -not $text.Contains($Evidence))) {
+ throw "Command failed or evidence missing: dotnet $($Arguments -join ' ')`n$text"
+ }
+ # Keep warnings visible even when a command succeeds.
+ $output | Where-Object { "$_" -match '\bwarning\b' } | ForEach-Object { Write-Host "$_" }
+ $script:checks++
+}
+
+# Exercise the target with no SDK: these inputs isolate metadata/filtering regressions.
+function Test-Synthetic {
+ param([string] $Directory, [string] $AssetKind, [object[]] $Items,
+ [bool] $Mismatch = $false, [string[]] $Properties = @(), [string] $Evidence = '')
+ $project = Join-Path $Directory 'VersionTest.proj'
+ $config = Join-Path $Directory 'packages.config'
+ if (Test-Path $config) { Remove-Item $config }
+ $assets = ''
+ if ($AssetKind -eq 'packages.config') {
+ $entries = ($Items | ForEach-Object { "" }) -join ''
+ Set-Content $config "$entries"
+ } else {
+ $entries = ($Items | ForEach-Object {
+ "<$AssetKind Include='$($_.Id).dll'>$($_.Id)$($_.Version)$AssetKind>"
+ }) -join ''
+ # All collected item kinds can include framework/project assets with no package metadata.
+ $bare = (@('ResolvedCompileFileDefinitions', 'ReferencePath', 'RuntimeCopyLocalItems', 'RuntimeTargetsCopyLocalItems') |
+ ForEach-Object { "<$_ Include='MetadataFree-$_.dll' />" }) -join ''
+ $assets = "$bare$entries"
+ }
+ Set-Content $project "$assets"
+ Invoke-CheckedDotnet (@('msbuild', $project, '-t:ValidateSqlClientFamilyPackageVersions', '-v:quiet', '-nologo') + $Properties) $Mismatch $Evidence
+}
+
+# Pack a minimal SDK library, optionally depending on Logging, into the private feed.
+function New-FixturePackage {
+ param([string] $Id, [string] $Version, [bool] $Wrapper = $false)
+ $directory = Join-Path $work "$Id-$Version"
+ New-Item -ItemType Directory $directory | Out-Null
+ $dependency = if ($Wrapper) { "" } else { '' }
+ Set-Content (Join-Path $directory 'Fixture.csproj') @"
+
+
+ net10.0
+ $Id$Id$Version
+ SqlClient testsIsolated version-validation test fixture.
+
+ $dependency
+
+"@
+ Invoke-CheckedDotnet @('pack', (Join-Path $directory 'Fixture.csproj'), '-o', $feed,
+ '--configfile', $configFile, '-v:quiet', '-nologo')
+}
+
+# Build without an explicit target import, then verify NuGet's generated import and the version anchor.
+function Test-Consumer {
+ param([string] $Name, [bool] $Transitive, [string] $FamilyVersion,
+ [string] $Package = '', [bool] $Mismatch = $false, [string[]] $Properties = @(),
+ [bool] $RuntimeOnly = $false, [bool] $ExcludeBuild = $false)
+ $directory = Join-Path $work $Name
+ New-Item -ItemType Directory $directory | Out-Null
+ $rootId = if ($Transitive) { 'SqlClient.VersionChecks.Wrapper' } else { $logging }
+ $rootVersion = if ($Transitive) { '1.0.0' } else { $anchor }
+ $exclude = if ($ExcludeBuild) { "ExcludeAssets='build;buildTransitive'" } else { '' }
+ $references = ""
+ $installed = if ($Package) { @($Package) } else { $family }
+ foreach ($id in $installed) {
+ $runtime = if ($RuntimeOnly) { "ExcludeAssets='compile'" } else { '' }
+ $references += ""
+ }
+ foreach ($id in $excluded) {
+ $references += ""
+ }
+ $project = Join-Path $directory 'Consumer.csproj'
+ Set-Content $project @"
+
+ net10.0
+ $references
+
+
+
+
+"@
+ Invoke-CheckedDotnet @('restore', $project, '--configfile', $configFile, '-v:quiet', '-nologo')
+ $import = Get-Content (Join-Path $directory 'obj/Consumer.csproj.nuget.g.targets') -Raw
+ if ($ExcludeBuild) {
+ if ($import.Contains('Microsoft.Data.SqlClient.Internal.Logging.targets')) {
+ throw 'Excluded build assets unexpectedly imported the target.'
+ }
+ } else {
+ if (-not $import.Contains('buildTransitive/Microsoft.Data.SqlClient.Internal.Logging.targets') -and
+ -not $import.Contains('build/Microsoft.Data.SqlClient.Internal.Logging.targets')) {
+ throw "NuGet did not import Logging's packaged target for $Name."
+ }
+ if ($Transitive -and -not $import.Contains('buildTransitive/Microsoft.Data.SqlClient.Internal.Logging.targets')) {
+ throw 'Transitive consumer did not import buildTransitive.'
+ }
+ if ($Transitive) {
+ $assets = Get-Content (Join-Path $directory 'obj/project.assets.json') -Raw | ConvertFrom-Json -AsHashtable
+ if ($assets.project.frameworks['net10.0'].dependencies.ContainsKey($logging)) {
+ throw 'Transitive test accidentally references Logging directly.'
+ }
+ }
+ }
+ $evidence = if ($Mismatch) { $Package } elseif (-not $ExcludeBuild -and $Properties.Count -eq 0) { "VERSION_CHECK_ANCHOR=$anchor" } else { '' }
+ Invoke-CheckedDotnet (@('build', $project, '--no-restore', '-v:minimal', '-nologo') + $Properties) $Mismatch $evidence
+ Write-Host "PASS: consumer $Name (automatic NuGet import, expected mismatch=$Mismatch)"
+}
+
+New-Item -ItemType Directory $work | Out-Null
+try {
+ $env:TMPDIR = $work
+ $synthetic = Join-Path $work 'synthetic'
+ New-Item -ItemType Directory $synthetic | Out-Null
+ foreach ($kind in @('ResolvedCompileFileDefinitions', 'ReferencePath', 'RuntimeCopyLocalItems', 'RuntimeTargetsCopyLocalItems', 'packages.config')) {
+ foreach ($anchorVersion in @('8.0.0', '8.0.0-preview1')) {
+ foreach ($id in $family) {
+ foreach ($version in @('8.0.0', '8.0.1', '7.1.1', '8.0.0-preview1', '8.0.0-preview2', '1.0.0')) {
+ $items = @(@{ Id = $logging; Version = $anchorVersion }, @{ Id = $id; Version = $version })
+ $items += $excluded | ForEach-Object { @{ Id = $_; Version = '1.0.0' } }
+ $mismatch = $version -ne $anchorVersion
+ $evidence = if ($mismatch) { $id } else { '' }
+ Test-Synthetic $synthetic $kind $items $mismatch @() $evidence
+ if ($mismatch) {
+ Test-Synthetic $synthetic $kind $items $false @('-p:SqlClientEnforceFamilyVersions=false')
+ }
+ }
+ }
+ }
+ Test-Synthetic $synthetic $kind @(@{ Id = $logging; Version = '8.0.0' })
+ Test-Synthetic $synthetic $kind @(@{ Id = $family[0]; Version = '1.0.0' })
+ if ($kind -ne 'packages.config') {
+ Test-Synthetic $synthetic $kind @(@{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '' })
+ }
+ Test-Synthetic $synthetic $kind @(@{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '1.0.0' }) $false @('-p:DesignTimeBuild=true')
+ }
+ # Duplicate Logging assets from compile and runtime resolution must not create a false mismatch.
+ Test-Synthetic $synthetic 'ResolvedCompileFileDefinitions' @(
+ @{ Id = $logging; Version = '8.0.0' }, @{ Id = $logging; Version = '8.0.0' },
+ @{ Id = $family[0]; Version = '8.0.0' })
+ Write-Host "PASS: synthetic matrix ($script:checks commands; all asset groups, stable/prerelease, opt-out, metadata-free assets, packages.config, exclusions)."
+
+ $feed = Join-Path $work 'feed'
+ New-Item -ItemType Directory $feed | Out-Null
+ $anchor = '8.0.0-preview1'
+ # Use the real packaging project, preserving its repository props/targets and versioning.
+ Invoke-CheckedDotnet @('pack', $loggingProject, '-c', 'Release', '-o', $feed,
+ "-p:SqlClientPackageVersion=$anchor", '-v:minimal', '-nologo')
+ $packageFile = Join-Path $feed "$logging.$anchor.nupkg"
+ $archive = [IO.Compression.ZipFile]::OpenRead($packageFile)
+ try {
+ foreach ($folder in @('build', 'buildTransitive')) {
+ $entry = $archive.GetEntry("$folder/$logging.targets")
+ if (-not $entry) { throw "Missing $folder target in the real Logging package." }
+ $reader = [IO.StreamReader]::new($entry.Open())
+ try { $packed = $reader.ReadToEnd() } finally { $reader.Dispose() }
+ if ($packed -ne (Get-Content $targets -Raw)) { throw "Stale $folder target packaged." }
+ }
+ } finally { $archive.Dispose() }
+ Write-Host 'PASS: real Logging nupkg contains identical build/ and buildTransitive/ targets.'
+
+ # Stop ancestor props/targets/CPM and user package caches/feeds from affecting fixture consumers.
+ Set-Content (Join-Path $work 'Directory.Build.props') ''
+ Set-Content (Join-Path $work 'Directory.Build.targets') ''
+ Set-Content (Join-Path $work 'Directory.Packages.props') ''
+ $env:NUGET_PACKAGES = Join-Path $work 'cache'
+ $configFile = Join-Path $work 'NuGet.Config'
+ $escapedFeed = [Security.SecurityElement]::Escape($feed)
+ Set-Content $configFile ""
+ foreach ($id in $family) {
+ New-FixturePackage $id $anchor
+ New-FixturePackage $id '8.0.0-preview2'
+ }
+ foreach ($id in $excluded) { New-FixturePackage $id '1.0.0' }
+ New-FixturePackage 'SqlClient.VersionChecks.Wrapper' '1.0.0' $true
+ foreach ($transitive in @($false, $true)) {
+ $mode = if ($transitive) { 'transitive' } else { 'direct' }
+ Test-Consumer "$mode-matching" $transitive $anchor
+ foreach ($id in $family) {
+ Test-Consumer "$mode-mixed-$id" $transitive '8.0.0-preview2' $id $true
+ }
+ Test-Consumer "$mode-opt-out" $transitive '8.0.0-preview2' $family[0] $false @('-p:SqlClientEnforceFamilyVersions=false')
+ Test-Consumer "$mode-runtime-only" $transitive '8.0.0-preview2' $family[3] $true @() $true
+ }
+ Test-Consumer 'direct-excluded-build-assets' $false '8.0.0-preview2' $family[0] $false @() $false $true
+ Write-Host "PASS: $script:checks checked dotnet commands; synthetic and isolated direct/transitive NuGet consumer regressions."
+} finally {
+ $env:TMPDIR = $previousTmp
+ $env:NUGET_PACKAGES = $previousPackages
+ Remove-Item -LiteralPath $work -Recurse -Force
+}
From 5085c026ebf5bfaa56b89668718c83c5edca240b Mon Sep 17 00:00:00 2001
From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com>
Date: Tue, 13 Oct 2026 10:30:55 -0300
Subject: [PATCH 2/4] Address family-version validation feedback for PR #4815
Support project-specific legacy manifests with NuGet precedence.
Convert version checks to Pester and invoke them directly from YAML.
Run checks in PR Logging test jobs and OneBranch package validation.
Document pinned PowerShell tooling and regression coverage.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
BUILDGUIDE.md | 17 +-
.../common/steps/test-family-versions.yml | 54 +++
.../onebranch/jobs/validate-packages-job.yml | 12 +-
eng/pipelines/pr/jobs/test-logging-job.yml | 47 +++
eng/pipelines/pr/stages/test-stages.yml | 9 +
eng/pipelines/scripts/tests/README.md | 1 +
.../tests/VersionCheckPipeline.Tests.ps1 | 74 ++++
...ft.Data.SqlClient.Internal.Logging.targets | 17 +-
.../Logging/src/PackageReadme.md | 5 +-
.../Logging/test/RunVersionChecks.ps1 | 230 ------------
.../Logging/test/VersionChecks.Tests.ps1 | 349 ++++++++++++++++++
11 files changed, 573 insertions(+), 242 deletions(-)
create mode 100644 eng/pipelines/common/steps/test-family-versions.yml
create mode 100644 eng/pipelines/pr/jobs/test-logging-job.yml
create mode 100644 eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1
delete mode 100644 src/Microsoft.Data.SqlClient.Internal/Logging/test/RunVersionChecks.ps1
create mode 100644 src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1
diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md
index 0313de22a3..19a65c0309 100644
--- a/BUILDGUIDE.md
+++ b/BUILDGUIDE.md
@@ -262,7 +262,8 @@ Installed Logging, Abstractions, SqlClient, Extensions.Azure, and AKV packages
must match the resolved Logging version exactly, including prerelease labels.
Optional family packages are not required; independently versioned
Microsoft.SqlServer.Server and native SNI packages are excluded. The target uses
-resolved compile/runtime package metadata or legacy `packages.config`, not
+resolved compile/runtime package metadata or the active legacy `packages.config`
+(including `packages..config`), not
assembly versions or declared version ranges, and skips design-time builds.
Missing version metadata or a missing Logging anchor is not guessed.
@@ -274,16 +275,24 @@ project). It suppresses this check only, not incompatibilities.
Ordinary project references do not propagate NuGet buildTransitive assets:
Project-mode repository builds are not validated by this consumer target.
-Run the focused synthetic and isolated NuGet consumer regression suite with:
+Run `dotnet tool restore` first to restore the repository's pinned PowerShell tool.
+The regression suite also requires Pester 5.7.1:
```powershell
-pwsh -NoProfile -File src/Microsoft.Data.SqlClient.Internal/Logging/test/RunVersionChecks.ps1
+dotnet tool run pwsh -NoProfile -Command "Install-Module Pester -RequiredVersion 5.7.1 -Repository PSGallery -Scope CurrentUser -Force -ErrorAction Stop"
+dotnet tool run pwsh -NoProfile -Command 'Import-Module Pester -RequiredVersion 5.7.1; Invoke-Pester -Path ./src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1,./eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1 -CI'
```
-The suite packs the real Logging project, creates small family-package fixtures
+The parameterized Pester suite packs the real Logging project, creates small family-package fixtures
in a private local feed, and verifies automatic imports for both direct and
transitive package consumers. It uses a unique directory under Logging/test and
removes that directory on completion; no SQL Server is required.
+Add `-Tag Synthetic` to `Invoke-Pester` to run only the metadata and legacy manifest
+matrix, or `-Tag Pipeline` to check pipeline wiring without builds.
+The PR test stages run the suite in dedicated Logging jobs on the modern Windows
+and Linux platforms. Official/non-official OneBranch runs it in the package-validation
+job. Both invoke Pester directly through a shared YAML step and publish failure-gated
+NUnit results; no PowerShell runner script is required.
Just like building and testing the various projects in this repository, packaging the projects into NuGet packages is
also handled by `build.proj`. From the root of your repository, run `dotnet build` against `build.proj` with a pack target,
diff --git a/eng/pipelines/common/steps/test-family-versions.yml b/eng/pipelines/common/steps/test-family-versions.yml
new file mode 100644
index 0000000000..d900d7bb91
--- /dev/null
+++ b/eng/pipelines/common/steps/test-family-versions.yml
@@ -0,0 +1,54 @@
+# Licensed to the .NET Foundation under one or more agreements.
+# The .NET Foundation licenses this file to you under the MIT license.
+# See the LICENSE file in the project root for more information.
+
+# Call from a test/validation job after installing the SDK and restoring local tools.
+parameters:
+ - name: testResultsPath
+ type: string
+
+steps:
+ - script: >-
+ dotnet tool run pwsh -NoProfile -NonInteractive -Command
+ "Install-Module Pester -RequiredVersion 5.7.1 -Repository PSGallery -Scope CurrentUser -Force -ErrorAction Stop"
+ displayName: Install family-version test prerequisites
+ workingDirectory: $(Build.SourcesDirectory)
+
+ - task: PowerShell@2
+ displayName: Test installed SqlClient family versions
+ inputs:
+ targetType: inline
+ pwsh: true
+ workingDirectory: $(Build.SourcesDirectory)
+ script: |
+ # Keep the Pester configuration literal across shells; execute with the pinned local tool.
+ & dotnet tool run pwsh -NoProfile -NonInteractive -Command @'
+ $ErrorActionPreference = 'Stop'
+ Import-Module Pester -RequiredVersion 5.7.1 -ErrorAction Stop
+ $configuration = New-PesterConfiguration
+ $configuration.Run.Path = @(
+ 'src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1'
+ 'eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1'
+ )
+ $configuration.Run.Exit = $true
+ $configuration.Output.Verbosity = 'Normal'
+ # The real Logging pack needs repository imports, so the suite owns its scratch directory.
+ $configuration.TestDrive.Enabled = $false
+ $configuration.TestResult.Enabled = $true
+ $configuration.TestResult.OutputFormat = 'NUnitXml'
+ $configuration.TestResult.OutputPath = '${{ parameters.testResultsPath }}'
+ Invoke-Pester -Configuration $configuration
+ '@
+ if ($LASTEXITCODE -ne 0) {
+ throw "Family-version Pester tests failed with exit code $LASTEXITCODE."
+ }
+
+ - task: PublishTestResults@2
+ displayName: Publish family-version test results
+ condition: succeededOrFailed()
+ inputs:
+ testResultsFormat: NUnit
+ testResultsFiles: '${{ parameters.testResultsPath }}'
+ testRunTitle: SqlClient family-version validation
+ failTaskOnFailedTests: true
+ failTaskOnMissingResultsFile: true
diff --git a/eng/pipelines/onebranch/jobs/validate-packages-job.yml b/eng/pipelines/onebranch/jobs/validate-packages-job.yml
index c3891369e1..1208baeec4 100644
--- a/eng/pipelines/onebranch/jobs/validate-packages-job.yml
+++ b/eng/pipelines/onebranch/jobs/validate-packages-job.yml
@@ -85,9 +85,9 @@ jobs:
- name: ob_outputDirectory
value: '$(JOB_OUTPUT)'
- # This job inspects already-built packages and produces no assemblies, so it has nothing for
- # APIScan or BinSkim to scan and no shipping component to describe in an SBOM. The build
- # jobs cover all three for the packages they produce.
+ # This job inspects already-built packages and builds only validation tools/test fixtures,
+ # so it has no shipping assemblies for APIScan, BinSkim, or SBOM. The build jobs cover
+ # all three for the packages they produce.
- name: ob_sdl_apiscan_enabled
value: false
- name: ob_sdl_binskim_enabled
@@ -164,6 +164,12 @@ jobs:
# PackageValidator targets net10.0, which the repo's global.json already pins.
- template: /eng/pipelines/common/steps/install-dotnet.yml@self
+ - template: /eng/pipelines/common/steps/restore-dotnet-tools.yml@self
+
+ - template: /eng/pipelines/common/steps/test-family-versions.yml@self
+ parameters:
+ testResultsPath: '$(JOB_OUTPUT)/test_results/family-versions.xml'
+
- template: /eng/pipelines/onebranch/steps/validate-packages-step.yml@self
parameters:
packagesPath: '$(packagesRoot)'
diff --git a/eng/pipelines/pr/jobs/test-logging-job.yml b/eng/pipelines/pr/jobs/test-logging-job.yml
new file mode 100644
index 0000000000..e656a09636
--- /dev/null
+++ b/eng/pipelines/pr/jobs/test-logging-job.yml
@@ -0,0 +1,47 @@
+#################################################################################
+# Licensed to the .NET Foundation under one or more agreements. #
+# The .NET Foundation licenses this file to you under the MIT license. #
+# See the LICENSE file in the project root for more information. #
+#################################################################################
+
+parameters:
+ - name: platformDisplayName
+ type: string
+
+ - name: poolImage
+ type: string
+
+ - name: poolName
+ type: string
+
+ - name: testResultsArtifactBaseName
+ type: string
+
+jobs:
+ - job: "test_${{ parameters.platformDisplayName }}_logging"
+ displayName: "logging_${{ parameters.platformDisplayName }}"
+
+ variables:
+ - name: testResultsPath
+ value: "$(Build.SourcesDirectory)/test_results/${{ parameters.platformDisplayName }}_logging"
+
+ pool:
+ name: ${{ parameters.poolName }}
+ demands:
+ - imageOverride -equals ${{ parameters.poolImage }}
+
+ steps:
+ - template: /eng/pipelines/common/steps/install-dotnet.yml@self
+
+ - template: /eng/pipelines/common/steps/restore-dotnet-tools.yml@self
+
+ - template: /eng/pipelines/common/steps/test-family-versions.yml@self
+ parameters:
+ testResultsPath: '$(testResultsPath)/family-versions.xml'
+
+ - task: PublishPipelineArtifact@1
+ displayName: Publish Logging Test Artifacts
+ condition: succeededOrFailed()
+ inputs:
+ artifact: ${{ parameters.testResultsArtifactBaseName }}_$(System.JobId)_attempt$(System.JobAttempt)
+ targetPath: $(testResultsPath)
diff --git a/eng/pipelines/pr/stages/test-stages.yml b/eng/pipelines/pr/stages/test-stages.yml
index ce4c5c32f8..865e5c70aa 100644
--- a/eng/pipelines/pr/stages/test-stages.yml
+++ b/eng/pipelines/pr/stages/test-stages.yml
@@ -125,6 +125,15 @@ stages:
- ${{ parameters.stageNameSecrets }}
jobs:
+ # The consumer fixtures target net10.0, so run once per modern platform.
+ - ${{ if not(contains(platform.dotnet, 'net4')) }}:
+ - template: /eng/pipelines/pr/jobs/test-logging-job.yml@self
+ parameters:
+ platformDisplayName: ${{ platform.displayName }}
+ poolImage: ${{ platform.image }}
+ poolName: ${{ parameters.poolName }}
+ testResultsArtifactBaseName: ${{ parameters.testResultsArtifactBaseName }}
+
# TestAbstractions
- template: /eng/pipelines/pr/jobs/test-buildproj-job.yml@self
parameters:
diff --git a/eng/pipelines/scripts/tests/README.md b/eng/pipelines/scripts/tests/README.md
index 2ad3fa021e..aaa479f25c 100644
--- a/eng/pipelines/scripts/tests/README.md
+++ b/eng/pipelines/scripts/tests/README.md
@@ -32,6 +32,7 @@ Add `-Output Detailed` to see per-test results.
| `Install-DockerCli.macos.Tests.ps1` | `Install-DockerCli.macos.ps1` — Homebrew bottle selection for the macOS docker CLI. |
| `Pipeline-Arguments.Tests.ps1` | PR build/pack/test argument quoting and the repository's CI variable naming convention. |
| `Build-Orchestration.Tests.ps1` | MSBuild test filters, dependency-pack ordering/version forwarding, local restore freshness, and generated command-line argument quoting. |
+| `VersionCheckPipeline.Tests.ps1` | Family-version suite placement, direct Pester invocation, pinned tooling, and failure-gated results in PR Logging tests and OneBranch package validation. |
The localization and Docker helper tests mock `git`, `tar`, `Invoke-RestMethod`,
and `Invoke-WebRequest`, so they do not access the network or modify a real
diff --git a/eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1 b/eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1
new file mode 100644
index 0000000000..46b9c01fae
--- /dev/null
+++ b/eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1
@@ -0,0 +1,74 @@
+# Licensed to the .NET Foundation under one or more agreements.
+# The .NET Foundation licenses this file to you under the MIT license.
+# See the LICENSE file in the project root for more information.
+
+BeforeAll {
+ $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../../..')).Path
+ $stepPath = '/eng/pipelines/common/steps/test-family-versions.yml@self'
+}
+
+Describe 'Family-version pipeline wiring' -Tag 'Pipeline' {
+ It ' runs the shared suite after installing the SDK and restoring tools' -ForEach @(
+ @{ Path = 'eng/pipelines/pr/jobs/test-logging-job.yml' }
+ @{ Path = 'eng/pipelines/onebranch/jobs/validate-packages-job.yml' }
+ ) {
+ # Arrange: inspect the actual callers, not a duplicated list in a test fixture.
+ $content = Get-Content -LiteralPath (Join-Path $repoRoot $Path) -Raw
+ $restoreIndex = $content.IndexOf('/eng/pipelines/common/steps/restore-dotnet-tools.yml@self')
+ $installIndex = $content.IndexOf('/eng/pipelines/common/steps/install-dotnet.yml@self')
+ $suiteIndex = $content.IndexOf($stepPath)
+
+ $installIndex | Should -BeGreaterOrEqual 0
+ $restoreIndex | Should -BeGreaterThan $installIndex
+ $suiteIndex | Should -BeGreaterThan $restoreIndex
+ [regex]::Matches($content, [regex]::Escape($stepPath)).Count | Should -Be 1
+ $content | Should -Match 'testResultsPath:'
+ }
+
+ It 'adds dedicated Logging jobs to the modern PR test platforms' {
+ $content = Get-Content (Join-Path $repoRoot 'eng/pipelines/pr/stages/test-stages.yml') -Raw
+ $content | Should -Match ('(?m)^ - \$\{\{ if not\(contains\(platform\.dotnet, ''net4''\)\) \}\}:\r?\n' +
+ ' - template: /eng/pipelines/pr/jobs/test-logging-job\.yml@self')
+ $content | Should -Match 'platformDisplayName: \$\{\{ platform\.displayName \}\}'
+ $content | Should -Match 'poolImage: \$\{\{ platform\.image \}\}'
+ }
+
+ It 'keeps regression fixtures out of production build and pack jobs' -ForEach @(
+ @{ Path = 'eng/pipelines/pr/stages/pack-stage.yml' }
+ @{ Path = 'eng/pipelines/onebranch/jobs/build-buildproj-job.yml' }
+ ) {
+ $content = Get-Content (Join-Path $repoRoot $Path) -Raw
+ $content | Should -Not -Match ([regex]::Escape($stepPath))
+ }
+
+ It 'uses the pinned local PowerShell tool for prerequisites and tests' {
+ $content = Get-Content (Join-Path $repoRoot 'eng/pipelines/common/steps/test-family-versions.yml') -Raw
+ [regex]::Matches($content, 'dotnet tool run pwsh').Count | Should -Be 2
+ $content | Should -Match 'Install-Module Pester -RequiredVersion 5\.7\.1'
+ $content | Should -Match 'Invoke-Pester -Configuration \$configuration'
+ $content | Should -Match '\$configuration\.Run\.Exit = \$true'
+ $content | Should -Match '\$configuration\.TestDrive\.Enabled = \$false'
+ $content | Should -Match '\$configuration\.TestResult\.OutputFormat = ''NUnitXml'''
+ $content | Should -Match 'VersionChecks\.Tests\.ps1'
+ $content | Should -Match 'VersionCheckPipeline\.Tests\.ps1'
+ $content | Should -Not -Match 'RunVersionChecks\.ps1'
+ $content | Should -Not -Match 'continueOnError: true'
+ }
+
+ It 'publishes PR Logging result artifacts with unique job/attempt names' {
+ $content = Get-Content (Join-Path $repoRoot 'eng/pipelines/pr/jobs/test-logging-job.yml') -Raw
+ $content | Should -Match 'PublishPipelineArtifact@1'
+ $content | Should -Match '\$\(System\.JobId\)_attempt\$\(System\.JobAttempt\)'
+ $content | Should -Match 'condition: succeededOrFailed\(\)'
+ $content | Should -Match '\$\{\{ parameters\.platformDisplayName \}\}_logging'
+ }
+
+ It 'publishes NUnit results even on test failure and gates missing or failed results' {
+ $content = Get-Content (Join-Path $repoRoot 'eng/pipelines/common/steps/test-family-versions.yml') -Raw
+ $content | Should -Match 'PublishTestResults@2'
+ $content | Should -Match 'condition: succeededOrFailed\(\)'
+ $content | Should -Match 'testResultsFormat: NUnit'
+ $content | Should -Match 'failTaskOnFailedTests: true'
+ $content | Should -Match 'failTaskOnMissingResultsFile: true'
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient.Internal/Logging/build/Microsoft.Data.SqlClient.Internal.Logging.targets b/src/Microsoft.Data.SqlClient.Internal/Logging/build/Microsoft.Data.SqlClient.Internal.Logging.targets
index 776947df5a..5d372bad64 100644
--- a/src/Microsoft.Data.SqlClient.Internal/Logging/build/Microsoft.Data.SqlClient.Internal.Logging.targets
+++ b/src/Microsoft.Data.SqlClient.Internal/Logging/build/Microsoft.Data.SqlClient.Internal.Logging.targets
@@ -1,3 +1,7 @@
+
-
+
+ <_SqlClientConfigPath>$(MSBuildProjectDirectory)/packages.$(MSBuildProjectName.Replace(' ', '_')).config
+ <_SqlClientConfigPath Condition="!Exists('$(_SqlClientConfigPath)')">$(MSBuildProjectDirectory)/packages.config
+
+
+ Condition="Exists('$(_SqlClientConfigPath)')">
-
+ Text="SqlClient family version mismatch in $(_SqlClientConfigPath): %(_SqlClientConfigMismatch.Identity), but Logging is $(_SqlClientConfigExpectedVersion). Upgrade all installed family packages together, including prerelease labels. Emergency opt-out: SqlClientEnforceFamilyVersions=false." />
diff --git a/src/Microsoft.Data.SqlClient.Internal/Logging/src/PackageReadme.md b/src/Microsoft.Data.SqlClient.Internal/Logging/src/PackageReadme.md
index 87813b4627..a561bae028 100644
--- a/src/Microsoft.Data.SqlClient.Internal/Logging/src/PackageReadme.md
+++ b/src/Microsoft.Data.SqlClient.Internal/Logging/src/PackageReadme.md
@@ -37,7 +37,10 @@ Versions must match exactly, including prerelease labels; a mismatch fails the
build with **SQLCLIENT001**. Upgrade all installed family packages together.
Historical extensions numbered `1.0.0` are not mapped to driver `7.0.0`/`7.0.1`.
The check uses resolved compile/runtime asset metadata for PackageReference and
-installed versions in `packages.config` for legacy consumers. It does not check
+installed versions in the active `packages.config` for legacy consumers. A
+`packages..config` file takes precedence over `packages.config`;
+spaces in the project name are replaced with underscores, matching NuGet.
+It does not check
assembly versions, restore ranges, or API compatibility. Assets missing version
metadata and builds without a Logging version anchor are skipped. Design-time
builds are skipped.
diff --git a/src/Microsoft.Data.SqlClient.Internal/Logging/test/RunVersionChecks.ps1 b/src/Microsoft.Data.SqlClient.Internal/Logging/test/RunVersionChecks.ps1
deleted file mode 100644
index 6d9e195e8f..0000000000
--- a/src/Microsoft.Data.SqlClient.Internal/Logging/test/RunVersionChecks.ps1
+++ /dev/null
@@ -1,230 +0,0 @@
-# Licensed to the .NET Foundation under one or more agreements.
-# The .NET Foundation licenses this file to you under the MIT license.
-# See the LICENSE file in the project root for more information.
-
-<#
-.SYNOPSIS
-Checks exact family versions and automatic NuGet imports using the real Logging package.
-.DESCRIPTION
-Synthetic projects cover every asset group and packages.config without a database.
-Isolated SDK consumers restore from a private fixture feed/cache, never manually import
-the target, and prove direct and transitive package imports. Other family packages are
-small test fixtures, not production builds. No ordinary project-reference propagation
-is asserted. All scratch files stay under this test directory and are removed.
-#>
-[CmdletBinding()]
-param()
-
-$ErrorActionPreference = 'Stop'
-$targets = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../build/Microsoft.Data.SqlClient.Internal.Logging.targets'))
-$loggingProject = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../src/Logging.csproj'))
-$work = Join-Path $PSScriptRoot ("version-checks-" + [guid]::NewGuid())
-$previousTmp = $env:TMPDIR
-$previousPackages = $env:NUGET_PACKAGES
-$family = @('Microsoft.Data.SqlClient.Extensions.Abstractions', 'Microsoft.Data.SqlClient',
- 'Microsoft.Data.SqlClient.Extensions.Azure', 'Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider')
-$logging = 'Microsoft.Data.SqlClient.Internal.Logging'
-$excluded = @('Microsoft.SqlServer.Server', 'Microsoft.Data.SqlClient.SNI', 'Microsoft.Data.SqlClient.SNI.runtime')
-$script:checks = 0
-
-# Execute one command, checking both the exit code and the expected diagnostic.
-function Invoke-CheckedDotnet {
- param([string[]] $Arguments, [bool] $Mismatch = $false, [string] $Evidence = '')
- $output = & dotnet @Arguments 2>&1
- $exitCode = $LASTEXITCODE
- $text = $output -join "`n"
- if ($Mismatch) {
- if ($exitCode -eq 0 -or $text -notmatch 'SQLCLIENT001' -or
- ($Evidence -and -not $text.Contains($Evidence)) -or
- $text -match 'MSB4096|error (?!SQLCLIENT001)[A-Z]+\d+') {
- throw "Expected only SQLCLIENT001: dotnet $($Arguments -join ' ')`n$text"
- }
- } elseif ($exitCode -ne 0 -or ($Evidence -and -not $text.Contains($Evidence))) {
- throw "Command failed or evidence missing: dotnet $($Arguments -join ' ')`n$text"
- }
- # Keep warnings visible even when a command succeeds.
- $output | Where-Object { "$_" -match '\bwarning\b' } | ForEach-Object { Write-Host "$_" }
- $script:checks++
-}
-
-# Exercise the target with no SDK: these inputs isolate metadata/filtering regressions.
-function Test-Synthetic {
- param([string] $Directory, [string] $AssetKind, [object[]] $Items,
- [bool] $Mismatch = $false, [string[]] $Properties = @(), [string] $Evidence = '')
- $project = Join-Path $Directory 'VersionTest.proj'
- $config = Join-Path $Directory 'packages.config'
- if (Test-Path $config) { Remove-Item $config }
- $assets = ''
- if ($AssetKind -eq 'packages.config') {
- $entries = ($Items | ForEach-Object { "" }) -join ''
- Set-Content $config "$entries"
- } else {
- $entries = ($Items | ForEach-Object {
- "<$AssetKind Include='$($_.Id).dll'>$($_.Id)$($_.Version)$AssetKind>"
- }) -join ''
- # All collected item kinds can include framework/project assets with no package metadata.
- $bare = (@('ResolvedCompileFileDefinitions', 'ReferencePath', 'RuntimeCopyLocalItems', 'RuntimeTargetsCopyLocalItems') |
- ForEach-Object { "<$_ Include='MetadataFree-$_.dll' />" }) -join ''
- $assets = "$bare$entries"
- }
- Set-Content $project "$assets"
- Invoke-CheckedDotnet (@('msbuild', $project, '-t:ValidateSqlClientFamilyPackageVersions', '-v:quiet', '-nologo') + $Properties) $Mismatch $Evidence
-}
-
-# Pack a minimal SDK library, optionally depending on Logging, into the private feed.
-function New-FixturePackage {
- param([string] $Id, [string] $Version, [bool] $Wrapper = $false)
- $directory = Join-Path $work "$Id-$Version"
- New-Item -ItemType Directory $directory | Out-Null
- $dependency = if ($Wrapper) { "" } else { '' }
- Set-Content (Join-Path $directory 'Fixture.csproj') @"
-
-
- net10.0
- $Id$Id$Version
- SqlClient testsIsolated version-validation test fixture.
-
- $dependency
-
-"@
- Invoke-CheckedDotnet @('pack', (Join-Path $directory 'Fixture.csproj'), '-o', $feed,
- '--configfile', $configFile, '-v:quiet', '-nologo')
-}
-
-# Build without an explicit target import, then verify NuGet's generated import and the version anchor.
-function Test-Consumer {
- param([string] $Name, [bool] $Transitive, [string] $FamilyVersion,
- [string] $Package = '', [bool] $Mismatch = $false, [string[]] $Properties = @(),
- [bool] $RuntimeOnly = $false, [bool] $ExcludeBuild = $false)
- $directory = Join-Path $work $Name
- New-Item -ItemType Directory $directory | Out-Null
- $rootId = if ($Transitive) { 'SqlClient.VersionChecks.Wrapper' } else { $logging }
- $rootVersion = if ($Transitive) { '1.0.0' } else { $anchor }
- $exclude = if ($ExcludeBuild) { "ExcludeAssets='build;buildTransitive'" } else { '' }
- $references = ""
- $installed = if ($Package) { @($Package) } else { $family }
- foreach ($id in $installed) {
- $runtime = if ($RuntimeOnly) { "ExcludeAssets='compile'" } else { '' }
- $references += ""
- }
- foreach ($id in $excluded) {
- $references += ""
- }
- $project = Join-Path $directory 'Consumer.csproj'
- Set-Content $project @"
-
- net10.0
- $references
-
-
-
-
-"@
- Invoke-CheckedDotnet @('restore', $project, '--configfile', $configFile, '-v:quiet', '-nologo')
- $import = Get-Content (Join-Path $directory 'obj/Consumer.csproj.nuget.g.targets') -Raw
- if ($ExcludeBuild) {
- if ($import.Contains('Microsoft.Data.SqlClient.Internal.Logging.targets')) {
- throw 'Excluded build assets unexpectedly imported the target.'
- }
- } else {
- if (-not $import.Contains('buildTransitive/Microsoft.Data.SqlClient.Internal.Logging.targets') -and
- -not $import.Contains('build/Microsoft.Data.SqlClient.Internal.Logging.targets')) {
- throw "NuGet did not import Logging's packaged target for $Name."
- }
- if ($Transitive -and -not $import.Contains('buildTransitive/Microsoft.Data.SqlClient.Internal.Logging.targets')) {
- throw 'Transitive consumer did not import buildTransitive.'
- }
- if ($Transitive) {
- $assets = Get-Content (Join-Path $directory 'obj/project.assets.json') -Raw | ConvertFrom-Json -AsHashtable
- if ($assets.project.frameworks['net10.0'].dependencies.ContainsKey($logging)) {
- throw 'Transitive test accidentally references Logging directly.'
- }
- }
- }
- $evidence = if ($Mismatch) { $Package } elseif (-not $ExcludeBuild -and $Properties.Count -eq 0) { "VERSION_CHECK_ANCHOR=$anchor" } else { '' }
- Invoke-CheckedDotnet (@('build', $project, '--no-restore', '-v:minimal', '-nologo') + $Properties) $Mismatch $evidence
- Write-Host "PASS: consumer $Name (automatic NuGet import, expected mismatch=$Mismatch)"
-}
-
-New-Item -ItemType Directory $work | Out-Null
-try {
- $env:TMPDIR = $work
- $synthetic = Join-Path $work 'synthetic'
- New-Item -ItemType Directory $synthetic | Out-Null
- foreach ($kind in @('ResolvedCompileFileDefinitions', 'ReferencePath', 'RuntimeCopyLocalItems', 'RuntimeTargetsCopyLocalItems', 'packages.config')) {
- foreach ($anchorVersion in @('8.0.0', '8.0.0-preview1')) {
- foreach ($id in $family) {
- foreach ($version in @('8.0.0', '8.0.1', '7.1.1', '8.0.0-preview1', '8.0.0-preview2', '1.0.0')) {
- $items = @(@{ Id = $logging; Version = $anchorVersion }, @{ Id = $id; Version = $version })
- $items += $excluded | ForEach-Object { @{ Id = $_; Version = '1.0.0' } }
- $mismatch = $version -ne $anchorVersion
- $evidence = if ($mismatch) { $id } else { '' }
- Test-Synthetic $synthetic $kind $items $mismatch @() $evidence
- if ($mismatch) {
- Test-Synthetic $synthetic $kind $items $false @('-p:SqlClientEnforceFamilyVersions=false')
- }
- }
- }
- }
- Test-Synthetic $synthetic $kind @(@{ Id = $logging; Version = '8.0.0' })
- Test-Synthetic $synthetic $kind @(@{ Id = $family[0]; Version = '1.0.0' })
- if ($kind -ne 'packages.config') {
- Test-Synthetic $synthetic $kind @(@{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '' })
- }
- Test-Synthetic $synthetic $kind @(@{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '1.0.0' }) $false @('-p:DesignTimeBuild=true')
- }
- # Duplicate Logging assets from compile and runtime resolution must not create a false mismatch.
- Test-Synthetic $synthetic 'ResolvedCompileFileDefinitions' @(
- @{ Id = $logging; Version = '8.0.0' }, @{ Id = $logging; Version = '8.0.0' },
- @{ Id = $family[0]; Version = '8.0.0' })
- Write-Host "PASS: synthetic matrix ($script:checks commands; all asset groups, stable/prerelease, opt-out, metadata-free assets, packages.config, exclusions)."
-
- $feed = Join-Path $work 'feed'
- New-Item -ItemType Directory $feed | Out-Null
- $anchor = '8.0.0-preview1'
- # Use the real packaging project, preserving its repository props/targets and versioning.
- Invoke-CheckedDotnet @('pack', $loggingProject, '-c', 'Release', '-o', $feed,
- "-p:SqlClientPackageVersion=$anchor", '-v:minimal', '-nologo')
- $packageFile = Join-Path $feed "$logging.$anchor.nupkg"
- $archive = [IO.Compression.ZipFile]::OpenRead($packageFile)
- try {
- foreach ($folder in @('build', 'buildTransitive')) {
- $entry = $archive.GetEntry("$folder/$logging.targets")
- if (-not $entry) { throw "Missing $folder target in the real Logging package." }
- $reader = [IO.StreamReader]::new($entry.Open())
- try { $packed = $reader.ReadToEnd() } finally { $reader.Dispose() }
- if ($packed -ne (Get-Content $targets -Raw)) { throw "Stale $folder target packaged." }
- }
- } finally { $archive.Dispose() }
- Write-Host 'PASS: real Logging nupkg contains identical build/ and buildTransitive/ targets.'
-
- # Stop ancestor props/targets/CPM and user package caches/feeds from affecting fixture consumers.
- Set-Content (Join-Path $work 'Directory.Build.props') ''
- Set-Content (Join-Path $work 'Directory.Build.targets') ''
- Set-Content (Join-Path $work 'Directory.Packages.props') ''
- $env:NUGET_PACKAGES = Join-Path $work 'cache'
- $configFile = Join-Path $work 'NuGet.Config'
- $escapedFeed = [Security.SecurityElement]::Escape($feed)
- Set-Content $configFile ""
- foreach ($id in $family) {
- New-FixturePackage $id $anchor
- New-FixturePackage $id '8.0.0-preview2'
- }
- foreach ($id in $excluded) { New-FixturePackage $id '1.0.0' }
- New-FixturePackage 'SqlClient.VersionChecks.Wrapper' '1.0.0' $true
- foreach ($transitive in @($false, $true)) {
- $mode = if ($transitive) { 'transitive' } else { 'direct' }
- Test-Consumer "$mode-matching" $transitive $anchor
- foreach ($id in $family) {
- Test-Consumer "$mode-mixed-$id" $transitive '8.0.0-preview2' $id $true
- }
- Test-Consumer "$mode-opt-out" $transitive '8.0.0-preview2' $family[0] $false @('-p:SqlClientEnforceFamilyVersions=false')
- Test-Consumer "$mode-runtime-only" $transitive '8.0.0-preview2' $family[3] $true @() $true
- }
- Test-Consumer 'direct-excluded-build-assets' $false '8.0.0-preview2' $family[0] $false @() $false $true
- Write-Host "PASS: $script:checks checked dotnet commands; synthetic and isolated direct/transitive NuGet consumer regressions."
-} finally {
- $env:TMPDIR = $previousTmp
- $env:NUGET_PACKAGES = $previousPackages
- Remove-Item -LiteralPath $work -Recurse -Force
-}
diff --git a/src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1 b/src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1
new file mode 100644
index 0000000000..ff91893ada
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1
@@ -0,0 +1,349 @@
+# Licensed to the .NET Foundation under one or more agreements.
+# The .NET Foundation licenses this file to you under the MIT license.
+# See the LICENSE file in the project root for more information.
+
+<#
+.SYNOPSIS
+Checks exact family versions and automatic NuGet imports using the real Logging package.
+.DESCRIPTION
+Synthetic projects cover every asset group and packages.config without a database.
+Isolated SDK consumers restore from a private fixture feed/cache, never manually import
+the target, and prove direct and transitive package imports. Other family packages are
+small test fixtures, not production builds. No ordinary project-reference propagation
+is asserted. All scratch files stay under this test directory and are removed.
+#>
+BeforeDiscovery {
+ # Expand the matrix during discovery so each contract has a named Pester result.
+ $family = @('Microsoft.Data.SqlClient.Extensions.Abstractions', 'Microsoft.Data.SqlClient',
+ 'Microsoft.Data.SqlClient.Extensions.Azure', 'Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider')
+ $syntheticCases = @(
+ foreach ($kind in @('ResolvedCompileFileDefinitions', 'ReferencePath', 'RuntimeCopyLocalItems',
+ 'RuntimeTargetsCopyLocalItems', 'packages.config', 'packages.VersionTest.config')) {
+ foreach ($anchorVersion in @('8.0.0', '8.0.0-preview1')) {
+ foreach ($id in $family) {
+ foreach ($version in @('8.0.0', '8.0.1', '7.1.1', '8.0.0-preview1', '8.0.0-preview2', '1.0.0')) {
+ @{
+ Kind = $kind; AnchorVersion = $anchorVersion; Id = $id; Version = $version
+ OptOut = $false
+ }
+ if ($version -ne $anchorVersion) {
+ @{
+ Kind = $kind; AnchorVersion = $anchorVersion; Id = $id; Version = $version
+ OptOut = $true
+ }
+ }
+ }
+ }
+ }
+ }
+ )
+ $assetKinds = @(
+ @{ Kind = 'ResolvedCompileFileDefinitions' }
+ @{ Kind = 'ReferencePath' }
+ @{ Kind = 'RuntimeCopyLocalItems' }
+ @{ Kind = 'RuntimeTargetsCopyLocalItems' }
+ @{ Kind = 'packages.config' }
+ @{ Kind = 'packages.VersionTest.config' }
+ )
+ $consumerCases = @(
+ foreach ($transitive in @($false, $true)) {
+ $mode = if ($transitive) { 'transitive' } else { 'direct' }
+ @{ Name = "$mode-matching"; Transitive = $transitive; Version = '8.0.0-preview1'; Package = ''; Mismatch = $false; Properties = @(); RuntimeOnly = $false; ExcludeBuild = $false }
+ foreach ($id in $family) {
+ @{ Name = "$mode-mixed-$id"; Transitive = $transitive; Version = '8.0.0-preview2'; Package = $id; Mismatch = $true; Properties = @(); RuntimeOnly = $false; ExcludeBuild = $false }
+ }
+ @{ Name = "$mode-opt-out"; Transitive = $transitive; Version = '8.0.0-preview2'; Package = $family[0]; Mismatch = $false; Properties = @('-p:SqlClientEnforceFamilyVersions=false'); RuntimeOnly = $false; ExcludeBuild = $false }
+ @{ Name = "$mode-runtime-only"; Transitive = $transitive; Version = '8.0.0-preview2'; Package = $family[3]; Mismatch = $true; Properties = @(); RuntimeOnly = $true; ExcludeBuild = $false }
+ }
+ @{ Name = 'direct-excluded-build-assets'; Transitive = $false; Version = '8.0.0-preview2'; Package = $family[0]; Mismatch = $false; Properties = @(); RuntimeOnly = $false; ExcludeBuild = $true }
+ )
+}
+
+BeforeAll {
+ $ErrorActionPreference = 'Stop'
+ $targets = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../build/Microsoft.Data.SqlClient.Internal.Logging.targets'))
+ $loggingProject = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../src/Logging.csproj'))
+ $work = Join-Path $PSScriptRoot ("version-checks-" + [guid]::NewGuid())
+ $previousTmp = $env:TMPDIR
+ $previousPackages = $env:NUGET_PACKAGES
+ $family = @('Microsoft.Data.SqlClient.Extensions.Abstractions', 'Microsoft.Data.SqlClient',
+ 'Microsoft.Data.SqlClient.Extensions.Azure', 'Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider')
+ $logging = 'Microsoft.Data.SqlClient.Internal.Logging'
+ $excluded = @('Microsoft.SqlServer.Server', 'Microsoft.Data.SqlClient.SNI', 'Microsoft.Data.SqlClient.SNI.runtime')
+
+ # Execute one command, checking both the exit code and the expected diagnostic.
+ function Invoke-CheckedDotnet {
+ param([string[]] $Arguments, [bool] $Mismatch = $false, [string] $Evidence = '')
+ # Do not leave reusable build workers writing fixture outputs after cleanup.
+ if ($Arguments[0] -in @('pack', 'build', 'restore')) {
+ $Arguments += '--disable-build-servers'
+ } else {
+ $Arguments += '-nodeReuse:false'
+ }
+ $output = & dotnet @Arguments 2>&1
+ $exitCode = $LASTEXITCODE
+ $text = $output -join "`n"
+ if ($Mismatch) {
+ if ($exitCode -eq 0 -or $text -notmatch 'SQLCLIENT001' -or
+ ($Evidence -and -not $text.Contains($Evidence)) -or
+ $text -match 'MSB4096|error (?!SQLCLIENT001)[A-Z]+\d+') {
+ throw "Expected only SQLCLIENT001: dotnet $($Arguments -join ' ')`n$text"
+ }
+ } elseif ($exitCode -ne 0 -or ($Evidence -and -not $text.Contains($Evidence))) {
+ throw "Command failed or evidence missing: dotnet $($Arguments -join ' ')`n$text"
+ }
+ # Keep warnings visible even when a command succeeds.
+ $output | Where-Object { "$_" -match '\bwarning\b' } | ForEach-Object { Write-Host "$_" }
+ }
+
+ # Exercise the target with no SDK: these inputs isolate metadata/filtering regressions.
+ function Test-Synthetic {
+ param([string] $Directory, [string] $AssetKind, [object[]] $Items,
+ [bool] $Mismatch = $false, [string[]] $Properties = @(), [string] $Evidence = '',
+ [string] $ProjectName = 'VersionTest', [object[]] $FallbackItems = @())
+ # Arrange: a fresh project prevents one manifest or asset group leaking into the next case.
+ $project = Join-Path $Directory "$ProjectName.proj"
+ foreach ($name in @('packages.config', 'packages.VersionTest.config', 'packages.Version_Test.config')) {
+ $config = Join-Path $Directory $name
+ if (Test-Path $config) { Remove-Item -LiteralPath $config }
+ }
+ $assets = ''
+ if ($AssetKind -like 'packages*.config') {
+ $entries = ($Items | ForEach-Object { "" }) -join ''
+ Set-Content (Join-Path $Directory $AssetKind) "$entries"
+ if ($FallbackItems.Count -gt 0) {
+ $entries = ($FallbackItems | ForEach-Object { "" }) -join ''
+ Set-Content (Join-Path $Directory 'packages.config') "$entries"
+ }
+ } else {
+ $entries = ($Items | ForEach-Object {
+ "<$AssetKind Include='$($_.Id).dll'>$($_.Id)$($_.Version)$AssetKind>"
+ }) -join ''
+ # All collected item kinds can include framework/project assets with no package metadata.
+ $bare = (@('ResolvedCompileFileDefinitions', 'ReferencePath', 'RuntimeCopyLocalItems', 'RuntimeTargetsCopyLocalItems') |
+ ForEach-Object { "<$_ Include='MetadataFree-$_.dll' />" }) -join ''
+ $assets = "$bare$entries"
+ }
+ Set-Content $project "$assets"
+ # Act/assert: run the real shipped target and require the expected diagnostic, not just failure.
+ Invoke-CheckedDotnet (@('msbuild', $project, '-t:ValidateSqlClientFamilyPackageVersions', '-v:quiet', '-nologo') + $Properties) $Mismatch $Evidence
+ }
+
+ # Pack a minimal SDK library, optionally depending on Logging, into the private feed.
+ function New-FixturePackage {
+ param([string] $Id, [string] $Version, [bool] $Wrapper = $false)
+ $directory = Join-Path $work "$Id-$Version"
+ New-Item -ItemType Directory $directory | Out-Null
+ $dependency = if ($Wrapper) { "" } else { '' }
+ Set-Content (Join-Path $directory 'Fixture.csproj') @"
+
+
+ net10.0
+ $Id$Id$Version
+ SqlClient testsIsolated version-validation test fixture.
+
+ $dependency
+
+"@
+ Invoke-CheckedDotnet @('pack', (Join-Path $directory 'Fixture.csproj'), '-o', $feed,
+ '--configfile', $configFile, '-v:quiet', '-nologo')
+ }
+
+ # Build without an explicit target import, then verify NuGet's generated import and the version anchor.
+ function Test-Consumer {
+ param([string] $Name, [bool] $Transitive, [string] $FamilyVersion,
+ [string] $Package = '', [bool] $Mismatch = $false, [string[]] $Properties = @(),
+ [bool] $RuntimeOnly = $false, [bool] $ExcludeBuild = $false)
+ $directory = Join-Path $work $Name
+ New-Item -ItemType Directory $directory | Out-Null
+ $rootId = if ($Transitive) { 'SqlClient.VersionChecks.Wrapper' } else { $logging }
+ $rootVersion = if ($Transitive) { '1.0.0' } else { $anchor }
+ $exclude = if ($ExcludeBuild) { "ExcludeAssets='build;buildTransitive'" } else { '' }
+ $references = ""
+ $installed = if ($Package) { @($Package) } else { $family }
+ foreach ($id in $installed) {
+ $runtime = if ($RuntimeOnly) { "ExcludeAssets='compile'" } else { '' }
+ $references += ""
+ }
+ foreach ($id in $excluded) {
+ $references += ""
+ }
+ $project = Join-Path $directory 'Consumer.csproj'
+ Set-Content $project @"
+
+ net10.0
+ $references
+
+
+
+
+"@
+ # Act: restore normally so NuGet, rather than the test, imports the shipped target.
+ Invoke-CheckedDotnet @('restore', $project, '--configfile', $configFile, '-v:quiet', '-nologo')
+ $import = (Get-Content (Join-Path $directory 'obj/Consumer.csproj.nuget.g.targets') -Raw) -replace '\\', '/'
+ if ($ExcludeBuild) {
+ if ($import.Contains('Microsoft.Data.SqlClient.Internal.Logging.targets')) {
+ throw 'Excluded build assets unexpectedly imported the target.'
+ }
+ } else {
+ if (-not $import.Contains('buildTransitive/Microsoft.Data.SqlClient.Internal.Logging.targets') -and
+ -not $import.Contains('build/Microsoft.Data.SqlClient.Internal.Logging.targets')) {
+ throw "NuGet did not import Logging's packaged target for $Name."
+ }
+ if ($Transitive -and -not $import.Contains('buildTransitive/Microsoft.Data.SqlClient.Internal.Logging.targets')) {
+ throw 'Transitive consumer did not import buildTransitive.'
+ }
+ if ($Transitive) {
+ $assets = Get-Content (Join-Path $directory 'obj/project.assets.json') -Raw | ConvertFrom-Json -AsHashtable
+ if ($assets.project.frameworks['net10.0'].dependencies.ContainsKey($logging)) {
+ throw 'Transitive test accidentally references Logging directly.'
+ }
+ }
+ }
+ # Assert both automatic import and the resolved anchor; an arbitrary build error is not success.
+ $evidence = if ($Mismatch) { $Package } elseif (-not $ExcludeBuild -and $Properties.Count -eq 0) { "VERSION_CHECK_ANCHOR=$anchor" } else { '' }
+ Invoke-CheckedDotnet (@('build', $project, '--no-restore', '-v:minimal', '-nologo') + $Properties) $Mismatch $evidence
+ Write-Host "PASS: consumer $Name (automatic NuGet import, expected mismatch=$Mismatch)"
+ }
+
+ New-Item -ItemType Directory $work | Out-Null
+ $env:TMPDIR = $work
+ $synthetic = Join-Path $work 'synthetic'
+ New-Item -ItemType Directory $synthetic | Out-Null
+}
+
+AfterAll {
+ $env:TMPDIR = $previousTmp
+ $env:NUGET_PACKAGES = $previousPackages
+ if ($work -and (Test-Path -LiteralPath $work)) {
+ Remove-Item -LiteralPath $work -Recurse -Force
+ }
+}
+
+Describe 'Synthetic family-version validation' -Tag 'Synthetic' {
+ It ': against Logging (opt-out: )' -ForEach $syntheticCases {
+ $items = @(@{ Id = $logging; Version = $AnchorVersion }, @{ Id = $Id; Version = $Version })
+ $items += $excluded | ForEach-Object { @{ Id = $_; Version = '1.0.0' } }
+ $mismatch = $Version -ne $AnchorVersion -and -not $OptOut
+ $properties = if ($OptOut) { @('-p:SqlClientEnforceFamilyVersions=false') } else { @() }
+ $evidence = if ($mismatch) { $Id } else { '' }
+ Test-Synthetic $synthetic $Kind $items $mismatch $properties $evidence
+ }
+
+ It ': does not require optional family packages' -ForEach $assetKinds {
+ Test-Synthetic $synthetic $Kind @(@{ Id = $logging; Version = '8.0.0' })
+ }
+
+ It ': does not guess a missing Logging anchor' -ForEach $assetKinds {
+ Test-Synthetic $synthetic $Kind @(@{ Id = $family[0]; Version = '1.0.0' })
+ }
+
+ It ': skips design-time builds' -ForEach $assetKinds {
+ Test-Synthetic $synthetic $Kind @(
+ @{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '1.0.0' }
+ ) $false @('-p:DesignTimeBuild=true')
+ }
+
+ It ': does not guess missing asset version metadata' -ForEach @(
+ @{ Kind = 'ResolvedCompileFileDefinitions' }
+ @{ Kind = 'ReferencePath' }
+ @{ Kind = 'RuntimeCopyLocalItems' }
+ @{ Kind = 'RuntimeTargetsCopyLocalItems' }
+ ) {
+ Test-Synthetic $synthetic $Kind @(
+ @{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '' })
+ }
+
+ # Duplicate Logging assets from compile and runtime resolution must not create a false mismatch.
+ It 'accepts duplicate matching Logging assets' {
+ Test-Synthetic $synthetic 'ResolvedCompileFileDefinitions' @(
+ @{ Id = $logging; Version = '8.0.0' }, @{ Id = $logging; Version = '8.0.0' },
+ @{ Id = $family[0]; Version = '8.0.0' })
+ }
+
+ It 'skips a consumer with neither manifest nor package metadata' {
+ Test-Synthetic $synthetic 'ReferencePath' @()
+ }
+}
+
+Describe 'Project-specific legacy manifests' -Tag 'Synthetic', 'LegacyManifest' {
+ It 'detects mismatches without a fallback packages.config' {
+ Test-Synthetic $synthetic 'packages.VersionTest.config' @(
+ @{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '8.0.1' }
+ ) $true @() $family[0]
+ }
+
+ It 'prefers a mismatching project manifest over a matching fallback' {
+ Test-Synthetic $synthetic 'packages.VersionTest.config' @(
+ @{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '8.0.1' }
+ ) $true @() $family[0] -FallbackItems @(
+ @{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '8.0.0' })
+ }
+
+ It 'ignores a mismatching fallback when the project manifest matches' {
+ Test-Synthetic $synthetic 'packages.VersionTest.config' @(
+ @{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '8.0.0' }
+ ) -FallbackItems @(
+ @{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '8.0.1' })
+ }
+
+ It 'does not take a Logging anchor from an inactive fallback' {
+ Test-Synthetic $synthetic 'packages.VersionTest.config' @(
+ @{ Id = $family[0]; Version = '8.0.1' }
+ ) -FallbackItems @(
+ @{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '8.0.1' })
+ }
+
+ It 'normalizes spaces in the project name to underscores' {
+ Test-Synthetic $synthetic 'packages.Version_Test.config' @(
+ @{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '8.0.1' }
+ ) $true @() $family[0] -ProjectName 'Version Test'
+ }
+}
+
+Describe 'Packaged direct and transitive consumers' -Tag 'Consumer' {
+ BeforeAll {
+ # Arrange once: package the real Logging project, not a substitute targets fixture.
+ $feed = Join-Path $work 'feed'
+ New-Item -ItemType Directory $feed | Out-Null
+ $anchor = '8.0.0-preview1'
+ # Use the real packaging project, preserving its repository props/targets and versioning.
+ Invoke-CheckedDotnet @('pack', $loggingProject, '-c', 'Release', '-o', $feed,
+ "-p:SqlClientPackageVersion=$anchor", '-v:minimal', '-nologo')
+ $packageFile = Join-Path $feed "$logging.$anchor.nupkg"
+ # Stop ancestor props/targets/CPM and user package caches/feeds from affecting fixture consumers.
+ Set-Content (Join-Path $work 'Directory.Build.props') ''
+ Set-Content (Join-Path $work 'Directory.Build.targets') ''
+ Set-Content (Join-Path $work 'Directory.Packages.props') ''
+ $env:NUGET_PACKAGES = Join-Path $work 'cache'
+ $configFile = Join-Path $work 'NuGet.Config'
+ $escapedFeed = [Security.SecurityElement]::Escape($feed)
+ Set-Content $configFile ""
+ foreach ($id in $family) {
+ New-FixturePackage $id $anchor
+ New-FixturePackage $id '8.0.0-preview2'
+ }
+ foreach ($id in $excluded) { New-FixturePackage $id '1.0.0' }
+ New-FixturePackage 'SqlClient.VersionChecks.Wrapper' '1.0.0' $true
+ }
+
+ It 'ships the current targets unchanged in ' -ForEach @(
+ @{ Folder = 'build' }
+ @{ Folder = 'buildTransitive' }
+ ) {
+ # Assert the archive contents, so a stale or omitted target cannot pass consumer checks.
+ $archive = [IO.Compression.ZipFile]::OpenRead($packageFile)
+ try {
+ $folder = $Folder
+ $entry = $archive.GetEntry("$folder/$logging.targets")
+ if (-not $entry) { throw "Missing $folder target in the real Logging package." }
+ $reader = [IO.StreamReader]::new($entry.Open())
+ try { $packed = $reader.ReadToEnd() } finally { $reader.Dispose() }
+ if ($packed -ne (Get-Content $targets -Raw)) { throw "Stale $folder target packaged." }
+ } finally { $archive.Dispose() }
+ }
+
+ It 'validates ' -ForEach $consumerCases {
+ Test-Consumer $Name $Transitive $Version $Package $Mismatch $Properties $RuntimeOnly $ExcludeBuild
+ }
+}
From 8dbde8a41473cdb33429823f5153507939a81f1e Mon Sep 17 00:00:00 2001
From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com>
Date: Tue, 13 Oct 2026 12:58:52 -0300
Subject: [PATCH 3/4] Simplify family-version test pipeline invocation for PR
#4815
Run pinned PowerShell commands through DotNetCoreCLI tasks.
Document test intent and remove YAML-wiring tests in favor of pipeline runs.
Update the documented Pester command to run only version-check tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
BUILDGUIDE.md | 4 +-
.../common/steps/test-family-versions.yml | 54 +++++++-------
eng/pipelines/pr/jobs/test-logging-job.yml | 7 ++
eng/pipelines/scripts/tests/README.md | 1 -
.../tests/VersionCheckPipeline.Tests.ps1 | 74 -------------------
.../Logging/test/VersionChecks.Tests.ps1 | 15 ++++
6 files changed, 49 insertions(+), 106 deletions(-)
delete mode 100644 eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1
diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md
index 19a65c0309..449393af72 100644
--- a/BUILDGUIDE.md
+++ b/BUILDGUIDE.md
@@ -280,7 +280,7 @@ The regression suite also requires Pester 5.7.1:
```powershell
dotnet tool run pwsh -NoProfile -Command "Install-Module Pester -RequiredVersion 5.7.1 -Repository PSGallery -Scope CurrentUser -Force -ErrorAction Stop"
-dotnet tool run pwsh -NoProfile -Command 'Import-Module Pester -RequiredVersion 5.7.1; Invoke-Pester -Path ./src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1,./eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1 -CI'
+dotnet tool run pwsh -NoProfile -Command 'Import-Module Pester -RequiredVersion 5.7.1; Invoke-Pester -Path ./src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1 -CI'
```
The parameterized Pester suite packs the real Logging project, creates small family-package fixtures
@@ -288,7 +288,7 @@ in a private local feed, and verifies automatic imports for both direct and
transitive package consumers. It uses a unique directory under Logging/test and
removes that directory on completion; no SQL Server is required.
Add `-Tag Synthetic` to `Invoke-Pester` to run only the metadata and legacy manifest
-matrix, or `-Tag Pipeline` to check pipeline wiring without builds.
+matrix without packing consumer fixtures.
The PR test stages run the suite in dedicated Logging jobs on the modern Windows
and Linux platforms. Official/non-official OneBranch runs it in the package-validation
job. Both invoke Pester directly through a shared YAML step and publish failure-gated
diff --git a/eng/pipelines/common/steps/test-family-versions.yml b/eng/pipelines/common/steps/test-family-versions.yml
index d900d7bb91..764733923c 100644
--- a/eng/pipelines/common/steps/test-family-versions.yml
+++ b/eng/pipelines/common/steps/test-family-versions.yml
@@ -8,40 +8,36 @@ parameters:
type: string
steps:
- - script: >-
- dotnet tool run pwsh -NoProfile -NonInteractive -Command
- "Install-Module Pester -RequiredVersion 5.7.1 -Repository PSGallery -Scope CurrentUser -Force -ErrorAction Stop"
+ - task: DotNetCoreCLI@2
displayName: Install family-version test prerequisites
- workingDirectory: $(Build.SourcesDirectory)
+ inputs:
+ command: custom
+ custom: tool
+ arguments: >-
+ run pwsh -NoProfile -NonInteractive -Command
+ "Install-Module Pester -RequiredVersion 5.7.1 -Repository PSGallery -Scope CurrentUser -Force -ErrorAction Stop"
+ workingDirectory: $(Build.SourcesDirectory)
- - task: PowerShell@2
+ - task: DotNetCoreCLI@2
displayName: Test installed SqlClient family versions
inputs:
- targetType: inline
- pwsh: true
+ command: custom
+ custom: tool
workingDirectory: $(Build.SourcesDirectory)
- script: |
- # Keep the Pester configuration literal across shells; execute with the pinned local tool.
- & dotnet tool run pwsh -NoProfile -NonInteractive -Command @'
- $ErrorActionPreference = 'Stop'
- Import-Module Pester -RequiredVersion 5.7.1 -ErrorAction Stop
- $configuration = New-PesterConfiguration
- $configuration.Run.Path = @(
- 'src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1'
- 'eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1'
- )
- $configuration.Run.Exit = $true
- $configuration.Output.Verbosity = 'Normal'
- # The real Logging pack needs repository imports, so the suite owns its scratch directory.
- $configuration.TestDrive.Enabled = $false
- $configuration.TestResult.Enabled = $true
- $configuration.TestResult.OutputFormat = 'NUnitXml'
- $configuration.TestResult.OutputPath = '${{ parameters.testResultsPath }}'
- Invoke-Pester -Configuration $configuration
- '@
- if ($LASTEXITCODE -ne 0) {
- throw "Family-version Pester tests failed with exit code $LASTEXITCODE."
- }
+ # The task passes this directly to dotnet, without an outer shell expanding PowerShell variables.
+ arguments: >-
+ run pwsh -NoProfile -NonInteractive -Command
+ "$ErrorActionPreference = 'Stop';
+ Import-Module Pester -RequiredVersion 5.7.1 -ErrorAction Stop;
+ $configuration = New-PesterConfiguration;
+ $configuration.Run.Path = 'src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1';
+ $configuration.Run.Exit = $true;
+ $configuration.Output.Verbosity = 'Normal';
+ $configuration.TestDrive.Enabled = $false;
+ $configuration.TestResult.Enabled = $true;
+ $configuration.TestResult.OutputFormat = 'NUnitXml';
+ $configuration.TestResult.OutputPath = '${{ parameters.testResultsPath }}';
+ Invoke-Pester -Configuration $configuration"
- task: PublishTestResults@2
displayName: Publish family-version test results
diff --git a/eng/pipelines/pr/jobs/test-logging-job.yml b/eng/pipelines/pr/jobs/test-logging-job.yml
index e656a09636..aace43a93f 100644
--- a/eng/pipelines/pr/jobs/test-logging-job.yml
+++ b/eng/pipelines/pr/jobs/test-logging-job.yml
@@ -4,7 +4,10 @@
# See the LICENSE file in the project root for more information. #
#################################################################################
+# Consumer build regressions need the SDK and package fixtures, but no SQL Server.
+# test-stages.yml schedules this job only for modern Windows/Linux platforms.
parameters:
+ # Used in job and result-directory names to isolate parallel platform runs.
- name: platformDisplayName
type: string
@@ -14,6 +17,7 @@ parameters:
- name: poolName
type: string
+ # Keep the artifact naming consistent with the other PR test jobs.
- name: testResultsArtifactBaseName
type: string
@@ -31,14 +35,17 @@ jobs:
- imageOverride -equals ${{ parameters.poolImage }}
steps:
+ # The SDK builds fixture consumers; local tool restoration supplies pinned PowerShell.
- template: /eng/pipelines/common/steps/install-dotnet.yml@self
- template: /eng/pipelines/common/steps/restore-dotnet-tools.yml@self
+ # Share Pester execution and failure-gated NUnit publication with OneBranch validation.
- template: /eng/pipelines/common/steps/test-family-versions.yml@self
parameters:
testResultsPath: '$(testResultsPath)/family-versions.xml'
+ # Preserve results on failure. Job/attempt suffixes avoid platform and rerun collisions.
- task: PublishPipelineArtifact@1
displayName: Publish Logging Test Artifacts
condition: succeededOrFailed()
diff --git a/eng/pipelines/scripts/tests/README.md b/eng/pipelines/scripts/tests/README.md
index aaa479f25c..2ad3fa021e 100644
--- a/eng/pipelines/scripts/tests/README.md
+++ b/eng/pipelines/scripts/tests/README.md
@@ -32,7 +32,6 @@ Add `-Output Detailed` to see per-test results.
| `Install-DockerCli.macos.Tests.ps1` | `Install-DockerCli.macos.ps1` — Homebrew bottle selection for the macOS docker CLI. |
| `Pipeline-Arguments.Tests.ps1` | PR build/pack/test argument quoting and the repository's CI variable naming convention. |
| `Build-Orchestration.Tests.ps1` | MSBuild test filters, dependency-pack ordering/version forwarding, local restore freshness, and generated command-line argument quoting. |
-| `VersionCheckPipeline.Tests.ps1` | Family-version suite placement, direct Pester invocation, pinned tooling, and failure-gated results in PR Logging tests and OneBranch package validation. |
The localization and Docker helper tests mock `git`, `tar`, `Invoke-RestMethod`,
and `Invoke-WebRequest`, so they do not access the network or modify a real
diff --git a/eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1 b/eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1
deleted file mode 100644
index 46b9c01fae..0000000000
--- a/eng/pipelines/scripts/tests/VersionCheckPipeline.Tests.ps1
+++ /dev/null
@@ -1,74 +0,0 @@
-# Licensed to the .NET Foundation under one or more agreements.
-# The .NET Foundation licenses this file to you under the MIT license.
-# See the LICENSE file in the project root for more information.
-
-BeforeAll {
- $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../../..')).Path
- $stepPath = '/eng/pipelines/common/steps/test-family-versions.yml@self'
-}
-
-Describe 'Family-version pipeline wiring' -Tag 'Pipeline' {
- It ' runs the shared suite after installing the SDK and restoring tools' -ForEach @(
- @{ Path = 'eng/pipelines/pr/jobs/test-logging-job.yml' }
- @{ Path = 'eng/pipelines/onebranch/jobs/validate-packages-job.yml' }
- ) {
- # Arrange: inspect the actual callers, not a duplicated list in a test fixture.
- $content = Get-Content -LiteralPath (Join-Path $repoRoot $Path) -Raw
- $restoreIndex = $content.IndexOf('/eng/pipelines/common/steps/restore-dotnet-tools.yml@self')
- $installIndex = $content.IndexOf('/eng/pipelines/common/steps/install-dotnet.yml@self')
- $suiteIndex = $content.IndexOf($stepPath)
-
- $installIndex | Should -BeGreaterOrEqual 0
- $restoreIndex | Should -BeGreaterThan $installIndex
- $suiteIndex | Should -BeGreaterThan $restoreIndex
- [regex]::Matches($content, [regex]::Escape($stepPath)).Count | Should -Be 1
- $content | Should -Match 'testResultsPath:'
- }
-
- It 'adds dedicated Logging jobs to the modern PR test platforms' {
- $content = Get-Content (Join-Path $repoRoot 'eng/pipelines/pr/stages/test-stages.yml') -Raw
- $content | Should -Match ('(?m)^ - \$\{\{ if not\(contains\(platform\.dotnet, ''net4''\)\) \}\}:\r?\n' +
- ' - template: /eng/pipelines/pr/jobs/test-logging-job\.yml@self')
- $content | Should -Match 'platformDisplayName: \$\{\{ platform\.displayName \}\}'
- $content | Should -Match 'poolImage: \$\{\{ platform\.image \}\}'
- }
-
- It 'keeps regression fixtures out of production build and pack jobs' -ForEach @(
- @{ Path = 'eng/pipelines/pr/stages/pack-stage.yml' }
- @{ Path = 'eng/pipelines/onebranch/jobs/build-buildproj-job.yml' }
- ) {
- $content = Get-Content (Join-Path $repoRoot $Path) -Raw
- $content | Should -Not -Match ([regex]::Escape($stepPath))
- }
-
- It 'uses the pinned local PowerShell tool for prerequisites and tests' {
- $content = Get-Content (Join-Path $repoRoot 'eng/pipelines/common/steps/test-family-versions.yml') -Raw
- [regex]::Matches($content, 'dotnet tool run pwsh').Count | Should -Be 2
- $content | Should -Match 'Install-Module Pester -RequiredVersion 5\.7\.1'
- $content | Should -Match 'Invoke-Pester -Configuration \$configuration'
- $content | Should -Match '\$configuration\.Run\.Exit = \$true'
- $content | Should -Match '\$configuration\.TestDrive\.Enabled = \$false'
- $content | Should -Match '\$configuration\.TestResult\.OutputFormat = ''NUnitXml'''
- $content | Should -Match 'VersionChecks\.Tests\.ps1'
- $content | Should -Match 'VersionCheckPipeline\.Tests\.ps1'
- $content | Should -Not -Match 'RunVersionChecks\.ps1'
- $content | Should -Not -Match 'continueOnError: true'
- }
-
- It 'publishes PR Logging result artifacts with unique job/attempt names' {
- $content = Get-Content (Join-Path $repoRoot 'eng/pipelines/pr/jobs/test-logging-job.yml') -Raw
- $content | Should -Match 'PublishPipelineArtifact@1'
- $content | Should -Match '\$\(System\.JobId\)_attempt\$\(System\.JobAttempt\)'
- $content | Should -Match 'condition: succeededOrFailed\(\)'
- $content | Should -Match '\$\{\{ parameters\.platformDisplayName \}\}_logging'
- }
-
- It 'publishes NUnit results even on test failure and gates missing or failed results' {
- $content = Get-Content (Join-Path $repoRoot 'eng/pipelines/common/steps/test-family-versions.yml') -Raw
- $content | Should -Match 'PublishTestResults@2'
- $content | Should -Match 'condition: succeededOrFailed\(\)'
- $content | Should -Match 'testResultsFormat: NUnit'
- $content | Should -Match 'failTaskOnFailedTests: true'
- $content | Should -Match 'failTaskOnMissingResultsFile: true'
- }
-}
diff --git a/src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1 b/src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1
index ff91893ada..cc7691c320 100644
--- a/src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1
+++ b/src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1
@@ -27,6 +27,7 @@ BeforeDiscovery {
OptOut = $false
}
if ($version -ne $anchorVersion) {
+ # Reuse each mismatching input to prove the emergency opt-out bypasses it.
@{
Kind = $kind; AnchorVersion = $anchorVersion; Id = $id; Version = $version
OptOut = $true
@@ -45,6 +46,7 @@ BeforeDiscovery {
@{ Kind = 'packages.config' }
@{ Kind = 'packages.VersionTest.config' }
)
+ # Exercise the same contracts with Logging referenced directly and through a wrapper package.
$consumerCases = @(
foreach ($transitive in @($false, $true)) {
$mode = if ($transitive) { 'transitive' } else { 'direct' }
@@ -63,6 +65,7 @@ BeforeAll {
$ErrorActionPreference = 'Stop'
$targets = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../build/Microsoft.Data.SqlClient.Internal.Logging.targets'))
$loggingProject = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../src/Logging.csproj'))
+ # Stay inside the repo for the real pack's imports; isolate each run from existing fixtures.
$work = Join-Path $PSScriptRoot ("version-checks-" + [guid]::NewGuid())
$previousTmp = $env:TMPDIR
$previousPackages = $env:NUGET_PACKAGES
@@ -84,6 +87,7 @@ BeforeAll {
$exitCode = $LASTEXITCODE
$text = $output -join "`n"
if ($Mismatch) {
+ # A restore/compiler failure must not masquerade as the expected version diagnostic.
if ($exitCode -eq 0 -or $text -notmatch 'SQLCLIENT001' -or
($Evidence -and -not $text.Contains($Evidence)) -or
$text -match 'MSB4096|error (?!SQLCLIENT001)[A-Z]+\d+') {
@@ -134,6 +138,7 @@ BeforeAll {
param([string] $Id, [string] $Version, [bool] $Wrapper = $false)
$directory = Join-Path $work "$Id-$Version"
New-Item -ItemType Directory $directory | Out-Null
+ # Exact ranges prevent restore from choosing another fixture version to repair a mismatch.
$dependency = if ($Wrapper) { "" } else { '' }
Set-Content (Join-Path $directory 'Fixture.csproj') @"
@@ -156,12 +161,14 @@ BeforeAll {
[bool] $RuntimeOnly = $false, [bool] $ExcludeBuild = $false)
$directory = Join-Path $work $Name
New-Item -ItemType Directory $directory | Out-Null
+ # Arrange: the wrapper makes Logging transitive without importing the target by hand.
$rootId = if ($Transitive) { 'SqlClient.VersionChecks.Wrapper' } else { $logging }
$rootVersion = if ($Transitive) { '1.0.0' } else { $anchor }
$exclude = if ($ExcludeBuild) { "ExcludeAssets='build;buildTransitive'" } else { '' }
$references = ""
$installed = if ($Package) { @($Package) } else { $family }
foreach ($id in $installed) {
+ # Excluding compile assets forces validation to find runtime-only package evidence.
$runtime = if ($RuntimeOnly) { "ExcludeAssets='compile'" } else { '' }
$references += ""
}
@@ -194,6 +201,7 @@ BeforeAll {
throw 'Transitive consumer did not import buildTransitive.'
}
if ($Transitive) {
+ # Prove the transitive case was not accidentally turned into a direct reference.
$assets = Get-Content (Join-Path $directory 'obj/project.assets.json') -Raw | ConvertFrom-Json -AsHashtable
if ($assets.project.frameworks['net10.0'].dependencies.ContainsKey($logging)) {
throw 'Transitive test accidentally references Logging directly.'
@@ -213,6 +221,7 @@ BeforeAll {
}
AfterAll {
+ # Restore process-wide settings even after failures; delete only this run's unique directory.
$env:TMPDIR = $previousTmp
$env:NUGET_PACKAGES = $previousPackages
if ($work -and (Test-Path -LiteralPath $work)) {
@@ -222,11 +231,13 @@ AfterAll {
Describe 'Synthetic family-version validation' -Tag 'Synthetic' {
It ': against Logging (opt-out: )' -ForEach $syntheticCases {
+ # Arrange matching/mixed family metadata alongside independently versioned packages.
$items = @(@{ Id = $logging; Version = $AnchorVersion }, @{ Id = $Id; Version = $Version })
$items += $excluded | ForEach-Object { @{ Id = $_; Version = '1.0.0' } }
$mismatch = $Version -ne $AnchorVersion -and -not $OptOut
$properties = if ($OptOut) { @('-p:SqlClientEnforceFamilyVersions=false') } else { @() }
$evidence = if ($mismatch) { $Id } else { '' }
+ # Act/assert through the shipped target rather than reproducing its comparison logic.
Test-Synthetic $synthetic $Kind $items $mismatch $properties $evidence
}
@@ -267,6 +278,7 @@ Describe 'Synthetic family-version validation' -Tag 'Synthetic' {
}
Describe 'Project-specific legacy manifests' -Tag 'Synthetic', 'LegacyManifest' {
+ # Conflicting manifests expose incorrect selection; matching files alone cannot prove precedence.
It 'detects mismatches without a fallback packages.config' {
Test-Synthetic $synthetic 'packages.VersionTest.config' @(
@{ Id = $logging; Version = '8.0.0' }, @{ Id = $family[0]; Version = '8.0.1' }
@@ -288,6 +300,7 @@ Describe 'Project-specific legacy manifests' -Tag 'Synthetic', 'LegacyManifest'
}
It 'does not take a Logging anchor from an inactive fallback' {
+ # Selection applies to the anchor as well as mismatches; never merge the two manifests.
Test-Synthetic $synthetic 'packages.VersionTest.config' @(
@{ Id = $family[0]; Version = '8.0.1' }
) -FallbackItems @(
@@ -311,6 +324,7 @@ Describe 'Packaged direct and transitive consumers' -Tag 'Consumer' {
Invoke-CheckedDotnet @('pack', $loggingProject, '-c', 'Release', '-o', $feed,
"-p:SqlClientPackageVersion=$anchor", '-v:minimal', '-nologo')
$packageFile = Join-Path $feed "$logging.$anchor.nupkg"
+ # Install isolation boundaries only after the real pack has consumed repository settings.
# Stop ancestor props/targets/CPM and user package caches/feeds from affecting fixture consumers.
Set-Content (Join-Path $work 'Directory.Build.props') ''
Set-Content (Join-Path $work 'Directory.Build.targets') ''
@@ -344,6 +358,7 @@ Describe 'Packaged direct and transitive consumers' -Tag 'Consumer' {
}
It 'validates ' -ForEach $consumerCases {
+ # Restore and build a fresh consumer so generated imports and resolved assets are real.
Test-Consumer $Name $Transitive $Version $Package $Mismatch $Properties $RuntimeOnly $ExcludeBuild
}
}
From 6bff84d2cd94f4dba711d620954cdcc8011a0a69 Mon Sep 17 00:00:00 2001
From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com>
Date: Tue, 13 Oct 2026 16:31:12 -0300
Subject: [PATCH 4/4] Remove Pester pipeline execution for PR #4815
Remove the shared Pester step and dedicated Logging test jobs.
Keep the family-version regression suite available for local execution.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
BUILDGUIDE.md | 6 +--
.../common/steps/test-family-versions.yml | 50 -----------------
.../onebranch/jobs/validate-packages-job.yml | 12 ++---
eng/pipelines/pr/jobs/test-logging-job.yml | 54 -------------------
eng/pipelines/pr/stages/test-stages.yml | 9 ----
5 files changed, 5 insertions(+), 126 deletions(-)
delete mode 100644 eng/pipelines/common/steps/test-family-versions.yml
delete mode 100644 eng/pipelines/pr/jobs/test-logging-job.yml
diff --git a/BUILDGUIDE.md b/BUILDGUIDE.md
index 25c88d80a9..75c94576c3 100644
--- a/BUILDGUIDE.md
+++ b/BUILDGUIDE.md
@@ -289,10 +289,8 @@ transitive package consumers. It uses a unique directory under Logging/test and
removes that directory on completion; no SQL Server is required.
Add `-Tag Synthetic` to `Invoke-Pester` to run only the metadata and legacy manifest
matrix without packing consumer fixtures.
-The PR test stages run the suite in dedicated Logging jobs on the modern Windows
-and Linux platforms. Official/non-official OneBranch runs it in the package-validation
-job. Both invoke Pester directly through a shared YAML step and publish failure-gated
-NUnit results; no PowerShell runner script is required.
+The suite is available for local execution only. PR and OneBranch pipelines do not
+install Pester or run these tests.
Just like building and testing the various projects in this repository, packaging the projects into NuGet packages is
also handled by `build.proj`. From the root of your repository, run `dotnet build` against `build.proj` with a pack target,
diff --git a/eng/pipelines/common/steps/test-family-versions.yml b/eng/pipelines/common/steps/test-family-versions.yml
deleted file mode 100644
index 764733923c..0000000000
--- a/eng/pipelines/common/steps/test-family-versions.yml
+++ /dev/null
@@ -1,50 +0,0 @@
-# Licensed to the .NET Foundation under one or more agreements.
-# The .NET Foundation licenses this file to you under the MIT license.
-# See the LICENSE file in the project root for more information.
-
-# Call from a test/validation job after installing the SDK and restoring local tools.
-parameters:
- - name: testResultsPath
- type: string
-
-steps:
- - task: DotNetCoreCLI@2
- displayName: Install family-version test prerequisites
- inputs:
- command: custom
- custom: tool
- arguments: >-
- run pwsh -NoProfile -NonInteractive -Command
- "Install-Module Pester -RequiredVersion 5.7.1 -Repository PSGallery -Scope CurrentUser -Force -ErrorAction Stop"
- workingDirectory: $(Build.SourcesDirectory)
-
- - task: DotNetCoreCLI@2
- displayName: Test installed SqlClient family versions
- inputs:
- command: custom
- custom: tool
- workingDirectory: $(Build.SourcesDirectory)
- # The task passes this directly to dotnet, without an outer shell expanding PowerShell variables.
- arguments: >-
- run pwsh -NoProfile -NonInteractive -Command
- "$ErrorActionPreference = 'Stop';
- Import-Module Pester -RequiredVersion 5.7.1 -ErrorAction Stop;
- $configuration = New-PesterConfiguration;
- $configuration.Run.Path = 'src/Microsoft.Data.SqlClient.Internal/Logging/test/VersionChecks.Tests.ps1';
- $configuration.Run.Exit = $true;
- $configuration.Output.Verbosity = 'Normal';
- $configuration.TestDrive.Enabled = $false;
- $configuration.TestResult.Enabled = $true;
- $configuration.TestResult.OutputFormat = 'NUnitXml';
- $configuration.TestResult.OutputPath = '${{ parameters.testResultsPath }}';
- Invoke-Pester -Configuration $configuration"
-
- - task: PublishTestResults@2
- displayName: Publish family-version test results
- condition: succeededOrFailed()
- inputs:
- testResultsFormat: NUnit
- testResultsFiles: '${{ parameters.testResultsPath }}'
- testRunTitle: SqlClient family-version validation
- failTaskOnFailedTests: true
- failTaskOnMissingResultsFile: true
diff --git a/eng/pipelines/onebranch/jobs/validate-packages-job.yml b/eng/pipelines/onebranch/jobs/validate-packages-job.yml
index 1208baeec4..c3891369e1 100644
--- a/eng/pipelines/onebranch/jobs/validate-packages-job.yml
+++ b/eng/pipelines/onebranch/jobs/validate-packages-job.yml
@@ -85,9 +85,9 @@ jobs:
- name: ob_outputDirectory
value: '$(JOB_OUTPUT)'
- # This job inspects already-built packages and builds only validation tools/test fixtures,
- # so it has no shipping assemblies for APIScan, BinSkim, or SBOM. The build jobs cover
- # all three for the packages they produce.
+ # This job inspects already-built packages and produces no assemblies, so it has nothing for
+ # APIScan or BinSkim to scan and no shipping component to describe in an SBOM. The build
+ # jobs cover all three for the packages they produce.
- name: ob_sdl_apiscan_enabled
value: false
- name: ob_sdl_binskim_enabled
@@ -164,12 +164,6 @@ jobs:
# PackageValidator targets net10.0, which the repo's global.json already pins.
- template: /eng/pipelines/common/steps/install-dotnet.yml@self
- - template: /eng/pipelines/common/steps/restore-dotnet-tools.yml@self
-
- - template: /eng/pipelines/common/steps/test-family-versions.yml@self
- parameters:
- testResultsPath: '$(JOB_OUTPUT)/test_results/family-versions.xml'
-
- template: /eng/pipelines/onebranch/steps/validate-packages-step.yml@self
parameters:
packagesPath: '$(packagesRoot)'
diff --git a/eng/pipelines/pr/jobs/test-logging-job.yml b/eng/pipelines/pr/jobs/test-logging-job.yml
deleted file mode 100644
index aace43a93f..0000000000
--- a/eng/pipelines/pr/jobs/test-logging-job.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-#################################################################################
-# Licensed to the .NET Foundation under one or more agreements. #
-# The .NET Foundation licenses this file to you under the MIT license. #
-# See the LICENSE file in the project root for more information. #
-#################################################################################
-
-# Consumer build regressions need the SDK and package fixtures, but no SQL Server.
-# test-stages.yml schedules this job only for modern Windows/Linux platforms.
-parameters:
- # Used in job and result-directory names to isolate parallel platform runs.
- - name: platformDisplayName
- type: string
-
- - name: poolImage
- type: string
-
- - name: poolName
- type: string
-
- # Keep the artifact naming consistent with the other PR test jobs.
- - name: testResultsArtifactBaseName
- type: string
-
-jobs:
- - job: "test_${{ parameters.platformDisplayName }}_logging"
- displayName: "logging_${{ parameters.platformDisplayName }}"
-
- variables:
- - name: testResultsPath
- value: "$(Build.SourcesDirectory)/test_results/${{ parameters.platformDisplayName }}_logging"
-
- pool:
- name: ${{ parameters.poolName }}
- demands:
- - imageOverride -equals ${{ parameters.poolImage }}
-
- steps:
- # The SDK builds fixture consumers; local tool restoration supplies pinned PowerShell.
- - template: /eng/pipelines/common/steps/install-dotnet.yml@self
-
- - template: /eng/pipelines/common/steps/restore-dotnet-tools.yml@self
-
- # Share Pester execution and failure-gated NUnit publication with OneBranch validation.
- - template: /eng/pipelines/common/steps/test-family-versions.yml@self
- parameters:
- testResultsPath: '$(testResultsPath)/family-versions.xml'
-
- # Preserve results on failure. Job/attempt suffixes avoid platform and rerun collisions.
- - task: PublishPipelineArtifact@1
- displayName: Publish Logging Test Artifacts
- condition: succeededOrFailed()
- inputs:
- artifact: ${{ parameters.testResultsArtifactBaseName }}_$(System.JobId)_attempt$(System.JobAttempt)
- targetPath: $(testResultsPath)
diff --git a/eng/pipelines/pr/stages/test-stages.yml b/eng/pipelines/pr/stages/test-stages.yml
index 865e5c70aa..ce4c5c32f8 100644
--- a/eng/pipelines/pr/stages/test-stages.yml
+++ b/eng/pipelines/pr/stages/test-stages.yml
@@ -125,15 +125,6 @@ stages:
- ${{ parameters.stageNameSecrets }}
jobs:
- # The consumer fixtures target net10.0, so run once per modern platform.
- - ${{ if not(contains(platform.dotnet, 'net4')) }}:
- - template: /eng/pipelines/pr/jobs/test-logging-job.yml@self
- parameters:
- platformDisplayName: ${{ platform.displayName }}
- poolImage: ${{ platform.image }}
- poolName: ${{ parameters.poolName }}
- testResultsArtifactBaseName: ${{ parameters.testResultsArtifactBaseName }}
-
# TestAbstractions
- template: /eng/pipelines/pr/jobs/test-buildproj-job.yml@self
parameters: