Fix up coverage with async on Windows (#84917)

Fixes the coverage collection for Windows and async tasks. This ensures
the async task still has access to the PSHost so that it can access the
runspace debugger tasks on the host.
This commit is contained in:
Jordan Borean 2025-04-03 12:56:51 +10:00 committed by GitHub
parent 390e112822
commit b7d76a93b2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 88 additions and 9 deletions

View File

@ -4,6 +4,7 @@
using namespace Microsoft.Win32.SafeHandles
using namespace System.Collections
using namespace System.IO
using namespace System.Management.Automation
using namespace System.Text
using namespace System.Threading
@ -43,6 +44,15 @@ param([ScriptBlock]$ScriptBlock, $Param)
Param = $execInfo.Parameters
})
# It is important we run with the invocation settings so that it has access
# to the same PSHost. The pipeline input also needs to be marked as complete
# so the exec_wrapper isn't waiting for input indefinitely.
$pipelineInput = [PSDataCollection[object]]::new()
$pipelineInput.Complete()
$invocationSettings = [PSInvocationSettings]@{
Host = $host
}
# Signals async_wrapper that we are ready to start the job and to stop waiting
$waitHandle = [SafeWaitHandle]::new([IntPtr]$WaitHandleId, $true)
$waitEvent = [ManualResetEvent]::new($false)
@ -52,7 +62,7 @@ $null = $waitEvent.Set()
$jobOutput = $null
$jobError = $null
try {
$jobAsyncResult = $ps.BeginInvoke()
$jobAsyncResult = $ps.BeginInvoke($pipelineInput, $invocationSettings, $null, $null)
$jobAsyncResult.AsyncWaitHandle.WaitOne($Timeout * 1000) > $null
$result.finished = 1

View File

@ -0,0 +1,9 @@
Function Test-CollectionPwshCoverage {
<#
.SYNOPSIS
Test coverage for collection pwsh util.
#>
'foo'
}
Export-ModuleMember -Function Test-CollectionPwshCoverage

View File

@ -1,6 +1,8 @@
#!powershell
#AnsibleRequires -CSharpUtil Ansible.Basic
#AnsibleRequires -PowerShell ..module_utils.CollectionPwshCoverage
$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
$module.Result.util = Test-CollectionPwshCoverage
$module.ExitJson()

View File

@ -0,0 +1,6 @@
#!powershell
#AnsibleRequires -CSharpUtil Ansible.Basic
$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
$module.ExitJson()

View File

@ -0,0 +1,8 @@
#!powershell
#AnsibleRequires -CSharpUtil Ansible.Basic
#AnsibleRequires -PowerShell Ansible.ModuleUtils.AdjacentPwshCoverage
$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
$module.Result.util = Test-AdjacentPwshCoverage
$module.ExitJson()

View File

@ -0,0 +1,9 @@
Function Test-AdjacentPwshCoverage {
<#
.SYNOPSIS
Test coverage for module_util adjacent pwsh util.
#>
'foo'
}
Export-ModuleMember -Function Test-AdjacentPwshCoverage

View File

@ -2,4 +2,21 @@
win_collection:
- name: run module in library adjacent to test coverage for test plugins
test_win_collection:
test_win_collection_normal:
register: library_result
- name: assert run module with library adjacent module
assert:
that:
- library_result.util == 'foo'
- name: test coverage under async
test_win_collection_async:
async: 30
poll: 2
- name: test coverage under become
test_win_collection_become:
become: yes
become_method: runas
become_user: SYSTEM

View File

@ -3,24 +3,42 @@ from __future__ import annotations
import json
import os
import os.path
import pathlib
def main() -> None:
collection_root = os.getcwd()
collection_root = pathlib.Path(os.getcwd())
print(f"Running windows-integration coverage test in '{collection_root}'")
result_path = os.path.join(collection_root, "tests", "output", "coverage", "coverage-powershell")
module_path = os.path.join(collection_root, "plugins", "modules", "win_collection.ps1")
test_path = os.path.join(collection_root, "tests", "integration", "targets", "win_collection", "library", "test_win_collection.ps1")
result_path = collection_root / "tests" / "output" / "coverage" / "coverage-powershell"
adjacent_modules_path = collection_root / "tests" / "integration" / "targets" / "win_collection" / "library"
adjacent_utils_path = collection_root / "tests" / "integration" / "targets" / "win_collection" / "module_utils"
collection_modules_path = collection_root / "plugins" / "modules"
collection_utils_path = collection_root / "plugins" / "module_utils"
expected_hits = {
str(adjacent_modules_path / 'test_win_collection_async.ps1'): {'5': 1, '6': 1},
str(adjacent_modules_path / 'test_win_collection_become.ps1'): {'5': 1, '6': 1},
str(adjacent_modules_path / 'test_win_collection_normal.ps1'): {'6': 1, '7': 1, '8': 1},
str(adjacent_utils_path / 'Ansible.ModuleUtils.AdjacentPwshCoverage.psm1'): {'6': 1, '9': 1},
str(collection_modules_path / 'win_collection.ps1'): {'6': 1, '7': 1, '8': 1},
str(collection_utils_path / 'CollectionPwshCoverage.psm1'): {'6': 1, '9': 1},
}
found_hits = set()
with open(result_path, mode="rb") as fd:
data = json.load(fd)
for path, result in data.items():
print(f"Testing result for path '{path}' -> {result!r}")
assert path in [module_path, test_path], f"Found unexpected coverage result path '{path}'"
assert result == {'5': 1, '6': 1}, "Coverage result did not pick up a hit on lines 5 and 6"
assert path in expected_hits, f"Found unexpected coverage result path '{path}'"
assert len(data) == 2, f"Expected coverage results for 2 files but got {len(data)}"
expected = expected_hits[path]
assert result == expected, f"Coverage result for {path} was {result!r} but was expecting {expected!r}"
found_hits.add(path)
missing_hits = set(expected_hits.keys()).difference(found_hits)
assert not missing_hits, f"Expected coverage results for {', '.join(missing_hits)} but they were not present"
if __name__ == '__main__':