From 782721d48bdb1f4f2cf092f686881fed66d2ea88 Mon Sep 17 00:00:00 2001
From: Cheena Malhotra <13396919+cheenamalhotra@users.noreply.github.com>
Date: Fri, 15 Aug 2025 03:57:45 -0700
Subject: [PATCH 01/11] Update AKV versions and dependencies (#3569)
Updated AKV nuspec to note compatibility with MDS 6.1.1.
---
eng/pipelines/variables/akv-official-variables.yml | 4 ++--
...a.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/eng/pipelines/variables/akv-official-variables.yml b/eng/pipelines/variables/akv-official-variables.yml
index f30f0e67e4..8e602cb8f9 100644
--- a/eng/pipelines/variables/akv-official-variables.yml
+++ b/eng/pipelines/variables/akv-official-variables.yml
@@ -22,7 +22,7 @@ variables:
# Base Variables -------------------------------------------------------
- name: mdsPackageVersion
- value: '6.1.0'
+ value: '6.1.1'
# @TODO: Version should ideally be pulled from one location (versions.props?)
- name: versionMajor
@@ -30,7 +30,7 @@ variables:
- name: versionMinor
value: '1'
- name: versionPatch
- value: '0'
+ value: '1'
- name: versionPreview
value: '-preview1'
diff --git a/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec b/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec
index 968237fd36..2140a32e7a 100644
--- a/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec
+++ b/tools/specs/add-ons/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.nuspec
@@ -25,19 +25,19 @@ Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyStoreProvider.SqlColumnEncrypti
sqlclient microsoft.data.sqlclient azurekeyvaultprovider akvprovider alwaysencrypted
-
+
-
+
-
+
From 84b3c81170bbe26984de3c6355c237bf336529f2 Mon Sep 17 00:00:00 2001
From: Benjamin Russell
Date: Tue, 19 Aug 2025 15:02:56 -0500
Subject: [PATCH 02/11] [6.1] AKV Provider Strong Name Signing (#3573)
* * Add signing key path to roslyn analyzers
* Make signing key path an argument to build and roslyn analyzers steps
* Enable strong name signing on buddy (unofficial) builds
* Add akv official job to solution file
* Remove diagnostic step
... thought I removed that already :man_facepalming:
* Add string type to roslyn analyzer step argument
* vbump
(bypassing rules because AKV build isn't tested via CI)
---
eng/pipelines/jobs/build-akv-official-job.yml | 2 ++
eng/pipelines/steps/compound-build-akv-step.yml | 5 ++++-
eng/pipelines/steps/roslyn-analyzers-akv-step.yml | 14 +++++++++++---
eng/pipelines/variables/akv-official-variables.yml | 2 +-
src/Microsoft.Data.SqlClient.sln | 3 +++
...nt.AlwaysEncrypted.AzureKeyVaultProvider.csproj | 6 +++---
6 files changed, 24 insertions(+), 8 deletions(-)
diff --git a/eng/pipelines/jobs/build-akv-official-job.yml b/eng/pipelines/jobs/build-akv-official-job.yml
index a4374b773b..af8f546eff 100644
--- a/eng/pipelines/jobs/build-akv-official-job.yml
+++ b/eng/pipelines/jobs/build-akv-official-job.yml
@@ -91,6 +91,7 @@ jobs:
assemblyFileVersion: '${{ parameters.assemblyFileVersion }}'
buildConfiguration: '${{ parameters.buildConfiguration }}'
mdsPackageVersion: '${{ parameters.mdsPackageVersion }}'
+ signingKeyPath: '$(Agent.TempDirectory)/netfxKeypair.snk'
- ${{ each targetFramework in parameters.targetFrameworks }}:
- template: ../steps/compound-extract-akv-apiscan-files-step.yml
@@ -105,6 +106,7 @@ jobs:
parameters:
buildConfiguration: '${{ parameters.buildConfiguration }}'
mdsPackageVersion: '${{ parameters.mdsPackageVersion }}'
+ signingKeyPath: '$(Agent.TempDirectory)/netfxKeypair.snk'
- template: ../steps/compound-esrp-code-signing-step.yml@self
parameters:
diff --git a/eng/pipelines/steps/compound-build-akv-step.yml b/eng/pipelines/steps/compound-build-akv-step.yml
index 906dcfaf72..fb6b0e2a06 100644
--- a/eng/pipelines/steps/compound-build-akv-step.yml
+++ b/eng/pipelines/steps/compound-build-akv-step.yml
@@ -19,6 +19,9 @@ parameters:
- name: mdsPackageVersion
type: string
+ - name: signingKeyPath
+ type: string
+
steps:
- task: DownloadSecureFile@1
displayName: 'Download Signing Key'
@@ -48,7 +51,7 @@ steps:
-p:AssemblyFileVersion=${{ parameters.assemblyFileVersion }}
-p:NugetPackageVersion=${{ parameters.mdsPackageVersion }}
-p:ReferenceType=Package
- -p:SigningKeyPath=$(Agent.TempDirectory)/netfxKeypair.snk
+ -p:SigningKeyPath=${{ parameters.signingKeyPath }}
- script: tree /a /f $(BUILD_OUTPUT)
displayName: Output Build Output Tree
diff --git a/eng/pipelines/steps/roslyn-analyzers-akv-step.yml b/eng/pipelines/steps/roslyn-analyzers-akv-step.yml
index 0e05177d5a..d65ec57ca4 100644
--- a/eng/pipelines/steps/roslyn-analyzers-akv-step.yml
+++ b/eng/pipelines/steps/roslyn-analyzers-akv-step.yml
@@ -4,9 +4,13 @@
# See the LICENSE file in the project root for more information. #
#################################################################################
-# @TODO: This can probably be made generic and pass in the command lines for msbuild
-# BUT, they should be kept separate by now as we rebuild build.proj in parallel, we won't
-# affect >1 project at a time.
+# NOTE: Because Roslyn analyzers run with the build process, this step must happen within our
+# build in order to generate logs that Guardian/SDL can consume. HOWEVER - this step will rebuild
+# the project and overwrite any previously build output! Therefore, the command line params in
+# this step and the build step must be the same to avoid packaging invalid binaries!
+# There is a way to avoid using this task and have analyzers run during the main build, but this
+# task will ensure we are using the latest analyzers as per SDL.
+# For more info, please see: https://eng.ms/docs/cloud-ai-platform/devdiv/one-engineering-system-1es/1es-mohanb/security-integration/guardian-wiki/sdl-azdo-extension/roslyn-analyzers-build-task
parameters:
- name: buildConfiguration
@@ -15,6 +19,9 @@ parameters:
- name: mdsPackageVersion
type: string
+ - name: signingKeyPath
+ type: string
+
steps:
- task: securedevelopmentteam.vss-secure-development-tools.build-task-roslynanalyzers.RoslynAnalyzers@3
displayName: 'Roslyn Analyzers'
@@ -27,5 +34,6 @@ steps:
-p:Configuration=${{ parameters.buildConfiguration }}
-p:NugetPackageVersion=${{ parameters.mdsPackageVersion }}
-p:ReferenceType=Package
+ -p:SigningKeyPath=${{ parameters.signingKeyPath }}
msBuildVersion: 17.0
setupCommandLinePicker: vs2022
diff --git a/eng/pipelines/variables/akv-official-variables.yml b/eng/pipelines/variables/akv-official-variables.yml
index 8e602cb8f9..30176ac98b 100644
--- a/eng/pipelines/variables/akv-official-variables.yml
+++ b/eng/pipelines/variables/akv-official-variables.yml
@@ -30,7 +30,7 @@ variables:
- name: versionMinor
value: '1'
- name: versionPatch
- value: '1'
+ value: '2'
- name: versionPreview
value: '-preview1'
diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln
index e4d29d999c..c3a9eeb55b 100644
--- a/src/Microsoft.Data.SqlClient.sln
+++ b/src/Microsoft.Data.SqlClient.sln
@@ -287,6 +287,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "variables", "variables", "{
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "jobs", "jobs", "{09352F1D-878F-4F55-8AA2-6E47F1AD37D5}"
+ ProjectSection(SolutionItems) = preProject
+ ..\eng\pipelines\jobs\build-akv-official-job.yml = ..\eng\pipelines\jobs\build-akv-official-job.yml
+ EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "steps", "steps", "{AD738BD4-6A02-4B88-8F93-FBBBA49A74C8}"
ProjectSection(SolutionItems) = preProject
diff --git a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj
index 51af5632e3..dcd2e49477 100644
--- a/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj
+++ b/src/Microsoft.Data.SqlClient/add-ons/AzureKeyVaultProvider/Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider.csproj
@@ -18,14 +18,14 @@
truetrue
+
-
+ true$(SigningKeyPath)
-
- $(SigningKeyPath)
+
$([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(GeneratedSourceFileName)'))
From d2f2f26c1ebf5fa4c9af41bb56e984114721694d Mon Sep 17 00:00:00 2001
From: Paul Medynski <31868385+paulmedynski@users.noreply.github.com>
Date: Thu, 28 Aug 2025 12:28:04 -0300
Subject: [PATCH 03/11] [6.1] Add Stress Tests to CI (#3565)
* Moved existing stress test projects from ADO into GitHub. (#3546)
(This code is effectively dead since it is not being referenced anywhere, and there are no other changes to the code. As such, we can bypass the one flaky test failure.)
* Minimal modifications to stress tests for Linux (#3548)
* Minimal modifications to get the stress tests building and running on Linux.
* Updated sample config to avoid a dummy password.
* User Story 38131: Stress Tests CI
- Added config file path configuration via STRESS_CONFIG_FILE environment variable.
- Added support for JSON config file.
- Fixed process exit code to reflect success vs failure.
- Added emission to console.
- Added emission of data source.
- Added EntraId password-based auth.
- Added CI stage.
- Added NuGet package version to MDS assembly.
- Added dotnet build configuration pararmeter to CI entry points.
- Added MDS package version configurability to stress tests.
- Added all target frameworks that MDS supports.
- Each test run now creates and drops its own database to avoid collisions.
- Moved to 1ES images where possible.
- Using a local SQL Server that we setup as part of CI.
* User Story 38131: Stress Tests CI
- Added top-level pipeline flag to enable stress tests, deafult is false until tests are reliably passing.
---
.editorconfig | 3 +
.../steps/configure-sql-server-linux-step.yml | 5 +
.../steps/configure-sql-server-macos-step.yml | 6 +-
.../steps/configure-sql-server-win-step.yml | 7 +-
eng/pipelines/dotnet-sqlclient-ci-core.yml | 25 +-
...qlclient-ci-package-reference-pipeline.yml | 14 +
...qlclient-ci-project-reference-pipeline.yml | 14 +
eng/pipelines/jobs/stress-tests-ci-job.yml | 214 ++++
.../stages/stress-tests-ci-stage.yml | 191 ++++
.../tests/StressTests/Directory.Build.props | 18 +
.../StressTests/Directory.Packages.props | 38 +
.../IMonitorLoader/IMonitorLoader.cs | 31 +
.../IMonitorLoader/IMonitorLoader.csproj | 6 +
.../IMonitorLoader/MonitorMetrics.cs | 96 ++
.../tests/StressTests/NuGet.config | 13 +
.../tests/StressTests/Readme.md | 230 +++++
.../GlobalExceptionHandlerAttribute.cs | 16 +
.../Attributes/GlobalTestCleanupAttribute.cs | 16 +
.../Attributes/GlobalTestSetupAttribute.cs | 16 +
.../Attributes/TestAttribute.cs | 272 +++++
.../Attributes/TestCleanupAttribute.cs | 16 +
.../Attributes/TestSetupAttribute.cs | 16 +
.../Attributes/TestVariationAttribute.cs | 35 +
.../DeadlockDetection.cs | 194 ++++
.../DeadlockDetectionTaskScheduler.cs | 93 ++
.../SqlClient.Stress.Common.csproj | 5 +
.../SqlClient.Stress.Common/TestMetrics.cs | 368 +++++++
.../SqlClient.Stress.Common/VersionUtil.cs | 40 +
.../SqlClient.Stress.Framework/AsyncUtils.cs | 185 ++++
.../SqlClient.Stress.Framework/DataSource.cs | 192 ++++
.../DataStressConnection.cs | 232 +++++
.../DataStressErrors.cs | 215 ++++
.../DataStressFactory.cs | 955 ++++++++++++++++++
.../DataStressReader.cs | 350 +++++++
.../DataStressSettings.cs | 310 ++++++
.../DataTestGroup.cs | 713 +++++++++++++
.../SqlClient.Stress.Framework/Extensions.cs | 94 ++
.../SqlClient.Stress.Framework.csproj | 15 +
.../StressConfigReader.cs | 195 ++++
.../StressTests.config.jsonc | 19 +
.../StressTests.config.xml | 20 +
.../TrackedRandom.cs | 184 ++++
.../SqlClient.Stress.Runner/Constants.cs | 53 +
.../Ex API/MemApi.Windows.cs | 18 +
.../ITestAttributeFilter.cs | 11 +
.../SqlClient.Stress.Runner/LogManager.cs | 49 +
.../Monitor/FakeConsole.cs | 34 +
.../SqlClient.Stress.Runner/Monitor/Logger.cs | 226 +++++
.../Monitor/MonitorLoadUtils.cs | 48 +
.../Monitor/RecordedExceptions.cs | 110 ++
.../SqlClient.Stress.Runner/PerfCounters.cs | 29 +
.../SqlClient.Stress.Runner/Program.cs | 306 ++++++
.../SqlClient.Stress.Runner.csproj | 18 +
.../SqlClient.Stress.Runner/StressEngine.cs | 208 ++++
.../SqlClient.Stress.Runner/TestFinder.cs | 166 +++
.../Tests/MultithreadedTest.cs | 170 ++++
.../Tests/StressTest.cs | 155 +++
.../SqlClient.Stress.Runner/Tests/Test.cs | 116 +++
.../SqlClient.Stress.Runner/Tests/TestBase.cs | 163 +++
.../Tests/ThreadPoolTest.cs | 174 ++++
.../FilteredDefaultTraceListener.cs | 210 ++++
.../HostsFileManager.cs | 481 +++++++++
.../MultiSubnetFailoverSetup.cs | 135 +++
.../SqlClient.Stress.Tests/NetUtils.cs | 206 ++++
.../SqlClient.Stress.Tests.csproj | 15 +
.../SqlClientStressFactory.cs | 297 ++++++
.../SqlClientTestGroup.cs | 620 ++++++++++++
.../tests/StressTests/StressTests.slnx | 7 +
tools/targets/GenerateThisAssemblyCs.targets | 1 +
69 files changed, 9699 insertions(+), 4 deletions(-)
create mode 100644 eng/pipelines/jobs/stress-tests-ci-job.yml
create mode 100644 eng/pipelines/stages/stress-tests-ci-stage.yml
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/MonitorMetrics.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/NuGet.config
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalExceptionHandlerAttribute.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestCleanupAttribute.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestSetupAttribute.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestAttribute.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestCleanupAttribute.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestSetupAttribute.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestVariationAttribute.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetection.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetectionTaskScheduler.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/TestMetrics.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/VersionUtil.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/AsyncUtils.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressConnection.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressErrors.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressReader.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressSettings.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataTestGroup.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/Extensions.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/SqlClient.Stress.Framework.csproj
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressConfigReader.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTests.config.jsonc
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/StressTests.config.xml
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/TrackedRandom.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Constants.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Ex API/MemApi.Windows.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/ITestAttributeFilter.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/LogManager.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/FakeConsole.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/Logger.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/MonitorLoadUtils.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Monitor/RecordedExceptions.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/PerfCounters.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Program.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/StressEngine.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/TestFinder.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/MultithreadedTest.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/StressTest.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/Test.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/TestBase.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/Tests/ThreadPoolTest.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/FilteredDefaultTraceListener.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/HostsFileManager.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/MultiSubnetFailoverSetup.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/NetUtils.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClient.Stress.Tests.csproj
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientStressFactory.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Tests/SqlClientTestGroup.cs
create mode 100644 src/Microsoft.Data.SqlClient/tests/StressTests/StressTests.slnx
diff --git a/.editorconfig b/.editorconfig
index f0ea20ec32..ff6d9f3bd7 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -14,6 +14,9 @@ indent_size = 4
[*.{json,jsonc}]
indent_size = 2
+[*.{yml,yaml}]
+indent_size = 2
+
# C# files
[*.cs]
# New line preferences
diff --git a/eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml b/eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml
index 5cff58cd4e..7568f01608 100644
--- a/eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml
+++ b/eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml
@@ -3,6 +3,11 @@
# The .NET Foundation licenses this file to you under the MIT license. #
# See the LICENSE file in the project root for more information. #
#################################################################################
+
+# This step configures an existing SQL Server running on the local Linux host.
+# For example, our 1ES Hosted Pool has images like ADO-UB20-SQL22 that come with
+# SQL Server 2022 pre-installed and running.
+
parameters:
- name: password
type: string
diff --git a/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml b/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml
index 3e83d6b830..8899b9e68f 100644
--- a/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml
+++ b/eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml
@@ -3,6 +3,10 @@
# The .NET Foundation licenses this file to you under the MIT license. #
# See the LICENSE file in the project root for more information. #
#################################################################################
+
+# This step installs the latest SQL Server 2022 onto the macOS host and
+# configures it for use.
+
parameters:
- name: password
type: string
@@ -13,7 +17,7 @@ parameters:
default: and(succeeded(), eq(variables['Agent.OS'], 'Darwin'))
steps:
-# Linux only steps
+# macOS only steps
- bash: |
# The "user" pipeline variable conflicts with homebrew, causing errors during install. Set it back to the pipeline user.
USER=`whoami`
diff --git a/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml b/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml
index d159191b01..6586450e2e 100644
--- a/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml
+++ b/eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml
@@ -3,6 +3,11 @@
# The .NET Foundation licenses this file to you under the MIT license. #
# See the LICENSE file in the project root for more information. #
#################################################################################
+
+# This step configures an existing SQL Server running on the local Windows host.
+# For example, our 1ES Hosted Pool has images like ADO-MMS22-SQL22 that come
+# with SQL Server 2022 pre-installed and running.
+
parameters:
# Windows only parameters
- name: instanceName
@@ -63,7 +68,7 @@ parameters:
default: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
steps:
-# windows only steps
+# Windows only steps
- powershell: |
try
{
diff --git a/eng/pipelines/dotnet-sqlclient-ci-core.yml b/eng/pipelines/dotnet-sqlclient-ci-core.yml
index 4fa8c6bcbc..b7d30b31ea 100644
--- a/eng/pipelines/dotnet-sqlclient-ci-core.yml
+++ b/eng/pipelines/dotnet-sqlclient-ci-core.yml
@@ -65,10 +65,22 @@ parameters:
- Project
- Package
+- name: buildConfiguration
+ displayName: 'Build Configuration'
+ default: Release
+ values:
+ - Release
+ - Debug
+
- name: defaultPoolName
type: string
default: $(ci_var_defaultPoolName)
+- name: enableStressTests
+ displayName: Enable Stress Tests
+ type: boolean
+ default: false
+
variables:
- template: libraries/ci-build-variables.yml@self
@@ -84,6 +96,7 @@ stages:
jobs:
- template: common/templates/jobs/ci-build-nugets-job.yml@self
parameters:
+ configuration: ${{ parameters.buildConfiguration }}
artifactName: $(artifactName)
${{if ne(parameters.SNIVersion, '')}}:
prebuildSteps:
@@ -92,6 +105,16 @@ stages:
SNIVersion: ${{parameters.SNIVersion}}
SNIValidationFeed: ${{parameters.SNIValidationFeed}}
+ - ${{ if eq(parameters.enableStressTests, true) }}:
+ - template: stages/stress-tests-ci-stage.yml@self
+ parameters:
+ buildConfiguration: ${{ parameters.buildConfiguration }}
+ dependsOn: [build_nugets]
+ pipelineArtifactName: $(artifactName)
+ mdsPackageVersion: $(NugetPackageVersion)
+ ${{ if eq(parameters.debug, 'true') }}:
+ verbosity: 'detailed'
+
- template: common/templates/stages/ci-run-tests-stage.yml@self
parameters:
debug: ${{ parameters.debug }}
@@ -139,7 +162,6 @@ stages:
testConfigurations:
windows_sql_19_x64: # configuration name
pool: ${{parameters.defaultPoolName }} # pool name
- hostedPool: false # whether the pool is hosted or not
images: # list of images to run tests on
Win22_Sql19: ADO-MMS22-SQL19 # stage display name: image name from the pool
TargetFrameworks: ${{parameters.targetFrameworks }} #[net462, net8.0] # list of target frameworks to run
@@ -181,7 +203,6 @@ stages:
windows_sql_19_x86: # configuration name
pool: ${{parameters.defaultPoolName }} # pool name
- hostedPool: false # whether the pool is hosted or not
images: # list of images to run tests on
Win22_Sql19_x86: ADO-MMS22-SQL19 # stage display name: image name from the pool
TargetFrameworks: [net8.0] #[net462, net8.0] # list of target frameworks to run
diff --git a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml
index 4956b15c89..ae01d2e9db 100644
--- a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml
+++ b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml
@@ -81,6 +81,18 @@ parameters: # parameters are shown up in ADO UI in a build queue time
- Project
- Package
+- name: buildConfiguration
+ displayName: 'Build Configuration'
+ default: Release
+ values:
+ - Release
+ - Debug
+
+- name: enableStressTests
+ displayName: Enable Stress Tests
+ type: boolean
+ default: false
+
extends:
template: dotnet-sqlclient-ci-core.yml@self
parameters:
@@ -92,3 +104,5 @@ extends:
useManagedSNI: ${{ parameters.useManagedSNI }}
codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }}
buildType: ${{ parameters.buildType }}
+ buildConfiguration: ${{ parameters.buildConfiguration }}
+ enableStressTests: ${{ parameters.enableStressTests }}
diff --git a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml
index ecdaacfafb..97a7a5af24 100644
--- a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml
+++ b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml
@@ -73,6 +73,18 @@ parameters: # parameters are shown up in ADO UI in a build queue time
- Project
- Package
+- name: buildConfiguration
+ displayName: 'Build Configuration'
+ default: Release
+ values:
+ - Release
+ - Debug
+
+- name: enableStressTests
+ displayName: Enable Stress Tests
+ type: boolean
+ default: false
+
extends:
template: dotnet-sqlclient-ci-core.yml@self
parameters:
@@ -84,3 +96,5 @@ extends:
useManagedSNI: ${{ parameters.useManagedSNI }}
codeCovTargetFrameworks: ${{ parameters.codeCovTargetFrameworks }}
buildType: ${{ parameters.buildType }}
+ buildConfiguration: ${{ parameters.buildConfiguration }}
+ enableStressTests: ${{ parameters.enableStressTests }}
diff --git a/eng/pipelines/jobs/stress-tests-ci-job.yml b/eng/pipelines/jobs/stress-tests-ci-job.yml
new file mode 100644
index 0000000000..2e01470fe5
--- /dev/null
+++ b/eng/pipelines/jobs/stress-tests-ci-job.yml
@@ -0,0 +1,214 @@
+################################################################################
+# 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.
+################################################################################
+
+# This stage builds and runs stress tests against an MDS NuGet package available
+# as a pipeline artifact.
+#
+# The stress tests are located here:
+#
+# src/Microsoft.Data.SqlClient/tests/StressTests
+#
+# This template defines a job named 'run_stress_tests_job_' that can be
+# depended on by downstream jobs.
+
+parameters:
+ # The suffix to append to the job name.
+ - name: jobNameSuffix
+ type: string
+ default: ''
+
+ # The prefix to prepend to the job's display name:
+ #
+ # [] Run Stress Tests
+ #
+ - name: displayNamePrefix
+ type: string
+ default: ''
+
+ # The name of the Azure Pipelines pool to use.
+ - name: poolName
+ type: string
+ default: ''
+
+ # The pool VM image to use.
+ - name: vmImage
+ type: string
+ default: ''
+
+ # The pipeline step to run to configure SQL Server.
+ #
+ # This step is expected to require no parameters. It must configure a SQL
+ # Server instance listening on localhost for SQL auth via the 'sa' user with
+ # the pipeline variable $(Password) as the password.
+ - name: sqlSetupStep
+ type: string
+ default: ''
+
+ # The name of the pipeline artifact to download that contains the MDS package
+ # to stress test.
+ - name: pipelineArtifactName
+ type: string
+ default: ''
+
+ # The solution file to restore/build.
+ - name: solution
+ type: string
+ default: ''
+
+ # The test project to run.
+ - name: testProject
+ type: string
+ default: ''
+
+ # dotnet CLI arguments for the restore step.
+ - name: restoreArguments
+ type: string
+ default: ''
+
+ # dotnet CLI arguments for the build and run steps.
+ - name: buildArguments
+ type: string
+ default: ''
+
+ # The list of .NET runtimes to test against.
+ - name: netTestRuntimes
+ type: object
+ default: []
+
+ # The list of .NET Framework runtimes to test against.
+ - name: netFrameworkTestRuntimes
+ type: object
+ default: []
+
+ # The stress test config file contents to write to the config file.
+ #
+ # This should point to the SQL Server configured via the sqlSetupStep
+ # parameter, with user 'sa' and password of $(Password).
+ - name: configContent
+ type: string
+ default: ''
+
+jobs:
+- job: run_stress_tests_job_${{ parameters.jobNameSuffix }}
+ displayName: '[${{ parameters.displayNamePrefix }}] Run Stress Tests'
+ pool:
+ name: ${{ parameters.poolName }}
+ ${{ if eq(parameters.poolName, 'Azure Pipelines') }}:
+ vmImage: ${{ parameters.vmImage }}
+ ${{ else }}:
+ demands:
+ - imageOverride -equals ${{ parameters.vmImage }}
+
+ variables:
+ # Stress test command-line arguments.
+ - name: testArguments
+ value: -a SqlClient.Stress.Tests -console
+
+ # Explicitly unset the $PLATFORM environment variable that is set by the
+ # 'ADO Build properties' Library in the ADO SqlClientDrivers public project.
+ # This is defined with a non-standard Platform of 'AnyCPU', and will fail
+ # the builds if left defined. The stress tests solution does not require
+ # any specific Platform, and so its solution file doesn't support any
+ # non-standard platforms.
+ #
+ # Note that Azure Pipelines will inject this variable as PLATFORM into the
+ # environment of all tasks in this job.
+ #
+ # See:
+ # https://learn.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch
+ #
+ - name: Platform
+ value: ''
+
+ # Do the same for $CONFIGURATION since we explicitly set it using our
+ # 'buildConfiguration' parameter, and we don't want the environment to
+ # override us.
+ - name: Configuration
+ value: ''
+
+ steps:
+
+ # Install the .NET 9.0 SDK.
+ - task: UseDotNet@2
+ displayName: Install .NET 9.0 SDK
+ inputs:
+ packageType: sdk
+ version: 9.x
+
+ # Install the .NET 8.0 runtime.
+ - task: UseDotNet@2
+ displayName: Install .NET 8.0 Runtime
+ inputs:
+ packageType: runtime
+ version: 8.x
+
+ # Download the pipeline artifact that contains the MDS package to test.
+ - task: DownloadPipelineArtifact@2
+ displayName: Download Pipeline Artifact
+ inputs:
+ artifactName: ${{ parameters.pipelineArtifactName }}
+ # The stress tests solution has a NuGet.config file that configures
+ # sources to look in this packages/ directory.
+ targetPath: $(Build.SourcesDirectory)/packages
+
+ # Setup the local SQL Server.
+ - template: ${{ parameters.sqlSetupStep }}@self
+
+ # We use the 'custom' command because the DotNetCoreCLI@2 task doesn't support
+ # all of our argument combinations for the different build steps.
+
+ # Restore the solution.
+ - task: DotNetCoreCLI@2
+ displayName: Restore Solution
+ inputs:
+ command: custom
+ custom: restore
+ projects: ${{ parameters.solution }}
+ arguments: ${{ parameters.restoreArguments }}
+
+ # Build the solution.
+ - task: DotNetCoreCLI@2
+ displayName: Build Solution
+ inputs:
+ command: custom
+ custom: build
+ projects: ${{ parameters.solution }}
+ arguments: ${{ parameters.buildArguments }} --no-restore
+
+ # Write the config file.
+ - task: PowerShell@2
+ displayName: Write Config File
+ inputs:
+ pwsh: true
+ targetType: inline
+ script: |
+ # Capture the multi-line JSON content into a variable.
+ $content = @"
+ ${{ parameters.configContent }}
+ "@
+
+ # Write the JSON content to the config file.
+ $content | Out-File -FilePath "config.json"
+
+ # Run the stress tests for each .NET runtime.
+ - ${{ each runtime in parameters.netTestRuntimes }}:
+ - task: DotNetCoreCLI@2
+ displayName: Test [${{runtime}}]
+ inputs:
+ command: custom
+ custom: run
+ projects: ${{ parameters.testProject }}
+ arguments: ${{ parameters.buildArguments }} --no-build -f ${{runtime}} -e STRESS_CONFIG_FILE=config.json -- $(testArguments)
+
+ # Run the stress tests for each .NET Framework runtime.
+ - ${{ each runtime in parameters.netFrameworkTestRuntimes }}:
+ - task: DotNetCoreCLI@2
+ displayName: Test [${{runtime}}]
+ inputs:
+ command: custom
+ custom: run
+ projects: ${{ parameters.testProject }}
+ arguments: ${{ parameters.buildArguments }} --no-build -f ${{runtime}} -e STRESS_CONFIG_FILE=config.json -- $(testArguments)
diff --git a/eng/pipelines/stages/stress-tests-ci-stage.yml b/eng/pipelines/stages/stress-tests-ci-stage.yml
new file mode 100644
index 0000000000..06b41cd421
--- /dev/null
+++ b/eng/pipelines/stages/stress-tests-ci-stage.yml
@@ -0,0 +1,191 @@
+################################################################################
+# 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.
+################################################################################
+
+# This stage builds and runs stress tests against an MDS NuGet package available
+# as a pipeline artifact.
+#
+# The stress tests are located here:
+#
+# src/Microsoft.Data.SqlClient/tests/StressTests
+#
+# All tests use a localhost SQL Server configured for SQL auth via the 'sa' user
+# and password of '$(Password)'. The $(Password) variable is defined in the ADO
+# Library "ADO Test Configuration properties", brought in by
+# common/templates/libraries/ci-build-variables.yml.
+#
+# This template defines a stage named 'run_stress_tests_stage' that can be
+# depended on by downstream stages.
+
+parameters:
+ # The type of build to produce (Release or Debug)
+ - name: buildConfiguration
+ displayName: Build Configuration
+ type: string
+ default: Release
+ values:
+ - Release
+ - Debug
+
+ # The names of any stages this stage depends on, for example the stages
+ # that publish the MDS package artifacts we will test.
+ - name: dependsOn
+ displayName: Depends On Stages
+ type: object
+ default: []
+
+ # The name of the pipeline artifact to download that contains the MDS package
+ # to stress test.
+ - name: pipelineArtifactName
+ displayName: Pipeline Artifact Name
+ type: string
+ default: Artifacts
+
+ # The MDS package version to stress test. This version must be available in
+ # one of the configured NuGet sources.
+ - name: mdsPackageVersion
+ displayName: MDS Package Version
+ type: string
+ default: ''
+
+ # The list of .NET runtimes to test against.
+ - name: netTestRuntimes
+ displayName: .NET Test Runtimes
+ type: object
+ default: [net8.0, net9.0]
+
+ # The list of .NET Framework runtimes to test against.
+ - name: netFrameworkTestRuntimes
+ displayName: .NET Framework Test Runtimes
+ type: object
+ default: [net462, net47, net471, net472, net48, net481]
+
+ # The verbosity level for the dotnet CLI commands.
+ - name: verbosity
+ displayName: Dotnet CLI verbosity
+ type: string
+ default: normal
+ values:
+ - quiet
+ - minimal
+ - normal
+ - detailed
+ - diagnostic
+
+stages:
+ - stage: run_stress_tests_stage
+ displayName: Run Stress Tests
+ dependsOn: ${{ parameters.dependsOn }}
+
+ variables:
+ # The directory where dotnet artifacts will be staged. Not to be
+ # confused with pipeline artifact.
+ - name: dotnetArtifactsDir
+ value: $(Build.StagingDirectory)/dotnetArtifacts
+
+ # The solution file to use for all dotnet CLI commands.
+ - name: solution
+ value: src/Microsoft.Data.SqlClient/tests/StressTests/StressTests.slnx
+
+ # The stress test project to run.
+ - name: testProject
+ value: src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj
+
+ # dotnet CLI arguments common to all commands.
+ - name: commonArguments
+ value: >-
+ --verbosity ${{parameters.verbosity}}
+ --artifacts-path $(dotnetArtifactsDir)
+ -p:MdsPackageVersion=${{parameters.mdsPackageVersion}}
+
+ # dotnet CLI arguments for build/run commands.
+ - name: buildArguments
+ value: >-
+ $(commonArguments)
+ --configuration ${{parameters.buildConfiguration}}
+
+ # The contents of the config file to use for all tests. We will write
+ # this to a JSON file for each test job, and then point to it via the
+ # STRESS_CONFIG_FILE environment variable.
+ - name: ConfigContent
+ value: |
+ [
+ {
+ "name": "Azure SQL",
+ "type": "SqlServer",
+ "isDefault": true,
+ "dataSource": "localhost",
+ "user": "sa",
+ "password": "$(Password)",
+ "supportsWindowsAuthentication": false,
+ "isLocal": false,
+ "disableMultiSubnetFailover": true,
+ "disableNamedPipes": true,
+ "encrypt": false
+ }
+ ]
+
+ jobs:
+
+ # --------------------------------------------------------------------------
+ # Build and test on Linux.
+
+ - template: ../jobs/stress-tests-ci-job.yml@self
+ parameters:
+ jobNameSuffix: linux
+ displayNamePrefix: Linux
+ poolName: $(ci_var_defaultPoolName)
+ vmImage: ADO-UB20-SQL22
+ sqlSetupStep: /eng/pipelines/common/templates/steps/configure-sql-server-linux-step.yml
+ pipelineArtifactName: ${{ parameters.pipelineArtifactName }}
+ solution: $(solution)
+ testProject: $(testProject)
+ restoreArguments: $(commonArguments)
+ buildArguments: $(buildArguments)
+ netTestRuntimes: ${{ parameters.netTestRuntimes }}
+ configContent: $(ConfigContent)
+
+ # --------------------------------------------------------------------------
+ # Build and test on Windows
+
+ - template: ../jobs/stress-tests-ci-job.yml
+ parameters:
+ jobNameSuffix: windows
+ displayNamePrefix: Win
+ poolName: $(ci_var_defaultPoolName)
+ # The Windows images include a suitable .NET Framework runtime, so we
+ # don't have to install one explicitly.
+ vmImage: ADO-MMS22-SQL22
+ sqlSetupStep: /eng/pipelines/common/templates/steps/configure-sql-server-win-step.yml
+ pipelineArtifactName: ${{ parameters.pipelineArtifactName }}
+ solution: $(solution)
+ testProject: $(testProject)
+ restoreArguments: $(commonArguments)
+ buildArguments: $(buildArguments)
+ netTestRuntimes: ${{ parameters.netTestRuntimes }}
+ # Note that we include the .NET Framework runtimes for test runs on
+ # Windows.
+ netFrameworkTestRuntimes: ${{ parameters.netFrameworkTestRuntimes }}
+ configContent: $(ConfigContent)
+
+ # --------------------------------------------------------------------------
+ # Build and test on macOS.
+
+ - template: ../jobs/stress-tests-ci-job.yml
+ parameters:
+ jobNameSuffix: macos
+ displayNamePrefix: macOS
+ # We don't have any 1ES Hosted Pool images for macOS, so we use a
+ # generic one from Azure Pipelines.
+ poolName: Azure Pipelines
+ vmImage: macos-latest
+ sqlSetupStep: /eng/pipelines/common/templates/steps/configure-sql-server-macos-step.yml
+ pipelineArtifactName: ${{ parameters.pipelineArtifactName }}
+ solution: $(solution)
+ testProject: $(testProject)
+ restoreArguments: $(commonArguments)
+ buildArguments: $(buildArguments)
+ netTestRuntimes: ${{ parameters.netTestRuntimes }}
+ configContent: $(ConfigContent)
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props
new file mode 100644
index 0000000000..66fbacae6c
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Build.props
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+ net462;net47;net471;net472;net48;net481;net8.0;net9.0
+
+
+ latest
+
+
+
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props
new file mode 100644
index 0000000000..45b1a5018f
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Directory.Packages.props
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ true
+ true
+
+
+
+
+
+
+ $(MdsPackageVersion)
+
+
+
+
+
+
+
+
+
+ 6.1.0-preview2.25178.5
+
+
+
+
+
+
+
+
+
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.cs
new file mode 100644
index 0000000000..1dcbde121f
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.cs
@@ -0,0 +1,31 @@
+// 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.
+
+using System.Collections.Generic;
+
+namespace Monitoring
+{
+ public interface IMonitorLoader
+ {
+ string HostMachine { get; set; }
+ string AssemblyPath { get; set; }
+ string TestName { get; set; }
+ bool Enabled { get; set; }
+
+ void Action(MonitorLoaderUtils.MonitorAction monitoraction);
+ void AddPerfData(MonitorMetrics data);
+ Dictionary GetPerfData();
+ }
+
+ public class MonitorLoaderUtils
+ {
+ public enum MonitorAction
+ {
+ Initialize,
+ Start,
+ Stop,
+ DoNothing
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj
new file mode 100644
index 0000000000..0968e1837f
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/IMonitorLoader.csproj
@@ -0,0 +1,6 @@
+
+
+ Monitoring
+ Monitoring
+
+
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/MonitorMetrics.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/MonitorMetrics.cs
new file mode 100644
index 0000000000..ed37544e4e
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/IMonitorLoader/MonitorMetrics.cs
@@ -0,0 +1,96 @@
+// 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.
+
+using System;
+
+namespace Monitoring
+{
+ public class MonitorMetrics
+ {
+ private string _name;
+ private string _strValue;
+ private string _unit;
+ private bool _isPrimary;
+ private bool _isHigherBetter;
+ private double _dblValue;
+ private long _lngValue;
+ private char _valueType; // D=double, L=long, S=String
+
+ public MonitorMetrics(string name, string value, string unit, bool HigherIsBetter, bool Primary)
+ {
+ _name = name;
+ _strValue = value;
+ _unit = unit;
+ _valueType = 'S';
+ _isHigherBetter = HigherIsBetter;
+ _isPrimary = Primary;
+ }
+
+ public MonitorMetrics(string name, double value, string unit, bool HigherIsBetter, bool Primary)
+ {
+ _name = name;
+ _dblValue = value;
+ _unit = unit;
+ _valueType = 'D';
+ _isHigherBetter = HigherIsBetter;
+ _isPrimary = Primary;
+ }
+
+ public MonitorMetrics(string name, long value, string unit, bool HigherIsBetter, bool Primary)
+ {
+ _name = name;
+ _lngValue = value;
+ _unit = unit;
+ _valueType = 'L';
+ _isHigherBetter = HigherIsBetter;
+ _isPrimary = Primary;
+ }
+
+ public string GetName()
+ {
+ return _name;
+ }
+
+ public string GetUnit()
+ {
+ return _unit;
+ }
+
+ public bool GetPrimary()
+ {
+ return _isPrimary;
+ }
+
+ public bool GetHigherIsBetter()
+ {
+ return _isHigherBetter;
+ }
+
+ public char GetValueType()
+ {
+ return _valueType;
+ }
+
+ public string GetStringValue()
+ {
+ if (_valueType == 'S')
+ return _strValue;
+ throw new Exception("Value is not a string");
+ }
+
+ public double GetDoubleValue()
+ {
+ if (_valueType == 'D')
+ return _dblValue;
+ throw new Exception("Value is not a double");
+ }
+
+ public long GetLongValue()
+ {
+ if (_valueType == 'L')
+ return _lngValue;
+ throw new Exception("Value is not a long");
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/NuGet.config b/src/Microsoft.Data.SqlClient/tests/StressTests/NuGet.config
new file mode 100644
index 0000000000..19c2531f5d
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/NuGet.config
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md b/src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md
new file mode 100644
index 0000000000..3ae5c9e3df
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/Readme.md
@@ -0,0 +1,230 @@
+# Microsoft.Data.SqlClient Stress Test
+
+This Stress testing application for `Microsoft.Data.SqlClient` is under progress.
+
+This project intends to help finding a certain level of effectiveness under
+unfavorable conditions, and verifying the mode of failures.
+
+This is a console application targeting all frameworks supported by MDS,
+currently:
+
+- .NET 8.0
+- .NET T9.0
+- .NET Framework 4.6.2
+- .NET Framework 4.7
+- .NET Framework 4.7.1
+- .NET Framework 4.7.2
+- .NET Framework 4.8
+- .NET Framework 4.8.1
+
+## Purpose of application for developers
+
+Define fuzz tests for all new features/APIs in the driver and to be run before
+every GA release.
+
+## Pre-Requisites
+
+Required in the config file:
+
+|Field|Values|Description|
+|-|-|-|
+|`name`||Stress testing source configuration name.|
+|`type`|`SqlServer`|Only `SqlServer` is acceptable.|
+|`isDefault`|`true`, `false`|If there is a source node with `isDefault=true`, this node is returned.|
+|`dataSource`||SQL Server data source name.|
+|`user`||User Id to connect the server.|
+|`password`||Paired password with the user.|
+|`supportsWindowsAuthentication`|`true`, `false`|Tries to use integrated security in connection string mixed with SQL Server authentication if it set to `true` by applying the randomization.|
+|`isLocal`|`true`, `false`|`true` means database is local.|
+|`disableMultiSubnetFailover`|`true`, `false`|Tries to add Multi-subnet Failover fake host entries when it equals `true`.|
+|`disableNamedPipes`|`true`, `false`|`true` means the connections will create just using tcp protocol.|
+|`encrypt`|`true`, `false`|Assigns the encrypt property of the connection strings.|
+
+Note: The database user must have permission to create and drop databases.
+Each execution of the stress tests will create a database with a name like:
+
+- `StressTests-`
+
+The database will be dropped as a best effort once testing is complete. This
+allows for multiple test runs to execute in parallel against the same database
+server without colliding.
+
+## Adding new Tests
+
+- [ToDo]
+
+## Building the application
+
+To build the application using the `StressTests.slnx` solution:
+
+```bash
+dotnet build [-c|--configuration ]
+```
+
+```bash
+# Builds the application for the Client Os in `Debug` Configuration for `AnyCpu`
+# platform.
+#
+# All supported target frameworks are built by default.
+
+$ dotnet build
+```
+
+```bash
+# Build the application for .Net framework 4.8.1 with `Debug` configuration.
+
+$ dotnet build -f net481
+```
+
+```bash
+# Build the application for .Net 9.0 with `Release` configuration.
+
+$ dotnet build -f net9.0 -c Release
+```
+
+```bash
+# Cleans all build directories
+
+$ dotnet clean
+```
+
+## Running tests
+
+After building the application, find the built folder with target framework and
+run the `stresstest.exe` file with required arguments.
+
+Find the result in a log file inside the `logs` folder besides the command
+prompt.
+
+You may specify the config file by supplying an environment variable that
+points to the file:
+
+- `STRESS_CONFIG_FILE=/path/to/my/config.jsonc`
+
+## Command prompt
+
+You must run the stress tests from the root of the Stress Tests project
+directory (i.e. the same directory this readme file is in).
+
+```bash
+# Linux
+$ cd /home/paul/dev/SqlClient/src/Microsoft.Data.SqlClient/tests/StressTests
+
+# Via dotnet run CLI:
+$ dotnet run --no-build -f net9.0 --project SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj -- -a SqlClient.Stress.Tests
+
+# Via dotnet CLI:
+$ dotnet SqlClient.Stress.Runner/bin/Debug/net9.0/stresstest.dll -a SqlClient.Stress.Tests
+
+# With a specific config file and all output to console:
+$ dotnet run --no-build -f net9.0 --project SqlClient.Stress.Runner/SqlClient.Stress.Runner.csproj -e STRESS_CONFIG_FILE=/path/to/config.jsonc -- -a SqlClient.Stress.Tests -console
+```
+
+```powershell
+# Windows
+> cd \dev\SqlClient\src\Microsoft.Data.SqlClient\tests\StressTests
+
+# Via dotnet run CLI:
+> dotnet run --no-build -f net9.0 --project SqlClient.Stress.Runner\SqlClient.Stress.Runner.csproj -- -a SqlClient.Stress.Tests
+
+# Via executable:
+> .\SqlClient.Stress.Runner\bin\Debug\net481\stresstest.exe -a SqlClient.Stress.Tests
+
+# With a specific config file and all output to console:
+> dotnet run --no-build -f net9.0 --project SqlClient.Stress.Runner\SqlClient.Stress.Runner.csproj -e STRESS_CONFIG_FILE=c:\path\to\config.jsonc -- -a SqlClient.Stress.Tests -console
+```
+
+## Supported arguments
+
+|Argument|Values|Description|
+|-|-|-|
+|-all||Run all tests - best for debugging, not perf measurements.|
+|-verify||Run in functional verification mode. [not implemented]|
+|-duration|<n>|Duration of the test in seconds. Default value is 1 second.|
+|-threads|<n>|Number of threads to use. Default value is 16.|
+|-override|<name> <value>|Override the value of a test property.|
+|-test|<name1;name2>|Run specific test(s).|
+|-debug||Print process ID in the beginning and wait for Enter (to give your time to attach the debugger).|
+|-console||Emit all output to the console instead of a log file.|
+|-exceptionThreshold|<n>|An optional limit on exceptions which will be caught. When reached, test will halt.|
+|-monitorenabled|true, false|True or False to enable monitoring. Default is false [not implemented]|
+|-randomSeed||Enables setting of the random number generator used internally. This serves both the purpose of helping to improve reproducibility and making it deterministic from Chess's perspective for a given schedule. Default is 0.|
+|-filter|<filter>|Run tests whose stress test attributes match the given filter. Filter is not applied if attribute does not implement ITestAttributeFilter. Example: -filter TestType=Query,Update;IsServerTest=True|
+|-printMethodName||Print tests' title in console window|
+|-deadlockdetection|true, false|True or False to enable deadlock detection. Default is `false`.|
+
+```powershell
+# Run the application for a built target framework and all discovered tests
+# without debugger attached.
+
+> .\stresstest.exe -a SqlClient.Stress.Tests -all
+```
+
+```powershell
+# Run the application for a built target framework and all discovered tests
+# without debugger attached and shows the test methods' names.
+
+> .\stresstest.exe -a SqlClient.Stress.Tests -all -printMethodName
+```
+
+```powershell
+# Run the application for a built target framework and all discovered tests and
+# will wait for debugger to be attached.
+
+> .\stresstest.exe -a SqlClient.Stress.Tests -all -debug
+```
+
+```powershell
+# Run the application for a built target framework and
+# "TestExecuteXmlReaderAsyncCancellation" test without debugger attached.
+
+> .\stresstest.exe -a SqlClient.Stress.Tests -test TestExecuteXmlReaderAsyncCancellation
+```
+
+```powershell
+# Run the application for a built target framework and
+# "TestExecuteXmlReaderAsyncCancellation" test without debugger attached.
+
+> .\stresstest.exe -a SqlClient.Stress.Tests -test TestExecuteXmlReaderAsyncCancellation
+```
+
+```powershell
+# Run the application for a built target framework and all discovered tests
+# without debugger attached for 10 seconds.
+
+> .\stresstest.exe -a SqlClient.Stress.Tests -all -duration 10
+```
+
+```powershell
+# Run the application for a built target framework and all discovered tests
+# without debugger attached with 5 threads.
+
+> .\stresstest.exe -a SqlClient.Stress.Tests -all -threads 5
+```
+
+```powershell
+# Run the application for a built target framework and all discovered tests
+# without debugger attached and dead lock detection process.
+
+> .\stresstest.exe -a SqlClient.Stress.Tests -all -deadlockdetection true
+```
+
+```powershell
+# Run the application for a built target framework and all discovered tests
+# without debugger attached with overriding the weight property with value 15.
+
+> .\stresstest.exe -a SqlClient.Stress.Tests -all -override Weight 15
+```
+
+```powershell
+# Run the application for a built target framework and all discovered tests
+# without debugger attached with injecting random seed of 5.
+
+> .\stresstest.exe -a SqlClient.Stress.Tests -all -randomSeed 5
+```
+
+## Further thoughts
+
+- Implement the uncompleted arguments.
+- Add more tests.
+- Add support running tests with **System.Data.SqlClient** too.
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalExceptionHandlerAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalExceptionHandlerAttribute.cs
new file mode 100644
index 0000000000..810580d9f8
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalExceptionHandlerAttribute.cs
@@ -0,0 +1,16 @@
+// 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.
+
+using System;
+
+namespace DPStressHarness
+{
+ [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public class GlobalExceptionHandlerAttribute : Attribute
+ {
+ public GlobalExceptionHandlerAttribute()
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestCleanupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestCleanupAttribute.cs
new file mode 100644
index 0000000000..2159d2630e
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestCleanupAttribute.cs
@@ -0,0 +1,16 @@
+// 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.
+
+using System;
+
+namespace DPStressHarness
+{
+ [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public class GlobalTestCleanupAttribute : Attribute
+ {
+ public GlobalTestCleanupAttribute()
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestSetupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestSetupAttribute.cs
new file mode 100644
index 0000000000..00ed3d5b05
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/GlobalTestSetupAttribute.cs
@@ -0,0 +1,16 @@
+// 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.
+
+using System;
+
+namespace DPStressHarness
+{
+ [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public class GlobalTestSetupAttribute : Attribute
+ {
+ public GlobalTestSetupAttribute()
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestAttribute.cs
new file mode 100644
index 0000000000..3146c2d808
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestAttribute.cs
@@ -0,0 +1,272 @@
+// 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.
+
+using System;
+
+namespace DPStressHarness
+{
+ public enum TestPriority
+ {
+ BVT = 0,
+ High = 1,
+ Medium = 2,
+ Low = 3
+ }
+
+ public class TestAttributeBase : Attribute
+ {
+ private string _title;
+ private string _description = "none provided";
+ private string _applicationName = "unknown";
+ private string _improvement = "ADONETV3";
+ private string _owner = "unknown";
+ private string _category = "unknown";
+ private TestPriority _priority = TestPriority.BVT;
+
+ public TestAttributeBase(string title)
+ {
+ _title = title;
+ }
+
+ public string Title
+ {
+ get { return _title; }
+ set { _title = value; }
+ }
+
+ public string Description
+ {
+ get { return _description; }
+ set { _description = value; }
+ }
+
+ public string Improvement
+ {
+ get { return _improvement; }
+ set { _improvement = value; }
+ }
+
+ public string Owner
+ {
+ get { return _owner; }
+ set { _owner = value; }
+ }
+
+ public string ApplicationName
+ {
+ get { return _applicationName; }
+ set { _applicationName = value; }
+ }
+
+ public TestPriority Priority
+ {
+ get { return _priority; }
+ set { _priority = value; }
+ }
+
+ public string Category
+ {
+ get { return _category; }
+ set { _category = value; }
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ public class TestAttribute : TestAttributeBase
+ {
+ private int _warmupIterations = 0;
+ private int _testIterations = 1;
+
+ public TestAttribute(string title) : base(title)
+ {
+ }
+
+ public int WarmupIterations
+ {
+ get
+ {
+ string propName = "WarmupIterations";
+
+ if (TestMetrics.Overrides.ContainsKey(propName))
+ {
+ return int.Parse(TestMetrics.Overrides[propName]);
+ }
+ else
+ {
+ return _warmupIterations;
+ }
+ }
+ set { _warmupIterations = value; }
+ }
+
+ public int TestIterations
+ {
+ get
+ {
+ string propName = "TestIterations";
+
+ if (TestMetrics.Overrides.ContainsKey(propName))
+ {
+ return int.Parse(TestMetrics.Overrides[propName]);
+ }
+ else
+ {
+ return _testIterations;
+ }
+ }
+ set { _testIterations = value; }
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ public class StressTestAttribute : TestAttributeBase
+ {
+ private int _weight = 1;
+
+ public StressTestAttribute(string title)
+ : base(title)
+ {
+ }
+
+ public int Weight
+ {
+ get { return _weight; }
+ set { _weight = value; }
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ public class MultiThreadedTestAttribute : TestAttributeBase
+ {
+ private int _warmupDuration = 60;
+ private int _testDuration = 60;
+ private int _threads = 16;
+
+ public MultiThreadedTestAttribute(string title)
+ : base(title)
+ {
+ }
+
+ public int WarmupDuration
+ {
+ get
+ {
+ string propName = "WarmupDuration";
+
+ if (TestMetrics.Overrides.ContainsKey(propName))
+ {
+ return int.Parse(TestMetrics.Overrides[propName]);
+ }
+ else
+ {
+ return _warmupDuration;
+ }
+ }
+ set { _warmupDuration = value; }
+ }
+
+ public int TestDuration
+ {
+ get
+ {
+ string propName = "TestDuration";
+
+ if (TestMetrics.Overrides.ContainsKey(propName))
+ {
+ return int.Parse(TestMetrics.Overrides[propName]);
+ }
+ else
+ {
+ return _testDuration;
+ }
+ }
+ set { _testDuration = value; }
+ }
+
+ public int Threads
+ {
+ get
+ {
+ string propName = "Threads";
+
+ if (TestMetrics.Overrides.ContainsKey(propName))
+ {
+ return int.Parse(TestMetrics.Overrides[propName]);
+ }
+ else
+ {
+ return _threads;
+ }
+ }
+ set { _threads = value; }
+ }
+ }
+
+ [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+ public class ThreadPoolTestAttribute : TestAttributeBase
+ {
+ private int _warmupDuration = 60;
+ private int _testDuration = 60;
+ private int _threads = 64;
+
+ public ThreadPoolTestAttribute(string title)
+ : base(title)
+ {
+ }
+
+ public int WarmupDuration
+ {
+ get
+ {
+ string propName = "WarmupDuration";
+
+ if (TestMetrics.Overrides.ContainsKey(propName))
+ {
+ return int.Parse(TestMetrics.Overrides[propName]);
+ }
+ else
+ {
+ return _warmupDuration;
+ }
+ }
+ set { _warmupDuration = value; }
+ }
+
+ public int TestDuration
+ {
+ get
+ {
+ string propName = "TestDuration";
+
+ if (TestMetrics.Overrides.ContainsKey(propName))
+ {
+ return int.Parse(TestMetrics.Overrides[propName]);
+ }
+ else
+ {
+ return _testDuration;
+ }
+ }
+ set { _testDuration = value; }
+ }
+
+ public int Threads
+ {
+ get
+ {
+ string propName = "Threads";
+
+ if (TestMetrics.Overrides.ContainsKey(propName))
+ {
+ return int.Parse(TestMetrics.Overrides[propName]);
+ }
+ else
+ {
+ return _threads;
+ }
+ }
+ set { _threads = value; }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestCleanupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestCleanupAttribute.cs
new file mode 100644
index 0000000000..32bc5ee6bc
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestCleanupAttribute.cs
@@ -0,0 +1,16 @@
+// 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.
+
+using System;
+
+namespace DPStressHarness
+{
+ [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public class TestCleanupAttribute : Attribute
+ {
+ public TestCleanupAttribute()
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestSetupAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestSetupAttribute.cs
new file mode 100644
index 0000000000..5626032b69
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestSetupAttribute.cs
@@ -0,0 +1,16 @@
+// 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.
+
+using System;
+
+namespace DPStressHarness
+{
+ [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
+ public class TestSetupAttribute : Attribute
+ {
+ public TestSetupAttribute()
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestVariationAttribute.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestVariationAttribute.cs
new file mode 100644
index 0000000000..e54acfa969
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/Attributes/TestVariationAttribute.cs
@@ -0,0 +1,35 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace DPStressHarness
+{
+ [AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = true)]
+ public class TestVariationAttribute : Attribute
+ {
+ private string _variationName;
+ private object _variationValue;
+
+ public TestVariationAttribute(string variationName, object variationValue)
+ {
+ _variationName = variationName;
+ _variationValue = variationValue;
+ }
+
+ public string VariationName
+ {
+ get { return _variationName; }
+ set { _variationName = value; }
+ }
+
+ public object VariationValue
+ {
+ get { return _variationValue; }
+ set { _variationValue = value; }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetection.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetection.cs
new file mode 100644
index 0000000000..50fc6d3d7a
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetection.cs
@@ -0,0 +1,194 @@
+// 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.
+
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DPStressHarness
+{
+ public class DeadlockDetection
+ {
+ ///
+ /// Information for a thread relating to deadlock detection. All of its information is stored in a reference object to make updating it easier.
+ ///
+ private class ThreadInfo
+ {
+ public ThreadInfo(long dueTime)
+ {
+ this.DueTime = dueTime;
+ }
+
+ ///
+ /// The time (in ticks) when the thread should be completed
+ ///
+ public long DueTime;
+
+ ///
+ /// True if the thread should not be aborted
+ ///
+ public bool DisableAbort;
+
+ ///
+ /// The time when DisableAbort was set to true
+ ///
+ public long DisableAbortTime;
+ }
+
+ ///
+ /// Maximum time that a test thread (i.e. a thread that is directly executing a [StressTest] method) can
+ /// execute before it is considered to be deadlocked. This should be longer than the
+ /// TaskThreadDeadlockTimeoutTicks because if the test is waiting for a task then the test will always
+ /// take longer to execute than the task.
+ ///
+ public const long TestThreadDeadlockTimeoutTicks = 20 * 60 * TimeSpan.TicksPerSecond;
+
+ ///
+ /// Maximum time that any Task can execute before it is considered to be deadlocked
+ ///
+ public const long TaskThreadDeadlockTimeoutTicks = 10 * 60 * TimeSpan.TicksPerSecond;
+
+ ///
+ /// Dictionary that maps Threads to the time (in ticks) when they should be completed. If they are not completed by that time then
+ /// they are considered to be deadlocked.
+ ///
+ private static ConcurrentDictionary s_threadDueTimes = null;
+
+ ///
+ /// Timer that scans through _threadDueTimes to find deadlocked threads
+ ///
+ private static Timer s_deadlockWatchdog = null;
+
+ ///
+ /// Interval of _deadlockWatchdog, in milliseconds
+ ///
+ private const int _watchdogIntervalMs = 60 * 1000;
+
+ ///
+ /// true if deadlock detection is enabled, otherwise false. Should be set only at process startup.
+ ///
+ private static bool s_isEnabled = false;
+
+ public static bool IsEnabled => s_isEnabled;
+
+ ///
+ /// Enables deadlock detection.
+ ///
+ public static void Enable()
+ {
+ // Switch out the default TaskScheduler. We must use reflection because it is private.
+ FieldInfo defaultTaskScheduler = typeof(TaskScheduler).GetField("s_defaultTaskScheduler", BindingFlags.NonPublic | BindingFlags.Static);
+ DeadlockDetectionTaskScheduler newTaskScheduler = new DeadlockDetectionTaskScheduler();
+ defaultTaskScheduler.SetValue(null, newTaskScheduler);
+
+ s_threadDueTimes = new ConcurrentDictionary();
+ s_deadlockWatchdog = new Timer(CheckForDeadlocks, null, _watchdogIntervalMs, _watchdogIntervalMs);
+
+ s_isEnabled = true;
+ }
+
+ ///
+ /// Adds the current Task execution thread to the tracked thread collection.
+ ///
+ public static void AddTaskThread()
+ {
+ if (s_isEnabled)
+ {
+ long dueTime = DateTime.UtcNow.Ticks + TaskThreadDeadlockTimeoutTicks;
+ AddThread(dueTime);
+ }
+ }
+
+ ///
+ /// Adds the current Test execution thread (i.e. a thread that is directly executing a [StressTest] method) to the tracked thread collection.
+ ///
+ public static void AddTestThread()
+ {
+ if (s_isEnabled)
+ {
+ long dueTime = DateTime.UtcNow.Ticks + TestThreadDeadlockTimeoutTicks;
+ AddThread(dueTime);
+ }
+ }
+
+ private static void AddThread(long dueTime)
+ {
+ s_threadDueTimes.TryAdd(Thread.CurrentThread, new ThreadInfo(dueTime));
+ }
+
+ ///
+ /// Removes the current thread from the tracked thread collection
+ ///
+ public static void RemoveThread()
+ {
+ if (s_isEnabled)
+ {
+ ThreadInfo unused;
+ s_threadDueTimes.TryRemove(Thread.CurrentThread, out unused);
+ }
+ }
+
+ ///
+ /// Disables abort of current thread. Call this when the current thread is waiting on a task.
+ ///
+ public static void DisableThreadAbort()
+ {
+ if (s_isEnabled)
+ {
+ ThreadInfo threadInfo;
+ if (s_threadDueTimes.TryGetValue(Thread.CurrentThread, out threadInfo))
+ {
+ threadInfo.DisableAbort = true;
+ threadInfo.DisableAbortTime = DateTime.UtcNow.Ticks;
+ }
+ }
+ }
+
+ ///
+ /// Enables abort of current thread after calling DisableThreadAbort(). The elapsed time since calling DisableThreadAbort() is added to the due time.
+ ///
+ public static void EnableThreadAbort()
+ {
+ if (s_isEnabled)
+ {
+ ThreadInfo threadInfo;
+ if (s_threadDueTimes.TryGetValue(Thread.CurrentThread, out threadInfo))
+ {
+ threadInfo.DueTime += DateTime.UtcNow.Ticks - threadInfo.DisableAbortTime;
+ threadInfo.DisableAbort = false;
+ }
+ }
+ }
+
+ ///
+ /// Looks through the tracked thread collection and aborts any thread that is past its due time
+ ///
+ /// unused
+ private static void CheckForDeadlocks(object state)
+ {
+ if (s_isEnabled)
+ {
+ long now = DateTime.UtcNow.Ticks;
+
+ // Find candidate threads
+ foreach (var threadDuePair in s_threadDueTimes)
+ {
+ if (!threadDuePair.Value.DisableAbort && now > threadDuePair.Value.DueTime)
+ {
+ // Abort the misbehaving thread and the return
+ // NOTE: We only want to abort a single thread at a time to allow the other thread in the deadlock pair to continue
+ Thread t = threadDuePair.Key;
+ Console.WriteLine("Deadlock detected on thread with managed thread id {0}", t.ManagedThreadId);
+ Debugger.Break();
+ t.Join();
+ return;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetectionTaskScheduler.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetectionTaskScheduler.cs
new file mode 100644
index 0000000000..22a540def8
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/DeadlockDetectionTaskScheduler.cs
@@ -0,0 +1,93 @@
+// 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.
+
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DPStressHarness
+{
+ public class DeadlockDetectionTaskScheduler : TaskScheduler
+ {
+ private readonly WaitCallback _runTaskCallback;
+ private readonly ParameterizedThreadStart _runTaskThreadStart;
+#if DEBUG
+ private readonly ConcurrentDictionary _queuedItems = new ConcurrentDictionary();
+#endif
+
+ public DeadlockDetectionTaskScheduler()
+ {
+ _runTaskCallback = new WaitCallback(RunTask);
+ _runTaskThreadStart = new ParameterizedThreadStart(RunTask);
+ }
+
+ // This is only used for debugging, so for retail we'd prefer the perf
+ protected override IEnumerable GetScheduledTasks()
+ {
+#if DEBUG
+ return _queuedItems.Keys;
+#else
+ return new Task[0];
+#endif
+ }
+
+ protected override void QueueTask(Task task)
+ {
+ if ((task.CreationOptions & TaskCreationOptions.LongRunning) == TaskCreationOptions.LongRunning)
+ {
+ // Create a new background thread for long running tasks
+ Thread thread = new Thread(_runTaskThreadStart) { IsBackground = true };
+ thread.Start(task);
+ }
+ else
+ {
+ // Otherwise queue the work on the threadpool
+#if DEBUG
+ _queuedItems.TryAdd(task, null);
+#endif
+
+ ThreadPool.QueueUserWorkItem(_runTaskCallback, task);
+ }
+ }
+
+ protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
+ {
+ if (!taskWasPreviouslyQueued)
+ {
+ // Run the task inline
+ RunTask(task);
+ return true;
+ }
+
+ // Couldn't run the task
+ return false;
+ }
+
+ private void RunTask(object state)
+ {
+ Task inTask = state as Task;
+
+#if DEBUG
+ // Remove from the dictionary of queued items
+ object ignored;
+ _queuedItems.TryRemove(inTask, out ignored);
+#endif
+
+ // Note when the thread started work
+ DeadlockDetection.AddTaskThread();
+
+ try
+ {
+ // Run the task
+ base.TryExecuteTask(inTask);
+ }
+ finally
+ {
+ // Remove the thread from the list when complete
+ DeadlockDetection.RemoveThread();
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj
new file mode 100644
index 0000000000..5540d4951e
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/SqlClient.Stress.Common.csproj
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/TestMetrics.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/TestMetrics.cs
new file mode 100644
index 0000000000..054a822dc1
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/TestMetrics.cs
@@ -0,0 +1,368 @@
+// 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.
+
+using System;
+using System.Diagnostics;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace DPStressHarness
+{
+ public static class TestMetrics
+ {
+ private const string _defaultValue = "unknown";
+
+ private static bool s_valid = false;
+ private static bool s_reset = true;
+ private static Stopwatch s_stopwatch = new Stopwatch();
+ private static long s_workingSet;
+ private static long s_peakWorkingSet;
+ private static long s_privateBytes;
+ private static Assembly s_targetAssembly;
+ private static string s_fileVersion = _defaultValue;
+ private static string s_privateBuild = _defaultValue;
+ private static string s_runLabel = DateTime.Now.ToString();
+ private static Dictionary s_overrides;
+ private static List s_variations = null;
+ private static List s_selectedTests = null;
+ private static bool s_isOfficial = false;
+ private static string s_milestone = _defaultValue;
+ private static string s_branch = _defaultValue;
+ private static List s_categories = null;
+ private static bool s_profileMeasuredCode = false;
+ private static int s_stressThreads = 16;
+ private static int s_stressDuration = 1;
+ private static int? s_exceptionThreshold = null;
+ private static bool s_monitorenabled = false;
+ private static string s_monitormachinename = "localhost";
+ private static int s_randomSeed = 0;
+ private static string s_filter = null;
+ private static bool s_printMethodName = false;
+
+ /// Starts the sample profiler.
+ ///
+ /// Do not inline to avoid errors when the functionality is not used
+ /// and the profiling DLL is not available.
+ ///
+ [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
+ private static void InternalStartProfiling()
+ {
+ // Microsoft.VisualStudio.Profiler.DataCollection.StartProfile(
+ // Microsoft.VisualStudio.Profiler.ProfileLevel.Global,
+ // Microsoft.VisualStudio.Profiler.DataCollection.CurrentId);
+ }
+
+ /// Stops the sample profiler.
+ ///
+ /// Do not inline to avoid errors when the functionality is not used
+ /// and the profiling DLL is not available.
+ ///
+ [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
+ private static void InternalStopProfiling()
+ {
+ // Microsoft.VisualStudio.Profiler.DataCollection.StopProfile(
+ // Microsoft.VisualStudio.Profiler.ProfileLevel.Global,
+ // Microsoft.VisualStudio.Profiler.DataCollection.CurrentId);
+ }
+
+ public static void StartCollection()
+ {
+ s_valid = false;
+
+ s_stopwatch.Reset();
+ s_stopwatch.Start();
+ s_reset = true;
+ }
+
+ public static void StartProfiling()
+ {
+ if (s_profileMeasuredCode)
+ {
+ InternalStartProfiling();
+ }
+ }
+
+ public static void StopProfiling()
+ {
+ if (s_profileMeasuredCode)
+ {
+ InternalStopProfiling();
+ }
+ }
+
+ public static void StopCollection()
+ {
+ s_stopwatch.Stop();
+
+ Process p = Process.GetCurrentProcess();
+ s_workingSet = p.WorkingSet64;
+ s_peakWorkingSet = p.PeakWorkingSet64;
+ s_privateBytes = p.PrivateMemorySize64;
+
+ s_valid = true;
+ }
+
+ public static void PauseTimer()
+ {
+ s_stopwatch.Stop();
+ }
+
+ public static void UnPauseTimer()
+ {
+ if (s_reset)
+ {
+ s_stopwatch.Reset();
+ s_reset = false;
+ }
+
+ s_stopwatch.Start();
+ }
+
+ private static void ThrowIfInvalid()
+ {
+ if (!s_valid) throw new InvalidOperationException("Collection must be stopped before accessing this metric.");
+ }
+
+ public static void Reset()
+ {
+ s_valid = false;
+ s_reset = true;
+ s_stopwatch = new Stopwatch();
+ s_workingSet = new long();
+ s_peakWorkingSet = new long();
+ s_privateBytes = new long();
+ s_targetAssembly = null;
+ s_fileVersion = _defaultValue;
+ s_privateBuild = _defaultValue;
+ s_runLabel = DateTime.Now.ToString();
+ s_overrides = null;
+ s_variations = null;
+ s_selectedTests = null;
+ s_isOfficial = false;
+ s_milestone = _defaultValue;
+ s_branch = _defaultValue;
+ s_categories = null;
+ s_profileMeasuredCode = false;
+ s_stressThreads = 16;
+ s_stressDuration = 1;
+ s_exceptionThreshold = null;
+ s_monitorenabled = false;
+ s_monitormachinename = "localhost";
+ s_randomSeed = 0;
+ s_filter = null;
+ s_printMethodName = false;
+ }
+
+ public static string FileVersion
+ {
+ get { return s_fileVersion; }
+ set { s_fileVersion = value; }
+ }
+
+ public static string PrivateBuild
+ {
+ get { return s_privateBuild; }
+ set { s_privateBuild = value; }
+ }
+
+ public static Assembly TargetAssembly
+ {
+ get { return s_targetAssembly; }
+
+ set
+ {
+ s_targetAssembly = value;
+ s_fileVersion = VersionUtil.GetFileVersion(s_targetAssembly.ManifestModule.FullyQualifiedName);
+ s_privateBuild = VersionUtil.GetPrivateBuild(s_targetAssembly.ManifestModule.FullyQualifiedName);
+ }
+ }
+
+ public static string RunLabel
+ {
+ get { return s_runLabel; }
+ set { s_runLabel = value; }
+ }
+
+ public static string Milestone
+ {
+ get { return s_milestone; }
+ set { s_milestone = value; }
+ }
+
+ public static string Branch
+ {
+ get { return s_branch; }
+ set { s_branch = value; }
+ }
+
+ public static bool IsOfficial
+ {
+ get { return s_isOfficial; }
+ set { s_isOfficial = value; }
+ }
+
+ public static bool IsDefaultValue(string val)
+ {
+ return val.Equals(_defaultValue);
+ }
+
+ public static double ElapsedSeconds
+ {
+ get
+ {
+ ThrowIfInvalid();
+ return s_stopwatch.ElapsedMilliseconds / 1000.0;
+ }
+ }
+
+ public static long WorkingSet
+ {
+ get
+ {
+ ThrowIfInvalid();
+ return s_workingSet;
+ }
+ }
+
+ public static long PeakWorkingSet
+ {
+ get
+ {
+ ThrowIfInvalid();
+ return s_peakWorkingSet;
+ }
+ }
+
+ public static long PrivateBytes
+ {
+ get
+ {
+ ThrowIfInvalid();
+ return s_privateBytes;
+ }
+ }
+
+
+ public static Dictionary Overrides
+ {
+ get
+ {
+ if (s_overrides == null)
+ {
+ s_overrides = new Dictionary(8);
+ }
+ return s_overrides;
+ }
+ }
+
+ public static List Variations
+ {
+ get
+ {
+ if (s_variations == null)
+ {
+ s_variations = new List(8);
+ }
+
+ return s_variations;
+ }
+ }
+
+ public static List SelectedTests
+ {
+ get
+ {
+ if (s_selectedTests == null)
+ {
+ s_selectedTests = new List(8);
+ }
+
+ return s_selectedTests;
+ }
+ }
+
+ public static bool IncludeTest(TestAttributeBase test)
+ {
+ if (s_selectedTests == null || s_selectedTests.Count == 0)
+ return true; // user has no selection - run all
+ else
+ return s_selectedTests.Contains(test.Title);
+ }
+
+ public static List Categories
+ {
+ get
+ {
+ if (s_categories == null)
+ {
+ s_categories = new List(8);
+ }
+
+ return s_categories;
+ }
+ }
+
+ public static bool ProfileMeasuredCode
+ {
+ get { return s_profileMeasuredCode; }
+ set { s_profileMeasuredCode = value; }
+ }
+
+ public static int StressDuration
+ {
+ get { return s_stressDuration; }
+ set { s_stressDuration = value; }
+ }
+
+ public static int StressThreads
+ {
+ get { return s_stressThreads; }
+ set { s_stressThreads = value; }
+ }
+
+ public static int? ExceptionThreshold
+ {
+ get { return s_exceptionThreshold; }
+ set { s_exceptionThreshold = value; }
+ }
+
+ public static bool MonitorEnabled
+ {
+ get { return s_monitorenabled; }
+ set
+ {
+ if(value)
+ {
+ throw new NotImplementedException($"The '{nameof(MonitorEnabled)}' isn't fully implemented!");
+ }
+ s_monitorenabled = value;
+ }
+ }
+
+
+ public static string MonitorMachineName
+ {
+ get { return s_monitormachinename; }
+ set { s_monitormachinename = value; }
+ }
+
+ public static int RandomSeed
+ {
+ get { return s_randomSeed; }
+ set { s_randomSeed = value; }
+ }
+
+ public static string Filter
+ {
+ get { return s_filter; }
+ set { s_filter = value; }
+ }
+
+ public static bool PrintMethodName
+ {
+ get { return s_printMethodName; }
+ set { s_printMethodName = value; }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/VersionUtil.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/VersionUtil.cs
new file mode 100644
index 0000000000..1778903834
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Common/VersionUtil.cs
@@ -0,0 +1,40 @@
+// 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.
+
+using System;
+using System.IO;
+using System.Diagnostics;
+
+#pragma warning disable 618
+
+namespace DPStressHarness
+{
+ public class VersionUtil
+ {
+ public static string GetFileVersion(string moduleName)
+ {
+ FileVersionInfo info = GetFileVersionInfo(moduleName);
+ return info.FileVersion;
+ }
+
+ public static string GetPrivateBuild(string moduleName)
+ {
+ FileVersionInfo info = GetFileVersionInfo(moduleName);
+ return info.PrivateBuild;
+ }
+
+ private static FileVersionInfo GetFileVersionInfo(string moduleName)
+ {
+ if (File.Exists(moduleName))
+ {
+ return FileVersionInfo.GetVersionInfo(Path.GetFullPath(moduleName));
+ }
+ else
+ {
+ string moduleInRuntimeDir = AppContext.BaseDirectory + moduleName;
+ return FileVersionInfo.GetVersionInfo(moduleInRuntimeDir);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/AsyncUtils.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/AsyncUtils.cs
new file mode 100644
index 0000000000..84f0fba0de
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/AsyncUtils.cs
@@ -0,0 +1,185 @@
+// 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.
+
+using System;
+using Microsoft.Data.SqlClient;
+using System.Linq;
+using System.Runtime.ExceptionServices;
+using System.Threading.Tasks;
+using System.Xml;
+using DPStressHarness;
+
+namespace Stress.Data
+{
+ public enum SyncAsyncMode
+ {
+ Sync, // call sync method, e.g. connection.Open(), and return completed task
+ SyncOverAsync, // call async method, e.g. connection.OpenAsync().Wait(), and return completed task
+ Async // call async method, e.g. connection.OpenAsync(), and return running task
+ }
+
+ public static class AsyncUtils
+ {
+ public static Task SyncOrAsyncMethod(Func syncFunc, Func> asyncFunc, SyncAsyncMode mode)
+ {
+ switch (mode)
+ {
+ case SyncAsyncMode.Sync:
+ TResult result = syncFunc();
+ return Task.FromResult(result);
+
+ case SyncAsyncMode.SyncOverAsync:
+ Task t = asyncFunc();
+ WaitAndUnwrapException(t);
+ return t;
+
+ case SyncAsyncMode.Async:
+ return asyncFunc();
+
+ default:
+ throw new ArgumentException(mode.ToString());
+ }
+ }
+
+ public static Task SyncOrAsyncMethod(Action syncFunc, Func asyncFunc, SyncAsyncMode mode)
+ {
+ switch (mode)
+ {
+ case SyncAsyncMode.Sync:
+ syncFunc();
+ return Task.CompletedTask;
+
+ case SyncAsyncMode.SyncOverAsync:
+ Task t = asyncFunc();
+ WaitAndUnwrapException(t);
+ return t;
+
+ case SyncAsyncMode.Async:
+ return asyncFunc();
+
+ default:
+ throw new ArgumentException(mode.ToString());
+ }
+ }
+
+ public static void WaitAll(params Task[] ts)
+ {
+ DeadlockDetection.DisableThreadAbort();
+ try
+ {
+ Task.WaitAll(ts);
+ }
+ finally
+ {
+ DeadlockDetection.EnableThreadAbort();
+ }
+ }
+
+ public static void WaitAllNullable(params Task[] ts)
+ {
+ DeadlockDetection.DisableThreadAbort();
+ try
+ {
+ Task[] tasks = ts.Where(t => t != null).ToArray();
+ Task.WaitAll(tasks);
+ }
+ finally
+ {
+ DeadlockDetection.EnableThreadAbort();
+ }
+ }
+
+ public static void WaitAndUnwrapException(Task t)
+ {
+ DeadlockDetection.DisableThreadAbort();
+ try
+ {
+ t.Wait();
+ }
+ catch (AggregateException ae)
+ {
+ // The callers of this API may not expect AggregateException, so throw the inner exception
+ // If AggregateException contains more than one InnerExceptions, throw it out as it is,
+ // because that is unexpected
+ if ((ae.InnerExceptions != null) && (ae.InnerExceptions.Count == 1))
+ {
+ if (ae.InnerException != null)
+ {
+ ExceptionDispatchInfo info = ExceptionDispatchInfo.Capture(ae.InnerException);
+ info.Throw();
+ }
+ }
+
+ throw;
+ }
+ finally
+ {
+ DeadlockDetection.EnableThreadAbort();
+ }
+ }
+
+ public static T GetResult(IAsyncResult result)
+ {
+ return GetResult((Task)result);
+ }
+
+ public static T GetResult(Task result)
+ {
+ DeadlockDetection.DisableThreadAbort();
+ try
+ {
+ return result.Result;
+ }
+ finally
+ {
+ DeadlockDetection.EnableThreadAbort();
+ }
+ }
+
+ public static SqlDataReader ExecuteReader(SqlCommand command)
+ {
+ DeadlockDetection.DisableThreadAbort();
+ try
+ {
+ return command.ExecuteReader();
+ }
+ finally
+ {
+ DeadlockDetection.EnableThreadAbort();
+ }
+ }
+
+ public static int ExecuteNonQuery(SqlCommand command)
+ {
+ DeadlockDetection.DisableThreadAbort();
+ try
+ {
+ return command.ExecuteNonQuery();
+ }
+ finally
+ {
+ DeadlockDetection.DisableThreadAbort();
+ }
+ }
+
+ public static XmlReader ExecuteXmlReader(SqlCommand command)
+ {
+ DeadlockDetection.DisableThreadAbort();
+ try
+ {
+ return command.ExecuteXmlReader();
+ }
+ finally
+ {
+ DeadlockDetection.EnableThreadAbort();
+ }
+ }
+
+ public static SyncAsyncMode ChooseSyncAsyncMode(Random rnd)
+ {
+ // Any mode is allowed
+ return (SyncAsyncMode)rnd.Next(3);
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs
new file mode 100644
index 0000000000..b61379aa0a
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataSource.cs
@@ -0,0 +1,192 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+
+namespace Stress.Data
+{
+ ///
+ /// supported source types - values for 'type' attribute for 'source' node in App.config
+ ///
+ public enum DataSourceType
+ {
+ SqlServer
+ }
+
+ ///
+ /// base class for database source information (SQL Server, Oracle Server, Access Database file, etc...).
+ /// Data sources are loaded from the app config file.
+ ///
+ public abstract class DataSource
+ {
+ ///
+ /// name of the source - can be used in command line: StressTest ... -override source "sourcename"
+ ///
+ public readonly string Name;
+
+ ///
+ /// database type
+ ///
+ public readonly DataSourceType Type;
+
+ ///
+ /// whether this source is the default one for the type specified
+ ///
+ public readonly bool IsDefault;
+
+ ///
+ /// constructs new data source - called by derived class c-tors only (thus protected)
+ ///
+ protected DataSource(string name, DataSourceType type, bool isDefault)
+ {
+ this.Name = name;
+ this.Type = type;
+ this.IsDefault = isDefault;
+ }
+
+ ///
+ /// this method is used to create the data source, based on its type
+ ///
+ public static DataSource Create(string name, DataSourceType sourceType, bool isDefault, IDictionary properties)
+ {
+ switch (sourceType)
+ {
+ case DataSourceType.SqlServer:
+ return new SqlServerDataSource(name, isDefault, properties);
+ default:
+ throw new ArgumentException("Wrong source type value: " + sourceType);
+ }
+ }
+
+ ///
+ /// used by GetRequiredAttributeValue or derived classes to construct exception on missing required attribute
+ ///
+ /// name of the source (from XML) to include in exception message (for troubleshooting)
+ protected Exception MissingAttributeValueException(string sourceName, string attributeName)
+ {
+ return new ArgumentException(string.Format("Missing or empty value for {0} attribute in the config file for source: {1}", attributeName, sourceName));
+ }
+
+ ///
+ /// search for required attribute or fail if not found
+ ///
+ protected string GetRequiredAttributeValue(string sourceName, IDictionary properties, string valueName, bool allowEmpty)
+ {
+ string value;
+ if (!properties.TryGetValue(valueName, out value) || (value == null) || (!allowEmpty && value.Length == 0))
+ {
+ throw MissingAttributeValueException(sourceName, valueName);
+ }
+ return value;
+ }
+
+ ///
+ /// search for optional attribute or return default vale
+ ///
+ protected string GetOptionalAttributeValue(IDictionary properties, string valueName, string defaultValue)
+ {
+ string value;
+ if (!properties.TryGetValue(valueName, out value) || (value == null))
+ {
+ value = defaultValue;
+ }
+ return value;
+ }
+
+ public abstract void Emit(byte indent);
+ }
+
+ ///
+ /// Represents SQL Server data source. This source is used by SqlClient as well as by ODBC and OLEDB when connecting to SQL with SNAC or MDAC/WDAC
+ ///
+ ///
+ ///
+ ///
+ ///
+ public class SqlServerDataSource : DataSource
+ {
+ public readonly string DataSource;
+ public readonly string Database = "StressTests-" + Guid.NewGuid().ToString();
+ public readonly bool IsLocal;
+ public readonly bool Encrypt;
+
+ // If EntraIdUser is set, the connection will use EntraID password-based
+ // authentication.
+ public readonly string EntraIdUser;
+ public readonly string EntraIdPassword;
+
+ // If EntraIdUser isn't set, and User is set, the connection will use
+ // classic SQL user/password based authentication.
+ public readonly string User;
+ public readonly string Password;
+
+ // if true, test can create connnection strings with integrated security (trusted connection) set to true (or SSPI).
+ public readonly bool SupportsWindowsAuthentication;
+
+ public bool DisableMultiSubnetFailoverSetup;
+
+ public bool DisableNamedPipes;
+
+ internal SqlServerDataSource(string name, bool isDefault, IDictionary properties)
+ : base(name, DataSourceType.SqlServer, isDefault)
+ {
+ this.DataSource = GetOptionalAttributeValue(properties, "dataSource", "localhost");
+
+ this.EntraIdUser = GetOptionalAttributeValue(properties, "entraIdUser", string.Empty);
+ this.EntraIdPassword = GetOptionalAttributeValue(properties, "entraIdPassword", string.Empty);
+
+ this.User = GetOptionalAttributeValue(properties, "user", string.Empty);
+ this.Password = GetOptionalAttributeValue(properties, "password", string.Empty);
+
+ this.IsLocal = bool.Parse(GetOptionalAttributeValue(properties, "isLocal", bool.FalseString));
+ this.Encrypt = bool.Parse(GetOptionalAttributeValue(properties, "encrypt", bool.FalseString));
+
+ this.DisableMultiSubnetFailoverSetup = bool.Parse(GetOptionalAttributeValue(properties, "DisableMultiSubnetFailoverSetup", bool.TrueString));
+
+ this.DisableNamedPipes = bool.Parse(GetOptionalAttributeValue(properties, "DisableNamedPipes", bool.TrueString));
+
+ string temp = GetOptionalAttributeValue(properties, "supportsWindowsAuthentication", "false");
+ if (!string.IsNullOrEmpty(temp))
+ SupportsWindowsAuthentication = Convert.ToBoolean(temp);
+ else
+ SupportsWindowsAuthentication = false;
+
+ if (string.IsNullOrEmpty(EntraIdUser)
+ && string.IsNullOrEmpty(User)
+ && !SupportsWindowsAuthentication)
+ {
+ throw new ArgumentException("SQL Server settings should include either a valid user or SupportsWindowsAuthentication=true");
+ }
+ }
+
+ public override void Emit(byte indent)
+ {
+ string ind = new(' ', indent);
+ Console.WriteLine($"{ind}SqlServerDataSource:");
+ ind = new(' ', indent + 2);
+ Console.WriteLine($"{ind}Name: {Name}");
+ Console.WriteLine($"{ind}Type: {Type}");
+ Console.WriteLine($"{ind}IsDefault: {IsDefault}");
+ Console.WriteLine($"{ind}DataSource: {DataSource}");
+ Console.WriteLine($"{ind}Database: {Database}");
+ Console.WriteLine($"{ind}EntraIdUser: {EntraIdUser}");
+ Console.WriteLine($"{ind}EntraIdPassword: {new string('*', EntraIdPassword.Length)}");
+ Console.WriteLine($"{ind}User: {User}");
+ Console.WriteLine($"{ind}Password: {new string('*', Password.Length)}");
+ Console.WriteLine($"{ind}WinAuth: {SupportsWindowsAuthentication}");
+ Console.WriteLine($"{ind}IsLocal: {IsLocal}");
+ Console.WriteLine($"{ind}Encrypt: {Encrypt}");
+ Console.WriteLine($"{ind}DisableMultiSubnet: {DisableMultiSubnetFailoverSetup}");
+ Console.WriteLine($"{ind}DisableNamedPipes: {DisableNamedPipes}");
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressConnection.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressConnection.cs
new file mode 100644
index 0000000000..46becd4897
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressConnection.cs
@@ -0,0 +1,232 @@
+// 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.
+
+using System;
+using System.Data.Common;
+using Microsoft.Data.SqlClient;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Stress.Data
+{
+ public class DataStressConnection : IDisposable
+ {
+ public DbConnection DbConnection { get; private set; }
+ private readonly bool _clearPoolBeforeClose;
+ public DataStressConnection(DbConnection conn, bool clearPoolBeforeClose = false)
+ {
+ if (conn == null)
+ throw new ArgumentException("Cannot pass in null DbConnection to make new DataStressConnection!");
+ this.DbConnection = conn;
+ _clearPoolBeforeClose = clearPoolBeforeClose;
+ }
+
+ private short _spid = 0;
+
+ [ThreadStatic]
+ private static TrackedRandom t_randomInstance;
+ private static TrackedRandom RandomInstance
+ {
+ get
+ {
+ if (t_randomInstance == null)
+ t_randomInstance = new TrackedRandom();
+ return t_randomInstance;
+ }
+ }
+
+ public void Open()
+ {
+ bool sync = RandomInstance.NextBool();
+
+ if (sync)
+ {
+ OpenSync();
+ }
+ else
+ {
+ Task t = OpenAsync();
+ AsyncUtils.WaitAndUnwrapException(t);
+ }
+ }
+
+ public async Task OpenAsync()
+ {
+ int startMilliseconds = Environment.TickCount;
+ try
+ {
+ await DbConnection.OpenAsync();
+ }
+ catch (ObjectDisposedException e)
+ {
+ HandleObjectDisposedException(e, true);
+ throw;
+ }
+ catch (InvalidOperationException e)
+ {
+ int endMilliseconds = Environment.TickCount;
+
+ // we may be able to handle this exception
+ HandleInvalidOperationException(e, startMilliseconds, endMilliseconds, true);
+ throw;
+ }
+
+ GetSpid();
+ }
+
+ private void OpenSync()
+ {
+ int startMilliseconds = Environment.TickCount;
+ try
+ {
+ DbConnection.Open();
+ }
+ catch (ObjectDisposedException e)
+ {
+ HandleObjectDisposedException(e, false);
+ throw;
+ }
+ catch (InvalidOperationException e)
+ {
+ int endMilliseconds = Environment.TickCount;
+
+ // we may be able to handle this exception
+ HandleInvalidOperationException(e, startMilliseconds, endMilliseconds, false);
+ throw;
+ }
+
+ GetSpid();
+ }
+
+ private void HandleObjectDisposedException(ObjectDisposedException e, bool async)
+ {
+ // Race condition in DbConnectionFactory.TryGetConnection results in an ObjectDisposedException when calling OpenAsync on a non-pooled connection
+ string methodName = async ? "OpenAsync()" : "Open()";
+ throw DataStressErrors.ProductError(
+ "Hit ObjectDisposedException in SqlConnection." + methodName, e);
+ }
+
+ private static int s_fastTimeoutCountOpen; // number of times hit by SqlConnection.Open
+ private static int s_fastTimeoutCountOpenAsync; // number of times hit by SqlConnection.OpenAsync
+ private static readonly DateTime s_startTime = DateTime.Now;
+
+ private const int MaxFastTimeoutCountPerDay = 200;
+
+ ///
+ /// Handles InvalidOperationException generated from Open or OpenAsync calls.
+ /// For any other type of Exception, it simply returns
+ ///
+ private void HandleInvalidOperationException(InvalidOperationException e, int startMilliseconds, int endMilliseconds, bool async)
+ {
+ int elapsedMilliseconds = unchecked(endMilliseconds - startMilliseconds); // unchecked to handle overflow of Environment.TickCount
+
+ // Since InvalidOperationExceptions due to timeout can be caused by issues
+ // (e.g. network hiccup, server unavailable, etc) we need a heuristic to guess whether or not this exception
+ // should have happened or not.
+ bool wasTimeoutFromPool = (e.GetType() == typeof(InvalidOperationException)) &&
+ (e.Message.StartsWith("Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool"));
+
+ bool wasTooEarly = (elapsedMilliseconds < ((DbConnection.ConnectionTimeout - 5) * 1000));
+
+ if (wasTimeoutFromPool && wasTooEarly)
+ {
+ if (async)
+ Interlocked.Increment(ref s_fastTimeoutCountOpenAsync);
+ else
+ Interlocked.Increment(ref s_fastTimeoutCountOpen);
+ }
+ }
+
+ ///
+ /// Gets spid value.
+ ///
+ ///
+ /// If we want to kill the connection, we get its spid up front before the test case uses the connection. Otherwise if
+ /// we try to get the spid when KillConnection is called, then the connection could be in a bad state (e.g. enlisted in
+ /// aborted transaction, or has open datareader) and we will fail to get the spid. Also the randomization is put here
+ /// instead of in KillConnection because otherwise this method would execute a command for every single connection which
+ /// most of the time will not be used later.
+ ///
+ private void GetSpid()
+ {
+ if (DbConnection is SqlConnection && RandomInstance.Next(0, 20) == 0)
+ {
+ using (var cmd = DbConnection.CreateCommand())
+ {
+ cmd.CommandText = "select @@spid";
+ _spid = (short)cmd.ExecuteScalar();
+ }
+ }
+ else
+ {
+ _spid = 0;
+ }
+ }
+
+ ///
+ /// Kills the given connection using "kill [spid]" if the parameter is nonzero
+ ///
+ private void KillConnection()
+ {
+ DataStressErrors.Assert(_spid != 0, "Called KillConnection with spid != 0");
+
+ using (var killerConn = DataTestGroup.Factory.CreateConnection())
+ {
+ killerConn.Open();
+
+ using (var killerCmd = killerConn.CreateCommand())
+ {
+ killerCmd.CommandText = "begin try kill " + _spid + " end try begin catch end catch";
+ killerCmd.ExecuteNonQuery();
+ }
+ }
+ }
+
+ ///
+ /// Kills the given connection using "kill [spid]" if the parameter is nonzero
+ ///
+ /// a Task that is asynchronously killing the connection, or null if the connection is not being killed
+ public Task KillConnectionAsync()
+ {
+ if (_spid == 0)
+ return null;
+ else
+ return Task.Factory.StartNew(() => KillConnection());
+ }
+
+ public void Close()
+ {
+ if (_spid != 0)
+ {
+ KillConnection();
+
+ // Wait before putting the connection back in the pool, to ensure that
+ // the pool checks the connection the next time it is used.
+ Task.Delay(10).ContinueWith((t) => DbConnection.Close());
+ }
+ else
+ {
+ // If this is a SqlConnection, and it is a connection with a unique connection string that we will never use again,
+ // then call SqlConnection.ClearPool() before closing so that it is fully closed and does not waste client & server resources.
+ if (_clearPoolBeforeClose)
+ {
+ SqlConnection sqlConn = DbConnection as SqlConnection;
+ if (sqlConn != null) SqlConnection.ClearPool(sqlConn);
+ }
+
+ DbConnection.Close();
+ }
+ }
+
+ public void Dispose()
+ {
+ Close();
+ }
+
+ public DbCommand CreateCommand()
+ {
+ return DbConnection.CreateCommand();
+ }
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressErrors.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressErrors.cs
new file mode 100644
index 0000000000..46b7751d50
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressErrors.cs
@@ -0,0 +1,215 @@
+// 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.
+
+using System;
+using System.Diagnostics;
+
+namespace Stress.Data
+{
+ public enum ErrorHandlingAction
+ {
+ // If you add an item here, remember to add it to all of the methods below
+ DebugBreak,
+ ThrowException
+ }
+
+ ///
+ /// Static class containing methods to report errors.
+ ///
+ /// The StressTest executor will eat exceptions that are thrown and write them out to the console. In theory these should all be
+ /// either harmless exceptions or product bugs, however at present there are a large number of test issues that will cause a flood
+ /// of exceptions. Therefore if something actually bad happens (e.g. a known product bug is hit due to regression, or a major test
+ /// programming error) this error would be easy to miss if it were reported just by throwing an exception. To solve this, we use
+ /// this class for structured & consistent handling of errors.
+ ///
+ public static class DataStressErrors
+ {
+ private static void DebugBreak(string message, Exception exception)
+ {
+ // Print out the error before breaking to make debugging easier
+ Console.WriteLine(message);
+ if (exception != null)
+ {
+ Console.WriteLine(exception);
+ }
+
+ Debugger.Break();
+ }
+
+ ///
+ /// Reports that a product bug has been hit. The action that will be taken is configurable in the .config file.
+ /// This can be used to check for regressions of known product bugs.
+ ///
+ /// A description of the product bug hit (e.g. title, bug number & database, more information)
+ /// The exception that was thrown that indicates a product bug, or null if the product bug was detected without
+ /// having thrown an exception
+ /// An exception that the caller should throw.
+ public static Exception ProductError(string description, Exception exception = null)
+ {
+ switch (DataStressSettings.Instance.ActionOnProductError)
+ {
+ case ErrorHandlingAction.DebugBreak:
+ DebugBreak("Hit product error: " + description, exception);
+ return new ProductErrorException(description, exception);
+
+ case ErrorHandlingAction.ThrowException:
+ return new ProductErrorException(description, exception);
+
+ default:
+ throw UnhandledCaseError(DataStressSettings.Instance.ActionOnProductError);
+ }
+ }
+
+ ///
+ /// Reports that a non-fatal test error has been hit. The action that will be taken is configurable in the .config file.
+ /// This should be used for test errors that do not prevent the test from running.
+ ///
+ /// A description of the error
+ /// The exception that was thrown that indicates an error, or null if the error was detected without
+ /// An exception that the caller should throw.
+ public static Exception TestError(string description, Exception exception = null)
+ {
+ switch (DataStressSettings.Instance.ActionOnTestError)
+ {
+ case ErrorHandlingAction.DebugBreak:
+ DebugBreak("Hit test error: " + description, exception);
+ return new TestErrorException(description, exception);
+
+ case ErrorHandlingAction.ThrowException:
+ return new TestErrorException(description, exception);
+
+ default:
+ throw UnhandledCaseError(DataStressSettings.Instance.ActionOnTestError);
+ }
+ }
+
+ ///
+ /// Reports that a programming error in the test code has occurred. The action that will be taken is configurable in the .config file.
+ /// This must strictly be used to report programming errors. It should not be in any way possible to see one of these errors unless
+ /// you make an incorrect change to the code, for example having an unhandled case in a switch statement.
+ ///
+ /// A description of the error
+ /// The exception that was thrown that indicates an error, or null if the error was detected without
+ /// having thrown an exception
+ /// An exception that the caller should throw.
+ private static Exception ProgrammingError(string description, Exception exception = null)
+ {
+ switch (DataStressSettings.Instance.ActionOnProgrammingError)
+ {
+ case ErrorHandlingAction.DebugBreak:
+ DebugBreak("Hit programming error: " + description, exception);
+ return new ProgrammingErrorException(description, exception);
+
+ case ErrorHandlingAction.ThrowException:
+ return new ProgrammingErrorException(description, exception);
+
+ default:
+ // If we are here then it's a programming error, but calling UnhandledCaseError here would cause an inifite loop.
+ goto case ErrorHandlingAction.DebugBreak;
+ }
+ }
+
+ ///
+ /// Reports that an unhandled case in a switch statement in the test code has occurred. The action that will be taken is configurable
+ /// as a programming error in the .config file. It should not be in any way possible to see one of these errors unless
+ /// you make an incorrect change to the test code, for example having an unhandled case in a switch statement.
+ ///
+ /// The value that was not handled in the switch statement
+ /// An exception that the caller should throw.
+ public static Exception UnhandledCaseError(T unhandledValue)
+ {
+ return ProgrammingError("Unhandled case in switch statement: " + unhandledValue);
+ }
+
+ ///
+ /// Asserts that a condition is true. If the condition is false then throws a ProgrammingError.
+ /// This must strictly be used to report programming errors. It should not be in any way possible to see one of these errors unless
+ /// you make an incorrect change to the code, for example having an unhandled case in a switch statement.
+ ///
+ /// A condition to assert
+ /// A description of the error
+ /// if the condition is false
+ public static void Assert(bool condition, string description)
+ {
+ if (!condition)
+ {
+ throw ProgrammingError(description);
+ }
+ }
+
+ ///
+ /// Reports that a fatal error has happened. This is an error that completely prevents the test from continuing,
+ /// for example a setup failure. Ordinary programming errors should not be handled by this method.
+ ///
+ /// A description of the error
+ /// An exception that the caller should throw.
+ public static Exception FatalError(string description)
+ {
+ Console.WriteLine("Fatal test error: {0}", description);
+ Debugger.Break(); // Give the user a chance to debug
+ Environment.FailFast("Fatal error. Exit.");
+ return new Exception(); // Caller should throw this to indicate to the compiler that any code after the call is unreachable
+ }
+
+ #region Exception types
+
+ // These exception types are provided so that they can be easily found in logs, i.e. just do a text search in the console
+ // output log for "ProductErrorException"
+
+ private class ProductErrorException : Exception
+ {
+ public ProductErrorException()
+ : base()
+ {
+ }
+
+ public ProductErrorException(string message)
+ : base(message)
+ {
+ }
+
+ public ProductErrorException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+
+ private class ProgrammingErrorException : Exception
+ {
+ public ProgrammingErrorException()
+ : base()
+ {
+ }
+
+ public ProgrammingErrorException(string message)
+ : base(message)
+ {
+ }
+
+ public ProgrammingErrorException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+
+ private class TestErrorException : Exception
+ {
+ public TestErrorException()
+ : base()
+ {
+ }
+
+ public TestErrorException(string message)
+ : base(message)
+ {
+ }
+
+ public TestErrorException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+ #endregion
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs
new file mode 100644
index 0000000000..8e06a1de89
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressFactory.cs
@@ -0,0 +1,955 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Data;
+using System.Data.Common;
+using System.Diagnostics;
+
+namespace Stress.Data
+{
+ ///
+ /// Base class to generate utility objects required for stress tests to run. For example: connection strings, command texts,
+ /// data tables and views, and other information
+ ///
+ public abstract class DataStressFactory : IDisposable
+ {
+ // This is the maximum number of rows, stress will operate on
+ public const int Depth = 100;
+
+ // A string value to be used for scalar data retrieval while constructing
+ // a select statement that retrieves multiple result sets.
+ public static readonly string LargeStringParam = new string('p', 2000);
+
+ // A temp table that when create puts the server session into a non-recoverable state until dropped.
+ private static readonly string s_tempTableName = string.Format("#stress_{0}", Guid.NewGuid().ToString("N"));
+
+ // The languages used for "SET LANGUAGE [language]" statements that modify the server session state. Let's
+ // keep error message readable so we're only using english languages.
+ private static string[] s_languages = new string[]
+ {
+ "English",
+ "British English",
+ };
+
+ public DbProviderFactory DbFactory { get; private set; }
+
+ protected DataStressFactory(DbProviderFactory factory)
+ {
+ DataStressErrors.Assert(factory != null, "Argument to DataStressFactory constructor is null");
+ this.DbFactory = factory;
+ }
+
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ }
+
+ public abstract string GetParameterName(string pName);
+
+
+ public abstract bool PrimaryKeyValueIsRequired
+ {
+ get;
+ }
+
+ [Flags]
+ public enum SelectStatementOptions
+ {
+ UseNOLOCK = 0x1,
+
+ // keep last
+ Default = 0
+ }
+
+ #region PoolingStressMode
+
+ public enum PoolingStressMode
+ {
+ RandomizeConnectionStrings, // Use many different connection strings with the same identity, which will result in many DbConnectionPoolGroups each containing one DbConnectionPool
+ }
+
+ protected PoolingStressMode CurrentPoolingStressMode
+ {
+ get;
+ private set;
+ }
+
+ #endregion
+
+
+ ///
+ /// Creates a new connection and initializes it with random connection string generated from the factory's source
+ /// Note: if rnd is null, create a connection with minimal string required to connect to the target database
+ ///
+ /// Randomizes Connection Pool enablement, the application Name to randomize connection pool
+ ///
+ ///
+ public DataStressConnection CreateConnection(Random rnd = null, ConnectionStringOptions options = ConnectionStringOptions.Default)
+ {
+ // Determine connection options (connection string, identity, etc)
+ string connectionString = CreateBaseConnectionString(rnd, options);
+ bool clearPoolBeforeClose = false;
+
+ if (rnd != null)
+ {
+ // Connection string and/or identity are randomized
+
+ // We implement this using the Application Name field in the connection string since this field
+ // should not affect behaviour other than connection pooling, since all connections in a pool
+ // must have the exact same connection string (including Application Name)
+
+ if (rnd.NextBool(.1))
+ {
+ // Disable pooling
+ connectionString += ";Pooling=false;";
+ }
+ else if (rnd.NextBool(0.001))
+ {
+ // Use a unique Application Name to get a new connection from a new pool. We do this in order to
+ // stress the code that creates/deletes pools.
+ connectionString = string.Format("{0}; Pooling=true; Application Name=\"{1}\";", connectionString, GetRandomApplicationName());
+
+ // Tell DataStressConnection to call SqlConnection.ClearPool when closing the connection. This ensures
+ // we do not keep a large number of connections in the pool that we will never use again.
+ clearPoolBeforeClose = true;
+ }
+ else
+ {
+ switch (CurrentPoolingStressMode)
+ {
+ case PoolingStressMode.RandomizeConnectionStrings:
+ // Use one of the pre-generated Application Names in order to get a pooled connection with a randomized connection string
+ connectionString = string.Format("{0}; Pooling=true; Application Name=\"{1}\";", connectionString, _applicationNames[rnd.Next(_applicationNames.Count)]);
+ break;
+ default:
+ throw DataStressErrors.UnhandledCaseError(CurrentPoolingStressMode);
+ }
+ }
+ }
+
+ // All options have been determined, now create
+ DbConnection con = DbFactory.CreateConnection();
+ con.ConnectionString = connectionString;
+ return new DataStressConnection(con, clearPoolBeforeClose);
+ }
+
+ [Flags]
+ public enum ConnectionStringOptions
+ {
+ Default = 0,
+
+ // by default, MARS is disabled
+ EnableMars = 0x2,
+
+ // by default, MultiSubnetFailover is enabled
+ DisableMultiSubnetFailover = 0x8
+ }
+
+ ///
+ /// Creates a new connection string.
+ /// Note: if rnd is null, create minimal connection string required to connect to the target database (used during setup)
+ /// Otherwise, string is randomized to enable multiple pools.
+ ///
+ public abstract string CreateBaseConnectionString(Random rnd, ConnectionStringOptions options);
+
+ protected virtual int GetNumDifferentApplicationNames()
+ {
+ return DataStressSettings.Instance.NumberOfConnectionPools;
+ }
+
+ private string GetRandomApplicationName()
+ {
+ return Guid.NewGuid().ToString();
+ }
+
+
+ ///
+ /// Returns index of a random table
+ /// This will be used to narrow down memory leaks
+ /// related to specific tables.
+ ///
+ public TableMetadata GetRandomTable(Random rnd)
+ {
+ return TableMetadataList[rnd.Next(TableMetadataList.Count)];
+ }
+
+ ///
+ /// Returns a random command object
+ ///
+ public DbCommand GetCommand(Random rnd, TableMetadata table, DataStressConnection conn, bool query, bool isXml = false)
+ {
+ if (query)
+ {
+ return GetSelectCommand(rnd, table, conn, isXml);
+ }
+ else
+ {
+ // make sure arguments are correct
+ DataStressErrors.Assert(!isXml, "wrong usage of GetCommand: cannot create command with FOR XML that is not query");
+
+ int select = rnd.Next(4);
+ switch (select)
+ {
+ case 0:
+ return GetUpdateCommand(rnd, table, conn);
+ case 1:
+ return GetInsertCommand(rnd, table, conn);
+ case 2:
+ return GetDeleteCommand(rnd, table, conn);
+ default:
+ return GetSelectCommand(rnd, table, conn);
+ }
+ }
+ }
+
+ private DbCommand CreateCommand(Random rnd, DataStressConnection conn)
+ {
+ DbCommand cmd;
+ if (conn == null)
+ {
+ cmd = DbFactory.CreateCommand();
+ }
+ else
+ {
+ cmd = conn.CreateCommand();
+ }
+
+ if (rnd != null)
+ {
+ cmd.CommandTimeout = rnd.NextBool() ? 30 : 600;
+ }
+
+ return cmd;
+ }
+
+ ///
+ /// Returns a random SELECT command
+ ///
+ public DbCommand GetSelectCommand(Random rnd, TableMetadata tableMetadata, DataStressConnection conn, bool isXml = false)
+ {
+ DbCommand com = CreateCommand(rnd, conn);
+ StringBuilder cmdText = new StringBuilder();
+ cmdText.Append(GetSelectCommandForMultipleRows(rnd, com, tableMetadata, isXml));
+
+ // 33% of the time, we also want to add another batch to the select command to allow for
+ // multiple result sets.
+ if ((!isXml) && (rnd.Next(0, 3) == 0))
+ {
+ cmdText.Append(";").Append(GetSelectCommandForScalarValue(com));
+ }
+
+ if ((!isXml) && ShouldModifySession(rnd))
+ {
+ cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd));
+ }
+
+ com.CommandText = cmdText.ToString();
+ return com;
+ }
+
+ ///
+ /// Returns a SELECT command that retrieves data from a table
+ ///
+ private string GetSelectCommandForMultipleRows(Random rnd, DbCommand com, TableMetadata inputTable, bool isXml)
+ {
+ int rowcount = rnd.Next(Depth);
+
+ StringBuilder cmdText = new StringBuilder();
+ cmdText.Append("SELECT TOP ");
+ cmdText.Append(rowcount); //Jonfo added this to prevent table scan of 75k row tables
+ cmdText.Append(" PrimaryKey");
+
+ List columns = inputTable.Columns;
+ int colindex = rnd.Next(0, columns.Count);
+
+ for (int i = 0; i <= colindex; i++)
+ {
+ if (columns[i].ColumnName == "PrimaryKey") continue;
+ cmdText.Append(", ");
+ cmdText.Append(columns[i].ColumnName);
+ }
+
+ cmdText.Append(" FROM \"");
+ cmdText.Append(inputTable.TableName);
+ cmdText.Append("\" WITH(NOLOCK) WHERE PrimaryKey ");
+
+ // We randomly pick an operator from '>' or '=' to allow for randomization
+ // of possible rows returned by this query. This approach *may* help
+ // in reducing the likelihood of multiple threads accessing same rows.
+ // If multiple threads access same rows, there may be locking issues
+ // which may be avoided because of this randomization.
+ string op = rnd.NextBool() ? ">" : "=";
+ cmdText.Append(op).Append(" ");
+
+ string pName = GetParameterName("P0");
+ cmdText.Append(pName);
+
+ DbParameter param = DbFactory.CreateParameter();
+ param.ParameterName = pName;
+ param.Value = GetRandomPK(rnd, inputTable);
+ param.DbType = DbType.Int32;
+ com.Parameters.Add(param);
+
+ return cmdText.ToString();
+ }
+
+ ///
+ /// Returns a SELECT command that returns a single string parameter value.
+ ///
+ private string GetSelectCommandForScalarValue(DbCommand com)
+ {
+ string pName = GetParameterName("P1");
+ StringBuilder cmdText = new StringBuilder();
+
+ cmdText.Append("SELECT ").Append(pName);
+
+ DbParameter param = DbFactory.CreateParameter();
+ param.ParameterName = pName;
+ param.Value = LargeStringParam;
+ param.Size = LargeStringParam.Length;
+ param.DbType = DbType.String;
+ com.Parameters.Add(param);
+
+ return cmdText.ToString();
+ }
+
+ ///
+ /// Returns a random existing Primary Key value
+ ///
+ private int GetRandomPK(Random rnd, TableMetadata table)
+ {
+ using (DataStressConnection conn = CreateConnection())
+ {
+ conn.Open();
+
+ // This technique to get a random row comes from http://www.4guysfromrolla.com/webtech/042606-1.shtml
+ // When you set rowcount and then select into a scalar value, then the query is optimised so that
+ // just the last value is selected. So if n = ROWCOUNT then the query returns the n'th row.
+
+ int rowNumber = rnd.Next(Depth);
+
+ DbCommand com = conn.CreateCommand();
+ string cmdText = string.Format(
+ @"SET ROWCOUNT {0};
+ DECLARE @PK INT;
+ SELECT @PK = PrimaryKey FROM {1} WITH(NOLOCK)
+ SELECT @PK", rowNumber, table.TableName);
+
+ com.CommandText = cmdText;
+
+ object result = com.ExecuteScalarSyncOrAsync(CancellationToken.None, rnd).Result;
+ if (result == DBNull.Value)
+ {
+ throw DataStressErrors.TestError(string.Format("Table {0} returned DBNull for primary key", table.TableName));
+ }
+ else
+ {
+ int primaryKey = (int)result;
+ return primaryKey;
+ }
+ }
+ }
+
+ private DbParameter CreateRandomParameter(Random rnd, string prefix, TableColumn column)
+ {
+ DbParameter param = DbFactory.CreateParameter();
+
+ param.ParameterName = GetParameterName(prefix);
+
+ param.Value = GetRandomData(rnd, column);
+
+ return param;
+ }
+
+ ///
+ /// Returns a random UPDATE command
+ ///
+ public DbCommand GetUpdateCommand(Random rnd, TableMetadata table, DataStressConnection conn)
+ {
+ DbCommand com = CreateCommand(rnd, conn);
+
+ StringBuilder cmdText = new StringBuilder();
+ cmdText.Append("UPDATE \"");
+ cmdText.Append(table.TableName);
+ cmdText.Append("\" SET ");
+
+ List columns = table.Columns;
+ int numColumns = rnd.Next(2, columns.Count);
+ bool mostlyNull = rnd.NextBool(0.1); // 10% of rows have 90% chance of each column being null, in order to test nbcrow
+
+ for (int i = 0; i < numColumns; i++)
+ {
+ if (columns[i].ColumnName == "PrimaryKey") continue;
+ if (columns[i].ColumnName.ToUpper() == "TIMESTAMP_FLD") continue;
+
+ if (i > 1) cmdText.Append(", ");
+ cmdText.Append(columns[i].ColumnName);
+ cmdText.Append(" = ");
+
+ if (mostlyNull && rnd.NextBool(0.9))
+ {
+ cmdText.Append("NULL");
+ }
+ else
+ {
+ DbParameter param = CreateRandomParameter(rnd, string.Format("P{0}", (i + 1)), columns[i]);
+ cmdText.Append(param.ParameterName);
+ com.Parameters.Add(param);
+ }
+ }
+
+ cmdText.Append(" WHERE PrimaryKey = ");
+ string pName = GetParameterName("P0");
+ cmdText.Append(pName);
+ DbParameter keyParam = DbFactory.CreateParameter();
+ keyParam.ParameterName = pName;
+ keyParam.Value = GetRandomPK(rnd, table);
+ com.Parameters.Add(keyParam);
+
+ if (ShouldModifySession(rnd))
+ {
+ cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd));
+ }
+
+ com.CommandText = cmdText.ToString();
+ return com;
+ }
+
+ ///
+ /// Returns a random INSERT command
+ ///
+ public DbCommand GetInsertCommand(Random rnd, TableMetadata table, DataStressConnection conn)
+ {
+ DbCommand com = CreateCommand(rnd, conn);
+
+ StringBuilder cmdText = new StringBuilder();
+ cmdText.Append("INSERT INTO \"");
+ cmdText.Append(table.TableName);
+ cmdText.Append("\" (");
+
+ StringBuilder valuesText = new StringBuilder();
+ valuesText.Append(") VALUES (");
+
+ List columns = table.Columns;
+ int numColumns = rnd.Next(2, columns.Count);
+ bool mostlyNull = rnd.NextBool(0.1); // 10% of rows have 90% chance of each column being null, in order to test nbcrow
+
+ for (int i = 0; i < numColumns; i++)
+ {
+ if (columns[i].ColumnName.ToUpper() == "PRIMARYKEY") continue;
+
+ if (i > 1)
+ {
+ cmdText.Append(", ");
+ valuesText.Append(", ");
+ }
+
+ cmdText.Append(columns[i].ColumnName);
+
+ if (columns[i].ColumnName.ToUpper() == "TIMESTAMP_FLD")
+ {
+ valuesText.Append("DEFAULT"); // Cannot insert an explicit value in a timestamp field
+ }
+ else if (mostlyNull && rnd.NextBool(0.9))
+ {
+ valuesText.Append("NULL");
+ }
+ else
+ {
+ DbParameter param = CreateRandomParameter(rnd, string.Format("P{0}", i + 1), columns[i]);
+
+ valuesText.Append(param.ParameterName);
+ com.Parameters.Add(param);
+ }
+ }
+
+ // To deal databases that do not support auto-incremented columns (Oracle?)
+ // if (!columns["PrimaryKey"].AutoIncrement)
+ if (PrimaryKeyValueIsRequired)
+ {
+ DbParameter param = CreateRandomParameter(rnd, "P0", table.GetColumn("PrimaryKey"));
+ cmdText.Append(", PrimaryKey");
+ valuesText.Append(", ");
+ valuesText.Append(param.ParameterName);
+ com.Parameters.Add(param);
+ }
+
+ valuesText.Append(")");
+ cmdText.Append(valuesText);
+
+ if (ShouldModifySession(rnd))
+ {
+ cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd));
+ }
+
+ com.CommandText = cmdText.ToString();
+ return com;
+ }
+
+ ///
+ /// Returns a random DELETE command
+ ///
+ public DbCommand GetDeleteCommand(Random rnd, TableMetadata table, DataStressConnection conn)
+ {
+ DbCommand com = CreateCommand(rnd, conn);
+
+ StringBuilder cmdText = new StringBuilder();
+ cmdText.Append("DELETE FROM \"");
+
+ List columns = table.Columns;
+ string pName = GetParameterName("P0");
+ cmdText.Append(table.TableName);
+ cmdText.Append("\" WHERE PrimaryKey = ");
+ cmdText.Append(pName);
+
+ DbParameter param = DbFactory.CreateParameter();
+ param.ParameterName = pName;
+ param.Value = GetRandomPK(rnd, table);
+ com.Parameters.Add(param);
+
+ if (ShouldModifySession(rnd))
+ {
+ cmdText.Append(";").Append(GetRandomSessionModificationStatement(rnd));
+ }
+
+ com.CommandText = cmdText.ToString();
+ return com;
+ }
+
+ public bool ShouldModifySession(Random rnd)
+ {
+ // 33% of the time, we want to modify the user session on the server
+ return rnd.NextBool(.33);
+ }
+
+ ///
+ /// Returns a random statement that will modify the session on the server.
+ ///
+ public string GetRandomSessionModificationStatement(Random rnd)
+ {
+ string sessionStmt = null;
+ int select = rnd.Next(3);
+ switch (select)
+ {
+ case 0:
+ // Create a SET CONTEXT_INFO statement using a hex string of random data
+ StringBuilder sb = new StringBuilder("0x");
+ int count = rnd.Next(1, 129);
+ for (int i = 0; i < count; i++)
+ {
+ sb.AppendFormat("{0:x2}", (byte)rnd.Next(0, (int)(byte.MaxValue + 1)));
+ }
+ string contextInfoData = sb.ToString();
+ sessionStmt = string.Format("SET CONTEXT_INFO {0}", contextInfoData);
+ break;
+
+ case 1:
+ // Create or drop the temp table
+ sessionStmt = string.Format("IF OBJECT_ID('tempdb..{0}') IS NULL CREATE TABLE {0}(id INT) ELSE DROP TABLE {0}", s_tempTableName);
+ break;
+
+ default:
+ // Create a SET LANGUAGE statement
+ sessionStmt = string.Format("SET LANGUAGE N'{0}'", s_languages[rnd.Next(s_languages.Length)]);
+ break;
+ }
+ return sessionStmt;
+ }
+
+ ///
+ /// Returns random data
+ ///
+ public object GetRandomData(Random rnd, TableColumn column)
+ {
+ int length = column.MaxLength;
+ int maxTargetLength = (length > 255 || length == -1) ? 255 : length;
+
+ DbType dbType = GetDbType(column);
+ return GetRandomData(rnd, dbType, maxTargetLength);
+ }
+
+ private DbType GetDbType(TableColumn column)
+ {
+ switch (column.ColumnName)
+ {
+ case "bit_FLD": return DbType.Boolean;
+ case "tinyint_FLD": return DbType.Byte;
+ case "smallint_FLD": return DbType.Int16;
+ case "int_FLD": return DbType.Int32;
+ case "PrimaryKey": return DbType.Int32;
+ case "bigint_FLD": return DbType.Int64;
+ case "real_FLD": return DbType.Single;
+ case "float_FLD": return DbType.Double;
+ case "smallmoney_FLD": return DbType.Decimal;
+ case "money_FLD": return DbType.Decimal;
+ case "decimal_FLD": return DbType.Decimal;
+ case "numeric_FLD": return DbType.Decimal;
+ case "datetime_FLD": return DbType.DateTime;
+ case "smalldatetime_FLD": return DbType.DateTime;
+ case "datetime2_FLD": return DbType.DateTime2;
+ case "timestamp_FLD": return DbType.Binary;
+ case "date_FLD": return DbType.Date;
+ case "time_FLD": return DbType.Time;
+ case "datetimeoffset_FLD": return DbType.DateTimeOffset;
+ case "uniqueidentifier_FLD": return DbType.Guid;
+ case "sql_variant_FLD": return DbType.Object;
+ case "image_FLD": return DbType.Binary;
+ case "varbinary_FLD": return DbType.Binary;
+ case "binary_FLD": return DbType.Binary;
+ case "char_FLD": return DbType.String;
+ case "varchar_FLD": return DbType.String;
+ case "text_FLD": return DbType.String;
+ case "ntext_FLD": return DbType.String;
+ case "nvarchar_FLD": return DbType.String;
+ case "nchar_FLD": return DbType.String;
+ case "nvarcharmax_FLD": return DbType.String;
+ case "varbinarymax_FLD": return DbType.Binary;
+ case "varcharmax_FLD": return DbType.String;
+ case "xml_FLD": return DbType.Xml;
+ default: throw DataStressErrors.UnhandledCaseError(column.ColumnName);
+ }
+ }
+
+ protected virtual object GetRandomData(Random rnd, DbType dbType, int maxLength)
+ {
+ byte[] buffer;
+ switch (dbType)
+ {
+ case DbType.Boolean:
+ return (rnd.Next(2) == 0 ? false : true);
+ case DbType.Byte:
+ return rnd.Next(byte.MinValue, byte.MaxValue + 1);
+ case DbType.Int16:
+ return rnd.Next(short.MinValue, short.MaxValue + 1);
+ case DbType.Int32:
+ return (rnd.Next(2) == 0 ? int.MaxValue / rnd.Next(1, 3) : int.MinValue / rnd.Next(1, 3));
+ case DbType.Int64:
+ return (rnd.Next(2) == 0 ? long.MaxValue / rnd.Next(1, 3) : long.MinValue / rnd.Next(1, 3));
+ case DbType.Single:
+ return rnd.NextDouble() * (rnd.Next(2) == 0 ? float.MaxValue : float.MinValue);
+ case DbType.Double:
+ return rnd.NextDouble() * (rnd.Next(2) == 0 ? double.MaxValue : double.MinValue);
+ case DbType.Decimal:
+ return rnd.Next(short.MinValue, short.MaxValue + 1);
+ case DbType.DateTime:
+ case DbType.DateTime2:
+ return DateTime.Now;
+ case DbType.Date:
+ return DateTime.Now.Date;
+ case DbType.Time:
+ return DateTime.Now.TimeOfDay.ToString("c");
+ case DbType.DateTimeOffset:
+ return DateTimeOffset.Now;
+ case DbType.Guid:
+ buffer = new byte[16];
+ rnd.NextBytes(buffer);
+ return (new Guid(buffer));
+ case DbType.Object:
+ case DbType.Binary:
+ rnd.NextBytes(buffer = new byte[rnd.Next(1, maxLength)]);
+ return buffer;
+ case DbType.String:
+ case DbType.Xml:
+ string openTag = "";
+ string closeTag = "";
+ int tagLength = openTag.Length + closeTag.Length;
+
+ if (tagLength > maxLength)
+ {
+ // Case (1): tagLength > maxTargetLength
+ return "";
+ }
+ else
+ {
+ StringBuilder builder = new StringBuilder(maxLength);
+
+ builder.Append(openTag);
+
+ // The data is just a repeat of one character because to the managed provider
+ // it is only really the length that matters, not the content of the data
+ char characterToUse = (char)rnd.Next((int)'@', (int)'~'); // Choosing random characters in this range to avoid special
+ // xml chars like '<' or '&'
+ int numRepeats = rnd.Next(0, maxLength - tagLength); // Case (2): tagLength == maxTargetLength
+ // Case (3): tagLength < maxTargetLength <-- most common
+ builder.Append(characterToUse, numRepeats);
+
+ builder.Append(closeTag);
+
+ DataStressErrors.Assert(builder.Length <= maxLength, "Incorrect length of randomly generated string");
+
+ return builder.ToString();
+ }
+ default:
+ throw DataStressErrors.UnhandledCaseError(dbType);
+ }
+ }
+
+ #region Table information to be used by stress
+
+ // method used to create stress tables in the database
+ protected void BuildUserTables(List TableMetadataList)
+ {
+ string CreateTable1 =
+ "CREATE TABLE stress_test_table_1 (PrimaryKey int identity(1,1) primary key, int_FLD int, smallint_FLD smallint, real_FLD real, float_FLD float, decimal_FLD decimal(28,4), " +
+ "smallmoney_FLD smallmoney, bit_FLD bit, tinyint_FLD tinyint, uniqueidentifier_FLD uniqueidentifier, varbinary_FLD varbinary(756), binary_FLD binary(756), " +
+ "image_FLD image, varbinarymax_FLD varbinary(max), timestamp_FLD timestamp, char_FLD char(756), text_FLD text, varcharmax_FLD varchar(max), " +
+ "varchar_FLD varchar(756), nchar_FLD nchar(756), ntext_FLD ntext, nvarcharmax_FLD nvarchar(max), nvarchar_FLD nvarchar(756), datetime_FLD datetime, " +
+ "smalldatetime_FLD smalldatetime);" +
+ "CREATE UNIQUE INDEX stress_test_table_1 on stress_test_table_1 ( PrimaryKey );" +
+ "insert into stress_test_table_1(int_FLD, smallint_FLD, real_FLD, float_FLD, decimal_FLD, " +
+ "smallmoney_FLD, bit_FLD, tinyint_FLD, uniqueidentifier_FLD, varbinary_FLD, binary_FLD, " +
+ "image_FLD, varbinarymax_FLD, char_FLD, text_FLD, varcharmax_FLD, " +
+ "varchar_FLD, nchar_FLD, ntext_FLD, nvarcharmax_FLD, nvarchar_FLD, datetime_FLD, " +
+ "smalldatetime_FLD) values ( 0, 0, 0, 0, 0, $0, 0, 0, '00000000-0000-0000-0000-000000000000', " +
+ "0x00, 0x00, 0x00, 0x00, '0', '0', '0', '0', N'0', N'0', N'0', N'0', '01/11/2000 12:54:01', '01/11/2000 12:54:00' );"
+ ;
+
+ string CreateTable2 =
+ "CREATE TABLE stress_test_table_2 (PrimaryKey int identity(1,1) primary key, bigint_FLD bigint, money_FLD money, numeric_FLD numeric, " +
+ "time_FLD time, date_FLD date, datetimeoffset_FLD datetimeoffset, sql_variant_FLD sql_variant, " +
+ "datetime2_FLD datetime2, xml_FLD xml);" +
+ "CREATE UNIQUE INDEX stress_test_table_2 on stress_test_table_2 ( PrimaryKey );" +
+ "insert into stress_test_table_2(bigint_FLD, money_FLD, numeric_FLD, " +
+ "time_FLD, date_FLD, datetimeoffset_FLD, sql_variant_FLD, " +
+ "datetime2_FLD, xml_FLD) values ( 0, $0, 0, '01/11/2015 12:54:01', '01/11/2015 12:54:01', '01/11/2000 12:54:01 -08:00', 0, '01/11/2000 12:54:01', '0' );"
+ ;
+
+ if (TableMetadataList == null)
+ {
+ TableMetadataList = new List();
+ }
+
+ List tableColumns1 = new List();
+ tableColumns1.Add(new TableColumn("PrimaryKey", -1));
+ tableColumns1.Add(new TableColumn("int_FLD", -1));
+ tableColumns1.Add(new TableColumn("smallint_FLD", -1));
+ tableColumns1.Add(new TableColumn("real_FLD", -1));
+ tableColumns1.Add(new TableColumn("float_FLD", -1));
+ tableColumns1.Add(new TableColumn("decimal_FLD", -1));
+ tableColumns1.Add(new TableColumn("smallmoney_FLD", -1));
+ tableColumns1.Add(new TableColumn("bit_FLD", -1));
+ tableColumns1.Add(new TableColumn("tinyint_FLD", -1));
+ tableColumns1.Add(new TableColumn("uniqueidentifier_FLD", -1));
+ tableColumns1.Add(new TableColumn("varbinary_FLD", 756));
+ tableColumns1.Add(new TableColumn("binary_FLD", 756));
+ tableColumns1.Add(new TableColumn("image_FLD", -1));
+ tableColumns1.Add(new TableColumn("varbinarymax_FLD", -1));
+ tableColumns1.Add(new TableColumn("timestamp_FLD", -1));
+ tableColumns1.Add(new TableColumn("char_FLD", -1));
+ tableColumns1.Add(new TableColumn("text_FLD", -1));
+ tableColumns1.Add(new TableColumn("varcharmax_FLD", -1));
+ tableColumns1.Add(new TableColumn("varchar_FLD", 756));
+ tableColumns1.Add(new TableColumn("nchar_FLD", 756));
+ tableColumns1.Add(new TableColumn("ntext_FLD", -1));
+ tableColumns1.Add(new TableColumn("nvarcharmax_FLD", -1));
+ tableColumns1.Add(new TableColumn("nvarchar_FLD", 756));
+ tableColumns1.Add(new TableColumn("datetime_FLD", -1));
+ tableColumns1.Add(new TableColumn("smalldatetime_FLD", -1));
+ TableMetadata tableMeta1 = new TableMetadata("stress_test_table_1", tableColumns1);
+ TableMetadataList.Add(tableMeta1);
+
+ List tableColumns2 = new List();
+ tableColumns2.Add(new TableColumn("PrimaryKey", -1));
+ tableColumns2.Add(new TableColumn("bigint_FLD", -1));
+ tableColumns2.Add(new TableColumn("money_FLD", -1));
+ tableColumns2.Add(new TableColumn("numeric_FLD", -1));
+ tableColumns2.Add(new TableColumn("time_FLD", -1));
+ tableColumns2.Add(new TableColumn("date_FLD", -1));
+ tableColumns2.Add(new TableColumn("datetimeoffset_FLD", -1));
+ tableColumns2.Add(new TableColumn("sql_variant_FLD", -1));
+ tableColumns2.Add(new TableColumn("datetime2_FLD", -1));
+ tableColumns2.Add(new TableColumn("xml_FLD", -1));
+ TableMetadata tableMeta2 = new TableMetadata("stress_test_table_2", tableColumns2);
+ TableMetadataList.Add(tableMeta2);
+
+ using (DataStressConnection conn = CreateConnection(null))
+ {
+ conn.Open();
+ using (DbCommand com = conn.CreateCommand())
+ {
+ try
+ {
+ com.CommandText = CreateTable1;
+ com.ExecuteNonQuery();
+ }
+ catch (DbException de)
+ {
+ // This can be improved by doing a Drop Table if exists.
+ if (de.Message.Contains("There is already an object named \'" + tableMeta1.TableName + "\' in the database."))
+ {
+ CleanupUserTables(tableMeta1);
+ com.ExecuteNonQuery();
+ }
+ else
+ {
+ throw;
+ }
+ }
+
+ try
+ {
+ com.CommandText = CreateTable2;
+ com.ExecuteNonQuery();
+ }
+ catch (DbException de)
+ {
+ // This can be improved by doing a Drop Table if exists in the query itself.
+ if (de.Message.Contains("There is already an object named \'" + tableMeta2.TableName + "\' in the database."))
+ {
+ CleanupUserTables(tableMeta2);
+ com.ExecuteNonQuery();
+ }
+ else
+ {
+ throw;
+ }
+ }
+
+ for (int i = 0; i < Depth; i++)
+ {
+ TrackedRandom randomInstance = new TrackedRandom();
+ randomInstance.Mark();
+
+ DbCommand comInsert1 = GetInsertCommand(randomInstance, tableMeta1, conn);
+ comInsert1.ExecuteNonQuery();
+
+ DbCommand comInsert2 = GetInsertCommand(randomInstance, tableMeta2, conn);
+ comInsert2.ExecuteNonQuery();
+ }
+ }
+ }
+ }
+
+ // method used to delete stress tables in the database
+ protected void CleanupUserTables(TableMetadata tableMetadata)
+ {
+ string DropTable = "drop TABLE " + tableMetadata.TableName + ";";
+
+ using (DataStressConnection conn = CreateConnection(null))
+ {
+ conn.Open();
+ using (DbCommand com = conn.CreateCommand())
+ {
+ try
+ {
+ com.CommandText = DropTable;
+ com.ExecuteNonQuery();
+ }
+ catch (Exception) { }
+ }
+ }
+ }
+
+ public List TableMetadataList
+ {
+ get;
+ private set;
+ }
+
+ public class TableMetadata
+ {
+ private string _tableName;
+ private List _columns = new List();
+
+ public TableMetadata(string tbleName, List cols)
+ {
+ _tableName = tbleName;
+ _columns = cols;
+ }
+
+ public string TableName
+ {
+ get { return _tableName; }
+ }
+
+ public List Columns
+ {
+ get { return _columns; }
+ }
+
+ public TableColumn GetColumn(string colName)
+ {
+ foreach (TableColumn column in _columns)
+ {
+ if (column.ColumnName.Equals(colName))
+ {
+ return column;
+ }
+ }
+ return null;
+ }
+ }
+
+ public class TableColumn
+ {
+ private string _columnName;
+ private int _maxLength;
+
+ public TableColumn(string colName, int maxLen)
+ {
+ _columnName = colName;
+ _maxLength = maxLen;
+ }
+
+ public string ColumnName
+ {
+ get { return _columnName; }
+ }
+
+ public int MaxLength
+ {
+ get { return _maxLength; }
+ }
+ }
+
+ private List _applicationNames;
+
+ ///
+ /// Gets schema of all tables from the back-end database and fills
+ /// the m_Tables DataSet with this schema. This DataSet is used to
+ /// generate random command text for tests.
+ ///
+ public void InitializeSharedData(DataSource source)
+ {
+ Trace.WriteLine("Creating shared objects", this.ToString());
+
+ // Initialize m_sharedDataSet
+ TableMetadataList = new List();
+ BuildUserTables(TableMetadataList);
+
+ // Initialize m_applicationNames
+ _applicationNames = new List();
+ for (int i = 0; i < GetNumDifferentApplicationNames(); i++)
+ {
+ _applicationNames.Add(GetRandomApplicationName());
+ }
+
+ // Initialize CurrentPoolingStressMode
+ CurrentPoolingStressMode = PoolingStressMode.RandomizeConnectionStrings;
+
+
+ Trace.WriteLine("Finished creating shared objects", this.ToString());
+ }
+
+ public void CleanupSharedData()
+ {
+ foreach (TableMetadata meta in TableMetadataList)
+ {
+ CleanupUserTables(meta);
+ }
+ TableMetadataList = null;
+ }
+
+ public abstract void CreateDatabase(DataSource source);
+ public abstract void DropDatabase(DataSource source);
+
+ #endregion
+ }
+}
diff --git a/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressReader.cs b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressReader.cs
new file mode 100644
index 0000000000..cad1bfa579
--- /dev/null
+++ b/src/Microsoft.Data.SqlClient/tests/StressTests/SqlClient.Stress.Framework/DataStressReader.cs
@@ -0,0 +1,350 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+using System.Xml;
+using System.Data;
+using System.Data.Common;
+using Microsoft.Data.SqlClient;
+using System.Data.SqlTypes;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Stress.Data
+{
+ public class DataStressReader : IDisposable
+ {
+ #region Type method mapping
+
+ private static Dictionary>> s_sqlTypes;
+ private static Dictionary>> s_clrTypes;
+
+ static DataStressReader()
+ {
+ InitSqlTypes();
+ InitClrTypes();
+ }
+
+ private static void InitSqlTypes()
+ {
+ s_sqlTypes = new Dictionary>>();
+
+ s_sqlTypes.Add(typeof(SqlBinary), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlBoolean), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlByte), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlBytes), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlChars), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlDateTime), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlDecimal), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlDouble), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlGuid), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlInt16), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlInt32), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlInt64), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlMoney), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlSingle), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlString), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_sqlTypes.Add(typeof(SqlXml), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ }
+
+ private static void InitClrTypes()
+ {
+ s_clrTypes = new Dictionary>>();
+
+ s_clrTypes.Add(typeof(bool), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(byte), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(short), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(int), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(long), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(float), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(double), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(string), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(char), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(decimal), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(Guid), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(DateTime), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(TimeSpan), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ s_clrTypes.Add(typeof(DateTimeOffset), (reader, ordinal, token, rnd) => reader.GetFieldValueSyncOrAsync(ordinal, token, rnd));
+ }
+
+ #endregion
+
+ private readonly DbDataReader _reader;
+ private SemaphoreSlim _closeAsyncSemaphore;
+
+ public DataStressReader(DbDataReader internalReader)
+ {
+ _reader = internalReader;
+ }
+
+ public void Close()
+ {
+ _reader.Dispose();
+ }
+
+ public void Dispose()
+ {
+ _reader.Dispose();
+ if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Dispose();
+ }
+
+ public Task CloseAsync()
+ {
+ _closeAsyncSemaphore = new SemaphoreSlim(1);
+ return Task.Run(() => ExecuteWithCloseAsyncSemaphore(Close));
+ }
+
+ ///
+ /// Executes the action while holding the CloseAsync Semaphore.
+ /// This MUST be used for reader.Close() and all methods that are not safe to call at the same time as reader.Close(), i.e. all sync methods.
+ /// Otherwise we will see AV's.
+ ///
+ public void ExecuteWithCloseAsyncSemaphore(Action a)
+ {
+ try
+ {
+ if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Wait();
+ a();
+ }
+ finally
+ {
+ if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Release();
+ }
+ }
+
+ ///
+ /// Executes the action while holding the CloseAsync Semaphore.
+ /// This MUST be used for reader.Close() and all methods that are not safe to call at the same time as reader.Close(), i.e. all sync methods.
+ /// Otherwise we will see AV's.
+ ///
+ public T ExecuteWithCloseAsyncSemaphore(Func f)
+ {
+ try
+ {
+ if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Wait();
+ return f();
+ }
+ finally
+ {
+ if (_closeAsyncSemaphore != null) _closeAsyncSemaphore.Release();
+ }
+ }
+
+ #region SyncOrAsync methods
+
+ public Task ReadSyncOrAsync(CancellationToken token, Random rnd)
+ {
+ return AsyncUtils.SyncOrAsyncMethod(
+ () => ExecuteWithCloseAsyncSemaphore(() => _reader.Read()),
+ () => ExecuteWithCloseAsyncSemaphore(() => _reader.ReadAsync(token)),
+ AsyncUtils.ChooseSyncAsyncMode(rnd)
+ );
+ }
+
+ public Task NextResultSyncOrAsync(CancellationToken token, Random rnd)
+ {
+ return AsyncUtils.SyncOrAsyncMethod(
+ () => ExecuteWithCloseAsyncSemaphore(() => _reader.NextResult()),
+ () => ExecuteWithCloseAsyncSemaphore(() => _reader.NextResultAsync(token)),
+ AsyncUtils.ChooseSyncAsyncMode(rnd)
+ );
+ }
+
+ public Task IsDBNullSyncOrAsync(int ordinal, CancellationToken token, Random rnd)
+ {
+ return AsyncUtils.SyncOrAsyncMethod(
+ () => ExecuteWithCloseAsyncSemaphore(() => _reader.IsDBNull(ordinal)),
+ () => ExecuteWithCloseAsyncSemaphore(() => _reader.IsDBNullAsync(ordinal, token)),
+ AsyncUtils.ChooseSyncAsyncMode(rnd)
+ );
+ }
+
+ public Task