-
Notifications
You must be signed in to change notification settings - Fork 4.9k
System.Net.Security test stabilization #7937
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
# Licensed to the .NET Foundation under one or more agreements. | ||
# The .NET Foundation licenses this file to you under the MIT license. | ||
# See the LICENSE file in the project root for more information. | ||
|
||
# Usage: | ||
# | ||
# . ParallelTestExecution.ps1 | ||
# cd <testFolder> (e.g. testFolder = src\System.Net.Security\tests\FunctionalTests) | ||
# RunMultiple <n> <delayVarianceMilliseconds> [-UntilFailed] | ||
# | ||
# The above sequence will open up <n> windows running the test project present in the current folder. | ||
# If -UntilFailed is used, the tests will continuously loop until a failure is detected. | ||
# Between loops, the execution will pause for <delayVarianceMilliseconds>. | ||
|
||
function BuildAndTestBinary | ||
{ | ||
$output = (msbuild /t:rebuild,test) | ||
if ($lastexitcode -ne 0) | ||
{ | ||
throw "Build/test failed." | ||
} | ||
|
||
return $output | ||
} | ||
|
||
function TileWindows | ||
{ | ||
$shell = New-Object -ComObject Shell.Application | ||
$shell.TileHorizontally() | ||
} | ||
|
||
function CurrentPath | ||
{ | ||
return (Get-Item -Path ".\" -Verbose).FullName | ||
} | ||
|
||
function ParseCurrentPath | ||
{ | ||
$p = CurrentPath | ||
|
||
$test_found = $false | ||
$contract_found = $false | ||
$root_found = $false | ||
|
||
while ((-not $root_found) -and ($p -ne "")) | ||
{ | ||
$leaf = Split-Path $p -Leaf | ||
|
||
if (Test-Path (Join-Path $p 'build.cmd')) | ||
{ | ||
$Global:RootPath = $p | ||
$root_found = $true | ||
} | ||
|
||
if ($test_found -and (-not $contract_found)) | ||
{ | ||
$Global:ContractName = $leaf | ||
$contract_found = $true | ||
} | ||
|
||
if ($leaf -eq "tests") | ||
{ | ||
$test_found = $true | ||
} | ||
|
||
$p = Split-Path $p | ||
} | ||
|
||
if (-not $test_found) | ||
{ | ||
throw "This folder doesn't appear to be part of a test (looking for ...\contract\tests\...)." | ||
} | ||
} | ||
|
||
function ParseTestExecutionCommand($msBuildOutput) | ||
{ | ||
$foundTestExecution = $false | ||
$cmdLine = "" | ||
|
||
foreach ($line in $msBuildOutput) | ||
{ | ||
if ($foundTestExecution -eq $true) | ||
{ | ||
$cmdLine = $line | ||
break | ||
} | ||
|
||
if ($line.Contains("RunTestsForProject:")) | ||
{ | ||
$foundTestExecution = $true | ||
} | ||
} | ||
|
||
if (-not $foundTestExecution) | ||
{ | ||
throw "Cannot parse MSBuild output: please ensure that the current folder contains a test." | ||
} | ||
|
||
$Global:TestCommand = $cmdLine.Trim() | ||
} | ||
|
||
function ParseTestFolder($testExecutionCmdLine) | ||
{ | ||
$coreRunPath = $testExecutionCmdLine.Split()[0] | ||
return Split-Path $coreRunPath | ||
} | ||
|
||
function Initialize | ||
{ | ||
ParseCurrentPath | ||
|
||
Write-Host -NoNewline "Initializing tests for $($Global:ContractName) . . . " | ||
|
||
try | ||
{ | ||
$output = BuildAndTestBinary | ||
ParseTestExecutionCommand($output) | ||
|
||
Write-Host -ForegroundColor Green "OK" | ||
} | ||
catch | ||
{ | ||
Write-Host -ForegroundColor Red "Failed" | ||
throw | ||
} | ||
} | ||
|
||
function RunOne($testCommand) | ||
{ | ||
if ($testCommand -ne "") | ||
{ | ||
$Global:TestCommand = $testCommand | ||
} | ||
|
||
if ($Global:TestCommand -eq $null) | ||
{ | ||
throw "Run Initialize first or pass the test command line as a parameter." | ||
} | ||
|
||
Write-Host $Global:TestCommand | ||
$path = ParseTestFolder($Global:TestCommand) | ||
Write-Host "$path" | ||
|
||
Push-Location | ||
cd $path | ||
Invoke-Expression $Global:TestCommand | ||
if ($lastexitcode -ne 0) | ||
{ | ||
throw "Test execution failed." | ||
} | ||
|
||
Pop-Location | ||
} | ||
|
||
function RunUntilFailed($testCommand, $delayVarianceMilliseconds = 0) | ||
{ | ||
|
||
try | ||
{ | ||
while($true) | ||
{ | ||
RunOne $testCommand | ||
|
||
if ($delayVarianceMilliseconds -ne 0) | ||
{ | ||
$sleepMilliseconds = Get-Random -Minimum 0 -Maximum $delayVarianceMilliseconds | ||
Write-Host -ForegroundColor Cyan "Sleeping $sleepMilliseconds" | ||
Start-Sleep -Milliseconds $sleepMilliseconds | ||
} | ||
} | ||
} | ||
catch | ||
{ | ||
Write-Host -ForegroundColor Red "Test execution failed!" | ||
Read-Host "Press ENTER to continue..." | ||
} | ||
} | ||
|
||
function RunMultiple( | ||
[int]$n = 2, | ||
[int]$RandomDelayVarianceMilliseconds = 0, | ||
[switch]$UntilFailed = $false) | ||
{ | ||
if ($Global:TestCommand -eq $null) | ||
{ | ||
Initialize | ||
} | ||
|
||
$script = $PSCommandPath | ||
$testCommand = $Global:TestCommand | ||
|
||
if ($untilFailed) | ||
{ | ||
$executionMethod = "RunUntilFailed" | ||
} | ||
else | ||
{ | ||
$executionMethod = "RunOne" | ||
} | ||
|
||
$cmdArguments = "-Command `"&{. $script; $executionMethod '$testCommand' $RandomDelayVarianceMilliseconds}`"" | ||
|
||
$processes = @() | ||
|
||
for ($i=0; $i -lt $n; $i++) | ||
{ | ||
$thisCmdArguments = $cmdArguments -replace ("testResults.xml", "testResults$i.xml") | ||
$process = Start-Process -PassThru powershell -ArgumentList $thisCmdArguments | ||
$processes += $process | ||
} | ||
|
||
$processesExited = $false | ||
while (-not ([console]::KeyAvailable -or $processesExited)) | ||
{ | ||
Clear-Host | ||
|
||
Write-Host -ForegroundColor Cyan "Active test processes:" | ||
Write-Host | ||
Write-Host "[Press any key to close.]" | ||
Write-Host | ||
$processes | Format-Table -Property Id, CPU, Handles, WS, ExitCode | ||
|
||
$processesExited = $true | ||
foreach($p in $processes) | ||
{ | ||
if (-not $p.HasExited) | ||
{ | ||
$processesExited = $false | ||
} | ||
} | ||
|
||
Start-Sleep -Milliseconds 1000 | ||
TileWindows | ||
} | ||
|
||
if (-not $processesExited) | ||
{ | ||
Write-Host -ForegroundColor Cyan "Terminating all processes." | ||
foreach ($p in $processes) | ||
{ | ||
if (-not $p.HasExited) | ||
{ | ||
$p.Kill() | ||
} | ||
} | ||
} | ||
|
||
Write-Host "Done." | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,7 @@ namespace System.Net.Test.Common | |
{ | ||
public class VirtualNetwork | ||
{ | ||
private readonly int WaitForReadDataTimeoutMilliseconds = 10 * 1000; | ||
private readonly int WaitForReadDataTimeoutMilliseconds = 30 * 1000; | ||
|
||
private readonly ConcurrentQueue<byte[]> _clientWriteQueue = new ConcurrentQueue<byte[]>(); | ||
private readonly ConcurrentQueue<byte[]> _serverWriteQueue = new ConcurrentQueue<byte[]>(); | ||
|
@@ -34,9 +34,29 @@ public void ReadFrame(bool server, out byte[] buffer) | |
packetQueue = _serverWriteQueue; | ||
} | ||
|
||
semaphore.Wait(WaitForReadDataTimeoutMilliseconds); | ||
if (!semaphore.Wait(WaitForReadDataTimeoutMilliseconds)) | ||
{ | ||
throw new TimeoutException("VirtualNetwork: Timeout reading the next frame."); | ||
} | ||
|
||
bool dequeueSucceeded = false; | ||
int remainingTries = 3; | ||
int backOffDelayMilliseconds = 2; | ||
|
||
do | ||
{ | ||
dequeueSucceeded = packetQueue.TryDequeue(out buffer); | ||
if (dequeueSucceeded) | ||
{ | ||
break; | ||
} | ||
|
||
remainingTries--; | ||
backOffDelayMilliseconds *= backOffDelayMilliseconds; | ||
Thread.Sleep(backOffDelayMilliseconds); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This math doesn't seem quite right. After the first try, we're going to sleep for Did you mean to be multiplying by 10 each time rather than by backoffDelayMilliseconds? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for catching this. Changing initial delay of 2 ms and rerunning tests. |
||
} | ||
while (!dequeueSucceeded && (remainingTries > 0)); | ||
|
||
bool dequeueSucceeded = packetQueue.TryDequeue(out buffer); | ||
Debug.Assert(dequeueSucceeded, "Packet queue: TryDequeue failed."); | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -114,16 +114,9 @@ public override Task<int> ReadAsync(byte[] buffer, int offset, int count, Cancel | |
|
||
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) | ||
{ | ||
try | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
Write(buffer, offset, count); | ||
return Task.CompletedTask; | ||
} | ||
catch (Exception e) | ||
{ | ||
return Task.FromException(e); | ||
} | ||
return cancellationToken.IsCancellationRequested ? | ||
Task.FromCanceled<int>(cancellationToken) : | ||
Task.Run(() => Write(buffer, offset, count)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is fine. FWIW, I didn't do this when I changed the ReadAsync because, whereas Read blocks waiting for data, Write completes quickly and without blocking. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed per #5991 - the cancellationToken was not honored correctly. |
||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In what situation is this going to fail? Rather than looping around the TryDequeue, should we be looping around the semaphore.Wait? If the Wait completes successfully and there's nothing to take from the queue, that's a serious problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The assert that got removed was the first thing that failed under stress after the test got switched to proper async/await.
The wait is on a semaphore, not a lock. It's acceptable to have multiple threads adding while multiple other threads are reading.
TryDequeue is, by contract, allowed to fail: http://stackoverflow.com/questions/5645348/under-what-conditions-can-trydequeue-and-similar-system-collections-concurrent-c
With this fix, the second TryDequeue always seems to pass. (Given the issue above I would have seen tests hanging for >27hrs which didn't happen overnight.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not following.
The reason TryDequeue might return false is if there's nothing in the collection. And the only reason that might happen is if the semaphore.Wait(timeout) returned false due to hitting the timeout (if it returned true, that meant that there was count in the semaphore that could be taken, and that would only happen if there was a corresponding item to be taken from the queue).
So I'm suggesting that instead of wrapping TryDequeue in a loop, you wrap the Wait in a loop. If the Wait returns true, TryDequeue should never return false, and if the Wait returns false, there's no reason to even try calling TryDequeue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I now see what you mean. The reason I didn't think that was possible because the tests were executing in <10s (about 7-8sec) and the semaphore wait timeout was 10s.
Retrying on the semaphore wait doesn't make the code better as the idea was to eventually time out.
Increasing the timeout to 30sec and throwing a TimeoutException if the semaphore wait returns false.