From 43dd0bbc6fd1b1a2a257ccaa21bb88c339816e23 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:09:43 -0300 Subject: [PATCH 1/9] Add Internal.Logging and Extensions.Abstractions version support to AzureAuthentication app - Add LoggingVersion and AbstractionsVersion MSBuild properties (default 1.0.0) - Add PackageVersion entries for Microsoft.Data.SqlClient.Internal.Logging and Microsoft.Data.SqlClient.Extensions.Abstractions - Reference both packages explicitly in the project to surface their resolved versions in the generated PackageVersions class - Emit Logging and Abstractions versions alongside the other package versions in App.cs and EntryPoint.cs startup output - Rename PackageVersions.AzureExtensionsVersion to MicrosoftDataSqlClientExtensionsAzureVersion to match the naming pattern used by all other generated version constants - Update README with new build parameters, help-text examples, and sample output --- doc/apps/AzureAuthentication/App.cs | 4 ++- .../AzureAuthentication.csproj | 2 ++ .../Directory.Packages.props | 18 ++++++++--- doc/apps/AzureAuthentication/EntryPoint.cs | 4 ++- .../PackageVersions.Partial.cs | 2 +- doc/apps/AzureAuthentication/README.md | 32 +++++++++++++------ 6 files changed, 45 insertions(+), 17 deletions(-) diff --git a/doc/apps/AzureAuthentication/App.cs b/doc/apps/AzureAuthentication/App.cs index 3447e5489f..86cb2bbe02 100644 --- a/doc/apps/AzureAuthentication/App.cs +++ b/doc/apps/AzureAuthentication/App.cs @@ -63,9 +63,11 @@ internal int Run(RunOptions options) --------------------------- Packages used: + Logging: {PackageVersions.MicrosoftDataSqlClientInternalLogging} + Abstractions: {PackageVersions.MicrosoftDataSqlClientExtensionsAbstractions} SqlClient: {PackageVersions.MicrosoftDataSqlClient} AKV Provider: {PackageVersions.MicrosoftDataSqlClientAlwaysEncryptedAzureKeyVaultProvider} - Azure: {PackageVersions.AzureExtensionsVersion} + Azure: {PackageVersions.MicrosoftDataSqlClientExtensionsAzureVersion} """); diff --git a/doc/apps/AzureAuthentication/AzureAuthentication.csproj b/doc/apps/AzureAuthentication/AzureAuthentication.csproj index 8b7c6f5ee9..a46068f0df 100644 --- a/doc/apps/AzureAuthentication/AzureAuthentication.csproj +++ b/doc/apps/AzureAuthentication/AzureAuthentication.csproj @@ -11,6 +11,8 @@ + + - - 7.0.0-preview4.26064.3 + + 1.0.0 + 1.0.0 + 7.0.0 + 7.0.0 - - 7.0.0-preview1.26064.3 + + + + + + diff --git a/doc/apps/AzureAuthentication/EntryPoint.cs b/doc/apps/AzureAuthentication/EntryPoint.cs index 20885ae1af..eba1f6f13c 100644 --- a/doc/apps/AzureAuthentication/EntryPoint.cs +++ b/doc/apps/AzureAuthentication/EntryPoint.cs @@ -55,9 +55,11 @@ Supply specific package versions when building to test different versions of the -p:AzureVersion=1.0.0-preview1 Current package versions: + Logging: {PackageVersions.MicrosoftDataSqlClientInternalLogging} + Abstractions: {PackageVersions.MicrosoftDataSqlClientExtensionsAbstractions} SqlClient: {PackageVersions.MicrosoftDataSqlClient} AKV Provider: {PackageVersions.MicrosoftDataSqlClientAlwaysEncryptedAzureKeyVaultProvider} - Azure: {PackageVersions.AzureExtensionsVersion} + Azure: {PackageVersions.MicrosoftDataSqlClientExtensionsAzureVersion} """) { connectionStringOption, diff --git a/doc/apps/AzureAuthentication/PackageVersions.Partial.cs b/doc/apps/AzureAuthentication/PackageVersions.Partial.cs index 3c88781daa..2f7d012cf9 100644 --- a/doc/apps/AzureAuthentication/PackageVersions.Partial.cs +++ b/doc/apps/AzureAuthentication/PackageVersions.Partial.cs @@ -14,7 +14,7 @@ internal static partial class PackageVersions /// /// Version of the Azure extensions package, or "N/A" when not referenced. /// - public const string AzureExtensionsVersion = + public const string MicrosoftDataSqlClientExtensionsAzureVersion = #if AZURE_EXTENSIONS MicrosoftDataSqlClientExtensionsAzure; #else diff --git a/doc/apps/AzureAuthentication/README.md b/doc/apps/AzureAuthentication/README.md index 967a728007..e3d2a93ed4 100644 --- a/doc/apps/AzureAuthentication/README.md +++ b/doc/apps/AzureAuthentication/README.md @@ -33,8 +33,10 @@ Package versions are controlled through build properties. Pass them on the comma | Property | Default | Description | | --- | --- | --- | -| `SqlClientVersion` | `7.0.0-preview4.26064.3` | Version of `Microsoft.Data.SqlClient` to reference. | -| `AkvProviderVersion` | `7.0.0-preview1.26064.3` | Version of `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` to reference. | +| `LoggingVersion` | `1.0.0` | Version of `Microsoft.Data.SqlClient.Internal.Logging` to reference. | +| `AbstractionsVersion` | `1.0.0` | Version of `Microsoft.Data.SqlClient.Extensions.Abstractions` to reference. | +| `SqlClientVersion` | `7.0.0` | Version of `Microsoft.Data.SqlClient` to reference. | +| `AkvProviderVersion` | `7.0.0` | Version of `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` to reference. | | `AzureVersion` | None | Version of `Microsoft.Data.SqlClient.Extensions.Azure` to reference. When omitted, the `Azure` package will not be referenced. | ## Local Package Source @@ -68,9 +70,11 @@ Description: Supply specific package versions when building to test different versions of the SqlClient suite, for example: - -p:SqlClientVersion=7.0.0.preview4 - -p:AkvProviderVersion=7.0.1-preview2 - -p:AzureVersion=1.0.0-preview1 + -p:LoggingVersion=1.0.1 + -p:AbstractionsVersion=1.0.1 + -p:SqlClientVersion=7.1.0.preview1 + -p:AkvProviderVersion=7.0.0 + -p:AzureVersion=1.1.0-preview1 Usage: AzureAuthentication [options] @@ -104,9 +108,11 @@ Azure Authentication Tester --------------------------- Packages used: - SqlClient: 7.0.0-preview4.26055.1 - AKV Provider: 6.1.2 - Azure: 1.0.0-preview1.26055.1 + Logging: 1.0.1 + Abstractions: 1.0.1 + SqlClient: 7.1.0.preview1 + AKV Provider: 7.0.0 + Azure: 1.1.0-preview1 Connection details: Data Source: adotest.database.windows.net @@ -151,10 +157,16 @@ Run including the `Azure` extensions package: dotnet run -p:AzureVersion=1.0.0-preview1 -- -c "" ``` -Override all three versions at once: +Override all five package versions at once: ```bash -dotnet run -p:SqlClientVersion=7.0.0-preview1 -p:AkvProviderVersion=7.0.0-preview1 -p:AzureVersion=1.0.0-preview1 -- -c "" +dotnet run \ + -p:LoggingVersion=1.0.1 \ + -p:AbstractionsVersion=1.0.1 \ + -p:SqlClientVersion=7.1.0-preview1 \ + -p:AkvProviderVersion=7.1.0-preview1 \ + -p:AzureVersion=1.0.0 \ + -- -c "" ``` ## Prerequisites From 4824dcd9a6eb1671ec81b7505dc621a198e80f84 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Fri, 24 Apr 2026 13:13:48 -0300 Subject: [PATCH 2/9] Move AzureAuthentication app to tools/PackageCompatibility and rename The primary purpose of this app is to test package compatibility, not specifically Azure authentication, so the name now better reflects its purpose. - git mv doc/apps/AzureAuthentication/ tools/PackageCompatibility/ - Rename AzureAuthentication.csproj to PackageCompatibility.csproj - Update RootNamespace and all source namespaces to Microsoft.Data.SqlClient.Tools.PackageCompatibility - Rename AppName constant to 'Package Compatibility Tester' - Update README title, help-text block, and sample output accordingly --- .../PackageCompatibility}/App.cs | 4 ++-- .../PackageCompatibility}/Directory.Build.props | 0 .../PackageCompatibility}/Directory.Packages.props | 0 .../PackageCompatibility}/EntryPoint.cs | 2 +- .../GeneratePackageVersions.targets | 0 .../PackageCompatibility}/NuGet.config | 0 .../PackageCompatibility/PackageCompatibility.csproj | 2 +- .../PackageCompatibility}/PackageVersions.Partial.cs | 2 +- .../PackageCompatibility}/README.md | 12 ++++++------ .../PackageCompatibility}/SqlClientEventListener.cs | 2 +- .../PackageCompatibility}/packages/.gitkeep | 0 11 files changed, 12 insertions(+), 12 deletions(-) rename {doc/apps/AzureAuthentication => tools/PackageCompatibility}/App.cs (98%) rename {doc/apps/AzureAuthentication => tools/PackageCompatibility}/Directory.Build.props (100%) rename {doc/apps/AzureAuthentication => tools/PackageCompatibility}/Directory.Packages.props (100%) rename {doc/apps/AzureAuthentication => tools/PackageCompatibility}/EntryPoint.cs (98%) rename {doc/apps/AzureAuthentication => tools/PackageCompatibility}/GeneratePackageVersions.targets (100%) rename {doc/apps/AzureAuthentication => tools/PackageCompatibility}/NuGet.config (100%) rename doc/apps/AzureAuthentication/AzureAuthentication.csproj => tools/PackageCompatibility/PackageCompatibility.csproj (95%) rename {doc/apps/AzureAuthentication => tools/PackageCompatibility}/PackageVersions.Partial.cs (93%) rename {doc/apps/AzureAuthentication => tools/PackageCompatibility}/README.md (97%) rename {doc/apps/AzureAuthentication => tools/PackageCompatibility}/SqlClientEventListener.cs (97%) rename {doc/apps/AzureAuthentication => tools/PackageCompatibility}/packages/.gitkeep (100%) diff --git a/doc/apps/AzureAuthentication/App.cs b/tools/PackageCompatibility/App.cs similarity index 98% rename from doc/apps/AzureAuthentication/App.cs rename to tools/PackageCompatibility/App.cs index 86cb2bbe02..e0d3b0aa49 100644 --- a/doc/apps/AzureAuthentication/App.cs +++ b/tools/PackageCompatibility/App.cs @@ -3,7 +3,7 @@ using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider; -namespace Microsoft.Data.SqlClient.Samples.AzureAuthentication; +namespace Microsoft.Data.SqlClient.Tools.PackageCompatibility; /// /// Console application that validates SqlClient connectivity using Entra ID (formerly Azure Active @@ -194,7 +194,7 @@ internal static void Err(string message) /// /// The display name of the application. /// - internal const string AppName = "Azure Authentication Tester"; + internal const string AppName = "Package Compatibility Tester"; /// /// The optional event listener used to capture SqlClient diagnostic events. diff --git a/doc/apps/AzureAuthentication/Directory.Build.props b/tools/PackageCompatibility/Directory.Build.props similarity index 100% rename from doc/apps/AzureAuthentication/Directory.Build.props rename to tools/PackageCompatibility/Directory.Build.props diff --git a/doc/apps/AzureAuthentication/Directory.Packages.props b/tools/PackageCompatibility/Directory.Packages.props similarity index 100% rename from doc/apps/AzureAuthentication/Directory.Packages.props rename to tools/PackageCompatibility/Directory.Packages.props diff --git a/doc/apps/AzureAuthentication/EntryPoint.cs b/tools/PackageCompatibility/EntryPoint.cs similarity index 98% rename from doc/apps/AzureAuthentication/EntryPoint.cs rename to tools/PackageCompatibility/EntryPoint.cs index eba1f6f13c..735580e5ab 100644 --- a/doc/apps/AzureAuthentication/EntryPoint.cs +++ b/tools/PackageCompatibility/EntryPoint.cs @@ -1,6 +1,6 @@ using System.CommandLine; -namespace Microsoft.Data.SqlClient.Samples.AzureAuthentication; +namespace Microsoft.Data.SqlClient.Tools.PackageCompatibility; /// /// Contains the application entry point responsible for parsing command-line arguments and diff --git a/doc/apps/AzureAuthentication/GeneratePackageVersions.targets b/tools/PackageCompatibility/GeneratePackageVersions.targets similarity index 100% rename from doc/apps/AzureAuthentication/GeneratePackageVersions.targets rename to tools/PackageCompatibility/GeneratePackageVersions.targets diff --git a/doc/apps/AzureAuthentication/NuGet.config b/tools/PackageCompatibility/NuGet.config similarity index 100% rename from doc/apps/AzureAuthentication/NuGet.config rename to tools/PackageCompatibility/NuGet.config diff --git a/doc/apps/AzureAuthentication/AzureAuthentication.csproj b/tools/PackageCompatibility/PackageCompatibility.csproj similarity index 95% rename from doc/apps/AzureAuthentication/AzureAuthentication.csproj rename to tools/PackageCompatibility/PackageCompatibility.csproj index a46068f0df..141a9d4987 100644 --- a/doc/apps/AzureAuthentication/AzureAuthentication.csproj +++ b/tools/PackageCompatibility/PackageCompatibility.csproj @@ -3,7 +3,7 @@ Exe net481;net10.0 - Microsoft.Data.SqlClient.Samples.AzureAuthentication + Microsoft.Data.SqlClient.Tools.PackageCompatibility enable enable latest diff --git a/doc/apps/AzureAuthentication/PackageVersions.Partial.cs b/tools/PackageCompatibility/PackageVersions.Partial.cs similarity index 93% rename from doc/apps/AzureAuthentication/PackageVersions.Partial.cs rename to tools/PackageCompatibility/PackageVersions.Partial.cs index 2f7d012cf9..0e34418e85 100644 --- a/doc/apps/AzureAuthentication/PackageVersions.Partial.cs +++ b/tools/PackageCompatibility/PackageVersions.Partial.cs @@ -1,4 +1,4 @@ -namespace Microsoft.Data.SqlClient.Samples.AzureAuthentication; +namespace Microsoft.Data.SqlClient.Tools.PackageCompatibility; /// /// Hand-written companion to the auto-generated PackageVersions.g.cs file diff --git a/doc/apps/AzureAuthentication/README.md b/tools/PackageCompatibility/README.md similarity index 97% rename from doc/apps/AzureAuthentication/README.md rename to tools/PackageCompatibility/README.md index e3d2a93ed4..003395918a 100644 --- a/doc/apps/AzureAuthentication/README.md +++ b/tools/PackageCompatibility/README.md @@ -1,4 +1,4 @@ -# AzureAuthentication Sample App +# PackageCompatibility Tool A minimal console application that verifies **SqlClient** can connect to a SQL Server using Entra ID authentication (formerly Azure Active Directory authentication) via the **Azure** package. It also @@ -62,8 +62,8 @@ The app has built-in help: dotnet run -- --help Description: - Azure Authentication Tester - --------------------------- + Package Compatibility Tester + ---------------------------- Validates SqlClient connectivity using EntraID (formerly Azure Active Directory) authentication. Connects to SQL Server using the supplied connection string, which must specify the authentication method. @@ -77,7 +77,7 @@ Description: -p:AzureVersion=1.1.0-preview1 Usage: - AzureAuthentication [options] + PackageCompatibility [options] Options: -c, --connection-string (REQUIRED) The ADO.NET connection string used to connect to SQL Server. @@ -104,8 +104,8 @@ dotnet run -- -c "Server=myserver.database.windows.net;Database=mydb;Authenticat On success the app emits to standard out: ```bash -Azure Authentication Tester ---------------------------- +Package Compatibility Tester +---------------------------- Packages used: Logging: 1.0.1 diff --git a/doc/apps/AzureAuthentication/SqlClientEventListener.cs b/tools/PackageCompatibility/SqlClientEventListener.cs similarity index 97% rename from doc/apps/AzureAuthentication/SqlClientEventListener.cs rename to tools/PackageCompatibility/SqlClientEventListener.cs index 5e861f4796..686a4f2c9a 100644 --- a/doc/apps/AzureAuthentication/SqlClientEventListener.cs +++ b/tools/PackageCompatibility/SqlClientEventListener.cs @@ -1,6 +1,6 @@ using System.Diagnostics.Tracing; -namespace Microsoft.Data.SqlClient.Samples.AzureAuthentication; +namespace Microsoft.Data.SqlClient.Tools.PackageCompatibility; /// /// Listens for events from Microsoft.Data.SqlClient.EventSource and emits them via the diff --git a/doc/apps/AzureAuthentication/packages/.gitkeep b/tools/PackageCompatibility/packages/.gitkeep similarity index 100% rename from doc/apps/AzureAuthentication/packages/.gitkeep rename to tools/PackageCompatibility/packages/.gitkeep From 2cdc4f763c90f28ef0354f84455ea5742fb383dc Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:17:54 -0300 Subject: [PATCH 3/9] Refine PackageCompatibility package version reporting and docs - add explicit Microsoft.SqlServer.Server version support and output - keep PackageCompatibility package/version ordering as currently arranged - update help text and sample output for current package version set - expand README guidance around package compatibility and auth flows --- tools/PackageCompatibility/App.cs | 5 +- .../Directory.Packages.props | 10 +- tools/PackageCompatibility/EntryPoint.cs | 6 +- .../PackageCompatibility.csproj | 9 +- tools/PackageCompatibility/README.md | 125 ++++++++++-------- 5 files changed, 87 insertions(+), 68 deletions(-) diff --git a/tools/PackageCompatibility/App.cs b/tools/PackageCompatibility/App.cs index e0d3b0aa49..2864ad0158 100644 --- a/tools/PackageCompatibility/App.cs +++ b/tools/PackageCompatibility/App.cs @@ -63,11 +63,12 @@ internal int Run(RunOptions options) --------------------------- Packages used: - Logging: {PackageVersions.MicrosoftDataSqlClientInternalLogging} Abstractions: {PackageVersions.MicrosoftDataSqlClientExtensionsAbstractions} - SqlClient: {PackageVersions.MicrosoftDataSqlClient} AKV Provider: {PackageVersions.MicrosoftDataSqlClientAlwaysEncryptedAzureKeyVaultProvider} Azure: {PackageVersions.MicrosoftDataSqlClientExtensionsAzureVersion} + Logging: {PackageVersions.MicrosoftDataSqlClientInternalLogging} + SqlClient: {PackageVersions.MicrosoftDataSqlClient} + SqlServer: {PackageVersions.MicrosoftSqlServerServer} """); diff --git a/tools/PackageCompatibility/Directory.Packages.props b/tools/PackageCompatibility/Directory.Packages.props index dc3b1a7cb1..3f377d77a0 100644 --- a/tools/PackageCompatibility/Directory.Packages.props +++ b/tools/PackageCompatibility/Directory.Packages.props @@ -5,10 +5,11 @@ - 1.0.0 1.0.0 - 7.0.0 7.0.0 + 1.0.0 + 7.0.0 + 1.0.0 @@ -19,12 +20,13 @@ Normal apps wouldn't explcitily reference these 2 packages, but we do here to force the desired versions. This helps detect version conflicts. --> - + - + + - - - + + + **Note:** All Entra ID modes require the `Extensions.Azure` package to be referenced (pass +> `-p:AzureVersion=`). Without it, SqlClient will throw at runtime because no +> authentication provider is registered for those modes. + ## Build Parameters Package versions are controlled through build properties. Pass them on the command line with `-p:` @@ -33,11 +59,12 @@ Package versions are controlled through build properties. Pass them on the comma | Property | Default | Description | | --- | --- | --- | -| `LoggingVersion` | `1.0.0` | Version of `Microsoft.Data.SqlClient.Internal.Logging` to reference. | | `AbstractionsVersion` | `1.0.0` | Version of `Microsoft.Data.SqlClient.Extensions.Abstractions` to reference. | -| `SqlClientVersion` | `7.0.0` | Version of `Microsoft.Data.SqlClient` to reference. | | `AkvProviderVersion` | `7.0.0` | Version of `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` to reference. | | `AzureVersion` | None | Version of `Microsoft.Data.SqlClient.Extensions.Azure` to reference. When omitted, the `Azure` package will not be referenced. | +| `LoggingVersion` | `1.0.0` | Version of `Microsoft.Data.SqlClient.Internal.Logging` to reference. | +| `SqlClientVersion` | `7.0.0` | Version of `Microsoft.Data.SqlClient` to reference. | +| `SqlServerVersion` | `1.0.0` | Version of `Microsoft.SqlServer.Server` to reference. | ## Local Package Source @@ -68,37 +95,20 @@ Description: Validates SqlClient connectivity using EntraID (formerly Azure Active Directory) authentication. Connects to SQL Server using the supplied connection string, which must specify the authentication method. - Supply specific package versions when building to test different versions of the SqlClient suite, for example: - - -p:LoggingVersion=1.0.1 - -p:AbstractionsVersion=1.0.1 - -p:SqlClientVersion=7.1.0.preview1 - -p:AkvProviderVersion=7.0.0 - -p:AzureVersion=1.1.0-preview1 - -Usage: - PackageCompatibility [options] - -Options: - -c, --connection-string (REQUIRED) The ADO.NET connection string used to connect to SQL Server. - Supports SQL, Entra ID, and integrated authentication modes. - -l, --log-events Enable SqlClient event emission to the console. - -t, --trace Pauses execution to allow dotnet-trace to be attached. - -v, --verbose Enable verbose output with detailed error information. - -?, -h, --help Show help and usage information - --version Show version information + ... ``` -The app expects a single argument: a full connection string. +The app requires a connection string. Use SQL authentication for a basic connectivity check: ```bash -dotnet run -- -c "" +dotnet run -- -c "Server=myserver;Database=mydb;User ID=sa;Password=;Encrypt=Mandatory;TrustServerCertificate=true" ``` -For Entra ID authentication, use an `Authentication` keyword in the connection string. For example: +To exercise Entra ID flows, include an `Authentication` keyword and reference the `Extensions.Azure` +package: ```bash -dotnet run -- -c "Server=myserver.database.windows.net;Database=mydb;Authentication=ActiveDirectoryDefault" +dotnet run -p:AzureVersion=1.0.0 -- -c "Server=myserver.database.windows.net;Database=mydb;Authentication=ActiveDirectoryDefault" ``` On success the app emits to standard out: @@ -108,11 +118,12 @@ Package Compatibility Tester ---------------------------- Packages used: - Logging: 1.0.1 Abstractions: 1.0.1 - SqlClient: 7.1.0.preview1 AKV Provider: 7.0.0 Azure: 1.1.0-preview1 + Logging: 1.0.1 + SqlClient: 7.1.0.preview1 + SqlServer: 1.0.0 Connection details: Data Source: adotest.database.windows.net @@ -134,44 +145,48 @@ Connection failed: ### Examples -Run with the default (published) package versions, and no `Azure` package: +Run a basic SQL authentication check using the default package versions: ```bash -dotnet run -- -c "" +dotnet run -- -c "Server=myserver;Database=mydb;User ID=sa;Password=;Encrypt=Mandatory;TrustServerCertificate=true" ``` -If the connection string specifies one of the Entra ID authentication methods, -`SqlClient` will fail with an error indicating that no authentication provider has been registered. -This is because the `Azure` package was not referenced, and the app did not provide its own custom -authentication provider. - -Run against locally-built packages (drop `.nupkg` files into the `packages/` folder first): +Run with no `Azure` package — Entra ID modes will fail at runtime if specified in the connection +string, which is itself useful for confirming the error path: ```bash -dotnet run -p:SqlClientVersion=7.0.0-preview4 -- -c "" +dotnet run -- -c "" ``` -Run including the `Azure` extensions package: +Include the `Azure` package to enable Entra ID authentication flows: ```bash dotnet run -p:AzureVersion=1.0.0-preview1 -- -c "" ``` +Run against locally-built packages (drop `.nupkg` files into `packages/` first): + +```bash +dotnet run -p:SqlClientVersion=7.1.0-preview1 -- -c "" +``` + Override all five package versions at once: ```bash dotnet run \ - -p:LoggingVersion=1.0.1 \ -p:AbstractionsVersion=1.0.1 \ - -p:SqlClientVersion=7.1.0-preview1 \ -p:AkvProviderVersion=7.1.0-preview1 \ -p:AzureVersion=1.0.0 \ + -p:LoggingVersion=1.0.1 \ + -p:SqlClientVersion=7.1.0-preview1 \ + -p:SqlServerVersio=1.0.0 \ -- -c "" ``` ## Prerequisites - [.NET 10.0 SDK](https://dotnet.microsoft.com/download) and .NET Framework 4.8.1 or later. -- A SQL Server or Azure SQL instance accessible with Entra ID credentials. -- Azure credentials available to `DefaultAzureCredential` (e.g. Azure CLI login, environment - variables, or managed identity). +- A SQL Server or Azure SQL instance reachable from the machine running the tool. +- For Entra ID authentication modes: Azure credentials available to `DefaultAzureCredential` + (e.g. Azure CLI login, environment variables, or managed identity), and the `AzureVersion` + build property set so the `Extensions.Azure` package is included. From a25a78c96c0fcc161b2c9663d9a38ed7ff72542a Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:37:27 -0300 Subject: [PATCH 4/9] - Moved this app from doc/apps to tools/ which seems like a better fit. - Added support to specify all SqlClient suite package versions. - Added a modern xUnit3 test suite. --- .github/instructions/testing.instructions.md | 39 +++ src/Microsoft.Data.SqlClient.slnx | 6 +- .../Directory.Packages.props | 7 +- tools/PackageCompatibility/README.md | 7 +- tools/PackageCompatibility/global.json | 15 + tools/PackageCompatibility/{ => src}/App.cs | 0 .../{ => src}/EntryPoint.cs | 2 +- .../{ => src}/GeneratePackageVersions.targets | 0 .../{ => src}/PackageCompatibility.csproj | 9 + .../{ => src}/PackageVersions.Partial.cs | 0 .../{ => src}/SqlClientEventListener.cs | 0 tools/PackageCompatibility/test/AppTests.cs | 126 +++++++ .../test/BuildVersionOverrideTests.cs | 311 ++++++++++++++++++ .../test/ConsoleCollection.cs | 26 ++ .../test/EntryPointTests.cs | 123 +++++++ .../test/PackageCompatibility.Test.csproj | 25 ++ 16 files changed, 687 insertions(+), 9 deletions(-) create mode 100644 tools/PackageCompatibility/global.json rename tools/PackageCompatibility/{ => src}/App.cs (100%) rename tools/PackageCompatibility/{ => src}/EntryPoint.cs (98%) rename tools/PackageCompatibility/{ => src}/GeneratePackageVersions.targets (100%) rename tools/PackageCompatibility/{ => src}/PackageCompatibility.csproj (89%) rename tools/PackageCompatibility/{ => src}/PackageVersions.Partial.cs (100%) rename tools/PackageCompatibility/{ => src}/SqlClientEventListener.cs (100%) create mode 100644 tools/PackageCompatibility/test/AppTests.cs create mode 100644 tools/PackageCompatibility/test/BuildVersionOverrideTests.cs create mode 100644 tools/PackageCompatibility/test/ConsoleCollection.cs create mode 100644 tools/PackageCompatibility/test/EntryPointTests.cs create mode 100644 tools/PackageCompatibility/test/PackageCompatibility.Test.csproj diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index 20fe03b1e8..f59bd7bd9d 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -173,6 +173,45 @@ dotnet test ... --filter "FullyQualifiedName=Namespace.ClassName.MethodName" ## Writing Tests +## Test Documentation Requirements + +To keep tests maintainable for contributors and AI agents, test intent must be documented at +class and method level. + +### Required XML Documentation +- Add XML `` comments to every test class. +- Add XML `` comments to every test method (`[Fact]`, `[Theory]`, conditional variants). +- For helper methods used by tests, add XML `` comments and XML `` / `` + where applicable. +- For fixture and collection types, add XML `` comments describing why the fixture exists + (for example, serialization of console-mutating tests). + +### What the Comments Must Explain +- The behavior/contract being tested (not just restating the method name). +- Why the scenario matters (for example: regression guard, parsing contract, sync/async parity, + isolation requirement). +- For helper methods, what side effects occur (for example console redirection, file system + copying, process execution) and why they are needed. + +### Style Guidance +- Keep comments concise and factual. +- Prefer behavior-focused wording over implementation trivia. +- Avoid comments that merely repeat obvious code. +- Use inline comments inside test methods only for non-obvious setup/act/assert details. + +### Example +```csharp +/// +/// Ensures malformed connection strings return a non-zero exit code and emit a parse error +/// without verbose exception details. +/// +[Fact] +public void AppRunWithMalformedConnectionStringReturnsOneAndWritesParseError() +{ + // Arrange / Act / Assert +} +``` + ### Test Structure ```csharp public class FeatureNameTests diff --git a/src/Microsoft.Data.SqlClient.slnx b/src/Microsoft.Data.SqlClient.slnx index d65c3edc90..c45bbdf485 100644 --- a/src/Microsoft.Data.SqlClient.slnx +++ b/src/Microsoft.Data.SqlClient.slnx @@ -10,9 +10,6 @@ - - - @@ -170,7 +167,6 @@ - @@ -215,4 +211,6 @@ + + diff --git a/tools/PackageCompatibility/Directory.Packages.props b/tools/PackageCompatibility/Directory.Packages.props index 3f377d77a0..60490d0bae 100644 --- a/tools/PackageCompatibility/Directory.Packages.props +++ b/tools/PackageCompatibility/Directory.Packages.props @@ -8,7 +8,7 @@ 1.0.0 7.0.0 1.0.0 - 7.0.0 + 7.0.1 1.0.0 @@ -37,8 +37,9 @@ - - + + + diff --git a/tools/PackageCompatibility/README.md b/tools/PackageCompatibility/README.md index f404bf6552..f1f3713ff2 100644 --- a/tools/PackageCompatibility/README.md +++ b/tools/PackageCompatibility/README.md @@ -33,6 +33,11 @@ Specifically, it: The app is designed to run against both **published NuGet packages** and **locally-built packages** (via the `packages/` directory configured in `NuGet.config`). +## Project Layout + +- `src/` contains the tool source files and project file. +- `test/` is reserved for tests. + ## Authentication Modes The authentication mode embedded in the connection string controls which code paths and packages are @@ -63,7 +68,7 @@ Package versions are controlled through build properties. Pass them on the comma | `AkvProviderVersion` | `7.0.0` | Version of `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` to reference. | | `AzureVersion` | None | Version of `Microsoft.Data.SqlClient.Extensions.Azure` to reference. When omitted, the `Azure` package will not be referenced. | | `LoggingVersion` | `1.0.0` | Version of `Microsoft.Data.SqlClient.Internal.Logging` to reference. | -| `SqlClientVersion` | `7.0.0` | Version of `Microsoft.Data.SqlClient` to reference. | +| `SqlClientVersion` | `7.0.1` | Version of `Microsoft.Data.SqlClient` to reference. | | `SqlServerVersion` | `1.0.0` | Version of `Microsoft.SqlServer.Server` to reference. | ## Local Package Source diff --git a/tools/PackageCompatibility/global.json b/tools/PackageCompatibility/global.json new file mode 100644 index 0000000000..2de58c1156 --- /dev/null +++ b/tools/PackageCompatibility/global.json @@ -0,0 +1,15 @@ +{ + "test": { + // xUnit v3 runs on Microsoft.Testing.Platform rather than the older VSTest runner. + // On .NET 10, dotnet test must therefore be opted into MTP mode for this test project. + "runner": "Microsoft.Testing.Platform" + }, + "sdk": { + // global.json lookup stops at the nearest file and does not merge with the repo-root file. + // This subtree therefore needs to repeat the root SDK settings so commands run from + // tools/PackageCompatibility keep using the same pinned SDK behavior. + "version": "10.0.107", + "rollForward": "disable", + "allowPrerelease": false + } +} diff --git a/tools/PackageCompatibility/App.cs b/tools/PackageCompatibility/src/App.cs similarity index 100% rename from tools/PackageCompatibility/App.cs rename to tools/PackageCompatibility/src/App.cs diff --git a/tools/PackageCompatibility/EntryPoint.cs b/tools/PackageCompatibility/src/EntryPoint.cs similarity index 98% rename from tools/PackageCompatibility/EntryPoint.cs rename to tools/PackageCompatibility/src/EntryPoint.cs index 827150f291..8729352024 100644 --- a/tools/PackageCompatibility/EntryPoint.cs +++ b/tools/PackageCompatibility/src/EntryPoint.cs @@ -50,7 +50,7 @@ which must specify the authentication method. Supply specific package versions when building to test different versions of the SqlClient suite, for example: - -p:AbstractionVersion=1.0.1 + -p:AbstractionsVersion=1.0.1 -p:AkvProviderVersion=7.0.1-preview2 -p:AzureVersion=1.0.0-preview1 -p:LoggingVersion=1.0.2 diff --git a/tools/PackageCompatibility/GeneratePackageVersions.targets b/tools/PackageCompatibility/src/GeneratePackageVersions.targets similarity index 100% rename from tools/PackageCompatibility/GeneratePackageVersions.targets rename to tools/PackageCompatibility/src/GeneratePackageVersions.targets diff --git a/tools/PackageCompatibility/PackageCompatibility.csproj b/tools/PackageCompatibility/src/PackageCompatibility.csproj similarity index 89% rename from tools/PackageCompatibility/PackageCompatibility.csproj rename to tools/PackageCompatibility/src/PackageCompatibility.csproj index fcf2385382..d33eff8e52 100644 --- a/tools/PackageCompatibility/PackageCompatibility.csproj +++ b/tools/PackageCompatibility/src/PackageCompatibility.csproj @@ -2,8 +2,11 @@ Exe + net481;net10.0 + Microsoft.Data.SqlClient.Tools.PackageCompatibility + enable enable latest @@ -38,6 +41,12 @@ + + + <_Parameter1>PackageCompatibility.Test + + + diff --git a/tools/PackageCompatibility/PackageVersions.Partial.cs b/tools/PackageCompatibility/src/PackageVersions.Partial.cs similarity index 100% rename from tools/PackageCompatibility/PackageVersions.Partial.cs rename to tools/PackageCompatibility/src/PackageVersions.Partial.cs diff --git a/tools/PackageCompatibility/SqlClientEventListener.cs b/tools/PackageCompatibility/src/SqlClientEventListener.cs similarity index 100% rename from tools/PackageCompatibility/SqlClientEventListener.cs rename to tools/PackageCompatibility/src/SqlClientEventListener.cs diff --git a/tools/PackageCompatibility/test/AppTests.cs b/tools/PackageCompatibility/test/AppTests.cs new file mode 100644 index 0000000000..3a80de1eaf --- /dev/null +++ b/tools/PackageCompatibility/test/AppTests.cs @@ -0,0 +1,126 @@ +using System; +using System.IO; + +using Microsoft.Data.SqlClient.Tools.PackageCompatibility; + +using Xunit; + +namespace Microsoft.Data.SqlClient.Tools.PackageCompatibility.Tests; + +/// +/// Verifies control flow and diagnostics for common failure +/// and instrumentation paths without requiring a live SQL Server. +/// +[Collection(ConsoleCollection.Name)] +public class AppTests +{ + /// + /// Ensures malformed connection strings produce a parse error and a non-zero exit code in + /// non-verbose mode, without leaking full exception details. + /// + [Fact] + public void AppRunWithMalformedConnectionStringReturnsOneAndWritesParseError() + { + // Use an intentionally invalid connection string to force parse failure branch. + CommandResult result = ExecuteApp( + connectionString: "Server", + verbose: false); + + // Non-verbose mode should report a friendly error without full exception type details. + Assert.Equal(1, result.ExitCode); + Assert.Contains("Failed to parse connection string:", result.StandardError, StringComparison.Ordinal); + Assert.DoesNotContain("System.", result.StandardError, StringComparison.Ordinal); + } + + /// + /// Ensures malformed connection strings produce detailed exception output when verbose mode is + /// enabled. + /// + [Fact] + public void AppRunWithMalformedConnectionStringAndVerboseIncludesExceptionDetails() + { + // Same malformed input, but verbose mode should include full exception diagnostics. + CommandResult result = ExecuteApp( + connectionString: "Server", + verbose: true); + + // Verbose mode intentionally includes type/stack-friendly exception text. + Assert.Equal(1, result.ExitCode); + Assert.Contains("Failed to parse connection string:", result.StandardError, StringComparison.Ordinal); + Assert.Contains("System.", result.StandardError, StringComparison.Ordinal); + } + + /// + /// Ensures enabling event logging emits the expected logging banner before the eventual + /// connection failure for an unreachable endpoint. + /// + [Fact] + public void AppRunWithLogEventsEmitsLoggingMessageBeforeConnectionFailure() + { + // Use an unreachable endpoint to keep this deterministic while exercising log-events path. + CommandResult result = ExecuteApp( + connectionString: "Server=127.0.0.1,1;Database=master;User ID=sa;Password=invalid;Connect Timeout=1;Encrypt=False", + logEvents: true, + verbose: false); + + // App should announce event logging before eventually failing the connection attempt. + Assert.Equal(1, result.ExitCode); + Assert.Contains("SqlClient event logging enabled; events will be prefixed with [EVENT]", result.StandardOutput, StringComparison.Ordinal); + Assert.Contains("Connection failed:", result.StandardError, StringComparison.Ordinal); + } + + /// + /// Executes with redirected console streams and returns + /// captured output and exit code for assertions. + /// + /// Connection string supplied to . + /// Whether to enable event listener wiring during the run. + /// Whether to enable trace-attach pause behavior. + /// Whether to emit verbose diagnostic details. + /// Captured stdout, stderr, and process-equivalent exit code. + private static CommandResult ExecuteApp(string connectionString, bool logEvents = false, bool trace = false, bool verbose = false) + { + // Redirect process-wide console streams so assertions can inspect output. + TextWriter originalOut = Console.Out; + TextWriter originalError = Console.Error; + using StringWriter standardOutput = new(); + using StringWriter standardError = new(); + + // Construct the same internal options object EntryPoint passes to App.Run. + using App app = new(); + App.RunOptions runOptions = new() + { + ConnectionString = connectionString, + LogEvents = logEvents, + Trace = trace, + Verbose = verbose + }; + + try + { + Console.SetOut(standardOutput); + Console.SetError(standardError); + + // Act: execute one App.Run invocation for the provided scenario. + int exitCode = app.Run(runOptions); + + // Capture both streams because App writes info and errors separately. + return new CommandResult(exitCode, standardOutput.ToString(), standardError.ToString()); + } + finally + { + // Restore global console streams to prevent cross-test interference. + Console.SetOut(originalOut); + Console.SetError(originalError); + } + } + + /// + /// Represents the observed result of a single in-process + /// invocation. + /// + /// Return code from . + /// Captured standard output stream contents. + /// Captured standard error stream contents. + private sealed record CommandResult(int ExitCode, string StandardOutput, string StandardError); +} diff --git a/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs b/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs new file mode 100644 index 0000000000..29e000d1c2 --- /dev/null +++ b/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs @@ -0,0 +1,311 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +using Xunit; + +namespace Microsoft.Data.SqlClient.Tools.PackageCompatibility.Tests; + +/// +/// Build-focused tests that verify MSBuild property overrides (for package versions) are reflected +/// in generated package-version constants. +/// +public class BuildVersionOverrideTests +{ + /// + /// Ensures building with explicit -p: overrides produces matching version constants in + /// the generated PackageVersions.g.cs file. + /// + /// MSBuild properties to override during build. + /// Expected package label-to-version mappings. + [Theory] + [MemberData(nameof(GetVersionPropertyTestCases))] + public void BuildGeneratesExpectedPackageVersionsWhenCustomPropertiesAreSupplied( + Dictionary buildProperties, + Dictionary expectedVersions) + { + // Build in an isolated copy so per-test overrides cannot leak into repo obj/bin state. + BuildArtifacts artifacts = BuildAppWithProperties(buildProperties); + + // The generated source file is the authoritative output of GeneratePackageVersions.targets. + string generatedVersions = File.ReadAllText(artifacts.GeneratedVersionsFile); + + // Assert each expected package label maps to the generated constant/version pair. + foreach (var kvp in expectedVersions) + { + string constantName = GetPackageVersionsConstantName(kvp.Key); + string expected = $"public const string {constantName} = \"{kvp.Value}\";"; + Assert.Contains(expected, generatedVersions, StringComparison.Ordinal); + } + } + + /// + /// Provides test vectors for single, partial, and full package-version override scenarios. + /// + /// + /// Theory data containing MSBuild override properties and expected generated version values. + /// + public static IEnumerable GetVersionPropertyTestCases() + { + // Override only SqlClient to prove a single-property override flows to generated code. + yield return new object[] + { + new Dictionary { { "SqlClientVersion", "7.0.0" } }, + new Dictionary + { + { "SqlClient", "7.0.0" }, + { "Abstractions", "1.0.0" }, + { "Logging", "1.0.0" }, + { "SqlServer", "1.0.0" } + } + }; + + // Override a subset of properties to verify mixed default/override behavior. + yield return new object[] + { + new Dictionary + { + { "SqlClientVersion", "7.0.0" }, + { "LoggingVersion", "1.0.0" }, + { "AbstractionsVersion", "1.0.0" } + }, + new Dictionary + { + { "SqlClient", "7.0.0" }, + { "Logging", "1.0.0" }, + { "Abstractions", "1.0.0" }, + { "AKV Provider", "7.0.0" } + } + }; + + // Override all supported package properties to validate full replacement behavior. + yield return new object[] + { + new Dictionary + { + { "AbstractionsVersion", "1.0.0" }, + { "AkvProviderVersion", "7.0.0" }, + { "LoggingVersion", "1.0.0" }, + { "SqlClientVersion", "7.0.0" }, + { "SqlServerVersion", "1.0.0" } + }, + new Dictionary + { + { "Abstractions", "1.0.0" }, + { "AKV Provider", "7.0.0" }, + { "Logging", "1.0.0" }, + { "SqlClient", "7.0.0" }, + { "SqlServer", "1.0.0" } + } + }; + } + + /// + /// Builds an isolated copy of the PackageCompatibility project with optional version override + /// properties and returns paths to relevant build artifacts. + /// + /// MSBuild properties passed via -p: arguments. + /// Paths to the isolated workspace, output folder, and generated versions file. + private static BuildArtifacts BuildAppWithProperties(Dictionary properties) + { + // Locate and clone the package-compatibility subtree so test builds are hermetic. + string packageCompatibilityDir = GetPackageCompatibilityDirectory(); + string tempProjectDir = Path.Combine(Path.GetTempPath(), $"PackageCompatibilityProject_{Guid.NewGuid():N}"); + string tempOutputDir = Path.Combine(Path.GetTempPath(), $"PackageCompatibility_{Guid.NewGuid():N}"); + CopyDirectory(packageCompatibilityDir, tempProjectDir); + Directory.CreateDirectory(tempOutputDir); + + string copiedProjectFile = Path.Combine(tempProjectDir, "src", "PackageCompatibility.csproj"); + string generatedVersionsFile = Path.Combine(tempProjectDir, "src", "obj", "Release", "net10.0", "PackageVersions.g.cs"); + + // Build with explicit property overrides; MSBuild treats these as highest-priority values. + var buildArgs = new List + { + "build", + copiedProjectFile, + "-c", "Release", + "-f", "net10.0", + "-o", tempOutputDir + }; + + // Add requested test case overrides as -p:= arguments. + foreach (var kvp in properties) + { + buildArgs.Add($"-p:{kvp.Key}={kvp.Value}"); + } + + var psi = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = string.Join(" ", buildArgs), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + using (var process = Process.Start(psi)) + { + if (process == null) + { + throw new InvalidOperationException("Failed to start build process"); + } + + string stdout = string.Empty; + string stderr = string.Empty; + +#if NET + bool completed = process.WaitForExit(TimeSpan.FromSeconds(60)); + if (!completed) + { + process.Kill(); + throw new InvalidOperationException("Build timed out after 60 seconds"); + } +#else + process.WaitForExit(60000); +#endif + + stdout = process.StandardOutput.ReadToEnd(); + stderr = process.StandardError.ReadToEnd(); + + // Fail fast with full command/stdout/stderr context for easy diagnosis. + if (process.ExitCode != 0) + { + throw new InvalidOperationException( + $"Build failed with exit code {process.ExitCode}.\n" + + $"Command: dotnet {string.Join(" ", buildArgs)}\n" + + $"Output:\n{stdout}\n" + + $"Errors:\n{stderr}"); + } + } + + // Validate that the generator actually produced its output file. + Assert.True(File.Exists(generatedVersionsFile), $"Generated versions file not found at {generatedVersionsFile}"); + + return new BuildArtifacts(tempProjectDir, tempOutputDir, generatedVersionsFile); + } + + /// + /// Maps human-readable package labels used in test vectors to generated constant names in + /// PackageVersions.g.cs. + /// + /// Package label used by test case data. + /// Generated constant name expected in PackageVersions.g.cs. + private static string GetPackageVersionsConstantName(string packageLabel) + { + // Keep human-readable labels in test cases while asserting exact generated symbol names. + return packageLabel switch + { + "Abstractions" => "MicrosoftDataSqlClientExtensionsAbstractions", + "AKV Provider" => "MicrosoftDataSqlClientAlwaysEncryptedAzureKeyVaultProvider", + "Logging" => "MicrosoftDataSqlClientInternalLogging", + "SqlClient" => "MicrosoftDataSqlClient", + "SqlServer" => "MicrosoftSqlServerServer", + _ => throw new InvalidOperationException($"Unsupported package label '{packageLabel}'.") + }; + } + + /// + /// Recursively copies the source tree into an isolated workspace, excluding generated artifacts + /// and test folders that can destabilize isolated build behavior. + /// + /// Source directory to copy. + /// Destination directory for the isolated copy. + private static void CopyDirectory(string sourceDirectory, string destinationDirectory) + { + // Copy source tree into temp workspace while excluding volatile build/test folders. + Directory.CreateDirectory(destinationDirectory); + + foreach (string directory in Directory.GetDirectories(sourceDirectory, "*", SearchOption.AllDirectories)) + { + string relativePath = Path.GetRelativePath(sourceDirectory, directory); + if (ShouldSkip(relativePath)) + { + continue; + } + + Directory.CreateDirectory(Path.Combine(destinationDirectory, relativePath)); + } + + foreach (string file in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories)) + { + string relativePath = Path.GetRelativePath(sourceDirectory, file); + if (ShouldSkip(relativePath)) + { + continue; + } + + string destinationFile = Path.Combine(destinationDirectory, relativePath); + Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!); + File.Copy(file, destinationFile, overwrite: true); + } + } + + /// + /// Determines whether a relative path should be excluded from the isolated workspace copy. + /// + /// Path relative to the copy source root. + /// when the path should be skipped; otherwise . + private static bool ShouldSkip(string relativePath) + { + // Ignore generated artifacts and the test project itself to avoid recursive/self-copy issues. + string normalizedPath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + string[] segments = normalizedPath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); + + foreach (string segment in segments) + { + if (segment.Equals("bin", StringComparison.Ordinal) + || segment.Equals("obj", StringComparison.Ordinal) + || segment.Equals("test", StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + /// + /// Resolves and validates the repository path for the PackageCompatibility subtree that contains + /// the source project and central package-management files. + /// + /// Absolute path to the PackageCompatibility subtree root. + private static string GetPackageCompatibilityDirectory() + { + // Resolve the PackageCompatibility folder relative to the running test assembly. + string testDir = Path.GetDirectoryName(typeof(BuildVersionOverrideTests).Assembly.Location) + ?? throw new InvalidOperationException("Cannot determine test assembly directory"); + + string packageCompatibilityDir = Path.Combine(testDir, "..", "..", "..", ".."); + + packageCompatibilityDir = Path.GetFullPath(packageCompatibilityDir); + + if (!Directory.Exists(packageCompatibilityDir)) + { + throw new InvalidOperationException( + $"PackageCompatibility directory not found at {packageCompatibilityDir}.\n" + + $"Test assembly location: {testDir}\n" + + $"Calculated package compat dir: {Path.GetFullPath(packageCompatibilityDir)}"); + } + + // Ensure the expected project file exists before attempting to build. + string csprojFile = Path.Combine(packageCompatibilityDir, "src", "PackageCompatibility.csproj"); + if (!File.Exists(csprojFile)) + { + throw new InvalidOperationException( + $"Project file not found at {csprojFile}.\n" + + $"Expected to find PackageCompatibility.csproj in {packageCompatibilityDir}/src"); + } + + return packageCompatibilityDir; + } + + /// + /// Paths to the isolated build workspace and generated version artifacts for a single test run. + /// + /// Root of the isolated copied project tree. + /// Build output directory for binaries. + /// Path to generated PackageVersions.g.cs. + private sealed record BuildArtifacts(string ProjectDirectory, string OutputDirectory, string GeneratedVersionsFile); +} diff --git a/tools/PackageCompatibility/test/ConsoleCollection.cs b/tools/PackageCompatibility/test/ConsoleCollection.cs new file mode 100644 index 0000000000..f5eb393f9b --- /dev/null +++ b/tools/PackageCompatibility/test/ConsoleCollection.cs @@ -0,0 +1,26 @@ +using Xunit; + +namespace Microsoft.Data.SqlClient.Tools.PackageCompatibility.Tests; + +/// +/// xUnit collection definition used by tests that temporarily redirect global console streams. +/// Parallelization is disabled so tests that mutate and +/// cannot interfere with one another. +/// +[CollectionDefinition(Name, DisableParallelization = true)] +public sealed class ConsoleCollection : ICollectionFixture +{ + /// + /// Stable collection name referenced by console-mutating test classes. + /// + public const string Name = "Console"; +} + +/// +/// Fixture type associated with . +/// Present to satisfy xUnit collection fixture wiring and to provide a future extension point +/// for shared setup/teardown if console coordination needs to expand. +/// +public sealed class ConsoleCollectionFixture +{ +} \ No newline at end of file diff --git a/tools/PackageCompatibility/test/EntryPointTests.cs b/tools/PackageCompatibility/test/EntryPointTests.cs new file mode 100644 index 0000000000..e8dee72e2a --- /dev/null +++ b/tools/PackageCompatibility/test/EntryPointTests.cs @@ -0,0 +1,123 @@ +using System; +using System.IO; +using System.Threading; + +using Microsoft.Data.SqlClient.Tools.PackageCompatibility; + +using Xunit; + +namespace Microsoft.Data.SqlClient.Tools.PackageCompatibility.Tests; + +/// +/// Verifies command-line entry-point behavior, including required argument handling and help-text +/// content that documents default package versions and override examples. +/// +[Collection(ConsoleCollection.Name)] +public class EntryPointTests +{ + /// + /// Ensures invoking the entry point without the required connection-string argument fails and + /// reports the missing option to the caller. + /// + [Fact] + public void EntryPointWithoutConnectionStringReturnsNonZero() + { + // Redirect console streams so we can assert on CLI output without polluting test logs. + TextWriter originalOut = Console.Out; + TextWriter originalError = Console.Error; + using StringWriter output = new(); + + int exitCode; + + try + { + Console.SetOut(output); + Console.SetError(output); + + // Act: invoke with no arguments; --connection-string is required. + exitCode = EntryPoint.Main(Array.Empty()); + } + finally + { + // Always restore global console streams for subsequent tests. + Console.SetOut(originalOut); + Console.SetError(originalError); + } + + // Wait for command help/validation text to flush before asserting. + string commandOutput = WaitForCapturedOutput(output, "--connection-string"); + + // Assert: parser should reject the command and mention the missing option. + Assert.NotEqual(0, exitCode); + Assert.Contains("--connection-string", commandOutput, StringComparison.Ordinal); + } + + /// + /// Ensures help output includes both version-override examples and currently resolved default + /// package versions so users can understand and troubleshoot package selection. + /// + [Fact] + public void HelpOutputContainsDefaultVersions() + { + // Capture help output for contract validation of documented build parameters. + TextWriter originalOut = Console.Out; + TextWriter originalError = Console.Error; + using StringWriter output = new(); + + string helpOutput; + + try + { + Console.SetOut(output); + Console.SetError(output); + + // Act: request help; this should never fail. + int exitCode = EntryPoint.Main(new[] { "--help" }); + Assert.Equal(0, exitCode); + + helpOutput = output.ToString(); + } + finally + { + // Restore global console state to keep tests isolated. + Console.SetOut(originalOut); + Console.SetError(originalError); + } + + // Ensure final help footer is present so all formatted content has been emitted. + helpOutput = WaitForCapturedOutput(output, "--version"); + + // Assert: sample property overrides remain documented in help text. + Assert.Contains("-p:AbstractionsVersion=1.0.1", helpOutput, StringComparison.Ordinal); + Assert.Contains("-p:AkvProviderVersion=7.0.1-preview2", helpOutput, StringComparison.Ordinal); + Assert.Contains("-p:AzureVersion=1.0.0-preview1", helpOutput, StringComparison.Ordinal); + Assert.Contains("-p:LoggingVersion=1.0.2", helpOutput, StringComparison.Ordinal); + Assert.Contains("-p:SqlClientVersion=7.0.0-preview4", helpOutput, StringComparison.Ordinal); + Assert.Contains("-p:SqlServerVersion=1.0.0", helpOutput, StringComparison.Ordinal); + + // Assert: currently resolved package defaults are visible to aid troubleshooting. + Assert.Contains("Abstractions: 1.0.0", helpOutput, StringComparison.Ordinal); + Assert.Contains("AKV Provider: 7.0.0", helpOutput, StringComparison.Ordinal); + Assert.Contains("Azure: N/A", helpOutput, StringComparison.Ordinal); + Assert.Contains("Logging: 1.0.0", helpOutput, StringComparison.Ordinal); + Assert.Contains("SqlClient: 7.0.1", helpOutput, StringComparison.Ordinal); + Assert.Contains("SqlServer: 1.0.0", helpOutput, StringComparison.Ordinal); + } + + /// + /// Waits briefly for asynchronously emitted command output to include a marker substring before + /// assertions run, reducing flakiness in console-capture tests. + /// + /// Captured console output writer. + /// Marker text that indicates output emission has completed. + /// The current captured output text. + private static string WaitForCapturedOutput(StringWriter output, string expectedSubstring) + { + // CommandLine writes asynchronously; wait briefly for expected marker text. + SpinWait.SpinUntil( + () => output.ToString().Contains(expectedSubstring, StringComparison.Ordinal), + TimeSpan.FromSeconds(1)); + + return output.ToString(); + } +} diff --git a/tools/PackageCompatibility/test/PackageCompatibility.Test.csproj b/tools/PackageCompatibility/test/PackageCompatibility.Test.csproj new file mode 100644 index 0000000000..4cf6dc685a --- /dev/null +++ b/tools/PackageCompatibility/test/PackageCompatibility.Test.csproj @@ -0,0 +1,25 @@ + + + + Exe + + net481;net10.0 + + Microsoft.Data.SqlClient.Tools.PackageCompatibility.Test + + enable + enable + latest + + true + + + + + + + + + + + From 85474ac2172e8d8c204fa620388191e68abb4457 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:43:56 -0300 Subject: [PATCH 5/9] Removed unused Azure.Core package version. --- tools/PackageCompatibility/Directory.Packages.props | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/PackageCompatibility/Directory.Packages.props b/tools/PackageCompatibility/Directory.Packages.props index 60490d0bae..da04009ad9 100644 --- a/tools/PackageCompatibility/Directory.Packages.props +++ b/tools/PackageCompatibility/Directory.Packages.props @@ -37,7 +37,6 @@ - From d8eb64e19c6af92f14e47871804919fd78360bc1 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:57:08 -0300 Subject: [PATCH 6/9] Address Copilot review comments on PR 4242 - Fix net481 build: add IsExternalInit polyfill, GetRelativePath helper, and Split overload compat for net481 in BuildVersionOverrideTests - Use PackageVersions constants instead of hard-coded version strings in EntryPointTests.HelpOutputContainsDefaultVersions (fix #1) - Fix WaitForExit deadlock risk on net481: read stdout/stderr async before waiting; check timeout bool and kill/throw on expiry (fix #2) - Fix Directory.Packages.props typo: explcitily -> explicitly (fix #4) - Fix README.md typo: -p:SqlServerVersio= -> -p:SqlServerVersion= (fix #5) - Move WaitForCapturedOutput inside try block so late console output is captured before streams are restored (fix #6) - Use ArgumentList on NET and QuoteArgument helper on net481 so paths with spaces are passed correctly to dotnet build (fix #7) - Fix testing.instructions.md list continuation indents (4-space -> 2-space) so GitHub renders them as wrapped text not code blocks (fix #8) --- .github/instructions/testing.instructions.md | 8 +-- .../Directory.Packages.props | 2 +- tools/PackageCompatibility/README.md | 2 +- .../test/BuildVersionOverrideTests.cs | 69 +++++++++++++++++-- .../test/EntryPointTests.cs | 23 +++---- .../test/IsExternalInit.cs | 13 ++++ 6 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 tools/PackageCompatibility/test/IsExternalInit.cs diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index f59bd7bd9d..a9c3f1067e 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -182,16 +182,16 @@ class and method level. - Add XML `` comments to every test class. - Add XML `` comments to every test method (`[Fact]`, `[Theory]`, conditional variants). - For helper methods used by tests, add XML `` comments and XML `` / `` - where applicable. + where applicable. - For fixture and collection types, add XML `` comments describing why the fixture exists - (for example, serialization of console-mutating tests). + (for example, serialization of console-mutating tests). ### What the Comments Must Explain - The behavior/contract being tested (not just restating the method name). - Why the scenario matters (for example: regression guard, parsing contract, sync/async parity, - isolation requirement). + isolation requirement). - For helper methods, what side effects occur (for example console redirection, file system - copying, process execution) and why they are needed. + copying, process execution) and why they are needed. ### Style Guidance - Keep comments concise and factual. diff --git a/tools/PackageCompatibility/Directory.Packages.props b/tools/PackageCompatibility/Directory.Packages.props index da04009ad9..161a606355 100644 --- a/tools/PackageCompatibility/Directory.Packages.props +++ b/tools/PackageCompatibility/Directory.Packages.props @@ -17,7 +17,7 @@ diff --git a/tools/PackageCompatibility/README.md b/tools/PackageCompatibility/README.md index f1f3713ff2..48d7339155 100644 --- a/tools/PackageCompatibility/README.md +++ b/tools/PackageCompatibility/README.md @@ -184,7 +184,7 @@ dotnet run \ -p:AzureVersion=1.0.0 \ -p:LoggingVersion=1.0.1 \ -p:SqlClientVersion=7.1.0-preview1 \ - -p:SqlServerVersio=1.0.0 \ + -p:SqlServerVersion=1.0.0 \ -- -c "" ``` diff --git a/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs b/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs index 29e000d1c2..bea0afaa5d 100644 --- a/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs +++ b/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs @@ -138,13 +138,21 @@ private static BuildArtifacts BuildAppWithProperties(Dictionary var psi = new ProcessStartInfo { FileName = "dotnet", - Arguments = string.Join(" ", buildArgs), UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; +#if NET + foreach (string buildArg in buildArgs) + { + psi.ArgumentList.Add(buildArg); + } +#else + psi.Arguments = string.Join(" ", buildArgs.Select(QuoteArgument)); +#endif + using (var process = Process.Start(psi)) { if (process == null) @@ -162,12 +170,21 @@ private static BuildArtifacts BuildAppWithProperties(Dictionary process.Kill(); throw new InvalidOperationException("Build timed out after 60 seconds"); } -#else - process.WaitForExit(60000); -#endif stdout = process.StandardOutput.ReadToEnd(); stderr = process.StandardError.ReadToEnd(); +#else + Task stdoutTask = process.StandardOutput.ReadToEndAsync(); + Task stderrTask = process.StandardError.ReadToEndAsync(); + bool completed = process.WaitForExit(60000); + stdout = stdoutTask.GetAwaiter().GetResult(); + stderr = stderrTask.GetAwaiter().GetResult(); + if (!completed) + { + process.Kill(); + throw new InvalidOperationException("Build timed out after 60 seconds"); + } +#endif // Fail fast with full command/stdout/stderr context for easy diagnosis. if (process.ExitCode != 0) @@ -219,7 +236,7 @@ private static void CopyDirectory(string sourceDirectory, string destinationDire foreach (string directory in Directory.GetDirectories(sourceDirectory, "*", SearchOption.AllDirectories)) { - string relativePath = Path.GetRelativePath(sourceDirectory, directory); + string relativePath = GetRelativePath(sourceDirectory, directory); if (ShouldSkip(relativePath)) { continue; @@ -230,7 +247,7 @@ private static void CopyDirectory(string sourceDirectory, string destinationDire foreach (string file in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories)) { - string relativePath = Path.GetRelativePath(sourceDirectory, file); + string relativePath = GetRelativePath(sourceDirectory, file); if (ShouldSkip(relativePath)) { continue; @@ -251,7 +268,7 @@ private static bool ShouldSkip(string relativePath) { // Ignore generated artifacts and the test project itself to avoid recursive/self-copy issues. string normalizedPath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); - string[] segments = normalizedPath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries); + string[] segments = normalizedPath.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); foreach (string segment in segments) { @@ -308,4 +325,42 @@ private static string GetPackageCompatibilityDirectory() /// Build output directory for binaries. /// Path to generated PackageVersions.g.cs. private sealed record BuildArtifacts(string ProjectDirectory, string OutputDirectory, string GeneratedVersionsFile); + +#if !NET + /// + /// Quotes a single command-line argument for use with + /// on .NET Framework, where ArgumentList is unavailable. + /// Wraps the value in double-quotes and escapes any embedded double-quotes. + /// + /// The argument to quote. + /// The argument, quoted if it contains whitespace or double-quote characters. + private static string QuoteArgument(string arg) + { + if (arg.IndexOfAny(new[] { ' ', '\t', '"' }) < 0) + { + return arg; + } + + return "\"" + arg.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; + } +#endif + + /// + /// Returns the relative path from to . + /// Polyfills which is unavailable on .NET Framework. + /// + private static string GetRelativePath(string relativeTo, string path) + { +#if NET + return Path.GetRelativePath(relativeTo, path); +#else + // Ensure the base URI ends with a separator so MakeRelativeUri treats it as a directory. + string baseStr = relativeTo.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + + Path.DirectorySeparatorChar; + Uri baseUri = new Uri(baseStr); + Uri targetUri = new Uri(path); + return Uri.UnescapeDataString(baseUri.MakeRelativeUri(targetUri).ToString()) + .Replace('/', Path.DirectorySeparatorChar); +#endif + } } diff --git a/tools/PackageCompatibility/test/EntryPointTests.cs b/tools/PackageCompatibility/test/EntryPointTests.cs index e8dee72e2a..e77e7ed429 100644 --- a/tools/PackageCompatibility/test/EntryPointTests.cs +++ b/tools/PackageCompatibility/test/EntryPointTests.cs @@ -28,6 +28,7 @@ public void EntryPointWithoutConnectionStringReturnsNonZero() using StringWriter output = new(); int exitCode; + string commandOutput; try { @@ -36,6 +37,9 @@ public void EntryPointWithoutConnectionStringReturnsNonZero() // Act: invoke with no arguments; --connection-string is required. exitCode = EntryPoint.Main(Array.Empty()); + + // Wait for command help/validation text to flush before asserting. + commandOutput = WaitForCapturedOutput(output, "--connection-string"); } finally { @@ -44,9 +48,6 @@ public void EntryPointWithoutConnectionStringReturnsNonZero() Console.SetError(originalError); } - // Wait for command help/validation text to flush before asserting. - string commandOutput = WaitForCapturedOutput(output, "--connection-string"); - // Assert: parser should reject the command and mention the missing option. Assert.NotEqual(0, exitCode); Assert.Contains("--connection-string", commandOutput, StringComparison.Ordinal); @@ -75,7 +76,8 @@ public void HelpOutputContainsDefaultVersions() int exitCode = EntryPoint.Main(new[] { "--help" }); Assert.Equal(0, exitCode); - helpOutput = output.ToString(); + // Ensure final help footer is present so all formatted content has been emitted. + helpOutput = WaitForCapturedOutput(output, "--version"); } finally { @@ -84,9 +86,6 @@ public void HelpOutputContainsDefaultVersions() Console.SetError(originalError); } - // Ensure final help footer is present so all formatted content has been emitted. - helpOutput = WaitForCapturedOutput(output, "--version"); - // Assert: sample property overrides remain documented in help text. Assert.Contains("-p:AbstractionsVersion=1.0.1", helpOutput, StringComparison.Ordinal); Assert.Contains("-p:AkvProviderVersion=7.0.1-preview2", helpOutput, StringComparison.Ordinal); @@ -96,12 +95,12 @@ public void HelpOutputContainsDefaultVersions() Assert.Contains("-p:SqlServerVersion=1.0.0", helpOutput, StringComparison.Ordinal); // Assert: currently resolved package defaults are visible to aid troubleshooting. - Assert.Contains("Abstractions: 1.0.0", helpOutput, StringComparison.Ordinal); - Assert.Contains("AKV Provider: 7.0.0", helpOutput, StringComparison.Ordinal); + Assert.Contains($"Abstractions: {PackageVersions.MicrosoftDataSqlClientExtensionsAbstractions}", helpOutput, StringComparison.Ordinal); + Assert.Contains($"AKV Provider: {PackageVersions.MicrosoftDataSqlClientAlwaysEncryptedAzureKeyVaultProvider}", helpOutput, StringComparison.Ordinal); Assert.Contains("Azure: N/A", helpOutput, StringComparison.Ordinal); - Assert.Contains("Logging: 1.0.0", helpOutput, StringComparison.Ordinal); - Assert.Contains("SqlClient: 7.0.1", helpOutput, StringComparison.Ordinal); - Assert.Contains("SqlServer: 1.0.0", helpOutput, StringComparison.Ordinal); + Assert.Contains($"Logging: {PackageVersions.MicrosoftDataSqlClientInternalLogging}", helpOutput, StringComparison.Ordinal); + Assert.Contains($"SqlClient: {PackageVersions.MicrosoftDataSqlClient}", helpOutput, StringComparison.Ordinal); + Assert.Contains($"SqlServer: {PackageVersions.MicrosoftSqlServerServer}", helpOutput, StringComparison.Ordinal); } /// diff --git a/tools/PackageCompatibility/test/IsExternalInit.cs b/tools/PackageCompatibility/test/IsExternalInit.cs new file mode 100644 index 0000000000..d108129a8e --- /dev/null +++ b/tools/PackageCompatibility/test/IsExternalInit.cs @@ -0,0 +1,13 @@ +// 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. + +// Polyfill required for 'record' types and 'init' setters on .NET Framework. +// The compiler emits a reference to this type when using these C# features; .NET 5+ +// includes it in the BCL, but .NET Framework does not. +#if NETFRAMEWORK +namespace System.Runtime.CompilerServices +{ + internal static class IsExternalInit { } +} +#endif From cdeb1b18f7314d63dee533a2f3f991206fa6942f Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:52:04 -0300 Subject: [PATCH 7/9] Added explanation of why this tool is useful, and how it differs from running manual tests. --- tools/PackageCompatibility/README.md | 68 ++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tools/PackageCompatibility/README.md b/tools/PackageCompatibility/README.md index 48d7339155..7dead9d686 100644 --- a/tools/PackageCompatibility/README.md +++ b/tools/PackageCompatibility/README.md @@ -33,6 +33,74 @@ Specifically, it: The app is designed to run against both **published NuGet packages** and **locally-built packages** (via the `packages/` directory configured in `NuGet.config`). +## How This Differs From the Existing Test Suite + +The SqlClient repository has a large suite of unit, functional, and manual tests. This tool +complements — but does not replace — those tests. The key differences are: + +### Heterogeneous package versions + +The test projects reference sibling packages via project references or a shared `Directory.Packages. +props` file. Every test run uses a **single, uniform version set** derived from whatever is +currently checked out. You cannot ask the test suite to run `SqlClient 7.0.1` against +`AkvProvider 7.1.0-preview1` without editing project files. + +This tool accepts any combination of independent version numbers — including versions that have not +been published yet — at the command line: + +```bash +dotnet run \ + -p:SqlClientVersion=7.0.1 \ + -p:AkvProviderVersion=7.1.0-preview1 \ + -- -c "" +``` + +This makes it straightforward to answer questions like *"does the new AKV provider build work +against the last published SqlClient release?"* without modifying any source files. + +### Pre-release and locally-built packages + +Because NuGet resolves packages from the `packages/` local feed before falling back to NuGet.org, +you can drop pre-release `.nupkg` files in that folder and reference them immediately — even before +they have been published. The existing tests have no equivalent mechanism; they can only reference +packages that are either checked out as source or already published to a configured feed. + +### End-to-end runtime coverage across the full package graph + +The test suite exercises individual classes and APIs in isolation. Functional and manual tests do +open real connections, but they always run against the packages as built from source in the current +branch. + +This tool loads every package in the dependency graph simultaneously in a single process and then +opens a live `SqlConnection`. This catches a class of failures that isolated tests miss: + +- **Binding redirect conflicts**: two packages pulling in incompatible versions of a shared + dependency (`Azure.Core`, `Microsoft.Identity.*`, etc.) that only manifest when all packages are + present in the same AppDomain. +- **Transitive version mismatches**: a package expecting an internal API surface that has changed in + a sibling package across a version boundary. +- **Registration side-effects**: authentication providers or other singleton registrations that + interfere when packages are composed in an unexpected order or version combination. + +### Diagnostic console output + +When a connection fails, the tool can emit structured TDS-level and authentication trace output via +the `--log` and `--trace` flags. This output is written directly to the console and requires no +test harness or log configuration — useful for quickly diagnosing authentication failures in CI +environments or on developer machines where full test infrastructure is unavailable. + +The test suite's diagnostics are routed through `EventSource`/`DiagnosticListener` and are only +visible if a listener is attached (e.g. via `dotnet-trace` or a custom test initializer). + +### What this tool does NOT do + +- It does not assert on individual API behaviours, query results, or error messages. For that, use + the existing unit and functional tests. +- It cannot run without a real SQL Server instance. The manual test suite has the same constraint + for connectivity tests, but unit and functional tests run without a server. +- It does not cover every authentication mode automatically. You must provide a suitable connection + string for each mode you want to validate. + ## Project Layout - `src/` contains the tool source files and project file. From 311a570380254e7e04c52c0d8273aba5df5232df Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Fri, 29 May 2026 10:54:48 -0300 Subject: [PATCH 8/9] Address review feedback: temp cleanup, packages/ exclusion, heading levels, console fixture - Exclude packages/ in ShouldSkip to avoid copying large local .nupkg files; create an empty packages/ dir in the temp workspace for NuGet.config validity. - Add try/finally cleanup of temp build directories (opt-out via PACKAGECOMPATIBILITY_PRESERVE_TEMP env var). - Demote sub-headings under 'Test Documentation Requirements' from ### to #### so they render as subsections of the ### parent. - Move console capture/restore into EntryPointTests constructor/Dispose to eliminate duplicated setup/teardown in each test method. --- .github/instructions/testing.instructions.md | 10 +-- .../test/BuildVersionOverrideTests.cs | 57 +++++++++++-- .../test/ConsoleCollection.cs | 5 +- .../test/EntryPointTests.cs | 79 +++++++------------ 4 files changed, 88 insertions(+), 63 deletions(-) diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index a9c3f1067e..8e1990be9e 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -173,12 +173,12 @@ dotnet test ... --filter "FullyQualifiedName=Namespace.ClassName.MethodName" ## Writing Tests -## Test Documentation Requirements +### Test Documentation Requirements To keep tests maintainable for contributors and AI agents, test intent must be documented at class and method level. -### Required XML Documentation +#### Required XML Documentation - Add XML `` comments to every test class. - Add XML `` comments to every test method (`[Fact]`, `[Theory]`, conditional variants). - For helper methods used by tests, add XML `` comments and XML `` / `` @@ -186,20 +186,20 @@ class and method level. - For fixture and collection types, add XML `` comments describing why the fixture exists (for example, serialization of console-mutating tests). -### What the Comments Must Explain +#### What the Comments Must Explain - The behavior/contract being tested (not just restating the method name). - Why the scenario matters (for example: regression guard, parsing contract, sync/async parity, isolation requirement). - For helper methods, what side effects occur (for example console redirection, file system copying, process execution) and why they are needed. -### Style Guidance +#### Style Guidance - Keep comments concise and factual. - Prefer behavior-focused wording over implementation trivia. - Avoid comments that merely repeat obvious code. - Use inline comments inside test methods only for non-obvious setup/act/assert details. -### Example +#### Example ```csharp /// /// Ensures malformed connection strings return a non-zero exit code and emit a parse error diff --git a/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs b/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs index bea0afaa5d..f45a217a4c 100644 --- a/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs +++ b/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs @@ -28,15 +28,26 @@ public void BuildGeneratesExpectedPackageVersionsWhenCustomPropertiesAreSupplied // Build in an isolated copy so per-test overrides cannot leak into repo obj/bin state. BuildArtifacts artifacts = BuildAppWithProperties(buildProperties); - // The generated source file is the authoritative output of GeneratePackageVersions.targets. - string generatedVersions = File.ReadAllText(artifacts.GeneratedVersionsFile); + try + { + // The generated source file is the authoritative output of GeneratePackageVersions.targets. + string generatedVersions = File.ReadAllText(artifacts.GeneratedVersionsFile); - // Assert each expected package label maps to the generated constant/version pair. - foreach (var kvp in expectedVersions) + // Assert each expected package label maps to the generated constant/version pair. + foreach (var kvp in expectedVersions) + { + string constantName = GetPackageVersionsConstantName(kvp.Key); + string expected = $"public const string {constantName} = \"{kvp.Value}\";"; + Assert.Contains(expected, generatedVersions, StringComparison.Ordinal); + } + } + finally { - string constantName = GetPackageVersionsConstantName(kvp.Key); - string expected = $"public const string {constantName} = \"{kvp.Value}\";"; - Assert.Contains(expected, generatedVersions, StringComparison.Ordinal); + if (!ShouldPreserveTempBuildDirectories()) + { + TryDeleteDirectory(artifacts.ProjectDirectory); + TryDeleteDirectory(artifacts.OutputDirectory); + } } } @@ -107,13 +118,44 @@ public static IEnumerable GetVersionPropertyTestCases() /// /// MSBuild properties passed via -p: arguments. /// Paths to the isolated workspace, output folder, and generated versions file. + private static bool ShouldPreserveTempBuildDirectories() + { + string? preserve = Environment.GetEnvironmentVariable("PACKAGECOMPATIBILITY_PRESERVE_TEMP"); + return string.Equals(preserve, "1", StringComparison.OrdinalIgnoreCase) + || string.Equals(preserve, "true", StringComparison.OrdinalIgnoreCase); + } + + private static void TryDeleteDirectory(string path) + { + if (string.IsNullOrEmpty(path) || !Directory.Exists(path)) + { + return; + } + + try + { + Directory.Delete(path, recursive: true); + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + } + private static BuildArtifacts BuildAppWithProperties(Dictionary properties) { // Locate and clone the package-compatibility subtree so test builds are hermetic. string packageCompatibilityDir = GetPackageCompatibilityDirectory(); string tempProjectDir = Path.Combine(Path.GetTempPath(), $"PackageCompatibilityProject_{Guid.NewGuid():N}"); string tempOutputDir = Path.Combine(Path.GetTempPath(), $"PackageCompatibility_{Guid.NewGuid():N}"); + bool preserveTemp = ShouldPreserveTempBuildDirectories(); + CopyDirectory(packageCompatibilityDir, tempProjectDir); + + // Create an empty packages/ directory so NuGet.config source path remains valid. + Directory.CreateDirectory(Path.Combine(tempProjectDir, "packages")); Directory.CreateDirectory(tempOutputDir); string copiedProjectFile = Path.Combine(tempProjectDir, "src", "PackageCompatibility.csproj"); @@ -274,6 +316,7 @@ private static bool ShouldSkip(string relativePath) { if (segment.Equals("bin", StringComparison.Ordinal) || segment.Equals("obj", StringComparison.Ordinal) + || segment.Equals("packages", StringComparison.Ordinal) || segment.Equals("test", StringComparison.Ordinal)) { return true; diff --git a/tools/PackageCompatibility/test/ConsoleCollection.cs b/tools/PackageCompatibility/test/ConsoleCollection.cs index f5eb393f9b..1072c85279 100644 --- a/tools/PackageCompatibility/test/ConsoleCollection.cs +++ b/tools/PackageCompatibility/test/ConsoleCollection.cs @@ -18,8 +18,9 @@ public sealed class ConsoleCollection : ICollectionFixture /// Fixture type associated with . -/// Present to satisfy xUnit collection fixture wiring and to provide a future extension point -/// for shared setup/teardown if console coordination needs to expand. +/// Present to satisfy xUnit collection fixture wiring; individual test classes in this collection +/// capture and restore console streams in their own constructor/Dispose lifecycle to keep each +/// test isolated. /// public sealed class ConsoleCollectionFixture { diff --git a/tools/PackageCompatibility/test/EntryPointTests.cs b/tools/PackageCompatibility/test/EntryPointTests.cs index e77e7ed429..a068a5e8f8 100644 --- a/tools/PackageCompatibility/test/EntryPointTests.cs +++ b/tools/PackageCompatibility/test/EntryPointTests.cs @@ -13,8 +13,28 @@ namespace Microsoft.Data.SqlClient.Tools.PackageCompatibility.Tests; /// content that documents default package versions and override examples. /// [Collection(ConsoleCollection.Name)] -public class EntryPointTests +public class EntryPointTests : IDisposable { + private readonly TextWriter _originalOut; + private readonly TextWriter _originalError; + private readonly StringWriter _output; + + public EntryPointTests(ConsoleCollectionFixture _) + { + _originalOut = Console.Out; + _originalError = Console.Error; + _output = new StringWriter(); + Console.SetOut(_output); + Console.SetError(_output); + } + + public void Dispose() + { + Console.SetOut(_originalOut); + Console.SetError(_originalError); + _output.Dispose(); + } + /// /// Ensures invoking the entry point without the required connection-string argument fails and /// reports the missing option to the caller. @@ -22,31 +42,11 @@ public class EntryPointTests [Fact] public void EntryPointWithoutConnectionStringReturnsNonZero() { - // Redirect console streams so we can assert on CLI output without polluting test logs. - TextWriter originalOut = Console.Out; - TextWriter originalError = Console.Error; - using StringWriter output = new(); + // Act: invoke with no arguments; --connection-string is required. + int exitCode = EntryPoint.Main(Array.Empty()); - int exitCode; - string commandOutput; - - try - { - Console.SetOut(output); - Console.SetError(output); - - // Act: invoke with no arguments; --connection-string is required. - exitCode = EntryPoint.Main(Array.Empty()); - - // Wait for command help/validation text to flush before asserting. - commandOutput = WaitForCapturedOutput(output, "--connection-string"); - } - finally - { - // Always restore global console streams for subsequent tests. - Console.SetOut(originalOut); - Console.SetError(originalError); - } + // Wait for command help/validation text to flush before asserting. + string commandOutput = WaitForCapturedOutput(_output, "--connection-string"); // Assert: parser should reject the command and mention the missing option. Assert.NotEqual(0, exitCode); @@ -60,31 +60,12 @@ public void EntryPointWithoutConnectionStringReturnsNonZero() [Fact] public void HelpOutputContainsDefaultVersions() { - // Capture help output for contract validation of documented build parameters. - TextWriter originalOut = Console.Out; - TextWriter originalError = Console.Error; - using StringWriter output = new(); - - string helpOutput; - - try - { - Console.SetOut(output); - Console.SetError(output); - - // Act: request help; this should never fail. - int exitCode = EntryPoint.Main(new[] { "--help" }); - Assert.Equal(0, exitCode); + // Act: request help; this should never fail. + int exitCode = EntryPoint.Main(new[] { "--help" }); + Assert.Equal(0, exitCode); - // Ensure final help footer is present so all formatted content has been emitted. - helpOutput = WaitForCapturedOutput(output, "--version"); - } - finally - { - // Restore global console state to keep tests isolated. - Console.SetOut(originalOut); - Console.SetError(originalError); - } + // Ensure final help footer is present so all formatted content has been emitted. + string helpOutput = WaitForCapturedOutput(_output, "--version"); // Assert: sample property overrides remain documented in help text. Assert.Contains("-p:AbstractionsVersion=1.0.1", helpOutput, StringComparison.Ordinal); From dfa046c6ea82be566652909219b12861d7361c28 Mon Sep 17 00:00:00 2001 From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com> Date: Fri, 29 May 2026 12:41:26 -0300 Subject: [PATCH 9/9] Remove unused preserveTemp local; align subtree SDK version with repo root - Remove dead 'bool preserveTemp' in BuildAppWithProperties (cleanup is controlled by the caller's ShouldPreserveTempBuildDirectories() call). - Update tools/PackageCompatibility/global.json SDK version from 10.0.107 to 10.0.300 to match the repo-root global.json. --- tools/PackageCompatibility/global.json | 2 +- tools/PackageCompatibility/test/BuildVersionOverrideTests.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/PackageCompatibility/global.json b/tools/PackageCompatibility/global.json index 2de58c1156..7c7fca5caa 100644 --- a/tools/PackageCompatibility/global.json +++ b/tools/PackageCompatibility/global.json @@ -8,7 +8,7 @@ // global.json lookup stops at the nearest file and does not merge with the repo-root file. // This subtree therefore needs to repeat the root SDK settings so commands run from // tools/PackageCompatibility keep using the same pinned SDK behavior. - "version": "10.0.107", + "version": "10.0.300", "rollForward": "disable", "allowPrerelease": false } diff --git a/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs b/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs index f45a217a4c..493e1d1209 100644 --- a/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs +++ b/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs @@ -150,7 +150,6 @@ private static BuildArtifacts BuildAppWithProperties(Dictionary string packageCompatibilityDir = GetPackageCompatibilityDirectory(); string tempProjectDir = Path.Combine(Path.GetTempPath(), $"PackageCompatibilityProject_{Guid.NewGuid():N}"); string tempOutputDir = Path.Combine(Path.GetTempPath(), $"PackageCompatibility_{Guid.NewGuid():N}"); - bool preserveTemp = ShouldPreserveTempBuildDirectories(); CopyDirectory(packageCompatibilityDir, tempProjectDir);