diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index 20fe03b1e8..8e1990be9e 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/doc/apps/AzureAuthentication/Directory.Packages.props b/doc/apps/AzureAuthentication/Directory.Packages.props deleted file mode 100644 index b512683765..0000000000 --- a/doc/apps/AzureAuthentication/Directory.Packages.props +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - 7.0.0-preview4.26064.3 - - - 7.0.0-preview1.26064.3 - - - - - - - - - - - - - - - - - - diff --git a/doc/apps/AzureAuthentication/README.md b/doc/apps/AzureAuthentication/README.md deleted file mode 100644 index 967a728007..0000000000 --- a/doc/apps/AzureAuthentication/README.md +++ /dev/null @@ -1,165 +0,0 @@ -# AzureAuthentication Sample App - -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 -references the **Azure Key Vault Provider** package to confirm there are no transitive dependency -conflicts between the packages. - -The following SqlClient packages are used, either directly or transitively: - -- `Microsoft.Data.SqlClient` -- `Microsoft.SqlServer.Server` -- `Microsoft.Data.SqlClient.Internal.Logging` -- `Microsoft.Data.SqlClient.Extensions.Abstractions` -- `Microsoft.Data.SqlClient.Extensions.Azure` -- `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` - -## Purpose - -This app serves as a smoke test for package compatibility. It: - -1. Instantiates `SqlColumnEncryptionAzureKeyVaultProvider` to ensure the AKV provider assembly loads - without conflicts. -2. Opens a `SqlConnection` using a connection string you provide, validating that authentication and - connectivity work end-to-end. - -The app is designed to run against both **published NuGet packages** and **locally-built packages** -(via the `packages/` directory configured in `NuGet.config`). - -## Build Parameters - -Package versions are controlled through build properties. Pass them on the command line with `-p:` -(or `/p:`) to override the defaults defined in `Directory.Packages.props`. - -| 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. | -| `AzureVersion` | None | Version of `Microsoft.Data.SqlClient.Extensions.Azure` to reference. When omitted, the `Azure` package will not be referenced. | - -## Local Package Source - -The `NuGet.config` adds a `packages/` directory as a local package source. To test against packages -that haven't been published to NuGet yet, copy the `.nupkg` files into this folder and specify the -matching version via the build properties above. - -NuGet will cache copies of the packages it finds in `packages/` after a successful restore. If you -update the `.nupkg` files in `packages/` without incrementing their version numbers (and referencing -those new version numbers) you will have to clear the NuGet caches in order for the next restore -operation to pick them up: - -```bash -dotnet nuget locals all --clear -``` - -## Running the App - -The app has built-in help: - -```bash -dotnet run -- --help - -Description: - Azure Authentication 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. - - 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 - -Usage: - AzureAuthentication [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. - -```bash -dotnet run -- -c "" -``` - -For Entra ID authentication, use an `Authentication` keyword in the connection string. For example: - -```bash -dotnet run -- -c "Server=myserver.database.windows.net;Database=mydb;Authentication=ActiveDirectoryDefault" -``` - -On success the app emits to standard out: - -```bash -Azure Authentication Tester ---------------------------- - -Packages used: - SqlClient: 7.0.0-preview4.26055.1 - AKV Provider: 6.1.2 - Azure: 1.0.0-preview1.26055.1 - -Connection details: - Data Source: adotest.database.windows.net - Initial Catalog: Northwind - Authentication: ActiveDirectoryPassword - -Testing connectivity... -Connected successfully! - Server version: 12.00.1017 -``` - -Errors will be emitted to standard error: - -```bash -Testing connectivity... -Connection failed: - Cannot find an authentication provider for 'ActiveDirectoryPassword'. -``` - -### Examples - -Run with the default (published) package versions, and no `Azure` package: - -```bash -dotnet run -- -c "" -``` - -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): - -```bash -dotnet run -p:SqlClientVersion=7.0.0-preview4 -- -c "" -``` - -Run including the `Azure` extensions package: - -```bash -dotnet run -p:AzureVersion=1.0.0-preview1 -- -c "" -``` - -Override all three 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 "" -``` - -## 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). 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/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/tools/PackageCompatibility/Directory.Packages.props b/tools/PackageCompatibility/Directory.Packages.props new file mode 100644 index 0000000000..161a606355 --- /dev/null +++ b/tools/PackageCompatibility/Directory.Packages.props @@ -0,0 +1,44 @@ + + + + + + + + 1.0.0 + 7.0.0 + 1.0.0 + 7.0.1 + 1.0.0 + + + + + + + + + + + + + + + + + + + + + + + + + + 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/tools/PackageCompatibility/README.md b/tools/PackageCompatibility/README.md new file mode 100644 index 0000000000..7dead9d686 --- /dev/null +++ b/tools/PackageCompatibility/README.md @@ -0,0 +1,265 @@ +# PackageCompatibility Tool + +A minimal console application that verifies that a set of SqlClient packages can coexist without +transitive dependency conflicts, API surface mismatches, or broken runtime functionality. It loads +assemblies from each package and then opens a `SqlConnection` to confirm that the resolved package +graph works end-to-end against a real SQL Server instance. + +The following SqlClient packages are verified, either directly or transitively: + +- `Microsoft.Data.SqlClient` +- `Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider` +- `Microsoft.Data.SqlClient.Internal.Logging` +- `Microsoft.Data.SqlClient.Extensions.Abstractions` +- `Microsoft.Data.SqlClient.Extensions.Azure` *(optional — included when `AzureVersion` is set)* +- `Microsoft.SqlServer.Server` + +## Purpose + +This tool is a smoke test for inter-package compatibility across the SqlClient suite. It catches +problems such as: + +- Assembly binding conflicts caused by mismatched transitive dependencies. +- API surface mismatches between independently-versioned packages. +- Runtime failures that only appear when multiple packages are loaded together. + +Specifically, it: + +1. Instantiates `SqlColumnEncryptionAzureKeyVaultProvider` to force the AKV provider assembly and + all its transitive dependencies to load alongside SqlClient. +2. Opens a `SqlConnection` using a connection string you provide, exercising the authentication + and network code paths end-to-end. + +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. +- `test/` is reserved for tests. + +## Authentication Modes + +The authentication mode embedded in the connection string controls which code paths and packages are +exercised during the connectivity test. Use different modes to broaden coverage: + +| Authentication mode | `Authentication=` value | Packages exercised | +| --- | --- | --- | +| SQL Server auth | *(omit or `SqlPassword`)* | `Microsoft.Data.SqlClient` only | +| Windows / Integrated | `ActiveDirectoryIntegrated` | `Microsoft.Data.SqlClient` + SSPI | +| Entra ID — default chain | `ActiveDirectoryDefault` | `Microsoft.Data.SqlClient` + `Extensions.Azure` (requires `AzureVersion`) | +| Entra ID — password | `ActiveDirectoryPassword` | `Microsoft.Data.SqlClient` + `Extensions.Azure` (requires `AzureVersion`) | +| Entra ID — interactive | `ActiveDirectoryInteractive` | `Microsoft.Data.SqlClient` + `Extensions.Azure` (requires `AzureVersion`) | +| Entra ID — service principal | `ActiveDirectoryServicePrincipal` | `Microsoft.Data.SqlClient` + `Extensions.Azure` (requires `AzureVersion`) | +| Entra ID — managed identity | `ActiveDirectoryManagedIdentity` | `Microsoft.Data.SqlClient` + `Extensions.Azure` (requires `AzureVersion`) | + +> **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:` +(or `/p:`) to override the defaults defined in `Directory.Packages.props`. + +| Property | Default | Description | +| --- | --- | --- | +| `AbstractionsVersion` | `1.0.0` | Version of `Microsoft.Data.SqlClient.Extensions.Abstractions` 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.1` | Version of `Microsoft.Data.SqlClient` to reference. | +| `SqlServerVersion` | `1.0.0` | Version of `Microsoft.SqlServer.Server` to reference. | + +## Local Package Source + +The `NuGet.config` adds a `packages/` directory as a local package source. To test against packages +that haven't been published to NuGet yet, copy the `.nupkg` files into this folder and specify the +matching version via the build properties above. + +NuGet will cache copies of the packages it finds in `packages/` after a successful restore. If you +update the `.nupkg` files in `packages/` without incrementing their version numbers (and referencing +those new version numbers) you will have to clear the NuGet caches in order for the next restore +operation to pick them up: + +```bash +dotnet nuget locals all --clear +``` + +## Running the App + +The app has built-in help: + +```bash +dotnet run -- --help + +Description: + 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. + + ... +``` + +The app requires a connection string. Use SQL authentication for a basic connectivity check: + +```bash +dotnet run -- -c "Server=myserver;Database=mydb;User ID=sa;Password=;Encrypt=Mandatory;TrustServerCertificate=true" +``` + +To exercise Entra ID flows, include an `Authentication` keyword and reference the `Extensions.Azure` +package: + +```bash +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: + +```bash +Package Compatibility Tester +---------------------------- + +Packages used: + Abstractions: 1.0.1 + 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 + Initial Catalog: Northwind + Authentication: ActiveDirectoryPassword + +Testing connectivity... +Connected successfully! + Server version: 12.00.1017 +``` + +Errors will be emitted to standard error: + +```bash +Testing connectivity... +Connection failed: + Cannot find an authentication provider for 'ActiveDirectoryPassword'. +``` + +### Examples + +Run a basic SQL authentication check using the default package versions: + +```bash +dotnet run -- -c "Server=myserver;Database=mydb;User ID=sa;Password=;Encrypt=Mandatory;TrustServerCertificate=true" +``` + +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 -- -c "" +``` + +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:AbstractionsVersion=1.0.1 \ + -p:AkvProviderVersion=7.1.0-preview1 \ + -p:AzureVersion=1.0.0 \ + -p:LoggingVersion=1.0.1 \ + -p:SqlClientVersion=7.1.0-preview1 \ + -p:SqlServerVersion=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 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. diff --git a/tools/PackageCompatibility/global.json b/tools/PackageCompatibility/global.json new file mode 100644 index 0000000000..7c7fca5caa --- /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.300", + "rollForward": "disable", + "allowPrerelease": false + } +} 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 diff --git a/doc/apps/AzureAuthentication/App.cs b/tools/PackageCompatibility/src/App.cs similarity index 92% rename from doc/apps/AzureAuthentication/App.cs rename to tools/PackageCompatibility/src/App.cs index 3447e5489f..2864ad0158 100644 --- a/doc/apps/AzureAuthentication/App.cs +++ b/tools/PackageCompatibility/src/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 @@ -63,9 +63,12 @@ internal int Run(RunOptions options) --------------------------- Packages used: - SqlClient: {PackageVersions.MicrosoftDataSqlClient} + Abstractions: {PackageVersions.MicrosoftDataSqlClientExtensionsAbstractions} AKV Provider: {PackageVersions.MicrosoftDataSqlClientAlwaysEncryptedAzureKeyVaultProvider} - Azure: {PackageVersions.AzureExtensionsVersion} + Azure: {PackageVersions.MicrosoftDataSqlClientExtensionsAzureVersion} + Logging: {PackageVersions.MicrosoftDataSqlClientInternalLogging} + SqlClient: {PackageVersions.MicrosoftDataSqlClient} + SqlServer: {PackageVersions.MicrosoftSqlServerServer} """); @@ -192,7 +195,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/EntryPoint.cs b/tools/PackageCompatibility/src/EntryPoint.cs similarity index 84% rename from doc/apps/AzureAuthentication/EntryPoint.cs rename to tools/PackageCompatibility/src/EntryPoint.cs index 20885ae1af..8729352024 100644 --- a/doc/apps/AzureAuthentication/EntryPoint.cs +++ b/tools/PackageCompatibility/src/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 @@ -50,14 +50,20 @@ which must specify the authentication method. Supply specific package versions when building to test different versions of the SqlClient suite, for example: - -p:SqlClientVersion=7.0.0-preview4 + -p:AbstractionsVersion=1.0.1 -p:AkvProviderVersion=7.0.1-preview2 -p:AzureVersion=1.0.0-preview1 + -p:LoggingVersion=1.0.2 + -p:SqlClientVersion=7.0.0-preview4 + -p:SqlServerVersion=1.0.0 Current package versions: + Logging: {PackageVersions.MicrosoftDataSqlClientInternalLogging} + Abstractions: {PackageVersions.MicrosoftDataSqlClientExtensionsAbstractions} SqlClient: {PackageVersions.MicrosoftDataSqlClient} + SqlServer: {PackageVersions.MicrosoftSqlServerServer} AKV Provider: {PackageVersions.MicrosoftDataSqlClientAlwaysEncryptedAzureKeyVaultProvider} - Azure: {PackageVersions.AzureExtensionsVersion} + Azure: {PackageVersions.MicrosoftDataSqlClientExtensionsAzureVersion} """) { connectionStringOption, diff --git a/doc/apps/AzureAuthentication/GeneratePackageVersions.targets b/tools/PackageCompatibility/src/GeneratePackageVersions.targets similarity index 100% rename from doc/apps/AzureAuthentication/GeneratePackageVersions.targets rename to tools/PackageCompatibility/src/GeneratePackageVersions.targets diff --git a/doc/apps/AzureAuthentication/AzureAuthentication.csproj b/tools/PackageCompatibility/src/PackageCompatibility.csproj similarity index 73% rename from doc/apps/AzureAuthentication/AzureAuthentication.csproj rename to tools/PackageCompatibility/src/PackageCompatibility.csproj index 8b7c6f5ee9..d33eff8e52 100644 --- a/doc/apps/AzureAuthentication/AzureAuthentication.csproj +++ b/tools/PackageCompatibility/src/PackageCompatibility.csproj @@ -2,8 +2,11 @@ Exe + net481;net10.0 - Microsoft.Data.SqlClient.Samples.AzureAuthentication + + Microsoft.Data.SqlClient.Tools.PackageCompatibility + enable enable latest @@ -12,11 +15,10 @@ - + + + + + + <_Parameter1>PackageCompatibility.Test + + + diff --git a/doc/apps/AzureAuthentication/PackageVersions.Partial.cs b/tools/PackageCompatibility/src/PackageVersions.Partial.cs similarity index 85% rename from doc/apps/AzureAuthentication/PackageVersions.Partial.cs rename to tools/PackageCompatibility/src/PackageVersions.Partial.cs index 3c88781daa..0e34418e85 100644 --- a/doc/apps/AzureAuthentication/PackageVersions.Partial.cs +++ b/tools/PackageCompatibility/src/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 @@ -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/SqlClientEventListener.cs b/tools/PackageCompatibility/src/SqlClientEventListener.cs similarity index 97% rename from doc/apps/AzureAuthentication/SqlClientEventListener.cs rename to tools/PackageCompatibility/src/SqlClientEventListener.cs index 5e861f4796..686a4f2c9a 100644 --- a/doc/apps/AzureAuthentication/SqlClientEventListener.cs +++ b/tools/PackageCompatibility/src/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/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..493e1d1209 --- /dev/null +++ b/tools/PackageCompatibility/test/BuildVersionOverrideTests.cs @@ -0,0 +1,408 @@ +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); + + 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) + { + string constantName = GetPackageVersionsConstantName(kvp.Key); + string expected = $"public const string {constantName} = \"{kvp.Value}\";"; + Assert.Contains(expected, generatedVersions, StringComparison.Ordinal); + } + } + finally + { + if (!ShouldPreserveTempBuildDirectories()) + { + TryDeleteDirectory(artifacts.ProjectDirectory); + TryDeleteDirectory(artifacts.OutputDirectory); + } + } + } + + /// + /// 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 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}"); + + 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"); + 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", + 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) + { + 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"); + } + + 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) + { + 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 = GetRelativePath(sourceDirectory, directory); + if (ShouldSkip(relativePath)) + { + continue; + } + + Directory.CreateDirectory(Path.Combine(destinationDirectory, relativePath)); + } + + foreach (string file in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories)) + { + string relativePath = 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(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string segment in segments) + { + if (segment.Equals("bin", StringComparison.Ordinal) + || segment.Equals("obj", StringComparison.Ordinal) + || segment.Equals("packages", 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); + +#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/ConsoleCollection.cs b/tools/PackageCompatibility/test/ConsoleCollection.cs new file mode 100644 index 0000000000..1072c85279 --- /dev/null +++ b/tools/PackageCompatibility/test/ConsoleCollection.cs @@ -0,0 +1,27 @@ +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; 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 +{ +} \ 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..a068a5e8f8 --- /dev/null +++ b/tools/PackageCompatibility/test/EntryPointTests.cs @@ -0,0 +1,103 @@ +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 : 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. + /// + [Fact] + public void EntryPointWithoutConnectionStringReturnsNonZero() + { + // Act: invoke with no arguments; --connection-string is required. + int exitCode = EntryPoint.Main(Array.Empty()); + + // 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() + { + // 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. + string 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: {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: {PackageVersions.MicrosoftDataSqlClientInternalLogging}", helpOutput, StringComparison.Ordinal); + Assert.Contains($"SqlClient: {PackageVersions.MicrosoftDataSqlClient}", helpOutput, StringComparison.Ordinal); + Assert.Contains($"SqlServer: {PackageVersions.MicrosoftSqlServerServer}", 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/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 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 + + + + + + + + + + +