diff --git a/test/NuGet.Config b/NuGet.Config similarity index 100% rename from test/NuGet.Config rename to NuGet.Config diff --git a/test-msbuild/NuGet.Config b/NuGet.withDevFeed.Config similarity index 74% rename from test-msbuild/NuGet.Config rename to NuGet.withDevFeed.Config index 5d8cd8b..dc288e2 100644 --- a/test-msbuild/NuGet.Config +++ b/NuGet.withDevFeed.Config @@ -3,6 +3,5 @@ - diff --git a/build.cmd b/build.cmd index aa85df8..731409b 100644 --- a/build.cmd +++ b/build.cmd @@ -1,5 +1,4 @@ @echo off -powershell -NoProfile -NoLogo -Command "%~dp0scripts\build.ps1 %*; exit $LastExitCode;" +powershell -NoProfile -NoLogo -Command "%~dp0\run-build.ps1 %*; exit $LastExitCode;" if %errorlevel% neq 0 exit /b %errorlevel% - diff --git a/build.proj b/build.proj index 742b165..3166511 100644 --- a/build.proj +++ b/build.proj @@ -24,9 +24,20 @@ .exe + + $(RepoRoot)/packages + $(RepoRoot)/artifacts - + + 1.0.0-beta-060000 + 1.0.0-beta-060000 + + + + + + - diff --git a/build/Build.targets b/build/Build.targets new file mode 100644 index 0000000..c88096c --- /dev/null +++ b/build/Build.targets @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/Test.targets b/build/Test.targets new file mode 100644 index 0000000..d409d14 --- /dev/null +++ b/build/Test.targets @@ -0,0 +1,63 @@ + + + + + + <_CommandText> + + + + + + + + + + + $(FSharpSdkVersion) + $(FSharpNETSdkVersion) + + + + <_NupkgsToTest Include="$(ArtifactsDir)/nupkgs/FSharp.Sdk.$(FSharpSdkVersionToTest).nupkg" /> + <_NupkgsToTest Include="$(ArtifactsDir)/nupkgs/FSharp.NET.Sdk.$(FSharpNETSdkVersionToTest).nupkg" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-msbuild/TestAppConsole/Program.fs b/examples/preview2.1/console/Program.fs similarity index 78% rename from test-msbuild/TestAppConsole/Program.fs rename to examples/preview2.1/console/Program.fs index a4928de..d5fd26e 100644 --- a/test-msbuild/TestAppConsole/Program.fs +++ b/examples/preview2.1/console/Program.fs @@ -4,5 +4,5 @@ open System [] let main argv = - printfn "Hello World from F#!" + printfn "Hello World!" 0 // return an integer exit code diff --git a/examples/preview2.1/console/project.json b/examples/preview2.1/console/project.json new file mode 100644 index 0000000..4124075 --- /dev/null +++ b/examples/preview2.1/console/project.json @@ -0,0 +1,27 @@ +{ + "version": "1.0.0-*", + "buildOptions": { + "debugType": "portable", + "emitEntryPoint": true, + "compilerName": "fsc", + "compile": { + "includeFiles": [ + "Program.fs" + ] + } + }, + "tools": { + "dotnet-compile-fsc": "1.0.0-preview2.1-*" + }, + "frameworks": { + "netcoreapp1.1": { + "dependencies": { + "Microsoft.NETCore.App": { + "type": "platform", + "version": "1.1.0" + }, + "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160629" + } + } + } +} diff --git a/examples/preview3/lib/Library.fs b/examples/preview2.1/lib/Library.fs similarity index 100% rename from examples/preview3/lib/Library.fs rename to examples/preview2.1/lib/Library.fs diff --git a/test/TestLibrary/project.json b/examples/preview2.1/lib/project.json similarity index 50% rename from test/TestLibrary/project.json rename to examples/preview2.1/lib/project.json index 95eaad3..7bb4132 100644 --- a/test/TestLibrary/project.json +++ b/examples/preview2.1/lib/project.json @@ -5,25 +5,19 @@ "compilerName": "fsc", "compile": { "includeFiles": [ - "Helper2.fs", - "Helper.fs" + "Library.fs" ] } }, - "dependencies": { - "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-*" + "tools": { + "dotnet-compile-fsc": "1.0.0-preview2.1-*" }, "frameworks": { "netstandard1.6": { "dependencies": { - "NETStandard.Library": "1.6.0" + "NETStandard.Library": "1.6.1", + "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160629" } } - }, - "tools": { - "dotnet-compile-fsc": { - "version": "1.0.0-preview2-*", - "imports": "dnxcore50" - } } } diff --git a/scripts/build.ps1 b/run-build.ps1 similarity index 71% rename from scripts/build.ps1 rename to run-build.ps1 index 41dfa34..c55501d 100644 --- a/scripts/build.ps1 +++ b/run-build.ps1 @@ -17,16 +17,18 @@ if($Help) } #make path absolute -$rootDir = Split-Path -parent (Split-Path -parent $PSCommandPath) +$RepoRoot = "$PSScriptRoot" + +$sdkVersion = '1.0.0-rc3-004530' function Install-DotnetSdk([string] $sdkVersion) { Write-Host "# Install .NET Core Sdk versione '$sdkVersion'" -foregroundcolor "magenta" - $sdkInstallScriptUrl = "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview3/scripts/obtain/dotnet-install.ps1" + $sdkInstallScriptUrl = "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-rc3/scripts/obtain/dotnet-install.ps1" $sdkInstallScriptPath = ".dotnetsdk\dotnet_cli_install.ps1" Write-Host "Downloading sdk install script '$sdkInstallScriptUrl' to '$sdkInstallScriptPath'" - New-Item "$rootDir\.dotnetsdk" -Type directory -ErrorAction Ignore - Invoke-WebRequest $sdkInstallScriptUrl -OutFile "$rootDir\$sdkInstallScriptPath" + New-Item "$RepoRoot\.dotnetsdk" -Type directory -ErrorAction Ignore + Invoke-WebRequest $sdkInstallScriptUrl -OutFile "$RepoRoot\$sdkInstallScriptPath" Write-Host "Running sdk install script..." ./.dotnetsdk/dotnet_cli_install.ps1 -InstallDir ".dotnetsdk\sdk-$sdkVersion" -Channel "preview" -version $sdkVersion @@ -45,28 +47,16 @@ function Run-Cmd function Using-Sdk ([string] $sdkVersion) { - $sdkPath = "$rootDir\.dotnetsdk\sdk-$sdkVersion" + $sdkPath = "$RepoRoot\.dotnetsdk\sdk-$sdkVersion" Write-Host "# Using sdk '$sdkVersion'" -foregroundcolor "magenta" $env:Path = "$sdkPath;$env:Path" Run-Cmd "dotnet" "--version" } -function Do-preview3 -{ - Install-DotnetSdk '1.0.0-preview3-004056' - - Using-Sdk '1.0.0-preview3-004056' - - dotnet msbuild build.proj /m /p:Architecture=$Architecture $ExtraParameters - if ($LASTEXITCODE -ne 0) { throw "Failed to build" } -} - # main -try { - Push-Location $PWD +Install-DotnetSdk $sdkVersion - Do-preview3 -} -finally { - Pop-Location -} +Using-Sdk $sdkVersion + +dotnet msbuild build.proj /m /v:diag /p:Architecture=$Architecture $ExtraParameters +if ($LASTEXITCODE -ne 0) { throw "Failed to build" } diff --git a/run-build.sh b/run-build.sh index d7c42ca..5ce1ae8 100755 --- a/run-build.sh +++ b/run-build.sh @@ -140,13 +140,13 @@ if [ ! -d ".dotnetsdk" ]; then mkdir ".dotnetsdk" fi -sdkInstallScriptUrl=https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-preview3/scripts/obtain/dotnet-install.sh -sdkInstallScriptPath=$REPOROOT/.dotnetsdk/dotnet_cli_install.ps1 +sdkInstallScriptUrl=https://raw.githubusercontent.com/dotnet/cli/rel/1.0.0-rc3/scripts/obtain/dotnet-install.sh +sdkInstallScriptPath=$REPOROOT/.dotnetsdk/dotnet_cli_install.sh download $sdkInstallScriptUrl $sdkInstallScriptPath chmod u+x $sdkInstallScriptPath -sdkVersion=1.0.0-preview3-004056 +sdkVersion=1.0.0-rc3-004530 sdkPath=$REPOROOT/.dotnetsdk/sdk-$sdkVersion DOTNET_INSTALL_DIR=$sdkPath diff --git a/scripts/docker/centos/Dockerfile b/scripts/docker/centos/Dockerfile index ae3e87d..6bba0fb 100644 --- a/scripts/docker/centos/Dockerfile +++ b/scripts/docker/centos/Dockerfile @@ -6,6 +6,10 @@ # Dockerfile that creates a container suitable to build dotnet-cli FROM centos:7.1.1503 +# Swap the "fakesystemd" package with the real "systemd" package, because fakesystemd conflicts with openssl-devel. +# The CentOS Docker image uses fakesystemd instead of systemd to reduce disk space. +RUN yum -q -y swap -- remove fakesystemd -- install systemd systemd-libs + RUN yum -q -y install deltarpm RUN yum -q -y install epel-release # RUN yum -y update diff --git a/scripts/dockerrun.sh b/scripts/dockerrun.sh index 4c289cc..b478a2a 100755 --- a/scripts/dockerrun.sh +++ b/scripts/dockerrun.sh @@ -132,5 +132,7 @@ docker run $INTERACTIVE -t --rm --sig-proxy=true \ -e CHECKSUM_STORAGE_KEY \ -e CHECKSUM_STORAGE_ACCOUNT \ -e CHECKSUM_STORAGE_CONTAINER \ + -e CLIBUILD_SKIP_TESTS \ + -e CommitCount \ $DOTNET_BUILD_CONTAINER_TAG \ $BUILD_COMMAND "$@" diff --git a/scripts/run-tests.ps1 b/scripts/run-tests.ps1 deleted file mode 100644 index 869bff6..0000000 --- a/scripts/run-tests.ps1 +++ /dev/null @@ -1,126 +0,0 @@ -$global:testSuite = @{} - -# test helper - -function Run-Test { - Param([string] $Name, [scriptblock] $Check) - - Write-Host "## Testing $Name..." -ForegroundColor "magenta" - try { - $Check.Invoke() - Write-Host "## Testing $Name [OK]" -ForegroundColor "green" - $global:testSuite.Add($Name, $TRUE) - } - catch { - Write-Host "## Testing $Name [FAILED]" -ForegroundColor "red" - Write-Host $_.Exception.Message - $global:testSuite.Add($Name, $FALSE) - } -} - -function Dotnet-Build { - Run-Cmd "dotnet" "--verbose build" -} - -function Dotnet-Run { - Param([string] $Arguments) - - Run-Cmd "dotnet" "--verbose run $Arguments" -} - -# dotnet new -<# - -dotnet new doesnt work ootb in preview2 - -Run-Test "dotnet new" { - - Remove-Item "$rootDir\test\test-dotnet-new" -Recurse -ErrorAction Ignore - - mkdir "$rootDir\test\test-dotnet-new" -Force | cd - - Run-Cmd "dotnet" "new --lang f#" - - Run-Cmd "dotnet" "restore -f `"$rootDir\bin`"" - - Dotnet-Build - - Dotnet-Run "c d" -} -#> - -# test from assets - -function Dotnet-Restore { - Run-Cmd "dotnet" "restore -v Information -f `"$rootDir\bin`" --configfile `"$rootDir\test\NuGet.Config`"" -} - -Run-Test "test/TestAppWithArgs" { - - cd "$rootDir\test\TestAppWithArgs" - - Dotnet-Restore - - Dotnet-Build - - Dotnet-Run "" -} - -Run-Test "test/TestLibrary" { - - cd "$rootDir\test\TestLibrary" - - Dotnet-Restore - - Dotnet-Build -} - -Run-Test "test/TestApp" { - - cd "$rootDir\test\TestApp" - - Dotnet-Restore - - Dotnet-Build - - Dotnet-Run "" -} - -# test templates - -function Dotnet-Restore-OnlyFallback { - Run-Cmd "dotnet" "restore -v Information -f `"$rootDir\bin`"" -} - -Run-Test "examples/preview2/console" { - - cd "$rootDir\examples\preview2\console" - - Dotnet-Restore-OnlyFallback - - Dotnet-Build - - Dotnet-Run "" -} - -Run-Test "examples/preview2/lib" { - - cd "$rootDir\examples\preview2\lib" - - Dotnet-Restore-OnlyFallback - - Dotnet-Build -} - -# results - -Write-Host "# Tests results" -ForegroundColor "magenta" -foreach ($h in $global:testSuite.GetEnumerator()) { - $color = If ($h.Value) {"green"} Else {"red"} - $text = If ($h.Value) {"PASSED"} Else {"FAILED"} - Write-Host "- $($h.Name): [$text]" -ForegroundColor $color -} - -If ($global:testSuite.ContainsValue($FALSE)) { - exit 2 -} diff --git a/src/FSharp.NET.Sdk/FSharp.NET.Sdk.csproj b/src/FSharp.NET.Sdk/FSharp.NET.Sdk.csproj new file mode 100644 index 0000000..1de4a99 --- /dev/null +++ b/src/FSharp.NET.Sdk/FSharp.NET.Sdk.csproj @@ -0,0 +1,40 @@ + + + + netstandard1.3;net40 + + FSharp.NET.Sdk + + FSharp.NET.Sdk + Enrico Sada + F# and .NET Core SDK working together + Compatible with .NET Core Sdk preview4/rc3 + f#;sdk;fsharp;msbuild + https://github.com/dotnet/netcorecli-fsc + git + https://github.com/dotnet/netcorecli-fsc + + false + true + + + + + . + true + + + . + true + + + . + true + + + . + true + + + + diff --git a/src/FSharp.NET.Sdk/FSharp.NET.Core.Sdk.targets b/src/FSharp.NET.Sdk/build/FSharp.NET.Core.Sdk.targets similarity index 92% rename from src/FSharp.NET.Sdk/FSharp.NET.Core.Sdk.targets rename to src/FSharp.NET.Sdk/build/FSharp.NET.Core.Sdk.targets index b778491..9e56153 100644 --- a/src/FSharp.NET.Sdk/FSharp.NET.Core.Sdk.targets +++ b/src/FSharp.NET.Sdk/build/FSharp.NET.Core.Sdk.targets @@ -123,12 +123,18 @@ this file. $(_IntermediateOutputPathFull)dotnet-compile.rsp + + + <_DotNetHostExecutableName>$(MSBuildExtensionsPath)/../../dotnet + <_DotNetHostExecutableName Condition=" '$(OS)' == 'Windows_NT' ">$(_DotNetHostExecutableName).exe - + diff --git a/src/FSharp.NET.Sdk/FSharp.NET.Current.Sdk.targets b/src/FSharp.NET.Sdk/build/FSharp.NET.Current.Sdk.targets similarity index 100% rename from src/FSharp.NET.Sdk/FSharp.NET.Current.Sdk.targets rename to src/FSharp.NET.Sdk/build/FSharp.NET.Current.Sdk.targets diff --git a/src/FSharp.NET.Sdk/FSharp.NET.Sdk.props b/src/FSharp.NET.Sdk/build/FSharp.NET.Sdk.props similarity index 77% rename from src/FSharp.NET.Sdk/FSharp.NET.Sdk.props rename to src/FSharp.NET.Sdk/build/FSharp.NET.Sdk.props index 19a7a74..d0c5f60 100644 --- a/src/FSharp.NET.Sdk/FSharp.NET.Sdk.props +++ b/src/FSharp.NET.Sdk/build/FSharp.NET.Sdk.props @@ -14,6 +14,13 @@ WARNING: You CAN MODIFY this file, doesnt matter if you are not knowledgeable a 4 + + + false + + + {F2A71F9B-5D33-465A-A702-920D77279786} + $(MSBuildThisFileDirectory)\FSharp.NET.Current.Sdk.targets diff --git a/src/FSharp.NET.Sdk/FSharp.NET.CrossTargeting.Sdk.targets b/src/FSharp.NET.Sdk/buildCrossTargeting/FSharp.NET.CrossTargeting.Sdk.targets similarity index 100% rename from src/FSharp.NET.Sdk/FSharp.NET.CrossTargeting.Sdk.targets rename to src/FSharp.NET.Sdk/buildCrossTargeting/FSharp.NET.CrossTargeting.Sdk.targets diff --git a/src/FSharp.NET.Sdk/project.json b/src/FSharp.NET.Sdk/project.json deleted file mode 100644 index 8fddaae..0000000 --- a/src/FSharp.NET.Sdk/project.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "version": "1.0.0-beta-*", - "frameworks": { - "netstandard1.3": {} - }, - "packOptions": { - "summary": "The .NET Sdk preview4 and F# integration", - "tags": [ "f#", "sdk", "fsharp", "msbuild" ], - "owners": [ "Enrico Sada" ], - "releaseNotes": "use Sdk attribute", - "projectUrl": "https://github.com/dotnet/netcorecli-fsc", - "requireLicenseAcceptance": false, - "repository": { - "type": "git", - "url": "https://github.com/dotnet/netcorecli-fsc" - }, - "files": { - "mappings": { - "Sdk/Sdk.props": "Sdk/Sdk.props", - "Sdk/Sdk.OnRestore.targets": "Sdk/Sdk.OnRestore.targets", - "Sdk/Sdk.targets": "Sdk/Sdk.targets", - "buildCrossTargeting/FSharp.NET.Sdk.targets": "FSharp.NET.CrossTargeting.Sdk.targets", - "build/FSharp.NET.Sdk.props": "FSharp.NET.Sdk.props", - "build/FSharp.NET.Core.Sdk.targets": "FSharp.NET.Core.Sdk.targets", - "build/FSharp.NET.Current.Sdk.targets": "FSharp.NET.Current.Sdk.targets" - } - } - } -} diff --git a/src/FSharp.Sdk/FSharp.Sdk.csproj b/src/FSharp.Sdk/FSharp.Sdk.csproj new file mode 100644 index 0000000..9f1b2dd --- /dev/null +++ b/src/FSharp.Sdk/FSharp.Sdk.csproj @@ -0,0 +1,36 @@ + + + + netstandard1.3 + + FSharp.Sdk + + FSharp.Sdk + Enrico Sada + SDK for F# bundled inside .NET Core SDK + Compatible with .NET Core Sdk preview4 + f#;sdk;fsharp;msbuild + https://github.com/dotnet/netcorecli-fsc + git + https://github.com/dotnet/netcorecli-fsc + + false + true + + + + + . + true + + + . + true + + + . + true + + + + diff --git a/src/FSharp.NET.Sdk/Sdk/Sdk.OnRestore.targets b/src/FSharp.Sdk/Sdk/Sdk.OnRestore.targets similarity index 100% rename from src/FSharp.NET.Sdk/Sdk/Sdk.OnRestore.targets rename to src/FSharp.Sdk/Sdk/Sdk.OnRestore.targets diff --git a/src/FSharp.NET.Sdk/Sdk/Sdk.props b/src/FSharp.Sdk/Sdk/Sdk.props similarity index 100% rename from src/FSharp.NET.Sdk/Sdk/Sdk.props rename to src/FSharp.Sdk/Sdk/Sdk.props diff --git a/src/FSharp.NET.Sdk/Sdk/Sdk.targets b/src/FSharp.Sdk/Sdk/Sdk.targets similarity index 100% rename from src/FSharp.NET.Sdk/Sdk/Sdk.targets rename to src/FSharp.Sdk/Sdk/Sdk.targets diff --git a/test-msbuild/TestApp/TestApp.fsproj b/test-msbuild/TestApp/TestApp.fsproj deleted file mode 100644 index 84faa16..0000000 --- a/test-msbuild/TestApp/TestApp.fsproj +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - Exe - netcoreapp1.0 - - - - 1.0.0-preview3 - Enrico Sada - - - - - - - - - - 1.0.1 - - - $(MicrosoftFSharpCorenetcoreVersion) - - - $(MicrosoftNETSdkVersion) - All - - - $(FSharpNETSdkVersion) - All - - - TestLibrary - - - - - - 1.0.0-preview2-020000 - - - - - - diff --git a/test-msbuild/TestAppConsole/TestAppConsole.fsproj b/test-msbuild/TestAppConsole/TestAppConsole.fsproj deleted file mode 100644 index 0905d8a..0000000 --- a/test-msbuild/TestAppConsole/TestAppConsole.fsproj +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - Exe - netcoreapp1.0 - - - - 1.0.0-preview3 - Enrico Sada - - - - - - - - - - 1.0.1 - - - $(MicrosoftFSharpCorenetcoreVersion) - - - $(MicrosoftNETSdkVersion) - All - - - $(FSharpNETSdkVersion) - All - - - - - - 1.0.0-preview2-020000 - - - - - - diff --git a/test-msbuild/TestAppUsingSdkTargetDirectly/Console1.fsproj b/test-msbuild/TestAppUsingSdkTargetDirectly/Console1.fsproj deleted file mode 100644 index c46649d..0000000 --- a/test-msbuild/TestAppUsingSdkTargetDirectly/Console1.fsproj +++ /dev/null @@ -1,41 +0,0 @@ - - - - - Exe - netcoreapp1.0 - - - - 1.0.0-preview3 - Enrico Sada - - - - - - - - - - 1.0.1 - - - $(MicrosoftFSharpCorenetcoreVersion) - - - $(MicrosoftNETSdkVersion) - All - - - - - - 1.0.0-preview2-020005 - - - - - - - diff --git a/test-msbuild/TestAppUsingSdkTargetDirectly/Program.fs b/test-msbuild/TestAppUsingSdkTargetDirectly/Program.fs deleted file mode 100644 index a4928de..0000000 --- a/test-msbuild/TestAppUsingSdkTargetDirectly/Program.fs +++ /dev/null @@ -1,8 +0,0 @@ -// Learn more about F# at http://fsharp.org - -open System - -[] -let main argv = - printfn "Hello World from F#!" - 0 // return an integer exit code diff --git a/test-msbuild/TestLibrary/TestLibrary.fsproj b/test-msbuild/TestLibrary/TestLibrary.fsproj deleted file mode 100644 index 86f57cf..0000000 --- a/test-msbuild/TestLibrary/TestLibrary.fsproj +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - netstandard1.6 - - - - 1.0.0-preview3 - Enrico Sada - - - - - - - - - - - 1.6.0 - - - $(MicrosoftFSharpCorenetcoreVersion) - - - $(MicrosoftNETSdkVersion) - All - - - $(FSharpNETSdkVersion) - All - - - - - - 1.0.0-preview2-020000 - - - - - - diff --git a/test-msbuild/TestXunit/TestXunit.fsproj b/test-msbuild/TestXunit/TestXunit.fsproj deleted file mode 100644 index 81e63ea..0000000 --- a/test-msbuild/TestXunit/TestXunit.fsproj +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - Exe - netcoreapp1.0 - - - - - - - - - - 1.0.1 - - - $(MicrosoftNETSdkVersion) - All - - - $(MicrosoftFSharpCorenetcoreVersion) - - - $(FSharpNETSdkVersion) - All - - - 15.0.0-preview-20161024-02 - - - 2.2.0-beta3-build3402 - - - 2.2.0-beta4-build1188 - - - - - - 1.0.0-preview2-020000 - - - - - - diff --git a/test-msbuild/TestXunit/Tests.fs b/test-msbuild/TestXunit/Tests.fs deleted file mode 100644 index 7d11a64..0000000 --- a/test-msbuild/TestXunit/Tests.fs +++ /dev/null @@ -1,8 +0,0 @@ -module Tests - -open System -open Xunit - -[] -let ``My test`` () = - Assert.True(true) diff --git a/test/.vscode/launch.json b/test/.vscode/launch.json new file mode 100644 index 0000000..5e99c49 --- /dev/null +++ b/test/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/dotnet-new.Tests/bin/Debug/netcoreapp1.0/dotnet-new.Tests.dll", + "args": [], + "cwd": "${workspaceRoot}", + "externalConsole": false, + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart" + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command.pickProcess}" + } + ] +} \ No newline at end of file diff --git a/test/.vscode/tasks.json b/test/.vscode/tasks.json new file mode 100644 index 0000000..dd93bdb --- /dev/null +++ b/test/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + "version": "0.1.0", + "command": "dotnet", + "isShellCommand": true, + "args": [], + "tasks": [ + { + "taskName": "build", + "args": [ + "${workspaceRoot}/dotnet-new.Tests/dotnet-new.Tests.csproj" + ], + "isBuildCommand": true, + "problemMatcher": "$msCompile" + }, + { + "taskName": "test", + "args": [ + "${workspaceRoot}/dotnet-new.Tests/dotnet-new.Tests.csproj" + ], + "isBuildCommand": true, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/test/CompileFailApp/project.json b/test/CompileFailApp/project.json deleted file mode 100644 index e29c140..0000000 --- a/test/CompileFailApp/project.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "version": "1.0.0-*", - "buildOptions": { - "emitEntryPoint": true, - "compilerName": "fsc", - "compile": { - "includeFiles": [ - "Program.fs" - ] - } - }, - "dependencies": { - "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160509", - "Microsoft.NETCore.App": "1.0.0" - }, - "tools": { - "dotnet-compile-fsc": { - "version": "1.0.0-preview2-*", - "imports": [ - "dnxcore50", - "portable-net45+win81", - "netstandard1.3" - ] - } - }, - "frameworks": { - "netcoreapp1.0": { - "imports": [ - "dnxcore50", - "netstandard1.3" - ] - } - }, - "runtimes": { - "win7-x64": {}, - "win7-x86": {}, - "osx.10.10-x64": {}, - "osx.10.11-x64": {}, - "ubuntu.14.04-x64": {}, - "ubuntu.16.04-x64": {}, - "centos.7-x64": {}, - "rhel.7.2-x64": {}, - "debian.8-x64": {}, - "fedora.23-x64": {}, - "opensuse.13.2-x64": {} - } -} diff --git a/test/Microsoft.DotNet.Cli.Utils/AnsiColorExtensions.cs b/test/Microsoft.DotNet.Cli.Utils/AnsiColorExtensions.cs new file mode 100644 index 0000000..08c617e --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/AnsiColorExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class AnsiColorExtensions + { + public static string Black(this string text) + { + return "\x1B[30m" + text + "\x1B[39m"; + } + + public static string Red(this string text) + { + return "\x1B[31m" + text + "\x1B[39m"; + } + public static string Green(this string text) + { + return "\x1B[32m" + text + "\x1B[39m"; + } + + public static string Yellow(this string text) + { + return "\x1B[33m" + text + "\x1B[39m"; + } + + public static string Blue(this string text) + { + return "\x1B[34m" + text + "\x1B[39m"; + } + + public static string Magenta(this string text) + { + return "\x1B[35m" + text + "\x1B[39m"; + } + + public static string Cyan(this string text) + { + return "\x1B[36m" + text + "\x1B[39m"; + } + + public static string White(this string text) + { + return "\x1B[37m" + text + "\x1B[39m"; + } + + public static string Bold(this string text) + { + return "\x1B[1m" + text + "\x1B[22m"; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/AnsiConsole.cs b/test/Microsoft.DotNet.Cli.Utils/AnsiConsole.cs new file mode 100644 index 0000000..308b226 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/AnsiConsole.cs @@ -0,0 +1,145 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class AnsiConsole + { + private AnsiConsole(TextWriter writer) + { + Writer = writer; + + OriginalForegroundColor = Console.ForegroundColor; + } + + private int _boldRecursion; + + public static AnsiConsole GetOutput() + { + return new AnsiConsole(Console.Out); + } + + public static AnsiConsole GetError() + { + return new AnsiConsole(Console.Error); + } + + public TextWriter Writer { get; } + + public ConsoleColor OriginalForegroundColor { get; } + + private void SetColor(ConsoleColor color) + { + const int Light = 0x08; + int c = (int)color; + + Console.ForegroundColor = + c < 0 ? color : // unknown, just use it + _boldRecursion > 0 ? (ConsoleColor)(c | Light) : // ensure color is light + (ConsoleColor)(c & ~Light); // ensure color is dark + } + + private void SetBold(bool bold) + { + _boldRecursion += bold ? 1 : -1; + if (_boldRecursion > 1 || (_boldRecursion == 1 && !bold)) + { + return; + } + + // switches on _boldRecursion to handle boldness + SetColor(Console.ForegroundColor); + } + + public void WriteLine(string message) + { + Write(message); + Writer.WriteLine(); + } + + + public void Write(string message) + { + var escapeScan = 0; + for (;;) + { + var escapeIndex = message.IndexOf("\x1b[", escapeScan, StringComparison.Ordinal); + if (escapeIndex == -1) + { + var text = message.Substring(escapeScan); + Writer.Write(text); + break; + } + else + { + var startIndex = escapeIndex + 2; + var endIndex = startIndex; + while (endIndex != message.Length && + message[endIndex] >= 0x20 && + message[endIndex] <= 0x3f) + { + endIndex += 1; + } + + var text = message.Substring(escapeScan, escapeIndex - escapeScan); + Writer.Write(text); + if (endIndex == message.Length) + { + break; + } + + switch (message[endIndex]) + { + case 'm': + int value; + if (int.TryParse(message.Substring(startIndex, endIndex - startIndex), out value)) + { + switch (value) + { + case 1: + SetBold(true); + break; + case 22: + SetBold(false); + break; + case 30: + SetColor(ConsoleColor.Black); + break; + case 31: + SetColor(ConsoleColor.Red); + break; + case 32: + SetColor(ConsoleColor.Green); + break; + case 33: + SetColor(ConsoleColor.Yellow); + break; + case 34: + SetColor(ConsoleColor.Blue); + break; + case 35: + SetColor(ConsoleColor.Magenta); + break; + case 36: + SetColor(ConsoleColor.Cyan); + break; + case 37: + SetColor(ConsoleColor.Gray); + break; + case 39: + Console.ForegroundColor = OriginalForegroundColor; + break; + } + } + break; + } + + escapeScan = endIndex + 1; + } + } + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/ArgumentEscaper.cs b/test/Microsoft.DotNet.Cli.Utils/ArgumentEscaper.cs new file mode 100644 index 0000000..c8469a9 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/ArgumentEscaper.cs @@ -0,0 +1,200 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class ArgumentEscaper + { + /// + /// Undo the processing which took place to create string[] args in Main, + /// so that the next process will receive the same string[] args + /// + /// See here for more info: + /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx + /// + /// + /// + public static string EscapeAndConcatenateArgArrayForProcessStart(IEnumerable args) + { + return string.Join(" ", EscapeArgArray(args)); + } + + /// + /// Undo the processing which took place to create string[] args in Main, + /// so that the next process will receive the same string[] args + /// + /// See here for more info: + /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx + /// + /// + /// + public static string EscapeAndConcatenateArgArrayForCmdProcessStart(IEnumerable args) + { + return string.Join(" ", EscapeArgArrayForCmd(args)); + } + + /// + /// Undo the processing which took place to create string[] args in Main, + /// so that the next process will receive the same string[] args + /// + /// See here for more info: + /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx + /// + /// + /// + private static IEnumerable EscapeArgArray(IEnumerable args) + { + var escapedArgs = new List(); + + foreach (var arg in args) + { + escapedArgs.Add(EscapeSingleArg(arg)); + } + + return escapedArgs; + } + + /// + /// This prefixes every character with the '^' character to force cmd to + /// interpret the argument string literally. An alternative option would + /// be to do this only for cmd metacharacters. + /// + /// See here for more info: + /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx + /// + /// + /// + private static IEnumerable EscapeArgArrayForCmd(IEnumerable arguments) + { + var escapedArgs = new List(); + + foreach (var arg in arguments) + { + escapedArgs.Add(EscapeArgForCmd(arg)); + } + + return escapedArgs; + } + + public static string EscapeSingleArg(string arg) + { + var sb = new StringBuilder(); + + var needsQuotes = ShouldSurroundWithQuotes(arg); + var isQuoted = needsQuotes || IsSurroundedWithQuotes(arg); + + if (needsQuotes) sb.Append("\""); + + for (int i = 0; i < arg.Length; ++i) + { + var backslashCount = 0; + + // Consume All Backslashes + while (i < arg.Length && arg[i] == '\\') + { + backslashCount++; + i++; + } + + // Escape any backslashes at the end of the arg + // when the argument is also quoted. + // This ensures the outside quote is interpreted as + // an argument delimiter + if (i == arg.Length && isQuoted) + { + sb.Append('\\', 2 * backslashCount); + } + + // At then end of the arg, which isn't quoted, + // just add the backslashes, no need to escape + else if (i == arg.Length) + { + sb.Append('\\', backslashCount); + } + + // Escape any preceding backslashes and the quote + else if (arg[i] == '"') + { + sb.Append('\\', (2 * backslashCount) + 1); + sb.Append('"'); + } + + // Output any consumed backslashes and the character + else + { + sb.Append('\\', backslashCount); + sb.Append(arg[i]); + } + } + + if (needsQuotes) sb.Append("\""); + + return sb.ToString(); + } + + /// + /// Prepare as single argument to + /// roundtrip properly through cmd. + /// + /// This prefixes every character with the '^' character to force cmd to + /// interpret the argument string literally. An alternative option would + /// be to do this only for cmd metacharacters. + /// + /// See here for more info: + /// http://blogs.msdn.com/b/twistylittlepassagesallalike/archive/2011/04/23/everyone-quotes-arguments-the-wrong-way.aspx + /// + /// + /// + private static string EscapeArgForCmd(string argument) + { + var sb = new StringBuilder(); + + var quoted = ShouldSurroundWithQuotes(argument); + + if (quoted) sb.Append("^\""); + + // Prepend every character with ^ + // This is harmless when passing through cmd + // and ensures cmd metacharacters are not interpreted + // as such + foreach (var character in argument) + { + sb.Append("^"); + sb.Append(character); + } + + if (quoted) sb.Append("^\""); + + return sb.ToString(); + } + + internal static bool ShouldSurroundWithQuotes(string argument) + { + // Don't quote already quoted strings + if (IsSurroundedWithQuotes(argument)) + { + return false; + } + + // Only quote if whitespace exists in the string + return ArgumentContainsWhitespace(argument); + } + + internal static bool IsSurroundedWithQuotes(string argument) + { + return argument.StartsWith("\"", StringComparison.Ordinal) && + argument.EndsWith("\"", StringComparison.Ordinal); + } + + internal static bool ArgumentContainsWhitespace(string argument) + { + return argument.Contains(" ") || argument.Contains("\t") || argument.Contains("\n"); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/BlockingMemoryStream.cs b/test/Microsoft.DotNet.Cli.Utils/BlockingMemoryStream.cs new file mode 100644 index 0000000..e80061b --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/BlockingMemoryStream.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Threading; + +namespace Microsoft.DotNet.Cli.Utils +{ + /// + /// An in-memory stream that will block any read calls until something was written to it. + /// + public sealed class BlockingMemoryStream : Stream + { + private readonly BlockingCollection _buffers = new BlockingCollection(); + private ArraySegment _remaining; + + public override void Write(byte[] buffer, int offset, int count) + { + byte[] tmp = new byte[count]; + Buffer.BlockCopy(buffer, offset, tmp, 0, count); + _buffers.Add(tmp); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (count == 0) + { + return 0; + } + + if (_remaining.Count == 0) + { + byte[] tmp; + if (!_buffers.TryTake(out tmp, Timeout.Infinite) || tmp.Length == 0) + { + return 0; + } + _remaining = new ArraySegment(tmp, 0, tmp.Length); + } + + if (_remaining.Count <= count) + { + count = _remaining.Count; + Buffer.BlockCopy(_remaining.Array, _remaining.Offset, buffer, offset, count); + _remaining = default(ArraySegment); + } + else + { + Buffer.BlockCopy(_remaining.Array, _remaining.Offset, buffer, offset, count); + _remaining = new ArraySegment(_remaining.Array, _remaining.Offset + count, _remaining.Count - count); + } + return count; + } + + public void DoneWriting() + { + _buffers.CompleteAdding(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _buffers.Dispose(); + } + + base.Dispose(disposing); + } + + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length { get { throw new NotImplementedException(); } } + public override long Position { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } + public override void Flush() { } + public override long Seek(long offset, SeekOrigin origin) { throw new NotImplementedException(); } + public override void SetLength(long value) { throw new NotImplementedException(); } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs b/test/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs new file mode 100644 index 0000000..731713a --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/BuiltInCommand.cs @@ -0,0 +1,193 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Cli.Utils +{ + /// + /// A Command that is capable of running in the current process. + /// + public class BuiltInCommand : ICommand + { + private readonly IEnumerable _commandArgs; + private readonly Func _builtInCommand; + private readonly IBuiltInCommandEnvironment _environment; + private readonly StreamForwarder _stdOut; + private readonly StreamForwarder _stdErr; + private string _workingDirectory; + + public string CommandName { get; } + public string CommandArgs => string.Join(" ", _commandArgs); + + public BuiltInCommand(string commandName, IEnumerable commandArgs, Func builtInCommand) + : this(commandName, commandArgs, builtInCommand, new BuiltInCommandEnvironment()) + { + } + + internal BuiltInCommand(string commandName, IEnumerable commandArgs, Func builtInCommand, IBuiltInCommandEnvironment environment) + { + CommandName = commandName; + _commandArgs = commandArgs; + _builtInCommand = builtInCommand; + _environment = environment; + + _stdOut = new StreamForwarder(); + _stdErr = new StreamForwarder(); + } + + public CommandResult Execute() + { + TextWriter originalConsoleOut = _environment.GetConsoleOut(); + TextWriter originalConsoleError = _environment.GetConsoleError(); + string originalWorkingDirectory = _environment.GetWorkingDirectory(); + + try + { + // redirecting the standard out and error so we can forward + // the output to the caller + using (BlockingMemoryStream outStream = new BlockingMemoryStream()) + using (BlockingMemoryStream errorStream = new BlockingMemoryStream()) + { + _environment.SetConsoleOut(new StreamWriter(outStream) { AutoFlush = true }); + _environment.SetConsoleError(new StreamWriter(errorStream) { AutoFlush = true }); + + // Reset the Reporters to the new Console Out and Error. + Reporter.Reset(); + + if (!string.IsNullOrEmpty(_workingDirectory)) + { + _environment.SetWorkingDirectory(_workingDirectory); + } + + var taskOut = _stdOut.BeginRead(new StreamReader(outStream)); + var taskErr = _stdErr.BeginRead(new StreamReader(errorStream)); + + int exitCode = _builtInCommand(_commandArgs.ToArray()); + + outStream.DoneWriting(); + errorStream.DoneWriting(); + + Task.WaitAll(taskOut, taskErr); + + // fake out a ProcessStartInfo using the Muxer command name, since this is a built-in command + ProcessStartInfo startInfo = new ProcessStartInfo(new Muxer().MuxerPath, $"{CommandName} {CommandArgs}"); + return new CommandResult(startInfo, exitCode, null, null); + } + } + finally + { + _environment.SetConsoleOut(originalConsoleOut); + _environment.SetConsoleError(originalConsoleError); + _environment.SetWorkingDirectory(originalWorkingDirectory); + + Reporter.Reset(); + } + } + + public ICommand OnOutputLine(Action handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + _stdOut.ForwardTo(writeLine: handler); + + return this; + } + + public ICommand OnErrorLine(Action handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + _stdErr.ForwardTo(writeLine: handler); + + return this; + } + + public ICommand WorkingDirectory(string workingDirectory) + { + _workingDirectory = workingDirectory; + + return this; + } + + private class BuiltInCommandEnvironment : IBuiltInCommandEnvironment + { + public TextWriter GetConsoleOut() + { + return Console.Out; + } + + public void SetConsoleOut(TextWriter newOut) + { + Console.SetOut(newOut); + } + + public TextWriter GetConsoleError() + { + return Console.Error; + } + + public void SetConsoleError(TextWriter newError) + { + Console.SetError(newError); + } + + public string GetWorkingDirectory() + { + return Directory.GetCurrentDirectory(); + } + + public void SetWorkingDirectory(string path) + { + Directory.SetCurrentDirectory(path); + } + } + + public CommandResolutionStrategy ResolutionStrategy + { + get + { + throw new NotImplementedException(); + } + } + + public ICommand CaptureStdErr() + { + _stdErr.Capture(); + return this; + } + + public ICommand CaptureStdOut() + { + _stdOut.Capture(); + return this; + } + + public ICommand EnvironmentVariable(string name, string value) + { + throw new NotImplementedException(); + } + + public ICommand ForwardStdErr(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true) + { + throw new NotImplementedException(); + } + + public ICommand ForwardStdOut(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true) + { + throw new NotImplementedException(); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/Command.cs b/test/Microsoft.DotNet.Cli.Utils/Command.cs new file mode 100644 index 0000000..631c508 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Command.cs @@ -0,0 +1,287 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Microsoft.DotNet.ProjectModel; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class Command : ICommand + { + private readonly Process _process; + private StreamForwarder _stdOut; + private StreamForwarder _stdErr; + + private bool _running = false; + + private Command(CommandSpec commandSpec) + { + var psi = new ProcessStartInfo + { + FileName = commandSpec.Path, + Arguments = commandSpec.Args, + UseShellExecute = false + }; + + _process = new Process + { + StartInfo = psi + }; + + ResolutionStrategy = commandSpec.ResolutionStrategy; + } + + public static Command CreateDotNet( + string commandName, + IEnumerable args, + NuGetFramework framework = null, + string configuration = Constants.DefaultConfiguration) + { + return Create("dotnet", + new[] { commandName }.Concat(args), + framework, + configuration: configuration); + } + + /// + /// Create a command with the specified arg array. Args will be + /// escaped properly to ensure that exactly the strings in this + /// array will be present in the corresponding argument array + /// in the command's process. + /// + /// + /// + /// + /// + public static Command Create( + string commandName, + IEnumerable args, + NuGetFramework framework = null, + string configuration = Constants.DefaultConfiguration, + string outputPath = null) + { + var commandSpec = CommandResolver.TryResolveCommandSpec(commandName, + args, + framework, + configuration: configuration, + outputPath: outputPath); + + if (commandSpec == null) + { + throw new CommandUnknownException(commandName); + } + + var command = new Command(commandSpec); + + return command; + } + + public static Command Create(CommandSpec commandSpec) + { + return new Command(commandSpec); + } + + public static Command CreateForScript( + string commandName, + IEnumerable args, + Project project, + string[] inferredExtensionList) + { + var commandSpec = CommandResolver.TryResolveScriptCommandSpec(commandName, + args, + project, + inferredExtensionList); + + if (commandSpec == null) + { + throw new CommandUnknownException(commandName); + } + + var command = new Command(commandSpec); + + return command; + } + + public CommandResult Execute() + { + + Reporter.Verbose.WriteLine($"Running {_process.StartInfo.FileName} {_process.StartInfo.Arguments}"); + + ThrowIfRunning(); + _running = true; + + _process.EnableRaisingEvents = true; + +#if DEBUG + var sw = Stopwatch.StartNew(); + Reporter.Verbose.WriteLine($"> {FormatProcessInfo(_process.StartInfo)}".White()); +#endif + using (PerfTrace.Current.CaptureTiming($"{Path.GetFileNameWithoutExtension(_process.StartInfo.FileName)} {_process.StartInfo.Arguments}")) + { + _process.Start(); + + Reporter.Verbose.WriteLine($"Process ID: {_process.Id}"); + + var taskOut = _stdOut?.BeginRead(_process.StandardOutput); + var taskErr = _stdErr?.BeginRead(_process.StandardError); + _process.WaitForExit(); + + taskOut?.Wait(); + taskErr?.Wait(); + } + + var exitCode = _process.ExitCode; + +#if DEBUG + var message = $"< {FormatProcessInfo(_process.StartInfo)} exited with {exitCode} in {sw.ElapsedMilliseconds} ms."; + if (exitCode == 0) + { + Reporter.Verbose.WriteLine(message.Green()); + } + else + { + Reporter.Verbose.WriteLine(message.Red().Bold()); + } +#endif + + return new CommandResult( + this._process.StartInfo, + exitCode, + _stdOut?.CapturedOutput, + _stdErr?.CapturedOutput); + } + + public ICommand WorkingDirectory(string projectDirectory) + { + _process.StartInfo.WorkingDirectory = projectDirectory; + return this; + } + + public ICommand EnvironmentVariable(string name, string value) + { +#if NET451 + _process.StartInfo.EnvironmentVariables[name] = value; +#else + _process.StartInfo.Environment[name] = value; +#endif + return this; + } + + public ICommand CaptureStdOut() + { + ThrowIfRunning(); + EnsureStdOut(); + _stdOut.Capture(); + return this; + } + + public ICommand CaptureStdErr() + { + ThrowIfRunning(); + EnsureStdErr(); + _stdErr.Capture(); + return this; + } + + public ICommand ForwardStdOut(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true) + { + ThrowIfRunning(); + if (!onlyIfVerbose || CommandContext.IsVerbose()) + { + EnsureStdOut(); + + if (to == null) + { + _stdOut.ForwardTo(writeLine: Reporter.Output.WriteLine); + EnvironmentVariable(CommandContext.Variables.AnsiPassThru, ansiPassThrough.ToString()); + } + else + { + _stdOut.ForwardTo(writeLine: to.WriteLine); + } + } + return this; + } + + public ICommand ForwardStdErr(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true) + { + ThrowIfRunning(); + if (!onlyIfVerbose || CommandContext.IsVerbose()) + { + EnsureStdErr(); + + if (to == null) + { + _stdErr.ForwardTo(writeLine: Reporter.Error.WriteLine); + EnvironmentVariable(CommandContext.Variables.AnsiPassThru, ansiPassThrough.ToString()); + } + else + { + _stdErr.ForwardTo(writeLine: to.WriteLine); + } + } + return this; + } + + public ICommand OnOutputLine(Action handler) + { + ThrowIfRunning(); + EnsureStdOut(); + + _stdOut.ForwardTo(writeLine: handler); + return this; + } + + public ICommand OnErrorLine(Action handler) + { + ThrowIfRunning(); + EnsureStdErr(); + + _stdErr.ForwardTo(writeLine: handler); + return this; + } + + public CommandResolutionStrategy ResolutionStrategy { get; } + + public string CommandName => _process.StartInfo.FileName; + + public string CommandArgs => _process.StartInfo.Arguments; + + private string FormatProcessInfo(ProcessStartInfo info) + { + if (string.IsNullOrWhiteSpace(info.Arguments)) + { + return info.FileName; + } + + return info.FileName + " " + info.Arguments; + } + + private void EnsureStdOut() + { + _stdOut = _stdOut ?? new StreamForwarder(); + _process.StartInfo.RedirectStandardOutput = true; + } + + private void EnsureStdErr() + { + _stdErr = _stdErr ?? new StreamForwarder(); + _process.StartInfo.RedirectStandardError = true; + } + + private void ThrowIfRunning([CallerMemberName] string memberName = null) + { + if (_running) + { + throw new InvalidOperationException($"Unable to invoke {memberName} after the command has been run"); + } + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandContext.cs b/test/Microsoft.DotNet.Cli.Utils/CommandContext.cs new file mode 100644 index 0000000..8209dd2 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandContext.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class CommandContext + { + public static class Variables + { + private static readonly string Prefix = "DOTNET_CLI_CONTEXT_"; + public static readonly string Verbose = Prefix + "VERBOSE"; + public static readonly string AnsiPassThru = Prefix + "ANSI_PASS_THRU"; + } + + private static Lazy _verbose = new Lazy(() => Env.GetEnvironmentVariableAsBool(Variables.Verbose)); + private static Lazy _ansiPassThru = new Lazy(() => Env.GetEnvironmentVariableAsBool(Variables.AnsiPassThru)); + + public static bool IsVerbose() + { + return _verbose.Value; + } + + public static bool ShouldPassAnsiCodesThrough() + { + return _ansiPassThru.Value; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandFactory.cs new file mode 100644 index 0000000..f480211 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandFactory.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class CommandFactory : ICommandFactory + { + public ICommand Create( + string commandName, + IEnumerable args, + NuGetFramework framework = null, + string configuration = Constants.DefaultConfiguration) + { + return Command.Create(commandName, args, framework, configuration); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Chain.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Chain.cs new file mode 100644 index 0000000..263f65b --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Chain.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.Cli.Utils.CommandParsing +{ + public struct Chain + { + public Chain(TLeft left, TDown down) + : this() + { + Left = left; + Down = down; + } + + public readonly TLeft Left; + public readonly TDown Down; + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/CommandGrammar.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/CommandGrammar.cs new file mode 100644 index 0000000..dfc0d86 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/CommandGrammar.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.DotNet.Cli.Utils.CommandParsing +{ + public class CommandGrammar : Grammar + { + private CommandGrammar(Func variable, bool preserveSurroundingQuotes) + { + var environmentVariablePiece = Ch('%').And(Rep(Ch().Not(Ch('%')))).And(Ch('%')).Left().Down().Str() + .Build(key => variable(key) ?? "%" + key + "%"); + + var escapeSequencePiece = + Ch('%').And(Ch('%')).Build(_=>"%") + .Or(Ch('^').And(Ch('^')).Build(_ => "^")) + .Or(Ch('\\').And(Ch('\\')).Build(_ => "\\")) + .Or(Ch('\\').And(Ch('\"')).Build(_ => "\"")) + ; + + var specialPiece = environmentVariablePiece.Or(escapeSequencePiece); + + var unquotedPiece = Rep1(Ch().Not(specialPiece).Not(Ch(' '))).Str(); + + var quotedPiece = Rep1(Ch().Not(specialPiece).Not(Ch('\"'))).Str(); + + var unquotedTerm = Rep1(unquotedPiece.Or(specialPiece)).Str(); + + var quotedTerm = Ch('\"').And(Rep(quotedPiece.Or(specialPiece)).Str()).And(Ch('\"')).Left().Down(); + if (preserveSurroundingQuotes) + { + // Str() value assigned to quotedTerm does not include quotation marks surrounding the quoted or + // special piece. Add those quotes back if requested. + quotedTerm = quotedTerm.Build(str => "\"" + str + "\""); + } + + var whitespace = Rep(Ch(' ')); + + var term = whitespace.And(quotedTerm.Or(unquotedTerm)).And(whitespace).Left().Down(); + + Parse = Rep(term); + } + + public readonly Parser> Parse; + + public static string[] Process(string text, Func variables, bool preserveSurroundingQuotes) + { + var grammar = new CommandGrammar(variables, preserveSurroundingQuotes); + var cursor = new Cursor(text, 0, text.Length); + + var result = grammar.Parse(cursor); + if (!result.Remainder.IsEnd) + { + throw new ArgumentException($"Malformed command text '{text}'", nameof(text)); + } + return result.Value.ToArray(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Cursor.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Cursor.cs new file mode 100644 index 0000000..d8b639a --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Cursor.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.Cli.Utils.CommandParsing +{ + public struct Cursor + { + private readonly string _text; + private readonly int _start; + private readonly int _end; + + public Cursor(string text, int start, int end) + { + _text = text; + _start = start; + _end = end; + } + + public bool IsEnd + { + get { return _start == _end; } + } + + public char Peek(int index) + { + return (index + _start) >= _end ? (char)0 : _text[index + _start]; + } + + public Result Advance(TValue result, int length) + { + return new Result(result, new Cursor(_text, _start + length, _end)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Grammar.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Grammar.cs new file mode 100644 index 0000000..fe32f67 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Grammar.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.DotNet.Cli.Utils.CommandParsing +{ + public class Grammar + { + protected static Parser> Rep1(Parser parser) + { + Parser> rep = Rep(parser); + return pos => + { + var result = rep(pos); + return result.IsEmpty || !result.Value.Any() ? Result>.Empty : result; + }; + } + + protected static Parser> Rep(Parser parser) + { + return pos => + { + var data = new List(); + for (; ; ) + { + var result = parser(pos); + if (result.IsEmpty) break; + data.Add(result.Value); + pos = result.Remainder; + } + return new Result>(data, pos); + }; + } + + protected static Parser Ch() + { + return pos => pos.IsEnd ? Result.Empty : pos.Advance(pos.Peek(0), 1); + } + + private static Parser IsEnd() + { + return pos => pos.IsEnd ? pos.Advance(true, 0) : Result.Empty; + } + + protected static Parser Ch(char ch) + { + return pos => pos.Peek(0) != ch ? Result.Empty : pos.Advance(ch, 1); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Parser.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Parser.cs new file mode 100644 index 0000000..997a317 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Parser.cs @@ -0,0 +1,7 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.Cli.Utils.CommandParsing +{ + public delegate Result Parser(Cursor cursor); +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/ParserExtensions.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/ParserExtensions.cs new file mode 100644 index 0000000..04b16f3 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/ParserExtensions.cs @@ -0,0 +1,85 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.DotNet.Cli.Utils.CommandParsing +{ + public static class ParserExtensions + { + public static Parser> And(this Parser parser1, + Parser parser2) + { + return pos => + { + var result1 = parser1(pos); + if (result1.IsEmpty) return Result>.Empty; + var result2 = parser2(result1.Remainder); + if (result2.IsEmpty) return Result>.Empty; + return result2.AsValue(new Chain(result1.Value, result2.Value)); + }; + } + + public static Parser Or(this Parser parser1, Parser parser2) + { + return pos => + { + var result1 = parser1(pos); + if (!result1.IsEmpty) return result1; + var result2 = parser2(pos); + if (!result2.IsEmpty) return result2; + return Result.Empty; + }; + } + + public static Parser Not(this Parser parser1, Parser parser2) + { + return pos => + { + var result2 = parser2(pos); + if (!result2.IsEmpty) return Result.Empty; + return parser1(pos); + }; + } + + public static Parser Left(this Parser> parser) + { + return pos => + { + var result = parser(pos); + return result.IsEmpty ? Result.Empty : result.AsValue(result.Value.Left); + }; + } + + public static Parser Down(this Parser> parser) + { + return pos => + { + var result = parser(pos); + return result.IsEmpty ? Result.Empty : result.AsValue(result.Value.Down); + }; + } + + public static Parser Build(this Parser parser, Func builder) + { + return pos => + { + var result = parser(pos); + if (result.IsEmpty) return Result.Empty; + return result.AsValue(builder(result.Value)); + }; + } + + public static Parser Str(this Parser> parser) + { + return parser.Build(x => new string(x.ToArray())); + } + + public static Parser Str(this Parser> parser) + { + return parser.Build(x => String.Concat(x.ToArray())); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Result.cs b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Result.cs new file mode 100644 index 0000000..9b01596 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandParsing/Result.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.Cli.Utils.CommandParsing +{ + public struct Result + { + public Result(TValue value, Cursor remainder) + : this() + { + Value = value; + Remainder = remainder; + } + + public readonly TValue Value; + public readonly Cursor Remainder; + + public bool IsEmpty + { + get { return Equals(this, default(Result)); } + } + + public static Result Empty + { + get { return default(Result); } + } + + public Result AsValue(TValue2 value2) + { + return new Result(value2, Remainder); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AbstractPathBasedCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AbstractPathBasedCommandResolver.cs new file mode 100644 index 0000000..a9949d2 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AbstractPathBasedCommandResolver.cs @@ -0,0 +1,51 @@ +using System; +namespace Microsoft.DotNet.Cli.Utils +{ + public abstract class AbstractPathBasedCommandResolver : ICommandResolver + { + protected IEnvironmentProvider _environment; + protected IPlatformCommandSpecFactory _commandSpecFactory; + + public AbstractPathBasedCommandResolver(IEnvironmentProvider environment, + IPlatformCommandSpecFactory commandSpecFactory) + { + if (environment == null) + { + throw new ArgumentNullException(nameof(environment)); + } + + if (commandSpecFactory == null) + { + throw new ArgumentNullException(nameof(commandSpecFactory)); + } + + _environment = environment; + _commandSpecFactory = commandSpecFactory; + } + + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.CommandName == null) + { + return null; + } + + var commandPath = ResolveCommandPath(commandResolverArguments); + + if (commandPath == null) + { + return null; + } + + return _commandSpecFactory.CreateCommandSpec( + commandResolverArguments.CommandName, + commandResolverArguments.CommandArguments.OrEmptyIfNull(), + commandPath, + GetCommandResolutionStrategy(), + _environment); + } + + internal abstract string ResolveCommandPath(CommandResolverArguments commandResolverArguments); + internal abstract CommandResolutionStrategy GetCommandResolutionStrategy(); + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseCommandResolver.cs new file mode 100644 index 0000000..cdf6104 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseCommandResolver.cs @@ -0,0 +1,22 @@ +using Microsoft.DotNet.InternalAbstractions; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class AppBaseCommandResolver : AbstractPathBasedCommandResolver + { + public AppBaseCommandResolver(IEnvironmentProvider environment, + IPlatformCommandSpecFactory commandSpecFactory) : base(environment, commandSpecFactory) { } + + internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments) + { + return _environment.GetCommandPathFromRootPath( + ApplicationEnvironment.ApplicationBasePath, + commandResolverArguments.CommandName); + } + + internal override CommandResolutionStrategy GetCommandResolutionStrategy() + { + return CommandResolutionStrategy.BaseDirectory; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseDllCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseDllCommandResolver.cs new file mode 100644 index 0000000..33604c4 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/AppBaseDllCommandResolver.cs @@ -0,0 +1,34 @@ +using System.IO; +using System.Linq; +using Microsoft.DotNet.InternalAbstractions; +using Microsoft.DotNet.ProjectModel; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class AppBaseDllCommandResolver : ICommandResolver + { + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.CommandName == null) + { + return null; + } + if (commandResolverArguments.CommandName.EndsWith(FileNameSuffixes.DotNet.DynamicLib)) + { + var localPath = Path.Combine(ApplicationEnvironment.ApplicationBasePath, + commandResolverArguments.CommandName); + if (File.Exists(localPath)) + { + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart( + new[] { localPath } + .Concat(commandResolverArguments.CommandArguments.OrEmptyIfNull())); + return new CommandSpec( + new Muxer().MuxerPath, + escapedArgs, + CommandResolutionStrategy.RootedPath); + } + } + return null; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs new file mode 100644 index 0000000..e239fcc --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolutionStrategy.cs @@ -0,0 +1,32 @@ +namespace Microsoft.DotNet.Cli.Utils +{ + public enum CommandResolutionStrategy + { + // command loaded from a deps file + DepsFile, + + // command loaded from project dependencies nuget package + ProjectDependenciesPackage, + + // command loaded from project tools nuget package + ProjectToolsPackage, + + // command loaded from the same directory as the executing assembly + BaseDirectory, + + // command loaded from the same directory as a project.json file + ProjectLocal, + + // command loaded from PATH environment variable + Path, + + // command loaded from rooted path + RootedPath, + + // command loaded from project build output path + OutputPath, + + // command not found + None + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolverArguments.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolverArguments.cs new file mode 100644 index 0000000..e43b377 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CommandResolverArguments.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class CommandResolverArguments + { + public string CommandName { get; set; } + + public IEnumerable CommandArguments { get; set; } + + public NuGetFramework Framework { get; set; } + + public string OutputPath { get; set; } + + public string ProjectDirectory { get; set; } + + public string Configuration { get; set; } + + public IEnumerable InferredExtensions { get; set; } + + public string BuildBasePath { get; set; } + + public string DepsJsonFile { get; set; } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CompositeCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CompositeCommandResolver.cs new file mode 100644 index 0000000..bce8811 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/CompositeCommandResolver.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class CompositeCommandResolver : ICommandResolver + { + private IList _orderedCommandResolvers; + + public IEnumerable OrderedCommandResolvers + { + get + { + return _orderedCommandResolvers; + } + } + + public CompositeCommandResolver() + { + _orderedCommandResolvers = new List(); + } + + public void AddCommandResolver(ICommandResolver commandResolver) + { + _orderedCommandResolvers.Add(commandResolver); + } + + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + foreach (var commandResolver in _orderedCommandResolvers) + { + var commandSpec = commandResolver.Resolve(commandResolverArguments); + + if (commandSpec != null) + { + return commandSpec; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs new file mode 100644 index 0000000..b4b21fa --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DefaultCommandResolverPolicy.cs @@ -0,0 +1,42 @@ +using Microsoft.DotNet.InternalAbstractions; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class DefaultCommandResolverPolicy + { + public static CompositeCommandResolver Create() + { + var environment = new EnvironmentProvider(); + var packagedCommandSpecFactory = new PackagedCommandSpecFactory(); + + var platformCommandSpecFactory = default(IPlatformCommandSpecFactory); + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + } + else + { + platformCommandSpecFactory = new GenericPlatformCommandSpecFactory(); + } + + return CreateDefaultCommandResolver(environment, packagedCommandSpecFactory, platformCommandSpecFactory); + } + + public static CompositeCommandResolver CreateDefaultCommandResolver( + IEnvironmentProvider environment, + IPackagedCommandSpecFactory packagedCommandSpecFactory, + IPlatformCommandSpecFactory platformCommandSpecFactory) + { + var compositeCommandResolver = new CompositeCommandResolver(); + + compositeCommandResolver.AddCommandResolver(new MuxerCommandResolver()); + compositeCommandResolver.AddCommandResolver(new RootedCommandResolver()); + compositeCommandResolver.AddCommandResolver(new ProjectToolsCommandResolver(packagedCommandSpecFactory)); + compositeCommandResolver.AddCommandResolver(new AppBaseDllCommandResolver()); + compositeCommandResolver.AddCommandResolver(new AppBaseCommandResolver(environment, platformCommandSpecFactory)); + compositeCommandResolver.AddCommandResolver(new PathCommandResolver(environment, platformCommandSpecFactory)); + + return compositeCommandResolver; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DepsJsonCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DepsJsonCommandResolver.cs new file mode 100644 index 0000000..6c67bd2 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/DepsJsonCommandResolver.cs @@ -0,0 +1,253 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.ProjectModel; +using Microsoft.Extensions.DependencyModel; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class DepsJsonCommandResolver : ICommandResolver + { + private static readonly string[] s_extensionPreferenceOrder = new[] + { + "", + ".exe", + ".dll" + }; + + private string _nugetPackageRoot; + private Muxer _muxer; + + public DepsJsonCommandResolver(string nugetPackageRoot) + : this(new Muxer(), nugetPackageRoot) { } + + public DepsJsonCommandResolver(Muxer muxer, string nugetPackageRoot) + { + _muxer = muxer; + _nugetPackageRoot = nugetPackageRoot; + } + + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.CommandName == null + || commandResolverArguments.DepsJsonFile == null) + { + return null; + } + + return ResolveFromDepsJsonFile( + commandResolverArguments.CommandName, + commandResolverArguments.CommandArguments.OrEmptyIfNull(), + commandResolverArguments.DepsJsonFile); + } + + private CommandSpec ResolveFromDepsJsonFile( + string commandName, + IEnumerable commandArgs, + string depsJsonFile) + { + var dependencyContext = LoadDependencyContextFromFile(depsJsonFile); + + var commandPath = GetCommandPathFromDependencyContext(commandName, dependencyContext); + if (commandPath == null) + { + return null; + } + + return CreateCommandSpecUsingMuxerIfPortable( + commandPath, + commandArgs, + depsJsonFile, + CommandResolutionStrategy.DepsFile, + _nugetPackageRoot, + IsPortableApp(commandPath)); + } + + public DependencyContext LoadDependencyContextFromFile(string depsJsonFile) + { + DependencyContext dependencyContext = null; + DependencyContextJsonReader contextReader = new DependencyContextJsonReader(); + + using (var contextStream = File.OpenRead(depsJsonFile)) + { + dependencyContext = contextReader.Read(contextStream); + } + + return dependencyContext; + } + + public string GetCommandPathFromDependencyContext(string commandName, DependencyContext dependencyContext) + { + var commandCandidates = new List(); + + var assemblyCommandCandidates = GetCommandCandidates( + commandName, + dependencyContext, + CommandCandidateType.RuntimeCommandCandidate); + var nativeCommandCandidates = GetCommandCandidates( + commandName, + dependencyContext, + CommandCandidateType.NativeCommandCandidate); + + commandCandidates.AddRange(assemblyCommandCandidates); + commandCandidates.AddRange(nativeCommandCandidates); + + var command = ChooseCommandCandidate(commandCandidates); + + return command?.GetAbsoluteCommandPath(_nugetPackageRoot); + } + + private IEnumerable GetCommandCandidates( + string commandName, + DependencyContext dependencyContext, + CommandCandidateType commandCandidateType) + { + var commandCandidates = new List(); + + foreach (var runtimeLibrary in dependencyContext.RuntimeLibraries) + { + IEnumerable runtimeAssetGroups = null; + + if (commandCandidateType == CommandCandidateType.NativeCommandCandidate) + { + runtimeAssetGroups = runtimeLibrary.NativeLibraryGroups; + } + else if (commandCandidateType == CommandCandidateType.RuntimeCommandCandidate) + { + runtimeAssetGroups = runtimeLibrary.RuntimeAssemblyGroups; + } + + commandCandidates.AddRange(GetCommandCandidatesFromRuntimeAssetGroups( + commandName, + runtimeAssetGroups, + runtimeLibrary.Name, + runtimeLibrary.Version)); + } + + return commandCandidates; + } + + private IEnumerable GetCommandCandidatesFromRuntimeAssetGroups( + string commandName, + IEnumerable runtimeAssetGroups, + string PackageName, + string PackageVersion) + { + var candidateAssetGroups = runtimeAssetGroups + .Where(r => r.Runtime == string.Empty) + .Where(a => + a.AssetPaths.Any(p => + Path.GetFileNameWithoutExtension(p).Equals(commandName, StringComparison.OrdinalIgnoreCase))); + + var commandCandidates = new List(); + foreach (var candidateAssetGroup in candidateAssetGroups) + { + var candidateAssetPaths = candidateAssetGroup.AssetPaths.Where( + p => Path.GetFileNameWithoutExtension(p) + .Equals(commandName, StringComparison.OrdinalIgnoreCase)); + + foreach (var candidateAssetPath in candidateAssetPaths) + { + commandCandidates.Add(new CommandCandidate + { + PackageName = PackageName, + PackageVersion = PackageVersion, + RelativeCommandPath = candidateAssetPath + }); + } + } + + return commandCandidates; + } + + private CommandCandidate ChooseCommandCandidate(IEnumerable commandCandidates) + { + foreach (var extension in s_extensionPreferenceOrder) + { + var candidate = commandCandidates + .FirstOrDefault(p => Path.GetExtension(p.RelativeCommandPath) + .Equals(extension, StringComparison.OrdinalIgnoreCase)); + + if (candidate != null) + { + return candidate; + } + } + + return null; + } + + private CommandSpec CreateCommandSpecUsingMuxerIfPortable( + string commandPath, + IEnumerable commandArgs, + string depsJsonFile, + CommandResolutionStrategy commandResolutionStrategy, + string nugetPackagesRoot, + bool isPortable) + { + var depsFileArguments = GetDepsFileArguments(depsJsonFile); + var additionalProbingPathArguments = GetAdditionalProbingPathArguments(); + + var muxerArgs = new List(); + muxerArgs.Add("exec"); + muxerArgs.AddRange(depsFileArguments); + muxerArgs.AddRange(additionalProbingPathArguments); + muxerArgs.Add(commandPath); + muxerArgs.AddRange(commandArgs); + + var escapedArgString = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(muxerArgs); + + return new CommandSpec(_muxer.MuxerPath, escapedArgString, commandResolutionStrategy); + } + + private bool IsPortableApp(string commandPath) + { + var commandDir = Path.GetDirectoryName(commandPath); + + var runtimeConfigPath = Directory.EnumerateFiles(commandDir) + .FirstOrDefault(x => x.EndsWith("runtimeconfig.json")); + + if (runtimeConfigPath == null) + { + return false; + } + + var runtimeConfig = new RuntimeConfig(runtimeConfigPath); + + return runtimeConfig.IsPortable; + } + + private IEnumerable GetDepsFileArguments(string depsJsonFile) + { + return new[] { "--depsfile", depsJsonFile }; + } + + private IEnumerable GetAdditionalProbingPathArguments() + { + return new[] { "--additionalProbingPath", _nugetPackageRoot }; + } + + private class CommandCandidate + { + public string PackageName { get; set; } + public string PackageVersion { get; set; } + public string RelativeCommandPath { get; set; } + + public string GetAbsoluteCommandPath(string nugetPackageRoot) + { + return Path.Combine( + nugetPackageRoot.Replace('/', Path.DirectorySeparatorChar), + PackageName.Replace('/', Path.DirectorySeparatorChar), + PackageVersion.Replace('/', Path.DirectorySeparatorChar), + RelativeCommandPath.Replace('/', Path.DirectorySeparatorChar)); + } + } + + private enum CommandCandidateType + { + NativeCommandCandidate, + RuntimeCommandCandidate + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/GenericPlatformCommandSpecFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/GenericPlatformCommandSpecFactory.cs new file mode 100644 index 0000000..9472eb6 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/GenericPlatformCommandSpecFactory.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class GenericPlatformCommandSpecFactory : IPlatformCommandSpecFactory + { + public CommandSpec CreateCommandSpec( + string commandName, + IEnumerable args, + string commandPath, + CommandResolutionStrategy resolutionStrategy, + IEnvironmentProvider environment) + { + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args); + return new CommandSpec(commandPath, escapedArgs, resolutionStrategy); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ICommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ICommandResolver.cs new file mode 100644 index 0000000..25b9835 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ICommandResolver.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public interface ICommandResolver + { + CommandSpec Resolve(CommandResolverArguments arguments); + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs new file mode 100644 index 0000000..3c53164 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPackagedCommandSpecFactory.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.DotNet.ProjectModel.Compilation; + +namespace Microsoft.DotNet.Cli.Utils +{ + public interface IPackagedCommandSpecFactory + { + CommandSpec CreateCommandSpecFromLibrary( + LockFileTargetLibrary toolLibrary, + string commandName, + IEnumerable commandArguments, + IEnumerable allowedExtensions, + string nugetPackagesRoot, + CommandResolutionStrategy commandResolutionStrategy, + string depsFilePath, + string runtimeConfigPath); + + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPlatformCommandSpecFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPlatformCommandSpecFactory.cs new file mode 100644 index 0000000..3748a91 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/IPlatformCommandSpecFactory.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Microsoft.DotNet.Cli.Utils +{ + public interface IPlatformCommandSpecFactory + { + CommandSpec CreateCommandSpec( + string commandName, + IEnumerable args, + string commandPath, + CommandResolutionStrategy resolutionStrategy, + IEnvironmentProvider environment); + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/MuxerCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/MuxerCommandResolver.cs new file mode 100644 index 0000000..132efca --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/MuxerCommandResolver.cs @@ -0,0 +1,17 @@ +namespace Microsoft.DotNet.Cli.Utils +{ + public class MuxerCommandResolver : ICommandResolver + { + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.CommandName == Muxer.MuxerName) + { + var muxer = new Muxer(); + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart( + commandResolverArguments.CommandArguments.OrEmptyIfNull()); + return new CommandSpec(muxer.MuxerPath, escapedArgs, CommandResolutionStrategy.RootedPath); + } + return null; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/OutputPathCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/OutputPathCommandResolver.cs new file mode 100644 index 0000000..37996e7 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/OutputPathCommandResolver.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using System.IO; +using Microsoft.DotNet.InternalAbstractions; +using Microsoft.DotNet.ProjectModel; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class OutputPathCommandResolver : AbstractPathBasedCommandResolver + { + public OutputPathCommandResolver(IEnvironmentProvider environment, + IPlatformCommandSpecFactory commandSpecFactory) : base(environment, commandSpecFactory) + { } + + + internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.Framework == null + || commandResolverArguments.ProjectDirectory == null + || commandResolverArguments.Configuration == null + || commandResolverArguments.CommandName == null) + { + return null; + } + + return ResolveFromProjectOutput( + commandResolverArguments.ProjectDirectory, + commandResolverArguments.Framework, + commandResolverArguments.Configuration, + commandResolverArguments.CommandName, + commandResolverArguments.CommandArguments.OrEmptyIfNull(), + commandResolverArguments.OutputPath, + commandResolverArguments.BuildBasePath); + } + + private string ResolveFromProjectOutput( + string projectDirectory, + NuGetFramework framework, + string configuration, + string commandName, + IEnumerable commandArguments, + string outputPath, + string buildBasePath) + { + var projectContext = GetProjectContextFromDirectory( + projectDirectory, + framework); + + if (projectContext == null) + { + return null; + } + + var buildOutputPath = projectContext.GetOutputPaths(configuration, buildBasePath, outputPath).RuntimeFiles.BasePath; + + if (! Directory.Exists(buildOutputPath)) + { + Reporter.Verbose.WriteLine($"outputpathresolver: {buildOutputPath} does not exist"); + return null; + } + + return _environment.GetCommandPathFromRootPath(buildOutputPath, commandName); + } + + private ProjectContext GetProjectContextFromDirectory(string directory, NuGetFramework framework) + { + if (directory == null || framework == null) + { + return null; + } + + var projectRootPath = directory; + + if (!File.Exists(Path.Combine(projectRootPath, Project.FileName))) + { + return null; + } + + var projectContext = ProjectContext.Create( + projectRootPath, + framework, + DotnetRuntimeIdentifiers.InferCurrentRuntimeIdentifiers()); + + if (projectContext.RuntimeIdentifier == null) + { + return null; + } + + return projectContext; + } + + internal override CommandResolutionStrategy GetCommandResolutionStrategy() + { + return CommandResolutionStrategy.OutputPath; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs new file mode 100644 index 0000000..6842d3e --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PackagedCommandSpecFactory.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using NuGet.Packaging; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class PackagedCommandSpecFactory : IPackagedCommandSpecFactory + { + public CommandSpec CreateCommandSpecFromLibrary( + LockFileTargetLibrary toolLibrary, + string commandName, + IEnumerable commandArguments, + IEnumerable allowedExtensions, + string nugetPackagesRoot, + CommandResolutionStrategy commandResolutionStrategy, + string depsFilePath, + string runtimeConfigPath) + { + + var toolAssembly = toolLibrary?.RuntimeAssemblies + .FirstOrDefault(r => Path.GetFileNameWithoutExtension(r.Path) == commandName); + + if (toolAssembly == null) + { + return null; + } + + var commandPath = GetCommandFilePath(nugetPackagesRoot, toolLibrary, toolAssembly); + + if (!File.Exists(commandPath)) + { + return null; + } + + return CreateCommandSpecWrappingWithMuxerIfDll( + commandPath, + commandArguments, + depsFilePath, + commandResolutionStrategy, + nugetPackagesRoot, + runtimeConfigPath); + } + + private string GetCommandFilePath(string nugetPackagesRoot, LockFileTargetLibrary toolLibrary, LockFileItem runtimeAssembly) + { + var packageDirectory = new VersionFolderPathResolver(nugetPackagesRoot) + .GetInstallPath(toolLibrary.Name, toolLibrary.Version); + + var filePath = Path.Combine(packageDirectory, runtimeAssembly.Path); + + return filePath; + } + + private CommandSpec CreateCommandSpecWrappingWithMuxerIfDll( + string commandPath, + IEnumerable commandArguments, + string depsFilePath, + CommandResolutionStrategy commandResolutionStrategy, + string nugetPackagesRoot, + string runtimeConfigPath) + { + var commandExtension = Path.GetExtension(commandPath); + + if (commandExtension == FileNameSuffixes.DotNet.DynamicLib) + { + return CreatePackageCommandSpecUsingMuxer( + commandPath, + commandArguments, + depsFilePath, + commandResolutionStrategy, + nugetPackagesRoot, + runtimeConfigPath); + } + + return CreateCommandSpec(commandPath, commandArguments, commandResolutionStrategy); + } + + private CommandSpec CreatePackageCommandSpecUsingMuxer( + string commandPath, + IEnumerable commandArguments, + string depsFilePath, + CommandResolutionStrategy commandResolutionStrategy, + string nugetPackagesRoot, + string runtimeConfigPath) + { + var host = string.Empty; + var arguments = new List(); + + var muxer = new Muxer(); + + host = muxer.MuxerPath; + if (host == null) + { + throw new Exception("Unable to locate dotnet multiplexer"); + } + + arguments.Add("exec"); + + if (runtimeConfigPath != null) + { + arguments.Add("--runtimeconfig"); + arguments.Add(runtimeConfigPath); + } + + if (depsFilePath != null) + { + arguments.Add("--depsfile"); + arguments.Add(depsFilePath); + } + + arguments.Add("--additionalprobingpath"); + arguments.Add(nugetPackagesRoot); + + arguments.Add(commandPath); + arguments.AddRange(commandArguments); + + return CreateCommandSpec(host, arguments, commandResolutionStrategy); + } + + private CommandSpec CreateCommandSpec( + string commandPath, + IEnumerable commandArguments, + CommandResolutionStrategy commandResolutionStrategy) + { + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(commandArguments); + + return new CommandSpec(commandPath, escapedArgs, commandResolutionStrategy); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PathCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PathCommandResolver.cs new file mode 100644 index 0000000..1a263d8 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/PathCommandResolver.cs @@ -0,0 +1,18 @@ +namespace Microsoft.DotNet.Cli.Utils +{ + public class PathCommandResolver : AbstractPathBasedCommandResolver + { + public PathCommandResolver(IEnvironmentProvider environment, + IPlatformCommandSpecFactory commandSpecFactory) : base(environment, commandSpecFactory) { } + + internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments) + { + return _environment.GetCommandPath(commandResolverArguments.CommandName); + } + + internal override CommandResolutionStrategy GetCommandResolutionStrategy() + { + return CommandResolutionStrategy.Path; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectDependenciesCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectDependenciesCommandResolver.cs new file mode 100644 index 0000000..08ec98d --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectDependenciesCommandResolver.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.InternalAbstractions; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class ProjectDependenciesCommandResolver : ICommandResolver + { + private static readonly CommandResolutionStrategy s_commandResolutionStrategy = + CommandResolutionStrategy.ProjectDependenciesPackage; + + private readonly IEnvironmentProvider _environment; + private readonly IPackagedCommandSpecFactory _packagedCommandSpecFactory; + + public ProjectDependenciesCommandResolver( + IEnvironmentProvider environment, + IPackagedCommandSpecFactory packagedCommandSpecFactory) + { + if (environment == null) + { + throw new ArgumentNullException(nameof(environment)); + } + + if (packagedCommandSpecFactory == null) + { + throw new ArgumentNullException(nameof(packagedCommandSpecFactory)); + } + + _environment = environment; + _packagedCommandSpecFactory = packagedCommandSpecFactory; + } + + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.Framework == null + || commandResolverArguments.ProjectDirectory == null + || commandResolverArguments.Configuration == null + || commandResolverArguments.CommandName == null) + { + return null; + } + + return ResolveFromProjectDependencies( + commandResolverArguments.ProjectDirectory, + commandResolverArguments.Framework, + commandResolverArguments.Configuration, + commandResolverArguments.CommandName, + commandResolverArguments.CommandArguments.OrEmptyIfNull(), + commandResolverArguments.OutputPath, + commandResolverArguments.BuildBasePath); + } + + private CommandSpec ResolveFromProjectDependencies( + string projectDirectory, + NuGetFramework framework, + string configuration, + string commandName, + IEnumerable commandArguments, + string outputPath, + string buildBasePath) + { + var allowedExtensions = GetAllowedCommandExtensionsFromEnvironment(_environment); + + var projectContext = GetProjectContextFromDirectory( + projectDirectory, + framework); + + if (projectContext == null) + { + return null; + } + + var depsFilePath = + projectContext.GetOutputPaths(configuration, buildBasePath, outputPath).RuntimeFiles.DepsJson; + + if (! File.Exists(depsFilePath)) + { + Reporter.Verbose.WriteLine($"projectdependenciescommandresolver: {depsFilePath} does not exist"); + return null; + } + + var runtimeConfigPath = + projectContext.GetOutputPaths(configuration, buildBasePath, outputPath).RuntimeFiles.RuntimeConfigJson; + + if (! File.Exists(runtimeConfigPath)) + { + Reporter.Verbose.WriteLine($"projectdependenciescommandresolver: {runtimeConfigPath} does not exist"); + return null; + } + + var toolLibrary = GetToolLibraryForContext(projectContext, commandName); + + return _packagedCommandSpecFactory.CreateCommandSpecFromLibrary( + toolLibrary, + commandName, + commandArguments, + allowedExtensions, + projectContext.PackagesDirectory, + s_commandResolutionStrategy, + depsFilePath, + runtimeConfigPath); + } + + private LockFileTargetLibrary GetToolLibraryForContext( + ProjectContext projectContext, string commandName) + { + var toolLibraries = projectContext.LockFile.Targets + .FirstOrDefault(t => t.TargetFramework.GetShortFolderName() + .Equals(projectContext.TargetFramework.GetShortFolderName())) + ?.Libraries.Where(l => l.Name == commandName || + l.RuntimeAssemblies.Any(r => Path.GetFileNameWithoutExtension(r.Path) == commandName)).ToList(); + + if (toolLibraries?.Count() > 1) + { + throw new InvalidOperationException($"Ambiguous command name: {commandName}"); + } + + return toolLibraries?.FirstOrDefault(); + } + + private ProjectContext GetProjectContextFromDirectory(string directory, NuGetFramework framework) + { + if (directory == null || framework == null) + { + return null; + } + + var projectRootPath = directory; + + if (!File.Exists(Path.Combine(projectRootPath, Project.FileName))) + { + return null; + } + + return ProjectContext.Create( + projectRootPath, + framework, + DotnetRuntimeIdentifiers.InferCurrentRuntimeIdentifiers()); + + } + + private IEnumerable GetAllowedCommandExtensionsFromEnvironment(IEnvironmentProvider environment) + { + var allowedCommandExtensions = new List(); + allowedCommandExtensions.AddRange(environment.ExecutableExtensions); + allowedCommandExtensions.Add(FileNameSuffixes.DotNet.DynamicLib); + + return allowedCommandExtensions; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectPathCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectPathCommandResolver.cs new file mode 100644 index 0000000..720e406 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectPathCommandResolver.cs @@ -0,0 +1,26 @@ +namespace Microsoft.DotNet.Cli.Utils +{ + public class ProjectPathCommandResolver : AbstractPathBasedCommandResolver + { + public ProjectPathCommandResolver(IEnvironmentProvider environment, + IPlatformCommandSpecFactory commandSpecFactory) : base(environment, commandSpecFactory) { } + + internal override string ResolveCommandPath(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.ProjectDirectory == null) + { + return null; + } + + return _environment.GetCommandPathFromRootPath( + commandResolverArguments.ProjectDirectory, + commandResolverArguments.CommandName, + commandResolverArguments.InferredExtensions.OrEmptyIfNull()); + } + + internal override CommandResolutionStrategy GetCommandResolutionStrategy() + { + return CommandResolutionStrategy.ProjectLocal; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs new file mode 100644 index 0000000..f104769 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ProjectToolsCommandResolver.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.InternalAbstractions; +using Microsoft.DotNet.ProjectModel; +using Microsoft.DotNet.ProjectModel.Graph; +using Microsoft.Extensions.DependencyModel; +using NuGet.Frameworks; +using FileFormatException = Microsoft.DotNet.ProjectModel.FileFormatException; +using LockFile = Microsoft.DotNet.ProjectModel.Graph.LockFile; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class ProjectToolsCommandResolver : ICommandResolver + { + private static readonly NuGetFramework s_toolPackageFramework = FrameworkConstants.CommonFrameworks.NetCoreApp10; + + private static readonly CommandResolutionStrategy s_commandResolutionStrategy = + CommandResolutionStrategy.ProjectToolsPackage; + + private List _allowedCommandExtensions; + private IPackagedCommandSpecFactory _packagedCommandSpecFactory; + + public ProjectToolsCommandResolver(IPackagedCommandSpecFactory packagedCommandSpecFactory) + { + _packagedCommandSpecFactory = packagedCommandSpecFactory; + + _allowedCommandExtensions = new List() + { + FileNameSuffixes.DotNet.DynamicLib + }; + } + + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.CommandName == null + || commandResolverArguments.ProjectDirectory == null) + { + return null; + } + + return ResolveFromProjectTools( + commandResolverArguments.CommandName, + commandResolverArguments.CommandArguments.OrEmptyIfNull(), + commandResolverArguments.ProjectDirectory); + } + + private CommandSpec ResolveFromProjectTools( + string commandName, + IEnumerable args, + string projectDirectory) + { + var projectContext = GetProjectContextFromDirectoryForFirstTarget(projectDirectory); + + if (projectContext == null) + { + return null; + } + + var toolsLibraries = projectContext.ProjectFile.Tools.OrEmptyIfNull(); + + return ResolveCommandSpecFromAllToolLibraries( + toolsLibraries, + commandName, + args, + projectContext); + } + + private CommandSpec ResolveCommandSpecFromAllToolLibraries( + IEnumerable toolsLibraries, + string commandName, + IEnumerable args, + ProjectContext projectContext) + { + foreach (var toolLibrary in toolsLibraries) + { + var commandSpec = ResolveCommandSpecFromToolLibrary(toolLibrary, commandName, args, projectContext); + + if (commandSpec != null) + { + return commandSpec; + } + } + + return null; + } + + private CommandSpec ResolveCommandSpecFromToolLibrary( + LibraryRange toolLibraryRange, + string commandName, + IEnumerable args, + ProjectContext projectContext) + { + var nugetPackagesRoot = projectContext.PackagesDirectory; + + var lockFile = GetToolLockFile(toolLibraryRange, nugetPackagesRoot); + + var toolLibrary = lockFile.Targets + .FirstOrDefault(t => t.TargetFramework.GetShortFolderName().Equals(s_toolPackageFramework.GetShortFolderName())) + ?.Libraries.FirstOrDefault(l => l.Name == toolLibraryRange.Name); + + if (toolLibrary == null) + { + return null; + } + + var depsFileRoot = Path.GetDirectoryName(lockFile.LockFilePath); + var depsFilePath = GetToolDepsFilePath(toolLibraryRange, lockFile, depsFileRoot); + + return _packagedCommandSpecFactory.CreateCommandSpecFromLibrary( + toolLibrary, + commandName, + args, + _allowedCommandExtensions, + projectContext.PackagesDirectory, + s_commandResolutionStrategy, + depsFilePath, + null); + } + + private LockFile GetToolLockFile( + LibraryRange toolLibrary, + string nugetPackagesRoot) + { + var lockFilePath = GetToolLockFilePath(toolLibrary, nugetPackagesRoot); + + if (!File.Exists(lockFilePath)) + { + return null; + } + + LockFile lockFile = null; + + try + { + lockFile = LockFileReader.Read(lockFilePath, designTime: false); + } + catch (FileFormatException ex) + { + throw ex; + } + + return lockFile; + } + + private string GetToolLockFilePath( + LibraryRange toolLibrary, + string nugetPackagesRoot) + { + var toolPathCalculator = new ToolPathCalculator(nugetPackagesRoot); + + return toolPathCalculator.GetBestLockFilePath( + toolLibrary.Name, + toolLibrary.VersionRange, + s_toolPackageFramework); + } + + private ProjectContext GetProjectContextFromDirectoryForFirstTarget(string projectRootPath) + { + if (projectRootPath == null) + { + return null; + } + + if (!File.Exists(Path.Combine(projectRootPath, Project.FileName))) + { + return null; + } + + var projectContext = ProjectContext.CreateContextForEachTarget(projectRootPath).FirstOrDefault(); + + return projectContext; + } + + private string GetToolDepsFilePath( + LibraryRange toolLibrary, + LockFile toolLockFile, + string depsPathRoot) + { + var depsJsonPath = Path.Combine( + depsPathRoot, + toolLibrary.Name + FileNameSuffixes.DepsJson); + + EnsureToolJsonDepsFileExists(toolLockFile, depsJsonPath); + + return depsJsonPath; + } + + private void EnsureToolJsonDepsFileExists( + LockFile toolLockFile, + string depsPath) + { + if (!File.Exists(depsPath)) + { + GenerateDepsJsonFile(toolLockFile, depsPath); + } + } + + // Need to unit test this, so public + public void GenerateDepsJsonFile( + LockFile toolLockFile, + string depsPath) + { + Reporter.Verbose.WriteLine($"Generating deps.json at: {depsPath}"); + + var projectContext = new ProjectContextBuilder() + .WithLockFile(toolLockFile) + .WithTargetFramework(s_toolPackageFramework.ToString()) + .Build(); + + var exporter = projectContext.CreateExporter(Constants.DefaultConfiguration); + + var dependencyContext = new DependencyContextBuilder() + .Build(null, + null, + exporter.GetAllExports(), + true, + s_toolPackageFramework, + string.Empty); + + var tempDepsFile = Path.GetTempFileName(); + using (var fileStream = File.Open(tempDepsFile, FileMode.Open, FileAccess.Write)) + { + var dependencyContextWriter = new DependencyContextWriter(); + + dependencyContextWriter.Write(dependencyContext, fileStream); + } + + try + { + File.Copy(tempDepsFile, depsPath); + } + catch (Exception e) + { + Reporter.Verbose.WriteLine($"unable to generate deps.json, it may have been already generated: {e.Message}"); + } + finally + { + try + { + File.Delete(tempDepsFile); + } + catch (Exception e2) + { + Reporter.Verbose.WriteLine($"unable to delete temporary deps.json file: {e2.Message}"); + } + } + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/RootedCommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/RootedCommandResolver.cs new file mode 100644 index 0000000..d7dac8b --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/RootedCommandResolver.cs @@ -0,0 +1,25 @@ +using System.IO; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class RootedCommandResolver : ICommandResolver + { + public CommandSpec Resolve(CommandResolverArguments commandResolverArguments) + { + if (commandResolverArguments.CommandName == null) + { + return null; + } + + if (Path.IsPathRooted(commandResolverArguments.CommandName)) + { + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart( + commandResolverArguments.CommandArguments.OrEmptyIfNull()); + + return new CommandSpec(commandResolverArguments.CommandName, escapedArgs, CommandResolutionStrategy.RootedPath); + } + + return null; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ScriptCommandResolverPolicy.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ScriptCommandResolverPolicy.cs new file mode 100644 index 0000000..04878c5 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ScriptCommandResolverPolicy.cs @@ -0,0 +1,39 @@ +using Microsoft.DotNet.InternalAbstractions; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class ScriptCommandResolverPolicy + { + public static CompositeCommandResolver Create() + { + var environment = new EnvironmentProvider(); + + var platformCommandSpecFactory = default(IPlatformCommandSpecFactory); + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + } + else + { + platformCommandSpecFactory = new GenericPlatformCommandSpecFactory(); + } + + return CreateScriptCommandResolver(environment, platformCommandSpecFactory); + } + + public static CompositeCommandResolver CreateScriptCommandResolver( + IEnvironmentProvider environment, + IPlatformCommandSpecFactory platformCommandSpecFactory) + { + var compositeCommandResolver = new CompositeCommandResolver(); + + compositeCommandResolver.AddCommandResolver(new RootedCommandResolver()); + compositeCommandResolver.AddCommandResolver(new MuxerCommandResolver()); + compositeCommandResolver.AddCommandResolver(new ProjectPathCommandResolver(environment, platformCommandSpecFactory)); + compositeCommandResolver.AddCommandResolver(new AppBaseCommandResolver(environment, platformCommandSpecFactory)); + compositeCommandResolver.AddCommandResolver(new PathCommandResolver(environment, platformCommandSpecFactory)); + + return compositeCommandResolver; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ToolPathCalculator.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ToolPathCalculator.cs new file mode 100644 index 0000000..dcb0f15 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/ToolPathCalculator.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Collections.Generic; +using NuGet.Frameworks; +using NuGet.Versioning; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class ToolPathCalculator + { + private readonly string _packagesDirectory; + + public ToolPathCalculator(string packagesDirectory) + { + _packagesDirectory = packagesDirectory; + } + + public string GetBestLockFilePath(string packageId, VersionRange versionRange, NuGetFramework framework) + { + if (versionRange == null) + { + throw new ArgumentNullException(nameof(versionRange)); + } + + if (framework == null) + { + throw new ArgumentNullException(nameof(framework)); + } + + var availableToolVersions = GetAvailableToolVersions(packageId); + + var bestVersion = versionRange.FindBestMatch(availableToolVersions); + if (bestVersion == null) + { + throw new GracefulException($"Version for package `{packageId}` could not be resolved."); + } + + return GetLockFilePath(packageId, bestVersion, framework); + } + + public string GetLockFilePath(string packageId, NuGetVersion version, NuGetFramework framework) + { + if (version == null) + { + throw new ArgumentNullException(nameof(version)); + } + + if (framework == null) + { + throw new ArgumentNullException(nameof(framework)); + } + + return Path.Combine( + GetBaseToolPath(packageId), + version.ToNormalizedString(), + framework.GetShortFolderName(), + "project.lock.json"); + } + + private string GetBaseToolPath(string packageId) + { + return Path.Combine( + _packagesDirectory, + ".tools", + packageId); + } + + private IEnumerable GetAvailableToolVersions(string packageId) + { + var availableVersions = new List(); + + var toolBase = GetBaseToolPath(packageId); + var versionDirectories = Directory.EnumerateDirectories(toolBase); + + foreach (var versionDirectory in versionDirectories) + { + var version = Path.GetFileName(versionDirectory); + + NuGetVersion nugetVersion = null; + NuGetVersion.TryParse(version, out nugetVersion); + + if (nugetVersion != null) + { + availableVersions.Add(nugetVersion); + } + } + + return availableVersions; + } + + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolution/WindowsExePreferredCommandSpecFactory.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/WindowsExePreferredCommandSpecFactory.cs new file mode 100644 index 0000000..4619f5b --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolution/WindowsExePreferredCommandSpecFactory.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class WindowsExePreferredCommandSpecFactory : IPlatformCommandSpecFactory + { + public CommandSpec CreateCommandSpec( + string commandName, + IEnumerable args, + string commandPath, + CommandResolutionStrategy resolutionStrategy, + IEnvironmentProvider environment) + { + var useCmdWrapper = false; + + if (Path.GetExtension(commandPath).Equals(".cmd", StringComparison.OrdinalIgnoreCase)) + { + var preferredCommandPath = environment.GetCommandPath(commandName, ".exe"); + + if (preferredCommandPath == null) + { + useCmdWrapper = true; + } + else + { + commandPath = preferredCommandPath; + } + } + + return useCmdWrapper + ? CreateCommandSpecWrappedWithCmd(commandPath, args, resolutionStrategy) + : CreateCommandSpecFromExecutable(commandPath, args, resolutionStrategy); + } + + private CommandSpec CreateCommandSpecFromExecutable( + string command, + IEnumerable args, + CommandResolutionStrategy resolutionStrategy) + { + var escapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForProcessStart(args); + return new CommandSpec(command, escapedArgs, resolutionStrategy); + } + + private CommandSpec CreateCommandSpecWrappedWithCmd( + string command, + IEnumerable args, + CommandResolutionStrategy resolutionStrategy) + { + var comSpec = Environment.GetEnvironmentVariable("ComSpec") ?? "cmd.exe"; + + // Handle the case where ComSpec is already the command + if (command.Equals(comSpec, StringComparison.OrdinalIgnoreCase)) + { + command = args.FirstOrDefault(); + args = args.Skip(1); + } + + var cmdEscapedArgs = ArgumentEscaper.EscapeAndConcatenateArgArrayForCmdProcessStart(args); + + if (ArgumentEscaper.ShouldSurroundWithQuotes(command)) + { + command = $"\"{command}\""; + } + + var escapedArgString = $"/s /c \"{command} {cmdEscapedArgs}\""; + + return new CommandSpec(comSpec, escapedArgString, resolutionStrategy); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResolver.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResolver.cs new file mode 100644 index 0000000..35f2f3c --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResolver.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.IO; +using Microsoft.DotNet.ProjectModel; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils +{ + internal static class CommandResolver + { + public static CommandSpec TryResolveCommandSpec( + string commandName, + IEnumerable args, + NuGetFramework framework = null, + string configuration = Constants.DefaultConfiguration, + string outputPath = null) + { + var commandResolverArgs = new CommandResolverArguments + { + CommandName = commandName, + CommandArguments = args, + Framework = framework, + ProjectDirectory = Directory.GetCurrentDirectory(), + Configuration = configuration, + OutputPath = outputPath + }; + + var defaultCommandResolver = DefaultCommandResolverPolicy.Create(); + + return defaultCommandResolver.Resolve(commandResolverArgs); + } + + public static CommandSpec TryResolveScriptCommandSpec( + string commandName, + IEnumerable args, + Project project, + string[] inferredExtensionList) + { + var commandResolverArgs = new CommandResolverArguments + { + CommandName = commandName, + CommandArguments = args, + ProjectDirectory = project.ProjectDirectory, + InferredExtensions = inferredExtensionList + }; + + var scriptCommandResolver = ScriptCommandResolverPolicy.Create(); + + return scriptCommandResolver.Resolve(commandResolverArgs); + } + } +} + diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandResult.cs b/test/Microsoft.DotNet.Cli.Utils/CommandResult.cs new file mode 100644 index 0000000..9b7af5b --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandResult.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics; + +namespace Microsoft.DotNet.Cli.Utils +{ + public struct CommandResult + { + public static readonly CommandResult Empty = new CommandResult(); + + public ProcessStartInfo StartInfo { get; } + public int ExitCode { get; } + public string StdOut { get; } + public string StdErr { get; } + + public CommandResult(ProcessStartInfo startInfo, int exitCode, string stdOut, string stdErr) + { + StartInfo = startInfo; + ExitCode = exitCode; + StdOut = stdOut; + StdErr = stdErr; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandSpec.cs b/test/Microsoft.DotNet.Cli.Utils/CommandSpec.cs new file mode 100644 index 0000000..5b94151 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandSpec.cs @@ -0,0 +1,18 @@ +namespace Microsoft.DotNet.Cli.Utils +{ + public class CommandSpec + { + public CommandSpec(string path, string args, CommandResolutionStrategy resolutionStrategy) + { + Path = path; + Args = args; + ResolutionStrategy = resolutionStrategy; + } + + public string Path { get; } + + public string Args { get; } + + public CommandResolutionStrategy ResolutionStrategy { get; } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/CommandUnknownException.cs b/test/Microsoft.DotNet.Cli.Utils/CommandUnknownException.cs new file mode 100644 index 0000000..d3d6fa9 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CommandUnknownException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class CommandUnknownException : GracefulException + { + public CommandUnknownException() + { + } + + public CommandUnknownException(string commandName) : base($"No executable found matching command \"{commandName}\"") + { + } + + public CommandUnknownException(string commandName, Exception innerException) : base($"No executable found matching command \"{commandName}\"", innerException) + { + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/Constants.cs b/test/Microsoft.DotNet.Cli.Utils/Constants.cs new file mode 100644 index 0000000..93bb49d --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Constants.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.DotNet.InternalAbstractions; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class Constants + { + private static Platform CurrentPlatform => RuntimeEnvironment.OperatingSystemPlatform; + public const string DefaultConfiguration = "Debug"; + + public static readonly string ProjectFileName = "project.json"; + public static readonly string ExeSuffix = CurrentPlatform == Platform.Windows ? ".exe" : string.Empty; + public static readonly string ConfigSuffix = ".config"; + + // Priority order of runnable suffixes to look for and run + public static readonly string[] RunnableSuffixes = CurrentPlatform == Platform.Windows + ? new string[] { ".exe", ".cmd", ".bat" } + : new string[] { string.Empty }; + + public static readonly string BinDirectoryName = "bin"; + public static readonly string ObjDirectoryName = "obj"; + + public static readonly string DynamicLibSuffix = CurrentPlatform == Platform.Windows ? ".dll" : + CurrentPlatform == Platform.Darwin ? ".dylib" : ".so"; + + public static readonly string LibCoreClrFileName = (CurrentPlatform == Platform.Windows ? "coreclr" : "libcoreclr"); + public static readonly string LibCoreClrName = LibCoreClrFileName + DynamicLibSuffix; + + public static readonly string StaticLibSuffix = CurrentPlatform == Platform.Windows ? ".lib" : ".a"; + + public static readonly string ResponseFileSuffix = ".rsp"; + + public static readonly string PublishedHostExecutableName = "dotnet"; + public static readonly string HostExecutableName = "corehost" + ExeSuffix; + public static readonly string[] HostBinaryNames = new string[] { + HostExecutableName, + (CurrentPlatform == Platform.Windows ? "hostpolicy" : "libhostpolicy") + DynamicLibSuffix, + (CurrentPlatform == Platform.Windows ? "hostfxr" : "libhostfxr") + DynamicLibSuffix + }; + + public static readonly string[] LibCoreClrBinaryNames = new string[] + { + "coreclr.dll", + "libcoreclr.so", + "libcoreclr.dylib" + }; + + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/CoreHost.cs b/test/Microsoft.DotNet.Cli.Utils/CoreHost.cs new file mode 100644 index 0000000..2424bf6 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/CoreHost.cs @@ -0,0 +1,52 @@ +using System; +using System.IO; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class CoreHost + { + internal static string _hostDir; + internal static string _hostExePath; + + public static string HostExePath + { + get + { + if (_hostExePath == null) + { + _hostExePath = Path.Combine(HostDir, Constants.HostExecutableName); + } + return _hostExePath; + } + } + + private static string HostDir + { + get + { + if (_hostDir == null) + { + var fxDepsFile = Muxer.GetDataFromAppDomain("FX_DEPS_FILE"); + _hostDir = Path.GetDirectoryName(fxDepsFile); + } + + return _hostDir; + } + } + + public static void CopyTo(string destinationPath, string hostExeName) + { + foreach (var binaryName in Constants.HostBinaryNames) + { + var outputBinaryName = binaryName.Equals(Constants.HostExecutableName) + ? hostExeName : binaryName; + var outputBinaryPath = Path.Combine(destinationPath, outputBinaryName); + var hostBinaryPath = Path.Combine(HostDir, binaryName); + File.Copy(hostBinaryPath, outputBinaryPath, overwrite: true); + + // Update the last write time so this file can be treated as an output of a build + File.SetLastWriteTimeUtc(outputBinaryPath, DateTime.UtcNow); + } + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/DebugHelper.cs b/test/Microsoft.DotNet.Cli.Utils/DebugHelper.cs new file mode 100644 index 0000000..1675e83 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/DebugHelper.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class DebugHelper + { + [Conditional("DEBUG")] + public static void HandleDebugSwitch(ref string[] args) + { + if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase)) + { + args = args.Skip(1).ToArray(); + WaitForDebugger(); + } + } + + public static void WaitForDebugger() + { + Console.WriteLine("Waiting for debugger to attach. Press ENTER to continue"); + Console.WriteLine($"Process ID: {Process.GetCurrentProcess().Id}"); + Console.ReadLine(); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/DepsJsonCommandFactory.cs b/test/Microsoft.DotNet.Cli.Utils/DepsJsonCommandFactory.cs new file mode 100644 index 0000000..55108f4 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/DepsJsonCommandFactory.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using NuGet.Frameworks; +using System.IO; +using System; +using Microsoft.DotNet.ProjectModel; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class DepsJsonCommandFactory : ICommandFactory + { + private DepsJsonCommandResolver _depsJsonCommandResolver; + private string _temporaryDirectory; + private string _depsJsonFile; + private string _runtimeConfigFile; + + public DepsJsonCommandFactory( + string depsJsonFile, + string runtimeConfigFile, + string nugetPackagesRoot, + string temporaryDirectory) + { + _depsJsonCommandResolver = new DepsJsonCommandResolver(nugetPackagesRoot); + + _temporaryDirectory = temporaryDirectory; + _depsJsonFile = depsJsonFile; + _runtimeConfigFile = runtimeConfigFile; + } + + public ICommand Create( + string commandName, + IEnumerable args, + NuGetFramework framework = null, + string configuration = Constants.DefaultConfiguration) + { + var commandResolverArgs = new CommandResolverArguments() + { + CommandName = commandName, + CommandArguments = args, + DepsJsonFile = _depsJsonFile + }; + + var commandSpec = _depsJsonCommandResolver.Resolve(commandResolverArgs); + + return Command.Create(commandSpec); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs b/test/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs new file mode 100644 index 0000000..56f6471 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/DotnetFiles.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Reflection; +using Microsoft.DotNet.InternalAbstractions; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class DotnetFiles + { + private static string SdkRootFolder => Path.Combine(typeof(DotnetFiles).GetTypeInfo().Assembly.Location, ".."); + + private static Lazy s_versionFileObject = + new Lazy(() => new DotnetVersionFile(VersionFile)); + + /// + /// The CLI ships with a .version file that stores the commit information and CLI version + /// + public static string VersionFile => Path.GetFullPath(Path.Combine(SdkRootFolder, ".version")); + + internal static DotnetVersionFile VersionFileObject + { + get { return s_versionFileObject.Value; } + } + + public static string NuGetPackagesArchive => + Path.GetFullPath(Path.Combine(SdkRootFolder, "nuGetPackagesArchive.lzma")); + + /// + /// Reads the version file and adds runtime specific information + /// + public static string ReadAndInterpretVersionFile() + { + var content = File.ReadAllText(DotnetFiles.VersionFile); + content += Environment.NewLine; + content += RuntimeEnvironment.GetRuntimeIdentifier(); + return content; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/DotnetRuntimeIdentifiers.cs b/test/Microsoft.DotNet.Cli.Utils/DotnetRuntimeIdentifiers.cs new file mode 100644 index 0000000..7f3da4c --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/DotnetRuntimeIdentifiers.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Microsoft.DotNet.InternalAbstractions; + +namespace Microsoft.DotNet.Cli.Utils +{ + internal static class DotnetRuntimeIdentifiers + { + public static IEnumerable InferCurrentRuntimeIdentifiers() + { + IEnumerable fallbackIdentifiers = null; + + // If the machine's RID isn't supported by the shared framework (i.e. the CLI + // is being used on a newer version of an OS), add the RID that the CLI was built + // with as a fallback. The RID the CLI was built with will have the correct + // runtime.* NuGet packages available. + // For example, when a user is using osx.10.12, but we only support osx.10.10 and + // osx.10.11, the project.json "runtimes" section cannot contain osx.10.12, since + // that RID isn't contained in the runtime graph - users will get a restore error. + FrameworkDependencyFile fxDepsFile = new FrameworkDependencyFile(); + if (!fxDepsFile.SupportsCurrentRuntime()) + { + string buildRid = DotnetFiles.VersionFileObject.BuildRid; + if (!string.IsNullOrEmpty(buildRid)) + { + fallbackIdentifiers = new string[] { buildRid }; + } + } + + return RuntimeEnvironmentRidExtensions.GetAllCandidateRuntimeIdentifiers(fallbackIdentifiers); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/DotnetVersionFile.cs b/test/Microsoft.DotNet.Cli.Utils/DotnetVersionFile.cs new file mode 100644 index 0000000..1f76067 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/DotnetVersionFile.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.DotNet.Cli.Utils +{ + internal class DotnetVersionFile + { + public bool Exists { get; set; } + + public string CommitSha { get; set; } + + public string BuildNumber { get; set; } + + /// + /// The runtime identifier (rid) that this CLI was built for. + /// + /// + /// This is different than RuntimeEnvironment.GetRuntimeIdentifier() because the + /// BuildRid is a RID that is guaranteed to exist and works on the current machine. The + /// RuntimeEnvironment.GetRuntimeIdentifier() may be for a new version of the OS that + /// doesn't have full support yet. + /// + public string BuildRid { get; set; } + + public DotnetVersionFile(string versionFilePath) + { + Exists = File.Exists(versionFilePath); + + if (Exists) + { + IEnumerable lines = File.ReadLines(versionFilePath); + + int index = 0; + foreach (string line in lines) + { + if (index == 0) + { + CommitSha = line.Substring(0, 10); + } + else if (index == 1) + { + BuildNumber = line; + } + else if (index == 2) + { + BuildRid = line; + } + else + { + break; + } + + index++; + } + } + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/Env.cs b/test/Microsoft.DotNet.Cli.Utils/Env.cs new file mode 100644 index 0000000..20e9e43 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Env.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class Env + { + private static IEnvironmentProvider _environment = new EnvironmentProvider(); + + public static IEnumerable ExecutableExtensions + { + get + { + return _environment.ExecutableExtensions; + } + } + + public static string GetCommandPath(string commandName, params string[] extensions) + { + return _environment.GetCommandPath(commandName, extensions); + } + + public static string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions) + { + return _environment.GetCommandPathFromRootPath(rootPath, commandName, extensions); + } + + public static string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions) + { + return _environment.GetCommandPathFromRootPath(rootPath, commandName, extensions); + } + + public static bool GetEnvironmentVariableAsBool(string name, bool defaultValue = false) + { + return _environment.GetEnvironmentVariableAsBool(name, defaultValue); + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs b/test/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs new file mode 100644 index 0000000..aa73345 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/EnvironmentProvider.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.InternalAbstractions; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class EnvironmentProvider : IEnvironmentProvider + { + private IEnumerable _searchPaths; + private IEnumerable _executableExtensions; + + public IEnumerable ExecutableExtensions + { + get + { + if (_executableExtensions == null) + { + + _executableExtensions = RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows + ? Environment.GetEnvironmentVariable("PATHEXT") + .Split(';') + .Select(e => e.ToLower().Trim('"')) + : new [] { string.Empty }; + } + + return _executableExtensions; + } + } + + private IEnumerable SearchPaths + { + get + { + if (_searchPaths == null) + { + var searchPaths = new List { ApplicationEnvironment.ApplicationBasePath }; + + searchPaths.AddRange(Environment + .GetEnvironmentVariable("PATH") + .Split(Path.PathSeparator) + .Select(p => p.Trim('"'))); + + _searchPaths = searchPaths; + } + + return _searchPaths; + } + } + + public EnvironmentProvider( + IEnumerable extensionsOverride = null, + IEnumerable searchPathsOverride = null) + { + _executableExtensions = extensionsOverride; + _searchPaths = searchPathsOverride; + } + + public string GetCommandPath(string commandName, params string[] extensions) + { + if (!extensions.Any()) + { + extensions = ExecutableExtensions.ToArray(); + } + + var commandPath = SearchPaths.Join( + extensions, + p => true, s => true, + (p, s) => Path.Combine(p, commandName + s)) + .FirstOrDefault(File.Exists); + + return commandPath; + } + + public string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions) + { + if (!extensions.Any()) + { + extensions = ExecutableExtensions.ToArray(); + } + + var commandPath = extensions.Select(e => Path.Combine(rootPath, commandName + e)) + .FirstOrDefault(File.Exists); + + return commandPath; + } + + public string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions) + { + var extensionsArr = extensions.OrEmptyIfNull().ToArray(); + + return GetCommandPathFromRootPath(rootPath, commandName, extensionsArr); + } + + public bool GetEnvironmentVariableAsBool(string name, bool defaultValue) + { + var str = Environment.GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(str)) + { + return defaultValue; + } + + switch (str.ToLowerInvariant()) + { + case "true": + case "1": + case "yes": + return true; + case "false": + case "0": + case "no": + return false; + default: + return defaultValue; + } + } + + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/Extensions/CollectionsExtensions.cs b/test/Microsoft.DotNet.Cli.Utils/Extensions/CollectionsExtensions.cs new file mode 100644 index 0000000..08779f6 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Extensions/CollectionsExtensions.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class CollectionsExtensions + { + public static IEnumerable OrEmptyIfNull(this IEnumerable enumerable) + { + return enumerable == null + ? Enumerable.Empty() + : enumerable; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/Extensions/ProjectContextCollectionExtensions.cs b/test/Microsoft.DotNet.Cli.Utils/Extensions/ProjectContextCollectionExtensions.cs new file mode 100644 index 0000000..249df0e --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Extensions/ProjectContextCollectionExtensions.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.DotNet.ProjectModel; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class ProjectContextCollectionExtensions + { + public static ProjectContextCollection EnsureValid(this ProjectContextCollection contextCollection, string projectFilePath) + { + IEnumerable errors; + + if (contextCollection == null) + { + errors = new[] + { + new DiagnosticMessage( + ErrorCodes.DOTNET1017, + $"Project file does not exist '{ProjectPathHelper.NormalizeProjectFilePath(projectFilePath)}'.", + projectFilePath, + DiagnosticMessageSeverity.Error) + }; + } + else + { + errors = contextCollection + .ProjectDiagnostics + .Where(d => d.Severity == DiagnosticMessageSeverity.Error); + } + + if (errors.Any()) + { + StringBuilder errorMessage = new StringBuilder($"The current project is not valid because of the following errors:{Environment.NewLine}"); + + foreach (DiagnosticMessage message in errors) + { + errorMessage.AppendLine(message.FormattedMessage); + } + + throw new GracefulException(errorMessage.ToString()); + } + + return contextCollection; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/FrameworkDependencyFile.cs b/test/Microsoft.DotNet.Cli.Utils/FrameworkDependencyFile.cs new file mode 100644 index 0000000..24130ed --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/FrameworkDependencyFile.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using System.Linq; +using Microsoft.DotNet.InternalAbstractions; +using Microsoft.Extensions.DependencyModel; + +namespace Microsoft.DotNet.Cli.Utils +{ + /// + /// Represents the .deps.json file in the shared framework + /// that the CLI is running against. + /// + internal class FrameworkDependencyFile + { + private readonly string _depsFilePath; + + public FrameworkDependencyFile() + { + _depsFilePath = Muxer.GetDataFromAppDomain("FX_DEPS_FILE"); + } + + public bool SupportsCurrentRuntime() + { + return IsRuntimeSupported(RuntimeEnvironment.GetRuntimeIdentifier()); + } + + public bool IsRuntimeSupported(string runtimeIdentifier) + { + DependencyContext fxDependencyContext = CreateDependencyContext(); + + return fxDependencyContext.RuntimeGraph.Any(g => g.Runtime == runtimeIdentifier); + } + + private DependencyContext CreateDependencyContext() + { + using (Stream depsFileStream = File.OpenRead(_depsFilePath)) + using (DependencyContextJsonReader reader = new DependencyContextJsonReader()) + { + return reader.Read(depsFileStream); + } + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/GracefulException.cs b/test/Microsoft.DotNet.Cli.Utils/GracefulException.cs new file mode 100644 index 0000000..4c061e4 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/GracefulException.cs @@ -0,0 +1,19 @@ +using System; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class GracefulException : Exception + { + public GracefulException() + { + } + + public GracefulException(string message) : base(message) + { + } + + public GracefulException(string message, Exception innerException) : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/IBuiltInCommandEnvironment.cs b/test/Microsoft.DotNet.Cli.Utils/IBuiltInCommandEnvironment.cs new file mode 100644 index 0000000..e71cbdb --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/IBuiltInCommandEnvironment.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; + +namespace Microsoft.DotNet.Cli.Utils +{ + internal interface IBuiltInCommandEnvironment + { + TextWriter GetConsoleOut(); + void SetConsoleOut(TextWriter newOut); + + TextWriter GetConsoleError(); + void SetConsoleError(TextWriter newError); + + string GetWorkingDirectory(); + void SetWorkingDirectory(string path); + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/ICommand.cs b/test/Microsoft.DotNet.Cli.Utils/ICommand.cs new file mode 100644 index 0000000..9f1cfe3 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/ICommand.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; + +namespace Microsoft.DotNet.Cli.Utils +{ + public interface ICommand + { + CommandResult Execute(); + + ICommand WorkingDirectory(string projectDirectory); + + ICommand EnvironmentVariable(string name, string value); + + ICommand CaptureStdOut(); + + ICommand CaptureStdErr(); + + ICommand ForwardStdOut(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true); + + ICommand ForwardStdErr(TextWriter to = null, bool onlyIfVerbose = false, bool ansiPassThrough = true); + + ICommand OnOutputLine(Action handler); + + ICommand OnErrorLine(Action handler); + + CommandResolutionStrategy ResolutionStrategy { get; } + + string CommandName { get; } + + string CommandArgs { get; } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/ICommandFactory.cs b/test/Microsoft.DotNet.Cli.Utils/ICommandFactory.cs new file mode 100644 index 0000000..017d7a0 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/ICommandFactory.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public interface ICommandFactory + { + ICommand Create( + string commandName, + IEnumerable args, + NuGetFramework framework = null, + string configuration = Constants.DefaultConfiguration); + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs b/test/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs new file mode 100644 index 0000000..e9cc613 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/IEnvironmentProvider.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Microsoft.DotNet.Cli.Utils +{ + public interface IEnvironmentProvider + { + IEnumerable ExecutableExtensions { get; } + + string GetCommandPath(string commandName, params string[] extensions); + + string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions); + + string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions); + + bool GetEnvironmentVariableAsBool(string name, bool defaultValue); + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/InvalidProjectException.cs b/test/Microsoft.DotNet.Cli.Utils/InvalidProjectException.cs new file mode 100644 index 0000000..c858f03 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/InvalidProjectException.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class InvalidProjectException : Exception + { + public InvalidProjectException() { } + public InvalidProjectException(string message) : base(message) { } + public InvalidProjectException(string message, Exception innerException) : base(message, innerException) { } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj b/test/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj new file mode 100644 index 0000000..031f4af --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Microsoft.DotNet.Cli.Utils.csproj @@ -0,0 +1,19 @@ + + + + net451;netstandard1.6 + + + + + + + + + + + + + + + diff --git a/test/Microsoft.DotNet.Cli.Utils/Muxer.cs b/test/Microsoft.DotNet.Cli.Utils/Muxer.cs new file mode 100644 index 0000000..a2387c1 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Muxer.cs @@ -0,0 +1,82 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class Muxer + { + public static readonly string MuxerName = "dotnet"; + private static readonly string s_muxerFileName = MuxerName + Constants.ExeSuffix; + + private string _muxerPath; + + public string MuxerPath + { + get + { + if (_muxerPath == null) + { + throw new InvalidOperationException("Unable to locate dotnet multiplexer"); + } + return _muxerPath; + } + } + + public Muxer() + { + if (!TryResolveMuxerFromParentDirectories()) + { + TryResolverMuxerFromPath(); + } + } + + public static string GetDataFromAppDomain(string propertyName) + { + var appDomainType = typeof(object).GetTypeInfo().Assembly?.GetType("System.AppDomain"); + var currentDomain = appDomainType?.GetProperty("CurrentDomain")?.GetValue(null); + var deps = appDomainType?.GetMethod("GetData")?.Invoke(currentDomain, new[] { propertyName }); + + return deps as string; + } + + private bool TryResolveMuxerFromParentDirectories() + { + var fxDepsFile = GetDataFromAppDomain("FX_DEPS_FILE"); + if (string.IsNullOrEmpty(fxDepsFile)) + { + return false; + } + + var muxerDir = new FileInfo(fxDepsFile).Directory?.Parent?.Parent?.Parent; + if (muxerDir == null) + { + return false; + } + + var muxerCandidate = Path.Combine(muxerDir.FullName, s_muxerFileName); + + if (!File.Exists(muxerCandidate)) + { + return false; + } + + _muxerPath = muxerCandidate; + return true; + } + + private bool TryResolverMuxerFromPath() + { + var muxerPath = Env.GetCommandPath(MuxerName, Constants.ExeSuffix); + + if (muxerPath == null || !File.Exists(muxerPath)) + { + return false; + } + + _muxerPath = muxerPath; + + return true; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/PathUtility.cs b/test/Microsoft.DotNet.Cli.Utils/PathUtility.cs new file mode 100644 index 0000000..c2e9c36 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/PathUtility.cs @@ -0,0 +1,247 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using Microsoft.DotNet.InternalAbstractions; + +namespace Microsoft.DotNet.Tools.Common +{ + public static class PathUtility + { + public static bool IsPlaceholderFile(string path) + { + return string.Equals(Path.GetFileName(path), "_._", StringComparison.Ordinal); + } + + public static bool IsChildOfDirectory(string dir, string candidate) + { + if (dir == null) + { + throw new ArgumentNullException(nameof(dir)); + } + if (candidate == null) + { + throw new ArgumentNullException(nameof(candidate)); + } + dir = Path.GetFullPath(dir); + dir = EnsureTrailingSlash(dir); + candidate = Path.GetFullPath(candidate); + return candidate.StartsWith(dir, StringComparison.OrdinalIgnoreCase); + } + + public static string EnsureTrailingSlash(string path) + { + return EnsureTrailingCharacter(path, Path.DirectorySeparatorChar); + } + + public static string EnsureTrailingForwardSlash(string path) + { + return EnsureTrailingCharacter(path, '/'); + } + + private static string EnsureTrailingCharacter(string path, char trailingCharacter) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + // if the path is empty, we want to return the original string instead of a single trailing character. + if (path.Length == 0 || path[path.Length - 1] == trailingCharacter) + { + return path; + } + + return path + trailingCharacter; + } + + public static void EnsureParentDirectory(string filePath) + { + string directory = Path.GetDirectoryName(filePath); + + EnsureDirectory(directory); + } + + public static void EnsureDirectory(string directoryPath) + { + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + } + + /// + /// Returns path2 relative to path1, with Path.DirectorySeparatorChar as separator + /// + public static string GetRelativePath(string path1, string path2) + { + return GetRelativePath(path1, path2, Path.DirectorySeparatorChar, true); + } + + /// + /// Returns path2 relative to path1, with Path.DirectorySeparatorChar as separator but ignoring directory + /// traversals. + /// + public static string GetRelativePathIgnoringDirectoryTraversals(string path1, string path2) + { + return GetRelativePath(path1, path2, Path.DirectorySeparatorChar, false); + } + + /// + /// Returns path2 relative to path1, with given path separator + /// + public static string GetRelativePath(string path1, string path2, char separator, bool includeDirectoryTraversals) + { + if (string.IsNullOrEmpty(path1)) + { + throw new ArgumentException("Path must have a value", nameof(path1)); + } + + if (string.IsNullOrEmpty(path2)) + { + throw new ArgumentException("Path must have a value", nameof(path2)); + } + + StringComparison compare; + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + compare = StringComparison.OrdinalIgnoreCase; + // check if paths are on the same volume + if (!string.Equals(Path.GetPathRoot(path1), Path.GetPathRoot(path2))) + { + // on different volumes, "relative" path is just path2 + return path2; + } + } + else + { + compare = StringComparison.Ordinal; + } + + var index = 0; + var path1Segments = path1.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var path2Segments = path2.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + // if path1 does not end with / it is assumed the end is not a directory + // we will assume that is isn't a directory by ignoring the last split + var len1 = path1Segments.Length - 1; + var len2 = path2Segments.Length; + + // find largest common absolute path between both paths + var min = Math.Min(len1, len2); + while (min > index) + { + if (!string.Equals(path1Segments[index], path2Segments[index], compare)) + { + break; + } + // Handle scenarios where folder and file have same name (only if os supports same name for file and directory) + // e.g. /file/name /file/name/app + else if ((len1 == index && len2 > index + 1) || (len1 > index && len2 == index + 1)) + { + break; + } + ++index; + } + + var path = ""; + + // check if path2 ends with a non-directory separator and if path1 has the same non-directory at the end + if (len1 + 1 == len2 && !string.IsNullOrEmpty(path1Segments[index]) && + string.Equals(path1Segments[index], path2Segments[index], compare)) + { + return path; + } + + if (includeDirectoryTraversals) + { + for (var i = index; len1 > i; ++i) + { + path += ".." + separator; + } + } + + for (var i = index; len2 - 1 > i; ++i) + { + path += path2Segments[i] + separator; + } + // if path2 doesn't end with an empty string it means it ended with a non-directory name, so we add it back + if (!string.IsNullOrEmpty(path2Segments[len2 - 1])) + { + path += path2Segments[len2 - 1]; + } + + return path; + } + + public static string GetAbsolutePath(string basePath, string relativePath) + { + if (basePath == null) + { + throw new ArgumentNullException(nameof(basePath)); + } + + if (relativePath == null) + { + throw new ArgumentNullException(nameof(relativePath)); + } + + Uri resultUri = new Uri(new Uri(basePath), new Uri(relativePath, UriKind.Relative)); + return resultUri.LocalPath; + } + + public static string GetDirectoryName(string path) + { + path = path.TrimEnd(Path.DirectorySeparatorChar); + return path.Substring(Path.GetDirectoryName(path).Length).Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + } + + public static string GetPathWithForwardSlashes(string path) + { + return path.Replace('\\', '/'); + } + + public static string GetPathWithBackSlashes(string path) + { + return path.Replace('/', '\\'); + } + + public static string GetPathWithDirectorySeparator(string path) + { + if (Path.DirectorySeparatorChar == '/') + { + return GetPathWithForwardSlashes(path); + } + else + { + return GetPathWithBackSlashes(path); + } + } + + public static bool HasExtension(string filePath, string extension) + { + var comparison = StringComparison.Ordinal; + + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + comparison = StringComparison.OrdinalIgnoreCase; + } + + return Path.GetExtension(filePath).Equals(extension, comparison); + } + + /// + /// Gets the fully-qualified path without failing if the + /// path is empty. + /// + public static string GetFullPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return path; + } + + return Path.GetFullPath(path); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/Product.cs b/test/Microsoft.DotNet.Cli.Utils/Product.cs new file mode 100644 index 0000000..8d2a8f0 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Product.cs @@ -0,0 +1,17 @@ +using System; +using System.Reflection; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class Product + { + public static readonly string LongName = ".NET Command Line Tools"; + public static readonly string Version = GetProductVersion(); + + private static string GetProductVersion() + { + var attr = typeof(Product).GetTypeInfo().Assembly.GetCustomAttribute(); + return attr?.InformationalVersion; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/ProjectDependenciesCommandFactory.cs b/test/Microsoft.DotNet.Cli.Utils/ProjectDependenciesCommandFactory.cs new file mode 100644 index 0000000..0f5cb31 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/ProjectDependenciesCommandFactory.cs @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using Microsoft.DotNet.InternalAbstractions; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class ProjectDependenciesCommandFactory : ICommandFactory + { + private readonly NuGetFramework _nugetFramework; + private readonly string _configuration; + private readonly string _outputPath; + private readonly string _buildBasePath; + private readonly string _projectDirectory; + + public ProjectDependenciesCommandFactory( + NuGetFramework nugetFramework, + string configuration, + string outputPath, + string buildBasePath, + string projectDirectory) + { + _nugetFramework = nugetFramework; + _configuration = configuration; + _outputPath = outputPath; + _buildBasePath = buildBasePath; + _projectDirectory = projectDirectory; + + if (_configuration == null) + { + _configuration = Constants.DefaultConfiguration; + } + } + + public ICommand Create( + string commandName, + IEnumerable args, + NuGetFramework framework = null, + string configuration = null) + { + if (string.IsNullOrEmpty(configuration)) + { + configuration = _configuration; + } + + if (framework == null) + { + framework = _nugetFramework; + } + + var commandSpec = FindProjectDependencyCommands( + commandName, + args, + configuration, + framework, + _outputPath, + _buildBasePath, + _projectDirectory); + + return Command.Create(commandSpec); + } + + private CommandSpec FindProjectDependencyCommands( + string commandName, + IEnumerable commandArgs, + string configuration, + NuGetFramework framework, + string outputPath, + string buildBasePath, + string projectDirectory) + { + var commandResolverArguments = new CommandResolverArguments + { + CommandName = commandName, + CommandArguments = commandArgs, + Framework = framework, + Configuration = configuration, + OutputPath = outputPath, + BuildBasePath = buildBasePath, + ProjectDirectory = projectDirectory + }; + + var commandResolver = GetProjectDependenciesCommandResolver(framework); + + var commandSpec = commandResolver.Resolve(commandResolverArguments); + if (commandSpec == null) + { + throw new CommandUnknownException(commandName); + } + + return commandSpec; + } + + private ICommandResolver GetProjectDependenciesCommandResolver(NuGetFramework framework) + { + var environment = new EnvironmentProvider(); + + if (framework.IsDesktop()) + { + IPlatformCommandSpecFactory platformCommandSpecFactory = null; + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + platformCommandSpecFactory = new WindowsExePreferredCommandSpecFactory(); + } + else + { + platformCommandSpecFactory = new GenericPlatformCommandSpecFactory(); + } + + return new OutputPathCommandResolver(environment, platformCommandSpecFactory); + } + else + { + var packagedCommandSpecFactory = new PackagedCommandSpecFactory(); + return new ProjectDependenciesCommandResolver(environment, packagedCommandSpecFactory); + } + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/Properties/AssemblyInfo.cs b/test/Microsoft.DotNet.Cli.Utils/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..9b043e9 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: AssemblyMetadataAttribute("Serviceable", "True")] +[assembly: InternalsVisibleTo("dotnet, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.DotNet.Cli.Utils.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/test/Microsoft.DotNet.Cli.Utils/Reporter.cs b/test/Microsoft.DotNet.Cli.Utils/Reporter.cs new file mode 100644 index 0000000..0afffa1 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Reporter.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.Cli.Utils +{ + // Stupid-simple console manager + public class Reporter + { + private static readonly Reporter NullReporter = new Reporter(console: null); + private static object _lock = new object(); + + private readonly AnsiConsole _console; + + static Reporter() + { + Reset(); + } + + private Reporter(AnsiConsole console) + { + _console = console; + } + + public static Reporter Output { get; private set; } + public static Reporter Error { get; private set; } + public static Reporter Verbose { get; private set; } + + /// + /// Resets the Reporters to write to the current Console Out/Error. + /// + public static void Reset() + { + lock (_lock) + { + Output = new Reporter(AnsiConsole.GetOutput()); + Error = new Reporter(AnsiConsole.GetError()); + Verbose = IsVerbose ? + new Reporter(AnsiConsole.GetOutput()) : + NullReporter; + } + } + + public static bool IsVerbose => CommandContext.IsVerbose(); + + public void WriteLine(string message) + { + lock (_lock) + { + if (CommandContext.ShouldPassAnsiCodesThrough()) + { + _console?.Writer?.WriteLine(message); + } + else + { + _console?.WriteLine(message); + } + } + } + + public void WriteLine() + { + lock (_lock) + { + _console?.Writer?.WriteLine(); + } + } + + public void Write(string message) + { + lock (_lock) + { + if (CommandContext.ShouldPassAnsiCodesThrough()) + { + _console?.Writer?.Write(message); + } + else + { + _console?.Write(message); + } + } + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/ScriptExecutor.cs b/test/Microsoft.DotNet.Cli.Utils/ScriptExecutor.cs new file mode 100644 index 0000000..e2dbd3b --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/ScriptExecutor.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.DotNet.Cli.Utils.CommandParsing; +using Microsoft.DotNet.InternalAbstractions; +using Microsoft.DotNet.ProjectModel; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class ScriptExecutor + { + public static ICommand CreateCommandForScript(Project project, string scriptCommandLine, IDictionary variables) + { + return CreateCommandForScript(project, scriptCommandLine, WrapVariableDictionary(variables)); + } + + public static ICommand CreateCommandForScript(Project project, string scriptCommandLine, Func getVariable) + { + var scriptArguments = ParseScriptArguments(project, scriptCommandLine, getVariable); + if (scriptArguments == null) + { + throw new Exception($"ScriptExecutor: failed to parse script \"{scriptCommandLine}\""); + } + + var inferredExtensions = DetermineInferredScriptExtensions(); + + return Command + .CreateForScript(scriptArguments.First(), scriptArguments.Skip(1), project, inferredExtensions) + .WorkingDirectory(project.ProjectDirectory); + } + + private static IEnumerable ParseScriptArguments(Project project, string scriptCommandLine, Func getVariable) + { + var scriptArguments = CommandGrammar.Process( + scriptCommandLine, + GetScriptVariable(project, getVariable), + preserveSurroundingQuotes: false); + + scriptArguments = scriptArguments.Where(argument => !string.IsNullOrEmpty(argument)).ToArray(); + if (scriptArguments.Length == 0) + { + return null; + } + + return scriptArguments; + } + + private static string[] DetermineInferredScriptExtensions() + { + if (RuntimeEnvironment.OperatingSystemPlatform == Platform.Windows) + { + return new string[] { "", ".cmd" }; + } + else + { + return new string[] { "", ".sh" }; + } + } + + private static Func WrapVariableDictionary(IDictionary contextVariables) + { + return key => + { + string value; + contextVariables.TryGetValue(key, out value); + return value; + }; + } + + private static Func GetScriptVariable(Project project, Func getVariable) + { + var keys = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + { "project:Directory", () => project.ProjectDirectory }, + { "project:Name", () => project.Name }, + { "project:Version", () => project.Version.ToString() }, + }; + + return key => + { + // try returning key from dictionary + Func valueFactory; + if (keys.TryGetValue(key, out valueFactory)) + { + return valueFactory(); + } + + // try returning command-specific key + var value = getVariable(key); + if (!string.IsNullOrEmpty(value)) + { + return value; + } + + // try returning environment variable + return Environment.GetEnvironmentVariable(key); + }; + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/ScriptNames.cs b/test/Microsoft.DotNet.Cli.Utils/ScriptNames.cs new file mode 100644 index 0000000..41bece0 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/ScriptNames.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class ScriptNames + { + public const string PreCompile = "precompile"; + public const string PostCompile = "postcompile"; + public const string PrePublish = "prepublish"; + public const string PostPublish = "postpublish"; + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/StreamForwarder.cs b/test/Microsoft.DotNet.Cli.Utils/StreamForwarder.cs new file mode 100644 index 0000000..aa16cb2 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/StreamForwarder.cs @@ -0,0 +1,133 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Cli.Utils +{ + public sealed class StreamForwarder + { + private static readonly char[] s_ignoreCharacters = new char[] { '\r' }; + private static readonly char s_flushBuilderCharacter = '\n'; + + private StringBuilder _builder; + private StringWriter _capture; + private Action _writeLine; + + public string CapturedOutput + { + get + { + return _capture?.GetStringBuilder()?.ToString(); + } + } + + public StreamForwarder Capture() + { + ThrowIfCaptureSet(); + + _capture = new StringWriter(); + + return this; + } + + public StreamForwarder ForwardTo(Action writeLine) + { + ThrowIfNull(writeLine); + + ThrowIfForwarderSet(); + + _writeLine = writeLine; + + return this; + } + + public Task BeginRead(TextReader reader) + { + return Task.Run(() => Read(reader)); + } + + public void Read(TextReader reader) + { + var bufferSize = 1; + + int readCharacterCount; + char currentCharacter; + + var buffer = new char[bufferSize]; + _builder = new StringBuilder(); + + // Using Read with buffer size 1 to prevent looping endlessly + // like we would when using Read() with no buffer + while ((readCharacterCount = reader.Read(buffer, 0, bufferSize)) > 0) + { + currentCharacter = buffer[0]; + + if (currentCharacter == s_flushBuilderCharacter) + { + WriteBuilder(); + } + else if (! s_ignoreCharacters.Contains(currentCharacter)) + { + _builder.Append(currentCharacter); + } + } + + // Flush anything else when the stream is closed + // Which should only happen if someone used console.Write + WriteBuilder(); + } + + private void WriteBuilder() + { + if (_builder.Length == 0) + { + return; + } + + WriteLine(_builder.ToString()); + _builder.Clear(); + } + + private void WriteLine(string str) + { + if (_capture != null) + { + _capture.WriteLine(str); + } + + if (_writeLine != null) + { + _writeLine(str); + } + } + + private void ThrowIfNull(object obj) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + } + + private void ThrowIfForwarderSet() + { + if (_writeLine != null) + { + throw new InvalidOperationException("WriteLine forwarder set previously"); + } + } + + private void ThrowIfCaptureSet() + { + if (_capture != null) + { + throw new InvalidOperationException("Already capturing stream!"); + } + } + } +} diff --git a/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTrace.cs b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTrace.cs new file mode 100644 index 0000000..abbf866 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTrace.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; + +namespace Microsoft.DotNet.Cli.Utils +{ + public static class PerfTrace + { + private static ConcurrentBag _threads = new ConcurrentBag(); + + [ThreadStatic] + private static PerfTraceThreadContext _current; + + public static bool Enabled { get; set; } + + public static PerfTraceThreadContext Current => _current ?? (_current = InitializeCurrent()); + + private static PerfTraceThreadContext InitializeCurrent() + { + var context = new PerfTraceThreadContext(Thread.CurrentThread.ManagedThreadId); + _threads.Add(context); + return context; + } + + public static IEnumerable GetEvents() + { + return _threads; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceEvent.cs b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceEvent.cs new file mode 100644 index 0000000..bcfb53b --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceEvent.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class PerfTraceEvent + { + public string Type { get; } + public string Instance { get; } + public DateTime StartUtc { get; } + public TimeSpan Duration { get; } + public IList Children { get; } + + public PerfTraceEvent(string type, string instance, IEnumerable children, DateTime startUtc, TimeSpan duration) + { + Type = type; + Instance = instance; + StartUtc = startUtc; + Duration = duration; + Children = children.OrderBy(e => e.StartUtc).ToList(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceOutput.cs b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceOutput.cs new file mode 100644 index 0000000..2212efb --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceOutput.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class PerfTraceOutput + { + private static TimeSpan _minDuration = TimeSpan.FromSeconds(0.001); + + public static void Print(Reporter reporter, IEnumerable contexts) + { + foreach (var threadContext in contexts) + { + Print(reporter, new[] { threadContext.Root }, threadContext.Root, null); + } + } + + private static void Print(Reporter reporter, IEnumerable events, PerfTraceEvent root, PerfTraceEvent parent, int padding = 0) + { + foreach (var e in events) + { + if (e.Duration < _minDuration) + { + continue; + } + reporter.Write(new string(' ', padding)); + reporter.WriteLine(FormatEvent(e, root, parent)); + Print(reporter, e.Children, root, e, padding + 2); + } + } + + private static string FormatEvent(PerfTraceEvent e, PerfTraceEvent root, PerfTraceEvent parent) + { + var builder = new StringBuilder(); + FormatEventTimeStat(builder, e, root, parent); + builder.Append($" {e.Type.Bold()} {e.Instance}"); + return builder.ToString(); + } + + private static void FormatEventTimeStat(StringBuilder builder, PerfTraceEvent e, PerfTraceEvent root, PerfTraceEvent parent) + { + builder.Append("["); + if (root != e) + { + AppendTime(builder, e.Duration.TotalSeconds / root.Duration.TotalSeconds, 0.2); + } + AppendTime(builder, e.Duration.TotalSeconds / parent?.Duration.TotalSeconds, 0.5); + builder.Append($"{e.Duration.ToString("ss\\.fff\\s").Blue()}]"); + } + + private static void AppendTime(StringBuilder builder, double? percent, double treshold) + { + if (percent != null) + { + var formattedPercent = $"{percent*100:00\\.00%}"; + if (percent > treshold) + { + builder.Append(formattedPercent.Red()); + } + else if (percent > treshold / 2) + { + builder.Append(formattedPercent.Yellow()); + } + else if (percent > treshold / 5) + { + builder.Append(formattedPercent.Green()); + } + else + { + builder.Append(formattedPercent); + } + builder.Append(" "); + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceThreadContext.cs b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceThreadContext.cs new file mode 100644 index 0000000..41a42d7 --- /dev/null +++ b/test/Microsoft.DotNet.Cli.Utils/Tracing/PerfTraceThreadContext.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Microsoft.DotNet.Cli.Utils +{ + public class PerfTraceThreadContext + { + private readonly int _threadId; + + private TimerDisposable _activeEvent; + + public PerfTraceEvent Root => _activeEvent.CreateEvent(); + + public PerfTraceThreadContext(int threadId) + { + _activeEvent = new TimerDisposable(this, "Thread", $"{threadId.ToString()}"); + _threadId = threadId; + } + + public IDisposable CaptureTiming(string instance = "", [CallerMemberName] string memberName = "", [CallerFilePath] string filePath = "") + { + if(!PerfTrace.Enabled) + { + return null; + } + + var newTimer = new TimerDisposable(this, $"{Path.GetFileNameWithoutExtension(filePath)}:{memberName}", instance); + var previousTimer = Interlocked.Exchange(ref _activeEvent, newTimer); + newTimer.Parent = previousTimer; + return newTimer; + } + + private void RecordTiming(PerfTraceEvent newEvent, TimerDisposable parent) + { + Interlocked.Exchange(ref _activeEvent, parent); + _activeEvent.Children.Add(newEvent); + } + + private class TimerDisposable : IDisposable + { + private readonly PerfTraceThreadContext _context; + private string _eventType; + private string _instance; + private DateTime _startUtc; + private Stopwatch _stopwatch = Stopwatch.StartNew(); + + public TimerDisposable Parent { get; set; } + + public ConcurrentBag Children { get; set; } = new ConcurrentBag(); + + public TimerDisposable(PerfTraceThreadContext context, string eventType, string instance) + { + _context = context; + _eventType = eventType; + _instance = instance; + _startUtc = DateTime.UtcNow; + } + + public void Dispose() + { + _stopwatch.Stop(); + + _context.RecordTiming(CreateEvent(), Parent); + } + + public PerfTraceEvent CreateEvent() => new PerfTraceEvent(_eventType, _instance, Children, _startUtc, _stopwatch.Elapsed); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs new file mode 100644 index 0000000..5a04d82 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultAssertions.cs @@ -0,0 +1,136 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Text.RegularExpressions; +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public class CommandResultAssertions + { + private CommandResult _commandResult; + + public CommandResultAssertions(CommandResult commandResult) + { + _commandResult = commandResult; + } + + public AndConstraint ExitWith(int expectedExitCode) + { + Execute.Assertion.ForCondition(_commandResult.ExitCode == expectedExitCode) + .FailWith(AppendDiagnosticsTo($"Expected command to exit with {expectedExitCode} but it did not.")); + return new AndConstraint(this); + } + + public AndConstraint Pass() + { + Execute.Assertion.ForCondition(_commandResult.ExitCode == 0) + .FailWith(AppendDiagnosticsTo($"Expected command to pass but it did not.")); + return new AndConstraint(this); + } + + public AndConstraint Fail() + { + Execute.Assertion.ForCondition(_commandResult.ExitCode != 0) + .FailWith(AppendDiagnosticsTo($"Expected command to fail but it did not.")); + return new AndConstraint(this); + } + + public AndConstraint HaveStdOut() + { + Execute.Assertion.ForCondition(!string.IsNullOrEmpty(_commandResult.StdOut)) + .FailWith(AppendDiagnosticsTo("Command did not output anything to stdout")); + return new AndConstraint(this); + } + + public AndConstraint HaveStdOut(string expectedOutput) + { + Execute.Assertion.ForCondition(_commandResult.StdOut.Equals(expectedOutput, StringComparison.Ordinal)) + .FailWith(AppendDiagnosticsTo($"Command did not output with Expected Output. Expected: {expectedOutput}")); + return new AndConstraint(this); + } + + public AndConstraint HaveStdOutContaining(string pattern) + { + Execute.Assertion.ForCondition(_commandResult.StdOut.Contains(pattern)) + .FailWith(AppendDiagnosticsTo($"The command output did not contain expected result: {pattern}{Environment.NewLine}")); + return new AndConstraint(this); + } + + public AndConstraint HaveStdOutMatching(string pattern, RegexOptions options = RegexOptions.None) + { + Execute.Assertion.ForCondition(Regex.Match(_commandResult.StdOut, pattern, options).Success) + .FailWith(AppendDiagnosticsTo($"Matching the command output failed. Pattern: {pattern}{Environment.NewLine}")); + return new AndConstraint(this); + } + + public AndConstraint HaveStdErr() + { + Execute.Assertion.ForCondition(!string.IsNullOrEmpty(_commandResult.StdErr)) + .FailWith(AppendDiagnosticsTo("Command did not output anything to stderr.")); + return new AndConstraint(this); + } + + public AndConstraint HaveStdErrContaining(string pattern) + { + Execute.Assertion.ForCondition(_commandResult.StdErr.Contains(pattern)) + .FailWith(AppendDiagnosticsTo($"The command error output did not contain expected result: {pattern}{Environment.NewLine}")); + return new AndConstraint(this); + } + + public AndConstraint NotHaveStdErrContaining(string pattern) + { + Execute.Assertion.ForCondition(!_commandResult.StdErr.Contains(pattern)) + .FailWith(AppendDiagnosticsTo($"The command error output contained a result it should not have contained: {pattern}{Environment.NewLine}")); + return new AndConstraint(this); + } + + public AndConstraint HaveStdErrMatching(string pattern, RegexOptions options = RegexOptions.None) + { + Execute.Assertion.ForCondition(Regex.Match(_commandResult.StdErr, pattern, options).Success) + .FailWith(AppendDiagnosticsTo($"Matching the command error output failed. Pattern: {pattern}{Environment.NewLine}")); + return new AndConstraint(this); + } + + public AndConstraint NotHaveStdOut() + { + Execute.Assertion.ForCondition(string.IsNullOrEmpty(_commandResult.StdOut)) + .FailWith(AppendDiagnosticsTo($"Expected command to not output to stdout but it was not:")); + return new AndConstraint(this); + } + + public AndConstraint NotHaveStdErr() + { + Execute.Assertion.ForCondition(string.IsNullOrEmpty(_commandResult.StdErr)) + .FailWith(AppendDiagnosticsTo("Expected command to not output to stderr but it was not:")); + return new AndConstraint(this); + } + + private string AppendDiagnosticsTo(string s) + { + return s + $"{Environment.NewLine}" + + $"File Name: {_commandResult.StartInfo.FileName}{Environment.NewLine}" + + $"Arguments: {_commandResult.StartInfo.Arguments}{Environment.NewLine}" + + $"Exit Code: {_commandResult.ExitCode}{Environment.NewLine}" + + $"StdOut:{Environment.NewLine}{_commandResult.StdOut}{Environment.NewLine}" + + $"StdErr:{Environment.NewLine}{_commandResult.StdErr}{Environment.NewLine}"; ; + } + + public AndConstraint HaveSkippedProjectCompilation(string skippedProject, string frameworkFullName) + { + _commandResult.StdOut.Should().Contain($"Project {skippedProject} ({frameworkFullName}) was previously compiled. Skipping compilation."); + + return new AndConstraint(this); + } + + public AndConstraint HaveCompiledProject(string compiledProject, string frameworkFullName) + { + _commandResult.StdOut.Should().Contain($"Project {compiledProject} ({frameworkFullName}) will be compiled"); + + return new AndConstraint(this); + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultExtensions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultExtensions.cs new file mode 100644 index 0000000..ad3bf23 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/CommandResultExtensions.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public static class CommandResultExtensions + { + public static CommandResultAssertions Should(this CommandResult commandResult) + { + return new CommandResultAssertions(commandResult); + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoAssertions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoAssertions.cs new file mode 100644 index 0000000..9a4e834 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoAssertions.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using FluentAssertions; +using FluentAssertions.Execution; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public class DirectoryInfoAssertions + { + private DirectoryInfo _dirInfo; + + public DirectoryInfoAssertions(DirectoryInfo dir) + { + _dirInfo = dir; + } + + public DirectoryInfo DirectoryInfo => _dirInfo; + + public AndConstraint Exist() + { + Execute.Assertion.ForCondition(_dirInfo.Exists) + .FailWith("Expected directory {0} does not exist.", _dirInfo.FullName); + return new AndConstraint(this); + } + + public AndConstraint HaveFile(string expectedFile) + { + var file = _dirInfo.EnumerateFiles(expectedFile, SearchOption.TopDirectoryOnly).SingleOrDefault(); + Execute.Assertion.ForCondition(file != null) + .FailWith("Expected File {0} cannot be found in directory {1}.", expectedFile, _dirInfo.FullName); + return new AndConstraint(this); + } + + public AndConstraint NotHaveFile(string expectedFile) + { + var file = _dirInfo.EnumerateFiles(expectedFile, SearchOption.TopDirectoryOnly).SingleOrDefault(); + Execute.Assertion.ForCondition(file == null) + .FailWith("File {0} should not be found in directory {1}.", expectedFile, _dirInfo.FullName); + return new AndConstraint(this); + } + + public AndConstraint HaveFiles(IEnumerable expectedFiles) + { + foreach (var expectedFile in expectedFiles) + { + HaveFile(expectedFile); + } + + return new AndConstraint(this); + } + + public AndConstraint NotHaveFiles(IEnumerable expectedFiles) + { + foreach (var expectedFile in expectedFiles) + { + NotHaveFile(expectedFile); + } + + return new AndConstraint(this); + } + + public AndConstraint HaveDirectory(string expectedDir) + { + var dir = _dirInfo.EnumerateDirectories(expectedDir, SearchOption.TopDirectoryOnly).SingleOrDefault(); + Execute.Assertion.ForCondition(dir != null) + .FailWith("Expected directory {0} cannot be found inside directory {1}.", expectedDir, _dirInfo.FullName); + + return new AndConstraint(new DirectoryInfoAssertions(dir)); + } + + public AndConstraint OnlyHaveFiles(IEnumerable expectedFiles) + { + var actualFiles = _dirInfo.EnumerateFiles("*", SearchOption.TopDirectoryOnly).Select(f => f.Name); + var missingFiles = Enumerable.Except(expectedFiles, actualFiles); + var extraFiles = Enumerable.Except(actualFiles, expectedFiles); + var nl = Environment.NewLine; + + Execute.Assertion.ForCondition(!missingFiles.Any()) + .FailWith($"Following files cannot be found inside directory {_dirInfo.FullName} {nl} {string.Join(nl, missingFiles)}"); + + Execute.Assertion.ForCondition(!extraFiles.Any()) + .FailWith($"Following extra files are found inside directory {_dirInfo.FullName} {nl} {string.Join(nl, extraFiles)}"); + + return new AndConstraint(this); + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoExtensions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoExtensions.cs new file mode 100644 index 0000000..e71f64b --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Assertions/DirectoryInfoExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public static class DirectoryInfoExtensions + { + public static DirectoryInfoAssertions Should(this DirectoryInfo dir) + { + return new DirectoryInfoAssertions(dir); + } + + public static DirectoryInfo Sub(this DirectoryInfo dir, string name) + { + return new DirectoryInfo(Path.Combine(dir.FullName, name)); + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/RunCommand.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/RunCommand.cs new file mode 100644 index 0000000..95e4341 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/RunCommand.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public sealed class RunCommand : TestCommand + { + private string _projectPath; + private string _framework; + private string _configuration; + private string _appArgs; + + private string ProjectPathOption => string.IsNullOrEmpty(_projectPath) ? "" : $"-p \"{_projectPath}\""; + + private string FrameworkOption => string.IsNullOrEmpty(_framework) ? "" : $"-f {_framework}"; + + private string ConfigurationOption => string.IsNullOrEmpty(_configuration) ? "" : $"-c {_configuration}"; + + private string AppArgsArgument => _appArgs; + + public RunCommand( + string projectPath, + string framework = "", + string configuration = "", + string appArgs = "") + : base("dotnet") + { + _projectPath = projectPath; + _framework = framework; + _configuration = configuration; + _appArgs = appArgs; + } + + public override CommandResult Execute(string args = "") + { + args = $"run {BuildArgs()} {args}"; + return base.Execute(args); + } + + public override CommandResult ExecuteWithCapturedOutput(string args = "") + { + args = $"run {BuildArgs()} {args}"; + return base.ExecuteWithCapturedOutput(args); + } + + public override Task ExecuteAsync(string args = "") + { + args = $"run {BuildArgs()} {args}"; + return base.ExecuteAsync(args); + } + + private string BuildArgs() + { + return string.Join(" ", + new[] + { + ProjectPathOption, + FrameworkOption, + ConfigurationOption, + AppArgsArgument, + } + .Where(s => !string.IsNullOrEmpty(s))); + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/TestCommand.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/TestCommand.cs new file mode 100644 index 0000000..c694fca --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Commands/TestCommand.cs @@ -0,0 +1,204 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public class TestCommand + { + protected string _command; + + private string _baseDirectory; + + public string WorkingDirectory { get; set; } + + public Process CurrentProcess { get; set; } + + public Dictionary Environment { get; } = new Dictionary(); + + public TestCommand(string command) + { + _command = command; +#if NET451 + _baseDirectory = AppDomain.CurrentDomain.BaseDirectory; +#else + _baseDirectory = AppContext.BaseDirectory; +#endif + } + + public TestCommand WithWorkingDirectory(string workingDirectory) + { + WorkingDirectory = workingDirectory; + return this; + } + + public virtual CommandResult Execute(string args = "") + { + var commandPath = _command; + ResolveCommand(ref commandPath, ref args); + + Console.WriteLine($"Executing - {commandPath} {args}"); + + var stdOut = new StreamForwarder(); + var stdErr = new StreamForwarder(); + + stdOut.ForwardTo(writeLine: Reporter.Output.WriteLine); + stdErr.ForwardTo(writeLine: Reporter.Output.WriteLine); + + return RunProcess(commandPath, args, stdOut, stdErr); + } + + public virtual Task ExecuteAsync(string args = "") + { + var commandPath = _command; + ResolveCommand(ref commandPath, ref args); + + Console.WriteLine($"Executing - {commandPath} {args}"); + + var stdOut = new StreamForwarder(); + var stdErr = new StreamForwarder(); + + stdOut.ForwardTo(writeLine: Reporter.Output.WriteLine); + stdErr.ForwardTo(writeLine: Reporter.Output.WriteLine); + + return RunProcessAsync(commandPath, args, stdOut, stdErr); + } + + public virtual CommandResult ExecuteWithCapturedOutput(string args = "") + { + var command = _command; + ResolveCommand(ref command, ref args); + var commandPath = Env.GetCommandPath(command, ".exe", ".cmd", "") ?? + Env.GetCommandPathFromRootPath(_baseDirectory, command, ".exe", ".cmd", ""); + + Console.WriteLine($"Executing (Captured Output) - {commandPath} {args}"); + + var stdOut = new StreamForwarder(); + var stdErr = new StreamForwarder(); + + stdOut.Capture(); + stdErr.Capture(); + + return RunProcess(commandPath, args, stdOut, stdErr); + } + + public void KillTree() + { + if (CurrentProcess == null) + { + throw new InvalidOperationException("No process is available to be killed"); + } + + CurrentProcess.KillTree(); + } + + private void ResolveCommand(ref string executable, ref string args) + { + if (executable.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + var newArgs = ArgumentEscaper.EscapeSingleArg(executable); + if (!string.IsNullOrEmpty(args)) + { + newArgs += " " + args; + } + args = newArgs; + executable = "dotnet"; + } + + if (!Path.IsPathRooted(executable)) + { + executable = Env.GetCommandPath(executable) ?? + Env.GetCommandPathFromRootPath(_baseDirectory, executable); + } + } + + private CommandResult RunProcess(string executable, string args, StreamForwarder stdOut, StreamForwarder stdErr) + { + CurrentProcess = StartProcess(executable, args); + var taskOut = stdOut.BeginRead(CurrentProcess.StandardOutput); + var taskErr = stdErr.BeginRead(CurrentProcess.StandardError); + + CurrentProcess.WaitForExit(); + Task.WaitAll(taskOut, taskErr); + + var result = new CommandResult( + CurrentProcess.StartInfo, + CurrentProcess.ExitCode, + stdOut.CapturedOutput, + stdErr.CapturedOutput); + + return result; + } + + private Task RunProcessAsync(string executable, string args, StreamForwarder stdOut, StreamForwarder stdErr) + { + CurrentProcess = StartProcess(executable, args); + var taskOut = stdOut.BeginRead(CurrentProcess.StandardOutput); + var taskErr = stdErr.BeginRead(CurrentProcess.StandardError); + + var tcs = new TaskCompletionSource(); + CurrentProcess.Exited += (sender, arg) => + { + Task.WaitAll(taskOut, taskErr); + var result = new CommandResult( + CurrentProcess.StartInfo, + CurrentProcess.ExitCode, + stdOut.CapturedOutput, + stdErr.CapturedOutput); + tcs.SetResult(result); + }; + + return tcs.Task; + } + + private Process StartProcess(string executable, string args) + { + var psi = new ProcessStartInfo + { + FileName = executable, + Arguments = args, + RedirectStandardError = true, + RedirectStandardOutput = true, + RedirectStandardInput = true, + UseShellExecute = false + }; + + foreach (var item in Environment) + { +#if NET451 + psi.EnvironmentVariables[item.Key] = item.Value; +#else + psi.Environment[item.Key] = item.Value; +#endif + } + + if (!string.IsNullOrWhiteSpace(WorkingDirectory)) + { + psi.WorkingDirectory = WorkingDirectory; + } + + var process = new Process + { + StartInfo = psi + }; + + process.EnableRaisingEvents = true; + process.Start(); + return process; + } + + public TestCommand WithEnvironmentVariable(string name, string value) + { + Environment.Add(name, value); + + return this; + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Microsoft.DotNet.Tools.Tests.Utilities.csproj b/test/Microsoft.DotNet.Tools.Tests.Utilities/Microsoft.DotNet.Tools.Tests.Utilities.csproj new file mode 100644 index 0000000..5c7eea6 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Microsoft.DotNet.Tools.Tests.Utilities.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp1.0;net451 + + + + + + + + + + + + diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/NetworkHelper.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/NetworkHelper.cs new file mode 100644 index 0000000..75f5447 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/NetworkHelper.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public class NetworkHelper + { + // in milliseconds + private const int Timeout = 50000; + + public static string Localhost { get; } = "http://localhost"; + + public static bool IsServerUp(string url) + { + return SpinWait.SpinUntil(() => + { + using (var client = new HttpClient()) + { + try + { + client.BaseAddress = new Uri(url); + HttpResponseMessage response = client.GetAsync("").Result; + return response.IsSuccessStatusCode; + } + catch (Exception) + { + Thread.Sleep(100); + return false; + } + } + }, Timeout); + } + + public static void TestGetRequest(string url, string expectedResponse) + { + using (var client = new HttpClient()) + { + client.BaseAddress = new Uri(url); + + HttpResponseMessage response = client.GetAsync("").Result; + if (response.IsSuccessStatusCode) + { + var responseString = response.Content.ReadAsStringAsync().Result; + responseString.Should().Contain(expectedResponse); + } + } + } + + public static string GetLocalhostUrlWithFreePort() + { + return $"{Localhost}:{PortManager.GetPort()}/"; + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/PortManager.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/PortManager.cs new file mode 100644 index 0000000..303f89a --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/NetworkUtils/PortManager.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public static class PortManager + { + private static int s_nextPort = 8001; + + public static int GetPort() + { + return Interlocked.Increment(ref s_nextPort); + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/NuGetConfig.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/NuGetConfig.cs new file mode 100644 index 0000000..1cbcf02 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/NuGetConfig.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public static class NuGetConfig + { + public static void Write(string directory) + { + var contents = @" + + + + + + + +"; + + var path = Path.Combine(directory, "NuGet.config"); + + File.WriteAllText(path, contents); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/ProcessExtensions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/ProcessExtensions.cs new file mode 100644 index 0000000..c82fd7a --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/ProcessExtensions.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + internal static class ProcessExtensions + { +#if NET451 + private static readonly bool _isWindows = true; +#else + private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); +#endif + private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30); + + public static void KillTree(this Process process) + { + process.KillTree(_defaultTimeout); + } + + public static void KillTree(this Process process, TimeSpan timeout) + { + string stdout; + if (_isWindows) + { + RunProcessAndWaitForExit( + "taskkill", + $"/T /F /PID {process.Id}", + timeout, + out stdout); + } + else + { + var children = new HashSet(); + GetAllChildIdsUnix(process.Id, children, timeout); + foreach (var childId in children) + { + KillProcessUnix(childId, timeout); + } + KillProcessUnix(process.Id, timeout); + } + } + + private static void GetAllChildIdsUnix(int parentId, ISet children, TimeSpan timeout) + { + string stdout; + var exitCode = RunProcessAndWaitForExit( + "pgrep", + $"-P {parentId}", + timeout, + out stdout); + + if (exitCode == 0 && !string.IsNullOrEmpty(stdout)) + { + using (var reader = new StringReader(stdout)) + { + while (true) + { + var text = reader.ReadLine(); + if (text == null) + { + return; + } + + int id; + if (int.TryParse(text, out id)) + { + children.Add(id); + // Recursively get the children + GetAllChildIdsUnix(id, children, timeout); + } + } + } + } + } + + private static void KillProcessUnix(int processId, TimeSpan timeout) + { + string stdout; + RunProcessAndWaitForExit( + "kill", + $"-TERM {processId}", + timeout, + out stdout); + } + + private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string stdout) + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var process = Process.Start(startInfo); + + stdout = null; + if (process.WaitForExit((int)timeout.TotalMilliseconds)) + { + stdout = process.StandardOutput.ReadToEnd(); + } + else + { + process.Kill(); + } + + return process.ExitCode; + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/ProjectUtils.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/ProjectUtils.cs new file mode 100644 index 0000000..3b812ed --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/ProjectUtils.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Linq; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public class ProjectUtils + { + public static string GetProjectJson(string testRoot, string project) + { + // We assume that the project name same as the directory name with contains the project.json + // We can do better here by using ProjectReader to get the correct project name + string projectPath = Directory.GetFiles(testRoot, "project.json", SearchOption.AllDirectories) + .FirstOrDefault(pj => Directory.GetParent(pj).Name.Equals(project)); + + if (string.IsNullOrEmpty(projectPath)) + { + throw new Exception($"Cannot file project '{project}' in '{testRoot}'"); + } + + return projectPath; + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/Properties/Properties.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/Properties/Properties.cs new file mode 100644 index 0000000..88c50a9 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/Properties/Properties.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.Extensions.DependencyModel.Tests , PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.DotNet.Configurer.UnitTests , PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableDirectory.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableDirectory.cs new file mode 100644 index 0000000..8baa5a1 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableDirectory.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public sealed class DisposableDirectory : TempDirectory, IDisposable + { + public DisposableDirectory(TempRoot root) + : base(root) + { + } + + public void Dispose() + { + if (Path != null && Directory.Exists(Path)) + { + try + { + Directory.Delete(Path, recursive: true); + } + catch + { + } + } + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableFile.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableFile.cs new file mode 100644 index 0000000..c17c1dd --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/DisposableFile.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public sealed class DisposableFile : TempFile, IDisposable + { + public DisposableFile(string path) + : base(path) + { + } + + public DisposableFile(string prefix = null, string extension = null, string directory = null, string callerSourcePath = null, int callerLineNumber = 0) + : base(prefix, extension, directory, callerSourcePath, callerLineNumber) + { + } + + public void Dispose() + { + if (Path != null && File.Exists(Path)) + { + try + { + File.Delete(Path); + } + catch (UnauthorizedAccessException) + { + try + { + // the file might still be memory-mapped, delete on close: + DeleteFileOnClose(Path); + } + catch (IOException ex) + { + throw new InvalidOperationException(string.Format(@" +The file '{0}' seems to have been opened in a way that prevents us from deleting it on close. +Is the file loaded as an assembly (e.g. via Assembly.LoadFile)? + +{1}: {2}", Path, ex.GetType().Name, ex.Message), ex); + } + catch (UnauthorizedAccessException) + { + // We should ignore this exception if we got it the second time, + // the most important reason is that the file has already been + // scheduled for deletion and will be deleted when all handles + // are closed. + } + } + } + } + + [DllImport("kernel32.dll", PreserveSig = false)] + private static extern void SetFileInformationByHandle(SafeFileHandle handle, int fileInformationClass, ref uint fileDispositionInfoDeleteFile, int bufferSize); + + private const int FileDispositionInfo = 4; + + internal static void PrepareDeleteOnCloseStreamForDisposal(FileStream stream) + { + // tomat: Set disposition to "delete" on the stream, so to avoid ForeFront EndPoint + // Protection driver scanning the file. Note that after calling this on a file that's open with DeleteOnClose, + // the file can't be opened again, not even by the same process. + uint trueValue = 1; + SetFileInformationByHandle(stream.SafeFileHandle, FileDispositionInfo, ref trueValue, sizeof(uint)); + } + + /// + /// Marks given file for automatic deletion when all its handles are closed. + /// Note that after doing this the file can't be opened again, not even by the same process. + /// + internal static void DeleteFileOnClose(string fullPath) + { + using (var stream = new FileStream(fullPath, FileMode.Open, FileAccess.ReadWrite, FileShare.Delete | FileShare.ReadWrite, 8, FileOptions.DeleteOnClose)) + { + PrepareDeleteOnCloseStreamForDisposal(stream); + } + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/FileNameUtilities.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/FileNameUtilities.cs new file mode 100644 index 0000000..625370b --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/FileNameUtilities.cs @@ -0,0 +1,183 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + /// + /// Implements a few file name utilities that are needed by the compiler. + /// In general the compiler is not supposed to understand the format of the paths. + /// In rare cases it needs to check if a string is a valid file name or change the extension + /// (embedded resources, netmodules, output name). + /// The APIs are intentionally limited to cover just these rare cases. Do not add more APIs. + /// + internal static class FileNameUtilities + { + private const string DirectorySeparatorStr = "\\"; + internal const char DirectorySeparatorChar = '\\'; + internal const char AltDirectorySeparatorChar = '/'; + internal const char VolumeSeparatorChar = ':'; + + /// + /// Returns true if the string represents an unqualified file name. + /// The name may contain any characters but directory and volume separators. + /// + /// Path. + /// + /// True if is a simple file name, false if it is null or includes a directory specification. + /// + internal static bool IsFileName(string path) + { + return IndexOfFileName(path) == 0; + } + + /// + /// Returns the offset in where the dot that starts an extension is, or -1 if the path doesn't have an extension. + /// + /// + /// Returns 0 for path ".foo". + /// Returns -1 for path "foo.". + /// + private static int IndexOfExtension(string path) + { + if (path == null) + { + return -1; + } + + int length = path.Length; + int i = length; + + while (--i >= 0) + { + char c = path[i]; + if (c == '.') + { + if (i != length - 1) + { + return i; + } + + return -1; + } + + if (c == DirectorySeparatorChar || c == AltDirectorySeparatorChar || c == VolumeSeparatorChar) + { + break; + } + } + + return -1; + } + + /// + /// Returns an extension of the specified path string. + /// + /// + /// The same functionality as but doesn't throw an exception + /// if there are invalid characters in the path. + /// + internal static string GetExtension(string path) + { + if (path == null) + { + return null; + } + + int index = IndexOfExtension(path); + return (index >= 0) ? path.Substring(index) : string.Empty; + } + + /// + /// Removes extension from path. + /// + /// + /// Returns "foo" for path "foo.". + /// Returns "foo.." for path "foo...". + /// + private static string RemoveExtension(string path) + { + if (path == null) + { + return null; + } + + int index = IndexOfExtension(path); + if (index >= 0) + { + return path.Substring(0, index); + } + + // trim last ".", if present + if (path.Length > 0 && path[path.Length - 1] == '.') + { + return path.Substring(0, path.Length - 1); + } + + return path; + } + + /// + /// Returns path with the extension changed to . + /// + /// + /// Equivalent of + /// + /// If is null, returns null. + /// If path does not end with an extension, the new extension is appended to the path. + /// If extension is null, equivalent to . + /// + internal static string ChangeExtension(string path, string extension) + { + if (path == null) + { + return null; + } + + var pathWithoutExtension = RemoveExtension(path); + if (extension == null || path.Length == 0) + { + return pathWithoutExtension; + } + + if (extension.Length == 0 || extension[0] != '.') + { + return pathWithoutExtension + "." + extension; + } + + return pathWithoutExtension + extension; + } + + /// + /// Returns the position in given path where the file name starts. + /// + /// -1 if path is null. + internal static int IndexOfFileName(string path) + { + if (path == null) + { + return -1; + } + + for (int i = path.Length - 1; i >= 0; i--) + { + char ch = path[i]; + if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar) + { + return i + 1; + } + } + + return 0; + } + + /// + /// Get file name from path. + /// + /// Unlike doesn't check for invalid path characters. + internal static string GetFileName(string path) + { + int fileNameStart = IndexOfFileName(path); + return (fileNameStart <= 0) ? path : path.Substring(fileNameStart); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/ImmutableArrayTestExtensions.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/ImmutableArrayTestExtensions.cs new file mode 100644 index 0000000..b3dcb57 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/ImmutableArrayTestExtensions.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + /// + /// The collection of extension methods for the type + /// + public static class ImmutableArrayTestExtensions + { + /// + /// Writes read-only array of bytes to the specified file. + /// + /// Data to write to the file. + /// File path. + internal static void WriteToFile(this ImmutableArray bytes, string path) + { + Debug.Assert(!bytes.IsDefault); + + const int bufferSize = 4096; + using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, bufferSize)) + { + // PERF: Consider using an ObjectPool here + byte[] buffer = new byte[Math.Min(bufferSize, bytes.Length)]; + + int offset = 0; + while (offset < bytes.Length) + { + int length = Math.Min(bufferSize, bytes.Length - offset); + bytes.CopyTo(offset, buffer, 0, length); + fileStream.Write(buffer, 0, length); + offset += length; + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/PathKind.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/PathKind.cs new file mode 100644 index 0000000..0c37a6f --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/PathKind.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + internal enum PathKind + { + /// + /// Null or empty. + /// + Empty, + + /// + /// "file" + /// + Relative, + + /// + /// ".\file" + /// + RelativeToCurrentDirectory, + + /// + /// "..\file" + /// + RelativeToCurrentParent, + + /// + /// "\dir\file" + /// + RelativeToCurrentRoot, + + /// + /// "C:dir\file" + /// + RelativeToDriveDirectory, + + /// + /// "C:\file" or "\\machine" (UNC). + /// + Absolute, + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/PathUtilities.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/PathUtilities.cs new file mode 100644 index 0000000..9b1b7b8 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/PathUtilities.cs @@ -0,0 +1,379 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + // Contains path parsing utilities. + // We need our own because System.IO.Path is insufficient for our purposes + // For example we need to be able to work with invalid paths or paths containing wildcards + internal static class PathUtilities + { + // We consider '/' a directory separator on Unix like systems. + // On Windows both / and \ are equally accepted. + internal static readonly char DirectorySeparatorChar = IsUnixLikePlatform ? '/' : '\\'; + internal static readonly char AltDirectorySeparatorChar = '/'; + internal static readonly string DirectorySeparatorStr = new string(DirectorySeparatorChar, 1); + internal const char VolumeSeparatorChar = ':'; + + private static bool IsUnixLikePlatform + { + get + { + return Path.DirectorySeparatorChar == '/'; + } + } + + internal static bool IsDirectorySeparator(char c) + { + return c == DirectorySeparatorChar || c == AltDirectorySeparatorChar; + } + + internal static string TrimTrailingSeparators(string s) + { + int lastSeparator = s.Length; + while (lastSeparator > 0 && IsDirectorySeparator(s[lastSeparator - 1])) + { + lastSeparator = lastSeparator - 1; + } + + if (lastSeparator != s.Length) + { + s = s.Substring(0, lastSeparator); + } + + return s; + } + + internal static string GetExtension(string path) + { + return FileNameUtilities.GetExtension(path); + } + + internal static string ChangeExtension(string path, string extension) + { + return FileNameUtilities.ChangeExtension(path, extension); + } + + internal static string RemoveExtension(string path) + { + return FileNameUtilities.ChangeExtension(path, extension: null); + } + + internal static string GetFileName(string path) + { + return FileNameUtilities.GetFileName(path); + } + + /// + /// Get directory name from path. + /// + /// + /// Unlike it + /// doesn't check for invalid path characters, + /// doesn't strip any trailing directory separators (TODO: tomat), + /// doesn't recognize UNC structure \\computer-name\share\directory-name\file-name (TODO: tomat). + /// + /// Prefix of path that represents a directory. + internal static string GetDirectoryName(string path) + { + int fileNameStart = FileNameUtilities.IndexOfFileName(path); + if (fileNameStart < 0) + { + return null; + } + + return path.Substring(0, fileNameStart); + } + + internal static PathKind GetPathKind(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return PathKind.Empty; + } + + // "C:\" + // "\\machine" (UNC) + // "/etc" (Unix) + if (IsAbsolute(path)) + { + return PathKind.Absolute; + } + + // "." + // ".." + // ".\" + // "..\" + if (path.Length > 0 && path[0] == '.') + { + if (path.Length == 1 || IsDirectorySeparator(path[1])) + { + return PathKind.RelativeToCurrentDirectory; + } + + if (path[1] == '.') + { + if (path.Length == 2 || IsDirectorySeparator(path[2])) + { + return PathKind.RelativeToCurrentParent; + } + } + } + + if (!IsUnixLikePlatform) + { + // "\" + // "\foo" + if (path.Length >= 1 && IsDirectorySeparator(path[0])) + { + return PathKind.RelativeToCurrentRoot; + } + + // "C:foo" + + if (path.Length >= 2 && path[1] == VolumeSeparatorChar && (path.Length <= 2 || !IsDirectorySeparator(path[2]))) + { + return PathKind.RelativeToDriveDirectory; + } + } + + // "foo.dll" + return PathKind.Relative; + } + + internal static bool IsAbsolute(string path) + { + if (string.IsNullOrEmpty(path)) + { + return false; + } + + if (IsUnixLikePlatform) + { + return path[0] == DirectorySeparatorChar; + } + + // "C:\" + if (IsDriveRootedAbsolutePath(path)) + { + // Including invalid paths (e.g. "*:\") + return true; + } + + // "\\machine\share" + // Including invalid/incomplete UNC paths (e.g. "\\foo") + return path.Length >= 2 && + IsDirectorySeparator(path[0]) && + IsDirectorySeparator(path[1]); + } + + /// + /// Returns true if given path is absolute and starts with a drive specification ("C:\"). + /// + private static bool IsDriveRootedAbsolutePath(string path) + { + Debug.Assert(!IsUnixLikePlatform); + return path.Length >= 3 && path[1] == VolumeSeparatorChar && IsDirectorySeparator(path[2]); + } + + /// + /// Get a prefix of given path which is the root of the path. + /// + /// + /// Root of an absolute path or null if the path isn't absolute or has invalid format (e.g. "\\"). + /// It may or may not end with a directory separator (e.g. "C:\", "C:\foo", "\\machine\share", etc.) . + /// + internal static string GetPathRoot(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return null; + } + + int length = GetPathRootLength(path); + return (length != -1) ? path.Substring(0, length) : null; + } + + private static int GetPathRootLength(string path) + { + Debug.Assert(!string.IsNullOrEmpty(path)); + + if (IsUnixLikePlatform) + { + if (IsDirectorySeparator(path[0])) + { + // "/*" + return 1; + } + } + else + { + // "C:\" + if (IsDriveRootedAbsolutePath(path)) + { + return 3; + } + + if (IsDirectorySeparator(path[0])) + { + // "\\machine\share" + return GetUncPathRootLength(path); + } + } + + return -1; + } + + /// + /// Calculates the length of root of an UNC path. + /// + /// + /// "\\server\share" is root of UNC path "\\server\share\dir1\dir2\file". + /// + private static int GetUncPathRootLength(string path) + { + Debug.Assert(IsDirectorySeparator(path[0])); + + // root: + // [directory-separator]{2,}[^directory-separator]+[directory-separator]+[^directory-separator]+ + + int serverIndex = IndexOfNonDirectorySeparator(path, 1); + if (serverIndex < 2) + { + return -1; + } + + int separator = IndexOfDirectorySeparator(path, serverIndex); + if (separator == -1) + { + return -1; + } + + int shareIndex = IndexOfNonDirectorySeparator(path, separator); + if (shareIndex == -1) + { + return -1; + } + + int rootEnd = IndexOfDirectorySeparator(path, shareIndex); + return rootEnd == -1 ? path.Length : rootEnd; + } + + private static int IndexOfDirectorySeparator(string path, int start) + { + for (int i = start; i < path.Length; i++) + { + if (IsDirectorySeparator(path[i])) + { + return i; + } + } + + return -1; + } + + private static int IndexOfNonDirectorySeparator(string path, int start) + { + for (int i = start; i < path.Length; i++) + { + if (!IsDirectorySeparator(path[i])) + { + return i; + } + } + + return -1; + } + + /// + /// Combines an absolute path with a relative. + /// + /// Absolute root path. + /// Relative path. + /// + /// An absolute combined path, or null if is + /// absolute (e.g. "C:\abc", "\\machine\share\abc"), + /// relative to the current root (e.g. "\abc"), + /// or relative to a drive directory (e.g. "C:abc\def"). + /// + /// + internal static string CombineAbsoluteAndRelativePaths(string root, string relativePath) + { + Debug.Assert(IsAbsolute(root)); + + return CombinePossiblyRelativeAndRelativePaths(root, relativePath); + } + + /// + /// Combine two paths, the first of which may be absolute. + /// + /// First path: absolute, relative, or null. + /// Second path: relative and non-null. + /// null, if is null; a combined path, otherwise. + /// + internal static string CombinePossiblyRelativeAndRelativePaths(string rootOpt, string relativePath) + { + if (string.IsNullOrEmpty(rootOpt)) + { + return null; + } + + switch (GetPathKind(relativePath)) + { + case PathKind.Empty: + return rootOpt; + + case PathKind.Absolute: + case PathKind.RelativeToCurrentRoot: + case PathKind.RelativeToDriveDirectory: + return null; + } + + return CombinePathsUnchecked(rootOpt, relativePath); + } + + internal static string CombinePathsUnchecked(string root, string relativePath) + { + Debug.Assert(!string.IsNullOrEmpty(root)); + + char c = root[root.Length - 1]; + if (!IsDirectorySeparator(c) && c != VolumeSeparatorChar) + { + return root + DirectorySeparatorStr + relativePath; + } + + return root + relativePath; + } + + internal static string RemoveTrailingDirectorySeparator(string path) + { + if (path.Length > 0 && IsDirectorySeparator(path[path.Length - 1])) + { + return path.Substring(0, path.Length - 1); + } + else + { + return path; + } + } + + /// + /// Determines whether an assembly reference is considered an assembly file path or an assembly name. + /// used, for example, on values of /r and #r. + /// + internal static bool IsFilePath(string assemblyDisplayNameOrPath) + { + Debug.Assert(assemblyDisplayNameOrPath != null); + + string extension = FileNameUtilities.GetExtension(assemblyDisplayNameOrPath); + return string.Equals(extension, ".dll", StringComparison.OrdinalIgnoreCase) + || string.Equals(extension, ".exe", StringComparison.OrdinalIgnoreCase) + || assemblyDisplayNameOrPath.IndexOf(DirectorySeparatorChar) != -1 + || assemblyDisplayNameOrPath.IndexOf(AltDirectorySeparatorChar) != -1; + } + } +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/TempDirectory.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/TempDirectory.cs new file mode 100644 index 0000000..0e6e07d --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/TempDirectory.cs @@ -0,0 +1,121 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.IO; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public class TempDirectory + { + private readonly string _path; + private readonly TempRoot _root; + + protected TempDirectory(TempRoot root) + : this(CreateUniqueDirectory(TempRoot.Root), root) + { + } + + private TempDirectory(string path, TempRoot root) + { + Debug.Assert(path != null); + Debug.Assert(root != null); + + _path = path; + _root = root; + } + + private static string CreateUniqueDirectory(string basePath) + { + while (true) + { + string dir = System.IO.Path.Combine(basePath, Guid.NewGuid().ToString()); + try + { + Directory.CreateDirectory(dir); + return dir; + } + catch (IOException) + { + // retry + } + } + } + + public string Path + { + get { return _path; } + } + + public DirectoryInfo DirectoryInfo => new DirectoryInfo(Path); + + /// + /// Creates a file in this directory. + /// + /// File name. + public TempFile CreateFile(string name) + { + string filePath = System.IO.Path.Combine(_path, name); + TempRoot.CreateStream(filePath); + return _root.AddFile(new DisposableFile(filePath)); + } + + /// + /// Creates a file in this directory that is a copy of the specified file. + /// + public TempFile CopyFile(string originalPath) + { + string name = System.IO.Path.GetFileName(originalPath); + string filePath = System.IO.Path.Combine(_path, name); + File.Copy(originalPath, filePath); + return _root.AddFile(new DisposableFile(filePath)); + } + + /// + /// Recursively copy the provided directory into this TempDirectory. + /// Does not handle links. + /// + /// + /// + public TempDirectory CopyDirectory(string sourceDirectory) + { + Debug.Assert(Directory.Exists(sourceDirectory), $"{sourceDirectory} does not exists"); + + var tempCopy = CreateDirectory(new DirectoryInfo(sourceDirectory).Name); + + foreach(var file in Directory.EnumerateFiles(sourceDirectory)) + { + tempCopy.CopyFile(file); + } + + foreach(var directory in Directory.EnumerateDirectories(sourceDirectory)) + { + tempCopy.CopyDirectory(directory); + } + + return tempCopy; + } + + /// + /// Creates a subdirectory in this directory. + /// + /// Directory name or unrooted directory path. + public TempDirectory CreateDirectory(string name) + { + string dirPath = System.IO.Path.Combine(_path, name); + Directory.CreateDirectory(dirPath); + return new TempDirectory(dirPath, _root); + } + + public void SetCurrentDirectory() + { + Directory.SetCurrentDirectory(_path); + } + + public override string ToString() + { + return _path; + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/TempFile.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/TempFile.cs new file mode 100644 index 0000000..5419878 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/TempFile.cs @@ -0,0 +1,118 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Immutable; +using System.IO; +using System.Text; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public class TempFile + { + private readonly string _path; + + internal TempFile(string path) + { + Debug.Assert(PathUtilities.IsAbsolute(path)); + _path = path; + } + + internal TempFile(string prefix, string extension, string directory, string callerSourcePath, int callerLineNumber) + { + while (true) + { + if (prefix == null) + { + prefix = System.IO.Path.GetFileName(callerSourcePath) + "_" + callerLineNumber.ToString() + "_"; + } + + _path = System.IO.Path.Combine(directory ?? TempRoot.Root, prefix + Guid.NewGuid() + (extension ?? ".tmp")); + + try + { + TempRoot.CreateStream(_path); + break; + } + catch (PathTooLongException) + { + throw; + } + catch (DirectoryNotFoundException) + { + throw; + } + catch (IOException) + { + // retry + } + } + } + + public FileStream Open(FileAccess access = FileAccess.ReadWrite) + { + return new FileStream(_path, FileMode.Open, access); + } + + public string Path + { + get { return _path; } + } + + public TempFile WriteAllText(string content, Encoding encoding) + { + File.WriteAllText(_path, content, encoding); + return this; + } + + public TempFile WriteAllText(string content) + { + File.WriteAllText(_path, content); + return this; + } + + public async Task WriteAllTextAsync(string content, Encoding encoding) + { + using (var sw = new StreamWriter(File.Create(_path), encoding)) + { + await sw.WriteAsync(content).ConfigureAwait(false); + } + + return this; + } + + public Task WriteAllTextAsync(string content) + { + return WriteAllTextAsync(content, Encoding.UTF8); + } + + public TempFile WriteAllBytes(byte[] content) + { + File.WriteAllBytes(_path, content); + return this; + } + + public TempFile WriteAllBytes(ImmutableArray content) + { + content.WriteToFile(_path); + return this; + } + + public string ReadAllText() + { + return File.ReadAllText(_path); + } + + public TempFile CopyContentFrom(string path) + { + return WriteAllBytes(File.ReadAllBytes(path)); + } + + public override string ToString() + { + return _path; + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/TempRoot.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/TempRoot.cs new file mode 100644 index 0000000..0cff5cb --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TempFileSystem/TempRoot.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public sealed class TempRoot : IDisposable + { + private static readonly bool DoDispose; + private readonly List _temps = new List(); + public static readonly string Root; + + static TempRoot() + { + var persistedRoot = Environment.GetEnvironmentVariable("TEST_ARTIFACTS"); + + if (string.IsNullOrWhiteSpace(persistedRoot)) + { + Root = Path.Combine(Path.GetTempPath(), "DotnetCLITests"); + DoDispose = true; + } + else + { + Root = persistedRoot; + DoDispose = false; + } + + Directory.CreateDirectory(Root); + } + + public void Dispose() + { + if (!DoDispose || _temps == null) return; + + DisposeAll(_temps); + _temps.Clear(); + } + + private static void DisposeAll(IEnumerable temps) + { + foreach (var temp in temps) + { + try + { + temp?.Dispose(); + } + catch + { + // ignore + } + } + } + + public TempDirectory CreateDirectory() + { + var dir = new DisposableDirectory(this); + _temps.Add(dir); + return dir; + } + + public TempFile CreateFile(string prefix = null, string extension = null, string directory = null, [CallerFilePath]string callerSourcePath = null, [CallerLineNumber]int callerLineNumber = 0) + { + return AddFile(new DisposableFile(prefix, extension, directory, callerSourcePath, callerLineNumber)); + } + + public DisposableFile AddFile(DisposableFile file) + { + _temps.Add(file); + return file; + } + + internal static void CreateStream(string fullPath) + { + using (var file = new FileStream(fullPath, FileMode.CreateNew)) { } + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TestAssets.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TestAssets.cs new file mode 100644 index 0000000..6a444b3 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TestAssets.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.Tools.Test.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; +using FluentAssertions; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public class TestAssets + { + private string testAssetsDir; + + public TestAssets(string rootDir) + { + this.testAssetsDir = Path.Combine(rootDir, "test", "testAssets"); + } + + public void CopyDirTo(string testAssetDirName, string toDir) + { + if (!Directory.Exists(toDir)) + Directory.CreateDirectory(toDir); + + string testAssetDir = Path.Combine(testAssetsDir, testAssetDirName); + + var files = Directory.GetFiles(testAssetDir, "*.*"); + foreach (var f in files) + { + File.Copy(f, Path.Combine(toDir, Path.GetFileName(f))); + } + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/TestBase.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/TestBase.cs new file mode 100644 index 0000000..a032088 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/TestBase.cs @@ -0,0 +1,142 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + + /// + /// Base class for all unit test classes. + /// + public abstract class TestBase : IDisposable + { + protected const string DefaultFramework = "netcoreapp1.0"; + protected const string DefaultLibraryFramework = "netstandard1.5"; + private TempRoot _temp; + private static string s_repoRoot; + + protected static string RepoRoot + { + get + { + if (!string.IsNullOrEmpty(s_repoRoot)) + { + return s_repoRoot; + } + +#if NET451 + string directory = AppDomain.CurrentDomain.BaseDirectory; +#else + string directory = AppContext.BaseDirectory; +#endif + + while (!Directory.Exists(Path.Combine(directory, ".git")) && directory != null) + { + directory = Directory.GetParent(directory).FullName; + } + + if (directory == null) + { + throw new Exception("Cannot find the git repository root"); + } + + s_repoRoot = directory; + return s_repoRoot; + } + } + + protected static TestAssets TestAssets + { + get + { + return new TestAssets(RepoRoot); + } + } + + protected TestBase() + { + } + + public static string GetUniqueName() + { + return Guid.NewGuid().ToString("D"); + } + + public TempRoot Temp + { + get + { + if (_temp == null) + { + _temp = new TempRoot(); + } + + return _temp; + } + } + + public virtual void Dispose() + { + if (_temp != null && !PreserveTemp()) + { + _temp.Dispose(); + } + } + + // Quick-n-dirty way to allow the temp output to be preserved when running tests + private bool PreserveTemp() + { + var val = Environment.GetEnvironmentVariable("DOTNET_TEST_PRESERVE_TEMP"); + return !string.IsNullOrEmpty(val) && ( + string.Equals("true", val, StringComparison.OrdinalIgnoreCase) || + string.Equals("1", val, StringComparison.OrdinalIgnoreCase) || + string.Equals("on", val, StringComparison.OrdinalIgnoreCase)); + } + + // protected CommandResult TestExecutable(string outputDir, + // string executableName, + // string expectedOutput) + // { + // var executablePath = Path.Combine(outputDir, executableName); + // var args = new List(); + + // if (IsPortable(executablePath)) + // { + // args.Add("exec"); + // args.Add(ArgumentEscaper.EscapeSingleArg(executablePath)); + + // var muxer = new Muxer(); + // executablePath = muxer.MuxerPath; + // } + + // var executableCommand = new TestCommand(executablePath); + + // var result = executableCommand.ExecuteWithCapturedOutput(string.Join(" ", args)); + + // if (!string.IsNullOrEmpty(expectedOutput)) + // { + // result.Should().HaveStdOut(expectedOutput); + // } + // result.Should().NotHaveStdErr(); + // result.Should().Pass(); + // return result; + // } + + protected string GetCompilationOutputPath(string outputDir, bool native) + { + var executablePath = outputDir; + if (native) + { + executablePath = Path.Combine(executablePath, "native"); + } + + return executablePath; + } + } +} diff --git a/test/Microsoft.DotNet.Tools.Tests.Utilities/WindowsOnlyFactAttribute.cs b/test/Microsoft.DotNet.Tools.Tests.Utilities/WindowsOnlyFactAttribute.cs new file mode 100644 index 0000000..1463168 --- /dev/null +++ b/test/Microsoft.DotNet.Tools.Tests.Utilities/WindowsOnlyFactAttribute.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.DotNet.InternalAbstractions; +using Xunit; + +namespace Microsoft.DotNet.Tools.Test.Utilities +{ + public class WindowsOnlyFactAttribute : FactAttribute + { + public WindowsOnlyFactAttribute() + { + if (RuntimeEnvironment.OperatingSystemPlatform != Platform.Windows) + { + this.Skip = "This test requires windows to run"; + } + } + } + + public class WindowsOnlyTheoryAttribute : TheoryAttribute + { + public WindowsOnlyTheoryAttribute() + { + if (RuntimeEnvironment.OperatingSystemPlatform != Platform.Windows) + { + this.Skip = "This test requires windows to run"; + } + } + } +} \ No newline at end of file diff --git a/test/TestApp/Program.fs b/test/TestApp/Program.fs deleted file mode 100644 index 7375d5c..0000000 --- a/test/TestApp/Program.fs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace TestApp - -open System -open System.Diagnostics - -module Program = - - open TestLibrary - - [] - let Main (args: string array) = - printfn "%s" (TestLibrary.Helper.GetMessage()) - 0 diff --git a/test/TestApp/project.json b/test/TestApp/project.json deleted file mode 100644 index fd03d36..0000000 --- a/test/TestApp/project.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "version": "1.0.0-*", - "buildOptions": { - "emitEntryPoint": true, - "debugType": "portable", - "compilerName": "fsc", - "compile": { - "includeFiles": [ - "Program.fs" - ] - } - }, - "dependencies": { - "TestLibrary": { - "version": "1.0.0-*", - "target": "project" - }, - "Microsoft.NETCore.App": "1.0.0", - "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-*" - }, - "frameworks": { - "netcoreapp1.0": { - "imports": "dnxcore50" - } - }, - "tools": { - "dotnet-compile-fsc": { - "version": "1.0.0-preview2-*", - "imports": "dnxcore50" - } - }, - "runtimes": { - "win7-x64": {}, - "win7-x86": {}, - "osx.10.10-x64": {}, - "osx.10.11-x64": {}, - "ubuntu.14.04-x64": {}, - "ubuntu.16.04-x64": {}, - "centos.7-x64": {}, - "rhel.7.2-x64": {}, - "debian.8-x64": {}, - "fedora.23-x64": {}, - "opensuse.13.2-x64": {} - } -} diff --git a/test/TestAppWithArgs/project.json b/test/TestAppWithArgs/project.json deleted file mode 100644 index 4f28874..0000000 --- a/test/TestAppWithArgs/project.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "version": "1.0.0-*", - "buildOptions": { - "emitEntryPoint": true, - "debugType": "portable", - "compilerName": "fsc", - "compile": { - "includeFiles": [ - "Program.fs" - ] - } - }, - "dependencies": { - "Microsoft.FSharp.Core.netcore": "1.0.0-alpha-*", - "Microsoft.NETCore.App": "1.0.0" - }, - "tools": { - "dotnet-compile-fsc": { - "version": "1.0.0-preview2-*", - "imports": "dnxcore50" - } - }, - "frameworks": { - "netcoreapp1.0": { - "imports": "dnxcore50" - } - }, - "runtimes": { - "win7-x64": {}, - "win7-x86": {}, - "osx.10.10-x64": {}, - "osx.10.11-x64": {}, - "ubuntu.14.04-x64": {}, - "ubuntu.16.04-x64": {}, - "centos.7-x64": {}, - "rhel.7.2-x64": {}, - "debian.8-x64": {}, - "fedora.23-x64": {}, - "opensuse.13.2-x64": {} - } -} diff --git a/test/TestLibrary/Helper.fs b/test/TestLibrary/Helper.fs deleted file mode 100644 index a29f6ac..0000000 --- a/test/TestLibrary/Helper.fs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace TestLibrary - -open Lib - -type Helper() = - - static member GetMessage () = Lib.message () - - static member SayHi () = Lib.sayHi () diff --git a/test/TestLibrary/Helper2.fs b/test/TestLibrary/Helper2.fs deleted file mode 100644 index 6d374f7..0000000 --- a/test/TestLibrary/Helper2.fs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -module Lib - -open System - -let message () = - "This string came from the test library!" - -let sayHi () = - Console.WriteLine("Hello there!") diff --git a/test/dotnet-new.Tests/CommonScenarioTests.cs b/test/dotnet-new.Tests/CommonScenarioTests.cs new file mode 100644 index 0000000..45fe72f --- /dev/null +++ b/test/dotnet-new.Tests/CommonScenarioTests.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.Tools.Test.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; +using FluentAssertions; +using static System.Environment; + +namespace NetcoreCliFsc.DotNet.Tests +{ + public class CommonScenario : TestBase + { + private static IEnumerable NugetConfigSources + { + get + { + yield return "https://api.nuget.org/v3/index.json"; + var pkgsDir = Path.Combine(RepoRoot, "test", "packagesToTest"); + if (Directory.Exists(pkgsDir)) + yield return pkgsDir; + } + } + + private static string NugetPackagesDir + { + get { return Path.Combine(RepoRoot, "test", "packages"); } + } + + private static string RestoreSourcesArgs(IEnumerable sources) + { + return string.Join(" ", sources.Select(x => $"--source \"{x}\"")); + } + + private static string RestoreProps() + { + var props = new Dictionary() + { + { "FSharpNETSdkVersion", GetEnvironmentVariable("TEST_SUITE_FSHARP_NET_SDK_PKG_VERSION")}, + { "MicrosoftFSharpCorenetcoreVersion", GetEnvironmentVariable("TEST_SUITE_MS_FSHARP_CORE_PKG_VERSION")}, + }; + + return string.Join(" ", props.Where(kv => kv.Value != null).Select(kv => $"/p:{kv.Key}={kv.Value}") ); + } + + private static string RestoreDefaultArgs + { + get { return $"--no-cache {LogArgs} --packages \"{NugetPackagesDir}\""; } + } + + private static string LogArgs => "-v n"; + + [Fact] + public void TestAppWithArgs() + { + var rootPath = Temp.CreateDirectory().Path; + + TestAssets.CopyDirTo("TestAppWithArgs", rootPath); + TestAssets.CopyDirTo("TestSuiteProps", rootPath); + + Func test = name => new TestCommand(name) { WorkingDirectory = rootPath }; + + test("dotnet") + .Execute($"restore {RestoreDefaultArgs} {RestoreSourcesArgs(NugetConfigSources)} {RestoreProps()}") + .Should().Pass(); + + test("dotnet") + .Execute($"build {LogArgs}") + .Should().Pass(); + + test("dotnet") + .Execute($"run {LogArgs}") + .Should().Pass(); + } + + [Fact] + public void TestLibrary() + { + var rootPath = Temp.CreateDirectory().Path; + + TestAssets.CopyDirTo("TestLibrary", rootPath); + TestAssets.CopyDirTo("TestSuiteProps", rootPath); + + Func test = name => new TestCommand(name) { WorkingDirectory = rootPath }; + + test("dotnet") + .Execute($"restore {RestoreDefaultArgs} {RestoreSourcesArgs(NugetConfigSources)} {RestoreProps()}") + .Should().Pass(); + + test("dotnet") + .Execute($"build {LogArgs}") + .Should().Pass(); + } + + [Fact] + public void TestApp() + { + var rootPath = Temp.CreateDirectory().Path; + + foreach (var a in new[] { "TestLibrary", "TestApp" }) + { + var projDir = Path.Combine(rootPath, a); + TestAssets.CopyDirTo(a, projDir); + TestAssets.CopyDirTo("TestSuiteProps", projDir); + } + + var appDir = Path.Combine(rootPath, "TestApp"); + + Func test = name => new TestCommand(name) { WorkingDirectory = appDir }; + + test("dotnet") + .Execute($"restore {RestoreDefaultArgs} {RestoreSourcesArgs(NugetConfigSources)} {RestoreProps()}") + .Should().Pass(); + + test("dotnet") + .Execute($"build {LogArgs}") + .Should().Pass(); + + test("dotnet") + .Execute($"run {LogArgs}") + .Should().Pass(); + } + + [Fact] + public void TestPathWithBlank() + { + var rootPath = Path.Combine(Temp.CreateDirectory().Path, "path with blank"); + + TestAssets.CopyDirTo("TestLibrary", rootPath); + TestAssets.CopyDirTo("TestSuiteProps", rootPath); + + Func test = name => new TestCommand(name) { WorkingDirectory = rootPath }; + + test("dotnet") + .Execute($"restore {RestoreDefaultArgs} {RestoreSourcesArgs(NugetConfigSources)} {RestoreProps()}") + .Should().Pass(); + + test("dotnet") + .Execute($"build {LogArgs}") + .Should().Pass(); + } + + private string GetCurrentRID() + { + var rootPath = Temp.CreateDirectory().Path; + + Func test = n => new TestCommand(n) { WorkingDirectory = rootPath }; + + var result = test("dotnet").ExecuteWithCapturedOutput($"--info"); + + result.Should().Pass(); + + var dotnetInfo = result.StdOut; + + string rid = + dotnetInfo + .Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => s.StartsWith("RID:")) + .Select(s => s.Replace("RID:", "").Trim()) + .FirstOrDefault(); + + return rid; + } + + private void CreateNoopExe(string intoDir, string name, bool fail = false) + { + var rootPath = Temp.CreateDirectory().Path; + + TestAssets.CopyDirTo("Noop", rootPath); + + Func test = n => new TestCommand(n) { WorkingDirectory = rootPath }; + + string rid = GetCurrentRID(); + string msbuildArgs = $"/p:AssemblyName={name} " + (fail? "/p:Fail=true" : ""); + + test("dotnet") + .Execute($"restore -r {rid} {RestoreDefaultArgs} {RestoreSourcesArgs(NugetConfigSources)} {RestoreProps()} {msbuildArgs}") + .Should().Pass(); + + test("dotnet") + .Execute($"publish -r {rid} -o \"{intoDir}\" {msbuildArgs}") + .Should().Pass(); + } + + [Fact] + public void DifferentDotnetInPATH() + { + var rootPath = Temp.CreateDirectory().Path; + + var fakeDotnetDir = Path.Combine(rootPath, "dotnetsdk"); + + Directory.CreateDirectory(fakeDotnetDir); + CreateNoopExe(fakeDotnetDir, "dotnet", fail : true); + + var appDir = Path.Combine(rootPath, "TestApp"); + + TestAssets.CopyDirTo("TestLibrary", appDir); + TestAssets.CopyDirTo("TestSuiteProps", appDir); + + Func test = name => new TestCommand(name) { WorkingDirectory = appDir }; + + test("dotnet") + .Execute($"restore {RestoreDefaultArgs} {RestoreSourcesArgs(NugetConfigSources)} {RestoreProps()}") + .Should().Pass(); + + var dotnetPath = Microsoft.DotNet.Cli.Utils.Env.GetCommandPath("dotnet"); + + var newPATHEnvVar = fakeDotnetDir + Path.PathSeparator + GetEnvironmentVariable("PATH"); + + test(dotnetPath) + .WithEnvironmentVariable("PATH", newPATHEnvVar) + .Execute($"build {LogArgs}") + .Should().Pass(); + } + } +} diff --git a/test/dotnet-new.Tests/GivenThatIWantANewFSharp.cs b/test/dotnet-new.Tests/GivenThatIWantANewFSharp.cs new file mode 100644 index 0000000..5e37a70 --- /dev/null +++ b/test/dotnet-new.Tests/GivenThatIWantANewFSharp.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.DotNet.Tools.Test.Utilities; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; +using FluentAssertions; + +namespace NetcoreCliFsc.DotNet.Tests +{ + public class GivenThatIWantANewFSApp : TestBase + { + private static string NugetConfigWithDevFeedPath + { + get { return Path.Combine(RepoRoot, "NuGet.withDevFeed.Config"); } + } + + [Theory] + [InlineData("console")] + [InlineData("lib")] + public void When_dotnet_build_is_invoked_Then_project_builds_without_warnings(string type) + { + var rootPath = Temp.CreateDirectory().Path; + + new TestCommand("dotnet") { WorkingDirectory = rootPath } + .Execute($"new --lang fsharp --type {type}") + .Should().Pass(); + + new TestCommand("dotnet") { WorkingDirectory = rootPath } + .Execute($"restore --no-cache -v n --configfile \"{NugetConfigWithDevFeedPath}\"") + .Should().Pass(); + + var buildResult = new TestCommand("dotnet") { WorkingDirectory = rootPath } + .Execute("build -v n") + .Should().Pass(); + + if (type == "console") + { + var runResult = new TestCommand("dotnet") { WorkingDirectory = rootPath } + .Execute("run -v n") + .Should().Pass(); + } + } + + /* + [Fact] + public void When_NewtonsoftJson_dependency_added_Then_project_restores_and_runs() + { + var rootPath = Temp.CreateDirectory().Path; + var projectJsonFile = Path.Combine(rootPath, "project.json"); + + new TestCommand("dotnet") { WorkingDirectory = rootPath } + .Execute("new --lang fsharp"); + + GivenThatIWantANewCSApp.AddProjectJsonDependency(projectJsonFile, "Newtonsoft.Json", "7.0.1"); + + new TestCommand("dotnet") { WorkingDirectory = rootPath } + .Execute("restore") + .Should().Pass(); + + new TestCommand("dotnet") { WorkingDirectory = rootPath } + .Execute("run") + .Should().Pass(); + } + */ + } +} diff --git a/test/dotnet-new.Tests/dotnet-new.Tests.csproj b/test/dotnet-new.Tests/dotnet-new.Tests.csproj new file mode 100644 index 0000000..0ebb601 --- /dev/null +++ b/test/dotnet-new.Tests/dotnet-new.Tests.csproj @@ -0,0 +1,20 @@ + + + + Exe + netcoreapp1.0 + + + + + + + + + + + + + + + diff --git a/test/testAssets/CompileFailApp/CompileFailApp.fsproj b/test/testAssets/CompileFailApp/CompileFailApp.fsproj new file mode 100644 index 0000000..05bcc51 --- /dev/null +++ b/test/testAssets/CompileFailApp/CompileFailApp.fsproj @@ -0,0 +1,26 @@ + + + + Exe + netcoreapp1.0 + + + + + + + + + + + + + + + + + 1.0.0-preview2-020000 + + + + diff --git a/test/CompileFailApp/Program.fs b/test/testAssets/CompileFailApp/Program.fs similarity index 100% rename from test/CompileFailApp/Program.fs rename to test/testAssets/CompileFailApp/Program.fs diff --git a/test/testAssets/Noop/Program.cs b/test/testAssets/Noop/Program.cs new file mode 100644 index 0000000..0cb0164 --- /dev/null +++ b/test/testAssets/Noop/Program.cs @@ -0,0 +1,16 @@ +using System; + +namespace ConsoleApp +{ + class Program + { + static int Main(string[] args) + { +#if FAIL + return 1; +#else + return 0; +#endif + } + } +} diff --git a/test/testAssets/Noop/noop.csproj b/test/testAssets/Noop/noop.csproj new file mode 100644 index 0000000..e0c4cb8 --- /dev/null +++ b/test/testAssets/Noop/noop.csproj @@ -0,0 +1,19 @@ + + + + Exe + netcoreapp1.0 + + FAIL;@(DefineConstants) + + + + netstandard1.6 + + + + + + + + diff --git a/test-msbuild/TestApp/Program.fs b/test/testAssets/TestApp/Program.fs similarity index 100% rename from test-msbuild/TestApp/Program.fs rename to test/testAssets/TestApp/Program.fs diff --git a/test/testAssets/TestApp/TestApp.fsproj b/test/testAssets/TestApp/TestApp.fsproj new file mode 100644 index 0000000..e17354f --- /dev/null +++ b/test/testAssets/TestApp/TestApp.fsproj @@ -0,0 +1,27 @@ + + + + Exe + netcoreapp1.0 + + + + + + + + + + + + + + + + + + 1.0.0-preview2-020000 + + + + diff --git a/test/TestAppWithArgs/Program.fs b/test/testAssets/TestAppWithArgs/Program.fs similarity index 100% rename from test/TestAppWithArgs/Program.fs rename to test/testAssets/TestAppWithArgs/Program.fs diff --git a/test/testAssets/TestAppWithArgs/TestAppWithArgs.fsproj b/test/testAssets/TestAppWithArgs/TestAppWithArgs.fsproj new file mode 100644 index 0000000..a140562 --- /dev/null +++ b/test/testAssets/TestAppWithArgs/TestAppWithArgs.fsproj @@ -0,0 +1,26 @@ + + + + Exe + netcoreapp1.0 + + + + + + + + + + + + + + + + + 1.0.0-preview2-020000 + + + + diff --git a/test-msbuild/TestLibrary/Helper.fs b/test/testAssets/TestLibrary/Helper.fs similarity index 100% rename from test-msbuild/TestLibrary/Helper.fs rename to test/testAssets/TestLibrary/Helper.fs diff --git a/test-msbuild/TestLibrary/Helper2.fs b/test/testAssets/TestLibrary/Helper2.fs similarity index 100% rename from test-msbuild/TestLibrary/Helper2.fs rename to test/testAssets/TestLibrary/Helper2.fs diff --git a/test/testAssets/TestLibrary/TestLibrary.fsproj b/test/testAssets/TestLibrary/TestLibrary.fsproj new file mode 100644 index 0000000..c140ca1 --- /dev/null +++ b/test/testAssets/TestLibrary/TestLibrary.fsproj @@ -0,0 +1,26 @@ + + + + netstandard1.6 + + + + + + + + + + + + + + + + + + 1.0.0-preview2-020000 + + + + diff --git a/test/testAssets/TestSuiteProps/TestSuite.props b/test/testAssets/TestSuiteProps/TestSuite.props new file mode 100644 index 0000000..ea9919f --- /dev/null +++ b/test/testAssets/TestSuiteProps/TestSuite.props @@ -0,0 +1,12 @@ + + + + 1.0.0-beta-* + + + + 1.0.0-alpha-161023 + + + +