function Start-WebTests { <# .SYNOPSIS Runs web tests on web-tier servers only. .DESCRIPTION Site must be available in IIS. Keeps track of glogal pass/fail and returns a bool .PARAMETER LoginsJson Json file containing logins to test with. Pulled from Get-LoginsToTest function. .PARAMETER CoreUrlsJson Json file containing urls to test with. Pulled from Get-CoreUrlsToTest function. .PARAMETER SitesToSkip A list (array) of sites to skip web tests on. Http/https will be trimmed #> [CmdletBinding()] [OutputType([System.Boolean])] param( [Parameter(Mandatory)] [string]$LoginsJson, [Parameter(Mandatory)] [string]$CoreUrlsJson, [Parameter(Mandatory = $false)] [bool]$SkipAdmin = $false, [Parameter(Mandatory = $false)] [bool]$SkipAccountCount = $true, [Parameter(Mandatory = $false)] [bool]$SkipWidgetWarmUp = $true, [Parameter(Mandatory = $false)] [int]$MaximumSitesToTest = 20, [Parameter(Mandatory = $false)] [string[]]$SitesToSkip, [Parameter(Mandatory = $false)] [int]$SiteThreads = 4, [Parameter(Mandatory = $false)] [int]$WidgetThreads = 8 ) begin { $previousGlobalVerbosity = $global:VerbosePreference # Quiet the logs for now. # $global:VerbosePreference = 'Continue' } process { $logLead = (Get-LogLeadName) $WebTestBaseName = "Start-WebTests" (Reset-WebTestLogFolder -BaseLogFileName $WebTestBaseName) $logFilePath = (Get-WebTestLogPath -LogName $WebTestBaseName) # Only run on Web-tier servers. if(!(Test-IsWebServer)) { "$logLead : Current server is not a Web-tier server. Skipping." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning return $true } Import-Module -Name PsRedis -Global # Variables for setup $logins = ($LoginsJson | ConvertFrom-Json) $coreurls = ($CoreUrlsJson | ConvertFrom-Json) # Get all of the URLs in the file that are on this web server. $siteNames = ((Get-IISSiteList).Bindings.Host).Where({!(Test-IsCollectionNullOrEmpty $logins.$_)}) | Sort-Object -Unique $siteNamesFormatted = ($siteNames | Format-Table -AutoSize | Out-String) "$logLead : Sites available to test:`n$siteNamesFormatted" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host "$logLead : SitesToSkip: $SitesToSkip" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host if(!(Test-IsCollectionNullOrEmpty $SitesToSkip)) { # Trim any leading "http" nonsense, if necessary. $SitesToSkip = $SitesToSkip | ForEach-Object { try { if ($_.StartsWith("http")) { $site = $_ [System.Uri]::new($_).Host } else { $_ } } catch { "$logLead : $site could not be parsed as a site to skip. It is being ignored." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning Resolve-Error } } $siteNames = $siteNames | Where-Object{$_ -notin $SitesToSkip} "$logLead : Site Names after exclusions: $siteNames" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host } # If no sites found that match on this webserver, return. if(Test-IsCollectionNullOrEmpty $siteNames) { "$logLead : No sites to test on this web server!" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning return $true } # Limit the number of sites to test. $siteNames = ($siteNames | Get-Random -Count $MaximumSitesToTest) $siteNamesFormatted = ($siteNames | Format-Table -AutoSize | Out-String) "$logLead : Limiting the number of sites to test to $MaximumSitesToTest. Actual number of sites under test is $($siteNames.Count)." | Tee-OutFile -Append -FilePath $logFilePath | Write-Host "$logLead : Limited Site Names:`n$siteNamesFormatted" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host $allTestResults = @() $StopWatch = [system.diagnostics.StopWatch]::StartNew() "$logLead : Starting Web Tests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host if($SkipAdmin) { "$logLead : Skipping Admin Tests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host } if($SkipAccountCount) { "$logLead : Skipping Account Count Tests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host } if($SkipWidgetWarmUp) { "$logLead : Skipping Site Widget Warm Up" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host } # Output some inputs "$logLead : Maximum Sites To Test: $MaximumSitesToTest" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host "$logLead : Site Threads: $SiteThreads" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host "$logLead : Widget Threads: $WidgetThreads" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host # Iterate over the sites and get Synthetic accounts from $logins $siteTestResult = Invoke-Parallel -Objects $siteNames -ReturnObjects -ThreadPerObject -NumThreads $SiteThreads -Arguments @($logins, $coreurls, $SkipAdmin, $SkipAccountCount, $SkipWidgetWarmUp, $WidgetThreads) -script { Param( $siteName, $inputArguments ) try { $logLead = "[Invoke-Parallel `$siteNames ($siteName)]" $logFilePath = (Get-WebTestLogPath -BankUrl $siteName) $sb_logins = $inputArguments[0] $sb_coreurls = $inputArguments[1] $sb_skipAdmin = $inputArguments[2] $sb_skipAccountCount = $inputArguments[3] $sb_skipWidgetWarmUp = $inputArguments[4] $sb_widgetThreads = $inputArguments[5] $siteLogins = @($sb_logins.$siteName) if (Test-IsCollectionNullOrEmpty $siteLogins) { "$logLead : No logins to find, can't test the site!" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning $allTestResults += (Test-Should -Result $adminResponse -Predicate ${function:Test-Passthrough} -Widget "Site Logins" -Route "Logins not available" -Login @{UrlSignature = $siteName; Username = "" } $true "There was no corresponding site entry for running this test against. Can not verify if the site is up or down.") continue } $siteTestResults = @() if (!$sb_skipWidgetWarmUp) { # Iterate over all of the widgets to warm them up. $widgetStartResult = Invoke-Parallel -Objects $siteLogins.Widgets -ReturnObjects -ThreadPerObject -NumThreads $sb_widgetThreads -Arguments @($siteLogins.UrlSignature) -script { Param( $widgetName, $inputArguments ) try { $logLead = "[Invoke-Parallel `$siteLogins.Widgets ($widgetName)]" $urlSignature = $inputArguments[0] $urlSignature = "https://$urlSignature/" $status = "unknown" $logFilePath = (Get-WebTestLogPath -BankUrl $urlSignature) "$logLead : Widget Warmup $urlSignature$widgetName" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose $result = (Invoke-Endpoint -Baseuri $urlSignature -Uri $widgetName) $status = $result.StatusCode if ([string]::IsNullOrWhiteSpace($status)) { $status = $result.LastError } if ([string]::IsNullOrWhiteSpace($status)) { $status = "Unknown" } $resultWidgetObj = New-Object PSObject -Property $([ordered]@{ FunctionName = "Site Warmup" ReturnValue = $true ReturnMessage = "Status code: $status" Username = $null Url = $urlSignature Route = $widgetName Widget = "Site Warmup" Parameters = $null Runtime = $result.TotalRuntime MachineName = (Get-FullyQualifiedServerName) }) return $resultWidgetObj } catch { Write-Warning "$logLead : Failed to properly warm up widgets." Write-Warning "$logLead : $_" $resultWidgetObj = New-Object PSObject -Property $([ordered]@{ FunctionName = "Site Warmup" ReturnValue = $false ReturnMessage = $_.Exception.Message Username = $null Url = $inputArguments[0] Route = $widgetName Widget = "Site Warmup" Parameters = $null Runtime = $null MachineName = (Get-FullyQualifiedServerName) }) return $resultWidgetObj } } $siteTestResults += $widgetStartResult # Give the machine a break. Start-Sleep -Seconds 3 } # Each site base-url only has one admin site url to test # Let's test that one first so we don't loop it when we loop users # Cole says these will always be the same for a given FI in the above json if (!$sb_skipAdmin) { $adminSignature = ($siteLogins | Select-Object -First 1).AdminUrlSignature # Test admin variant of URL. $fakeAdminLogin = @{UrlSignature = $adminSignature; Username = "" } # This is for logging purposes only $bankAdminUrl = "https://$($adminSignature)" "$logLead : Testing Admin URL : $bankAdminUrl" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose $adminResponse = Invoke-Endpoint -Uri $bankAdminUrl if ($adminResponse.HasErrors -and !($adminResponse.success)) { $resultObj = New-Object PSObject -Property $([ordered]@{ FunctionName = "Invoke-Endpoint" ReturnValue = $false ReturnMessage = $adminResponse.LastError Username = $null Url = $authUrl Route = $null Widget = "Admin Site Warmup" Parameters = $null Runtime = $adminResponse.TotalRuntime MachineName = (Get-FullyQualifiedServerName) }) $siteTestResults += $resultObj "$logLead : Error caught for Admin URL: $authUrl. Skipping tests!" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning "$logLead : Exception: $($adminResponse.LastError)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning } if ($adminResponse.Success) { "$logLead : Running test Test-HaveStatusCode." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose $testResult = (Test-Should -Result $adminResponse.Result -Predicate ${function:Test-HaveStatusCode} -Widget "Admin" -Route "Admin" -Login $fakeAdminLogin 200) $siteTestResults += $testResult "$logLead : Running test Test-HaveResponseHeader." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose $testResult = (Test-Should -Result $adminResponse.Result -Predicate ${function:Test-HaveResponseHeader} -Widget "Admin" -Route "Admin" -Login $fakeAdminLogin 'Content-Type' 'text/html') $siteTestResults += $testResult # This is awful, but it's less awful than the rest of the options. TR # Could I oneliner it? Yes. Would that be harder to maintain - or even read - in 6 months? Definitely. TR $Content = $adminResponse.Result.Content $newLine = [System.Environment]::NewLine $lines = $Content.Split($newLine) $iframe = $lines.Where( { $_ -match " $WebSession = $authResponse.WebSession if ($authResponse.Success) { "$logLead : Testing if we authenticated successfully..." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose "$logLead : Running test Test-HaveStatusCode." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose $testResult = (Test-Should -Result $authResponse.Result -Predicate ${function:Test-HaveStatusCode} -Widget "Authentication" -Route "Authenticated" -Login $login 200) $successfulAuthentication = $testResult.ReturnValue $siteTestResults += $testResult "$logLead : Running test Test-HaveResponseHeader." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose $testResult = (Test-Should -Result $authResponse.Result -Predicate ${function:Test-HaveResponseHeader} -Widget "Authentication" -Route "Authenticated" -Login $login 'Content-Type' 'text/html') $siteTestResults += $testResult # Test if we landed on /Authentication/ page after attempting to login. if ($authResponse.Result.BaseResponse.ResponseUri.AbsolutePath -match "/Authentication") { $successfulAuthentication = $false "$logLead : Didn't authenticate for Auth URL: $authUrl. Retrying again in 30 seconds." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning Start-Sleep -Seconds 3 $retryCount = $retryCount + 1 } else { $stoploop = $true } } else { "$logLead : Didn't authenticate for Auth URL: $authUrl. Retrying again in 30 seconds." | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning Start-Sleep -Seconds 10 $retryCount = $retryCount + 1 } } else { $sbStopWatch.Stop() # Break out because we tried 8 times without success. $stoploop = $true $resultObj = New-Object PSObject -Property $([ordered]@{ FunctionName = "Invoke-Endpoint" ReturnValue = $false ReturnMessage = "Authentication failed after $maxRetries retries." Username = $userName Url = $login.UrlSignature Route = "Authenticated" Widget = "Authentication" Parameters = $null Runtime = [System.TimeSpan]::FromMilliseconds($StopWatch.ElapsedMilliseconds).ToString() MachineName = (Get-FullyQualifiedServerName) }) $siteTestResults += $resultObj } } catch { "$logLead : Exception for Auth URL: $authUrl. Exception:`n$($_.Exception.Message)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning Start-Sleep -Seconds 10 $retryCount = $retryCount + 1 } } while ($stoploop -eq $false) # ensure it got stopped $sbStopWatch.Stop() # Test final auth attempt. if (!$successfulAuthentication) { $resultObj = New-Object PSObject -Property $([ordered]@{ FunctionName = "Invoke-Endpoint" ReturnValue = $false ReturnMessage = "Authentication failed! We landed back on login." Username = $userName Url = $login.UrlSignature Route = "Authenticated" Widget = "Authentication" Parameters = $null Runtime = [System.TimeSpan]::FromMilliseconds($sbStopWatch.ElapsedMilliseconds).ToString() MachineName = (Get-FullyQualifiedServerName) }) $siteTestResults += $resultObj } # Test if we authenticated. if ($authResponse.Success) { # Test if we landed on /AtLogin/ page if ($authResponse.Result.BaseResponse.ResponseUri.AbsolutePath -match "/AtLogin") { $successfulAuthentication = $false $resultObj = New-Object PSObject -Property $([ordered]@{ FunctionName = "Invoke-Endpoint" ReturnValue = $false ReturnMessage = "Authentication failed! We landed on AtLogin. Has user logged in before? Is this a faux-synthetic?" Username = $userName Url = $login.UrlSignature Route = "Authenticated" Widget = "AtLogin" Parameters = $null Runtime = [System.TimeSpan]::FromMilliseconds($sbStopWatch.ElapsedMilliseconds).ToString() MachineName = (Get-FullyQualifiedServerName) }) $siteTestResults += $resultObj } if ($successfulAuthentication) { "$logLead : Testing widgets." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose # Since webSession and Cookie collection can't be serialized going into the job, we must create a simple # array with HashTable to hold the data and rebuild it inside the Invoke-Parallel. $session = $WebSession $cookies = @() foreach ($cookie in $session.Cookies.GetCookies($authUrl)) { $cookies += @{Name = $cookie.Name; Value = $cookie.Value; Domain = $cookie.Domain } } # Loop over the actual widget URL endpoints. Only the URLs where the widget name matches to the login widget. $widgetUrlResult = Invoke-Parallel -Objects $login.Widgets -ReturnObjects -ThreadPerObject -NumThreads $sb_widgetThreads -Arguments @($sb_coreUrls, $login, $cookies, $bankUrl, $userAccountsCount, $userName, $sb_skipAccountCount) -script { Param( $widget, $inputArguments ) try { $logLead = "[Invoke-Parallel `$login.Widgets ($widget)]" $widgetResults = @() $sb_coreUrls = $inputArguments[0] $sb_widgetUrls = @() if ($null -ne $sb_coreUrls.$widget) { $sb_widgetUrls = @($sb_coreUrls.$widget) } $sb1_login = $inputArguments[1] $sb1_WebSessionCookies = $inputArguments[2] $sb1_bankUrl = $inputArguments[3] $sb1_userAccountsCount = $inputArguments[4] $sb1_userName = $inputArguments[5] $sb1_skipAccountCount = $inputArguments[6] # Create a new session with the cookies array passed in. $sb_WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession foreach ($cookie in $sb1_WebSessionCookies) { # Create a new cookie with the data passed in. $newCookie = New-Object System.Net.Cookie $newCookie.Name = $cookie.Name $newCookie.Value = $cookie.Value $newCookie.Domain = $cookie.Domain $sb_WebSession.Cookies.Add($newCookie) } if (Test-IsCollectionNullOrEmpty $sb_widgetUrls) { # We intentionally don't have anything to test here. # This is okay. # Also note the very special logging parameter because we don't have anything to do, so put this in the parent/site log $logFilePath = (Get-WebTestLogPath -BankUrl $sb1_bankUrl) "$logLead : Nothing to test for $widget. This is cool." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose continue } $logFilePath = (Get-WebTestLogPath -BankUrl $sb1_bankUrl -Widget $widget) "$logLead : Testing widget [$widget] routes [$($sb_widgetUrls -join ',')]" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose foreach ($url in $sb_widgetUrls) { $sbStopWatch2 = [System.Diagnostics.StopWatch]::StartNew() if ([string]::IsNullOrWhiteSpace($url)) { "$logLead : Found an empty url string for $widget. This indicates that either there was an empty string in the coreurls json array for this widget or something was misread. Continuing to the next route." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose continue } "$logLead : Testing URL : $sb1_bankUrl$url" | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose $result = (Invoke-Endpoint -Baseuri $sb1_bankUrl -Uri $url -WebSession $sb_WebSession) if ($result.HasErrors -and !($result.success)) { $sbStopWatch2.Stop() $resultObj = New-Object PSObject -Property $([ordered]@{ FunctionName = "Invoke-Endpoint" ReturnValue = $false ReturnMessage = $result.LastError Username = $sb1_userName Url = $sb1_bankUrl Route = $url Widget = $widget Parameters = $null Runtime = $result.TotalRuntime MachineName = (Get-FullyQualifiedServerName) }) $widgetResults += $resultObj "$logLead : Could not test $sb1_bankUrl$url for $widget - Is the widget installed?" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning "$logLead : Exception: $($result.LastError)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Warning } if ($result.Success) { $sbStopWatch2.Stop() $runtime = $sbStopWatch2.Elapsed.ToString("ss\.fffffff") $status = $result.StatusCode $resultObj = New-Object PSObject -Property $([ordered]@{ FunctionName = "Invoke-Endpoint" ReturnValue = $true ReturnMessage = "Status code: $status" Username = $sb1_userName Url = $sb1_bankUrl Route = $url Widget = $widget Parameters = $null Runtime = $runtime MachineName = (Get-FullyQualifiedServerName) }) $widgetResults += $resultObj # Only check the account count if the path is MyAccountsV2. if (!$sb1_skipAccountCount -and $url -eq "/MyAccountsV2/") { "$logLead : Url is /MyAccountsV2/, Running test Test-HaveAccountCount." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose $testResult = (Test-Should -Result $result.Result -Predicate ${function:Test-HaveAccountCount} -Widget $widget -Route $url -Login $sb1_login $sb1_userAccountsCount) $widgetResults += $testResult } "$logLead : Running test Test-HaveStatusCode." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose $testResult = (Test-Should -Result $result.Result -Predicate ${function:Test-HaveStatusCode} -Widget $widget -Route $url -Login $sb1_login 200) $widgetResults += $testResult # Commented out for now. Sub-paths may return JSON, so this test fails. #$testResult = (Test-Should -Result $result -Predicate ${function:Test-HaveResponseHeader} 'Content-Type' 'text/html;') #$siteTestResults += $testResult # If the URL ends with a '/', run the test. if (($url -split '/')[-1].Length -eq 0) { "$logLead : Running test Test-HaveContentThatMatches." | Tee-OutFile -Append -FilePath $logFilePath | Write-Verbose $testResult = (Test-Should -Result $result.Result -Predicate ${function:Test-HaveContentThatMatches} -Widget $widget -Route $url -Login $sb1_login $url) $widgetResults += $testResult } } } return $widgetResults } catch { Write-Warning "$logLead : Exception occurred when testing widgets." Write-Warning "$logLead : $_" $resultObj = New-Object PSObject -Property $([ordered]@{ FunctionName = "Invoke-Endpoint" ReturnValue = $false ReturnMessage = $_.Exception.Message Username = $sb1_userName Url = $sb1_bankUrl Route = $null Widget = $widget Parameters = $null Runtime = $null MachineName = (Get-FullyQualifiedServerName) }) $widgetResults += $resultObj return $widgetResults } } $siteTestResults += $widgetUrlResult } } } # foreach site result, write the aggregate times out by the widget $hash = @{} $maxKeyLength = 0 foreach ($result in $siteTestResults) { # Can't calculate runtimes if you don't have them if ([string]::IsNullOrWhiteSpace($result.Runtime)) { continue } try { $widgetName = $result.Widget $runtime = $result.Runtime $runtimeSplit = $runtime.Split(':') switch ($runtimeSplit.Length) { 3 { $runtime = $runtime; break; } 2 { $runtime = "00:$runtime"; break; } 1 { $runtime = "00:00:$runtime"; break; } } $runtime = [System.TimeSpan]::Parse($runtime) if ($null -eq $hash.$widgetName) { $hash.$widgetName = [System.TimeSpan]::new(0) } $hash.$widgetName += $runtime if ($widgetName.Length -gt $maxKeyLength) { $maxKeyLength = $widgetName.Length } } catch { Write-Warning "$logLead : Couldn't parse the runtime [$($result.Runtime)] for widget [$($result.Widget)]" Resolve-Error } } $maxKeyLength += 2 $siteUrl = $siteTestResults[0].Url "$logLead : Test runtime results for $siteUrl" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host foreach ($key in $hash.Keys) { "$siteUrl - $($key.PadLeft($maxKeyLength)) - $($hash.$key)" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host } return $siteTestResults } catch { Write-Warning "$logLead : Exception occurred while running tests." Write-Warning "$logLead : $_" $resultObj = New-Object PSObject -Property $([ordered]@{ FunctionName = "Invoke-Endpoint" ReturnValue = $false ReturnMessage = $_.Exception.Message Username = $sb1_userName Url = $sb1_bankUrl Route = $null Widget = $widget Parameters = $null Runtime = $null MachineName = (Get-FullyQualifiedServerName) }) $siteTestResults += $resultObj return $siteTestResults } } $allTestResults += @($siteTestResult) # Log all test results at once for a pretty table $resultMessage = (Format-Table -AutoSize -Property @( @{Label = "Should Test"; Expression = {$_.FunctionName.Replace("Test-","")}; Alignment = "Left"}, @{Label = "Passed?"; Expression = {$_.ReturnValue}; Alignment = "Left"}, @{Label = "Url"; Expression = {$_.Url}; Alignment = "Left"}, @{Label = "Route"; Expression = {$_.Route}; Alignment = "Left"}, @{Label = "Result Message"; Expression = {$_.ReturnMessage}; Alignment = "Left";} @{Label = "Runtime"; Expression = {$_.Runtime}; Alignment = "Left";} ) -InputObject $allTestResults) | Out-String -Width 300 "`n" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host $resultMessage.Trim() | Tee-OutFile -Append -FilePath $logFilePath | Write-Host "`n" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host <# Commented out but keeping so we can bring this work into a next cycle. Per discussion with Brent, Justin, and Cole The two long javascript and css strings should be put into a ride-along file that we get-content and use inline below for maintainability. This was demoable but not usable from the agent. Future work would be to generate this on the agent along with parsing all the results On the agent so we can do BuildProblems on the failing lines as well. $htmlParams = @{ Title = "Start-WebTests :: All Results" PreContent = "

All Results

" PostContent = "Generated $([System.DateTime]::Now.ToString())" } $allTestResults | ConvertTo-Html @htmlParams | Out-File .\all.html #> $passedAllTests = $allTestResults.Where({ $_.returnValue -eq $false }).Count -eq 0 "$logLead : Passed all tests? $passedAllTests" | Tee-OutFile -Append -FilePath $logFilePath | Write-Host $StopWatch.Stop() $totalHours = $StopWatch.Elapsed.Hours $totalMinutes = $StopWatch.Elapsed.Minutes $totalSecs = $StopWatch.Elapsed.Seconds "$logLead : Web tests ran for a total of $totalHours hours, $totalMinutes minutes, $totalSecs seconds." | Tee-OutFile -Append -FilePath $logFilePath | Write-Host return $passedAllTests } end { $global:VerbosePreference = $previousGlobalVerbosity } }