From 3904a647f583074089c4554d508b4549de4b8249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Jare=C5=A1?= Date: Tue, 23 Nov 2021 19:13:18 +0100 Subject: [PATCH] Add task aware string writer with asynclocal (#940) * Add thread safe string writer with asynclocal * Use task aware string writer to collect output Co-authored-by: Medeni Baykal <433724+Haplois@users.noreply.github.com> --- TestFx.sln | 31 +- scripts/test.ps1 | 2 +- .../Desktop.Legacy/MSTest.TestAdapter.props | 33 + .../Desktop.Legacy/MSTest.TestAdapter.targets | 35 + .../Build/Desktop/MSTest.TestAdapter.props | 2 +- .../Discovery/AssemblyEnumerator.cs | 3 +- .../Execution/LogMessageListener.cs | 46 +- .../Execution/TestMethodInfo.cs | 6 +- .../Execution/TestMethodRunner.cs | 6 +- .../Execution/UnitTestRunner.cs | 9 +- .../MSTest.CoreAdapter.csproj | 1 - .../AssemblyResolver.cs | 676 +++++++++++++ .../Constants.cs | 44 + .../Data/CsvDataConnection.cs | 170 ++++ .../Data/OdbcDataConnection.cs | 121 +++ .../Data/OleDataConnection.cs | 109 ++ .../Data/SqlDataConnection.cs | 73 ++ .../Data/TestDataConnection.cs | 168 +++ .../Data/TestDataConnectionFactory.cs | 85 ++ .../Data/TestDataConnectionSql.cs | 957 ++++++++++++++++++ .../Data/XmlDataConnection.cs | 129 +++ .../Deployment/AssemblyLoadWorker.cs | 266 +++++ .../Deployment/DesktopTestRunDirectories.cs | 78 ++ .../DesktopTestSource.cs | 68 ++ .../Friends.cs | 9 + .../PlatformServices.Desktop.Legacy.csproj | 165 +++ .../Properties/AssemblyInfo.cs | 44 + .../Resources/README.txt | 1 + .../Services/DesktopAdapterTraceLogger.cs | 62 ++ .../Services/DesktopFileOperations.cs | 134 +++ .../Services/DesktopReflectionOperations.cs | 68 ++ .../DesktopTestContextImplementation.cs | 508 ++++++++++ .../Services/DesktopTestDataSource.cs | 147 +++ .../Services/DesktopTestSource.cs | 70 ++ .../Services/DesktopTestSourceHost.cs | 370 +++++++ .../Services/DesktopThreadOperations.cs | 118 +++ .../Utilities/AppDomainUtilities.cs | 252 +++++ .../Utilities/AppDomainWrapper.cs | 24 + .../Utilities/DesktopAssemblyUtility.cs | 282 ++++++ .../Utilities/DesktopDeploymentUtility.cs | 250 +++++ .../Utilities/DesktopReflectionUtility.cs | 314 ++++++ .../Utilities/IAppDomain.cs | 29 + .../Utilities/IAssemblyUtility.cs | 24 + .../Utilities/RandomIntPermutation.cs | 56 + .../Utilities/SequentialIntPermutation.cs | 41 + .../Utilities/VSInstallationUtilities.cs | 263 +++++ .../Utilities/XmlUtilities.cs | 158 +++ .../app.config | 11 + .../PlatformServices.Desktop.csproj | 14 +- .../DesktopTestContextImplementation.cs | 2 +- .../PlatformServices.Desktop/app.config | 8 +- .../PlatformServices.NetCore.csproj | 1 + .../NetCoreTestContextImplementation.cs | 2 +- .../PlatformServices.Portable.csproj | 3 + .../Services/ns10TestContextImplementation.cs | 2 +- .../Services/ns10ThreadSafeStringWriter.cs} | 17 +- .../Services/ns13ThreadSafeStringWriter.cs | 193 ++++ src/Package/MSTest.TestAdapter.nuspec | 14 +- src/Package/MSTest.TestAdapter.symbols.nuspec | 16 +- .../App.config | 14 +- ...ormServices.Desktop.Component.Tests.csproj | 9 +- .../DiscoveryAndExecutionTests.csproj | 2 +- .../Execution/TestMethodInfoTests.cs | 2 +- .../Execution/ThreadSafeStringWriterTests.cs | 67 +- .../Execution/UnitTestRunnerTests.cs | 37 +- .../App.config | 12 +- ...PlatformServices.Desktop.Unit.Tests.csproj | 18 +- ...PlatformServices.NetCore.Unit.Tests.csproj | 2 + .../ns13ThreadSafeStringWriterTests.cs | 144 +++ 69 files changed, 6988 insertions(+), 109 deletions(-) create mode 100644 src/Adapter/Build/Desktop.Legacy/MSTest.TestAdapter.props create mode 100644 src/Adapter/Build/Desktop.Legacy/MSTest.TestAdapter.targets create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/AssemblyResolver.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Constants.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Data/CsvDataConnection.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Data/OdbcDataConnection.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Data/OleDataConnection.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Data/SqlDataConnection.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnection.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnectionFactory.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnectionSql.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Data/XmlDataConnection.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Deployment/AssemblyLoadWorker.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Deployment/DesktopTestRunDirectories.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/DesktopTestSource.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Friends.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/PlatformServices.Desktop.Legacy.csproj create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Properties/AssemblyInfo.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Resources/README.txt create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopAdapterTraceLogger.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopFileOperations.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopReflectionOperations.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestContextImplementation.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestDataSource.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestSource.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestSourceHost.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopThreadOperations.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Utilities/AppDomainUtilities.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Utilities/AppDomainWrapper.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopAssemblyUtility.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopDeploymentUtility.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopReflectionUtility.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Utilities/IAppDomain.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Utilities/IAssemblyUtility.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Utilities/RandomIntPermutation.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Utilities/SequentialIntPermutation.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Utilities/VSInstallationUtilities.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/Utilities/XmlUtilities.cs create mode 100644 src/Adapter/PlatformServices.Desktop.Legacy/app.config rename src/Adapter/{MSTest.CoreAdapter/Execution/ThreadSafeStringWriter.cs => PlatformServices.Shared/netstandard1.0/Services/ns10ThreadSafeStringWriter.cs} (83%) create mode 100644 src/Adapter/PlatformServices.Shared/netstandard1.3/Services/ns13ThreadSafeStringWriter.cs create mode 100644 test/UnitTests/PlatformServices.Shared.Unit.Tests/netstandard1.3/ns13ThreadSafeStringWriterTests.cs diff --git a/TestFx.sln b/TestFx.sln index f4925ddaba..7c9375407c 100644 --- a/TestFx.sln +++ b/TestFx.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29728.190 +# Visual Studio Version 17 +VisualStudioVersion = 17.1.31910.343 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FF8B1B72-55A1-4FFE-809E-7B79323ED8D0}" EndProject @@ -220,6 +220,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferencedProjectFromDataSo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscoverInternalsProject", "test\E2ETests\TestAssets\DiscoverInternalsProject\DiscoverInternalsProject.csproj", "{44A504D9-A0D6-427D-BFB2-DB144A74F0D5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlatformServices.Desktop.Legacy", "src\Adapter\PlatformServices.Desktop.Legacy\PlatformServices.Desktop.Legacy.csproj", "{F64A748C-DDBA-4B57-99F4-D9E55684A7A4}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Adapter\PlatformServices.Shared\PlatformServices.Shared.projitems*{2177c273-ae07-43b3-b87a-443e47a23c5a}*SharedItemsImports = 13 @@ -1322,6 +1324,30 @@ Global {44A504D9-A0D6-427D-BFB2-DB144A74F0D5}.Release|x64.Build.0 = Release|Any CPU {44A504D9-A0D6-427D-BFB2-DB144A74F0D5}.Release|x86.ActiveCfg = Release|Any CPU {44A504D9-A0D6-427D-BFB2-DB144A74F0D5}.Release|x86.Build.0 = Release|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Code Analysis Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Code Analysis Debug|Any CPU.Build.0 = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Code Analysis Debug|ARM.ActiveCfg = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Code Analysis Debug|ARM.Build.0 = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Code Analysis Debug|x64.ActiveCfg = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Code Analysis Debug|x64.Build.0 = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Code Analysis Debug|x86.ActiveCfg = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Code Analysis Debug|x86.Build.0 = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Debug|ARM.ActiveCfg = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Debug|ARM.Build.0 = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Debug|x64.ActiveCfg = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Debug|x64.Build.0 = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Debug|x86.ActiveCfg = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Debug|x86.Build.0 = Debug|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Release|Any CPU.Build.0 = Release|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Release|ARM.ActiveCfg = Release|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Release|ARM.Build.0 = Release|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Release|x64.ActiveCfg = Release|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Release|x64.Build.0 = Release|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Release|x86.ActiveCfg = Release|Any CPU + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1393,6 +1419,7 @@ Global {35D010CC-CDF2-4115-BCFB-E2E3D21C1055} = {CA01DAF5-8D9D-496E-9AD3-94BB7FBB2D34} {6B4DE65C-4162-4C52-836A-8F9FA901814A} = {D53BD452-F69F-4FB3-8B98-386EDA28A4C8} {44A504D9-A0D6-427D-BFB2-DB144A74F0D5} = {D53BD452-F69F-4FB3-8B98-386EDA28A4C8} + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4} = {24088844-2107-4DB2-8F3F-CBCA94FC4B28} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {31E0F4D5-975A-41CC-933E-545B2201FAF9} diff --git a/scripts/test.ps1 b/scripts/test.ps1 index 4c114f863a..4ce92d1f74 100644 --- a/scripts/test.ps1 +++ b/scripts/test.ps1 @@ -144,7 +144,7 @@ function Run-Test([string[]] $testContainers, [string[]] $netCoreTestContainers) { $vstestPath = Get-VSTestPath - $additionalArguments = '' + $additionalArguments = @('/Blame:CollectHangDump;TestTimeout=5min') if($TFT_Parallel) { $additionalArguments += "/parallel" diff --git a/src/Adapter/Build/Desktop.Legacy/MSTest.TestAdapter.props b/src/Adapter/Build/Desktop.Legacy/MSTest.TestAdapter.props new file mode 100644 index 0000000000..c923a1eb10 --- /dev/null +++ b/src/Adapter/Build/Desktop.Legacy/MSTest.TestAdapter.props @@ -0,0 +1,33 @@ + + + + + Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.dll + PreserveNewest + False + + + Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.dll + PreserveNewest + False + + + Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.dll + PreserveNewest + False + + + Microsoft.TestPlatform.AdapterUtilities.dll + PreserveNewest + False + + + + + + + + diff --git a/src/Adapter/Build/Desktop.Legacy/MSTest.TestAdapter.targets b/src/Adapter/Build/Desktop.Legacy/MSTest.TestAdapter.targets new file mode 100644 index 0000000000..1c80426d01 --- /dev/null +++ b/src/Adapter/Build/Desktop.Legacy/MSTest.TestAdapter.targets @@ -0,0 +1,35 @@ + + + + true + + + + + + + + + + + + + + + + + + %(CurrentUICultureHierarchy.Identity) + + + + %(MSTestV2ResourceFiles.CultureString)\%(Filename)%(Extension) + PreserveNewest + False + + + + + \ No newline at end of file diff --git a/src/Adapter/Build/Desktop/MSTest.TestAdapter.props b/src/Adapter/Build/Desktop/MSTest.TestAdapter.props index c923a1eb10..aa6baccb4a 100644 --- a/src/Adapter/Build/Desktop/MSTest.TestAdapter.props +++ b/src/Adapter/Build/Desktop/MSTest.TestAdapter.props @@ -11,7 +11,7 @@ PreserveNewest False - + Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.dll PreserveNewest False diff --git a/src/Adapter/MSTest.CoreAdapter/Discovery/AssemblyEnumerator.cs b/src/Adapter/MSTest.CoreAdapter/Discovery/AssemblyEnumerator.cs index bf45deed6e..3b50233d4c 100644 --- a/src/Adapter/MSTest.CoreAdapter/Discovery/AssemblyEnumerator.cs +++ b/src/Adapter/MSTest.CoreAdapter/Discovery/AssemblyEnumerator.cs @@ -16,6 +16,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Discovery using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using UTF = Microsoft.VisualStudio.TestTools.UnitTesting; @@ -265,7 +266,7 @@ private bool DynamicDataAttached(IDictionary sourceLevelParamete return false; } - using (var writer = new ThreadSafeStringWriter(CultureInfo.InvariantCulture)) + using (var writer = new ThreadSafeStringWriter(CultureInfo.InvariantCulture, "all")) { var testMethod = test.TestMethod; var testContext = PlatformServiceProvider.Instance.GetTestContext(testMethod, writer, sourceLevelParameters); diff --git a/src/Adapter/MSTest.CoreAdapter/Execution/LogMessageListener.cs b/src/Adapter/MSTest.CoreAdapter/Execution/LogMessageListener.cs index fa1f36ae15..f012d8c802 100644 --- a/src/Adapter/MSTest.CoreAdapter/Execution/LogMessageListener.cs +++ b/src/Adapter/MSTest.CoreAdapter/Execution/LogMessageListener.cs @@ -6,7 +6,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution using System; using System.Globalization; using System.IO; - + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestTools.UnitTesting.Logging; @@ -18,8 +18,8 @@ public class LogMessageListener : IDisposable { private static LogMessageListener activeRedirector; private readonly LogMessageListener previousRedirector; - private readonly TextWriter redirectLoggerOut; - private readonly TextWriter redirectStdErr; + private readonly ThreadSafeStringWriter redirectLoggerOut; + private readonly ThreadSafeStringWriter redirectStdErr; private readonly bool captureDebugTraces; /// @@ -41,8 +41,8 @@ public LogMessageListener(bool captureDebugTraces) this.captureDebugTraces = captureDebugTraces; // Cache the original output/error streams and replace it with the own stream. - this.redirectLoggerOut = new ThreadSafeStringWriter(CultureInfo.InvariantCulture); - this.redirectStdErr = new ThreadSafeStringWriter(CultureInfo.InvariantCulture); + this.redirectLoggerOut = new ThreadSafeStringWriter(CultureInfo.InvariantCulture, "out"); + this.redirectStdErr = new ThreadSafeStringWriter(CultureInfo.InvariantCulture, "err"); Logger.OnLogMessage += this.redirectLoggerOut.WriteLine; @@ -51,7 +51,7 @@ public LogMessageListener(bool captureDebugTraces) if (this.captureDebugTraces) { - this.traceListener = PlatformServiceProvider.Instance.GetTraceListener(new ThreadSafeStringWriter(CultureInfo.InvariantCulture)); + this.traceListener = PlatformServiceProvider.Instance.GetTraceListener(new ThreadSafeStringWriter(CultureInfo.InvariantCulture, "trace")); this.traceListenerManager = PlatformServiceProvider.Instance.GetTraceListenerManager(this.redirectLoggerOut, this.redirectStdErr); // If there was a previous LogMessageListener active, remove its @@ -94,6 +94,40 @@ public string DebugTrace } } + public string GetAndClearStandardOutput() + { + var output = this.redirectLoggerOut.ToString(); + this.redirectLoggerOut.Clear(); + return output; + } + + public string GetAndClearStandardError() + { + var output = this.redirectStdErr.ToString(); + this.redirectStdErr.Clear(); + return output; + } + + public string GetAndClearDebugTrace() + { + var writer = this.traceListener?.GetWriter(); + if (writer == null) + { + return null; + } + + if (writer is StringWriter sw) + { + var sb = sw.GetStringBuilder(); + var output = sb?.ToString(); + sb?.Clear(); + return output; + } + + // we cannot clear it because it is just a text writer + return writer.ToString(); + } + public void Dispose() { this.Dispose(true); diff --git a/src/Adapter/MSTest.CoreAdapter/Execution/TestMethodInfo.cs b/src/Adapter/MSTest.CoreAdapter/Execution/TestMethodInfo.cs index 1d4d0d13bb..c202475da2 100644 --- a/src/Adapter/MSTest.CoreAdapter/Execution/TestMethodInfo.cs +++ b/src/Adapter/MSTest.CoreAdapter/Execution/TestMethodInfo.cs @@ -137,9 +137,9 @@ public virtual TestResult Invoke(object[] arguments) if (result != null) { result.Duration = watch.Elapsed; - result.DebugTrace = listener.DebugTrace; - result.LogOutput = listener.StandardOutput; - result.LogError = listener.StandardError; + result.DebugTrace = listener.GetAndClearDebugTrace(); + result.LogOutput = listener.GetAndClearStandardOutput(); + result.LogError = listener.GetAndClearStandardError(); result.TestContextMessages = this.TestMethodOptions.TestContext.GetAndClearDiagnosticMessages(); result.ResultFiles = this.TestMethodOptions.TestContext.GetResultFiles(); } diff --git a/src/Adapter/MSTest.CoreAdapter/Execution/TestMethodRunner.cs b/src/Adapter/MSTest.CoreAdapter/Execution/TestMethodRunner.cs index 34bfe46bac..7a9a26f898 100644 --- a/src/Adapter/MSTest.CoreAdapter/Execution/TestMethodRunner.cs +++ b/src/Adapter/MSTest.CoreAdapter/Execution/TestMethodRunner.cs @@ -126,9 +126,9 @@ internal UnitTestResult[] Execute() } finally { - initLogs = logListener.StandardOutput; - initTrace = logListener.DebugTrace; - initErrorLogs = logListener.StandardError; + initLogs = logListener.GetAndClearStandardOutput(); + initTrace = logListener.GetAndClearDebugTrace(); + initErrorLogs = logListener.GetAndClearStandardError(); inittestContextMessages = this.testContext.GetAndClearDiagnosticMessages(); } } diff --git a/src/Adapter/MSTest.CoreAdapter/Execution/UnitTestRunner.cs b/src/Adapter/MSTest.CoreAdapter/Execution/UnitTestRunner.cs index a6c0cfff4c..51d6bf570e 100644 --- a/src/Adapter/MSTest.CoreAdapter/Execution/UnitTestRunner.cs +++ b/src/Adapter/MSTest.CoreAdapter/Execution/UnitTestRunner.cs @@ -14,6 +14,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Microsoft.VisualStudio.TestTools.UnitTesting; using TPOM = Microsoft.VisualStudio.TestPlatform.ObjectModel; using UTF = Microsoft.VisualStudio.TestTools.UnitTesting; @@ -112,7 +113,7 @@ internal UnitTestResult[] RunSingleTest(TestMethod testMethod, IDictionary(testContextProperties); var testContext = PlatformServiceProvider.Instance.GetTestContext(testMethod, writer, properties); @@ -181,9 +182,9 @@ internal RunCleanupResult RunCleanup() { // Replacing the null character with a string.replace should work. // If this does not work for a specific dotnet version a custom function doing the same needs to be put in place. - result.StandardOut = redirector.StandardOutput?.Replace("\0", "\\0"); - result.StandardError = redirector.StandardError?.Replace("\0", "\\0"); - result.DebugTrace = redirector.DebugTrace?.Replace("\0", "\\0"); + result.StandardOut = redirector.GetAndClearStandardOutput()?.Replace("\0", "\\0"); + result.StandardError = redirector.GetAndClearStandardError()?.Replace("\0", "\\0"); + result.DebugTrace = redirector.GetAndClearDebugTrace()?.Replace("\0", "\\0"); } } diff --git a/src/Adapter/MSTest.CoreAdapter/MSTest.CoreAdapter.csproj b/src/Adapter/MSTest.CoreAdapter/MSTest.CoreAdapter.csproj index 23dcee5f16..bf810b0bc5 100644 --- a/src/Adapter/MSTest.CoreAdapter/MSTest.CoreAdapter.csproj +++ b/src/Adapter/MSTest.CoreAdapter/MSTest.CoreAdapter.csproj @@ -106,7 +106,6 @@ - diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/AssemblyResolver.cs b/src/Adapter/PlatformServices.Desktop.Legacy/AssemblyResolver.cs new file mode 100644 index 0000000000..e424b582eb --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/AssemblyResolver.cs @@ -0,0 +1,676 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Runtime.InteropServices.WindowsRuntime; + using System.Security; + using System.Security.Permissions; + + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + + /// + /// Helps resolve MSTestFramework assemblies for CLR loader. + /// The idea is that Unit Test Adapter creates App Domain for running tests and sets AppBase to tests dir. + /// Since we don't want to put our assemblies to GAC and they are not in tests dir, we use custom way to resolve them. + /// + public class AssemblyResolver : MarshalByRefObject, IDisposable + { + /// + /// The assembly name of the dll containing logger APIs(EqtTrace) from the TestPlatform. + /// + /// + /// The reason we have this is because the AssemblyResolver itself logs information during resolution. + /// If the resolver is called for the assembly containing the logger APIs, we do not log so as to prevent a stack overflow. + /// + private const string LoggerAssemblyNameLegacy = "Microsoft.VisualStudio.TestPlatform.ObjectModel"; + + /// + /// The assembly name of the dll containing logger APIs(EqtTrace) from the TestPlatform. + /// + /// + /// The reason we have this is because the AssemblyResolver itself logs information during resolution. + /// If the resolver is called for the assembly containing the logger APIs, we do not log so as to prevent a stack overflow. + /// + private const string LoggerAssemblyName = "Microsoft.TestPlatform.CoreUtilities"; + + /// + /// This will have the list of all directories read from runsettings. + /// + private Queue directoryList; + + /// + /// The directories to look for assemblies to resolve. + /// + private List searchDirectories; + + /// + /// Dictionary of Assemblies discovered to date. + /// + private Dictionary resolvedAssemblies = new Dictionary(); + + /// + /// Dictionary of Reflection-Only Assemblies discovered to date. + /// + private Dictionary reflectionOnlyResolvedAssemblies = new Dictionary(); + + /// + /// lock for the loaded assemblies cache. + /// + private object syncLock = new object(); + + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// + /// A list of directories for resolution path + /// + /// + /// If there are additional paths where a recursive search is required + /// call AddSearchDirectoryFromRunSetting method with that list. + /// + public AssemblyResolver(IList directories) + { + if (directories == null || directories.Count == 0) + { + throw new ArgumentNullException(nameof(directories)); + } + + this.searchDirectories = new List(directories); + this.directoryList = new Queue(); + + AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(this.OnResolve); + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += new ResolveEventHandler(this.ReflectionOnlyOnResolve); + + // This is required for winmd resolution for arm built sources discovery on desktop. + WindowsRuntimeMetadata.ReflectionOnlyNamespaceResolve += new EventHandler(this.WindowsRuntimeMetadataReflectionOnlyNamespaceResolve); + } + + /// + /// Finalizes an instance of the class. + /// + ~AssemblyResolver() + { + this.Dispose(false); + } + + /// + /// The dispose. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Returns object to be used for controlling lifetime, null means infinite lifetime. + /// + /// + /// Note that LinkDemand is needed by FxCop. + /// + /// + /// The . + /// + [SecurityCritical] + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)] + public override object InitializeLifetimeService() + { + return null; + } + + /// + /// It will add a list of search directories path with property recursive/non-recursive in assembly resolver . + /// + /// + /// The recursive Directory Path. + /// + public void AddSearchDirectoriesFromRunSetting(List recursiveDirectoryPath) + { + // Enqueue elements from the list in Queue + if (recursiveDirectoryPath == null) + { + return; + } + + foreach (var recPath in recursiveDirectoryPath) + { + this.directoryList.Enqueue(recPath); + } + } + + /// + /// Assembly Resolve event handler for App Domain - called when CLR loader cannot resolve assembly. + /// + /// The sender App Domain. + /// The args. + /// The . + internal Assembly ReflectionOnlyOnResolve(object sender, ResolveEventArgs args) + { + return this.OnResolveInternal(sender, args, true); + } + + /// + /// Assembly Resolve event handler for App Domain - called when CLR loader cannot resolve assembly. + /// + /// The sender App Domain. + /// The args. + /// The . + internal Assembly OnResolve(object sender, ResolveEventArgs args) + { + return this.OnResolveInternal(sender, args, false); + } + + /// + /// Adds the subdirectories of the provided path to the collection. + /// + /// Path go get subdirectories for. + /// The search Directories. + internal void AddSubdirectories(string path, List searchDirectories) + { + Debug.Assert(!string.IsNullOrEmpty(path), "'path' cannot be null or empty."); + Debug.Assert(searchDirectories != null, "'searchDirectories' cannot be null."); + + // If the directory exists, get it's subdirectories + if (this.DoesDirectoryExist(path)) + { + // Get the directories in the path provided. + var directories = this.GetDirectories(path); + + // Add each directory and its subdirectories to the collection. + foreach (var directory in directories) + { + searchDirectories.Add(directory); + + this.AddSubdirectories(directory, searchDirectories); + } + } + } + + /// + /// The dispose. + /// + /// + /// The disposing. + /// + protected virtual void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + // cleanup Managed resources like calling dispose on other managed object created. + AppDomain.CurrentDomain.AssemblyResolve -= new ResolveEventHandler(this.OnResolve); + AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve -= new ResolveEventHandler(this.ReflectionOnlyOnResolve); + + WindowsRuntimeMetadata.ReflectionOnlyNamespaceResolve -= new EventHandler(this.WindowsRuntimeMetadataReflectionOnlyNamespaceResolve); + } + + // cleanup native resources + this.disposed = true; + } + } + + /// + /// Verifies if a directory exists. + /// + /// The path to the directory. + /// True if the directory exists. + /// Only present for unit testing scenarios. + protected virtual bool DoesDirectoryExist(string path) + { + return Directory.Exists(path); + } + + /// + /// Gets the directories from a path. + /// + /// The path to the directory. + /// A list of directories in path. + /// Only present for unit testing scenarios. + protected virtual string[] GetDirectories(string path) + { + return Directory.GetDirectories(path); + } + + protected virtual bool DoesFileExist(string filePath) + { + return File.Exists(filePath); + } + + protected virtual Assembly LoadAssemblyFrom(string path) + { + return Assembly.LoadFrom(path); + } + + protected virtual Assembly ReflectionOnlyLoadAssemblyFrom(string path) + { + return Assembly.ReflectionOnlyLoadFrom(path); + } + + /// + /// It will search for a particular assembly in the given list of directory. + /// + /// The search Directorypaths. + /// The name. + /// Indicates whether this is called under a Reflection Only Load context. + /// The . + protected virtual Assembly SearchAssembly(List searchDirectorypaths, string name, bool isReflectionOnly) + { + if (searchDirectorypaths == null || searchDirectorypaths.Count == 0) + { + return null; + } + + // args.Name is like: "Microsoft.VisualStudio.TestTools.Common, Version=[VersionMajor].0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a". + AssemblyName requestedName = null; + + try + { + // Can throw ArgumentException, FileLoadException if arg is empty/wrong format, etc. Should not return null. + requestedName = new AssemblyName(name); + } + catch (Exception ex) + { + this.SafeLog( + name, + () => + { + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info( + "AssemblyResolver: {0}: Failed to create assemblyName. Reason:{1} ", + name, + ex); + } + }); + + return null; + } + + Debug.Assert(requestedName != null && !string.IsNullOrEmpty(requestedName.Name), "AssemblyResolver.OnResolve: requested is null or name is empty!"); + + foreach (var dir in searchDirectorypaths) + { + if (string.IsNullOrEmpty(dir)) + { + continue; + } + + this.SafeLog( + name, + () => + { + if (EqtTrace.IsVerboseEnabled) + { + EqtTrace.Verbose("AssemblyResolver: Searching assembly: {0} in the directory: {1}", requestedName.Name, dir); + } + }); + + foreach (var extension in new string[] { ".dll", ".exe" }) + { + var assemblyPath = Path.Combine(dir, requestedName.Name + extension); + + var assembly = this.SearchAndLoadAssembly(assemblyPath, name, requestedName, isReflectionOnly); + if (assembly != null) + { + return assembly; + } + } + } + + return null; + } + + /// + /// Verifies that found assembly name matches requested to avoid security issues. + /// Looks only at PublicKeyToken and Version, empty matches anything. + /// + /// The requested Name. + /// The found Name. + /// The . + private static bool RequestedAssemblyNameMatchesFound(AssemblyName requestedName, AssemblyName foundName) + { + Debug.Assert(requestedName != null, "requested assembly name should not be null."); + Debug.Assert(foundName != null, "found assembly name should not be null."); + + var requestedPublicKey = requestedName.GetPublicKeyToken(); + if (requestedPublicKey != null) + { + var foundPublicKey = foundName.GetPublicKeyToken(); + if (foundPublicKey == null) + { + return false; + } + + for (var i = 0; i < requestedPublicKey.Length; ++i) + { + if (requestedPublicKey[i] != foundPublicKey[i]) + { + return false; + } + } + } + + return requestedName.Version == null || requestedName.Version.Equals(foundName.Version); + } + + /// + /// Event handler for windows winmd resolution. + /// + /// The sender App Domain. + /// The args. + private void WindowsRuntimeMetadataReflectionOnlyNamespaceResolve(object sender, NamespaceResolveEventArgs args) + { + // Note: This will throw on pre-Win8 OS versions + IEnumerable fileNames = WindowsRuntimeMetadata.ResolveNamespace( + args.NamespaceName, + null, // Will use OS installed .winmd files, you can pass explicit Windows SDK path here for searching 1st party WinRT types + this.searchDirectories); // You can pass package graph paths, they will be used for searching .winmd files with 3rd party WinRT types + + foreach (string fileName in fileNames) + { + args.ResolvedAssemblies.Add(Assembly.ReflectionOnlyLoadFrom(fileName)); + } + } + + /// + /// Assembly Resolve event handler for App Domain - called when CLR loader cannot resolve assembly. + /// + /// The sender App Domain. + /// The args. + /// Indicates whether this is called under a Reflection Only Load context. + /// The . + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "senderAppDomain", Justification = "This is an event handler.")] + private Assembly OnResolveInternal(object senderAppDomain, ResolveEventArgs args, bool isReflectionOnly) + { + if (string.IsNullOrEmpty(args?.Name)) + { + Debug.Fail("AssemblyResolver.OnResolve: args.Name is null or empty."); + return null; + } + + this.SafeLog( + args.Name, + () => + { + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("AssemblyResolver: Resolving assembly: {0}.", args.Name); + } + }); + + string assemblyNameToLoad = AppDomain.CurrentDomain.ApplyPolicy(args.Name); + + this.SafeLog( + assemblyNameToLoad, + () => + { + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("AssemblyResolver: Resolving assembly after applying policy: {0}.", assemblyNameToLoad); + } + }); + + lock (this.syncLock) + { + // Since both normal and reflection only cache are accessed in same block, putting only one lock should be sufficient. + if (this.TryLoadFromCache(assemblyNameToLoad, isReflectionOnly, out var assembly)) + { + return assembly; + } + + assembly = this.SearchAssembly(this.searchDirectories, assemblyNameToLoad, isReflectionOnly); + + if (assembly != null) + { + return assembly; + } + + if (this.directoryList != null && this.directoryList.Any()) + { + // required assembly is not present in searchDirectories?? + // see, if we can find it in user specified search directories. + while (assembly == null && this.directoryList.Count > 0) + { + // instead of loading whole search directory in one time, we are adding directory on the basis of need + var currentNode = this.directoryList.Dequeue(); + + List increamentalSearchDirectory = new List(); + + if (this.DoesDirectoryExist(currentNode.DirectoryPath)) + { + increamentalSearchDirectory.Add(currentNode.DirectoryPath); + + if (currentNode.IncludeSubDirectories) + { + // Add all its sub-directory in depth first search order. + this.AddSubdirectories(currentNode.DirectoryPath, increamentalSearchDirectory); + } + + // Add this directory list in this.searchDirectories so that when we will try to resolve some other + // assembly, then it will look in this whole directory first. + this.searchDirectories.AddRange(increamentalSearchDirectory); + + assembly = this.SearchAssembly(increamentalSearchDirectory, assemblyNameToLoad, isReflectionOnly); + } + else + { + // generate warning that path does not exist. + this.SafeLog( + assemblyNameToLoad, + () => + { + if (EqtTrace.IsWarningEnabled) + { + EqtTrace.Warning( + "The Directory: {0}, does not exist", + currentNode.DirectoryPath); + } + }); + } + } + + if (assembly != null) + { + return assembly; + } + } + + // Try for default load for System dlls that can't be found in search paths. Needs to loaded just by name. + try + { + if (isReflectionOnly) + { + // Put it in the resolved assembly cache so that if the Load call below + // triggers another assembly resolution, then we don't end up in stack overflow. + this.reflectionOnlyResolvedAssemblies[assemblyNameToLoad] = null; + + assembly = Assembly.ReflectionOnlyLoad(assemblyNameToLoad); + + if (assembly != null) + { + this.reflectionOnlyResolvedAssemblies[assemblyNameToLoad] = assembly; + } + } + else + { + // Put it in the resolved assembly cache so that if the Load call below + // triggers another assembly resolution, then we don't end up in stack overflow. + this.resolvedAssemblies[assemblyNameToLoad] = null; + + assembly = Assembly.Load(assemblyNameToLoad); + + if (assembly != null) + { + this.resolvedAssemblies[assemblyNameToLoad] = assembly; + } + } + + return assembly; + } + catch (Exception ex) + { + this.SafeLog( + args?.Name, + () => + { + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("AssemblyResolver: {0}: Failed to load assembly. Reason: {1}", assemblyNameToLoad, ex); + } + }); + } + + return assembly; + } + } + + /// + /// Load assembly from cache if available. + /// + /// The assembly Name. + /// Indicates if this is a reflection-only context. + /// The assembly. + /// The . + private bool TryLoadFromCache(string assemblyName, bool isReflectionOnly, out Assembly assembly) + { + bool isFoundInCache = false; + + if (isReflectionOnly) + { + isFoundInCache = this.reflectionOnlyResolvedAssemblies.TryGetValue(assemblyName, out assembly); + } + else + { + isFoundInCache = this.resolvedAssemblies.TryGetValue(assemblyName, out assembly); + } + + if (isFoundInCache) + { + this.SafeLog( + assemblyName, + () => + { + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("AssemblyResolver: Resolved: {0}.", assemblyName); + } + }); + return true; + } + + return false; + } + + /// + /// Call logger APIs safely. We do not want a stackoverflow when objectmodel assembly itself + /// is being resolved and an EqtTrace message prompts the load of the same dll again. + /// CLR does not trigger a load when the EqtTrace messages are in a lamda expression. Leaving it that way + /// to preserve readability instead of creating wrapper functions. + /// + /// The assembly being resolved. + /// The logger function. + private void SafeLog(string assemblyName, Action loggerAction) + { + // Logger assembly was in `Microsoft.VisualStudio.TestPlatform.ObjectModel` assembly in legacy versions and we need to omit it as well. + if (!string.IsNullOrEmpty(assemblyName) && !assemblyName.StartsWith(LoggerAssemblyName) && !assemblyName.StartsWith(LoggerAssemblyNameLegacy)) + { + loggerAction.Invoke(); + } + } + + /// + /// Search for assembly and if exists then load. + /// + /// The assembly Path. + /// The assembly Name. + /// The requested Name. + /// Indicates whether this is called under a Reflection Only Load context. + /// The . + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom", Justification = "The assembly location is figured out from the configuration that the user passes in.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + private Assembly SearchAndLoadAssembly(string assemblyPath, string assemblyName, AssemblyName requestedName, bool isReflectionOnly) + { + try + { + if (!this.DoesFileExist(assemblyPath)) + { + return null; + } + + var foundName = AssemblyName.GetAssemblyName(assemblyPath); + + if (!RequestedAssemblyNameMatchesFound(requestedName, foundName)) + { + return null; // File exists but version/public key is wrong. Try next extension. + } + + Assembly assembly; + + if (isReflectionOnly) + { + assembly = this.ReflectionOnlyLoadAssemblyFrom(assemblyPath); + this.reflectionOnlyResolvedAssemblies[assemblyName] = assembly; + } + else + { + assembly = this.LoadAssemblyFrom(assemblyPath); + this.resolvedAssemblies[assemblyName] = assembly; + } + + this.SafeLog( + assemblyName, + () => + { + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("AssemblyResolver: Resolved assembly: {0}. ", assemblyName); + } + }); + return assembly; + } + catch (FileLoadException ex) + { + this.SafeLog( + assemblyName, + () => + { + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("AssemblyResolver: Failed to load assembly: {0}. Reason:{1} ", assemblyName, ex); + } + }); + + // Re-throw FileLoadException, because this exception means that the assembly + // was found, but could not be loaded. This will allow us to report a more + // specific error message to the user for things like access denied. + throw; + } + catch (Exception ex) + { + // For all other exceptions, try the next extension. + this.SafeLog( + assemblyName, + () => + { + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("AssemblyResolver: Failed to load assembly: {0}. Reason:{1} ", assemblyName, ex); + } + }); + } + + return null; + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Constants.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Constants.cs new file mode 100644 index 0000000000..55ab419df1 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Constants.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System.Collections.Generic; + + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + + internal class Constants + { + /// + /// Constants for detecting .net framework. + /// + public const string TargetFrameworkAttributeFullName = "System.Runtime.Versioning.TargetFrameworkAttribute"; + + public const string DotNetFrameWorkStringPrefix = ".NETFramework,Version="; + + public const string TargetFrameworkName = "TargetFrameworkName"; + + /// + /// Constants for MSTest in Portable Mode + /// + public const string PortableVsTestLocation = "PortableVsTestLocation"; + + public const string PublicAssemblies = "PublicAssemblies"; + + public const string PrivateAssemblies = "PrivateAssemblies"; + + public static readonly TestProperty DeploymentItemsProperty = TestProperty.Register("MSTestDiscoverer.DeploymentItems", DeploymentItemsLabel, typeof(KeyValuePair[]), TestPropertyAttributes.Hidden, typeof(TestCase)); + + internal const string DllExtension = ".dll"; + internal const string ExeExtension = ".exe"; + internal const string PhoneAppxPackageExtension = ".appx"; + + // These are tied to a specific VS version. Can be changed to have a list of supported version instead. + internal const string VisualStudioRootRegKey32ForDev14 = @"SOFTWARE\Microsoft\VisualStudio\" + VisualStudioVersion; + internal const string VisualStudioRootRegKey64ForDev14 = @"SOFTWARE\Wow6432Node\Microsoft\VisualStudio\" + VisualStudioVersion; + + internal const string VisualStudioVersion = "14.0"; + + private const string DeploymentItemsLabel = "DeploymentItems"; + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Data/CsvDataConnection.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Data/CsvDataConnection.cs new file mode 100644 index 0000000000..9972c24dbe --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Data/CsvDataConnection.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Data; + using System.Data.OleDb; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Text; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + + /// + /// Utility classes to access databases, and to handle quoted strings etc for comma separated value files. + /// + internal sealed class CsvDataConnection : TestDataConnection + { + // Template used to map from a filename to a DB connection string + private const string CsvConnectionTemplate = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};Persist Security Info=False;Extended Properties=\"text;HDR=YES;FMT=Delimited\""; + private const string CsvConnectionTemplate64 = "Provider=Microsoft.Ace.OLEDB.12.0;Data Source={0};Persist Security Info=False;Extended Properties=\"text;HDR=YES;FMT=Delimited\""; + + private string fileName; + + public CsvDataConnection(string fileName, List dataFolders) + : base(dataFolders) + { + Debug.Assert(!string.IsNullOrEmpty(fileName), "fileName"); + this.fileName = fileName; + } + + private string TableName + { + get + { + // Only one table based on the name of the file, with dots converted to # signs + return Path.GetFileName(this.fileName).Replace('.', '#'); + } + } + + public override List GetDataTablesAndViews() + { + List tableNames = new List(1); + tableNames.Add(this.TableName); + return tableNames; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + public override List GetColumns(string tableName) + { + // Somewhat heavy, this could be improved, right now I simply + // read the table in then check the columns... + try + { + DataTable table = this.ReadTable(tableName, null); + if (table != null) + { + List columnNames = new List(); + foreach (DataColumn column in table.Columns) + { + columnNames.Add(column.ColumnName); + } + + return columnNames; + } + } + catch (Exception exception) + { + EqtTrace.ErrorIf(EqtTrace.IsErrorEnabled, exception.Message + " for CSV data source " + this.fileName); + } + + return null; + } + + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Untested. Leaving as-is.")] + [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security", Justification = "Not passed in from user.")] + public DataTable ReadTable(string tableName, IEnumerable columns, int maxRows) + { + // We specifically use OleDb to read a CSV file... + WriteDiagnostics("ReadTable: {0}", tableName); + WriteDiagnostics("Current Directory: {0}", Directory.GetCurrentDirectory()); + + // We better work with a full path, if nothing else, errors become easier to report + string fullPath = this.FixPath(this.fileName) ?? Path.GetFullPath(this.fileName); + + // We can map simplified CSVs to an OLEDB/Text connection, then proceed as normal + using (OleDbConnection connection = new OleDbConnection()) + using (OleDbDataAdapter dataAdapter = new OleDbDataAdapter()) + using (OleDbCommandBuilder commandBuilder = new OleDbCommandBuilder()) + using (OleDbCommand command = new OleDbCommand()) + { + // We have to use the name of the folder which contains the CSV file in the connection string + // If target platform is x64, then use CsvConnectionTemplate64 connection string. + if (IntPtr.Size == 8) + { + connection.ConnectionString = string.Format(CultureInfo.InvariantCulture, CsvConnectionTemplate64, Path.GetDirectoryName(fullPath)); + } + else + { + connection.ConnectionString = string.Format(CultureInfo.InvariantCulture, CsvConnectionTemplate, Path.GetDirectoryName(fullPath)); + } + + WriteDiagnostics("Connection String: {0}", connection.ConnectionString); + + // We have to open the connection now, before we try to quote + // the table name, otherwise QuoteIdentifier fails (for OleDb, go figure!) + // The connection will get closed when we dispose of it + connection.Open(); + + string quotedTableName = commandBuilder.QuoteIdentifier(tableName, connection); + + command.Connection = connection; + + string topClause; + if (maxRows >= 0) + { + topClause = string.Format(CultureInfo.InvariantCulture, " top {0}", maxRows.ToString(NumberFormatInfo.InvariantInfo)); + } + else + { + topClause = string.Empty; + } + + string columnsClause; + if (columns != null) + { + StringBuilder builder = new StringBuilder(); + foreach (string columnName in columns) + { + if (builder.Length > 0) + { + builder.Append(','); + } + + builder.Append(commandBuilder.QuoteIdentifier(columnName, connection)); + } + + columnsClause = builder.ToString(); + if (columnsClause.Length == 0) + { + columnsClause = "*"; + } + } + else + { + columnsClause = "*"; + } + + command.CommandText = string.Format(CultureInfo.InvariantCulture, "select {0} {1} from {2}", topClause, columnsClause, quotedTableName); + WriteDiagnostics("Query: " + command.CommandText); + + dataAdapter.SelectCommand = command; + + DataTable table = new DataTable(); + table.Locale = CultureInfo.InvariantCulture; + dataAdapter.Fill(table); + return table; + } + } + + public override DataTable ReadTable(string tableName, IEnumerable columns) + { + return this.ReadTable(tableName, columns, -1); + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Data/OdbcDataConnection.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Data/OdbcDataConnection.cs new file mode 100644 index 0000000000..13af29c8ff --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Data/OdbcDataConnection.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data +{ + using System; + using System.Collections.Generic; + using System.Data.Odbc; + using System.Diagnostics; + + /// + /// Utility classes to access databases, and to handle quoted strings etc for ODBC. + /// + internal sealed class OdbcDataConnection : TestDataConnectionSql + { + private bool isMSSql; + + public OdbcDataConnection(string invariantProviderName, string connectionString, List dataFolders) + : base(invariantProviderName, FixConnectionString(connectionString, dataFolders), dataFolders) + { + // Need open connection to get Connection.Driver. + Debug.Assert(this.IsOpen(), "The connection must be open!"); + + this.isMSSql = this.Connection != null && IsMSSql(this.Connection.Driver); + } + + public new OdbcCommandBuilder CommandBuilder + { + get { return (OdbcCommandBuilder)base.CommandBuilder; } + } + + public new OdbcConnection Connection + { + get { return (OdbcConnection)base.Connection; } + } + + /// + /// This is overridden because we need manually get quote literals, OleDb does not fill those automatically. + /// + public override void GetQuoteLiterals() + { + this.GetQuoteLiteralsHelper(); + } + + public override string GetDefaultSchema() + { + if (this.isMSSql) + { + return this.GetDefaultSchemaMSSql(); + } + + return base.GetDefaultSchema(); + } + + protected override SchemaMetaData[] GetSchemaMetaData() + { + // The following may fail for Oracle ODBC, need to test that... + SchemaMetaData data1 = new SchemaMetaData() + { + SchemaTable = "Tables", + SchemaColumn = "TABLE_SCHEM", + NameColumn = "TABLE_NAME", + TableTypeColumn = "TABLE_TYPE", + ValidTableTypes = new string[] { "TABLE", "SYSTEM TABLE" }, + InvalidSchemas = null + }; + SchemaMetaData data2 = new SchemaMetaData() + { + SchemaTable = "Views", + SchemaColumn = "TABLE_SCHEM", + NameColumn = "TABLE_NAME", + TableTypeColumn = "TABLE_TYPE", + ValidTableTypes = new string[] { "VIEW" }, + InvalidSchemas = new string[] { "sys", "INFORMATION_SCHEMA" } + }; + return new SchemaMetaData[] { data1, data2 }; + } + + protected override string QuoteIdentifier(string identifier) + { + Debug.Assert(!string.IsNullOrEmpty(identifier), "identifier"); + return this.CommandBuilder.QuoteIdentifier(identifier, this.Connection); // Must pass connection. + } + + protected override string UnquoteIdentifier(string identifier) + { + Debug.Assert(!string.IsNullOrEmpty(identifier), "identifier"); + return this.CommandBuilder.UnquoteIdentifier(identifier, this.Connection); // Must pass connection. + } + + // Need to fix up excel connections + private static string FixConnectionString(string connectionString, List dataFolders) + { + OdbcConnectionStringBuilder builder = new OdbcConnectionStringBuilder(connectionString); + + // only fix this for excel + if (!string.Equals(builder.Dsn, "Excel Files")) + { + return connectionString; + } + + string fileName = builder["dbq"] as string; + + if (string.IsNullOrEmpty(fileName)) + { + return connectionString; + } + else + { + // Fix-up magic file paths + string fixedFilePath = FixPath(fileName, dataFolders); + if (fixedFilePath != null) + { + builder["dbq"] = fixedFilePath; + } + + return builder.ConnectionString; + } + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Data/OleDataConnection.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Data/OleDataConnection.cs new file mode 100644 index 0000000000..35c4fcb89b --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Data/OleDataConnection.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data +{ + using System.Collections.Generic; + using System.Data.OleDb; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + + /// + /// Utility classes to access databases, and to handle quoted strings etc for OLE DB. + /// + [SuppressMessage("Microsoft.Naming", "CA1706", Justification = "OleDb instead of Oledb to match System.Data.OleDb")] + internal sealed class OleDataConnection : TestDataConnectionSql + { + private bool isMSSql; + + public OleDataConnection(string invariantProviderName, string connectionString, List dataFolders) + : base(invariantProviderName, FixConnectionString(connectionString, dataFolders), dataFolders) + { + // Need open connection to get Connection.Provider. + Debug.Assert(this.IsOpen(), "The connection must be open!"); + + // Fill m_isMSSql. + this.isMSSql = this.Connection != null && IsMSSql(this.Connection.Provider); + } + + public new OleDbCommandBuilder CommandBuilder + { + get { return (OleDbCommandBuilder)base.CommandBuilder; } + } + + public new OleDbConnection Connection + { + get { return (OleDbConnection)base.Connection; } + } + + /// + /// This is overridden because we need manually get quote literals, OleDb does not fill those automatically. + /// + public override void GetQuoteLiterals() + { + this.GetQuoteLiteralsHelper(); + } + + public override string GetDefaultSchema() + { + if (this.isMSSql) + { + return this.GetDefaultSchemaMSSql(); + } + + return base.GetDefaultSchema(); + } + + protected override SchemaMetaData[] GetSchemaMetaData() + { + // Note, in older iterations of the code there seemed to be + // cases when we also need to look in the "views" table + // but I do not see that in my test cases + SchemaMetaData data = new SchemaMetaData() + { + SchemaTable = "Tables", + SchemaColumn = "TABLE_SCHEMA", + NameColumn = "TABLE_NAME", + TableTypeColumn = "TABLE_TYPE", + ValidTableTypes = new string[] { "VIEW", "TABLE" }, + InvalidSchemas = null + }; + return new SchemaMetaData[] { data }; + } + + protected override string QuoteIdentifier(string identifier) + { + Debug.Assert(!string.IsNullOrEmpty(identifier), "identifier"); + return this.CommandBuilder.QuoteIdentifier(identifier, this.Connection); + } + + protected override string UnquoteIdentifier(string identifier) + { + Debug.Assert(!string.IsNullOrEmpty(identifier), "identifier"); + return this.CommandBuilder.UnquoteIdentifier(identifier, this.Connection); + } + + private static string FixConnectionString(string connectionString, List dataFolders) + { + OleDbConnectionStringBuilder oleDbBuilder = new OleDbConnectionStringBuilder(connectionString); + + string fileName = oleDbBuilder.DataSource; + + if (string.IsNullOrEmpty(fileName)) + { + return connectionString; + } + else + { + // Fix-up magic file paths + string fixedFilePath = FixPath(fileName, dataFolders); + if (fixedFilePath != null) + { + oleDbBuilder.DataSource = fixedFilePath; + } + + return oleDbBuilder.ConnectionString; + } + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Data/SqlDataConnection.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Data/SqlDataConnection.cs new file mode 100644 index 0000000000..ebe0019703 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Data/SqlDataConnection.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data +{ + using System.Collections.Generic; + using System.Data.SqlClient; + + /// + /// Utility classes to access databases, and to handle quoted strings etc for SQL Server. + /// + internal sealed class SqlDataConnection : TestDataConnectionSql + { + public SqlDataConnection(string invariantProviderName, string connectionString, List dataFolders) + : base(invariantProviderName, FixConnectionString(connectionString, dataFolders), dataFolders) + { + } + + /// + /// Returns default database schema. + /// this.Connection must be already opened. + /// + /// The default database schema. + public override string GetDefaultSchema() + { + return this.GetDefaultSchemaMSSql(); + } + + protected override SchemaMetaData[] GetSchemaMetaData() + { + SchemaMetaData data = new SchemaMetaData() + { + SchemaTable = "Tables", + SchemaColumn = "TABLE_SCHEMA", + NameColumn = "TABLE_NAME", + TableTypeColumn = "TABLE_TYPE", + ValidTableTypes = new string[] { "VIEW", "BASE TABLE" }, + InvalidSchemas = null + }; + return new SchemaMetaData[] { data }; + } + + private static string FixConnectionString(string connectionString, List dataFolders) + { + SqlConnectionStringBuilder sqlBuilder = new SqlConnectionStringBuilder(connectionString); + + string attachedFile = sqlBuilder.AttachDBFilename; + + if (string.IsNullOrEmpty(attachedFile)) + { + // No file, so no need to rewrite the connection string + return connectionString; + } + else + { + // Force pooling off for SQL when there is a file involved + // Without this, after the connection is closed, an exclusive lock persists + // for a long time, preventing us from moving files around + sqlBuilder.Pooling = false; + + // Fix-up magic file paths + string fixedFilePath = FixPath(attachedFile, dataFolders); + if (fixedFilePath != null) + { + sqlBuilder.AttachDBFilename = fixedFilePath; + } + + // Return modified connection string + return sqlBuilder.ConnectionString; + } + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnection.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnection.cs new file mode 100644 index 0000000000..7baa356ace --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnection.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Data; + using System.Data.Common; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Security; + + /// + /// This used to be "DataUtility", a helper class to handle quoted strings etc for different + /// data providers but the purpose has been expanded to be a general abstraction over a + /// connection, including the ability to read data and metadata (tables and columns) + /// + internal abstract class TestDataConnection : IDisposable + { + internal const string ConnectionDirectoryKey = "|DataDirectory|\\"; + + private static bool? extendedDiagnosticsEnabled; + + // List of places to look for files when substituting |DataDirectory| + private List dataFolders; + + internal protected TestDataConnection(List dataFolders) + { + this.dataFolders = dataFolders; + } + + /// + /// Gets the connection. + /// + /// This will only return non-null for true DB based connections (TestDataConnectionSql) + public virtual DbConnection Connection + { + get { return null; } + } + + private static bool ExtendedDiagnosticsEnabled + { + get + { + if (!extendedDiagnosticsEnabled.HasValue) + { + // We use an environment variable so that we can enable this extended + // diagnostic trace + try + { + string value = Environment.GetEnvironmentVariable("VSTS_DIAGNOSTICS"); + extendedDiagnosticsEnabled = (value != null) && value.Contains("TestDataConnection"); + } + catch (SecurityException) + { + extendedDiagnosticsEnabled = false; + } + } + + return extendedDiagnosticsEnabled.Value; + } + } + + /// + /// Get a list of tables and views for this connection. Filters out "system" tables + /// + /// List of names or null if error + public abstract List GetDataTablesAndViews(); + + /// + /// Given a table name, return a list of column names + /// + /// The name of the table. + /// List of names or null if error + public abstract List GetColumns(string tableName); + + /// + /// Read the content of a table or view into memory + /// Try to limit to columns specified, if columns is null, read all columns + /// + /// Minimally quoted table name + /// Array of columns + /// Data table or null if error + public abstract DataTable ReadTable(string tableName, IEnumerable columns); + + // It is critical that is class be disposed of properly, otherwise + // data connections may be left open. In general it is best to use create instances + // in a "using" + public virtual void Dispose() + { + GC.SuppressFinalize(this); + } + + internal static bool PathNeedsFixup(string path) + { + if (!string.IsNullOrEmpty(path)) + { + if (path.StartsWith(ConnectionDirectoryKey, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + // Only use this if "PathNeedsFixup" returns true + internal static string GetRelativePart(string path) + { + Debug.Assert(PathNeedsFixup(path), "Incorrect path."); + return path.Substring(ConnectionDirectoryKey.Length); + } + + // Check a string to see if it has our magic prefix + // and if it does, assume what follows is a relative + // path, which we then convert by making it a full path + // otherwise return null + internal static string FixPath(string path, List foldersToCheck) + { + if (PathNeedsFixup(path)) + { + string relPath = GetRelativePart(path); + + // First bet, relative to the current directory + string fullPath = Path.GetFullPath(relPath); + if (File.Exists(fullPath)) + { + return fullPath; + } + + // Second bet, any on our folders foldersToCheck list + if (foldersToCheck != null) + { + foreach (string folder in foldersToCheck) + { + fullPath = Path.GetFullPath(Path.Combine(folder, relPath)); + if (File.Exists(fullPath)) + { + return fullPath; + } + } + } + + // Finally assume the file ended up directly in the current directory. + return Path.GetFullPath(Path.GetFileName(relPath)); + } + + return null; + } + + [Conditional("DEBUG")] + protected internal static void WriteDiagnostics(string formatString, params object[] parameters) + { + if (ExtendedDiagnosticsEnabled) + { + Debug.WriteLine("TestDataConnection: " + string.Format(CultureInfo.InvariantCulture, formatString, parameters)); + } + } + + protected string FixPath(string path) + { + return FixPath(path, this.dataFolders); + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnectionFactory.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnectionFactory.cs new file mode 100644 index 0000000000..c7351a7237 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnectionFactory.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + + /// + /// Defines a class that creates TestDataConnection instances to connect to data sources. + /// + internal class TestDataConnectionFactory + { + // These are not "real" providers, but are recognized by the test runtime + private const string CsvProvider = "Microsoft.VisualStudio.TestTools.DataSource.CSV"; + private const string XmlProvider = "Microsoft.VisualStudio.TestTools.DataSource.XML"; + + /// + /// Test Specific Providers: maps provider name to provider factory that we lookup prior to using (by default) SqlTestDataConnection. + /// Notes + /// - the key (provider name is case-sensitive). + /// - other providers can be registered using RegisterProvider (per app domain). + /// + private static Dictionary specializedProviders = new Dictionary + { + // The XML case is quite unlike all others, as there is no real DB connection at all! + { XmlProvider, new XmlTestDataConnectionFactory() }, + + // The CSV case does use a database connection, but it is hidden, and schema + // queries are highly specialized + { CsvProvider, new CsvTestDataConnectionFactory() }, + }; + + /// + /// Construct a wrapper for a database connection, what is actually returned indirectly depends + /// on the invariantProviderName, and the specific call knows how to deal with database variations + /// + /// The provider name. + /// The connection string. + /// null, or a list of locations to check when fixing up connection string + /// The TestDataConnection instance. + public virtual TestDataConnection Create(string invariantProviderName, string connectionString, List dataFolders) + { + Debug.Assert(!string.IsNullOrEmpty(invariantProviderName), "invariantProviderName"); + Debug.Assert(!string.IsNullOrEmpty(connectionString), "connectionString"); + + TestDataConnection.WriteDiagnostics("Create {0}, {1}", invariantProviderName, connectionString); + + // Most, but not all, connections are actually database based, + // here we look for special cases + if (specializedProviders.TryGetValue(invariantProviderName, out var factory)) + { + Debug.Assert(factory != null, "factory"); + return factory.Create(invariantProviderName, connectionString, dataFolders); + } + else + { + // Default is to use a conventional SQL based connection, this create method in turn + // handles variations between DB based implementations + return TestDataConnectionSql.Create(invariantProviderName, connectionString, dataFolders); + } + } + + #region TestDataConnectionFactories + + private class XmlTestDataConnectionFactory : TestDataConnectionFactory + { + public override TestDataConnection Create(string invariantProviderName, string connectionString, List dataFolders) + { + return new XmlDataConnection(connectionString, dataFolders); + } + } + + private class CsvTestDataConnectionFactory : TestDataConnectionFactory + { + public override TestDataConnection Create(string invariantProviderName, string connectionString, List dataFolders) + { + return new CsvDataConnection(connectionString, dataFolders); + } + } + + #endregion TestDataConnectionFactories + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnectionSql.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnectionSql.cs new file mode 100644 index 0000000000..d849b19923 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Data/TestDataConnectionSql.cs @@ -0,0 +1,957 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Data; + using System.Data.Common; + using System.Data.Odbc; + using System.Data.OleDb; + using System.Data.SqlClient; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Text; + using ObjectModel; + + /// + /// Data connections based on direct DB implementations all derive from this one + /// + internal class TestDataConnectionSql : TestDataConnection + { + private string quoteSuffix; + private string quotePrefix; + private DbCommandBuilder commandBuilder; + private DbConnection connection; + private DbProviderFactory factory; + + #region Constructor + + internal protected TestDataConnectionSql(string invariantProviderName, string connectionString, List dataFolders) + : base(dataFolders) + { + this.factory = DbProviderFactories.GetFactory(invariantProviderName); + WriteDiagnostics("DbProviderFactory {0}", this.factory); + Debug.Assert(this.factory != null, "factory should not be null."); + + this.connection = this.factory.CreateConnection(); + WriteDiagnostics("DbConnection {0}", this.connection); + Debug.Assert(this.connection != null, "connection"); + + this.commandBuilder = this.factory.CreateCommandBuilder(); + WriteDiagnostics("DbCommandBuilder {0}", this.commandBuilder); + Debug.Assert(this.commandBuilder != null, "builder"); + + if (!string.IsNullOrEmpty(connectionString)) + { + this.connection.ConnectionString = connectionString; + WriteDiagnostics("Current directory: {0}", Directory.GetCurrentDirectory()); + WriteDiagnostics("Opening connection {0}: {1}", invariantProviderName, connectionString); + this.connection.Open(); + } + + WriteDiagnostics("Connection state is {0}", this.connection.State); + } + + #endregion + + #region Data Properties + + public override DbConnection Connection + { + get { return this.connection; } + } + + protected DbCommandBuilder CommandBuilder + { + get { return this.commandBuilder; } + } + + protected DbProviderFactory Factory + { + get { return this.factory; } + } + + #endregion + + public static TestDataConnectionSql Create(string invariantProviderName, string connectionString, List dataFolders) + { + Debug.Assert(!string.IsNullOrEmpty(invariantProviderName), "invariantProviderName"); + + // unit tests pass a null for connection string, so let it pass. However, not all + // providers can handle that, an example being ODBC + WriteDiagnostics("CreateSql {0}, {1}", invariantProviderName, connectionString); + + // For invariant providers we recognize, we have specific sub-classes + if (string.Equals(invariantProviderName, "System.Data.SqlClient", StringComparison.OrdinalIgnoreCase)) + { + return new SqlDataConnection(invariantProviderName, connectionString, dataFolders); + } + else if (string.Equals(invariantProviderName, "System.Data.OleDb", StringComparison.OrdinalIgnoreCase)) + { + return new OleDataConnection(invariantProviderName, connectionString, dataFolders); + } + else if (string.Equals(invariantProviderName, "System.Data.Odbc", StringComparison.OrdinalIgnoreCase)) + { + return new OdbcDataConnection(invariantProviderName, connectionString, dataFolders); + } + else + { + // All other providers handled by my base class + WriteDiagnostics("Using default SQL implementation for {0}, {1}", invariantProviderName, connectionString); + return new TestDataConnectionSql(invariantProviderName, connectionString, dataFolders); + } + } + + protected virtual SchemaMetaData[] GetSchemaMetaData() + { + // A bare minimum set of things that should vaguely work for all databases + SchemaMetaData data = new SchemaMetaData() + { + SchemaTable = "Tables", + SchemaColumn = null, + NameColumn = "TABLE_NAME", + TableTypeColumn = null, + ValidTableTypes = null, + InvalidSchemas = null + }; + return new SchemaMetaData[] { data }; + } + + #region Quotes + +#pragma warning disable SA1201 // Elements must appear in the correct order + public virtual string QuotePrefix +#pragma warning restore SA1201 // Elements must appear in the correct order + { + get + { + if (string.IsNullOrEmpty(this.quotePrefix)) + { + this.GetQuoteLiterals(); + } + + return this.quotePrefix; + } + + set + { + this.quotePrefix = value; + } + } + + public virtual string QuoteSuffix + { + get + { + if (string.IsNullOrEmpty(this.quoteSuffix)) + { + this.GetQuoteLiterals(); + } + + return this.quoteSuffix; + } + + set + { + this.quoteSuffix = value; + } + } + + private char CatalogSeperatorChar + { + get + { + if (this.CommandBuilder != null) + { + string catalogSeperator = this.CommandBuilder.CatalogSeparator; + if (!string.IsNullOrEmpty(catalogSeperator)) + { + Debug.Assert(catalogSeperator.Length == 1, "catalogSeperator should have 1 element."); + return catalogSeperator[0]; + } + } + + return '.'; + } + } + + private char SchemaSeperatorChar + { + get + { + if (this.CommandBuilder != null) + { + string schemaSeperator = this.CommandBuilder.SchemaSeparator; + if (!string.IsNullOrEmpty(schemaSeperator)) + { + Debug.Assert(schemaSeperator.Length == 1, "schemaSeperator should have 1 element."); + return schemaSeperator[0]; + } + } + + return '.'; + } + } + + /// + /// Take a possibly qualified name with at least minimal quoting + /// and return a fully quoted string + /// Take care to only convert names that are of a recognized form + /// + /// The table name. + /// A fully quoted string. + public string PrepareNameForSql(string tableName) + { + string[] parts = this.SplitName(tableName); + + if (parts != null && parts.Length > 0) + { + // Seems to be well formed, so make sure we end up fully quoted + return this.JoinAndQuoteName(parts, true); + } + else + { + // Just use what they gave us, literally, since we do not really understand the format + return tableName; + } + } + + /// + /// Take a possibly qualified name and break it down into an + /// array of identifiers unquoting any quoted names + /// + /// A string. + /// An array of unquoted parts, or null if the name fails to conform + public string[] SplitName(string name) + { + List parts = new List(); + + int here = 0; + int end = name.Length; + char firstDelimiter = ' '; // initialize since code analysis is not smart enough + char currentDelimiter; + char catalogSeperatorChar = this.CatalogSeperatorChar; + char schemaSeperatorChar = this.SchemaSeperatorChar; + + while (here < end) + { + int next = this.FindIdentifierEnd(name, here); + string identifier = name.Substring(here, next - here); + + if (string.IsNullOrEmpty(identifier)) + { + // Not well formed, split failed + return null; + } + + if (identifier.StartsWith(this.QuotePrefix, StringComparison.Ordinal)) + { + identifier = this.UnquoteIdentifier(identifier); + } + + parts.Add(identifier); + + if (next < end) + { + currentDelimiter = name[next]; + switch (parts.Count) + { + case 1: + // We infer there will be at least 2 parts + firstDelimiter = currentDelimiter; + if (firstDelimiter != catalogSeperatorChar + && firstDelimiter != schemaSeperatorChar) + { + // Not well formed, split failed + return null; + } + + break; + + case 2: + // We infer there will be at least 3 parts + if (firstDelimiter != catalogSeperatorChar + || currentDelimiter != schemaSeperatorChar) + { + // Not well formed, split failed + return null; + } + + break; + + default: + // We infer there will be at least 4 or more parts + // so not well formed, split failed + return null; + } + + // Skip delimiter + here = next + 1; + } + else + { + // We have found the end + if (parts.Count == 2 && firstDelimiter != schemaSeperatorChar) + { + // Not well formed, split failed + return null; + } + + return parts.ToArray(); + } + } + + // Ended in a delimiter, or no parts at all, either is invalid + return null; + } + + /// + /// Take a list of unquoted name parts and join them into a + /// qualified name. Either minimally quote (to the extent required + /// to reliably split the name again) or fully quote, therefore made suitable + /// for a database query + /// + /// Name parts. + /// Should full quote. + /// A qualified name + public string JoinAndQuoteName(string[] parts, bool fullyQuote) + { + int partCount = parts.Length; + StringBuilder result = new StringBuilder(); + + Debug.Assert(partCount > 0 && partCount < 4, "partCount should be 1,2 or 3."); + + int currentPart = 0; + if (partCount > 2) + { + result.Append(this.MaybeQuote(parts[currentPart++], fullyQuote)); + result.Append(this.CommandBuilder.CatalogSeparator); + } + + if (partCount > 1) + { + result.Append(this.MaybeQuote(parts[currentPart++], fullyQuote)); + result.Append(this.CommandBuilder.SchemaSeparator); + } + + result.Append(this.MaybeQuote(parts[currentPart], fullyQuote)); + return result.ToString(); + } + + /// + /// Note that for Oledb and Odbc CommandBuilder.QuotePrefix/Suffix is empty. + /// So we use GetQuoteLiterals for those. For all others we use CommandBuilder.QuotePrefix/Suffix. + /// + public virtual void GetQuoteLiterals() + { + this.quotePrefix = this.CommandBuilder.QuotePrefix; + this.quoteSuffix = this.CommandBuilder.QuoteSuffix; + } + + protected virtual string QuoteIdentifier(string identifier) + { + Debug.Assert(!string.IsNullOrEmpty(identifier), "identifier should not be null."); + return this.CommandBuilder.QuoteIdentifier(identifier); + } + + protected virtual string UnquoteIdentifier(string identifier) + { + Debug.Assert(!string.IsNullOrEmpty(identifier), "identifier should not be null."); + return this.CommandBuilder.UnquoteIdentifier(identifier); + } + + protected void GetQuoteLiteralsHelper() + { + // Try to get quote chars by hand for those providers that for some reason return empty QuotePrefix/Suffix. + string s = "abcdefgh"; + string quoted = this.QuoteIdentifier(s); + string[] parts = quoted.Split(new string[] { s }, StringSplitOptions.None); + + Debug.Assert(parts != null && parts.Length == 2, "TestDataConnectionSql.GetQuotesLiteralHelper: Failure when trying to quote an indentifier!"); + Debug.Assert(!string.IsNullOrEmpty(parts[0]), "TestDataConnectionSql.GetQuotesLiteralHelper: Trying to set empty value for QuotePrefix!"); + Debug.Assert(!string.IsNullOrEmpty(parts[1]), "TestDataConnectionSql.GetQuotesLiteralHelper: Trying to set empty value for QuoteSuffix!"); + + this.QuotePrefix = parts[0]; + this.QuoteSuffix = parts[1]; + } + + private string MaybeQuote(string identifier, bool force) + { + if (force || this.FindSeperators(identifier, 0) != -1) + { + return this.QuoteIdentifier(identifier); + } + + return identifier; + } + + /// + /// Find the first separator in a string + /// + /// The string. + /// Index. + /// Location of the separator. + private int FindSeperators(string text, int from) + { + return text.IndexOfAny(new char[] { this.SchemaSeperatorChar, this.CatalogSeperatorChar }, from); + } + + /// + /// Given a string and a position in that string, assumed + /// to be the start of an identifier, find the end of that + /// identifier. Take into account quoting rules + /// + /// The string. + /// start index. + /// Position in string after end of identifier (may be off end of string) + private int FindIdentifierEnd(string text, int start) + { + // These routine assumes prefixes and suffixes + // are single characters + string prefix = this.QuotePrefix; + Debug.Assert(prefix.Length == 1, "prefix length should be 1."); + char prefixChar = prefix[0]; + + int end = text.Length; + if (text[start] == prefixChar) + { + // Identifier is quoted. Repeatedly look for + // suffix character, until not found, + // the character after is end of string, + // or not another suffix character + + // Skip opening quote + int here = start + 1; + + string suffix = this.QuoteSuffix; + Debug.Assert(suffix.Length == 1, "suffix length should be 1."); + char suffixChar = suffix[0]; + + while (here < end) + { + here = text.IndexOf(suffixChar, here); + if (here == -1) + { + // If this happens the string is malformed, since we had an + // opening quote without a closing one, but we can survive this + break; + } + + // Skip the quote we just found + here++; + + // If this the end? + if (here == end || text[here] != suffixChar) + { + // Well formed end of identifier + return here; + } + + // We have a double quote, skip the second one, then keep looking + here++; + } + + // If we fall off end of loop, + // we didn't find the matching close quote + // Best thing to do is to just return the whole string + return end; + } + else + { + // In the case of an unquoted strings, the processing is much + // simpler... the end is end of string, or the first + // of several possible separators. + int seperatorPosition = this.FindSeperators(text, start); + return seperatorPosition == -1 ? end : seperatorPosition; + } + } + + #endregion + + #region Schema + +#pragma warning disable SA1202 // Elements must be ordered by access + + /// + /// Returns default database schema, can be null if there is no default schema like for Excel. + /// Can throw. + /// + /// The default database schema. + public virtual string GetDefaultSchema() + { + return null; + } + +#pragma warning restore SA1202 // Elements must be ordered by access + + /// + /// Returns list of data tables and views. Sorted. + /// Any errors, return an empty list + /// + /// List of sorted tables and views. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + public override List GetDataTablesAndViews() + { + WriteDiagnostics("GetDataTablesAndViews"); + List tableNames = new List(); + try + { + string defaultSchema = this.GetDefaultSchema(); + WriteDiagnostics("Default schema is {0}", defaultSchema); + + SchemaMetaData[] metadatas = this.GetSchemaMetaData(); + + // TODO: may be find better way to enumerate tables/views. + foreach (SchemaMetaData metadata in metadatas) + { + DataTable dataTable = null; + try + { + WriteDiagnostics("Getting schema table {0}", metadata.SchemaTable); + dataTable = this.Connection.GetSchema(metadata.SchemaTable); + } + catch (Exception ex) + { + WriteDiagnostics("Failed to get schema table"); + + // This can be normal case as some providers do not support views. + EqtTrace.WarningIf(EqtTrace.IsWarningEnabled, "DataUtil.GetDataTablesAndViews: exception (can be normal case as some providers do not support views): " + ex); + continue; + } + + Debug.Assert(dataTable != null, "Failed to get data table that contains metadata about tables!"); + + foreach (DataRow row in dataTable.Rows) + { + WriteDiagnostics("Row: {0}", row); + string tableSchema = null; + bool isDefaultSchema = false; + + // Check the table type for validity + if (metadata.TableTypeColumn != null) + { + if (row[metadata.TableTypeColumn] != DBNull.Value) + { + string tableType = row[metadata.TableTypeColumn] as string; + if (!IsInArray(tableType, metadata.ValidTableTypes)) + { + WriteDiagnostics("Table type {0} is not acceptable", tableType); + + // Not a valid table type, get the next row + continue; + } + } + } + + // Get the schema name, and filter bad schemas + if (row[metadata.SchemaColumn] != DBNull.Value) + { + tableSchema = row[metadata.SchemaColumn] as string; + + if (IsInArray(tableSchema, metadata.InvalidSchemas)) + { + WriteDiagnostics("Schema {0} is not acceptable", tableSchema); + + // A table in a schema we do not want to see, get the next row + continue; + } + + isDefaultSchema = string.Equals(tableSchema, defaultSchema, StringComparison.OrdinalIgnoreCase); + } + + string tableName = row[metadata.NameColumn] as string; + WriteDiagnostics("Table {0}{1} found", tableSchema != null ? tableSchema + "." : string.Empty, tableName); + + // If schema is defined and is not equal to default, prepend table schema in front of the table. + string qualifiedTableName = tableName; + if (isDefaultSchema) + { + qualifiedTableName = this.FormatTableNameForDisplay(null, tableName); + } + else + { + qualifiedTableName = this.FormatTableNameForDisplay(tableSchema, tableName); + } + + WriteDiagnostics("Adding Table {0}", qualifiedTableName); + tableNames.Add(qualifiedTableName); + } + + tableNames.Sort(StringComparer.OrdinalIgnoreCase); + } + } + catch (Exception e) + { + WriteDiagnostics("Failed to fetch tables for {0}, exception: {1}", this.Connection.ConnectionString, e); + + // OK to fall through and return whatever we do have... + } + + return tableNames; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + public override List GetColumns(string tableName) + { + WriteDiagnostics("GetColumns for {0}", tableName); + try + { + this.SplitTableName(tableName, out var targetSchema, out var targetName); + + // This lets us specifically query for columns from the appropriate table name + // but assumes all databases have the same restrictions on all the column + // schema tables + string[] restrictions = new string[4] + { + null, // Catalog (don't care) + targetSchema, // Table schema + targetName, // Table name + null + }; // Column name (don't care) + + DataTable columns = null; + try + { + columns = this.Connection.GetSchema("Columns", restrictions); + } + catch (System.NotSupportedException e) + { + WriteDiagnostics("GetColumns for {0} failed to get column metadata, exception {1}", tableName, e); + } + + if (columns != null) + { + List result = new List(); + + // Add all the columns + foreach (DataRow columnRow in columns.Rows) + { + WriteDiagnostics("Column info: {0}", columnRow); + result.Add(columnRow["COLUMN_NAME"].ToString()); + } + + // Now we are done, since for any particular table or view, all the columns + // must be found in a single metadata collection + return result; + } + else + { + WriteDiagnostics("Column metadata is null"); + } + } + catch (Exception e) + { + WriteDiagnostics("GetColumns for {0}, failed {1}", tableName, e); + } + + return null; // Some problem occurred + } + + /// + /// Split a table name into schema and table name, providing default + /// schema if available + /// + /// The name. + /// The schema name output. + /// The table name output. + protected void SplitTableName(string name, out string schemaName, out string tableName) + { + // Split the name because we need to separately look for + // tableSchema and tableName + string[] parts = this.SplitName(name); + + Debug.Assert(parts.Length > 0, "parts should have more than one element."); + + // Right now this processing ignores any three part names (where the catalog is specified) + // We use the default schema if the name does not specify one explicitly + schemaName = parts.Length > 1 ? parts[parts.Length - 2] : this.GetDefaultSchema(); + tableName = parts[parts.Length - 1]; + } + + /// + /// Returns qualified data table name, formatted for display in Data Table list or use in + /// code or test files. Note that this may not return a suitable string for SQL + /// + /// Schema part of qualified table name. Quoted or not quoted. + /// Table name. Quoted or not quoted. + /// Qualified data table name. + protected string FormatTableNameForDisplay(string tableSchema, string tableName) + { + // Note: schema can be null/empty, that is OK + Debug.Assert(!string.IsNullOrEmpty(tableName), "FormatDataTableNameForDisplay should be called only when table name is not empty."); + + if (string.IsNullOrEmpty(tableSchema)) + { + return this.JoinAndQuoteName(new string[] { tableName }, false); + } + else + { + return this.JoinAndQuoteName(new string[] { tableSchema, tableName }, false); + } + } + + /// + /// Just a helper method to see if a string is in a string array + /// Note that the array can be null, this is treated as an empty array + /// + /// The string. + /// An array of values. + /// True if string exists in array. + private static bool IsInArray(string candidate, string[] values) + { + if (values != null) + { + foreach (string value in values) + { + if (string.Equals(value, candidate, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + } + + return false; + } + + #endregion + + #region Helpers + +#pragma warning disable SA1202 // Elements must be ordered by access + public bool IsOpen() +#pragma warning restore SA1202 // Elements must be ordered by access + { + return this.connection != null && this.connection.State == ConnectionState.Open; + } + + /// + /// Returns true when given provider (OLEDB or ODBC) is for MSSql. + /// + /// OLEDB or ODBC provider. + /// True if provider is for MSSql. + protected static bool IsMSSql(string providerName) + { + return (!string.IsNullOrEmpty(providerName) && + (providerName.StartsWith(KnownOleDbProviderNames.SqlOleDb, StringComparison.OrdinalIgnoreCase) || + providerName.StartsWith(KnownOleDbProviderNames.MSSqlNative, StringComparison.OrdinalIgnoreCase))) || + string.Equals(providerName, KnownOdbcDrivers.MSSql, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Classify a table schema as being hidden from the user + /// This helps to hide system tables such as INFORMATION_SCHEMA.COLUMNS + /// + /// A candidate table schema + /// True always. + protected virtual bool IsUserSchema(string tableSchema) + { + // Default is to allow all schemas + return true; + } + + /// + /// Returns default database schema. Returns null for error + /// this.Connection must be already opened. + /// + /// The default db schema. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + protected string GetDefaultSchemaMSSql() + { + Debug.Assert(this.Connection != null, "Connection should not be null."); + + try + { + OleDbConnection oleDbConnection = this.Connection as OleDbConnection; + OdbcConnection odbcConnection = this.Connection as OdbcConnection; + Debug.Assert( + this.Connection is SqlConnection || + (oleDbConnection != null && IsMSSql(oleDbConnection.Provider)) || + (odbcConnection != null && IsMSSql(odbcConnection.Driver)), + "GetDefaultSchemaMSSql should be called only for MS SQL (either native or Ole Db or Odbc)."); + + Debug.Assert(this.IsOpen(), "The connection must already be open!"); + Debug.Assert(!string.IsNullOrEmpty(this.Connection.ServerVersion), "GetDefaultSchema: the ServerVersion is null or empty!"); + + int index = this.Connection.ServerVersion.IndexOf(".", StringComparison.Ordinal); + Debug.Assert(index > 0, "GetDefaultSchema: index should be 0"); + + string versionString = this.Connection.ServerVersion.Substring(0, index); + Debug.Assert(!string.IsNullOrEmpty(versionString), "GetDefaultSchema: version string is not present!"); + + int version = int.Parse(versionString, CultureInfo.InvariantCulture); + + // For Yukon (9.0) there are non-default schemas, for MSSql schema is the same as user name. + string sql = version >= 9 ? + "select default_schema_name from sys.database_principals where name = user_name()" : + "select user_name()"; + + using (DbCommand cmd = this.Connection.CreateCommand()) + { + cmd.CommandText = sql; + string defaultSchema = cmd.ExecuteScalar() as string; + return defaultSchema; + } + } + catch (Exception e) + { + // Any problems, at least return null, which says there is no default + WriteDiagnostics("Got an exception trying to determine default schema: {0}", e); + } + + return null; + } + + #endregion + + #region Data + + /// + /// Read a table from the connection, into a DataTable + /// Code used to be in UnitTestDataManager + /// + /// The table name. + /// Columns. + /// new DataTable + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Un-tested. Leaving behavior as is.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security", Justification = "Not passed in from the user.")] +#pragma warning disable SA1202 // Elements must be ordered by access + public override DataTable ReadTable(string tableName, IEnumerable columns) +#pragma warning restore SA1202 // Elements must be ordered by access + { + using (DbDataAdapter dataAdapter = this.Factory.CreateDataAdapter()) + using (DbCommand command = this.Factory.CreateCommand()) + { + // We need to escape bad characters in table name like [Sheet1$] in Excel. + // But if table name is quoted in terms of provider, don't touch it to avoid e.g. [dbo.tables.etc]. + string quotedTableName = this.PrepareNameForSql(tableName); + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("ReadTable: data driven test: got table name from attribute: {0}", tableName); + EqtTrace.Info("ReadTable: data driven test: will use table name: {0}", tableName); + } + + command.Connection = this.Connection; + command.CommandText = string.Format(CultureInfo.InvariantCulture, "select {0} from {1}", this.GetColumnsSQL(columns), quotedTableName); + + WriteDiagnostics("ReadTable: SQL Query: {0}", command.CommandText); + dataAdapter.SelectCommand = command; + + DataTable table = new DataTable(); + table.Locale = CultureInfo.InvariantCulture; + dataAdapter.Fill(table); + + table.TableName = tableName; // Make table name in the data set the same as original table name. + return table; + } + } + + private string GetColumnsSQL(IEnumerable columns) + { + string result = null; + if (columns != null) + { + StringBuilder builder = new StringBuilder(); + foreach (string columnName in columns) + { + if (builder.Length > 0) + { + builder.Append(','); + } + + builder.Append(this.QuoteIdentifier(columnName)); + } + + result = builder.ToString(); + } + + // Return a valid list of columns, or default to * for all columns + return !string.IsNullOrEmpty(result) ? result : "*"; + } + + #endregion + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2215:Dispose methods should call base class dispose", Justification = "Un-tested. Just preserving behavior.")] +#pragma warning disable SA1202 // Elements must be ordered by access + public override void Dispose() +#pragma warning restore SA1202 // Elements must be ordered by access + { + // Ensure that we Dispose of all disposables... + if (this.commandBuilder != null) + { + this.commandBuilder.Dispose(); + } + + if (this.connection != null) + { + this.connection.Dispose(); + } + + GC.SuppressFinalize(this); + } + + #region Types + + /// + /// When querying for tables, metadata varies quite a bit from DB to DB + /// This struct encapsulates those variations + /// + protected struct SchemaMetaData + { + // Name of a table containing tables or views + public string SchemaTable; + + // Column that contains schema names, if null, unused + public string SchemaColumn; + + // Column that contains the table names + public string NameColumn; + + // Column that contains a table "type", if null, type is unchecked + public string TableTypeColumn; + + // If table type is available, it is checked to be one of the values on this list + public string[] ValidTableTypes; + + // If schema is available, it is checked to not be one of the values on this list + public string[] InvalidSchemas; + } + + /// + /// Known OLE DB providers for MS SQL and Oracle. + /// + /// + /// How Data Connection dialog maps to different providers: + /// SqlOleDb: Data Source = MS SQL, Provider = OLE DB + /// SqlOleDb.1: Data Source = Other, Provider = Microsoft OLE DB Provider for Sql Server + /// Provider=SQLOLEDB;Data Source=SqlServer;Integrated Security=SSPI;Initial Catalog=DatabaseName + /// SQLNCLI.1: Data Source = Other, Provider = Sql Native Client + /// Provider=SQLNCLI.1;Data Source=SqlServer;Integrated Security=SSPI;Initial Catalog=DatabaseName + /// + protected static class KnownOleDbProviderNames + { + internal const string SqlOleDb = "SQLOLEDB"; + internal const string MSSqlNative = "SQLNCLI"; + + // Note MSDASQL (OLE DB Provider for ODBC) is not supported by .NET. + } + + /// + /// Known ODBC drivers. + /// + /// + /// sqlsrv32.dll: Driver={SQL Server};Server=SqlServer;Database=DatabaseName;Trusted_Connection=yes + /// msorcl32.dll: Driver={Microsoft ODBC for Oracle};Server=OracleServer;Uid=user;Pwd=password + /// + protected static class KnownOdbcDrivers + { + internal const string MSSql = "sqlsrv32.dll"; + } + + #endregion + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Data/XmlDataConnection.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Data/XmlDataConnection.cs new file mode 100644 index 0000000000..fe5ad50af1 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Data/XmlDataConnection.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Data; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Security; + using System.Xml; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + + /// + /// Utility classes to access databases, and to handle quoted strings etc for XML data. + /// + internal sealed class XmlDataConnection : TestDataConnection + { + private string fileName; + + public XmlDataConnection(string fileName, List dataFolders) + : base(dataFolders) + { + Debug.Assert(!string.IsNullOrEmpty(fileName), "fileName"); + this.fileName = fileName; + } + + public override List GetDataTablesAndViews() + { + DataSet dataSet = this.LoadDataSet(true); + + if (dataSet != null) + { + List tableNames = new List(); + + int tableCount = dataSet.Tables.Count; + for (int i = 0; i < tableCount; i++) + { + DataTable table = dataSet.Tables[i]; + tableNames.Add(table.TableName); + } + + return tableNames; + } + else + { + return null; + } + } + + public override List GetColumns(string tableName) + { + DataSet dataSet = this.LoadDataSet(true); + if (dataSet != null) + { + DataTable table = dataSet.Tables[tableName]; + if (table != null) + { + List columnNames = new List(); + foreach (DataColumn column in table.Columns) + { + // Only show "normal" columns, we try to hide derived columns used as part + // of the support for relations + if (column.ColumnMapping != MappingType.Hidden) + { + columnNames.Add(column.ColumnName); + } + } + + return columnNames; + } + } + + return null; + } + + public override DataTable ReadTable(string tableName, IEnumerable columns) + { + // Reading XML is very simple... + // We do not ask it to just load a specific table, or specific columns + // so there is inefficiency since we will reload the entire file + // once for every table in it. Oh well. Reading XML is pretty quick + // compared to other forms of data source + DataSet ds = this.LoadDataSet(false); + return ds != null ? ds.Tables[tableName] : null; + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Un-tested. Preserving behavior.")] + private DataSet LoadDataSet(bool schemaOnly) + { + try + { + DataSet dataSet = new DataSet(); + dataSet.Locale = CultureInfo.CurrentCulture; + string path = this.FixPath(this.fileName) ?? Path.GetFullPath(this.fileName); + if (schemaOnly) + { + dataSet.ReadXmlSchema(path); + } + else + { + dataSet.ReadXml(path); + } + + return dataSet; + } + catch (SecurityException securityException) + { + EqtTrace.ErrorIf(EqtTrace.IsErrorEnabled, securityException.Message + " for XML data source " + this.fileName); + } + catch (XmlException xmlException) + { + EqtTrace.ErrorIf(EqtTrace.IsErrorEnabled, xmlException.Message + " for XML data source " + this.fileName); + } + catch (Exception exception) + { + // Yes, we get other exceptions too! + EqtTrace.ErrorIf(EqtTrace.IsErrorEnabled, exception.Message + " for XML data source " + this.fileName); + } + + return null; + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Deployment/AssemblyLoadWorker.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Deployment/AssemblyLoadWorker.cs new file mode 100644 index 0000000000..61fb9ea607 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Deployment/AssemblyLoadWorker.cs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Reflection; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + + /// + /// Utility function for Assembly related info + /// The caller is supposed to create AppDomain and create instance of given class in there. + /// + internal class AssemblyLoadWorker : MarshalByRefObject + { + private IAssemblyUtility assemblyUtility; + + public AssemblyLoadWorker() + : this(new AssemblyUtility()) + { + } + + internal AssemblyLoadWorker(IAssemblyUtility assemblyUtility) + { + this.assemblyUtility = assemblyUtility; + } + + /// + /// Returns the full path to the dependent assemblies of the parameter managed assembly recursively. + /// It does not report GAC assemblies. + /// + /// Path to the assembly file to load from. + /// The warnings. + /// Full path to dependent assemblies. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + public string[] GetFullPathToDependentAssemblies(string assemblyPath, out IList warnings) + { + Debug.Assert(!string.IsNullOrEmpty(assemblyPath), "assemblyPath"); + + warnings = new List(); + Assembly assembly = null; + try + { + EqtTrace.Verbose($"AssemblyLoadWorker.GetFullPathToDependentAssemblies: Reflection loading {assemblyPath}."); + + // First time we load in LoadFromContext to avoid issues. + assembly = this.assemblyUtility.ReflectionOnlyLoadFrom(assemblyPath); + } + catch (Exception ex) + { + EqtTrace.Error($"AssemblyLoadWorker.GetFullPathToDependentAssemblies: Reflection loading of {assemblyPath} failed:"); + EqtTrace.Error(ex); + + warnings.Add(ex.Message); + return new string[0]; // Otherwise just return no dependencies. + } + + Debug.Assert(assembly != null, "assembly"); + + List result = new List(); + HashSet visitedAssemblies = new HashSet(); + + visitedAssemblies.Add(assembly.FullName); + + this.ProcessChildren(assembly, result, visitedAssemblies, warnings); + + return result.ToArray(); + } + + /// + /// initialize the lifetime service. + /// + /// The . + public override object InitializeLifetimeService() + { + // Infinite. + return null; + } + + /// + /// Get the target dotNet framework string for the assembly + /// + /// Path of the assembly file + /// String representation of the target dotNet framework e.g. .NETFramework,Version=v4.0 + internal string GetTargetFrameworkVersionStringFromPath(string path) + { + if (File.Exists(path)) + { + try + { + Assembly a = this.assemblyUtility.ReflectionOnlyLoadFrom(path); + return this.GetTargetFrameworkStringFromAssembly(a); + } + catch (BadImageFormatException) + { + if (EqtTrace.IsErrorEnabled) + { + EqtTrace.Error("AssemblyHelper:GetTargetFrameworkVersionString() caught BadImageFormatException. Falling to native binary."); + } + } + catch (Exception ex) + { + if (EqtTrace.IsErrorEnabled) + { + EqtTrace.Error("AssemblyHelper:GetTargetFrameworkVersionString() Returning default. Unhandled exception: {0}.", ex); + } + } + } + + return string.Empty; + } + + /// + /// Get the target dot net framework string for the assembly + /// + /// Assembly from which target framework has to find + /// String representation of the target dot net framework e.g. .NETFramework,Version=v4.0 + private string GetTargetFrameworkStringFromAssembly(Assembly assembly) + { + string dotNetVersion = string.Empty; + foreach (CustomAttributeData data in CustomAttributeData.GetCustomAttributes(assembly)) + { + if (data?.NamedArguments?.Count > 0) + { + var declaringType = data.NamedArguments[0].MemberInfo.DeclaringType; + if (declaringType != null) + { + string attributeName = declaringType.FullName; + if (string.Equals( + attributeName, + PlatformServices.Constants.TargetFrameworkAttributeFullName, + StringComparison.OrdinalIgnoreCase)) + { + dotNetVersion = data.ConstructorArguments[0].Value.ToString(); + break; + } + } + } + } + + return dotNetVersion; + } + + /// + /// Processes references, modules, satellites. + /// Fills parameter results. + /// + /// The assembly. + /// The result. + /// The visited Assemblies. + /// The warnings. + private void ProcessChildren(Assembly assembly, IList result, ISet visitedAssemblies, IList warnings) + { + Debug.Assert(assembly != null, "assembly"); + + EqtTrace.Verbose($"AssemblyLoadWorker.GetFullPathToDependentAssemblies: Processing assembly {assembly.FullName}."); + foreach (AssemblyName reference in assembly.GetReferencedAssemblies()) + { + this.GetDependentAssembliesInternal(reference.FullName, result, visitedAssemblies, warnings); + } + + // Take care of .netmodule's. + var modules = new Module[0]; + try + { + EqtTrace.Verbose($"AssemblyLoadWorker.GetFullPathToDependentAssemblies: Getting modules of {assembly.FullName}."); + modules = assembly.GetModules(); + } + catch (FileNotFoundException e) + { + string warning = string.Format(CultureInfo.CurrentCulture, Resource.MissingDeploymentDependency, e.FileName, e.Message); + warnings.Add(warning); + return; + } + + // Assembly.GetModules() returns all modules including main one. + if (modules.Length > 1) + { + // The modules must be in the same directory as assembly that references them. + foreach (Module m in modules) + { + // Module.Name ~ MyModule.netmodule. Module.FullyQualifiedName ~ C:\dir\MyModule.netmodule. + string shortName = m.Name; + + // Note that "MyModule" may contain dots: + int dotIndex = shortName.LastIndexOf('.'); + if (dotIndex > 0) + { + shortName = shortName.Substring(0, dotIndex); + } + + if (string.Equals(shortName, assembly.GetName().Name, StringComparison.OrdinalIgnoreCase)) + { + // This is main assembly module. + continue; + } + + if (!visitedAssemblies.Add(m.Name)) + { + // The assembly was already in the set, meaning that we already visited it. + continue; + } + + if (!File.Exists(m.FullyQualifiedName)) + { + string warning = string.Format(CultureInfo.CurrentCulture, Resource.MissingDeploymentDependencyWithoutReason, m.FullyQualifiedName); + warnings.Add(warning); + continue; + } + + result.Add(m.FullyQualifiedName); + } + } + } + + /// + /// Loads in Load Context. Fills private members. + /// + /// Full or partial assembly name passed to Assembly.Load. + /// The result. + /// The visited Assemblies. + /// The warnings. + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + private void GetDependentAssembliesInternal(string assemblyString, IList result, ISet visitedAssemblies, IList warnings) + { + Debug.Assert(!string.IsNullOrEmpty(assemblyString), "assemblyString"); + + if (!visitedAssemblies.Add(assemblyString)) + { + // The assembly was already in the hashset, so we already visited it. + return; + } + + Assembly assembly = null; + try + { + EqtTrace.Verbose($"AssemblyLoadWorker.GetDependentAssembliesInternal: Reflection loading {assemblyString}."); + + string postPolicyAssembly = AppDomain.CurrentDomain.ApplyPolicy(assemblyString); + Debug.Assert(!string.IsNullOrEmpty(postPolicyAssembly), "postPolicyAssembly"); + + assembly = this.assemblyUtility.ReflectionOnlyLoad(postPolicyAssembly); + visitedAssemblies.Add(assembly.FullName); // Just in case. + } + catch (Exception ex) + { + EqtTrace.Error($"AssemblyLoadWorker.GetDependentAssembliesInternal: Reflection loading {assemblyString} failed:."); + EqtTrace.Error(ex); + + string warning = string.Format(CultureInfo.CurrentCulture, Resource.MissingDeploymentDependency, assemblyString, ex.Message); + warnings.Add(warning); + return; + } + + EqtTrace.Verbose($"AssemblyLoadWorker.GetDependentAssembliesInternal: Assembly {assemblyString} was added as dependency."); + result.Add(assembly.Location); + + this.ProcessChildren(assembly, result, visitedAssemblies, warnings); + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Deployment/DesktopTestRunDirectories.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Deployment/DesktopTestRunDirectories.cs new file mode 100644 index 0000000000..69d217aae1 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Deployment/DesktopTestRunDirectories.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment +{ + using System; + using System.Diagnostics; + using System.IO; + + /// + /// The test run directories. + /// + [Serializable] +#pragma warning disable SA1649 // File name must match first type name + public class TestRunDirectories +#pragma warning restore SA1649 // File name must match first type name + { + /// + /// The default deployment root directory. We do not want to localize it. + /// + internal const string DefaultDeploymentRootDirectory = "TestResults"; + + /// + /// The deployment in directory suffix. + /// + internal const string DeploymentInDirectorySuffix = "In"; + + /// + /// The deployment out directory suffix. + /// + internal const string DeploymentOutDirectorySuffix = "Out"; + + public TestRunDirectories(string rootDirectory) + { + Debug.Assert(!string.IsNullOrEmpty(rootDirectory), "rootDirectory"); + + this.RootDeploymentDirectory = rootDirectory; + } + + /// + /// Gets or sets the root deployment directory + /// + public string RootDeploymentDirectory { get; set; } + + /// + /// Gets the In directory + /// + public string InDirectory + { + get + { + return Path.Combine(this.RootDeploymentDirectory, DeploymentInDirectorySuffix); + } + } + + /// + /// Gets the Out directory + /// + public string OutDirectory + { + get + { + return Path.Combine(this.RootDeploymentDirectory, DeploymentOutDirectorySuffix); + } + } + + /// + /// Gets In\MachineName directory + /// + public string InMachineNameDirectory + { + get + { + return Path.Combine(Path.Combine(this.RootDeploymentDirectory, DeploymentInDirectorySuffix), Environment.MachineName); + } + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/DesktopTestSource.cs b/src/Adapter/PlatformServices.Desktop.Legacy/DesktopTestSource.cs new file mode 100644 index 0000000000..70f5da123f --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/DesktopTestSource.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System.Collections.Generic; + using System.Reflection; + + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + + using ObjectModel.Utilities; + + /// + /// This platform service is responsible for any data or operations to validate + /// the test sources provided to the adapter. + /// + public class TestSource : ITestSource + { + /// + /// Gets the set of valid extensions for sources targeting this platform. + /// + public IEnumerable ValidSourceExtensions + { + get + { + // Since desktop Platform service would also discover other platform tests on dekstop, + // this extension list needs to be updated with all platforms supported file extensions. + return new List + { + Constants.DllExtension, + Constants.PhoneAppxPackageExtension, + Constants.ExeExtension + }; + } + } + + /// + /// Verifies if the assembly provided is referenced by the source. + /// + /// The assembly name. + /// The source. + /// True if the assembly is referenced. + public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) + { + // This loads the dll in a different app domain. We can optimize this to load in the current domain since this code ould be run in a new app domain anyway. + bool? utfReference = AssemblyHelper.DoesReferencesAssembly(source, assemblyName); + + // If no reference to UTF don't run discovery. Take conservative approach. If not able to find proceed with discovery. + if (utfReference.HasValue && utfReference.Value == false) + { + return false; + } + + return true; + } + + /// + /// Gets the set of sources (dll's/exe's) that contain tests. If a source is a package(appx), return the file(dll/exe) that contains tests from it. + /// + /// Sources given to the adapter. + /// Sources that contains tests. . + /// + public IEnumerable GetTestSources(IEnumerable sources) + { + return sources; + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Friends.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Friends.cs new file mode 100644 index 0000000000..9f80b2a65f --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Friends.cs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +// Friend assemblies +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("MSTestAdapter.PlatformServices.Desktop.UnitTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("PlatformServices.Desktop.ComponentTests, PublicKey=002400000480000094000000060200000024000052534131000400000100010007d1fa57c4aed9f0a32e84aa0faefd0de9e8fd6aec8f87fb03766c834c99921eb23be79ad9d5dcc1dd9ad236132102900b723cf980957fc4e177108fc607774f29e8320e92ea05ece4e821c0a5efe8f1645c4c0c93c1ab99285d622caa652c1dfad63d745d6f2de5f17e5eaf0fc4963d261c8a12436518206dc093344d5ad293")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] \ No newline at end of file diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/PlatformServices.Desktop.Legacy.csproj b/src/Adapter/PlatformServices.Desktop.Legacy/PlatformServices.Desktop.Legacy.csproj new file mode 100644 index 0000000000..cc1f764134 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/PlatformServices.Desktop.Legacy.csproj @@ -0,0 +1,165 @@ + + + + + {F64A748C-DDBA-4B57-99F4-D9E55684A7A4} + Library + Properties + Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices + Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices + v4.5 + true + + + true + full + false + TRACE;DEBUG;CODE_ANALYSIS + prompt + 4 + + + pdbonly + true + TRACE + prompt + 4 + true + + + + + + + + + + + + + + + {bbc99a6b-4490-49dd-9c12-af2c1e95576e} + PlatformServices.Interface + False + + + {a7ea583b-a2b0-47da-a058-458f247c7575} + Extension.Desktop + False + + + {7252D9E3-267D-442C-96BC-C73AEF3241D6} + MSTest.Core + False + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + ns10RecursiveDirectoryPath.cs + + + Services\ns10MSTestSettingsProvider.cs + + + Services\ns10TestContextPropertyStrings.cs + + + Services\ns10ThreadSafeStringWriter.cs + + + Utilities\ns10Validate.cs + + + Extensions\ns13ExceptionExtensions.cs + + + Deployment\ns13DeploymentItem.cs + + + Resources\Resource.Designer.cs + True + True + Resource.resx + + + Services\ns13MSTestAdapterSettings.cs + + + Services\ns13TestDeployment.cs + + + Utilities\ns13DeploymentItemUtility.cs + + + Utilities\ns13DeploymentUtilityBase.cs + + + Utilities\ns13FileUtility.cs + + + + + + + + + + + + + + + + + + + Services\ns13TraceListener.cs + + + Services\ns13TraceListenerManager.cs + + + + + + + + + + + + + + + + + + + + + + + Resources\Resource.resx + ResXFileCodeGenerator + Resource.Designer.cs + Designer + Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices + + + + \ No newline at end of file diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Properties/AssemblyInfo.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..d45de01426 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Properties/AssemblyInfo.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PlatformServices.Desktop")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft Corporation")] +[assembly: AssemblyProduct("PlatformServices.Desktop")] +[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b0fce474-14bc-449a-91ea-a433342c0d63")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] + +// This is set by GlobalAssemblyInfo which is auto-generated due to import of TestPlatform.NonRazzle.targets +// [assembly: AssemblyVersion("1.0.0.0")] +// [assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: TypeForwardedTo(typeof(SerializableAttribute))] +[assembly: TypeForwardedTo(typeof(MarshalByRefObject))] \ No newline at end of file diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Resources/README.txt b/src/Adapter/PlatformServices.Desktop.Legacy/Resources/README.txt new file mode 100644 index 0000000000..10c5456835 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Resources/README.txt @@ -0,0 +1 @@ +This file is kept to commit Resources directory as language specific resx files needs to be copied here. \ No newline at end of file diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopAdapterTraceLogger.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopAdapterTraceLogger.cs new file mode 100644 index 0000000000..e6a662fcb5 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopAdapterTraceLogger.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + +#pragma warning disable SA1649 // SA1649FileNameMustMatchTypeName + + /// + /// A service to log any trace messages from the adapter that would be shown in *.TpTrace files. + /// + public class AdapterTraceLogger : IAdapterTraceLogger + { + /// + /// Log an error in a given format. + /// + /// The format. + /// The args. + public void LogError(string format, params object[] args) + { + if (EqtTrace.IsErrorEnabled) + { + EqtTrace.Error(this.PrependAdapterName(format), args); + } + } + + /// + /// Log a warning in a given format. + /// + /// The format. + /// The args. + public void LogWarning(string format, params object[] args) + { + if (EqtTrace.IsWarningEnabled) + { + EqtTrace.Warning(this.PrependAdapterName(format), args); + } + } + + /// + /// Log an information message in a given format. + /// + /// The format. + /// The args. + public void LogInfo(string format, params object[] args) + { + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info(this.PrependAdapterName(format), args); + } + } + + private string PrependAdapterName(string format) + { + return $"MSTest - {format}"; + } + } + +#pragma warning restore SA1649 // SA1649FileNameMustMatchTypeName +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopFileOperations.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopFileOperations.cs new file mode 100644 index 0000000000..7aa3f2ceff --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopFileOperations.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.IO; + using System.Reflection; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + + /// + /// This service is responsible for any file based operations. + /// + public class FileOperations : IFileOperations + { + /// + /// Loads an assembly into the current context. + /// + /// The name of the assembly. + /// + /// Indicates whether this should be a reflection only load. + /// + /// A handle to the loaded assembly. + public Assembly LoadAssembly(string assemblyFileName, bool isReflectionOnly) + { + if (isReflectionOnly) + { + return Assembly.ReflectionOnlyLoadFrom(assemblyFileName); + } + else + { + return Assembly.LoadFrom(assemblyFileName); + } + } + + /// + /// Gets the path to the .DLL of the assembly. + /// + /// The assembly. + /// Path to the .DLL of the assembly. + public string GetAssemblyPath(Assembly assembly) + { + return assembly.Location; + } + + /// + /// Verify if a file exists in the current context. + /// + /// The assembly file name. + /// true if the file exists. + public bool DoesFileExist(string assemblyFileName) + { + return (SafeInvoke(() => File.Exists(assemblyFileName)) as bool?) ?? false; + } + + /// + /// Creates a Navigation session for the source file. + /// This is used to get file path and line number information for its components. + /// + /// The source file. + /// A Navigation session instance for the current platform. + public object CreateNavigationSession(string source) + { + var messageFormatOnException = + string.Join("MSTestDiscoverer:DiaSession: Could not create diaSession for source:", source, ". Reason:{0}"); + return SafeInvoke(() => new DiaSession(source), messageFormatOnException) as DiaSession; + } + + /// + /// Get's the navigation data for a navigation session. + /// + /// The navigation session. + /// The class name. + /// The method name. + /// The min line number. + /// The file name. + public void GetNavigationData(object navigationSession, string className, string methodName, out int minLineNumber, out string fileName) + { + fileName = null; + minLineNumber = -1; + + var diasession = navigationSession as DiaSession; + var navigationData = diasession?.GetNavigationData(className, methodName); + + if (navigationData != null) + { + minLineNumber = navigationData.MinLineNumber; + fileName = navigationData.FileName; + } + } + + /// + /// Dispose's the navigation session instance. + /// + /// The navigation session. + public void DisposeNavigationSession(object navigationSession) + { + var diasession = navigationSession as DiaSession; + diasession?.Dispose(); + } + + /// + /// Gets the full file path of an assembly file. + /// + /// The file name. + /// The full file path. + public string GetFullFilePath(string assemblyFileName) + { + return (SafeInvoke(() => Path.GetFullPath(assemblyFileName)) as string) ?? assemblyFileName; + } + + private static object SafeInvoke(Func action, string messageFormatOnException = null) + { + try + { + return action.Invoke(); + } + catch (Exception exception) + { + if (string.IsNullOrEmpty(messageFormatOnException)) + { + messageFormatOnException = "{0}"; + } + + EqtTrace.ErrorIf(EqtTrace.IsErrorEnabled, messageFormatOnException, exception.Message); + } + + return null; + } + } + +#pragma warning restore SA1649 // SA1649FileNameMustMatchTypeName +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopReflectionOperations.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopReflectionOperations.cs new file mode 100644 index 0000000000..08af6a077e --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopReflectionOperations.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.Reflection; + + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; + + /// + /// This service is responsible for platform specific reflection operations. + /// + /// + /// The test platform triggers discovery of test assets built for all architectures including ARM on desktop. In such cases we would need to load + /// these sources in a reflection only context. Since Reflection-Only context currently is primarily prevalent in .Net Framework only, this service is required + /// so that some operations like fetching attributes in a reflection only context can be performed. + /// + public class ReflectionOperations : IReflectionOperations + { + private ReflectionUtility reflectionUtility; + + /// + /// Initializes a new instance of the class. + /// + public ReflectionOperations() + { + this.reflectionUtility = new ReflectionUtility(); + } + + /// + /// Gets all the custom attributes adorned on a member. + /// + /// The member. + /// True to inspect the ancestors of element; otherwise, false. + /// The list of attributes on the member. Empty list if none found. + public object[] GetCustomAttributes(MemberInfo memberInfo, bool inherit) + { + return this.reflectionUtility.GetCustomAttributes(memberInfo, inherit); + } + + /// + /// Gets all the custom attributes of a given type adorned on a member. + /// + /// The member info. + /// The attribute type. + /// True to inspect the ancestors of element; otherwise, false. + /// The list of attributes on the member. Empty list if none found. + public object[] GetCustomAttributes(MemberInfo memberInfo, Type type, bool inherit) + { + return this.reflectionUtility.GetCustomAttributes(memberInfo, type, inherit); + } + + /// + /// Gets all the custom attributes of a given type on an assembly. + /// + /// The assembly. + /// The attribute type. + /// The list of attributes of the given type on the member. Empty list if none found. + public object[] GetCustomAttributes(Assembly assembly, Type type) + { + return this.reflectionUtility.GetCustomAttributes(assembly, type); + } + } + +#pragma warning restore SA1649 // SA1649FileNameMustMatchTypeName +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestContextImplementation.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestContextImplementation.cs new file mode 100644 index 0000000000..8a9481968f --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestContextImplementation.cs @@ -0,0 +1,508 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Data; + using System.Data.Common; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Threading; + + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface.ObjectModel; + + using UTF = Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// Internal implementation of TestContext exposed to the user. + /// The virtual string properties of the TestContext are retrieved from the property dictionary + /// like GetProperty<string>("TestName") or GetProperty<string>("FullyQualifiedTestClassName"); + /// + public class TestContextImplementation : UTF.TestContext, ITestContext + { + /// + /// List of result files associated with the test + /// + private IList testResultFiles; + + /// + /// Properties + /// + private IDictionary properties; + + /// + /// Unit test outcome + /// + private UTF.UnitTestOutcome outcome; + + /// + /// Writer on which the messages given by the user should be written + /// + private StringWriter stringWriter; + + /// + /// Specifies whether the writer is disposed or not + /// + private bool stringWriterDisposed = false; + + /// + /// Test Method + /// + private ITestMethod testMethod; + + /// + /// DB connection for test context + /// + private DbConnection dbConnection; + + /// + /// Data row for TestContext + /// + private DataRow dataRow; + + /// + /// Initializes a new instance of the class. + /// + /// The test method. + /// The writer where diagnostic messages are written to. + /// Properties/configuration passed in. + public TestContextImplementation(ITestMethod testMethod, StringWriter stringWriter, IDictionary properties) + { + Debug.Assert(testMethod != null, "TestMethod is not null"); + Debug.Assert(stringWriter != null, "StringWriter is not null"); + Debug.Assert(properties != null, "properties is not null"); + + this.testMethod = testMethod; + this.stringWriter = stringWriter; + this.properties = new Dictionary(properties); + this.CancellationTokenSource = new CancellationTokenSource(); + this.InitializeProperties(); + + this.testResultFiles = new List(); + } + + #region TestContext impl + + /// + public override UTF.UnitTestOutcome CurrentTestOutcome + { + get + { + return this.outcome; + } + } + + /// + public override DbConnection DataConnection + { + get + { + return this.dbConnection; + } + } + + /// + public override DataRow DataRow + { + get + { + return this.dataRow; + } + } + + /// + public override IDictionary Properties + { + get + { + return this.properties as IDictionary; + } + } + + /// + public override string TestRunDirectory + { + get + { + return this.GetStringPropertyValue(TestContextPropertyStrings.TestRunDirectory); + } + } + + /// + public override string DeploymentDirectory + { + get + { + return this.GetStringPropertyValue(TestContextPropertyStrings.DeploymentDirectory); + } + } + + /// + public override string ResultsDirectory + { + get + { + return this.GetStringPropertyValue(TestContextPropertyStrings.ResultsDirectory); + } + } + + /// + public override string TestRunResultsDirectory + { + get + { + return this.GetStringPropertyValue(TestContextPropertyStrings.TestRunResultsDirectory); + } + } + + /// + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", Justification = "TestResultsDirectory is what we need.")] + public override string TestResultsDirectory + { + get + { + // In MSTest, it is actually "In\697105f7-004f-42e8-bccf-eb024870d3e9\User1", but + // we are setting it to "In" only because MSTest does not create this directory. + return this.GetStringPropertyValue(TestContextPropertyStrings.TestResultsDirectory); + } + } + + /// + public override string TestDir + { + get + { + return this.GetStringPropertyValue(TestContextPropertyStrings.TestDir); + } + } + + /// + public override string TestDeploymentDir + { + get + { + return this.GetStringPropertyValue(TestContextPropertyStrings.TestDeploymentDir); + } + } + + /// + public override string TestLogsDir + { + get + { + return this.GetStringPropertyValue(TestContextPropertyStrings.TestLogsDir); + } + } + + /// + public override string FullyQualifiedTestClassName + { + get + { + return this.GetStringPropertyValue(TestContextPropertyStrings.FullyQualifiedTestClassName); + } + } + + /// + public override string ManagedType + { + get + { + return this.GetStringPropertyValue(TestContextPropertyStrings.ManagedType); + } + } + + /// + public override string ManagedMethod + { + get + { + return this.GetStringPropertyValue(TestContextPropertyStrings.ManagedMethod); + } + } + + /// + public override string TestName + { + get + { + return this.GetStringPropertyValue(TestContextPropertyStrings.TestName); + } + } + + public UTF.TestContext Context + { + get + { + return this as UTF.TestContext; + } + } + + /// + public override void AddResultFile(string fileName) + { + if (string.IsNullOrEmpty(fileName)) + { + throw new ArgumentException(Resource.Common_CannotBeNullOrEmpty, nameof(fileName)); + } + + this.testResultFiles.Add(Path.GetFullPath(fileName)); + } + + /// + public override void BeginTimer(string timerName) + { + throw new NotSupportedException(); + } + + /// + public override void EndTimer(string timerName) + { + throw new NotSupportedException(); + } + + /// + /// When overridden in a derived class, used to write trace messages while the + /// test is running. + /// + /// The formatted string that contains the trace message. + public override void Write(string message) + { + if (this.stringWriterDisposed) + { + return; + } + + try + { + var msg = message?.Replace("\0", "\\0"); + this.stringWriter.Write(msg); + } + catch (ObjectDisposedException) + { + this.stringWriterDisposed = true; + } + } + + /// + /// When overridden in a derived class, used to write trace messages while the + /// test is running. + /// + /// The string that contains the trace message. + /// Arguments to add to the trace message. + public override void Write(string format, params object[] args) + { + if (this.stringWriterDisposed) + { + return; + } + + try + { + string message = string.Format(CultureInfo.CurrentCulture, format?.Replace("\0", "\\0"), args); + this.stringWriter.Write(message); + } + catch (ObjectDisposedException) + { + this.stringWriterDisposed = true; + } + } + + /// + /// When overridden in a derived class, used to write trace messages while the + /// test is running. + /// + /// The formatted string that contains the trace message. + public override void WriteLine(string message) + { + if (this.stringWriterDisposed) + { + return; + } + + try + { + var msg = message?.Replace("\0", "\\0"); + this.stringWriter.WriteLine(msg); + } + catch (ObjectDisposedException) + { + this.stringWriterDisposed = true; + } + } + + /// + /// When overridden in a derived class, used to write trace messages while the + /// test is running. + /// + /// The string that contains the trace message. + /// Arguments to add to the trace message. + public override void WriteLine(string format, params object[] args) + { + if (this.stringWriterDisposed) + { + return; + } + + try + { + string message = string.Format(CultureInfo.CurrentCulture, format?.Replace("\0", "\\0"), args); + this.stringWriter.WriteLine(message); + } + catch (ObjectDisposedException) + { + this.stringWriterDisposed = true; + } + } + + /// + /// Set the unit-test outcome + /// + /// The test outcome. + public void SetOutcome(UTF.UnitTestOutcome outcome) + { + this.outcome = ToUTF(outcome); + } + + /// + /// Set data row for particular run of TestMethod. + /// + /// data row. + public void SetDataRow(object dataRow) + { + this.dataRow = dataRow as DataRow; + } + + /// + /// Set connection for TestContext + /// + /// db Connection. + public void SetDataConnection(object dbConnection) + { + this.dbConnection = dbConnection as DbConnection; + } + + /// + /// Returns whether property with parameter name is present or not + /// + /// The property name. + /// The property value. + /// True if found. + public bool TryGetPropertyValue(string propertyName, out object propertyValue) + { + if (this.properties == null) + { + propertyValue = null; + return false; + } + + return this.properties.TryGetValue(propertyName, out propertyValue); + } + + /// + /// Adds the parameter name/value pair to property bag + /// + /// The property name. + /// The property value. + public void AddProperty(string propertyName, string propertyValue) + { + if (this.properties == null) + { + this.properties = new Dictionary(); + } + + this.properties.Add(propertyName, propertyValue); + } + + /// + /// Result files attached + /// + /// Results files generated in run. + public IList GetResultFiles() + { + if (!this.testResultFiles.Any()) + { + return null; + } + + List results = this.testResultFiles.ToList(); + + // clear the result files to handle data driven tests + this.testResultFiles.Clear(); + + return results; + } + + /// + /// Gets messages from the testContext writeLines + /// + /// The test context messages added so far. + public string GetDiagnosticMessages() + { + return this.stringWriter.ToString(); + } + + /// + /// Clears the previous testContext writeline messages. + /// + public void ClearDiagnosticMessages() + { + var sb = this.stringWriter.GetStringBuilder(); + sb?.Remove(0, sb.Length); + } + + #endregion + + /// + /// Converts the parameter outcome to UTF outcome + /// + /// The UTF outcome. + /// test outcome + private static UTF.UnitTestOutcome ToUTF(UTF.UnitTestOutcome outcome) + { + switch (outcome) + { + case UTF.UnitTestOutcome.Error: + case UTF.UnitTestOutcome.Failed: + case UTF.UnitTestOutcome.Inconclusive: + case UTF.UnitTestOutcome.Passed: + case UTF.UnitTestOutcome.Timeout: + case UTF.UnitTestOutcome.InProgress: + return outcome; + + default: + Debug.Fail("Unknown outcome " + outcome); + return UTF.UnitTestOutcome.Unknown; + } + } + + /// + /// Helper to safely fetch a property value. + /// + /// Property Name + /// Property value + private string GetStringPropertyValue(string propertyName) + { + this.properties.TryGetValue(propertyName, out var propertyValue); + return propertyValue as string; + } + + /// + /// Helper to initialize the properties. + /// + private void InitializeProperties() + { + this.properties[TestContextPropertyStrings.FullyQualifiedTestClassName] = this.testMethod.FullClassName; + this.properties[TestContextPropertyStrings.ManagedType] = this.testMethod.ManagedTypeName; + this.properties[TestContextPropertyStrings.ManagedMethod] = this.testMethod.ManagedMethodName; + this.properties[TestContextPropertyStrings.TestName] = this.testMethod.Name; + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestDataSource.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestDataSource.cs new file mode 100644 index 0000000000..20a4d589b5 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestDataSource.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.Collections.Generic; + using System.Configuration; + using System.Data; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Data; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + + using UTF = Microsoft.VisualStudio.TestTools.UnitTesting; + +#pragma warning disable SA1649 // SA1649FileNameMustMatchTypeName + + /// + /// The platform service that provides values from data source when data driven tests are run. + /// + /// + /// NOTE NOTE NOTE: This platform service refers to the inbox UTF extension assembly for UTF.ITestMethod which can only be loadable inside of the app domain that discovers/runs + /// the tests since it can only be found at the test output directory. DO NOT call into this platform service outside of the appdomain context if you do not want to hit + /// a ReflectionTypeLoadException. + /// + public class TestDataSource : ITestDataSource + { + public IEnumerable GetData(UTF.ITestMethod testMethodInfo, ITestContext testContext) + { + // Figure out where (as well as the current directory) we could look for data files + // for unit tests this means looking at the location of the test itself + List dataFolders = new List(); + dataFolders.Add(Path.GetDirectoryName(new Uri(testMethodInfo.MethodInfo.Module.Assembly.CodeBase).LocalPath)); + + List dataRowResults = new List(); + + // Connect to data source. + TestDataConnectionFactory factory = new TestDataConnectionFactory(); + + string providerNameInvariant; + string connectionString; + string tableName; + UTF.DataAccessMethod dataAccessMethod; + + try + { + this.GetConnectionProperties(testMethodInfo.GetAttributes(false)[0], out providerNameInvariant, out connectionString, out tableName, out dataAccessMethod); + } + catch (Exception ex) + { + throw ex; + } + + try + { + using (TestDataConnection connection = factory.Create(providerNameInvariant, connectionString, dataFolders)) + { + DataTable table = connection.ReadTable(tableName, null); + DataRow[] rows = table.Select(); + Debug.Assert(rows != null, "rows should not be null."); + + // check for row length is 0 + if (rows.Length == 0) + { + return null; + } + + IEnumerable permutation = this.GetPermutation(dataAccessMethod, rows.Length); + + object[] rowsAfterPermutation = new object[rows.Length]; + int index = 0; + foreach (int rowIndex in permutation) + { + rowsAfterPermutation[index++] = rows[rowIndex]; + } + + testContext.SetDataConnection(connection.Connection); + return rowsAfterPermutation; + } + } + catch (Exception ex) + { + string message = ExceptionExtensions.GetExceptionMessage(ex); + throw new Exception(string.Format(CultureInfo.CurrentCulture, Resource.UTA_ErrorDataConnectionFailed, ex.Message), ex); + } + } + + /// + /// Get permutations for data row access + /// + /// The data access method. + /// Number of permutations. + /// Permutations. + private IEnumerable GetPermutation(UTF.DataAccessMethod dataAccessMethod, int length) + { + switch (dataAccessMethod) + { + case UTF.DataAccessMethod.Sequential: + return new SequentialIntPermutation(length); + + case UTF.DataAccessMethod.Random: + return new RandomIntPermutation(length); + + default: + Debug.Fail("Unknown DataAccessMehtod: " + dataAccessMethod); + return new SequentialIntPermutation(length); + } + } + + /// + /// Get connection property based on DataSourceAttribute. If its in config file then read it from config. + /// + /// The dataSourceAttribute. + /// The provider name. + /// The connection string. + /// The table name. + /// The data access method. + private void GetConnectionProperties(UTF.DataSourceAttribute dataSourceAttribute, out string providerNameInvariant, out string connectionString, out string tableName, out UTF.DataAccessMethod dataAccessMethod) + { + if (string.IsNullOrEmpty(dataSourceAttribute.DataSourceSettingName) == false) + { + UTF.DataSourceElement elem = UTF.TestConfiguration.ConfigurationSection.DataSources[dataSourceAttribute.DataSourceSettingName]; + if (elem == null) + { + throw new Exception(string.Format(CultureInfo.CurrentCulture, Resource.UTA_DataSourceConfigurationSectionMissing, dataSourceAttribute.DataSourceSettingName)); + } + + providerNameInvariant = ConfigurationManager.ConnectionStrings[elem.ConnectionString].ProviderName; + connectionString = ConfigurationManager.ConnectionStrings[elem.ConnectionString].ConnectionString; + tableName = elem.DataTableName; + dataAccessMethod = (UTF.DataAccessMethod)Enum.Parse(typeof(UTF.DataAccessMethod), elem.DataAccessMethod); + } + else + { + providerNameInvariant = dataSourceAttribute.ProviderInvariantName; + connectionString = dataSourceAttribute.ConnectionString; + tableName = dataSourceAttribute.TableName; + dataAccessMethod = dataSourceAttribute.DataAccessMethod; + } + } + } + +#pragma warning restore SA1649 // SA1649FileNameMustMatchTypeName +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestSource.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestSource.cs new file mode 100644 index 0000000000..f00b3095e3 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestSource.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System.Collections.Generic; + using System.Reflection; + + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; + +#pragma warning disable SA1649 // SA1649FileNameMustMatchTypeName + + /// + /// This platform service is responsible for any data or operations to validate + /// the test sources provided to the adapter. + /// + public class TestSource : ITestSource + { + /// + /// Gets the set of valid extensions for sources targeting this platform. + /// + public IEnumerable ValidSourceExtensions + { + get + { + // Since desktop Platform service would also discover other platform tests on desktop, + // this extension list needs to be updated with all platforms supported file extensions. + return new List + { + Constants.DllExtension, + Constants.PhoneAppxPackageExtension, + Constants.ExeExtension + }; + } + } + + /// + /// Verifies if the assembly provided is referenced by the source. + /// + /// The assembly name. + /// The source. + /// True if the assembly is referenced. + public bool IsAssemblyReferenced(AssemblyName assemblyName, string source) + { + // This loads the dll in a different app domain. We can optimize this to load in the current domain since this code could be run in a new app domain anyway. + bool? utfReference = AssemblyHelper.DoesReferencesAssembly(source, assemblyName); + + // If no reference to UTF don't run discovery. Take conservative approach. If not able to find proceed with discovery. + if (utfReference.HasValue && utfReference.Value == false) + { + return false; + } + + return true; + } + + /// + /// Gets the set of sources (dll's/exe's) that contain tests. If a source is a package(appx), return the file(dll/exe) that contains tests from it. + /// + /// Sources given to the adapter. + /// Sources that contains tests. . + public IEnumerable GetTestSources(IEnumerable sources) + { + return sources; + } + } + +#pragma warning restore SA1649 // SA1649FileNameMustMatchTypeName +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestSourceHost.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestSourceHost.cs new file mode 100644 index 0000000000..227d338194 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopTestSourceHost.cs @@ -0,0 +1,370 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Reflection; + + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter; + using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities; + + /// + /// A host that loads the test source.This can be in isolation for desktop using an AppDomain or just loading the source in the current context. + /// + public class TestSourceHost : ITestSourceHost + { + /// + /// Child AppDomain used to discover/execute tests + /// + private AppDomain domain; + + /// + /// Assembly resolver used in the current app-domain + /// + private AssemblyResolver parentDomainAssemblyResolver; + + /// + /// Assembly resolver used in the new child app-domain created for discovery/execution + /// + private AssemblyResolver childDomainAssemblyResolver; + + /// + /// Determines whether child-appdomain needs to be created based on DisableAppDomain Flag set in runsettings + /// + private bool isAppDomainCreationDisabled; + + private string sourceFileName; + private IRunSettings runSettings; + private IFrameworkHandle frameworkHandle; + + private string currentDirectory = null; + private IAppDomain appDomain; + + private string targetFrameworkVersion; + + /// + /// Initializes a new instance of the class. + /// + /// The source file name. + /// The run-settings provided for this session. + /// The handle to the test platform. + public TestSourceHost(string sourceFileName, IRunSettings runSettings, IFrameworkHandle frameworkHandle) + : this(sourceFileName, runSettings, frameworkHandle, new AppDomainWrapper()) + { + } + + internal TestSourceHost(string sourceFileName, IRunSettings runSettings, IFrameworkHandle frameworkHandle, IAppDomain appDomain) + { + this.sourceFileName = sourceFileName; + this.runSettings = runSettings; + this.frameworkHandle = frameworkHandle; + this.appDomain = appDomain; + + // Set the environment context. + this.SetContext(sourceFileName); + + // Set isAppDomainCreationDisabled flag + this.isAppDomainCreationDisabled = (this.runSettings != null) && MSTestAdapterSettings.IsAppDomainCreationDisabled(this.runSettings.SettingsXml); + } + + internal AppDomain AppDomain + { + get + { + return this.domain; + } + } + + /// + /// Setup the isolation host. + /// + public void SetupHost() + { + List resolutionPaths = this.GetResolutionPaths(this.sourceFileName, VSInstallationUtilities.IsCurrentProcessRunningInPortableMode()); + + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("DesktopTestSourceHost.SetupHost(): Creating assembly resolver with resolution paths {0}.", string.Join(",", resolutionPaths.ToArray())); + } + + // Case when DisableAppDomain setting is present in runsettings and no child-appdomain needs to be created + if (this.isAppDomainCreationDisabled) + { + this.parentDomainAssemblyResolver = new AssemblyResolver(resolutionPaths); + this.AddSearchDirectoriesSpecifiedInRunSettingsToAssemblyResolver(this.parentDomainAssemblyResolver, Path.GetDirectoryName(this.sourceFileName)); + } + + // Create child-appdomain and set assembly resolver on it + else + { + // Setup app-domain + var appDomainSetup = new AppDomainSetup(); + this.targetFrameworkVersion = this.GetTargetFrameworkVersionString(this.sourceFileName); + AppDomainUtilities.SetAppDomainFrameworkVersionBasedOnTestSource(appDomainSetup, this.targetFrameworkVersion); + + appDomainSetup.ApplicationBase = this.GetAppBaseAsPerPlatform(); + var configFile = this.GetConfigFileForTestSource(this.sourceFileName); + AppDomainUtilities.SetConfigurationFile(appDomainSetup, configFile); + + EqtTrace.Info("DesktopTestSourceHost.SetupHost(): Creating app-domain for source {0} with application base path {1}.", this.sourceFileName, appDomainSetup.ApplicationBase); + + string domainName = string.Format("TestSourceHost: Enumerating source ({0})", this.sourceFileName); + this.domain = this.appDomain.CreateDomain(domainName, null, appDomainSetup); + + // Load objectModel before creating assembly resolver otherwise in 3.5 process, we run into a recursive assembly resolution + // which is trigged by AppContainerUtilities.AttachEventToResolveWinmd method. + EqtTrace.SetupRemoteEqtTraceListeners(this.domain); + + // Add an assembly resolver in the child app-domain... + Type assemblyResolverType = typeof(AssemblyResolver); + + EqtTrace.Info("DesktopTestSourceHost.SetupHost(): assemblyenumerator location: {0} , fullname: {1} ", assemblyResolverType.Assembly.Location, assemblyResolverType.FullName); + + var resolver = AppDomainUtilities.CreateInstance( + this.domain, + assemblyResolverType, + new object[] { resolutionPaths }); + + EqtTrace.Info( + "DesktopTestSourceHost.SetupHost(): resolver type: {0} , resolve type assembly: {1} ", + resolver.GetType().FullName, + resolver.GetType().Assembly.Location); + + this.childDomainAssemblyResolver = (AssemblyResolver)resolver; + + this.AddSearchDirectoriesSpecifiedInRunSettingsToAssemblyResolver(this.childDomainAssemblyResolver, Path.GetDirectoryName(this.sourceFileName)); + } + } + + /// + /// Creates an instance of a given type in the test source host. + /// + /// The type that needs to be created in the host. + /// The arguments to pass to the constructor. + /// This array of arguments must match in number, order, and type the parameters of the constructor to invoke. + /// Pass in null for a constructor with no arguments. + /// + /// An instance of the type created in the host. + /// If a type is to be created in isolation then it needs to be a MarshalByRefObject. + public object CreateInstanceForType(Type type, object[] args) + { + // Honor DisableAppDomain setting if it is present in runsettings + if (this.isAppDomainCreationDisabled) + { + return Activator.CreateInstance(type, args); + } + + return AppDomainUtilities.CreateInstance(this.domain, type, args); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + if (this.parentDomainAssemblyResolver != null) + { + this.parentDomainAssemblyResolver.Dispose(); + this.parentDomainAssemblyResolver = null; + } + + if (this.childDomainAssemblyResolver != null) + { + this.childDomainAssemblyResolver.Dispose(); + this.childDomainAssemblyResolver = null; + } + + if (this.domain != null) + { + try + { + this.appDomain.Unload(this.domain); + } + catch (Exception exception) + { + // This happens usually when a test spawns off a thread and fails to clean it up. + EqtTrace.Error("DesktopTestSourceHost.Dispose(): The app domain running tests could not be unloaded. Exception: {0}", exception); + + if (this.frameworkHandle != null) + { + // Let the test platform know that it should tear down the test host process + // since we have issues in unloading appdomain. We do so to avoid any assembly locking issues. + this.frameworkHandle.EnableShutdownAfterTestRun = true; + + EqtTrace.Verbose("DesktopTestSourceHost.Dispose(): Notifying the test platform that the test host process should be shut down because the app domain running tests could not be unloaded successfully."); + } + } + + this.domain = null; + } + + this.ResetContext(); + + GC.SuppressFinalize(this); + } + + /// + /// Gets child-domain's appbase to point to appropriate location. + /// + /// Appbase path that should be set for child appdomain + internal string GetAppBaseAsPerPlatform() + { + // The below logic of preferential setting the appdomains appbase is needed because: + // 1. We set this to the location of the test source if it is built for Full CLR -> Ideally this needs to be done in all situations. + // 2. We set this to the location where the current adapter is being picked up from for UWP and .Net Core scenarios -> This needs to be + // different especially for UWP because we use the desktop adapter(from %temp%\VisualStudioTestExplorerExtensions) itself for test discovery + // in IDE scenarios. If the app base is set to the test source location, discovery will not work because we drop the + // UWP platform service assembly at the test source location and since CLR starts looking for assemblies from the app base location, + // there would be a mismatch of platform service assemblies during discovery. + if (this.targetFrameworkVersion.Contains(PlatformServices.Constants.DotNetFrameWorkStringPrefix)) + { + return Path.GetDirectoryName(this.sourceFileName) ?? Path.GetDirectoryName(typeof(TestSourceHost).Assembly.Location); + } + else + { + return Path.GetDirectoryName(typeof(TestSourceHost).Assembly.Location); + } + } + + /// + /// Gets the probing paths to load the test assembly dependencies. + /// + /// + /// The source File Name. + /// + /// + /// True if running in portable mode else false. + /// + /// + /// A list of path. + /// + internal virtual List GetResolutionPaths(string sourceFileName, bool isPortableMode) + { + List resolutionPaths = new List(); + + // Add path of test assembly in resolution path. Mostly will be used for resolving winmd. + resolutionPaths.Add(Path.GetDirectoryName(sourceFileName)); + + if (!isPortableMode) + { + EqtTrace.Info("DesktopTestSourceHost.GetResolutionPaths(): Not running in portable mode"); + + string pathToPublicAssemblies = VSInstallationUtilities.PathToPublicAssemblies; + if (!StringUtilities.IsNullOrWhiteSpace(pathToPublicAssemblies)) + { + resolutionPaths.Add(pathToPublicAssemblies); + } + + string pathToPrivateAssemblies = VSInstallationUtilities.PathToPrivateAssemblies; + if (!StringUtilities.IsNullOrWhiteSpace(pathToPrivateAssemblies)) + { + resolutionPaths.Add(pathToPrivateAssemblies); + } + } + + // Adding adapter folder to resolution paths + if (!resolutionPaths.Contains(Path.GetDirectoryName(typeof(TestSourceHost).Assembly.Location))) + { + resolutionPaths.Add(Path.GetDirectoryName(typeof(TestSourceHost).Assembly.Location)); + } + + // Adding TestPlatform folder to resolution paths + if (!resolutionPaths.Contains(Path.GetDirectoryName(typeof(AssemblyHelper).Assembly.Location))) + { + resolutionPaths.Add(Path.GetDirectoryName(typeof(AssemblyHelper).Assembly.Location)); + } + + return resolutionPaths; + } + + internal virtual string GetTargetFrameworkVersionString(string sourceFileName) + { + return AppDomainUtilities.GetTargetFrameworkVersionString(sourceFileName); + } + + private string GetConfigFileForTestSource(string sourceFileName) + { + return new DeploymentUtility().GetConfigFile(sourceFileName); + } + + /// + /// Sets context required for running tests. + /// + /// + /// source parameter used for setting context + /// + private void SetContext(string source) + { + if (string.IsNullOrEmpty(source)) + { + return; + } + + Exception setWorkingDirectoryException = null; + this.currentDirectory = Environment.CurrentDirectory; + + try + { + Environment.CurrentDirectory = Path.GetDirectoryName(source); + EqtTrace.Info("MSTestExecutor: Changed the working directory to {0}", Environment.CurrentDirectory); + } + catch (IOException ex) + { + setWorkingDirectoryException = ex; + } + catch (System.Security.SecurityException ex) + { + setWorkingDirectoryException = ex; + } + + if (setWorkingDirectoryException != null) + { + EqtTrace.Error("MSTestExecutor.SetWorkingDirectory: Failed to set the working directory to '{0}'. {1}", Path.GetDirectoryName(source), setWorkingDirectoryException); + } + } + + /// + /// Resets the context as it was before calling SetContext() + /// + private void ResetContext() + { + if (!string.IsNullOrEmpty(this.currentDirectory)) + { + Environment.CurrentDirectory = this.currentDirectory; + } + } + + private void AddSearchDirectoriesSpecifiedInRunSettingsToAssemblyResolver(AssemblyResolver assemblyResolver, string baseDirectory) + { + // Check if user specified any adapter settings + MSTestAdapterSettings adapterSettings = MSTestSettingsProvider.Settings; + + if (adapterSettings != null) + { + try + { + var additionalSearchDirectories = adapterSettings.GetDirectoryListWithRecursiveProperty(baseDirectory); + if (additionalSearchDirectories?.Count > 0) + { + assemblyResolver.AddSearchDirectoriesFromRunSetting(additionalSearchDirectories); + } + } + catch (Exception exception) + { + EqtTrace.Error( + "DesktopTestSourceHost.AddSearchDirectoriesSpecifiedInRunSettingsToAssemblyResolver(): Exception hit while trying to set assembly resolver for domain. Exception : {0} \n Message : {1}", + exception, + exception.Message); + } + } + } +} + +#pragma warning restore SA1649 // SA1649FileNameMustMatchTypeName +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopThreadOperations.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopThreadOperations.cs new file mode 100644 index 0000000000..d6b17e43a8 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Services/DesktopThreadOperations.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.Reflection; + using System.Threading; + + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; + +#pragma warning disable SA1649 // SA1649FileNameMustMatchTypeName + + /// + /// This service is responsible for any Async operations specific to a platform. + /// + public class ThreadOperations : IThreadOperations + { + /// + /// Execute the given action synchronously on a background thread in the given timeout. + /// + /// The action to execute. + /// Timeout for the specified action in milliseconds. + /// Token to cancel the execution + /// Returns true if the action executed before the timeout. returns false otherwise. + public bool Execute(Action action, int timeout, CancellationToken cancelToken) + { + bool executionAborted = false; + Thread executionThread = new Thread(new ThreadStart(action)) + { + IsBackground = true, + Name = "MSTestAdapter Thread" + }; + + executionThread.SetApartmentState(Thread.CurrentThread.GetApartmentState()); + executionThread.Start(); + cancelToken.Register(() => + { + executionAborted = true; + AbortThread(executionThread); + }); + + if (JoinThread(timeout, executionThread)) + { + if (executionAborted) + { + return false; + } + + // Successfully completed + return true; + } + else if (executionAborted) + { + // Execution aborted due to user choice + return false; + } + else + { + // Timed out + AbortThread(executionThread); + return false; + } + } + + /// + /// Execute an action with handling for Thread Aborts (if possible) so the main thread of the adapter does not die. + /// + /// The action to execute. + public void ExecuteWithAbortSafety(Action action) + { + try + { + action.Invoke(); + } + catch (ThreadAbortException exception) + { + Thread.ResetAbort(); + + // Throwing an exception so that the test is marked as failed. + // This is a TargetInvocation exception because we want just the ThreadAbort exception to be shown to the user and not something we create here. + // TargetInvocation exceptions are stripped off by the test failure handler surfacing the actual exception. + throw new TargetInvocationException(exception); + } + } + + private static bool JoinThread(int timeout, Thread executionThread) + { + try + { + return executionThread.Join(timeout); + } + catch (ThreadStateException) + { + // Join was called on a thread not started + } + + return false; + } + + private static void AbortThread(Thread executionThread) + { + try + { + // Abort test thread after timeout. + executionThread.Abort(); + } + catch (ThreadStateException) + { + // Catch and discard ThreadStateException. If Abort is called on a thread that has been suspended, + // a ThreadStateException is thrown in the thread that called Abort, + // and AbortRequested is added to the ThreadState property of the thread being aborted. + // A ThreadAbortException is not thrown in the suspended thread until Resume is called. + } + } + } +#pragma warning restore SA1649 // SA1649FileNameMustMatchTypeName +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/AppDomainUtilities.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/AppDomainUtilities.cs new file mode 100644 index 0000000000..7be4ff734c --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/AppDomainUtilities.cs @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Reflection; + + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + + /// + /// Utilities for AppDomain + /// + internal static class AppDomainUtilities + { + private const string ObjectModelVersionBuiltAgainst = "11.0.0.0"; + + private static Version defaultVersion = new Version(); + private static Version version45 = new Version("4.5"); + + private static XmlUtilities xmlUtilities = null; + + /// + /// Gets or sets the Xml Utilities instance. + /// + internal static XmlUtilities XmlUtilities + { + get + { + if (xmlUtilities == null) + { + xmlUtilities = new XmlUtilities(); + } + + return xmlUtilities; + } + + set + { + xmlUtilities = value; + } + } + + /// + /// Set the target framework for app domain setup if target framework of dll is > 4.5 + /// + /// AppdomainSetup for app domain creation + /// The target framework version of the test source. + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Reviewed. Suppression is OK here.")] + internal static void SetAppDomainFrameworkVersionBasedOnTestSource(AppDomainSetup setup, string frameworkVersionString) + { + if (GetTargetFrameworkVersionFromVersionString(frameworkVersionString).CompareTo(version45) > 0) + { + PropertyInfo pInfo = typeof(AppDomainSetup).GetProperty(PlatformServices.Constants.TargetFrameworkName); + if (pInfo != null) + { + pInfo.SetValue(setup, frameworkVersionString, null); + } + } + } + + /// + /// Get target framework version string from the given dll + /// + /// + /// The path of the dll + /// + /// + /// Framework string + /// TODO: Need to add components/E2E tests to cover these scenarios. + /// + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "Reviewed. Suppression is OK here.")] + internal static string GetTargetFrameworkVersionString(string testSourcePath) + { + AppDomainSetup appDomainSetup = new AppDomainSetup(); + + appDomainSetup.LoaderOptimization = LoaderOptimization.MultiDomainHost; + + AppDomainUtilities.SetConfigurationFile(appDomainSetup, new DeploymentUtility().GetConfigFile(testSourcePath)); + + if (File.Exists(testSourcePath)) + { + AppDomain appDomain = null; + + try + { + appDomain = AppDomain.CreateDomain("Framework Version String Domain", null, appDomainSetup); + + // Wire the eqttrace logs in this domain to the current domain. + EqtTrace.SetupRemoteEqtTraceListeners(appDomain); + + // Add an assembly resolver to resolve ObjectModel or any Test Platform dependencies. + // Not moving to IMetaDataImport APIs because the time taken for this operation is <20 ms and + // IMetaDataImport needs COM registration which is not a guarantee in Dev15. + var assemblyResolverType = typeof(AssemblyResolver); + + var resolutionPaths = new List { Path.GetDirectoryName(typeof(TestCase).Assembly.Location) }; + resolutionPaths.Add(Path.GetDirectoryName(testSourcePath)); + + AppDomainUtilities.CreateInstance( + appDomain, + assemblyResolverType, + new object[] { resolutionPaths }); + + var assemblyLoadWorker = + (AssemblyLoadWorker)AppDomainUtilities.CreateInstance( + appDomain, + typeof(AssemblyLoadWorker), + null); + + return assemblyLoadWorker.GetTargetFrameworkVersionStringFromPath(testSourcePath); + } + catch (Exception exception) + { + if (EqtTrace.IsErrorEnabled) + { + EqtTrace.Error(exception); + } + } + finally + { + if (appDomain != null) + { + AppDomain.Unload(appDomain); + } + } + } + + return string.Empty; + } + + /// + /// Set configuration file on the parameter appDomain. + /// + /// The app Domain Setup. + /// The test Source Config File. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + internal static void SetConfigurationFile(AppDomainSetup appDomainSetup, string testSourceConfigFile) + { + if (!string.IsNullOrEmpty(testSourceConfigFile)) + { + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("UnitTestAdapter: Using configuration file {0} to setup appdomain for test source {1}.", testSourceConfigFile, Path.GetFileNameWithoutExtension(testSourceConfigFile)); + } + + appDomainSetup.ConfigurationFile = Path.GetFullPath(testSourceConfigFile); + + try + { + // Add redirection of the built 11.0 Object Model assembly to the current version if that is not 11.0 + var currentVersionOfObjectModel = typeof(TestCase).Assembly.GetName().Version.ToString(); + if (!string.Equals(currentVersionOfObjectModel, ObjectModelVersionBuiltAgainst)) + { + var assemblyName = typeof(TestCase).Assembly.GetName(); + var configurationBytes = + XmlUtilities.AddAssemblyRedirection( + testSourceConfigFile, + assemblyName, + ObjectModelVersionBuiltAgainst, + assemblyName.Version.ToString()); + appDomainSetup.SetConfigurationBytes(configurationBytes); + } + } + catch (Exception ex) + { + if (EqtTrace.IsErrorEnabled) + { + EqtTrace.Error("Exception hit while adding binding redirects to test source config file. Exception : {0}", ex); + } + } + } + else + { + // Use the current domains configuration setting. + appDomainSetup.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile; + } + } + + internal static object CreateInstance(AppDomain appDomain, Type type, object[] arguments) + { + Debug.Assert(appDomain != null, "appDomain is null"); + Debug.Assert(type != null, "type is null"); + + var typeAssemblyLocation = type.Assembly.Location; + var fullFilePath = typeAssemblyLocation == null ? null : Path.Combine(appDomain.SetupInformation.ApplicationBase, Path.GetFileName(typeAssemblyLocation)); + + if (fullFilePath == null || File.Exists(fullFilePath)) + { + // If the assembly exists in the app base directory, load it from there itself. + // Even if it does not exist, Create the type in the default Load Context and let the CLR resolve the assembly path. + // This would load the assembly in the Default Load context. + return appDomain.CreateInstanceAndUnwrap( + type.Assembly.FullName, + type.FullName, + false, + BindingFlags.Default, + null, + arguments, + null, + null); + } + else + { + // This means that the file is not present in the app base directory. Load it from Path instead. + // NOTE: We expect that all types that we are creating from here are types we know the location for. + // This would load the assembly in the Load-From context. + // While the above if condition is satisfied for most common cases, there could be a case where the adapter dlls + // do not get copied over to where the test assembly is, in which case we load them from where the parent AppDomain is picking them up from. + return appDomain.CreateInstanceFromAndUnwrap( + typeAssemblyLocation, + type.FullName, + false, + BindingFlags.Default, + null, + arguments, + null, + null); + } + } + + /// + /// Get the Version for the target framework version string + /// + /// Target framework string + /// Framework Version + internal static Version GetTargetFrameworkVersionFromVersionString(string version) + { + try + { + if (version.Length > PlatformServices.Constants.DotNetFrameWorkStringPrefix.Length + 1) + { + string versionPart = version.Substring(PlatformServices.Constants.DotNetFrameWorkStringPrefix.Length + 1); + return new Version(versionPart); + } + } + catch (FormatException ex) + { + // if the version is ".NETPortable,Version=v4.5,Profile=Profile259", then above code will throw exception. + EqtTrace.Warning(string.Format("AppDomainUtilities.GetTargetFrameworkVersionFromVersionString: Could not create version object from version string '{0}' due to error '{1}':", version, ex.Message)); + } + + return defaultVersion; + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/AppDomainWrapper.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/AppDomainWrapper.cs new file mode 100644 index 0000000000..8d511417c7 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/AppDomainWrapper.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.Security.Policy; + + /// + /// Abstraction over the AppDomain APIs. + /// + internal class AppDomainWrapper : IAppDomain + { + public AppDomain CreateDomain(string friendlyName, Evidence securityInfo, AppDomainSetup info) + { + return AppDomain.CreateDomain(friendlyName, securityInfo, info); + } + + public void Unload(AppDomain appDomain) + { + AppDomain.Unload(appDomain); + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopAssemblyUtility.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopAssemblyUtility.cs new file mode 100644 index 0000000000..14808261dc --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopAssemblyUtility.cs @@ -0,0 +1,282 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Reflection; + + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + + /// + /// Utility for assembly specific functionality. + /// + internal class AssemblyUtility : IAssemblyUtility + { + private static Dictionary cultures; + private readonly string[] assemblyExtensions = new string[] { ".dll", ".exe" }; + + /// + /// Gets all supported culture names in Keys. The Values are always null. + /// + private static Dictionary Cultures + { + get + { + if (cultures == null) + { + cultures = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var info in CultureInfo.GetCultures(CultureTypes.AllCultures)) + { + cultures.Add(info.Name, null); + } + } + + return cultures; + } + } + + /// + /// Loads an assembly into the reflection-only context, given its path. + /// + /// The path of the file that contains the manifest of the assembly. + /// The loaded assembly. + public Assembly ReflectionOnlyLoadFrom(string assemblyPath) + { + return Assembly.ReflectionOnlyLoadFrom(assemblyPath); + } + + /// + /// Loads an assembly into the reflection-only context, given its display name. + /// + /// The display name of the assembly, as returned by the System.Reflection.AssemblyName.FullName property. + /// The loaded assembly. + public Assembly ReflectionOnlyLoad(string assemblyString) + { + return Assembly.ReflectionOnlyLoad(assemblyString); + } + + /// + /// Whether file extension is an assembly file extension. + /// Returns true for .exe and .dll, otherwise false. + /// + /// Extension containing leading dot, e.g. ".exe". + /// Path.GetExtension() returns extension with leading dot. + /// True if this is an assembly extension. + internal bool IsAssemblyExtension(string extensionWithLeadingDot) + { + foreach (var realExtension in this.assemblyExtensions) + { + if (string.Equals(extensionWithLeadingDot, realExtension, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + /// + /// Determines whether given file is managed assembly. Does not load the assembly. Does not check file extension. + /// Performance: takes ~0.1 seconds on 2x CPU P4. + /// + /// The path to the assembly. + /// True if managed assembly. + internal bool IsAssembly(string path) + { + Debug.Assert(!string.IsNullOrEmpty(path), "path"); + try + { + // AssemblyName.GetAssemblyName: causes the file to be opened and closed, but the assembly is not added to this domain. + // Also if there are dependencies, they are never loaded. + AssemblyName.GetAssemblyName(path); + return true; + } + catch (FileLoadException) + { + // This is an executable image but not an assembly. + } + catch (BadImageFormatException) + { + // Happens when file is not a DLL/EXE, etc. + } + + // If file cannot be found we will throw. + // If there's anything else like SecurityException - we just pass exception through. + return false; + } + + /// + /// Returns satellite assemblies. Returns full canonicalized paths. + /// If the file is not an assembly returns empty list. + /// + /// The assembly to get satellites for. + /// List of satellite assemblies. + internal virtual List GetSatelliteAssemblies(string assemblyPath) + { + if (!this.IsAssemblyExtension(Path.GetExtension(assemblyPath)) || !this.IsAssembly(assemblyPath)) + { + EqtTrace.ErrorIf( + EqtTrace.IsErrorEnabled, + "AssemblyUtilities.GetSatelliteAssemblies: the specified file '{0}' is not managed assembly.", + assemblyPath); + Debug.Fail("AssemblyUtilities.GetSatelliteAssemblies: the file '" + assemblyPath + "' is not an assembly."); + + // If e.g. this is unmanaged dll, we don't care about the satellites. + return new List(); + } + + assemblyPath = Path.GetFullPath(assemblyPath); + var assemblyDir = Path.GetDirectoryName(assemblyPath); + var satellites = new List(); + + // Directory.Exists for 266 dirs takes 9ms while Path.GetDirectories can take up to 80ms on 10k dirs. + foreach (string dir in Cultures.Keys) + { + var dirPath = Path.Combine(assemblyDir, dir); + if (!Directory.Exists(dirPath)) + { + continue; + } + + // Check if the satellite exists in this dir. + // We check filenames like: MyAssembly.dll -> MyAssembly.resources.dll. + // Suprisingly, but both DLL and EXE are found by resource manager. + foreach (var extension in this.assemblyExtensions) + { + // extension contains leading dot. + string satellite = Path.ChangeExtension(Path.GetFileName(assemblyPath), "resources" + extension); + string satellitePath = Path.Combine(assemblyDir, Path.Combine(dir, satellite)); + + // We don't use Assembly.LoadFrom/Assembly.GetSatelliteAssebmlies because this is rather slow + // (1620ms for 266 cultures when directories do not exist). + if (File.Exists(satellitePath)) + { + // If the satellite found is not a managed assembly we do not report it as a reference. + if (!this.IsAssembly(satellitePath)) + { + EqtTrace.ErrorIf( + EqtTrace.IsErrorEnabled, + "AssemblyUtilities.GetSatelliteAssemblies: found assembly '{0}' installed as satellite but it's not managed assembly.", + satellitePath); + continue; + } + + // If both .exe and .dll exist we return both silently. + satellites.Add(satellitePath); + } + } + } + + return satellites; + } + + /// + /// Returns the dependent assemblies of the parameter assembly. + /// + /// Path to assembly to get dependencies for. + /// Config file to use while trying to resolve dependencies. + /// The warnings. + /// The . + internal virtual string[] GetFullPathToDependentAssemblies(string assemblyPath, string configFile, out IList warnings) + { + Debug.Assert(!string.IsNullOrEmpty(assemblyPath), "assemblyPath"); + + EqtTrace.InfoIf(EqtTrace.IsInfoEnabled, "AssemblyDependencyFinder.GetDependentAssemblies: start."); + + AppDomainSetup setupInfo = new AppDomainSetup(); + var dllDirectory = Path.GetDirectoryName(Path.GetFullPath(assemblyPath)); + setupInfo.ApplicationBase = dllDirectory; + + Debug.Assert(string.IsNullOrEmpty(configFile) || File.Exists(configFile), "Config file is specified but does not exist: {0}", configFile); + + AppDomainUtilities.SetConfigurationFile(setupInfo, configFile); + + EqtTrace.InfoIf(EqtTrace.IsInfoEnabled, "AssemblyDependencyFinder.GetDependentAssemblies: Using config file: '{0}'.", setupInfo.ConfigurationFile); + + setupInfo.LoaderOptimization = LoaderOptimization.MultiDomainHost; + + AppDomain appDomain = null; + try + { + appDomain = AppDomain.CreateDomain("Dependency finder domain", null, setupInfo); + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("AssemblyDependencyFinder.GetDependentAssemblies: Created AppDomain."); + } + + var assemblyResolverType = typeof(AssemblyResolver); + + EqtTrace.SetupRemoteEqtTraceListeners(appDomain); + + // This has to be LoadFrom, otherwise we will have to use AssemblyResolver to find self. + using ( + AssemblyResolver resolver = + (AssemblyResolver)AppDomainUtilities.CreateInstance( + appDomain, + assemblyResolverType, + new object[] { this.GetResolutionPaths() })) + { + // This has to be Load, otherwise Serialization of argument types will not work correctly. + AssemblyLoadWorker worker = + (AssemblyLoadWorker)AppDomainUtilities.CreateInstance(appDomain, typeof(AssemblyLoadWorker), null); + + EqtTrace.InfoIf(EqtTrace.IsInfoEnabled, "AssemblyDependencyFinder.GetDependentAssemblies: loaded the worker."); + + var allDependencies = worker.GetFullPathToDependentAssemblies(assemblyPath, out warnings); + var dependenciesFromDllDirectory = new List(); + var dllDirectoryUppercase = dllDirectory.ToUpperInvariant(); + foreach (var dependency in allDependencies) + { + if (dependency.ToUpperInvariant().Contains(dllDirectoryUppercase)) + { + dependenciesFromDllDirectory.Add(dependency); + } + } + + return dependenciesFromDllDirectory.ToArray(); + } + } + finally + { + if (appDomain != null) + { + EqtTrace.InfoIf(EqtTrace.IsInfoEnabled, "AssemblyDependencyFinder.GetDependentAssemblies: unloading AppDomain..."); + AppDomain.Unload(appDomain); + EqtTrace.InfoIf(EqtTrace.IsInfoEnabled, "AssemblyDependencyFinder.GetDependentAssemblies: unloading AppDomain succeeded."); + } + } + } + + /// + /// Gets the resolution paths for app domain creation. + /// + /// The of resolution paths. + internal IList GetResolutionPaths() + { + // Use dictionary to ensure we get a list of unique paths, but keep a list as the + // dictionary does not guarantee order. + Dictionary resolutionPathsDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + List resolutionPaths = new List(); + + // Add the path of the currently executing assembly (use Uri(CodeBase).LocalPath as Location can be on shadow dir). + string currentlyExecutingAssembly = Path.GetDirectoryName(Path.GetFullPath(new Uri(Assembly.GetExecutingAssembly().CodeBase).LocalPath)); + resolutionPaths.Add(currentlyExecutingAssembly); + resolutionPathsDictionary[currentlyExecutingAssembly] = null; + + // Add the application base for this domain. + if (!resolutionPathsDictionary.ContainsKey(AppDomain.CurrentDomain.BaseDirectory)) + { + resolutionPaths.Add(AppDomain.CurrentDomain.BaseDirectory); + resolutionPathsDictionary[AppDomain.CurrentDomain.BaseDirectory] = null; + } + + return resolutionPaths; + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopDeploymentUtility.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopDeploymentUtility.cs new file mode 100644 index 0000000000..ae7ec87509 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopDeploymentUtility.cs @@ -0,0 +1,250 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Security; + + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Deployment; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Extensions; + using Microsoft.VisualStudio.TestPlatform.ObjectModel; + + internal class DeploymentUtility : DeploymentUtilityBase + { + public DeploymentUtility() + : base() + { + } + + public DeploymentUtility(DeploymentItemUtility deploymentItemUtility, AssemblyUtility assemblyUtility, FileUtility fileUtility) + : base(deploymentItemUtility, assemblyUtility, fileUtility) + { + } + + public override void AddDeploymentItemsBasedOnMsTestSetting(string testSource, IList deploymentItems, List warnings) + { + if (MSTestSettingsProvider.Settings.DeployTestSourceDependencies) + { + EqtTrace.Info("Adding the references and satellite assemblies to the deployment items list"); + + // Get the referenced assemblies. + this.ProcessNewStorage(testSource, deploymentItems, warnings); + + // Get the satellite assemblies + var satelliteItems = this.GetSatellites(deploymentItems, testSource, warnings); + foreach (var satelliteItem in satelliteItems) + { + this.DeploymentItemUtility.AddDeploymentItem(deploymentItems, satelliteItem); + } + } + else + { + EqtTrace.Info("Adding the test source directory to the deployment items list"); + this.DeploymentItemUtility.AddDeploymentItem(deploymentItems, new DeploymentItem(Path.GetDirectoryName(testSource))); + } + } + + /// + /// Get root deployment directory + /// + /// The base directory. + /// Root deployment directory. + public override string GetRootDeploymentDirectory(string baseDirectory) + { + string dateTimeSufix = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", DateTimeFormatInfo.InvariantInfo); + string directoryName = string.Format(CultureInfo.CurrentCulture, Resource.TestRunName, DeploymentFolderPrefix, Environment.UserName, dateTimeSufix); + directoryName = this.FileUtility.ReplaceInvalidFileNameCharacters(directoryName); + + return this.FileUtility.GetNextIterationDirectoryName(baseDirectory, directoryName); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Requirement is to handle all kinds of user exceptions and message appropriately.")] + protected void ProcessNewStorage(string testSource, IList deploymentItems, IList warnings) + { + // Add deployment items and process .config files only for storages we have not processed before. + if (!this.DeploymentItemUtility.IsValidDeploymentItem(testSource, string.Empty, out var errorMessage)) + { + warnings.Add(errorMessage); + return; + } + + this.DeploymentItemUtility.AddDeploymentItem(deploymentItems, new DeploymentItem(testSource, string.Empty, DeploymentItemOriginType.TestStorage)); + + // Deploy .config file if exists, only for assemblies, i.e. DLL and EXE. + // First check .config, then if not found check for App.Config + // and deploy AppConfig to .config. + if (this.AssemblyUtility.IsAssemblyExtension(Path.GetExtension(testSource))) + { + var configFile = this.AddTestSourceConfigFileIfExists(testSource, deploymentItems); + + // Deal with test dependencies: update dependencyDeploymentItems and missingDependentAssemblies. + try + { + // We look for dependent assemblies only for DLL and EXE's. + this.AddDependencies(testSource, configFile, deploymentItems, warnings); + } + catch (Exception e) + { + string warning = string.Format(CultureInfo.CurrentCulture, Resource.DeploymentErrorFailedToDeployDependencies, testSource, e); + warnings.Add(warning); + } + } + } + + protected override void AddDependenciesOfDeploymentItem(string deploymentItemFile, IList filesToDeploy, IList warnings) + { + var dependencies = new List(); + + this.AddDependencies(deploymentItemFile, null, dependencies, warnings); + + foreach (var dependencyItem in dependencies) + { + Debug.Assert(Path.IsPathRooted(dependencyItem.SourcePath), "Path of the dependency " + dependencyItem.SourcePath + " is not rooted."); + + // Add dependencies to filesToDeploy. + filesToDeploy.Add(dependencyItem.SourcePath); + } + } + + protected IEnumerable GetSatellites(IEnumerable deploymentItems, string testSource, IList warnings) + { + List satellites = new List(); + foreach (DeploymentItem item in deploymentItems) + { + // We do not care about deployment items which are directories because in that case we deploy all files underneath anyway. + string path = null; + try + { + path = this.GetFullPathToDeploymentItemSource(item.SourcePath, testSource); + path = Path.GetFullPath(path); + + if (string.IsNullOrEmpty(path) || !this.AssemblyUtility.IsAssemblyExtension(Path.GetExtension(path)) + || !this.FileUtility.DoesFileExist(path) || !this.AssemblyUtility.IsAssembly(path)) + { + continue; + } + } + catch (ArgumentException ex) + { + EqtTrace.WarningIf(EqtTrace.IsWarningEnabled, "DeploymentManager.GetSatellites: {0}", ex); + } + catch (SecurityException ex) + { + EqtTrace.WarningIf(EqtTrace.IsWarningEnabled, "DeploymentManager.GetSatellites: {0}", ex); + } + catch (IOException ex) + { + // This covers PathTooLongException. + EqtTrace.WarningIf(EqtTrace.IsWarningEnabled, "DeploymentManager.GetSatellites: {0}", ex); + } + catch (NotSupportedException ex) + { + EqtTrace.WarningIf(EqtTrace.IsWarningEnabled, "DeploymentManager.GetSatellites: {0}", ex); + } + + // Note: now Path operations with itemPath should not result in any exceptions. + // path is already canonicalized. + + // If we cannot access satellite due to security, etc, we report warning. + try + { + string itemDir = Path.GetDirectoryName(path).TrimEnd( + new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); + List itemSatellites = this.AssemblyUtility.GetSatelliteAssemblies(path); + foreach (string satellite in itemSatellites) + { + Debug.Assert(!string.IsNullOrEmpty(satellite), "DeploymentManager.DoDeployment: got empty satellite!"); + Debug.Assert( + satellite.IndexOf(itemDir, StringComparison.OrdinalIgnoreCase) == 0, + "DeploymentManager.DoDeployment: Got satellite that does not start with original item path"); + + string satelliteDir = Path.GetDirectoryName(satellite).TrimEnd( + new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); + + Debug.Assert(!string.IsNullOrEmpty(satelliteDir), "DeploymentManager.DoDeployment: got empty satellite dir!"); + Debug.Assert(satelliteDir.Length > itemDir.Length + 1, "DeploymentManager.DoDeployment: wrong satellite dir length!"); + + string localeDir = satelliteDir.Substring(itemDir.Length + 1); + Debug.Assert(!string.IsNullOrEmpty(localeDir), "DeploymentManager.DoDeployment: got empty dir name for satellite dir!"); + + string relativeOutputDir = Path.Combine(item.RelativeOutputDirectory, localeDir); + + // Now finally add the item! + DeploymentItem satelliteItem = new DeploymentItem(satellite, relativeOutputDir, DeploymentItemOriginType.Satellite); + this.DeploymentItemUtility.AddDeploymentItem(satellites, satelliteItem); + } + } + catch (ArgumentException ex) + { + EqtTrace.WarningIf(EqtTrace.IsWarningEnabled, "DeploymentManager.GetSatellites: {0}", ex); + string warning = string.Format(CultureInfo.CurrentCulture, Resource.DeploymentErrorGettingSatellite, item, ex.GetType(), ex.GetExceptionMessage()); + warnings.Add(warning); + } + catch (SecurityException ex) + { + EqtTrace.WarningIf(EqtTrace.IsWarningEnabled, "DeploymentManager.GetSatellites: {0}", ex); + string warning = string.Format(CultureInfo.CurrentCulture, Resource.DeploymentErrorGettingSatellite, item, ex.GetType(), ex.GetExceptionMessage()); + warnings.Add(warning); + } + catch (IOException ex) + { + // This covers PathTooLongException. + EqtTrace.WarningIf(EqtTrace.IsWarningEnabled, "DeploymentManager.GetSatellites: {0}", ex); + string warning = string.Format(CultureInfo.CurrentCulture, Resource.DeploymentErrorGettingSatellite, item, ex.GetType(), ex.GetExceptionMessage()); + warnings.Add(warning); + } + } + + return satellites; + } + + /// + /// Process test storage and add dependent assemblies to dependencyDeploymentItems. + /// + /// The test source. + /// The config file. + /// Deployment items. + /// Warnings. + private void AddDependencies(string testSource, string configFile, IList deploymentItems, IList warnings) + { + Debug.Assert(!string.IsNullOrEmpty(testSource), "testSource should not be null or empty."); + + // config file can be null. + Debug.Assert(deploymentItems != null, "deploymentItems should not be null."); + Debug.Assert(Path.IsPathRooted(testSource), "path should be rooted."); + + var sw = Stopwatch.StartNew(); + + // Note: if this is not an assembly we simply return empty array, also: + // we do recursive search and report missing. + string[] references = this.AssemblyUtility.GetFullPathToDependentAssemblies(testSource, configFile, out var warningList); + if (warningList != null && warningList.Count > 0) + { + warnings = warnings.Concat(warningList).ToList(); + } + + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("DeploymentManager: Source:{0} has following references", testSource); + EqtTrace.Info("DeploymentManager: Resolving dependencies took {0} ms", sw.ElapsedMilliseconds); + } + + foreach (string reference in references) + { + DeploymentItem deploymentItem = new DeploymentItem(reference, string.Empty, DeploymentItemOriginType.Dependency); + this.DeploymentItemUtility.AddDeploymentItem(deploymentItems, deploymentItem); + + if (EqtTrace.IsInfoEnabled) + { + EqtTrace.Info("DeploymentManager: Reference:{0} ", reference); + } + } + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopReflectionUtility.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopReflectionUtility.cs new file mode 100644 index 0000000000..5cce3d9a3f --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/DesktopReflectionUtility.cs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Reflection; + + /// + /// Utility for reflection API's + /// + internal class ReflectionUtility + { + /// + /// Gets the custom attributes of the provided type on a memberInfo + /// + /// The member to reflect on. + /// The attribute type. + /// The vale of the custom attribute. + internal virtual object[] GetCustomAttributes(MemberInfo attributeProvider, Type type) + { + return this.GetCustomAttributes(attributeProvider, type, true); + } + + /// + /// Gets all the custom attributes adorned on a member. + /// + /// The member. + /// True to inspect the ancestors of element; otherwise, false. + /// The list of attributes on the member. Empty list if none found. + internal object[] GetCustomAttributes(MemberInfo memberInfo, bool inherit) + { + return this.GetCustomAttributes(memberInfo, type: null, inherit: inherit); + } + + /// + /// Get custom attributes on a member for both normal and reflection only load. + /// + /// Member for which attributes needs to be retrieved. + /// Type of attribute to retrieve. + /// If inherited type of attribute. + /// All attributes of give type on member. + internal object[] GetCustomAttributes(MemberInfo memberInfo, Type type, bool inherit) + { + if (memberInfo == null) + { + return null; + } + + bool shouldGetAllAttributes = type == null; + + if (!this.IsReflectionOnlyLoad(memberInfo)) + { + if (shouldGetAllAttributes) + { + return memberInfo.GetCustomAttributes(inherit); + } + else + { + return memberInfo.GetCustomAttributes(type, inherit); + } + } + else + { + List nonUniqueAttributes = new List(); + Dictionary uniqueAttributes = new Dictionary(); + + var inheritanceThreshold = 10; + var inheritanceLevel = 0; + + if (inherit && memberInfo.MemberType == MemberTypes.TypeInfo) + { + // This code is based on the code for fetching CustomAttributes in System.Reflection.CustomAttribute(RuntimeType type, RuntimeType caType, bool inherit) + var tempTypeInfo = memberInfo as TypeInfo; + + do + { + var attributes = CustomAttributeData.GetCustomAttributes(tempTypeInfo); + this.AddNewAttributes( + attributes, + shouldGetAllAttributes, + type, + uniqueAttributes, + nonUniqueAttributes); + tempTypeInfo = tempTypeInfo.BaseType?.GetTypeInfo(); + inheritanceLevel++; + } + while (tempTypeInfo != null && tempTypeInfo != typeof(object).GetTypeInfo() + && inheritanceLevel < inheritanceThreshold); + } + else if (inherit && memberInfo.MemberType == MemberTypes.Method) + { + // This code is based on the code for fetching CustomAttributes in System.Reflection.CustomAttribute(RuntimeMethodInfo method, RuntimeType caType, bool inherit). + var tempMethodInfo = memberInfo as MethodInfo; + + do + { + var attributes = CustomAttributeData.GetCustomAttributes(tempMethodInfo); + this.AddNewAttributes( + attributes, + shouldGetAllAttributes, + type, + uniqueAttributes, + nonUniqueAttributes); + var baseDefinition = tempMethodInfo.GetBaseDefinition(); + + if (baseDefinition != null) + { + if (string.Equals( + string.Concat(tempMethodInfo.DeclaringType.FullName, tempMethodInfo.Name), + string.Concat(baseDefinition.DeclaringType.FullName, baseDefinition.Name))) + { + break; + } + } + + tempMethodInfo = baseDefinition; + inheritanceLevel++; + } + while (tempMethodInfo != null && inheritanceLevel < inheritanceThreshold); + } + else + { + // Ideally we should not be reaching here. We only query for attributes on types/methods currently. + // Return the attributes that CustomAttributeData returns in this cases not considering inheritance. + var firstLevelAttributes = + CustomAttributeData.GetCustomAttributes(memberInfo); + this.AddNewAttributes(firstLevelAttributes, shouldGetAllAttributes, type, uniqueAttributes, nonUniqueAttributes); + } + + nonUniqueAttributes.AddRange(uniqueAttributes.Values); + return nonUniqueAttributes.ToArray(); + } + } + + internal object[] GetCustomAttributes(Assembly assembly, Type type) + { + if (assembly.ReflectionOnly) + { + List customAttributes = new List(); + customAttributes.AddRange(CustomAttributeData.GetCustomAttributes(assembly)); + + List attributesArray = new List(); + + foreach (var attribute in customAttributes) + { + if (this.IsTypeInheriting(attribute.Constructor.DeclaringType, type) + || attribute.Constructor.DeclaringType.AssemblyQualifiedName.Equals( + type.AssemblyQualifiedName)) + { + Attribute attributeInstance = CreateAttributeInstance(attribute); + if (attributeInstance != null) + { + attributesArray.Add(attributeInstance); + } + } + } + + return attributesArray.ToArray(); + } + else + { + return assembly.GetCustomAttributes(type).ToArray(); + } + } + + /// + /// Create instance of the attribute for reflection only load. + /// + /// The attribute data. + /// An attribute. + private static Attribute CreateAttributeInstance(CustomAttributeData attributeData) + { + object attribute = null; + try + { + // Create instance of attribute. For some case, constructor param is returned as ReadOnlyCollection + // instead of array. So convert it to array else constructor invoke will fail. + Type attributeType = Type.GetType(attributeData.Constructor.DeclaringType.AssemblyQualifiedName); + + List constructorParameters = new List(); + List constructorArguments = new List(); + foreach (var parameter in attributeData.ConstructorArguments) + { + Type parameterType = Type.GetType(parameter.ArgumentType.AssemblyQualifiedName); + constructorParameters.Add(parameterType); + if (parameterType.IsArray) + { + IEnumerable enumerable = parameter.Value as IEnumerable; + if (enumerable != null) + { + ArrayList list = new ArrayList(); + foreach (var item in enumerable) + { + if (item is CustomAttributeTypedArgument) + { + list.Add(((CustomAttributeTypedArgument)item).Value); + } + else + { + list.Add(item); + } + } + + constructorArguments.Add(list.ToArray(parameterType.GetElementType())); + } + else + { + constructorArguments.Add(parameter.Value); + } + } + else + { + constructorArguments.Add(parameter.Value); + } + } + + ConstructorInfo constructor = attributeType.GetConstructor(constructorParameters.ToArray()); + attribute = constructor.Invoke(constructorArguments.ToArray()); + + foreach (var namedArgument in attributeData.NamedArguments) + { + attributeType.GetProperty(namedArgument.MemberInfo.Name).SetValue(attribute, namedArgument.TypedValue.Value, null); + } + } + + // If not able to create instance of attribute ignore attribute. (May happen for custom user defined attributes). + catch (BadImageFormatException) + { + } + catch (FileLoadException) + { + } + catch (TypeLoadException) + { + } + + return attribute as Attribute; + } + + private void AddNewAttributes( + IList customAttributes, + bool shouldGetAllAttributes, + Type type, + Dictionary uniqueAttributes, + List nonUniqueAttributes) + { + foreach (var attribute in customAttributes) + { + if (shouldGetAllAttributes + || (this.IsTypeInheriting(attribute.Constructor.DeclaringType, type) + || attribute.Constructor.DeclaringType.AssemblyQualifiedName.Equals( + type.AssemblyQualifiedName))) + { + Attribute attributeInstance = CreateAttributeInstance(attribute); + if (attributeInstance != null) + { + var attributeUsageAttribute = + this.GetCustomAttributes( + attributeInstance.GetType().GetTypeInfo(), + typeof(AttributeUsageAttribute), + true).FirstOrDefault() as AttributeUsageAttribute; + + if (attributeUsageAttribute != null && !attributeUsageAttribute.AllowMultiple) + { + if (!uniqueAttributes.ContainsKey(attributeInstance.GetType().FullName)) + { + uniqueAttributes.Add(attributeInstance.GetType().FullName, attributeInstance); + } + } + else + { + nonUniqueAttributes.Add(attributeInstance); + } + } + } + } + } + + /// + /// Check whether the member is loaded in a reflection only context. + /// + /// The member Info. + /// True if the member is loaded in a reflection only context. + private bool IsReflectionOnlyLoad(MemberInfo memberInfo) + { + if (memberInfo != null) + { + return memberInfo.Module.Assembly.ReflectionOnly; + } + + return false; + } + + private bool IsTypeInheriting(Type type1, Type type2) + { + while (type1 != null) + { + if (type1.AssemblyQualifiedName.Equals(type2.AssemblyQualifiedName)) + { + return true; + } + + type1 = type1.GetTypeInfo().BaseType; + } + + return false; + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/IAppDomain.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/IAppDomain.cs new file mode 100644 index 0000000000..b03c07cb0b --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/IAppDomain.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.Security.Policy; + + /// + /// This interface is an abstraction over the AppDomain APIs + /// + internal interface IAppDomain + { + /// + /// Unloads the specified application domain. + /// + /// An application domain to unload. + void Unload(AppDomain appDomain); + + /// + /// Creates a new application domain using the specified name, evidence, and application domain setup information. + /// + /// The friendly name of the domain. + /// Evidence that establishes the identity of the code that runs in the application domain. Pass null to use the evidence of the current application domain. + /// An object that contains application domain initialization information. + /// The newly created application domain. + AppDomain CreateDomain(string friendlyName, Evidence securityInfo, AppDomainSetup info); + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/IAssemblyUtility.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/IAssemblyUtility.cs new file mode 100644 index 0000000000..1d6acb6f93 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/IAssemblyUtility.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities +{ + using System.Reflection; + + internal interface IAssemblyUtility + { + /// + /// Loads an assembly into the reflection-only context, given its path. + /// + /// The path of the file that contains the manifest of the assembly. + /// The loaded assembly. + Assembly ReflectionOnlyLoadFrom(string assemblyPath); + + /// + /// Loads an assembly into the reflection-only context, given its display name. + /// + /// The display name of the assembly, as returned by the System.Reflection.AssemblyName.FullName property. + /// The loaded assembly. + Assembly ReflectionOnlyLoad(string assemblyString); + } +} \ No newline at end of file diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/RandomIntPermutation.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/RandomIntPermutation.cs new file mode 100644 index 0000000000..9b713cc486 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/RandomIntPermutation.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.Collections; + using System.Collections.Generic; + + /// + /// Permutation of integers from 0 to (numberOfObjects - 1), in random order and in the end all values are returned. + /// Used to get random permutation for data row access in data driven test. + /// + internal class RandomIntPermutation : IEnumerable + { + private int[] objects; + + public RandomIntPermutation(int numberOfObjects) + { + if (numberOfObjects < 0) + { + throw new ArgumentException(Resource.WrongNumberOfObjects, nameof(numberOfObjects)); + } + + this.objects = new int[numberOfObjects]; + for (int i = 0; i < numberOfObjects; ++i) + { + this.objects[i] = i; + } + + Random random = new Random(); + for (int last = this.objects.Length - 1; last > 0; --last) + { + // Swap last and at random position which can be last in which case we don't swap. + int position = random.Next(last); // 0 .. last - 1 + int temp = this.objects[last]; + this.objects[last] = this.objects[position]; + this.objects[position] = temp; + } + } + + public IEnumerator GetEnumerator() + { + // Iterate over created permutation, do not change it. + for (int i = 0; i < this.objects.Length; ++i) + { + yield return this.objects[i]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/SequentialIntPermutation.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/SequentialIntPermutation.cs new file mode 100644 index 0000000000..921c4e5cf8 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/SequentialIntPermutation.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.Collections; + using System.Collections.Generic; + + /// + /// Permutation of integers from 0 to (numberOfObjects - 1) returned by increment of 1. + /// Used to get sequential permutation for data row access in data driven test. + /// + internal class SequentialIntPermutation : IEnumerable + { + private int numberOfObjects; + + public SequentialIntPermutation(int numberOfObjects) + { + if (numberOfObjects < 0) + { + throw new ArgumentException(Resource.WrongNumberOfObjects, nameof(numberOfObjects)); + } + + this.numberOfObjects = numberOfObjects; + } + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < this.numberOfObjects; ++i) + { + yield return i; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/VSInstallationUtilities.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/VSInstallationUtilities.cs new file mode 100644 index 0000000000..f606b7b8ff --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/VSInstallationUtilities.cs @@ -0,0 +1,263 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Runtime.InteropServices; + using static System.String; + + public static class VSInstallationUtilities + { + /// + /// Public assemblies directory name + /// + private const string PublicAssembliesDirectoryName = "PublicAssemblies"; + + /// + /// Folder name of private assemblies + /// + private const string PrivateAssembliesFolderName = "PrivateAssemblies"; + + /// + /// The manifest file name to determine if it is running in portable mode + /// + private const string PortableVsTestManifestFilename = "Portable.VsTest.Manifest"; + + private static string vsInstallPath = null; + + private static bool vsInstallPathEvaluated = false; + + /// + /// Gets the visual studio installation path on the local machine. + /// + /// VS install path + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Need to ignore failures to read the registry settings")] + public static string VSInstallPath + { + get + { + if (!vsInstallPathEvaluated) + { + try + { + vsInstallPath = null; + + // Use the Setup API to find the installation folder for currently running VS instance. + var setupConfiguration = new SetupConfiguration() as ISetupConfiguration; + if (setupConfiguration != null) + { + var currentConfiguration = setupConfiguration.GetInstanceForCurrentProcess(); + var currentInstallationPath = currentConfiguration.GetInstallationPath(); + vsInstallPath = Path.Combine(currentInstallationPath, @"Common7\IDE"); + } + } + catch + { + // SetupConfiguration won't work if VS is not installed or VS is pre-vs2017 version. + // So ignore all exception from it. + } + finally + { + vsInstallPathEvaluated = true; + } + } + + return vsInstallPath; + } + } + + /// + /// Gets path to public assemblies. + /// + /// Returns null if VS is not installed on this machine. + /// + public static string PathToPublicAssemblies => GetFullPath(PublicAssembliesDirectoryName); + + /// + /// Gets path to private assemblies. + /// + /// Returns null if VS is not installed on this machine. + /// + public static string PathToPrivateAssemblies => GetFullPath(PrivateAssembliesFolderName); + + /// + /// Is Current process running in Portable Mode + /// + /// True, if portable mode; false, otherwise + public static bool IsCurrentProcessRunningInPortableMode() + { + return IsProcessRunningInPortableMode(Process.GetCurrentProcess().MainModule.FileName); + } + + /// + /// Is the EXE specified running in Portable Mode + /// + /// EXE name. + /// True, if portable mode; false, otherwise + public static bool IsProcessRunningInPortableMode(string exeName) + { + // Get the directory of the exe + var exeDir = Path.GetDirectoryName(exeName); + if (!string.IsNullOrEmpty(exeDir)) + { + return File.Exists(Path.Combine(exeDir, PortableVsTestManifestFilename)); + } + + return false; + } + + private static string GetFullPath(string folderName) + { + var vsInstallDir = VSInstallPath; + return IsNullOrWhiteSpace(vsInstallDir?.Trim()) ? null : Path.Combine(vsInstallDir, folderName); + } + + /// + /// Information about an instance of a product. + /// + [Guid("B41463C3-8866-43B5-BC33-2B0676F7F42E")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +#pragma warning disable SA1201 // Elements must appear in the correct order + public interface ISetupInstance +#pragma warning restore SA1201 // Elements must appear in the correct order + { + /// + /// Gets the instance identifier (should match the name of the parent instance directory). + /// + /// The instance identifier. + [return: MarshalAs(UnmanagedType.BStr)] + string GetInstanceId(); + + /// + /// Gets the local date and time when the installation was originally installed. + /// + /// The local date and time when the installation was originally installed. + [return: MarshalAs(UnmanagedType.Struct)] + System.Runtime.InteropServices.ComTypes.FILETIME GetInstallDate(); + + /// + /// Gets the unique name of the installation, often indicating the branch and other information used for telemetry. + /// + /// The unique name of the installation, often indicating the branch and other information used for telemetry. + [return: MarshalAs(UnmanagedType.BStr)] + string GetInstallationName(); + + /// + /// Gets the path to the installation root of the product. + /// + /// The path to the installation root of the product. + [return: MarshalAs(UnmanagedType.BStr)] + string GetInstallationPath(); + + /// + /// Gets the version of the product installed in this instance. + /// + /// The version of the product installed in this instance. + [return: MarshalAs(UnmanagedType.BStr)] + string GetInstallationVersion(); + + /// + /// Gets the display name (title) of the product installed in this instance. + /// + /// The LCID for the display name. + /// The display name (title) of the product installed in this instance. + [return: MarshalAs(UnmanagedType.BStr)] + string GetDisplayName([In, MarshalAs(UnmanagedType.U4)] int lcid); + + /// + /// Gets the description of the product installed in this instance. + /// + /// The LCID for the description. + /// The description of the product installed in this instance. + [return: MarshalAs(UnmanagedType.BStr)] + string GetDescription([In, MarshalAs(UnmanagedType.U4)] int lcid); + + /// + /// Resolves the optional relative path to the root path of the instance. + /// + /// A relative path within the instance to resolve, or NULL to get the root path. + /// The full path to the optional relative path within the instance. If the relative path is NULL, the root path will always terminate in a backslash. + [return: MarshalAs(UnmanagedType.BStr)] + string ResolvePath([In, MarshalAs(UnmanagedType.LPWStr)] string relativePath); + } + + /// + /// A enumerator of installed objects. + /// + [Guid("6380BCFF-41D3-4B2E-8B2E-BF8A6810C848")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IEnumSetupInstances + { + /// + /// Retrieves the next set of product instances in the enumeration sequence. + /// + /// The number of product instances to retrieve. + /// A pointer to an array of . + /// A pointer to the number of product instances retrieved. If celt is 1 this parameter may be NULL. + void Next( + [In] int celt, + [Out, MarshalAs(UnmanagedType.Interface)] out ISetupInstance rgelt, + [Out] out int pceltFetched); + + /// + /// Skips the next set of product instances in the enumeration sequence. + /// + /// The number of product instances to skip. + void Skip([In, MarshalAs(UnmanagedType.U4)] int celt); + + /// + /// Resets the enumeration sequence to the beginning. + /// + void Reset(); + + /// + /// Creates a new enumeration object in the same state as the current enumeration object: the new object points to the same place in the enumeration sequence. + /// + /// A pointer to a pointer to a new interface. If the method fails, this parameter is undefined. + [return: MarshalAs(UnmanagedType.Interface)] + IEnumSetupInstances Clone(); + } + + /// + /// Gets information about product instances set up on the machine. + /// + [Guid("42843719-DB4C-46C2-8E7C-64F1816EFD5B")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface ISetupConfiguration + { + /// + /// Enumerates all product instances installed. + /// + /// An enumeration of installed product instances. + [return: MarshalAs(UnmanagedType.Interface)] + IEnumSetupInstances EnumInstances(); + + /// + /// Gets the instance for the current process path. + /// + /// The instance for the current process path. + [return: MarshalAs(UnmanagedType.Interface)] + ISetupInstance GetInstanceForCurrentProcess(); + + /// + /// Gets the instance for the given path. + /// + /// Path used to determine instance + /// The instance for the given path. + [return: MarshalAs(UnmanagedType.Interface)] + ISetupInstance GetInstanceForPath([In, MarshalAs(UnmanagedType.LPWStr)] string wzPath); + } + + /// + /// CoClass that implements . + /// + [ComImport] + [Guid("177F0C4A-1CD3-4DE7-A32C-71DBBB9FA36D")] + public class SetupConfiguration + { + } + } +} \ No newline at end of file diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/XmlUtilities.cs b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/XmlUtilities.cs new file mode 100644 index 0000000000..5a0871cdd7 --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/Utilities/XmlUtilities.cs @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Utilities +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Reflection; + using System.Text; + using System.Xml; + + internal class XmlUtilities + { + private const string XmlNamespace = "urn:schemas-microsoft-com:asm.v1"; + + /// + /// Adds assembly redirection and converts the resulting config file to a byte array. + /// + /// The config File. + /// The assembly name. + /// The old version. + /// The new version. + /// A byte array of the config file with the redirections added. + internal byte[] AddAssemblyRedirection(string configFile, AssemblyName assemblyName, string oldVersion, string newVersion) + { + var doc = this.GetXmlDocument(configFile); + + var configurationElement = FindOrCreateElement(doc, doc, "configuration"); + var assemblyBindingSection = FindOrCreateAssemblyBindingSection(doc, configurationElement); + AddAssemblyBindingRedirect(doc, assemblyBindingSection, assemblyName, oldVersion, newVersion); + using (var ms = new MemoryStream()) + { + doc.Save(ms); + return ms.ToArray(); + } + } + + /// + /// Gets the Xml document from the config file. This is virtual for unit testing. + /// + /// The config file. + /// An XmlDocument. + internal virtual XmlDocument GetXmlDocument(string configFile) + { + var doc = new XmlDocument(); + if (!string.IsNullOrEmpty(configFile?.Trim())) + { + using (var xmlReader = new XmlTextReader(configFile)) + { + xmlReader.DtdProcessing = DtdProcessing.Prohibit; + xmlReader.XmlResolver = null; + doc.Load(xmlReader); + } + } + + return doc; + } + + private static XmlElement FindOrCreateElement(XmlDocument doc, XmlNode parent, string name) + { + var ret = parent[name]; + + if (ret != null) + { + return ret; + } + + ret = doc.CreateElement(name, parent.NamespaceURI); + parent.AppendChild(ret); + return ret; + } + + private static XmlElement FindOrCreateAssemblyBindingSection(XmlDocument doc, XmlElement configurationElement) + { + // Each section must be created with the xmlns specified so that + // we don't end up with xmlns="" on each element. + + // Find or create the runtime section (this one should not have an xmlns on it). + var runtimeSection = FindOrCreateElement(doc, configurationElement, "runtime"); + + // Use the assemblyBinding section if it exists; otherwise, create one. + var assemblyBindingSection = runtimeSection["assemblyBinding"]; + if (assemblyBindingSection != null) + { + return assemblyBindingSection; + } + + assemblyBindingSection = doc.CreateElement("assemblyBinding", XmlNamespace); + runtimeSection.AppendChild(assemblyBindingSection); + return assemblyBindingSection; + } + + /// + /// Add an assembly binding redirect entry to the config file. + /// + /// The doc. + /// The assembly Binding Section. + /// The assembly Name. + /// The from Version. + /// The to Version. + private static void AddAssemblyBindingRedirect( + XmlDocument doc, + XmlElement assemblyBindingSection, + AssemblyName assemblyName, + string fromVersion, + string toVersion) + { + Debug.Assert(assemblyName != null, "assemblyName should not be null."); + if (assemblyName == null) + { + throw new ArgumentNullException(nameof(assemblyName)); + } + + // Convert the public key token into a string. + StringBuilder publicKeyTokenString = null; + var publicKeyToken = assemblyName.GetPublicKeyToken(); + if (publicKeyToken != null) + { + publicKeyTokenString = new StringBuilder(publicKeyToken.GetLength(0) * 2); + for (var i = 0; i < publicKeyToken.GetLength(0); i++) + { + publicKeyTokenString.AppendFormat( + System.Globalization.CultureInfo.InvariantCulture, + "{0:x2}", + new object[] { publicKeyToken[i] }); + } + } + + // Get the culture as a string. + var cultureString = assemblyName.CultureInfo.ToString(); + if (string.IsNullOrEmpty(cultureString)) + { + cultureString = "neutral"; + } + + // Add the dependentAssembly section. + var dependentAssemblySection = doc.CreateElement("dependentAssembly", XmlNamespace); + assemblyBindingSection.AppendChild(dependentAssemblySection); + + // Add the assemblyIdentity element. + var assemblyIdentityElement = doc.CreateElement("assemblyIdentity", XmlNamespace); + assemblyIdentityElement.SetAttribute("name", assemblyName.Name); + if (publicKeyTokenString != null) + { + assemblyIdentityElement.SetAttribute("publicKeyToken", publicKeyTokenString.ToString()); + } + + assemblyIdentityElement.SetAttribute("culture", cultureString); + dependentAssemblySection.AppendChild(assemblyIdentityElement); + + var bindingRedirectElement = doc.CreateElement("bindingRedirect", XmlNamespace); + bindingRedirectElement.SetAttribute("oldVersion", fromVersion); + bindingRedirectElement.SetAttribute("newVersion", toVersion); + dependentAssemblySection.AppendChild(bindingRedirectElement); + } + } +} diff --git a/src/Adapter/PlatformServices.Desktop.Legacy/app.config b/src/Adapter/PlatformServices.Desktop.Legacy/app.config new file mode 100644 index 0000000000..33d949fb5d --- /dev/null +++ b/src/Adapter/PlatformServices.Desktop.Legacy/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/Adapter/PlatformServices.Desktop/PlatformServices.Desktop.csproj b/src/Adapter/PlatformServices.Desktop/PlatformServices.Desktop.csproj index 020b9b88d6..4474c64230 100644 --- a/src/Adapter/PlatformServices.Desktop/PlatformServices.Desktop.csproj +++ b/src/Adapter/PlatformServices.Desktop/PlatformServices.Desktop.csproj @@ -1,15 +1,15 @@  - {B0FCE474-14BC-449A-91EA-A433342C0D63} Library Properties Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices - v4.5 + v4.6 true + true @@ -27,7 +27,6 @@ 4 true - @@ -40,7 +39,6 @@ - {bbc99a6b-4490-49dd-9c12-af2c1e95576e} PlatformServices.Interface @@ -57,7 +55,6 @@ False - @@ -65,7 +62,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive @@ -73,7 +69,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - ns10RecursiveDirectoryPath.cs @@ -84,6 +79,9 @@ Services\ns10TestContextPropertyStrings.cs + + Services\ns13ThreadSafeStringWriter.cs + Utilities\ns10Validate.cs @@ -153,7 +151,6 @@ - @@ -165,6 +162,5 @@ Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices - \ No newline at end of file diff --git a/src/Adapter/PlatformServices.Desktop/Services/DesktopTestContextImplementation.cs b/src/Adapter/PlatformServices.Desktop/Services/DesktopTestContextImplementation.cs index e103c48149..8a9481968f 100644 --- a/src/Adapter/PlatformServices.Desktop/Services/DesktopTestContextImplementation.cs +++ b/src/Adapter/PlatformServices.Desktop/Services/DesktopTestContextImplementation.cs @@ -455,7 +455,7 @@ public string GetDiagnosticMessages() public void ClearDiagnosticMessages() { var sb = this.stringWriter.GetStringBuilder(); - sb.Remove(0, sb.Length); + sb?.Remove(0, sb.Length); } #endregion diff --git a/src/Adapter/PlatformServices.Desktop/app.config b/src/Adapter/PlatformServices.Desktop/app.config index 33d949fb5d..18e6a636ec 100644 --- a/src/Adapter/PlatformServices.Desktop/app.config +++ b/src/Adapter/PlatformServices.Desktop/app.config @@ -1,11 +1,11 @@ - + - - + + - \ No newline at end of file + diff --git a/src/Adapter/PlatformServices.NetCore/PlatformServices.NetCore.csproj b/src/Adapter/PlatformServices.NetCore/PlatformServices.NetCore.csproj index 46c0f48cea..efa2f5cf62 100644 --- a/src/Adapter/PlatformServices.NetCore/PlatformServices.NetCore.csproj +++ b/src/Adapter/PlatformServices.NetCore/PlatformServices.NetCore.csproj @@ -57,6 +57,7 @@ + diff --git a/src/Adapter/PlatformServices.NetCore/Services/NetCoreTestContextImplementation.cs b/src/Adapter/PlatformServices.NetCore/Services/NetCoreTestContextImplementation.cs index 108268ce23..bcf6eca384 100644 --- a/src/Adapter/PlatformServices.NetCore/Services/NetCoreTestContextImplementation.cs +++ b/src/Adapter/PlatformServices.NetCore/Services/NetCoreTestContextImplementation.cs @@ -400,7 +400,7 @@ public string GetDiagnosticMessages() public void ClearDiagnosticMessages() { var sb = this.stringWriter.GetStringBuilder(); - sb.Remove(0, sb.Length); + sb?.Remove(0, sb.Length); } public void SetDataRow(object dataRow) diff --git a/src/Adapter/PlatformServices.Portable/PlatformServices.Portable.csproj b/src/Adapter/PlatformServices.Portable/PlatformServices.Portable.csproj index c229207ca5..12010c2a3d 100644 --- a/src/Adapter/PlatformServices.Portable/PlatformServices.Portable.csproj +++ b/src/Adapter/PlatformServices.Portable/PlatformServices.Portable.csproj @@ -98,6 +98,9 @@ Services\ns10ThreadOperations.cs + + Services\ns10ThreadSafeStringWriter.cs + Services\ns10TraceListener.cs diff --git a/src/Adapter/PlatformServices.Shared/netstandard1.0/Services/ns10TestContextImplementation.cs b/src/Adapter/PlatformServices.Shared/netstandard1.0/Services/ns10TestContextImplementation.cs index db9d7e2fe3..078724763b 100644 --- a/src/Adapter/PlatformServices.Shared/netstandard1.0/Services/ns10TestContextImplementation.cs +++ b/src/Adapter/PlatformServices.Shared/netstandard1.0/Services/ns10TestContextImplementation.cs @@ -307,7 +307,7 @@ public string GetDiagnosticMessages() public void ClearDiagnosticMessages() { var sb = this.stringWriter.GetStringBuilder(); - sb.Remove(0, sb.Length); + sb?.Remove(0, sb.Length); } public void SetDataRow(object dataRow) diff --git a/src/Adapter/MSTest.CoreAdapter/Execution/ThreadSafeStringWriter.cs b/src/Adapter/PlatformServices.Shared/netstandard1.0/Services/ns10ThreadSafeStringWriter.cs similarity index 83% rename from src/Adapter/MSTest.CoreAdapter/Execution/ThreadSafeStringWriter.cs rename to src/Adapter/PlatformServices.Shared/netstandard1.0/Services/ns10ThreadSafeStringWriter.cs index cf170daf29..8c8576a94d 100644 --- a/src/Adapter/MSTest.CoreAdapter/Execution/ThreadSafeStringWriter.cs +++ b/src/Adapter/PlatformServices.Shared/netstandard1.0/Services/ns10ThreadSafeStringWriter.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices { using System; using System.IO; @@ -9,7 +9,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution /// /// StringWriter which has thread safe ToString(). /// - internal class ThreadSafeStringWriter : StringWriter + public class ThreadSafeStringWriter : StringWriter { private readonly object lockObject = new object(); @@ -19,7 +19,10 @@ internal class ThreadSafeStringWriter : StringWriter /// /// The format provider. /// - public ThreadSafeStringWriter(IFormatProvider formatProvider) + /// + /// Id of the session. + /// + public ThreadSafeStringWriter(IFormatProvider formatProvider, string outputType) : base(formatProvider) { } @@ -40,6 +43,14 @@ public override string ToString() } } + public void Clear() + { + lock (this.lockObject) + { + InvokeBaseClass(() => this.GetStringBuilder().Clear()); + } + } + /// public override void Write(char value) { diff --git a/src/Adapter/PlatformServices.Shared/netstandard1.3/Services/ns13ThreadSafeStringWriter.cs b/src/Adapter/PlatformServices.Shared/netstandard1.3/Services/ns13ThreadSafeStringWriter.cs new file mode 100644 index 0000000000..cc0e17354d --- /dev/null +++ b/src/Adapter/PlatformServices.Shared/netstandard1.3/Services/ns13ThreadSafeStringWriter.cs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Text; + using System.Threading; + + /// + /// StringWriter which has thread safe ToString(). + /// + public class ThreadSafeStringWriter : StringWriter + { +#if DEBUG + private static readonly StringBuilder AllOutput = new StringBuilder(); +#endif + private static readonly AsyncLocal> State = new AsyncLocal>(); + private readonly string outputType; + private readonly object lockObject = new object(); + + /// + /// Initializes a new instance of the class. + /// + /// + /// The format provider. + /// + /// + /// Id of the session. + /// + public ThreadSafeStringWriter(IFormatProvider formatProvider, string outputType) + : base(formatProvider) + { + this.outputType = outputType; + + lock (this.lockObject) + { + // Ensure that State.Value is populated, so we can inherit it to the child + // async flow, and also keep reference to it here in the parent flow. + // otherwise if there is `async Task` test method, the method will run as child async flow + // populate it but the parent will remain null, because the changes to context only flow downwards + // and not upwards. + this.GetOrAddStringBuilder(); + } + } + + /// + public override string ToString() + { + lock (this.lockObject) + { + try + { + return this.GetStringBuilderOrNull()?.ToString(); + } + catch (ObjectDisposedException) + { + return default(string); + } + } + } + + public void Clear() + { + lock (this.lockObject) + { + this.GetStringBuilderOrNull()?.Clear(); + } + } + + /// + public override void Write(char value) + { + lock (this.lockObject) + { +#if DEBUG + AllOutput.Append(value); +#endif + this.GetOrAddStringBuilder().Append(value); + } + } + + /// + public override void Write(string value) + { + lock (this.lockObject) + { +#if DEBUG + AllOutput.Append(value); +#endif + this.GetOrAddStringBuilder().Append(value); + } + } + + public override void WriteLine(string value) + { + lock (this.lockObject) + { +#if DEBUG + AllOutput.AppendLine(value); +#endif + this.GetOrAddStringBuilder().AppendLine(value); + } + } + + /// + public override void Write(char[] buffer, int index, int count) + { + lock (this.lockObject) + { +#if DEBUG + AllOutput.Append(buffer, index, count); +#endif + this.GetOrAddStringBuilder().Append(buffer, index, count); + } + } + + // + public override StringBuilder GetStringBuilder() + { + return this.GetStringBuilderOrNull(); + } + + /// + protected override void Dispose(bool disposing) + { + lock (this.lockObject) + { + ThreadSafeStringWriter.State?.Value?.Remove(this.outputType); + InvokeBaseClass(() => base.Dispose(disposing)); + } + } + + private static void InvokeBaseClass(Action action) + { + try + { + action(); + } + catch (ObjectDisposedException) + { + } + } + + // Avoiding name GetStringBuilder because it is already present on the base class. + private StringBuilder GetStringBuilderOrNull() + { + if (State.Value == null) + { + return null; + } + else if (!State.Value.TryGetValue(this.outputType, out var stringBuilder)) + { + return null; + } + else + { + return stringBuilder; + } + } + + private StringBuilder GetOrAddStringBuilder() + { + if (State.Value == null) + { + // The storage for the current async operation is empty + // create the array and appropriate stringbuilder. + // Avoid looking up the value after we add it to the dictionary. + var sb = new StringBuilder(); + State.Value = new Dictionary { [this.outputType] = sb }; + return sb; + } + else if (!State.Value.TryGetValue(this.outputType, out var stringBuilder)) + { + // The storage for the current async operation has the dictionary, but not the key + // for the output type, add it, and avoid looking up the value again. + var sb = new StringBuilder(); + State.Value.Add(this.outputType, sb); + return sb; + } + else + { + // The storage for the current async operation has the dictionary, and the key + // for the output type, just return it. + return stringBuilder; + } + } + } +} \ No newline at end of file diff --git a/src/Package/MSTest.TestAdapter.nuspec b/src/Package/MSTest.TestAdapter.nuspec index bcfc2dc1a3..d287ef4b98 100644 --- a/src/Package/MSTest.TestAdapter.nuspec +++ b/src/Package/MSTest.TestAdapter.nuspec @@ -52,7 +52,7 @@ - + @@ -79,13 +79,19 @@ - - + + + + + + + + - + diff --git a/src/Package/MSTest.TestAdapter.symbols.nuspec b/src/Package/MSTest.TestAdapter.symbols.nuspec index 7daa72ffa9..afccefc4b9 100644 --- a/src/Package/MSTest.TestAdapter.symbols.nuspec +++ b/src/Package/MSTest.TestAdapter.symbols.nuspec @@ -52,7 +52,7 @@ - + @@ -79,14 +79,20 @@ - - + + + + + + + + - + @@ -106,7 +112,7 @@ - + diff --git a/test/ComponentTests/PlatformServices.Desktop.Component.Tests/App.config b/test/ComponentTests/PlatformServices.Desktop.Component.Tests/App.config index 9c21941772..a134be39b3 100644 --- a/test/ComponentTests/PlatformServices.Desktop.Component.Tests/App.config +++ b/test/ComponentTests/PlatformServices.Desktop.Component.Tests/App.config @@ -1,18 +1,18 @@ - + - - + + - - + + - + - \ No newline at end of file + diff --git a/test/ComponentTests/PlatformServices.Desktop.Component.Tests/PlatformServices.Desktop.Component.Tests.csproj b/test/ComponentTests/PlatformServices.Desktop.Component.Tests/PlatformServices.Desktop.Component.Tests.csproj index e668199da2..a4d1f54a37 100644 --- a/test/ComponentTests/PlatformServices.Desktop.Component.Tests/PlatformServices.Desktop.Component.Tests.csproj +++ b/test/ComponentTests/PlatformServices.Desktop.Component.Tests/PlatformServices.Desktop.Component.Tests.csproj @@ -1,7 +1,6 @@  - Debug AnyCPU @@ -10,7 +9,7 @@ Properties MSTestAdapter.PlatformServices.Desktop.ComponentTests PlatformServices.Desktop.ComponentTests - v4.5.2 + v4.6 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 10.0 @@ -35,16 +34,13 @@ prompt 4 - - FrameworkV1 - {b0fce474-14bc-449a-91ea-a433342c0d63} PlatformServices.Desktop @@ -63,7 +59,6 @@ SampleFrameworkExtensions - @@ -71,7 +66,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -82,7 +76,6 @@ Designer - \ No newline at end of file diff --git a/test/E2ETests/DiscoveryAndExecutionTests/DiscoveryAndExecutionTests.csproj b/test/E2ETests/DiscoveryAndExecutionTests/DiscoveryAndExecutionTests.csproj index 840b0a9209..fbf5b81e8b 100644 --- a/test/E2ETests/DiscoveryAndExecutionTests/DiscoveryAndExecutionTests.csproj +++ b/test/E2ETests/DiscoveryAndExecutionTests/DiscoveryAndExecutionTests.csproj @@ -1,6 +1,6 @@  - net452 + net46 v4.5.2 false diff --git a/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/TestMethodInfoTests.cs b/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/TestMethodInfoTests.cs index 1f915aba00..25fa22dc53 100644 --- a/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/TestMethodInfoTests.cs +++ b/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/TestMethodInfoTests.cs @@ -243,7 +243,7 @@ public void TestMethodInfoInvokeShouldNotListenForDebugAndTraceLogsWhenDisabled( PlatformServiceProvider.Instance = testablePlatformServiceProvider; var result = method.Invoke(null); - Assert.AreEqual(string.Empty, result.DebugTrace); + Assert.IsNull(result.DebugTrace); }); } diff --git a/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/ThreadSafeStringWriterTests.cs b/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/ThreadSafeStringWriterTests.cs index d326c06162..07dda8a4b5 100644 --- a/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/ThreadSafeStringWriterTests.cs +++ b/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/ThreadSafeStringWriterTests.cs @@ -6,9 +6,12 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Execution extern alias FrameworkV1; using System; + using System.Diagnostics; using System.Globalization; + using System.Threading; using System.Threading.Tasks; - using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; + using FluentAssertions; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; using Assert = FrameworkV1::Microsoft.VisualStudio.TestTools.UnitTesting.Assert; using TestClass = FrameworkV1::Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute; using TestMethod = FrameworkV1::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute; @@ -16,31 +19,67 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Execution [TestClass] public class ThreadSafeStringWriterTests { + private bool task2flag; + [TestMethod] public void ThreadSafeStringWriterWriteLineHasContentFromMultipleThreads() { - using (var stringWriter = new ThreadSafeStringWriter(CultureInfo.InvariantCulture)) + using (ExecutionContext.SuppressFlow()) { - Action action = (string x) => - { - for (var i = 0; i < 100000; i++) + using (var stringWriter = new ThreadSafeStringWriter(CultureInfo.InvariantCulture, "tst")) + { + Action action = (string x) => { + var count = 10; + for (var i = 0; i < count; i++) + { // Choose WriteLine since it calls the entire sequence: // Write(string) -> Write(char[]) -> Write(char) stringWriter.WriteLine(x); + } + }; + + var task1 = Task.Run(() => + { + var timeout = Stopwatch.StartNew(); + action("content1"); + action("content1"); + action("content1"); + action("content1"); + while (this.task2flag != true && timeout.Elapsed < TimeSpan.FromSeconds(5)) + { } - }; + action("content1"); + action("content1"); + action("content1"); + action("content1"); + }); + var task2 = Task.Run(() => + { + action("content2"); + action("content2"); + action("content2"); + action("content2"); + this.task2flag = true; + action("content2"); + action("content2"); + action("content2"); + action("content2"); + }); - var task1 = Task.Run(() => action("content1")); - var task2 = Task.Run(() => action("content2")); + task2.GetAwaiter().GetResult(); + task1.GetAwaiter().GetResult(); - task1.Wait(); - task2.Wait(); + var content = stringWriter.ToString(); + content.Should().NotBeNullOrWhiteSpace(); - // Validate that only whole lines are written, not a mix of random chars - foreach (var line in stringWriter.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)) - { - Assert.IsTrue(line.Equals("content1") || line.Equals("content2")); + // Validate that only whole lines are written, not a mix of random chars + var lines = content.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + lines.Should().HaveCountGreaterThan(0); + foreach (var line in lines) + { + Assert.IsTrue(line.Equals("content1") || line.Equals("content2")); + } } } } diff --git a/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/UnitTestRunnerTests.cs b/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/UnitTestRunnerTests.cs index de4b5b4a93..79e5130a2a 100644 --- a/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/UnitTestRunnerTests.cs +++ b/test/UnitTests/MSTest.CoreAdapter.Unit.Tests/Execution/UnitTestRunnerTests.cs @@ -9,6 +9,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Execution using System; using System.Collections.Generic; + using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -389,20 +390,34 @@ public void RunCleanupShouldReturnCleanupResultsForAssemblyAndClassCleanupMethod public void RunCleanupShouldReturnCleanupResultsWithDebugTraceLogsSetIfDebugTraceEnabled() { this.unitTestRunner = new UnitTestRunner(this.GetSettingsWithDebugTrace(true)); - var type = typeof(DummyTestClassWithCleanupMethods); - var methodInfo = type.GetMethod("TestMethod"); - var testMethod = new TestMethod(methodInfo.Name, type.FullName, "A", isAsync: false); + try + { + var type = typeof(DummyTestClassWithCleanupMethods); + var testMethod = new TestMethod(nameof(DummyTestClassWithCleanupMethods.TestMethod), type.FullName, "A", isAsync: false); - this.testablePlatformServiceProvider.MockFileOperations.Setup(fo => fo.LoadAssembly("A", It.IsAny())) - .Returns(Assembly.GetExecutingAssembly()); + this.testablePlatformServiceProvider.MockFileOperations.Setup(fo => fo.LoadAssembly("A", It.IsAny())) + .Returns(Assembly.GetExecutingAssembly()); - StringWriter writer = new StringWriter(new StringBuilder("DummyTrace")); - this.testablePlatformServiceProvider.MockTraceListener.Setup(tl => tl.GetWriter()).Returns(writer); + StringWriter writer = new StringWriter(new StringBuilder("DummyTrace")); - this.unitTestRunner.RunSingleTest(testMethod, this.testRunParameters); + DummyTestClassWithCleanupMethods.ClassCleanupMethodBody = () => + { + writer.Write("ClassCleanup"); + }; - var cleanupresult = this.unitTestRunner.RunCleanup(); - Assert.AreEqual("DummyTrace", cleanupresult.DebugTrace); + this.testablePlatformServiceProvider.MockTraceListener.Setup(tl => tl.GetWriter()).Returns(writer); + + var testResult = this.unitTestRunner.RunSingleTest(testMethod, this.testRunParameters).FirstOrDefault(); + Assert.IsNotNull(testResult); + Assert.AreEqual("DummyTrace", testResult.DebugTrace); + + var cleanupresult = this.unitTestRunner.RunCleanup(); + Assert.AreEqual("ClassCleanup", cleanupresult.DebugTrace); + } + finally + { + DummyTestClassWithCleanupMethods.ClassCleanupMethodBody = null; + } } [TestMethodV1] @@ -421,7 +436,7 @@ public void RunCleanupShouldReturnCleanupResultsWithNoDebugAndTraceLogsSetIfDebu this.unitTestRunner.RunSingleTest(testMethod, this.testRunParameters); var cleanupresult = this.unitTestRunner.RunCleanup(); - Assert.AreEqual(cleanupresult.DebugTrace, string.Empty); + Assert.AreEqual(null, cleanupresult.DebugTrace); } #endregion diff --git a/test/UnitTests/PlatformServices.Desktop.Unit.Tests/App.config b/test/UnitTests/PlatformServices.Desktop.Unit.Tests/App.config index fb3c2554dd..a134be39b3 100644 --- a/test/UnitTests/PlatformServices.Desktop.Unit.Tests/App.config +++ b/test/UnitTests/PlatformServices.Desktop.Unit.Tests/App.config @@ -1,18 +1,18 @@ - + - - + + - - + + - + diff --git a/test/UnitTests/PlatformServices.Desktop.Unit.Tests/PlatformServices.Desktop.Unit.Tests.csproj b/test/UnitTests/PlatformServices.Desktop.Unit.Tests/PlatformServices.Desktop.Unit.Tests.csproj index d289fcdf02..f44290bfab 100644 --- a/test/UnitTests/PlatformServices.Desktop.Unit.Tests/PlatformServices.Desktop.Unit.Tests.csproj +++ b/test/UnitTests/PlatformServices.Desktop.Unit.Tests/PlatformServices.Desktop.Unit.Tests.csproj @@ -1,14 +1,13 @@  - {599833DC-EC5A-40CA-B5CF-DEF719548EEF} Library Properties MSTestAdapter.PlatformServices.Desktop.UnitTests MSTestAdapter.PlatformServices.Desktop.UnitTests - v4.5.2 + v4.6 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 10.0 @@ -33,16 +32,13 @@ prompt 4 - - FrameworkV1 - {b0fce474-14bc-449a-91ea-a433342c0d63} PlatformServices.Desktop @@ -66,16 +62,16 @@ MSTest.CoreAdapter.TestUtilities - + + 5.10.3 + - runtime; build; native; contentfiles; analyzers; buildtransitive - Deployment\ns10DeploymentItemTests.cs @@ -113,6 +109,9 @@ Utilities\ns13ReflectionUtilityTests.cs + + Services\ns13ThreadSafeStringWriterTests.cs + @@ -129,17 +128,14 @@ - Designer - Always Designer - \ No newline at end of file diff --git a/test/UnitTests/PlatformServices.NetCore.Unit.Tests/PlatformServices.NetCore.Unit.Tests.csproj b/test/UnitTests/PlatformServices.NetCore.Unit.Tests/PlatformServices.NetCore.Unit.Tests.csproj index 26911a23ba..f4f523604d 100644 --- a/test/UnitTests/PlatformServices.NetCore.Unit.Tests/PlatformServices.NetCore.Unit.Tests.csproj +++ b/test/UnitTests/PlatformServices.NetCore.Unit.Tests/PlatformServices.NetCore.Unit.Tests.csproj @@ -19,6 +19,7 @@ + @@ -44,6 +45,7 @@ + diff --git a/test/UnitTests/PlatformServices.Shared.Unit.Tests/netstandard1.3/ns13ThreadSafeStringWriterTests.cs b/test/UnitTests/PlatformServices.Shared.Unit.Tests/netstandard1.3/ns13ThreadSafeStringWriterTests.cs new file mode 100644 index 0000000000..cfb3aa3f4c --- /dev/null +++ b/test/UnitTests/PlatformServices.Shared.Unit.Tests/netstandard1.3/ns13ThreadSafeStringWriterTests.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.VisualStudio.TestPlatform.MSTestAdapter.UnitTests.Execution +{ +#if NETCOREAPP + using Microsoft.VisualStudio.TestTools.UnitTesting; +#else + extern alias FrameworkV1; + extern alias FrameworkV2; + + using Assert = FrameworkV1::Microsoft.VisualStudio.TestTools.UnitTesting.Assert; + using CollectionAssert = FrameworkV1::Microsoft.VisualStudio.TestTools.UnitTesting.CollectionAssert; + using StringAssert = FrameworkV1::Microsoft.VisualStudio.TestTools.UnitTesting.StringAssert; + using TestClass = FrameworkV1::Microsoft.VisualStudio.TestTools.UnitTesting.TestClassAttribute; + using TestInitialize = FrameworkV1::Microsoft.VisualStudio.TestTools.UnitTesting.TestInitializeAttribute; + using TestMethod = FrameworkV1::Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute; + using UnitTestOutcome = FrameworkV2::Microsoft.VisualStudio.TestTools.UnitTesting.UnitTestOutcome; +#endif + + using System; + using System.Diagnostics; + using System.Globalization; + using System.Threading; + using System.Threading.Tasks; + using FluentAssertions; + using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices; + + [TestClass] + public class ThreadSafeStringWriterTests + { + private bool task2flag; + + [TestMethod] + public void ThreadSafeStringWriterWritesLinesFromDifferentsTasksSeparately() + { + // Suppress the flow of parent context here becuase this test method will run in + // a task already and we don't want the existing async context to interfere with this. + using (ExecutionContext.SuppressFlow()) + { + // String writer needs to be task aware to write output from different tasks + // into different output. The tasks below wait for each other to ensure + // we are mixing output from different tasks at the same time. + using (var stringWriter = new ThreadSafeStringWriter(CultureInfo.InvariantCulture, "output")) + { + var task1 = Task.Run(() => + { + var timeout = Stopwatch.StartNew(); + stringWriter.WriteLine("content1"); + stringWriter.WriteLine("content1"); + stringWriter.WriteLine("content1"); + stringWriter.WriteLine("content1"); + while (this.task2flag != true && timeout.Elapsed < TimeSpan.FromSeconds(5)) + { + } + stringWriter.WriteLine("content1"); + stringWriter.WriteLine("content1"); + stringWriter.WriteLine("content1"); + stringWriter.WriteLine("content1"); + return stringWriter.ToString(); + }); + var task2 = Task.Run(() => + { + stringWriter.WriteLine("content2"); + stringWriter.WriteLine("content2"); + stringWriter.WriteLine("content2"); + stringWriter.WriteLine("content2"); + this.task2flag = true; + stringWriter.WriteLine("content2"); + stringWriter.WriteLine("content2"); + stringWriter.WriteLine("content2"); + stringWriter.WriteLine("content2"); + return stringWriter.ToString(); + }); + + var task2Output = task2.GetAwaiter().GetResult(); + var task1Output = task1.GetAwaiter().GetResult(); + + // there was no output in the current task, the output should be empty + var content = stringWriter.ToString(); + content.Should().BeNullOrWhiteSpace(); + + // task1 and task2 should output into their respective buckets + task1Output.Should().NotBeNullOrWhiteSpace(); + task2Output.Should().NotBeNullOrWhiteSpace(); + + task1Output.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Should().OnlyContain(i => i == "content1").And.HaveCount(8); + task2Output.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Should().OnlyContain(i => i == "content2").And.HaveCount(8); + } + } + } + + [TestMethod] + public void ThreadSafeStringWriterWritesLinesIntoDifferentWritesSeparately() + { + // Suppress the flow of parent context here becuase this test method will run in + // a task already and we don't want the existing async context to interfere with this. + using (ExecutionContext.SuppressFlow()) + { + // The string writer mixes output captured by different instances if they are in the same taks, or under the same task context + // and use the same output type. In the any of the "out" writers we should see all the output from the writers marked as "out" + // and in any of the debug writers we should see all "debug" output. + using (var stringWriter1 = new ThreadSafeStringWriter(CultureInfo.InvariantCulture, "out")) + { + using (var stringWriter2 = new ThreadSafeStringWriter(CultureInfo.InvariantCulture, "debug")) + { + using (var stringWriter3 = new ThreadSafeStringWriter(CultureInfo.InvariantCulture, "out")) + { + using (var stringWriter4 = new ThreadSafeStringWriter(CultureInfo.InvariantCulture, "debug")) + { + // Writing the data needs to run in a task, because that is how the writer is designed, + // because we always run test in a task, so we must not setup the parent context, otherwise + // it would capture output of all tests. + var result = Task.Run(() => + { + stringWriter1.WriteLine("out"); + stringWriter2.WriteLine("debug"); + + Task.Run(() => + { + stringWriter3.WriteLine("out"); + stringWriter4.WriteLine("debug"); + }).GetAwaiter().GetResult(); + + return new { Out = stringWriter1.ToString(), Debug = stringWriter2.ToString() }; + }).GetAwaiter().GetResult(); + + // task1 and task2 should output into their respective buckets + result.Out.Should().NotBeNullOrWhiteSpace(); + result.Debug.Should().NotBeNullOrWhiteSpace(); + + var output = result.Out.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); + output.Should().OnlyContain(i => i == "out").And.HaveCount(2); + + var debug = result.Debug.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); + debug.Should().OnlyContain(i => i == "debug").And.HaveCount(2); + } + } + } + } + } + } + } +}