Launching Visual Studio Solutions
Starting off simple, I created a function to start the default process associated with the first file matching a given pattern. This function has nothing to do with Visual Studio other than that I was generally using it against .sln files.
function Invoke-First ($pattern) { @(Get-ChildItem $pattern)[0] | ForEach-Object {start-process $_.FullName} }
Often I’m in a directory and know it has only one solution so I run Invoke-First *.sln
, or if there are multiple solutions, something like Invoke-First *.UWP.sln
. It’s on me to know that if the pattern matches multiple files, only the first match will be launched. I could check the length to handle 0 or more than 1 match but the function name implies the expectation.
Building the Solution (MSBuild)
Sometimes after pulling the latest code I just want to build the app and run it without going through Visual Studio.
Getting MSBuild Path
First I wanted the MSBuild path so I used Get-VsSetupInstance
from the VSSetup PowerShell module to resolve the base Visual Studio install path. I then borrowed some techniques from the Invoke-MSBuild module (which didn’t quite work for me as is) to further resolve the MSBuild path from there.
function Get-MSBuildPath ([switch] $amd64) { $vsInstallPath = (Get-VsSetupInstance).InstallationPath $msBuild32Search = (Join-Path $vsInstallPath "MsBuild\*\Bin\MsBuild.exe") $msBuild64Search = (Join-Path "$vsInstallPath" "MsBuild\*\Bin\amd64\MsBuild.exe") $msBuildPaths = Get-Item -Path $msBuild32Search, $msBuild64Search if (($msBuildPaths -eq $null) -or ($msBuildPaths.Length -eq 0)) { # slow fallback $msBuildPaths = Get-ChildItem -Path $vsInstallPath -Recurse | Where-Object { $_.Name -ieq 'MsBuild.exe' } } $msBuildPathsSorted = $msBuildPaths | Sort-Object -Property FullName -Descending $msBuildPath = $null if ($amd64) { $msBuildPath = $msBuildPathsSorted | Where-Object { $_.Directory.Name -ieq 'amd64' } | Select-Object -ExpandProperty FullName -First 1 } else { $msBuildPath = $msBuildPathsSorted | Where-Object { $_.Directory.Name -ine 'amd64' } | Select-Object -ExpandProperty FullName -First 1 } $msBuildPath }
Running a MSBuild Task
With the MSBuild path resolved I can invoke MSBuild. First file matches are resolved to the solution to use with MSBuild, so I can be lazier with something like Invoke-MSBuild *.UWP.sln
. Unlike with Invoke-First
above, a single match to the pattern is ensured. Next, MSBuild is invoked with a restore target by default, as building from the command line won’t automatically restore NuGet packages as it would from Visual Studio. Finally, MSBuild is launched with the specified target; generally that’s Rebuild for me, so that’s the default, but often I’d use Publish or Clean as well.
function Invoke-MSBuild ($solution, $target = "Rebuild", [switch] $amd64, [switch]$noRestore) { # allow for wildcard matching on $solution $solutionMatches = @(Get-ChildItem $solution -ErrorAction SilentlyContinue) if ($solutionMatches.Length -ne 1) { Write-Warning "Expected 1 solution match but found $($solutionMatches.Length). Refine solution path." return } $sln = $solutionMatches[0] $msBuildPath = Get-MSBuildPath -amd64:$amd64 # nuget packages may need to be restored; have to explictly indicate from cmd line if (!$noRestore) { # "/t:restore" is NuGet 4.0+ and MSBuild 15.1+ # This still may fail for .net standard libs: https://github.com/NuGet/Home/issues/4532 "Restoring packages for $($sln.FullName)" &$msBuildPath $sln.FullName /t:restore } "Running target $($target) for $($sln.FullName)" &$msBuildPath $sln.FullName /t:$target }
Running Tests
When Visual Studio isn’t open I’ll often run tests from PowerShell but even when it’s up I sometimes find it more convenient to run the tests in PowerShell. GUI test runners have their quirks. Test Explorer often has issues discovering tests and I find the interface limited. ReSharper’s test runner and windows I find much better but sometimes it too has issues with discovery and I’ve had some weird ReSharper Test Session Window issues.
For this app I created a Tools/RunTests.ps1 file that can execute tests a couple of ways. One mode runs tests against existing test binaries directly with the xunit’s console runner. The other goes through our Cake (C# Make) script, first building the app and then executing tests. The top of the script:
param( # configuration - i.e. Debug or Release [string] $config = "Debug", # run mode - what type of tests to run [ValidateSet("all", "unit", "integration")] [string] $runMode = "all", # if set, tests are run via Cake process which will first build app [switch]$build ) $projectName = "MyApp" # ...
At the end of the script a driver function executes the tests differently based on the $build
parameter.
function Invoke-Tests { if (!$build) { Invoke-ConsoleRunner } else { Invoke-CakeBuild } } Invoke-Tests
Running Tests Via Console Runner
The console runner function first resolves paths to the runner and test assemblies. When invoking the runner it passes different assemblies according to whether the desire is to run unit tests, integration tests, or both. Passing -verbose
outputs the test names as they are being executed. Use of -html
creates a temporary HTML output file should I want to see more detailed, expandable, formatted test results with execution times and other details.
function Invoke-ConsoleRunner { $runner = Join-Path (Get-ConsoleRunnerPath) "xunit.console.exe" $unitTestDll = Get-TestDll "unit" $integrationTestDll = Get-TestDll "integration" $htmlOut = "$env:Temp\$projectName.Tests.html" $testDlls = @() if ($runMode -eq "unit" -or $runMode -eq "all") { $testDlls += $unitTestDll } if ($runMode -eq "integration" -or $runMode -eq "all") { $testDlls += $integrationTestDll } & $runner $testDlls -nologo -noshadow -verbose -html $htmlOut $htmlOut }
The helper functions below resolve the runner (without specifying a NuGet package version) and the test dll filename relative to the script location. For this app there are two test assemblies, $projectName
.Tests.Unit.dll and $projectName
.Tests.Integration.dll.
function Get-ConsoleRunnerPath { $pattern = "$PSScriptRoot\..\packages\xunit.runner.console.*\tools" $items = Get-ChildItem $pattern | Sort-Object -Property FullName -Descending if (($items -eq $null) -or ($items.Length -eq 0)) { Write-Error "Failed to find '$pattern'; NuGet packages may need to be restored." return } $items[0].FullName } function Get-TestDll ($type) { $unitTestDll = Join-Path $PSScriptRoot "..\$projectName.Tests.$type\bin\$config\$projectName.Tests.$type.dll" $unitTestDll }
Running Tests Via Cake Build Script
A Cake directory of the repo has the Cake defaults of build.cake as the build script and build.ps1 as the script to execute it (see Cake’s Getting Started). Even when passing a full cake filename to build.ps1 the script seemed to assume the current directory was that of the script, hence the Push-Location
before invoking it and Pop-Location
afterwards.
function Invoke-CakeBuild { Push-Location "$PSScriptRoot\..\Cake" $target = "all-tests" if ($runMode -eq "unit") { $target = "unit-tests" } elseif ($runMode -eq "integration") { $target = "integration-tests" } .\build.ps1 -Configuration $config -Target $target Pop-Location # revert changes build made to UWP appxmanifest file. git checkout -- *Package.appxmanifest }
The relevant bits of the Cake script follow.
#tool "nuget:?package=xunit.runner.console" var _buildConfiguration = Argument("build_configuration", "Debug"); const string TEST_DLL_FORMAT = "../MyApp.Tests.{0}/bin/{1}/MyApp.Tests.{0}.dll"; // ... Task("unit-tests") .IsDependentOn("build") .Does(() => { XUnit2(string.Format(TEST_DLL_FORMAT, "Unit", _buildConfiguration)); }); Task("integration-tests") .IsDependentOn("build") .Does(() => { XUnit2(string.Format(TEST_DLL_FORMAT, "Integration", _buildConfiguration)); }); Task("all-tests") .IsDependentOn("unit-tests") .IsDependentOn("integration-tests") .Does(() => { Information("All tests run"); });
Partial sample output of Tools/RunTests.ps1 -build
…
Time Elapsed 00:01:51.18 Finished executing task: build ======================================== unit-tests ======================================== Executing task: unit-tests xUnit.net Console Runner (64-bit .NET 4.0.30319.42000) Discovering: MyApp.Tests.Unit Discovered: MyApp.Tests.Unit Starting: MyApp.Tests.Unit Finished: MyApp.Tests.Unit === TEST EXECUTION SUMMARY === MyApp.Tests.Unit Total: 116, Errors: 0, Failed: 0, Skipped: 0, Time: 16.918s Finished executing task: unit-tests ======================================== integration-tests ======================================== Executing task: integration-tests xUnit.net Console Runner (64-bit .NET 4.0.30319.42000) Discovering: MyApp.Tests.Integration Discovered: MyApp.Tests.Integration Starting: MyApp.Tests.Integration Finished: MyApp.Tests.Integration === TEST EXECUTION SUMMARY === MyApp.Tests.Integration Total: 44, Errors: 0, Failed: 0, Skipped: 0, Time: 97.553s Finished executing task: integration-tests ======================================== all-tests ======================================== Executing task: all-tests All tests run Finished executing task: all-tests Task Duration -------------------------------------------------- clean 00:00:01.4274669 restore-nuget-packages 00:00:36.8457843 gitInfo 00:00:00.2807391 version 00:00:00.1500806 build 00:01:51.8009839 unit-tests 00:00:19.5155430 integration-tests 00:01:40.6757714 all-tests 00:00:00.0051116 -------------------------------------------------- Total: 00:04:30.7014808
In hindsight it would have probably been better to have the all-tests target just repeat the steps of unit-tests and integration-tests, considering the small amount of duplication and how it makes it look like all-tests happened instantaneously. I could’ve also done away with direct interaction with MSBuild altogether and just gone through Cake, though our Cake script does a bit more than necessary for local dev builds.
Cleaning Up Binary Files
After creating a lot of binary files through builds and NuGet restores I sometimes need to clean them up. The below function recursively deletes bin, obj, and packages folders. There’s some custom error handling because PowerShell seems to still trip up on itself sporadically when deleting files recursively. Google searches will show a lot of posts showing problems doing this such as this StackOverflow post or this one. Supposedly there was some discussion on the PowerShell Core GitHub about handling this better in the new and improved cross platform PowerShell.
function Remove-Bin { Get-ChildItem .\ -Include bin,obj,packages -Recurse | ForEach-Object ($_) { "Deleting $($_.FullName)" Remove-Item $_.FullName -Force -Recurse -ErrorAction SilentlyContinue -ErrorVariable e if ($e) { Write-Warning $e.Exception.Message } } }
C:\projects\AutoMapper [master ≡]> Remove-Bin
Deleting C:\projects\AutoMapper\packages
Deleting C:\projects\AutoMapper\src\AutoMapper\bin
Deleting C:\projects\AutoMapper\src\AutoMapper\obj
Deleting C:\projects\AutoMapper\src\AutoMapperSamples\bin
Deleting C:\projects\AutoMapper\src\AutoMapperSamples\obj
Deleting C:\projects\AutoMapper\src\AutoMapperSamples.EF\bin
Deleting C:\projects\AutoMapper\src\AutoMapperSamples.EF\obj
Deleting C:\projects\AutoMapper\src\AutoMapperSamples.OData\bin
Deleting C:\projects\AutoMapper\src\AutoMapperSamples.OData\obj
Deleting C:\projects\AutoMapper\src\Benchmark\bin
Deleting C:\projects\AutoMapper\src\Benchmark\obj
Deleting C:\projects\AutoMapper\src\IntegrationTests\bin
Deleting C:\projects\AutoMapper\src\IntegrationTests\obj
Deleting C:\projects\AutoMapper\src\UnitTests\bin
Deleting C:\projects\AutoMapper\src\UnitTests\obj