diff --git a/.gitignore b/.gitignore index e810d394..25a04d1c 100644 --- a/.gitignore +++ b/.gitignore @@ -173,6 +173,7 @@ PublishScripts/ # NuGet Packages *.nupkg +/StrawberryPerl.5.28.0.1/ # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. diff --git a/Build/Local/Nuget/nuget.exe b/Build/Local/Nuget/nuget.exe new file mode 100644 index 00000000..d6ecf835 Binary files /dev/null and b/Build/Local/Nuget/nuget.exe differ diff --git a/Directory.Build.props b/Directory.Build.props index 01f3960c..00d7f92b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -11,7 +11,10 @@ Enlistment root is based off of wherever this file is. Be sure not to set this property anywhere else. --> $(MSBuildThisFileDirectory.TrimEnd('\\')) - net472 + + $([MSBuild]::IsOSPlatform('Windows')) + net472 diff --git a/SmoBuild/DdlEvents.targets b/SmoBuild/DdlEvents.targets index 8e4fb1eb..a3cc3912 100644 --- a/SmoBuild/DdlEvents.targets +++ b/SmoBuild/DdlEvents.targets @@ -1,14 +1,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - $(BaseIntermediateOutputPath)$(Configuration)\$(TargetFramework) + $(BaseIntermediateOutputPath)$(Configuration)/$(TargetFramework) - c:\strawberry\perl\bin\perl.exe - $(EnlistmentRoot)\packages\StrawberryPerl.5.28.0.1\bin\perl.exe + c:\strawberry\perl\bin\perl.exe + $(EnlistmentRoot)\packages\StrawberryPerl.5.28.0.1\bin\perl.exe + $(EnlistmentRoot)\StrawberryPerl.5.28.0.1\bin\perl.exe + $(EnlistmentRoot)\packages\StrawberryPerl.5.28.0.1\bin\perl.exe + + perl @@ -26,12 +30,12 @@ BeforeTargets="ResolveReferences" Inputs="$(MSBuildThisFileFullPath);@(DdlEventsGenCompile)" Outputs="@(EventsFileName->'$(SmoCodeGenOutputDir)\%(CodeFile).cs')" > - - - - - - + + + + + + diff --git a/SmoBuild/DdlEvents/evtschema.pl b/SmoBuild/DdlEvents/evtschema.pl index d40396fa..47be1772 100644 --- a/SmoBuild/DdlEvents/evtschema.pl +++ b/SmoBuild/DdlEvents/evtschema.pl @@ -22,7 +22,7 @@ open(EVTSCHEMACPP, $eventsschema_cpp) || die "Cannot open $eventsschema_cpp\n"; # New destination file -open(OUTF, ">" . $OutputDir . "\\eventsschema.tmp") || die "Cannot write to eventsschema.tmp\n"; +open(OUTF, ">" . $OutputDir . "/eventsschema.tmp") || die "Cannot write to eventsschema.tmp\n"; # read and copy while() @@ -46,7 +46,7 @@ sub IncludeFile { local($IncName) = @_; - open(INCFILE, $OutputDir . "\\" . $IncName) || die "Cannot open " . $IncName . "\n"; + open(INCFILE, $OutputDir . "/" . $IncName) || die "Cannot open " . $IncName . "\n"; while() { print OUTF $_; diff --git a/SmoBuild/DdlEvents/trc2evt.pl b/SmoBuild/DdlEvents/trc2evt.pl index 33333cd5..342316e3 100644 --- a/SmoBuild/DdlEvents/trc2evt.pl +++ b/SmoBuild/DdlEvents/trc2evt.pl @@ -303,7 +303,7 @@ # print event group enumeration if($option == 0) { - open(FOUT, ">" . $OutputDir . "\\grpenum.inc") || die "Cannot write to " . $OutputDir . "\\grpenum.inc\n"; + open(FOUT, ">" . $OutputDir . "/grpenum.inc") || die "Cannot write to " . $OutputDir . "/grpenum.inc\n"; print FOUT "// Extended Event Group for SQLTrace\n"; print FOUT "enum ETraceEventGroup {\n"; print FOUT "\tEGROUP_TRCAT_START = EGROUP_TRCAT_ALL, // Setting beginning\n"; @@ -318,7 +318,7 @@ # # print event group name - open(FOUT, ">" . $OutputDir . "\\grpdefs.inc") || die "Cannot write to " . $OutputDir . "\\grpdefs.inc\n"; + open(FOUT, ">" . $OutputDir . "/grpdefs.inc") || die "Cannot write to " . $OutputDir . "/grpdefs.inc\n"; print FOUT "// Extended Event Groups for SQLTrace\n"; for (my $i = 1; $i <= $#rgsCategoryIDs; $i++) { @@ -330,7 +330,7 @@ # # print paraminfo array - open(FOUT, ">" . $OutputDir . "\\parinfo.inc") || die "Cannot write to " . $OutputDir . "\\parinfo.inc\n"; + open(FOUT, ">" . $OutputDir . "/parinfo.inc") || die "Cannot write to " . $OutputDir . "/parinfo.inc\n"; my $ColumnCount = @rgsColumnNames; print FOUT "//----------------------------------------------------------------\n"; print FOUT "// TraceEventTag Enum\n"; @@ -400,7 +400,7 @@ # # print schema array - open(FOUT, ">" . $OutputDir . "\\schema.inc") || die "Cannot write to " . $OutputDir . "\\schema.inc\n"; + open(FOUT, ">" . $OutputDir . "/schema.inc") || die "Cannot write to " . $OutputDir . "/schema.inc\n"; print FOUT "// Schema array extended for SQLTrace\n"; for (my $i = $EventClassStart; $i <= $#rgsEventIDs; $i++) { @@ -447,7 +447,7 @@ # # Generate event hierarchy graph for reference. This file is not used for compilation. - open(FOUT, ">" . $OutputDir . "\\hierarchy.txt") || die "Cannot write to " . $OutputDir . "\\hierarchy.txt.\n"; + open(FOUT, ">" . $OutputDir . "/hierarchy.txt") || die "Cannot write to " . $OutputDir . "/hierarchy.txt.\n"; print FOUT "========== SQL Trace event hierarchy ==================\n\n"; print FOUT "ALL_EVENTS\n"; print FOUT "|____ TRC_ALL_EVENTS\n"; @@ -483,8 +483,8 @@ # Form more information, take a look at: # http://blogs.msdn.com/brettsh/archive/2006/06/07/620986.aspx # ---------------------------------------------------------------- - open(FOUT, ">:raw:encoding(UTF16-LE):crlf:utf8", $OutputDir . "\\etwcls.mof") - || die "Cannot write to " . $OutputDir . "\\etwcls.mof\n"; + open(FOUT, ">:raw:encoding(UTF16-LE):crlf:utf8", $OutputDir . "/etwcls.mof") + || die "Cannot write to " . $OutputDir . "/etwcls.mof\n"; print FOUT "\x{FEFF}"; # print BOM (Byte Order Mark) for the unicode file # ---------------------------------------------------------------- @@ -548,7 +548,7 @@ # # GUID include file which lists all ETW event class GUIDs # - open(FOUT, ">" .$OutputDir . "\\etwguid.inc") || die "Cannot write to " . $OutputDir . "\\etwguid.inc\n"; + open(FOUT, ">" .$OutputDir . "/etwguid.inc") || die "Cannot write to " . $OutputDir . "/etwguid.inc\n"; print FOUT "// Array of Event Class Info\n\n"; for (my $i = $EventClassStart; $i <= $#rgsEventIDs; $i++) @@ -566,7 +566,7 @@ # Dump All Columns to XML # - open(FOUT, ">" .$OutputDir . "\\events_trace_template.xst") || die "Cannot write to " . $OutputDir . "\\events_trace_template.xst\n"; + open(FOUT, ">" .$OutputDir . "/events_trace_template.xst") || die "Cannot write to " . $OutputDir . "/events_trace_template.xst\n"; for (my $col = 1; $col < $ColumnCount; $col++) { #print param defs for active cols except for SPID/EventClass @@ -585,7 +585,7 @@ # 2: instance guid (INSTGUID) if($option == 1) { - open(FOUT, ">" . $OutputDir . "\\etwsetup.inc") || die "Cannot write to " . $OutputDir . "\\etwsetup.inc\n"; + open(FOUT, ">" . $OutputDir . "/etwsetup.inc") || die "Cannot write to " . $OutputDir . "/etwsetup.inc\n"; print FOUT "{NOPARAM, L\"// ************************************************************************\\n\"},\n"; print FOUT "{NOPARAM, L\"// \\tCopyrights (c) Microsoft Corporation\\n//\\n\"},\n"; print FOUT "{NOPARAM, L\"// ************************************************************************\\n\\n\"},\n"; diff --git a/init.cmd b/init.cmd index bb1d5bb2..9c6a4d07 100644 --- a/init.cmd +++ b/init.cmd @@ -28,7 +28,7 @@ doskey prod=pushd %BASEDIR%src\Microsoft\SqlServer\Management\$* doskey sfc=pushd %BASEDIR%src\Microsoft\SqlServer\Management\Sdk\sfc doskey smo=pushd %BASEDIR%src\Microsoft\SqlServer\Management\Smo doskey smoenum=pushd %BASEDIR%src\Microsoft\SqlServer\Management\SqlEnum\$* -doskey clean=powershell.exe -ExecutionPolicy Unrestricted -File "%BASEDIR%init.ps1" -Clean + doskey tst=pushd %BASEDIR%src\FunctionalTest\Smo\$* doskey slngen19=slngen -vs "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\devenv.exe" $* @@ -44,16 +44,30 @@ doskey netcoretests=pushd %BASEDIR%bin\debug\net8.0$Tvstest.console.exe microsof title git %BASEDIR% -REM Migration to PowerShell environment -powershell.exe -ExecutionPolicy Unrestricted -File "%BASEDIR%init.ps1" -Initialize -if "%errorlevel%" neq "0" ( - echo Failed to setup local dev build environment correctly +REM Read the required SDK version from global.json so this script never drifts from it +for /f "usebackq tokens=2 delims=:, " %%V in (`findstr /r /c:"\"version\"" %BASEDIR%global.json`) do set DOTNET_SDK_VERSION=%%~V +set DOTNET_SDK_VERSION=%DOTNET_SDK_VERSION:"=% +dotnet --list-sdks | findstr /b /c:"%DOTNET_SDK_VERSION% " >nul +IF ERRORLEVEL 1 ( + echo .NET SDK %DOTNET_SDK_VERSION% ^(required by global.json^) was not found. + echo Install it from https://dotnet.microsoft.com/download/dotnet/%DOTNET_SDK_VERSION:~0,-4% ) dotnet tool install --global Microsoft.VisualStudio.SlnGen.Tool dotnet tool install --global Microsoft.SqlPackage + +set NUGET_EXE=%BASEDIR%Build\Local\Nuget\nuget.exe +IF NOT EXIST %NUGET_EXE% ( + echo nuget.exe not found, downloading from https://dist.nuget.org/win-x86-commandline/latest/nuget.exe + IF NOT EXIST %BASEDIR%Build\Local\Nuget mkdir %BASEDIR%Build\Local\Nuget + powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' -OutFile '%NUGET_EXE%'" + IF NOT EXIST %NUGET_EXE% ( + echo Failed to download nuget.exe. Please download it manually from https://dist.nuget.org/win-x86-commandline/latest/nuget.exe and place it at %NUGET_EXE% + goto :EOF + ) +) IF NOT EXIST %BASEDIR%packages\StrawberryPerl.5.28.0.1\bin\perl.exe ( - %BASEDIR%Build\Local\Nuget\nuget.exe install StrawberryPerl -Version 5.28.0.1 + %NUGET_EXE% install StrawberryPerl -Version 5.28.0.1 ) echo. diff --git a/init.sh b/init.sh new file mode 100644 index 00000000..c95cb2e6 --- /dev/null +++ b/init.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# +# Setup the developer environment on Linux (e.g. Ubuntu). +# **This script is not run on build machines** +# +# Use this script to idempotently install the tools needed to build dirs.proj on Linux. +# +# Notes: +# - net472 (the .NET Framework) cannot be built or run on Linux, so a Linux build only +# produces the netstandard2.0/net8.0/net10.0 targets. This is handled automatically +# by the MSBuild props (see $(IsWindowsBuild) in Directory.Build.props) - you don't +# need to pass any extra flags. +# - Build with: dotnet build "$BASEDIR/dirs.proj" +# - Run unit tests for a project with: dotnet test +# +set -euo pipefail + +BASEDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTNET_SDK_VERSION="$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$BASEDIR/global.json" | head -1 | sed -E 's/.*"([^"]+)"$/\1/')" +if [ -z "$DOTNET_SDK_VERSION" ]; then + echo "Error: could not read sdk.version from $BASEDIR/global.json" >&2 + exit 1 +fi +DOTNET_INSTALL_DIR="${DOTNET_INSTALL_DIR:-$HOME/.dotnet}" + +echo "Enlistment root: $BASEDIR" + +# --------------------------------------------------------------------------- +# perl - required to generate DDL event code (SmoBuild/DdlEvents/*.pl) +# --------------------------------------------------------------------------- +if ! command -v perl >/dev/null 2>&1; then + echo "perl not found, installing..." + if command -v apt-get >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y perl + else + echo "Warning: apt-get not found. Please install perl manually and re-run this script." >&2 + fi +else + echo "perl already installed: $(command -v perl)" +fi + +# --------------------------------------------------------------------------- +# .NET SDK - required to build and test the repo +# --------------------------------------------------------------------------- +has_required_sdk() { + command -v dotnet >/dev/null 2>&1 && dotnet --list-sdks 2>/dev/null | grep -q "^${DOTNET_SDK_VERSION} " +} + +if has_required_sdk; then + echo ".NET SDK $DOTNET_SDK_VERSION already installed: $(command -v dotnet)" +else + echo ".NET SDK $DOTNET_SDK_VERSION not found, installing to $DOTNET_INSTALL_DIR..." + curl -sSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh + bash /tmp/dotnet-install.sh --version "$DOTNET_SDK_VERSION" --install-dir "$DOTNET_INSTALL_DIR" + rm -f /tmp/dotnet-install.sh + + export PATH="$DOTNET_INSTALL_DIR:$PATH" + if ! grep -q "$DOTNET_INSTALL_DIR" "$HOME/.bashrc" 2>/dev/null; then + { + echo "" + echo "# Added by init.sh for sqlmanagementobjects" + echo "export DOTNET_ROOT=\"$DOTNET_INSTALL_DIR\"" + echo "export PATH=\"$DOTNET_INSTALL_DIR:\$PATH\"" + } >> "$HOME/.bashrc" + echo "Added $DOTNET_INSTALL_DIR to PATH in ~/.bashrc (start a new shell, or run 'source ~/.bashrc')." + fi +fi + +# --------------------------------------------------------------------------- +# dotnet global tools used by the repo +# --------------------------------------------------------------------------- +install_global_tool_if_missing() { + local tool_name="$1" + if dotnet tool list --global 2>/dev/null | grep -qi "^${tool_name} "; then + echo "$tool_name already installed as a global dotnet tool" + else + dotnet tool install --global "$tool_name" + fi +} + +install_global_tool_if_missing "Microsoft.VisualStudio.SlnGen.Tool" +install_global_tool_if_missing "Microsoft.SqlPackage" + +echo +echo +echo "To build for SMO development and to run unit tests:" +echo " dotnet build $BASEDIR/dirs.proj" +echo +echo "Note: net472 (the .NET Framework) is skipped on Linux; the build produces" +echo " the netstandard2.0/net8.0/net10.0 targets only." +echo +echo "To run tests for a single test project:" +echo " dotnet test $BASEDIR/src/UnitTest/Smo/Microsoft.SqlServer.Test.SmoUnitTests.csproj" +echo +echo "To run functional tests, see $BASEDIR/src/FunctionalTest/runtests.cmd for the list" +echo "of filters and environment variables (e.g. SqlTestTargetServersFilter) that are" +echo "used to select which tests/backends to run, then invoke with dotnet test." diff --git a/src/Codegen/CodeGen.cs b/src/Codegen/CodeGen.cs index 4de88f80..0583d7e7 100644 --- a/src/Codegen/CodeGen.cs +++ b/src/Codegen/CodeGen.cs @@ -436,7 +436,7 @@ public static int Main(string[] args) string output_dir = ""; if (args.Length == 4) { - output_dir = args[3] + "\\"; + output_dir = args[3]; } bool bGenerateNewFile = false; @@ -724,7 +724,7 @@ static void GenerateClass(XmlNode node, string node_namespace, string output_dir bool bGenMetadata = Boolean.Parse(GetAttribute("gen_metadata", node, "true")); string sUrn = GetAttribute("urn", node, null); - string modelOutputDir = output_dir + "model\\"; + string modelOutputDir = Path.Combine(output_dir, "model"); if (!String.IsNullOrEmpty(output_dir) && !Directory.Exists(output_dir)) { diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 566bf5e7..ced97ced 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ bin - $(EnlistmentRoot)\$(TargetFolder)\gac + $(EnlistmentRoot)/$(TargetFolder)/gac false @@ -14,12 +14,12 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) Debug - $(EnlistmentRoot)\$(TargetFolder) - $(BaseOutputPath)\$(Configuration) + $(EnlistmentRoot)/$(TargetFolder) + $(BaseOutputPath)/$(Configuration) - $(EnlistmentRoot)\obj\$([MSBuild]::MakeRelative('$(EnlistmentRoot)\', $(MSBuildProjectDirectory))) + $(EnlistmentRoot)/obj/$([MSBuild]::MakeRelative('$(EnlistmentRoot)/', $(MSBuildProjectDirectory))) - $(EnlistmentRoot)\SmoBuild\ + $(EnlistmentRoot)/SmoBuild/ netcore netfx CSharp.NetFx.props @@ -28,7 +28,7 @@ $(NoWarn);NU5125;MSB3026;CA1416;CS1668;NU1903 true - $(EnlistmentRoot)\packages + $(EnlistmentRoot)/packages false diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index adb0b811..04580f55 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -38,7 +38,7 @@ public const int MajorVersion = $(AssemblyMajorVersion)%3B - + @@ -46,7 +46,7 @@ public const int MajorVersion = $(AssemblyMajorVersion)%3B - + diff --git a/src/FunctionalTest/Framework/Microsoft.SqlServer.Test.Manageability.Utils.csproj b/src/FunctionalTest/Framework/Microsoft.SqlServer.Test.Manageability.Utils.csproj index a72e6960..c3652ab4 100644 --- a/src/FunctionalTest/Framework/Microsoft.SqlServer.Test.Manageability.Utils.csproj +++ b/src/FunctionalTest/Framework/Microsoft.SqlServer.Test.Manageability.Utils.csproj @@ -34,7 +34,7 @@ - $(BaseOutputPath)\Documentation\$(TargetFramework)\$(AssemblyName).xml + $(BaseOutputPath)/Documentation/$(TargetFramework)/$(AssemblyName).xml $(NoWarn);1591 diff --git a/src/FunctionalTest/Identity/Microsoft.SqlServer.ADO.Identity.csproj b/src/FunctionalTest/Identity/Microsoft.SqlServer.ADO.Identity.csproj index 162420ce..eb012e83 100644 --- a/src/FunctionalTest/Identity/Microsoft.SqlServer.ADO.Identity.csproj +++ b/src/FunctionalTest/Identity/Microsoft.SqlServer.ADO.Identity.csproj @@ -8,7 +8,7 @@ false - $(BaseOutputPath)\Documentation\$(TargetFramework)\$(AssemblyName).xml + $(BaseOutputPath)/Documentation/$(TargetFramework)/$(AssemblyName).xml Embedded @@ -18,8 +18,10 @@ - - + + + diff --git a/src/Microsoft/Directory.Build.targets b/src/Microsoft/Directory.Build.targets index e20fcfd7..1adb570a 100644 --- a/src/Microsoft/Directory.Build.targets +++ b/src/Microsoft/Directory.Build.targets @@ -2,7 +2,7 @@ - $(BaseOutputPath)\Documentation\$(TargetFramework)\$(AssemblyName).xml + $(BaseOutputPath)/Documentation/$(TargetFramework)/$(AssemblyName).xml $(NoWarn);1591 diff --git a/src/Microsoft/SqlServer/Management/ConnectionInfo/ServerInformation.cs b/src/Microsoft/SqlServer/Management/ConnectionInfo/ServerInformation.cs index 5cb6d551..4ad8620a 100644 --- a/src/Microsoft/SqlServer/Management/ConnectionInfo/ServerInformation.cs +++ b/src/Microsoft/SqlServer/Management/ConnectionInfo/ServerInformation.cs @@ -157,17 +157,9 @@ public bool IsFabricServer private static readonly HashSet validEditions = new HashSet(Enum.GetValues(typeof(DatabaseEngineEdition)).Cast()); // this query needs to be safe on all platforms. DW and Sql2005 don't support CONNECTIONPROPERTY - private const string serverVersionQuery = @"DECLARE @edition sysname; -SET @edition = cast(SERVERPROPERTY(N'EDITION') as sysname); -SELECT case when @edition = N'SQL Azure' then 2 else 1 end as 'DatabaseEngineType', -@edition AS Edition, -CONVERT(bigint, SERVERPROPERTY(N'EditionID')) AS EditionID, -SERVERPROPERTY('EngineEdition') AS DatabaseEngineEdition, -SERVERPROPERTY('ProductVersion') AS ProductVersion, -@@MICROSOFTVERSION AS MicrosoftVersion, -case when serverproperty('EngineEdition') = 12 then 1 when serverproperty('EngineEdition') = 11 and @@version like 'Microsoft Azure SQL Data Warehouse%' then 1 else 0 end as IsFabricServer, -convert(sysname, SERVERPROPERTY(N'Collation')) AS Collation; -"; + // Declared as a single line with explicit \r\n so the text is identical regardless of how this + // source file's own line endings were checked out or which OS is running. + private const string serverVersionQuery = "DECLARE @edition sysname;\r\nSET @edition = cast(SERVERPROPERTY(N'EDITION') as sysname);\r\nSELECT case when @edition = N'SQL Azure' then 2 else 1 end as 'DatabaseEngineType',\r\n@edition AS Edition,\r\nCONVERT(bigint, SERVERPROPERTY(N'EditionID')) AS EditionID,\r\nSERVERPROPERTY('EngineEdition') AS DatabaseEngineEdition,\r\nSERVERPROPERTY('ProductVersion') AS ProductVersion,\r\n@@MICROSOFTVERSION AS MicrosoftVersion,\r\ncase when serverproperty('EngineEdition') = 12 then 1 when serverproperty('EngineEdition') = 11 and @@version like 'Microsoft Azure SQL Data Warehouse%' then 1 else 0 end as IsFabricServer,\r\nconvert(sysname, SERVERPROPERTY(N'Collation')) AS Collation;\r\n"; static public ServerInformation GetServerInformation(IDbConnection sqlConnection, IDbDataAdapter dataAdapter, string serverVersionString) { var serverVersion = ParseStringServerVersion(serverVersionString); @@ -175,18 +167,15 @@ static public ServerInformation GetServerInformation(IDbConnection sqlConnection if (serverVersion.Major >= 14) { - cmdBuilder.AppendLine(@"select host_platform from sys.dm_os_host_info"); + cmdBuilder.Append("select host_platform from sys.dm_os_host_info\r\n"); } else { // Pre v14 is all Windows - cmdBuilder.AppendLine(@"select N'Windows' as host_platform"); + cmdBuilder.Append("select N'Windows' as host_platform\r\n"); } - cmdBuilder.AppendLine(@"if @edition = N'SQL Azure' - select 'TCP' as ConnectionProtocol -else - exec ('select CONVERT(nvarchar(40),CONNECTIONPROPERTY(''net_transport'')) as ConnectionProtocol')"); + cmdBuilder.Append("if @edition = N'SQL Azure' \r\n select 'TCP' as ConnectionProtocol\r\nelse\r\n exec ('select CONVERT(nvarchar(40),CONNECTIONPROPERTY(''net_transport'')) as ConnectionProtocol')\r\n"); using (var sqlCommand = sqlConnection.CreateCommand()) { diff --git a/src/Microsoft/SqlServer/Management/Sdk/Sfc/Enumerator/ObjectLoadInfoManager.cs b/src/Microsoft/SqlServer/Management/Sdk/Sfc/Enumerator/ObjectLoadInfoManager.cs index 16117031..3915b14a 100644 --- a/src/Microsoft/SqlServer/Management/Sdk/Sfc/Enumerator/ObjectLoadInfoManager.cs +++ b/src/Microsoft/SqlServer/Management/Sdk/Sfc/Enumerator/ObjectLoadInfoManager.cs @@ -157,7 +157,7 @@ static private void LoadHierarchy() m_Hierarchy = new SortedList(System.StringComparer.Ordinal); #if SMOCODEGEN - string realFile = Path.Combine(Path.Combine(CodeGenSettings.Directory, "core\\src\\xml"), "Config.xml"); + string realFile = Path.Combine(CodeGenSettings.Directory, "core", "src", "xml", "Config.xml"); if (!File.Exists(realFile)) { realFile = Path.Combine(Path.Combine(CodeGenSettings.Directory, "xml"), "Config.xml"); diff --git a/src/Microsoft/SqlServer/Management/Smo/Microsoft.SqlServer.Smo.csproj b/src/Microsoft/SqlServer/Management/Smo/Microsoft.SqlServer.Smo.csproj index 5252d934..b968f0c4 100644 --- a/src/Microsoft/SqlServer/Management/Smo/Microsoft.SqlServer.Smo.csproj +++ b/src/Microsoft/SqlServer/Management/Smo/Microsoft.SqlServer.Smo.csproj @@ -8,7 +8,7 @@ false $(NoWarn);0169;0414;0649;3021 Microsoft.SqlServer.Management.Smo - $(BaseIntermediateOutputPath)$(TargetFramework)\Gen + $(BaseIntermediateOutputPath)$(TargetFramework)/Gen .\ true true @@ -671,11 +671,11 @@ - "$(OutDir)..\$(CodeGenPlatform)\SmoCodeGen.exe" - dotnet --roll-forward LatestMajor "$(OutDir)..\$(CodeGenPlatform)\SmoCodeGen.dll" + "$(OutDir)../$(CodeGenPlatform)/SmoCodeGen.exe" + dotnet --roll-forward LatestMajor "$(OutDir)../$(CodeGenPlatform)/SmoCodeGen.dll" - - + + diff --git a/src/Microsoft/SqlServer/Management/SqlAssessment/Microsoft.SqlServer.Management.Assessment.csproj b/src/Microsoft/SqlServer/Management/SqlAssessment/Microsoft.SqlServer.Management.Assessment.csproj index 6b6e995b..f7cfb171 100644 --- a/src/Microsoft/SqlServer/Management/SqlAssessment/Microsoft.SqlServer.Management.Assessment.csproj +++ b/src/Microsoft/SqlServer/Management/SqlAssessment/Microsoft.SqlServer.Management.Assessment.csproj @@ -9,7 +9,7 @@ - "$(OutputPath)\Microsoft.SqlServer.Management.Assessment.xml" + "$(OutputPath)/Microsoft.SqlServer.Management.Assessment.xml" diff --git a/src/Microsoft/SqlServer/Management/SqlEnum/Util.cs b/src/Microsoft/SqlServer/Management/SqlEnum/Util.cs index 42539b84..7ee59755 100644 --- a/src/Microsoft/SqlServer/Management/SqlEnum/Util.cs +++ b/src/Microsoft/SqlServer/Management/SqlEnum/Util.cs @@ -426,7 +426,22 @@ static public string Combine(string path1, string path2, PathType pathType) } else if (pathType == PathType.Windows) { - return Path.Combine(path1, path2); + // Don't use System.IO.Path here: its behavior follows the host OS, but a Windows path + // needs to combine the same way whether we're running on Windows or Linux. + if (path1.Length == 0) + { + return path2; + } + if (path2.Length == 0) + { + return path1; + } + if (IsWindowsPathRooted(path2)) + { + return path2; + } + char lastChar = path1[path1.Length - 1]; + return (lastChar == '\\' || lastChar == '/') ? path1 + path2 : path1 + '\\' + path2; } else // mimic Path.Combine { @@ -439,6 +454,44 @@ static public string Combine(string path1, string path2, PathType pathType) } } + /// + /// Mimics Path.IsPathRooted for a Windows-style path, without relying on the host OS's own rules + /// (System.IO.Path's behavior on Linux differs from Windows for backslash/drive-letter paths). + /// + static private bool IsWindowsPathRooted(string path) + { + if (string.IsNullOrEmpty(path)) + { + return false; + } + if (path[0] == '\\' || path[0] == '/') + { + return true; + } + return path.Length >= 2 && path[1] == ':' && IsWindowsDriveLetter(path[0]); + } + + static private bool IsWindowsDriveLetter(char c) + { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + static private bool IsWindowsUncRoot(string path) + { + if (path.Length < 2 || (path[0] != '\\' && path[0] != '/') || (path[1] != '\\' && path[1] != '/')) + { + return false; + } + + int serverSeparatorIndex = path.IndexOfAny(['\\', '/'], 2); + if (serverSeparatorIndex < 0 || serverSeparatorIndex == path.Length - 1) + { + return true; + } + + return path.IndexOfAny(['\\', '/'], serverSeparatorIndex + 1) < 0; + } + /// /// Returns the directory string given a path. Handles XI path. It is a simple /// wrapper so it will be Garbage-In|Garbage-out. @@ -472,19 +525,45 @@ static public string GetDirectoryName(string s1, PathType pathType) } else if (pathType == PathType.Windows) { - return Path.GetDirectoryName(s1); + // Don't use System.IO.Path here: its behavior follows the host OS, but a Windows path's + // directory name needs to be computed the same way whether we're running on Windows or Linux. + if (s1.Length == 0) + { + return s1; + } + // A bare drive root (e.g. "C:\") has no parent directory + if (s1.Length == 3 && IsWindowsDriveLetter(s1[0]) && s1[1] == ':' && (s1[2] == '\\' || s1[2] == '/')) + { + return null; + } + if (IsWindowsUncRoot(s1)) + { + return null; + } + var lastSeparatorIndex = s1.LastIndexOfAny(['\\', '/']); + if (lastSeparatorIndex < 0) + { + return String.Empty; + } + var dir = s1[..lastSeparatorIndex]; + // Path.GetDirectoryName keeps the trailing separator for a bare drive root, e.g. "C:\MyDir" -> "C:\" + if (dir.Length == 2 && IsWindowsDriveLetter(dir[0]) && dir[1] == ':') + { + dir += '\\'; + } + return dir; } else // mimic Path.GetDirectoryName { if (s1 == null || s1.Trim().Length == 0) { - throw new ArgumentNullException("s1"); + throw new ArgumentNullException(nameof(s1)); } if (s1 == "/") { return null; } - var lastSeparatorIndex = s1.LastIndexOf("/", StringComparison.Ordinal); + var lastSeparatorIndex = s1.LastIndexOf('/'); // No directory if (lastSeparatorIndex < 0) { @@ -543,7 +622,7 @@ static public bool IsRooted(string path, PathType pathType) { pathType = path.StartsWith("/") ? PathType.Linux : PathType.Windows; } - return pathType == PathType.Windows ? Path.IsPathRooted(path) : path.StartsWith("/"); + return pathType == PathType.Windows ? IsWindowsPathRooted(path) : path.StartsWith("/"); } } } diff --git a/src/Microsoft/SqlServer/Management/SqlEnum/XmlRead.CS b/src/Microsoft/SqlServer/Management/SqlEnum/XmlRead.CS index 1abd0098..471c3016 100644 --- a/src/Microsoft/SqlServer/Management/SqlEnum/XmlRead.CS +++ b/src/Microsoft/SqlServer/Management/SqlEnum/XmlRead.CS @@ -432,19 +432,20 @@ namespace Microsoft.SqlServer.Management.Smo string realPath = CodeGenSettings.Directory; // Additional paths to look into. Pray that they have no duplicates! + // Built with Path.Combine so they resolve correctly on both Windows and Linux. string[] subPaths = { - @"..\..\..\SqlEnum\xml", - @"..\..\..\SqlEnum\xml\endpoint", - @"..\..\..\SqlEnum\xml\extendedproperty", - @"..\..\..\SqlEnum\xml\include", - @"..\..\..\SqlEnum\xml\perm", - @"..\..\..\Smo.Broker\xml", - @"..\..\..\SMO\Enumerator\sql\src\xml", - @"..\..\..\SMO\Enumerator\sql\src\xml\endpoint", - @"..\..\..\SMO\Enumerator\sql\src\xml\extendedproperty", - @"..\..\..\SMO\Enumerator\sql\src\xml\include", - @"..\..\..\SMO\Enumerator\sql\src\xml\perm", - @"..\..\..\SMO\Enumerator\ServiceBroker\src\xml" }; + Path.Combine("..", "..", "..", "SqlEnum", "xml"), + Path.Combine("..", "..", "..", "SqlEnum", "xml", "endpoint"), + Path.Combine("..", "..", "..", "SqlEnum", "xml", "extendedproperty"), + Path.Combine("..", "..", "..", "SqlEnum", "xml", "include"), + Path.Combine("..", "..", "..", "SqlEnum", "xml", "perm"), + Path.Combine("..", "..", "..", "Smo.Broker", "xml"), + Path.Combine("..", "..", "..", "SMO", "Enumerator", "sql", "src", "xml"), + Path.Combine("..", "..", "..", "SMO", "Enumerator", "sql", "src", "xml", "endpoint"), + Path.Combine("..", "..", "..", "SMO", "Enumerator", "sql", "src", "xml", "extendedproperty"), + Path.Combine("..", "..", "..", "SMO", "Enumerator", "sql", "src", "xml", "include"), + Path.Combine("..", "..", "..", "SMO", "Enumerator", "sql", "src", "xml", "perm"), + Path.Combine("..", "..", "..", "SMO", "Enumerator", "ServiceBroker", "src", "xml") }; string realFile = ""; foreach( string subPath in subPaths ) diff --git a/src/Microsoft/dirs.proj b/src/Microsoft/dirs.proj index d50c2057..564457b5 100644 --- a/src/Microsoft/dirs.proj +++ b/src/Microsoft/dirs.proj @@ -4,5 +4,7 @@ + + \ No newline at end of file diff --git a/src/UnitTest/Common/UnitTestBase.cs b/src/UnitTest/Common/UnitTestBase.cs index b3244a4b..e29b5221 100644 --- a/src/UnitTest/Common/UnitTestBase.cs +++ b/src/UnitTest/Common/UnitTestBase.cs @@ -37,4 +37,16 @@ public virtual void MyTestCleanup() } } + + public static class StringTestExtensions + { + /// + /// Converts \r\n in a string literal to Environment.NewLine, so string literals authored + /// with \r\n compare correctly against output that uses the host OS's native newline. + /// + public static string FixNewLines(this string input) + { + return Environment.NewLine == "\r\n" ? input : input.Replace("\r\n", Environment.NewLine); + } + } } \ No newline at end of file diff --git a/src/UnitTest/ConnectionInfo/ServerInformationTests.cs b/src/UnitTest/ConnectionInfo/ServerInformationTests.cs index 5509a6fd..01d7bbde 100644 --- a/src/UnitTest/ConnectionInfo/ServerInformationTests.cs +++ b/src/UnitTest/ConnectionInfo/ServerInformationTests.cs @@ -33,22 +33,7 @@ public void GetServerInformation_executes_one_query_and_accepts_three_result_tab var connectMock = new Mock(); var commandMock = new Mock(); var dataAdapterMock = new Mock(); - var expectedScript = @"DECLARE @edition sysname; -SET @edition = cast(SERVERPROPERTY(N'EDITION') as sysname); -SELECT case when @edition = N'SQL Azure' then 2 else 1 end as 'DatabaseEngineType', -@edition AS Edition, -CONVERT(bigint, SERVERPROPERTY(N'EditionID')) AS EditionID, -SERVERPROPERTY('EngineEdition') AS DatabaseEngineEdition, -SERVERPROPERTY('ProductVersion') AS ProductVersion, -@@MICROSOFTVERSION AS MicrosoftVersion, -case when serverproperty('EngineEdition') = 12 then 1 when serverproperty('EngineEdition') = 11 and @@version like 'Microsoft Azure SQL Data Warehouse%' then 1 else 0 end as IsFabricServer, -convert(sysname, SERVERPROPERTY(N'Collation')) AS Collation; -select host_platform from sys.dm_os_host_info -if @edition = N'SQL Azure' - select 'TCP' as ConnectionProtocol -else - exec ('select CONVERT(nvarchar(40),CONNECTIONPROPERTY(''net_transport'')) as ConnectionProtocol') -"; + var expectedScript = "DECLARE @edition sysname;\r\nSET @edition = cast(SERVERPROPERTY(N'EDITION') as sysname);\r\nSELECT case when @edition = N'SQL Azure' then 2 else 1 end as 'DatabaseEngineType',\r\n@edition AS Edition,\r\nCONVERT(bigint, SERVERPROPERTY(N'EditionID')) AS EditionID,\r\nSERVERPROPERTY('EngineEdition') AS DatabaseEngineEdition,\r\nSERVERPROPERTY('ProductVersion') AS ProductVersion,\r\n@@MICROSOFTVERSION AS MicrosoftVersion,\r\ncase when serverproperty('EngineEdition') = 12 then 1 when serverproperty('EngineEdition') = 11 and @@version like 'Microsoft Azure SQL Data Warehouse%' then 1 else 0 end as IsFabricServer,\r\nconvert(sysname, SERVERPROPERTY(N'Collation')) AS Collation;\r\nselect host_platform from sys.dm_os_host_info\r\nif @edition = N'SQL Azure' \r\n select 'TCP' as ConnectionProtocol\r\nelse\r\n exec ('select CONVERT(nvarchar(40),CONNECTIONPROPERTY(''net_transport'')) as ConnectionProtocol')\r\n"; // The Moq verification doesn't print a nice diff of the expected/actual, // so let's use the nUnit assert for the string comparison commandMock.SetupSet(c => c.CommandText = It.IsAny()). @@ -89,22 +74,7 @@ public void When_ServerVersion_is_less_than_14_HostPlatform_is_hard_coded_Window var commandMock = new Mock(); var dataAdapterMock = new Mock(); var expectedScript = - @"DECLARE @edition sysname; -SET @edition = cast(SERVERPROPERTY(N'EDITION') as sysname); -SELECT case when @edition = N'SQL Azure' then 2 else 1 end as 'DatabaseEngineType', -@edition AS Edition, -CONVERT(bigint, SERVERPROPERTY(N'EditionID')) AS EditionID, -SERVERPROPERTY('EngineEdition') AS DatabaseEngineEdition, -SERVERPROPERTY('ProductVersion') AS ProductVersion, -@@MICROSOFTVERSION AS MicrosoftVersion, -case when serverproperty('EngineEdition') = 12 then 1 when serverproperty('EngineEdition') = 11 and @@version like 'Microsoft Azure SQL Data Warehouse%' then 1 else 0 end as IsFabricServer, -convert(sysname, SERVERPROPERTY(N'Collation')) AS Collation; -select N'Windows' as host_platform -if @edition = N'SQL Azure' - select 'TCP' as ConnectionProtocol -else - exec ('select CONVERT(nvarchar(40),CONNECTIONPROPERTY(''net_transport'')) as ConnectionProtocol') -"; + "DECLARE @edition sysname;\r\nSET @edition = cast(SERVERPROPERTY(N'EDITION') as sysname);\r\nSELECT case when @edition = N'SQL Azure' then 2 else 1 end as 'DatabaseEngineType',\r\n@edition AS Edition,\r\nCONVERT(bigint, SERVERPROPERTY(N'EditionID')) AS EditionID,\r\nSERVERPROPERTY('EngineEdition') AS DatabaseEngineEdition,\r\nSERVERPROPERTY('ProductVersion') AS ProductVersion,\r\n@@MICROSOFTVERSION AS MicrosoftVersion,\r\ncase when serverproperty('EngineEdition') = 12 then 1 when serverproperty('EngineEdition') = 11 and @@version like 'Microsoft Azure SQL Data Warehouse%' then 1 else 0 end as IsFabricServer,\r\nconvert(sysname, SERVERPROPERTY(N'Collation')) AS Collation;\r\nselect N'Windows' as host_platform\r\nif @edition = N'SQL Azure' \r\n select 'TCP' as ConnectionProtocol\r\nelse\r\n exec ('select CONVERT(nvarchar(40),CONNECTIONPROPERTY(''net_transport'')) as ConnectionProtocol')\r\n"; // The Moq verification doesn't print a nice diff of the expected/actual, // so let's use the nUnit assert for the string comparison diff --git a/src/UnitTest/ConnectionInfo/SqlConnectionInfoTests.cs b/src/UnitTest/ConnectionInfo/SqlConnectionInfoTests.cs index 27786359..12bc8957 100644 --- a/src/UnitTest/ConnectionInfo/SqlConnectionInfoTests.cs +++ b/src/UnitTest/ConnectionInfo/SqlConnectionInfoTests.cs @@ -11,6 +11,7 @@ using NUnit.Framework; using System; using System.ComponentModel; +using System.Runtime.InteropServices; using Assert = NUnit.Framework.Assert; @@ -34,6 +35,11 @@ public void SqlConnectionInfoWithConnection_copy_returns_editable_ServerConnecti [TestCategory("Unit")] public void SqlConnectionInfo_impersonation_failure_throws_ConnectionFailureException() { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Assert.Ignore (NUnit) isn't understood by the MSTest adapter running this class and is reported as a failure, so use MSTest's own Inconclusive instead. + Microsoft.VisualStudio.TestTools.UnitTesting.Assert.Inconclusive("ConnectAsUser impersonation relies on the Windows LogonUser API (advapi32.dll), which isn't supported on this platform."); + } var conn = new ServerConnection("someservername") { ConnectAsUser = true, ConnectAsUserName = "someuser", ConnectAsUserPassword = Guid.NewGuid().ToString() }; var ex = Assert.Throws(conn.Connect, "Connect() should throw ConnectionFailureException when impersonation fails"); Assert.That(ex.InnerException, Is.InstanceOf(), "InnerException"); diff --git a/src/UnitTest/Directory.Build.targets b/src/UnitTest/Directory.Build.targets index 8b3953b5..6bdd3524 100644 --- a/src/UnitTest/Directory.Build.targets +++ b/src/UnitTest/Directory.Build.targets @@ -5,25 +5,32 @@ - /Settings:$(EnlistmentRoot)\src\UnitTest\unittest.runsettings - $(PkgMicrosoft_TestPlatform)\tools\net462\Common7\IDE\Extensions\TestPlatform\ + /Settings:$(EnlistmentRoot)/src/UnitTest/unittest.runsettings + --settings "$(EnlistmentRoot)/src/UnitTest/unittest.runsettings" + $(PkgMicrosoft_TestPlatform)/tools/net462/Common7/IDE/Extensions/TestPlatform/ $(OutputPath)$(AssemblyName).dll Unit /TestCaseFilter:"TestCategory=$(TestCategory)" + --filter "TestCategory=$(TestCategory)" - - + + + + + + + - + diff --git a/src/UnitTest/Dmf/ExpressionTreeTests.cs b/src/UnitTest/Dmf/ExpressionTreeTests.cs index 42c0a43a..7c380a1f 100644 --- a/src/UnitTest/Dmf/ExpressionTreeTests.cs +++ b/src/UnitTest/Dmf/ExpressionTreeTests.cs @@ -51,7 +51,7 @@ public void ExpressionNode_Parse() var xml = SerializeExpressionNode(node); Assert.That(node.ToString(), Is.EqualTo(testCase[0]), $"Should round trip expression [{testCase[0]}]"); Assert.That(node.ToStringForDisplay(), Is.EqualTo(testCase[1]), $"ToStringForDisplay of [{testCase[0]}]"); - Assert.That(xml, Is.EqualTo(testCase[2]), $"SerializeExpressionNode for [{testCase[0]}]"); + Assert.That(xml, Is.EqualTo(testCase[2].FixNewLines()), $"SerializeExpressionNode for [{testCase[0]}]"); } }); } diff --git a/src/UnitTest/RegisteredServers/RegisteredServersTests.cs b/src/UnitTest/RegisteredServers/RegisteredServersTests.cs index 6dbd23b7..b70ca267 100644 --- a/src/UnitTest/RegisteredServers/RegisteredServersTests.cs +++ b/src/UnitTest/RegisteredServers/RegisteredServersTests.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.InteropServices; using Assert = NUnit.Framework.Assert; namespace Microsoft.SqlServer.Test.RegisteredServersUnitTests @@ -75,6 +76,11 @@ public void RegisteredServersStore_initializes_from_valid_file() [TestCategory("Unit")] public void ServerGroup_Alter_saves_file_with_added_server() { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Assert.Ignore (NUnit) isn't understood by the MSTest adapter running this class and is reported as a failure, so use MSTest's own Inconclusive instead. + Microsoft.VisualStudio.TestTools.UnitTesting.Assert.Inconclusive("Encrypted credential persistence relies on Windows DPAPI (ProtectedData), which isn't supported on this platform."); + } var tempFile = Path.GetTempFileName(); File.Delete(tempFile); try diff --git a/src/UnitTest/Sfc/Microsoft.SqlServer.Test.SfcUnitTests.csproj b/src/UnitTest/Sfc/Microsoft.SqlServer.Test.SfcUnitTests.csproj index 9254a0d6..52cb05c4 100644 --- a/src/UnitTest/Sfc/Microsoft.SqlServer.Test.SfcUnitTests.csproj +++ b/src/UnitTest/Sfc/Microsoft.SqlServer.Test.SfcUnitTests.csproj @@ -8,5 +8,6 @@ + \ No newline at end of file diff --git a/src/UnitTest/Smo/PathWrapperTests.cs b/src/UnitTest/Smo/PathWrapperTests.cs index c62f6f16..55f25e3d 100644 --- a/src/UnitTest/Smo/PathWrapperTests.cs +++ b/src/UnitTest/Smo/PathWrapperTests.cs @@ -26,6 +26,7 @@ public void When_PathType_is_Windows_Combine_mimics_PathdotCombine() Assert.That(PathWrapper.Combine(@"c:^*&)(_=@#'\^.*(.txt", @"subdir\file.txt"), Is.EqualTo(@"c:^*&)(_=@#'\^.*(.txt\subdir\file.txt"), "unsupported characters"); Assert.That(PathWrapper.Combine(@"", @"subdir\file.txt"), Is.EqualTo(@"subdir\file.txt"), "path1 empty"); + Assert.That(PathWrapper.Combine(@"c:\temp", ""), Is.EqualTo(@"c:\temp"), "path2 empty"); Assert.That(PathWrapper.Combine(@"\\server\share\", "folder"), Is.EqualTo(@"\\server\share\folder"), "unc path"); Assert.That( @@ -75,6 +76,8 @@ public void When_PathType_is_Windows_GetDirectoryName_mimics_PathdotGetDirectory Assert.That(PathWrapper.GetDirectoryName(@"C:\MyDir"), Is.EqualTo(@"C:\"), "folder in root"); Assert.That(PathWrapper.GetDirectoryName(@"C:\"), Is.Null, "root"); Assert.That(PathWrapper.GetDirectoryName(@"filename", PathType.Windows), Is.Empty, "no folder"); + Assert.That(PathWrapper.GetDirectoryName(@"\\server\share"), Is.Null, "unc share root"); + Assert.That(PathWrapper.GetDirectoryName(@"\\server\share\"), Is.EqualTo(@"\\server\share"), "unc share root with trailing separator"); Assert.That(PathWrapper.GetDirectoryName(@"\\server\share\folder"), Is.EqualTo(@"\\server\share"), "unc path"); } diff --git a/src/UnitTest/dirs.proj b/src/UnitTest/dirs.proj index 2fa73a2b..e9df8845 100644 --- a/src/UnitTest/dirs.proj +++ b/src/UnitTest/dirs.proj @@ -2,5 +2,7 @@ + + \ No newline at end of file