Skip to content

Commit e0be0f0

Browse files
authored
Additional test stabilization (#935): Drain Queue Before Verifying Results in AsyncBlockTests (#940)
* fix 935: drain queue before verifying opcodes in all verification tests Apply queue drain timing fix to tests that verify async opcodes. These tests were checking opcodes immediately after async completion without ensuring all cleanup work had been recorded to the opcode log, causing intermittent test failures. * Refine cdb test soak script Refactor cdb test script to capture stacks independently, as well as output log, stacks, and dmp for all abnormal exits (including Ctrl+C). * Fix AsyncBlockTests: drain queue before verifying in al tests Apply consistent queue drain pattern to 8 AsyncBlockTests before final queue verification to eliminate timing races where cleanup work completes asynchronously after XAsyncGetStatus() returns. Root Cause: The async framework's Cleanup operation is initiated by the provider but completed asynchronously through the task queue. Tests checking queue state or opcode snapshots immediately after XAsyncGetStatus() could race with the pending Cleanup work, resulting in intermittent failures (heisenbug-like behavior with "8 vs 9 opcodes" or "queue not empty" errors). Solution: All queue verification now preceded by explicit drain loop: - Checks both Completion and Work ports - 10ms sleep granularity, 2000ms timeout - Ensures all async cleanup completes before verification
1 parent a84418b commit e0be0f0

2 files changed

Lines changed: 96 additions & 60 deletions

File tree

Tests/UnitTests/Tests/AsyncBlockTests.cpp

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -429,15 +429,15 @@ DEFINE_TEST_CLASS(AsyncBlockTests)
429429
ops.push_back(XAsyncOp::GetResult);
430430
ops.push_back(XAsyncOp::Cleanup);
431431

432-
VerifyOps(data.Ref->GetOpCodes(), ops);
433-
432+
// Drain the queue before verifying opcodes to ensure cleanup has been recorded
434433
UINT64 drainTicks = GetTickCount64();
435434
while ((!XTaskQueueIsEmpty(queue, XTaskQueuePort::Completion) || !XTaskQueueIsEmpty(queue, XTaskQueuePort::Work))
436435
&& GetTickCount64() - drainTicks < 2000)
437436
{
438437
Sleep(10);
439438
}
440439

440+
VerifyOps(data.Ref->GetOpCodes(), ops);
441441
VERIFY_QUEUE_EMPTY(queue);
442442
}
443443

@@ -480,6 +480,14 @@ DEFINE_TEST_CLASS(AsyncBlockTests)
480480

481481
VERIFY_ARE_EQUAL(count, completionCount);
482482

483+
// Drain the queue before verifying it's empty to ensure all cleanup has been recorded
484+
UINT64 drainTicks = GetTickCount64();
485+
while ((!XTaskQueueIsEmpty(queue, XTaskQueuePort::Completion) || !XTaskQueueIsEmpty(queue, XTaskQueuePort::Work))
486+
&& GetTickCount64() - drainTicks < 2000)
487+
{
488+
Sleep(10);
489+
}
490+
483491
// Note: FactorialCallData array elements were cleaned up by FactorialResult.
484492
VERIFY_QUEUE_EMPTY(queue);
485493
}
@@ -527,14 +535,15 @@ DEFINE_TEST_CLASS(AsyncBlockTests)
527535
ops.push_back(XAsyncOp::GetResult);
528536
ops.push_back(XAsyncOp::Cleanup);
529537

530-
VerifyOps(data.Ref->GetOpCodes(), ops);
531-
538+
// Drain the queue before verifying opcodes to ensure cleanup has been recorded
532539
UINT64 drainTicks = GetTickCount64();
533540
while ((!XTaskQueueIsEmpty(queue, XTaskQueuePort::Completion) || !XTaskQueueIsEmpty(queue, XTaskQueuePort::Work))
534541
&& GetTickCount64() - drainTicks < 2000)
535542
{
536543
Sleep(10);
537544
}
545+
546+
VerifyOps(data.Ref->GetOpCodes(), ops);
538547
VERIFY_QUEUE_EMPTY(queue);
539548
}
540549

@@ -605,9 +614,16 @@ DEFINE_TEST_CLASS(AsyncBlockTests)
605614

606615
XAsyncCancel(&async);
607616
VERIFY_ARE_EQUAL(XAsyncGetStatus(&async, true), E_ABORT);
608-
Sleep(500);
609617
VERIFY_ARE_EQUAL(E_ABORT, hrCallback);
610618

619+
// Drain the queue before verifying opcodes to ensure cleanup has been recorded
620+
UINT64 drainTicks = GetTickCount64();
621+
while ((!XTaskQueueIsEmpty(queue, XTaskQueuePort::Completion) || !XTaskQueueIsEmpty(queue, XTaskQueuePort::Work))
622+
&& GetTickCount64() - drainTicks < 2000)
623+
{
624+
Sleep(10);
625+
}
626+
611627
auto opCodes = data.Ref->GetOpCodes();
612628
VerifyHasOp(opCodes, XAsyncOp::Cancel);
613629
VerifyHasOp(opCodes, XAsyncOp::Cleanup);
@@ -640,7 +656,14 @@ DEFINE_TEST_CLASS(AsyncBlockTests)
640656
XAsyncCancel(&async);
641657

642658
VERIFY_ARE_EQUAL(XAsyncGetStatus(&async, true), E_ABORT);
643-
XTaskQueueDispatch(queue, XTaskQueuePort::Completion, 700);
659+
660+
// Drain the queue before verifying opcodes to ensure cleanup has been recorded
661+
UINT64 drainTicks = GetTickCount64();
662+
while ((!XTaskQueueIsEmpty(queue, XTaskQueuePort::Completion) || !XTaskQueueIsEmpty(queue, XTaskQueuePort::Work))
663+
&& GetTickCount64() - drainTicks < 2000)
664+
{
665+
Sleep(10);
666+
}
644667

645668
auto opCodes = data.Ref->GetOpCodes();
646669
VerifyHasOp(opCodes, XAsyncOp::Cancel);
@@ -674,7 +697,14 @@ DEFINE_TEST_CLASS(AsyncBlockTests)
674697
XAsyncCancel(&async);
675698

676699
VERIFY_ARE_EQUAL(XAsyncGetStatus(&async, true), E_ABORT);
677-
Sleep(500);
700+
701+
// Drain the queue before verifying opcodes to ensure cleanup has been recorded
702+
UINT64 drainTicks = GetTickCount64();
703+
while ((!XTaskQueueIsEmpty(queue, XTaskQueuePort::Completion) || !XTaskQueueIsEmpty(queue, XTaskQueuePort::Work))
704+
&& GetTickCount64() - drainTicks < 2000)
705+
{
706+
Sleep(10);
707+
}
678708

679709
auto opCodes = data.Ref->GetOpCodes();
680710
VerifyHasOp(opCodes, XAsyncOp::Cancel);
@@ -701,7 +731,14 @@ DEFINE_TEST_CLASS(AsyncBlockTests)
701731
VERIFY_SUCCEEDED(XAsyncRun(&async, WorkThunk::Callback));
702732

703733
result = XAsyncGetStatus(&async, true);
704-
Sleep(500);
734+
735+
// Drain the queue before verifying it's empty to ensure cleanup has been recorded
736+
UINT64 drainTicks = GetTickCount64();
737+
while ((!XTaskQueueIsEmpty(queue, XTaskQueuePort::Completion) || !XTaskQueueIsEmpty(queue, XTaskQueuePort::Work))
738+
&& GetTickCount64() - drainTicks < 2000)
739+
{
740+
Sleep(10);
741+
}
705742

706743
VERIFY_ARE_EQUAL(result, expected);
707744
VERIFY_QUEUE_EMPTY(queue);
@@ -815,7 +852,14 @@ DEFINE_TEST_CLASS(AsyncBlockTests)
815852

816853
VERIFY_SUCCEEDED(FactorialAllocateAsync(5, &async));
817854
VERIFY_SUCCEEDED(XAsyncGetStatus(&async, true));
818-
Sleep(500);
855+
856+
// Drain the queue before verifying it's empty to ensure cleanup has been recorded
857+
UINT64 drainTicks = GetTickCount64();
858+
while ((!XTaskQueueIsEmpty(queue, XTaskQueuePort::Completion) || !XTaskQueueIsEmpty(queue, XTaskQueuePort::Work))
859+
&& GetTickCount64() - drainTicks < 2000)
860+
{
861+
Sleep(10);
862+
}
819863

820864
VERIFY_ARE_EQUAL(result, (DWORD)120);
821865
VERIFY_QUEUE_EMPTY(queue);

Tests/test_under_cdb.ps1

Lines changed: 43 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -90,59 +90,50 @@ while ($true) {
9090
if (Test-Path $dumpPath) { Remove-Item $dumpPath -Force -ErrorAction SilentlyContinue }
9191

9292
$dumpPathCdb = $dumpPath -replace '\\','/'
93-
$dumpOnBreakpoint = 'sxd ibp'
94-
$dumpSymbols = @(
95-
'TE_Common!WEX::TestExecution::Verify::VerificationFailed<tagVARIANT,tagVARIANT,WEX::TestExecution::Verify::VerifyMessageFunctor>',
96-
'TE_Common!WEX::TestExecution::Verify::VerificationFailed<tagVARIANT,tagVARIANT,WEX::TestExecution::Private::VerifyMessageFunctor>',
97-
'TE_Common!WEX::TestExecution::ComVerify::AreEqual',
98-
'libHttpClient_UnitTest_TAEF!WEX::TestExecution::Private::MacroVerify::VerificationFailed',
99-
'libHttpClient_UnitTest_TAEF!WEX::TestExecution::Private::MacroVerify::AreEqualImpl<unsigned int>',
100-
'libHttpClient_UnitTest_TAEF!WEX::TestExecution::Private::MacroVerify::IsTrueImpl<char>',
101-
'ucrtbased!abort',
102-
'ucrtbase!abort',
103-
'ucrtbased!issue_debug_notification',
104-
'ucrtbase!issue_debug_notification',
105-
'vcruntime140d!__fastfail',
106-
'vcruntime140_1d!__fastfail',
107-
'vcruntime140d!_invoke_watson',
108-
'vcruntime140_1d!_invoke_watson',
109-
'ucrtbased!_CrtDbgReportW',
110-
'ucrtbased!_raiseabort',
111-
'ucrtbase!_raiseabort'
93+
$stackTraceFile = Join-Path $LogDir ("stack_{0:D6}_{1}_{2}.txt" -f $iteration, $stamp, $nonce)
94+
$stackPathCdb = $stackTraceFile -replace '\\','/'
95+
96+
# Helper function to generate diagnostic commands (log stacks, dump, exit)
97+
function Get-DiagnosticCommand {
98+
param(
99+
[string]$StackFile,
100+
[string]$DumpFile,
101+
[switch]$IncludeAnalyze
102+
)
103+
$analyze = if ($IncludeAnalyze) { '!analyze -v; ' } else { '' }
104+
return ".logopen $StackFile; ${analyze}~*k; .logclose; .dump /ma /o $DumpFile; q"
105+
}
106+
107+
$diagCmd = Get-DiagnosticCommand -StackFile $stackPathCdb -DumpFile $dumpPathCdb -IncludeAnalyze
108+
$diagCmdNoAnalyze = Get-DiagnosticCommand -StackFile $stackPathCdb -DumpFile $dumpPathCdb
109+
110+
# Ctrl+C handler: capture all thread stacks and dump before quitting
111+
$ctrlCHandler = 'sxe -c "' + $diagCmd + '" 0x40010005'
112+
# Access violation handler: capture stacks and dump
113+
$avHandler = 'sxe -c "' + $diagCmd + '" av'
114+
# Stack overflow handler: capture stacks and dump
115+
$stackOverflowHandler = 'sxe -c "' + $diagCmd + '" 0xc0000409'
116+
117+
# Common debugger setup
118+
$cmdLines = @(
119+
'.childdbg 1'
120+
'sxd ibp'
121+
'.logopen ' + $logPath
122+
('bp ucrtbased!abort "' + $diagCmd + '"')
123+
('bp ucrtbase!abort "' + $diagCmd + '"')
124+
('bp vcruntime140d!_invoke_watson "' + $diagCmd + '"')
125+
$stackOverflowHandler
126+
$avHandler
127+
'sxd c0000374'
128+
$ctrlCHandler
129+
'g'
112130
)
113-
$dumpBreakpoints = $dumpSymbols | ForEach-Object { New-DumpOnSymbol -Symbol $_ -DumpPath $dumpPathCdb }
114131

132+
# Append mode-specific exit behavior
115133
if ($ManualDebug) {
116-
$cmdLines = @(
117-
'.childdbg 1',
118-
$dumpOnBreakpoint
119-
) + $dumpBreakpoints + @(
120-
'sxd eh',
121-
'sxi 0xe06d7363',
122-
'sxe 0x40000015',
123-
'sxe 0xc0000409',
124-
'sxe 0xc000013a',
125-
'sxe av',
126-
'sxe c0000374',
127-
'g',
128-
'g'
129-
)
134+
$cmdLines += 'g'
130135
} else {
131-
$cmdLines = @(
132-
'.childdbg 1',
133-
$dumpOnBreakpoint
134-
) + $dumpBreakpoints + @(
135-
'sxd eh',
136-
'sxi 0xe06d7363',
137-
('sxe -c "!analyze -v; ~*k; .dump /ma /o ' + $dumpPathCdb + '; .kill; q" 0xc000013a'),
138-
('sxe -c "!analyze -v; ~*k; .dump /ma /o ' + $dumpPathCdb + '; .kill; q" 0x40000015'),
139-
('sxe -c "!analyze -v; ~*k; .dump /ma /o ' + $dumpPathCdb + '; .kill; q" 0xc0000409'),
140-
('sxd -c "!analyze -v; ~*k; .dump /ma /o ' + $dumpPathCdb + '; .kill; q" av'),
141-
('sxd -c "!analyze -v; ~*k; .dump /ma /o ' + $dumpPathCdb + '; .kill; q" c0000374'),
142-
'g',
143-
'g', # resume on TE's initial breakpoint
144-
'q'
145-
)
136+
$cmdLines += 'q'
146137
}
147138
$cmdLines | Set-Content -Path $cmdFile -Encoding ASCII
148139

@@ -152,6 +143,7 @@ while ($true) {
152143
if (Test-Path -Path $dumpPath -PathType Leaf ) {
153144
Write-Host "Crash dump detected; stopping" -ForegroundColor Red
154145
Write-Host "Log: $logPath"
146+
Write-Host "Stack trace: $stackTraceFile"
155147
Write-Host "Dump: $dumpPath"
156148
break
157149

@@ -160,13 +152,13 @@ while ($true) {
160152
if ($exitCode -ne 0) {
161153
Write-Host "Non-zero exit code: $exitCode" -ForegroundColor Red
162154
Write-Host "Log: $logPath"
155+
Write-Host "Stack trace: $stackTraceFile"
163156
Write-Host "Dump: $dumpPath"
164157
break
165158
}
166159

167-
# Successful run: clean up log/dump/cmd to avoid accumulation
160+
# Successful run: clean up log/cmd to avoid accumulation (keep dumps/stacks for retention)
168161
if (Test-Path $logPath) { Remove-Item $logPath -Force -ErrorAction SilentlyContinue }
169-
if (Test-Path $dumpPath) { Remove-Item $dumpPath -Force -ErrorAction SilentlyContinue }
170162
if (Test-Path $cmdFile) { Remove-Item $cmdFile -Force -ErrorAction SilentlyContinue }
171163
}
172164

0 commit comments

Comments
 (0)