From b7d76a93b23097e13230788b78d4dd2d0c02b76d Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Thu, 3 Apr 2025 12:56:51 +1000 Subject: [PATCH] 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. --- .../executor/powershell/async_watchdog.ps1 | 12 ++++++- .../module_utils/CollectionPwshCoverage.psm1 | 9 ++++++ .../ns/col/plugins/modules/win_collection.ps1 | 2 ++ ...tion.ps1 => test_win_collection_async.ps1} | 0 .../library/test_win_collection_become.ps1 | 6 ++++ .../library/test_win_collection_normal.ps1 | 8 +++++ ...ible.ModuleUtils.AdjacentPwshCoverage.psm1 | 9 ++++++ .../targets/win_collection/tasks/main.yml | 19 ++++++++++- .../test-coverage.py | 32 +++++++++++++++---- 9 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/plugins/module_utils/CollectionPwshCoverage.psm1 rename test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/{test_win_collection.ps1 => test_win_collection_async.ps1} (100%) create mode 100644 test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection_become.ps1 create mode 100644 test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection_normal.ps1 create mode 100644 test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/module_utils/Ansible.ModuleUtils.AdjacentPwshCoverage.psm1 diff --git a/lib/ansible/executor/powershell/async_watchdog.ps1 b/lib/ansible/executor/powershell/async_watchdog.ps1 index c33ff3a320b..9eef1efa960 100644 --- a/lib/ansible/executor/powershell/async_watchdog.ps1 +++ b/lib/ansible/executor/powershell/async_watchdog.ps1 @@ -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 diff --git a/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/plugins/module_utils/CollectionPwshCoverage.psm1 b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/plugins/module_utils/CollectionPwshCoverage.psm1 new file mode 100644 index 00000000000..01a83483e57 --- /dev/null +++ b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/plugins/module_utils/CollectionPwshCoverage.psm1 @@ -0,0 +1,9 @@ +Function Test-CollectionPwshCoverage { + <# + .SYNOPSIS + Test coverage for collection pwsh util. + #> + 'foo' +} + +Export-ModuleMember -Function Test-CollectionPwshCoverage diff --git a/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/plugins/modules/win_collection.ps1 b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/plugins/modules/win_collection.ps1 index 53b2f2da3b3..708e3fa65ef 100644 --- a/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/plugins/modules/win_collection.ps1 +++ b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/plugins/modules/win_collection.ps1 @@ -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() diff --git a/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection.ps1 b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection_async.ps1 similarity index 100% rename from test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection.ps1 rename to test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection_async.ps1 diff --git a/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection_become.ps1 b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection_become.ps1 new file mode 100644 index 00000000000..53b2f2da3b3 --- /dev/null +++ b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection_become.ps1 @@ -0,0 +1,6 @@ +#!powershell + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$module = [Ansible.Basic.AnsibleModule]::Create($args, @{}) +$module.ExitJson() diff --git a/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection_normal.ps1 b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection_normal.ps1 new file mode 100644 index 00000000000..ba11c1255a3 --- /dev/null +++ b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/library/test_win_collection_normal.ps1 @@ -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() diff --git a/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/module_utils/Ansible.ModuleUtils.AdjacentPwshCoverage.psm1 b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/module_utils/Ansible.ModuleUtils.AdjacentPwshCoverage.psm1 new file mode 100644 index 00000000000..36eada2d443 --- /dev/null +++ b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/module_utils/Ansible.ModuleUtils.AdjacentPwshCoverage.psm1 @@ -0,0 +1,9 @@ +Function Test-AdjacentPwshCoverage { + <# + .SYNOPSIS + Test coverage for module_util adjacent pwsh util. + #> + 'foo' +} + +Export-ModuleMember -Function Test-AdjacentPwshCoverage diff --git a/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/tasks/main.yml b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/tasks/main.yml index 6196b768c6b..b4c59aeeaed 100644 --- a/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/tasks/main.yml +++ b/test/integration/targets/ansible-test-coverage-windows/ansible_collections/ns/col/tests/integration/targets/win_collection/tasks/main.yml @@ -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 diff --git a/test/integration/targets/ansible-test-coverage-windows/test-coverage.py b/test/integration/targets/ansible-test-coverage-windows/test-coverage.py index 98dbca7437c..b3471089ffe 100644 --- a/test/integration/targets/ansible-test-coverage-windows/test-coverage.py +++ b/test/integration/targets/ansible-test-coverage-windows/test-coverage.py @@ -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}'" + + expected = expected_hits[path] + assert result == expected, f"Coverage result for {path} was {result!r} but was expecting {expected!r}" + found_hits.add(path) - assert len(data) == 2, f"Expected coverage results for 2 files but got {len(data)}" + 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__':