From 9b012a88df48ecba347945ce7aff9be9816d28ab Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Tue, 23 Sep 2025 10:14:30 +0200 Subject: [PATCH 1/9] fixes for sanity and sap_company unit --- .github/workflows/ansible-test.yml | 64 +++---------------- changelogs/changelog.yaml | 2 +- plugins/modules/sap_hdbsql.py | 2 +- tests/sanity/ignore-2.17.txt | 3 +- tests/sanity/ignore-2.18.txt | 3 +- tests/sanity/ignore-2.19.txt | 9 +++ tests/sanity/ignore-2.20.txt | 9 +++ tests/unit/compat/mock.py | 6 +- tests/unit/mock/procenv.py | 13 +--- tests/unit/mock/yaml_helper.py | 26 ++------ .../unit/plugins/modules/test_sap_company.py | 32 ++++++---- tests/unit/plugins/modules/utils.py | 18 +++++- 12 files changed, 74 insertions(+), 113 deletions(-) create mode 100644 tests/sanity/ignore-2.19.txt create mode 100644 tests/sanity/ignore-2.20.txt diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml index 405bbce..b3b6fda 100644 --- a/.github/workflows/ansible-test.yml +++ b/.github/workflows/ansible-test.yml @@ -2,11 +2,15 @@ name: CI on: # Run CI against all pushes (direct commits, also merged PRs), Pull Requests push: + branches: + - main + - dev pull_request: # Run CI once per day (at 06:00 UTC) # This ensures that even if there haven't been commits that we are still testing against latest version of ansible-test for each ansible-base version schedule: - cron: '0 6 * * *' + workflow_dispatch: env: NAMESPACE: community COLLECTION_NAME: sap_libs @@ -17,8 +21,6 @@ jobs: strategy: matrix: ansible: - - stable-2.13 - - stable-2.14 - stable-2.15 - stable-2.16 - stable-2.17 @@ -35,7 +37,7 @@ jobs: - name: Check out code uses: actions/checkout@v4 - - name: Perform sanity testing with ansible-test + - name: Perform sanity testing uses: ansible-community/ansible-test-gh-action@release/v1 with: ansible-core-version: ${{ matrix.ansible }} @@ -53,8 +55,6 @@ jobs: fail-fast: true matrix: ansible: - - stable-2.13 - - stable-2.14 - stable-2.15 - stable-2.16 - stable-2.17 @@ -63,62 +63,14 @@ jobs: - devel steps: + - name: Check out code uses: actions/checkout@v4 - - name: Perform unit testing with ansible-test + - name: Perform unit testing uses: ansible-community/ansible-test-gh-action@release/v1 with: ansible-core-version: ${{ matrix.ansible }} testing-type: units - # test-deps: >- - # ansible.netcommon - # ansible.utils - -# Please consult the Readme for information on why we disabled integration tests temporarily. - - # integration: - # runs-on: ubuntu-latest - # name: I (Ⓐ${{ matrix.ansible }}+py${{ matrix.python }}) - # strategy: - # fail-fast: false - # matrix: - # ansible: - # - stable-2.9 # Only if your collection supports Ansible 2.9 - # - stable-2.10 - # - stable-2.11 - # - stable-2.12 - # - stable-2.13 - # - devel - # python: - # - 2.6 - # - 2.7 - # - 3.5 - # - 3.6 - # - 3.7 - # - 3.8 - # - 3.9 - # exclude: - # # Because ansible-test doesn't support python3.9 for Ansible 2.9 - # - ansible: stable-2.9 - # python: 3.9 - # - ansible: devel - # python: 2.6 - - # steps: - # - name: Check out code - # uses: actions/checkout@v3 - # - name: Perform integration testing with ansible-test - # uses: ansible-community/ansible-test-gh-action@release/v1 - # with: - # ansible-core-version: ${{ matrix.ansible }} - # python-version: 3.8 - # pre-test-cmd: >- - # mkdir -p tests/output/ - # touch tests/output/coverage - # target-python-version: ${{ matrix.python }} - # testing-type: integration - # test-deps: >- - # ansible.netcommon - # ansible.utils \ No newline at end of file +# Please consult the Readme for information on why we disabled integration tests temporarily. \ No newline at end of file diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 32ef1ed..185713b 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -123,4 +123,4 @@ releases: in this collection that have been made after the previous release. fragments: - 30-fix-lint-issues.yml - release_date: '2024-23-01' + release_date: '2024-01-23' diff --git a/plugins/modules/sap_hdbsql.py b/plugins/modules/sap_hdbsql.py index 68b4f29..349fa11 100644 --- a/plugins/modules/sap_hdbsql.py +++ b/plugins/modules/sap_hdbsql.py @@ -152,7 +152,7 @@ import csv from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.six import StringIO +from io import StringIO from ansible.module_utils.common.text.converters import to_native diff --git a/tests/sanity/ignore-2.17.txt b/tests/sanity/ignore-2.17.txt index d5f81aa..3883757 100644 --- a/tests/sanity/ignore-2.17.txt +++ b/tests/sanity/ignore-2.17.txt @@ -6,5 +6,4 @@ plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_user.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 -plugins/modules/sapcar_extract.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 -tests/unit/compat/mock.py pylint:use-yield-from # suggested construct does not work with Python 2 \ No newline at end of file +plugins/modules/sapcar_extract.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 \ No newline at end of file diff --git a/tests/sanity/ignore-2.18.txt b/tests/sanity/ignore-2.18.txt index d5f81aa..3883757 100644 --- a/tests/sanity/ignore-2.18.txt +++ b/tests/sanity/ignore-2.18.txt @@ -6,5 +6,4 @@ plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed u plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 plugins/modules/sap_user.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 -plugins/modules/sapcar_extract.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 -tests/unit/compat/mock.py pylint:use-yield-from # suggested construct does not work with Python 2 \ No newline at end of file +plugins/modules/sapcar_extract.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 \ No newline at end of file diff --git a/tests/sanity/ignore-2.19.txt b/tests/sanity/ignore-2.19.txt new file mode 100644 index 0000000..3883757 --- /dev/null +++ b/tests/sanity/ignore-2.19.txt @@ -0,0 +1,9 @@ +plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_user.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sapcar_extract.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 \ No newline at end of file diff --git a/tests/sanity/ignore-2.20.txt b/tests/sanity/ignore-2.20.txt new file mode 100644 index 0000000..3883757 --- /dev/null +++ b/tests/sanity/ignore-2.20.txt @@ -0,0 +1,9 @@ +plugins/modules/sap_pyrfc.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_company.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_control_exec.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_hdbsql.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_snote.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_system_facts.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_task_list_execute.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sap_user.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 +plugins/modules/sapcar_extract.py validate-modules:missing-gplv3-license # Licensed under Apache 2.0 \ No newline at end of file diff --git a/tests/unit/compat/mock.py b/tests/unit/compat/mock.py index bdbea94..318d240 100644 --- a/tests/unit/compat/mock.py +++ b/tests/unit/compat/mock.py @@ -51,8 +51,7 @@ def _iterate_read_data(read_data): # newline that our naive format() added data_as_list[-1] = data_as_list[-1][:-1] - for line in data_as_list: - yield line + yield from data_as_list def mock_open(mock=None, read_data=''): """ @@ -80,8 +79,7 @@ def _readline_side_effect(): if handle.readline.return_value is not None: while True: yield handle.readline.return_value - for line in _data: - yield line + yield from _data global file_spec if file_spec is None: diff --git a/tests/unit/mock/procenv.py b/tests/unit/mock/procenv.py index 5673863..b0bfadd 100644 --- a/tests/unit/mock/procenv.py +++ b/tests/unit/mock/procenv.py @@ -12,7 +12,6 @@ from contextlib import contextmanager from io import BytesIO, StringIO from ansible_collections.community.general.tests.unit.compat import unittest -from ansible.module_utils.six import PY3 from ansible.module_utils.common.text.converters import to_bytes @@ -24,11 +23,8 @@ def swap_stdin_and_argv(stdin_data='', argv_data=tuple()): real_stdin = sys.stdin real_argv = sys.argv - if PY3: - fake_stream = StringIO(stdin_data) - fake_stream.buffer = BytesIO(to_bytes(stdin_data)) - else: - fake_stream = BytesIO(to_bytes(stdin_data)) + fake_stream = StringIO(stdin_data) + fake_stream.buffer = BytesIO(to_bytes(stdin_data)) try: sys.stdin = fake_stream @@ -47,10 +43,7 @@ def swap_stdout(): """ old_stdout = sys.stdout - if PY3: - fake_stream = StringIO() - else: - fake_stream = BytesIO() + fake_stream = StringIO() try: sys.stdout = fake_stream diff --git a/tests/unit/mock/yaml_helper.py b/tests/unit/mock/yaml_helper.py index a646c02..e9fa894 100644 --- a/tests/unit/mock/yaml_helper.py +++ b/tests/unit/mock/yaml_helper.py @@ -21,17 +21,11 @@ def _loader(self, stream): def _dump_stream(self, obj, stream, dumper=None): """Dump to a py2-unicode or py3-string stream.""" - if PY3: - return yaml.dump(obj, stream, Dumper=dumper) - else: - return yaml.dump(obj, stream, Dumper=dumper, encoding=None) + return yaml.dump(obj, stream, Dumper=dumper) def _dump_string(self, obj, dumper=None): """Dump to a py2-unicode or py3-string""" - if PY3: - return yaml.dump(obj, Dumper=dumper) - else: - return yaml.dump(obj, Dumper=dumper, encoding=None) + return yaml.dump(obj, Dumper=dumper) def _dump_load_cycle(self, obj): # Each pass though a dump or load revs the 'generation' @@ -89,12 +83,8 @@ def _old_dump_load_cycle(self, obj): stream_obj_from_stream = io.StringIO() stream_obj_from_string = io.StringIO() - if PY3: - yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper) - yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper) - else: - yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper, encoding=None) - yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper, encoding=None) + yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper) + yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper) yaml_string_stream_obj_from_stream = stream_obj_from_stream.getvalue() yaml_string_stream_obj_from_string = stream_obj_from_string.getvalue() @@ -102,12 +92,8 @@ def _old_dump_load_cycle(self, obj): stream_obj_from_stream.seek(0) stream_obj_from_string.seek(0) - if PY3: - yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper) - yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper) - else: - yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper, encoding=None) - yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper, encoding=None) + yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper) + yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper) assert yaml_string == yaml_string_obj_from_stream assert yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string diff --git a/tests/unit/plugins/modules/test_sap_company.py b/tests/unit/plugins/modules/test_sap_company.py index b6b7dd1..46053ef 100644 --- a/tests/unit/plugins/modules/test_sap_company.py +++ b/tests/unit/plugins/modules/test_sap_company.py @@ -28,13 +28,13 @@ def define_rfc_connect(self, mocker): def test_without_required_parameters(self): """Failure must occurs when all parameters are missing""" with self.assertRaises(AnsibleFailJson): - set_module_args({}) - self.module.main() + with set_module_args({}): + self.module.main() def test_error_user_create(self): """test fail to create company""" - set_module_args({ + args = { "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", @@ -48,7 +48,7 @@ def test_error_user_create(self): "street": "test_street", "street_no": "1", "e_mail": "test@test.de", - }) + } with patch.object(self.module, 'call_rfc_method') as RAW: RAW.return_value = {'RETURN': [{'FIELD': '', 'ID': '01', 'LOG_MSG_NO': '000000', 'LOG_NO': '', 'MESSAGE': 'Something went wrong', 'MESSAGE_V1': 'ADMIN', @@ -56,13 +56,14 @@ def test_error_user_create(self): 'PARAMETER': '', 'ROW': 0, 'SYSTEM': '', 'TYPE': 'E'}]} with self.assertRaises(AnsibleFailJson) as result: - sap_company.main() + with set_module_args(args): + sap_company.main() self.assertEqual(result.exception.args[0]['msg'], 'Something went wrong') def test_success(self): """test execute company create success""" - set_module_args({ + args = { "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", @@ -76,7 +77,7 @@ def test_success(self): "street": "test_street", "street_no": "1", "e_mail": "test@test.de", - }) + } with patch.object(self.module, 'call_rfc_method') as RAW: RAW.return_value = {'RETURN': [{'FIELD': '', 'ID': '01', 'LOG_MSG_NO': '000000', 'LOG_NO': '', 'MESSAGE': 'Company address COMP_ID created', 'MESSAGE_V1': 'ADMIN', @@ -84,13 +85,14 @@ def test_success(self): 'PARAMETER': '', 'ROW': 0, 'SYSTEM': '', 'TYPE': 'S'}]} with self.assertRaises(AnsibleExitJson) as result: - sap_company.main() + with set_module_args(args): + sap_company.main() self.assertEqual(result.exception.args[0]['msg'], 'Company address COMP_ID created') def test_no_changes(self): """test execute company no changes""" - set_module_args({ + args = { "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", @@ -104,7 +106,7 @@ def test_no_changes(self): "street": "test_street", "street_no": "1", "e_mail": "test@test.de", - }) + } with patch.object(self.module, 'call_rfc_method') as RAW: RAW.return_value = {'RETURN': [{'FIELD': '', 'ID': '01', 'LOG_MSG_NO': '000000', 'LOG_NO': '', 'MESSAGE': 'Company address COMP_ID changed', 'MESSAGE_V1': 'ADMIN', @@ -112,19 +114,20 @@ def test_no_changes(self): 'PARAMETER': '', 'ROW': 0, 'SYSTEM': '', 'TYPE': 'S'}]} with self.assertRaises(AnsibleExitJson) as result: - sap_company.main() + with set_module_args(args): + sap_company.main() self.assertEqual(result.exception.args[0]['msg'], 'No changes where made.') def test_absent(self): """test execute company delete success""" - set_module_args({ + args = { "state": "absent", "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", "company_id": "Comp_ID", - }) + } with patch.object(self.module, 'call_rfc_method') as RAW: RAW.return_value = {'RETURN': [{'FIELD': '', 'ID': '01', 'LOG_MSG_NO': '000000', 'LOG_NO': '', 'MESSAGE': 'Company address COMP_ID deleted', 'MESSAGE_V1': 'ADMIN', @@ -132,5 +135,6 @@ def test_absent(self): 'PARAMETER': '', 'ROW': 0, 'SYSTEM': '', 'TYPE': 'S'}]} with self.assertRaises(AnsibleExitJson) as result: - sap_company.main() + with set_module_args(args): + sap_company.main() self.assertEqual(result.exception.args[0]['msg'], 'Company address COMP_ID deleted') diff --git a/tests/unit/plugins/modules/utils.py b/tests/unit/plugins/modules/utils.py index 5a8ec56..12532e7 100644 --- a/tests/unit/plugins/modules/utils.py +++ b/tests/unit/plugins/modules/utils.py @@ -4,6 +4,7 @@ __metaclass__ = type import json +import contextlib from ansible_collections.community.sap_libs.tests.unit.compat import unittest from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch @@ -11,14 +12,26 @@ from ansible.module_utils.common.text.converters import to_bytes +@contextlib.contextmanager def set_module_args(args): + """ + Context manager that sets module arguments for AnsibleModule + """ if '_ansible_remote_tmp' not in args: args['_ansible_remote_tmp'] = '/tmp' if '_ansible_keep_remote_files' not in args: args['_ansible_keep_remote_files'] = False - args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) - basic._ANSIBLE_ARGS = to_bytes(args) + try: + from ansible.module_utils.testing import patch_module_args + except ImportError: + # Before ansible-core 2.19, this was the way to go: + serialized_args = to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': args})) + with patch.object(basic, '_ANSIBLE_ARGS', serialized_args): + yield + else: + with patch_module_args(args): + yield class AnsibleExitJson(Exception): @@ -47,6 +60,5 @@ def setUp(self): self.mock_module.start() self.mock_sleep = patch('time.sleep') self.mock_sleep.start() - set_module_args({}) self.addCleanup(self.mock_module.stop) self.addCleanup(self.mock_sleep.stop) From c1f592f801af66a4c15b335bd730222eac361dae Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Tue, 23 Sep 2025 11:20:30 +0200 Subject: [PATCH 2/9] update remaining tests with new set_module_args --- README.md | 7 +- tests/unit/mock/yaml_helper.py | 1 - .../plugins/modules/test_sap_control_exec.py | 53 ++++++++------- tests/unit/plugins/modules/test_sap_hdbsql.py | 25 ++++--- tests/unit/plugins/modules/test_sap_pyrfc.py | 18 ++--- tests/unit/plugins/modules/test_sap_snote.py | 67 +++++++++++-------- .../plugins/modules/test_sap_system_facts.py | 20 ++++-- .../modules/test_sap_task_list_execute.py | 25 ++++--- tests/unit/plugins/modules/test_sap_user.py | 46 +++++++------ .../plugins/modules/test_sapcar_extract.py | 11 +-- 10 files changed, 156 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index 39d0c91..75f0b35 100644 --- a/README.md +++ b/README.md @@ -65,10 +65,11 @@ Every voice is important. If you have something on your mind, create an issue or ## Tested with Ansible and the following Python versions Tested Ansible versions: -- 2.13 -- 2.14 - 2.15 - 2.16 +- 2.17 +- 2.18 +- 2.19 - devel Tested Python versions: @@ -78,6 +79,8 @@ Tested Python versions: - 3.9 - 3.10 - 3.11 +- 3.12 +- 3.13 Due to SAP licensing and hardware requirements, integration tests are momentarily not feasible. The modules are tested manually against SAP systems until we found a solution or have some diff --git a/tests/unit/mock/yaml_helper.py b/tests/unit/mock/yaml_helper.py index e9fa894..0a62dad 100644 --- a/tests/unit/mock/yaml_helper.py +++ b/tests/unit/mock/yaml_helper.py @@ -6,7 +6,6 @@ import io import yaml -from ansible.module_utils.six import PY3 from ansible.parsing.yaml.loader import AnsibleLoader from ansible.parsing.yaml.dumper import AnsibleDumper diff --git a/tests/unit/plugins/modules/test_sap_control_exec.py b/tests/unit/plugins/modules/test_sap_control_exec.py index 272beaf..f185491 100644 --- a/tests/unit/plugins/modules/test_sap_control_exec.py +++ b/tests/unit/plugins/modules/test_sap_control_exec.py @@ -29,101 +29,108 @@ def define_rfc_connect(self, mocker): def test_without_required_parameters(self): """Failure must occurs when all parameters are missing""" with self.assertRaises(AnsibleFailJson): - set_module_args({}) - self.module.main() + with set_module_args({}): + self.module.main() def test_error_module_not_found(self): """tests fail module error""" - set_module_args({ + args = { "hostname": "192.168.8.15", "sysnr": "01", "function": "GetProcessList" - }) + } with self.assertRaises(AnsibleFailJson) as result: self.module.HAS_SUDS_LIBRARY = False self.module.SUDS_LIBRARY_IMPORT_ERROR = 'Module not found' - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['exception'], 'Module not found') def test_error_connection(self): """tests fail module exception""" - set_module_args({ + args = { "hostname": "192.168.8.15", "sysnr": "01", "function": "GetProcessList" - }) + } with self.assertRaises(AnsibleFailJson) as result: self.module.Client.side_effect = Mock(side_effect=Exception('Test')) - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'Something went wrong connecting to the SAPCONTROL SOAP API.') def test_error_port_sysnr(self): """tests fail multi provide parameters""" - set_module_args({ + args = { "hostname": "192.168.8.15", "sysnr": "01", "port": "50113", "function": "GetProcessList" - }) + } with self.assertRaises(AnsibleFailJson) as result: - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'parameters are mutually exclusive: sysnr|port') def test_error_missing_force(self): """tests fail missing force""" - set_module_args({ + args = { "hostname": "192.168.8.15", "sysnr": "01", "function": "Stop" - }) + } with self.assertRaises(AnsibleFailJson) as result: - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'Stop function requires force: True') def test_success_sysnr(self): """test success with sysnr""" - set_module_args({ + args = { "hostname": "192.168.8.15", "sysnr": "01", "function": "GetProcessList" - }) + } with patch.object(self.module, 'recursive_dict') as ret: ret.return_value = {'item': [{'name': 'hdbdaemon', 'value': '1'}]} with self.assertRaises(AnsibleExitJson) as result: - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['out'], [{'item': [{'name': 'hdbdaemon', 'value': '1'}]}]) def test_success_port(self): """test success with port""" - set_module_args({ + args = { "hostname": "192.168.8.15", "port": "50113", "function": "GetProcessList" - }) + } with patch.object(self.module, 'recursive_dict') as ret: ret.return_value = {'item': [{'name': 'hdbdaemon', 'value': '1'}]} with self.assertRaises(AnsibleExitJson) as result: - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['out'], [{'item': [{'name': 'hdbdaemon', 'value': '1'}]}]) def test_success_string(self): """test success with sysnr""" - set_module_args({ + args = { "hostname": "192.168.8.15", "sysnr": "01", "function": "ParameterValue", "parameter": "ztta/short_area" - }) + } with patch.object(self.module, 'connection') as ret: ret.return_value = '1600000' with self.assertRaises(AnsibleExitJson) as result: - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['out'], ['1600000']) diff --git a/tests/unit/plugins/modules/test_sap_hdbsql.py b/tests/unit/plugins/modules/test_sap_hdbsql.py index 2232273..c54667e 100644 --- a/tests/unit/plugins/modules/test_sap_hdbsql.py +++ b/tests/unit/plugins/modules/test_sap_hdbsql.py @@ -40,12 +40,12 @@ def tearDown(self): def test_without_required_parameters(self): """Failure must occurs when all parameters are missing.""" with self.assertRaises(AnsibleFailJson): - set_module_args({}) - self.module.main() + with set_module_args({}): + self.module.main() def test_sap_hdbsql(self): """Check that result is processed.""" - set_module_args({ + args = { 'sid': "HDB", 'instance': "01", 'encrypted': False, @@ -54,11 +54,12 @@ def test_sap_hdbsql(self): 'password': "1234Qwer", 'database': "HDB", 'query': "SELECT * FROM users;" - }) + } with patch.object(basic.AnsibleModule, 'run_command') as run_command: run_command.return_value = 0, 'username,name\n testuser,test user \n myuser, my user \n', '' with self.assertRaises(AnsibleExitJson) as result: - sap_hdbsql.main() + with set_module_args(args): + sap_hdbsql.main() self.assertEqual(result.exception.args[0]['query_result'], [[ {'username': 'testuser', 'name': 'test user'}, {'username': 'myuser', 'name': 'my user'}, @@ -67,7 +68,7 @@ def test_sap_hdbsql(self): def test_hana_userstore_query(self): """Check that result is processed with userstore.""" - set_module_args({ + args = { 'sid': "HDB", 'instance': "01", 'encrypted': False, @@ -76,11 +77,12 @@ def test_hana_userstore_query(self): 'userstore': True, 'database': "HDB", 'query': "SELECT * FROM users;" - }) + } with patch.object(basic.AnsibleModule, 'run_command') as run_command: run_command.return_value = 0, 'username,name\n testuser,test user \n myuser, my user \n', '' with self.assertRaises(AnsibleExitJson) as result: - sap_hdbsql.main() + with set_module_args(args): + sap_hdbsql.main() self.assertEqual(result.exception.args[0]['query_result'], [[ {'username': 'testuser', 'name': 'test user'}, {'username': 'myuser', 'name': 'my user'}, @@ -90,7 +92,7 @@ def test_hana_userstore_query(self): def test_hana_failed_no_passwd(self): """Check that result is failed with no password.""" with self.assertRaises(AnsibleFailJson): - set_module_args({ + args = { 'sid': "HDB", 'instance': "01", 'encrypted': False, @@ -98,5 +100,6 @@ def test_hana_failed_no_passwd(self): 'user': "SYSTEM", 'database': "HDB", 'query': "SELECT * FROM users;" - }) - self.module.main() + } + with set_module_args(args): + self.module.main() diff --git a/tests/unit/plugins/modules/test_sap_pyrfc.py b/tests/unit/plugins/modules/test_sap_pyrfc.py index 2e206b3..6783b54 100644 --- a/tests/unit/plugins/modules/test_sap_pyrfc.py +++ b/tests/unit/plugins/modules/test_sap_pyrfc.py @@ -33,13 +33,13 @@ def tearDown(self): def test_without_required_parameters(self): """Failure must occurs when all parameters are missing""" with self.assertRaises(AnsibleFailJson): - set_module_args({}) - self.module.main() + with set_module_args({}): + self.module.main() def test_error_module_not_found(self): """tests fail module error""" - set_module_args({ + args = { "function": "STFC_CONNECTION", "parameters": {"REQUTEXT": "Hello SAP!"}, "connection": {"ashost": "s4hana.poc.cloud", @@ -48,18 +48,19 @@ def test_error_module_not_found(self): "user": "DDIC", "passwd": "Password1", "lang": "EN"} - }) + } with self.assertRaises(AnsibleFailJson) as result: self.module.HAS_PYRFC_LIBRARY = False self.module.PYRFC_LIBRARY_IMPORT_ERROR = 'Module not found' - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual( result.exception.args[0]['exception'], 'Module not found') def test_success_communication(self): """tests success""" - set_module_args({ + args = { "function": "STFC_CONNECTION", "parameters": {"REQUTEXT": "Hello SAP!"}, "connection": {"ashost": "s4hana.poc.cloud", @@ -68,9 +69,10 @@ def test_success_communication(self): "user": "DDIC", "passwd": "Password1", "lang": "EN"} - }) + } with patch.object(self.module, 'get_connection') as patch_call: patch_call.call.return_value = 'Patched' with self.assertRaises(AnsibleExitJson) as result: - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['changed'], True) diff --git a/tests/unit/plugins/modules/test_sap_snote.py b/tests/unit/plugins/modules/test_sap_snote.py index 95ce6df..f7d2795 100644 --- a/tests/unit/plugins/modules/test_sap_snote.py +++ b/tests/unit/plugins/modules/test_sap_snote.py @@ -28,128 +28,135 @@ def define_rfc_connect(self, mocker): def test_without_required_parameters(self): """Failure must occurs when all parameters are missing""" with self.assertRaises(AnsibleFailJson): - set_module_args({}) - self.module.main() + with set_module_args({}): + self.module.main() def test_error_module_not_found(self): """tests fail module error""" - set_module_args({ + args = { "conn_username": "ADMIN", "conn_password": "Test1234", "host": "10.1.8.9", "snote_path": "/user/sap/trans/temp/000123456.txt" - }) + } with self.assertRaises(AnsibleFailJson) as result: self.module.HAS_PYRFC_LIBRARY = False self.module.ANOTHER_LIBRARY_IMPORT_ERROR = 'Module not found' - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['exception'], 'Module not found') def test_error_connection(self): """tests fail module error""" - set_module_args({ + args = { "conn_username": "ADMIN", "conn_password": "Test1234", "host": "10.1.8.9", "snote_path": "/user/sap/trans/temp/000123456.txt" - }) + } with self.assertRaises(AnsibleFailJson) as result: self.module.Connection.side_effect = Mock(side_effect=Exception('Test')) - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'Something went wrong connecting to the SAP system.') def test_error_wrong_path(self): """tests fail wrong path extension""" - set_module_args({ + args = { "conn_username": "ADMIN", "conn_password": "Test1234", "host": "10.1.8.9", "snote_path": "/user/sap/trans/temp/000123456_00.tx" - }) + } with self.assertRaises(AnsibleFailJson) as result: - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'The path must include the extracted snote file and ends with txt.') def test_error_wrong_user(self): """tests fail wrong path extension""" - set_module_args({ + args = { "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", "snote_path": "/user/sap/trans/temp/000123456_00.tx" - }) + } with self.assertRaises(AnsibleFailJson) as result: - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'User C(DDIC) or C(SAP*) not allowed for this operation.') def test_success_absent(self): """test absent execute snote""" - set_module_args({ + args = { "conn_username": "ADMIN", "conn_password": "Test1234", "host": "10.1.8.9", "state": "absent", "snote_path": "/user/sap/trans/temp/000123456.txt" - }) + } with patch.object(self.module, 'call_rfc_method') as call: call.return_value = {'EV_RC': 0} with self.assertRaises(AnsibleExitJson) as result: with patch.object(self.module, 'check_implementation') as check: check.side_effect = [True, False] - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'SNOTE "000123456" deimplemented.') def test_success_absent_snot_only(self): """test absent execute snote""" - set_module_args({ + args = { "conn_username": "ADMIN", "conn_password": "Test1234", "host": "10.1.8.9", "state": "absent", "snote": "000123456" - }) + } with patch.object(self.module, 'call_rfc_method') as call: call.return_value = {'EV_RC': 0} with self.assertRaises(AnsibleExitJson) as result: with patch.object(self.module, 'check_implementation') as check: check.side_effect = [True, False] - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'SNOTE "000123456" deimplemented.') def test_nothing_to_do(self): """test nothing to do""" - set_module_args({ + args = { "conn_username": "ADMIN", "conn_password": "Test1234", "host": "10.1.8.9", "state": "present", "snote_path": "/user/sap/trans/temp/000123456.txt" - }) + } with patch.object(self.module, 'check_implementation') as check: check.return_value = True with self.assertRaises(AnsibleExitJson) as result: - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'Nothing to do.') def test_success_present_with_copy(self): """test present execute snote""" - set_module_args({ + args = { "conn_username": "ADMIN", "conn_password": "Test1234", "host": "10.1.8.9", "state": "present", "snote_path": "/user/sap/trans/temp/000123456.txt" - }) + } with patch.object(self.module, 'call_rfc_method') as call: call.return_value = {'EV_RC': 0} with self.assertRaises(AnsibleExitJson) as result: @@ -157,19 +164,20 @@ def test_success_present_with_copy(self): check.side_effect = [False, True] with patch.object(self.module, 'call_rfc_method') as callrfc: callrfc.side_effect = [{'EV_RC': 0}, {'EV_RC': 0}, {'ET_MANUAL_ACTIVITIES': ''}] - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'SNOTE "000123456" implemented.') def test_success_present_implement_only(self): """test present implement snote""" - set_module_args({ + args = { "conn_username": "ADMIN", "conn_password": "Test1234", "host": "10.1.8.9", "state": "present", "snote": "000123456" - }) + } with patch.object(self.module, 'call_rfc_method') as call: call.return_value = {'EV_RC': 0} with self.assertRaises(AnsibleExitJson) as result: @@ -177,5 +185,6 @@ def test_success_present_implement_only(self): check.side_effect = [False, True] with patch.object(self.module, 'call_rfc_method') as callrfc: callrfc.side_effect = [{'EV_RC': 0}, {'ET_MANUAL_ACTIVITIES': ''}] - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'SNOTE "000123456" implemented.') diff --git a/tests/unit/plugins/modules/test_sap_system_facts.py b/tests/unit/plugins/modules/test_sap_system_facts.py index 4c88234..9d969ee 100644 --- a/tests/unit/plugins/modules/test_sap_system_facts.py +++ b/tests/unit/plugins/modules/test_sap_system_facts.py @@ -8,7 +8,7 @@ __metaclass__ = type from ansible_collections.community.sap_libs.plugins.modules import sap_system_facts -from ansible_collections.community.sap_libs.tests.unit.plugins.modules.utils import AnsibleExitJson, ModuleTestCase +from ansible_collections.community.sap_libs.tests.unit.plugins.modules.utils import AnsibleExitJson, ModuleTestCase, set_module_args from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch from ansible.module_utils import basic @@ -36,7 +36,8 @@ def tearDown(self): def test_no_systems_available(self): """No SAP Systems""" with self.assertRaises(AnsibleExitJson) as result: - self.module.main() + with set_module_args({}): + self.module.main() self.assertEqual(result.exception.args[0]['ansible_facts'], {}) def test_sap_system_facts_all(self): @@ -51,7 +52,8 @@ def test_sap_system_facts_all(self): get_nw_nr.return_value = [{"InstanceType": "NW", "NR": "00", "SID": "ABC", "TYPE": "ASCS"}, {"InstanceType": "NW", "NR": "01", "SID": "ABC", "TYPE": "PAS"}] with self.assertRaises(AnsibleExitJson) as result: - self.module.main() + with set_module_args({}): + self.module.main() self.assertEqual(result.exception.args[0]['ansible_facts'], {'sap': [{"InstanceType": "HANA", "NR": "01", "SID": "HDB", "TYPE": "HDB"}, {"InstanceType": "NW", "NR": "00", "SID": "ABC", "TYPE": "ASCS"}, {"InstanceType": "NW", "NR": "01", "SID": "ABC", "TYPE": "PAS"}]}) @@ -65,7 +67,8 @@ def test_sap_system_facts_command_hana(self): with patch.object(basic.AnsibleModule, 'run_command') as run_command: run_command.return_value = [0, '', ''] with self.assertRaises(AnsibleExitJson) as result: - self.module.main() + with set_module_args({}): + self.module.main() self.assertEqual(result.exception.args[0]['ansible_facts'], {'sap': [{"InstanceType": "HANA", "NR": "01", "SID": "HDB", "TYPE": "HDB"}]}) def test_sap_system_facts_pas_nw(self): @@ -77,7 +80,8 @@ def test_sap_system_facts_pas_nw(self): with patch.object(basic.AnsibleModule, 'run_command') as run_command: run_command.return_value = [0, 'SAP\nINSTANCE_NAME, Attribute, D00\nSAP', ''] with self.assertRaises(AnsibleExitJson) as result: - self.module.main() + with set_module_args({}): + self.module.main() self.assertEqual(result.exception.args[0]['ansible_facts'], {'sap': [{'InstanceType': 'NW', 'NR': '00', 'SID': 'ABC', 'TYPE': 'PAS'}]}) def test_sap_system_facts_future_nw(self): @@ -89,7 +93,8 @@ def test_sap_system_facts_future_nw(self): with patch.object(basic.AnsibleModule, 'run_command') as run_command: run_command.return_value = [0, 'SAP\nINSTANCE_NAME, Attribute, XY00\nSAP', ''] with self.assertRaises(AnsibleExitJson) as result: - self.module.main() + with set_module_args({}): + self.module.main() self.assertEqual(result.exception.args[0]['ansible_facts'], {'sap': [{'InstanceType': 'NW', 'NR': '00', 'SID': 'ABC', 'TYPE': 'XXX'}]}) def test_sap_system_facts_wd_nw(self): @@ -101,5 +106,6 @@ def test_sap_system_facts_wd_nw(self): with patch.object(basic.AnsibleModule, 'run_command') as run_command: run_command.return_value = [0, 'SAP\nINSTANCE_NAME, Attribute, WD80\nSAP', ''] with self.assertRaises(AnsibleExitJson) as result: - self.module.main() + with set_module_args({}): + self.module.main() self.assertEqual(result.exception.args[0]['ansible_facts'], {'sap': [{'InstanceType': 'NW', 'NR': '80', 'SID': 'ABC', 'TYPE': 'WebDisp'}]}) diff --git a/tests/unit/plugins/modules/test_sap_task_list_execute.py b/tests/unit/plugins/modules/test_sap_task_list_execute.py index a7ac42e..5f649a3 100644 --- a/tests/unit/plugins/modules/test_sap_task_list_execute.py +++ b/tests/unit/plugins/modules/test_sap_task_list_execute.py @@ -30,34 +30,35 @@ def define_rfc_connect(self, mocker): def test_without_required_parameters(self): """Failure must occurs when all parameters are missing""" with self.assertRaises(AnsibleFailJson): - set_module_args({}) - self.module.main() + with set_module_args({}): + self.module.main() def test_error_no_task_list(self): """tests fail to exec task list""" - set_module_args({ + args = { "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", "task_to_execute": "SAP_BASIS_SSL_CHECK" - }) + } with patch.object(self.module, 'Connection') as conn: conn.return_value = '' with self.assertRaises(AnsibleFailJson) as result: - self.module.main() + with set_module_args(args): + self.module.main() self.assertEqual(result.exception.args[0]['msg'], 'The task list does not exist.') def test_success(self): """test execute task list success""" - set_module_args({ + args = { "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", "task_to_execute": "SAP_BASIS_SSL_CHECK" - }) + } with patch.object(self.module, 'xml_to_dict') as XML: XML.return_value = {'item': [{'TASK': {'CHECK_STATUS_DESCR': 'Check successfully', 'STATUS_DESCR': 'Executed successfully', 'TASKNAME': 'CL_STCT_CHECK_SEC_CRYPTO', @@ -66,7 +67,8 @@ def test_success(self): 'ACTION_MAINTAIN': None}}]} with self.assertRaises(AnsibleExitJson) as result: - sap_task_list_execute.main() + with set_module_args(args): + sap_task_list_execute.main() self.assertEqual(result.exception.args[0]['out'], {'item': [{'TASK': {'CHECK_STATUS_DESCR': 'Check successfully', 'STATUS_DESCR': 'Executed successfully', 'TASKNAME': 'CL_STCT_CHECK_SEC_CRYPTO', 'LNR': '1', 'DESCRIPTION': 'Check SAP Cryptographic Library', 'DOCU_EXIST': 'X', @@ -76,14 +78,15 @@ def test_success(self): def test_success_no_log(self): """test execute task list success without logs""" - set_module_args({ + args = { "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", "task_to_execute": "SAP_BASIS_SSL_CHECK" - }) + } with patch.object(self.module, 'xml_to_dict') as XML: XML.return_value = "No logs available." with self.assertRaises(AnsibleExitJson) as result: - sap_task_list_execute.main() + with set_module_args(args): + sap_task_list_execute.main() self.assertEqual(result.exception.args[0]['out'], 'No logs available.') diff --git a/tests/unit/plugins/modules/test_sap_user.py b/tests/unit/plugins/modules/test_sap_user.py index 51f57d9..02aa788 100644 --- a/tests/unit/plugins/modules/test_sap_user.py +++ b/tests/unit/plugins/modules/test_sap_user.py @@ -28,13 +28,13 @@ def define_rfc_connect(self, mocker): def test_without_required_parameters(self): """Failure must occurs when all parameters are missing""" with self.assertRaises(AnsibleFailJson): - set_module_args({}) - self.module.main() + with set_module_args({}): + self.module.main() def test_error_user_create(self): """test fail to create user""" - set_module_args({ + args = { "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", @@ -45,7 +45,7 @@ def test_error_user_create(self): "password": "Test123456", "useralias": "ADMIN", "company": "DEFAULT_COMPANY" - }) + } with patch.object(self.module, 'check_user') as check: check.return_value = False @@ -57,13 +57,14 @@ def test_error_user_create(self): 'PARAMETER': '', 'ROW': 0, 'SYSTEM': '', 'TYPE': 'E'}]} with self.assertRaises(AnsibleFailJson) as result: - sap_user.main() + with set_module_args(args): + sap_user.main() self.assertEqual(result.exception.args[0]['msg'], 'Something went wrong') def test_success(self): """test execute user create success""" - set_module_args({ + args = { "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", @@ -74,7 +75,7 @@ def test_success(self): "password": "Test123456", "useralias": "ADMIN", "company": "DEFAULT_COMPANY" - }) + } with patch.object(self.module, 'check_user') as check: check.return_value = False @@ -85,13 +86,14 @@ def test_success(self): 'PARAMETER': '', 'ROW': 0, 'SYSTEM': '', 'TYPE': 'S'}]} with self.assertRaises(AnsibleExitJson) as result: - sap_user.main() + with set_module_args(args): + sap_user.main() self.assertEqual(result.exception.args[0]['msg'], 'User ADMIN created') def test_no_changes(self): """test execute user no changes""" - set_module_args({ + args = { "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", @@ -102,7 +104,7 @@ def test_no_changes(self): "password": "Test123456", "useralias": "ADMIN", "company": "DEFAULT_COMPANY" - }) + } with patch.object(self.module, 'check_user') as check: check.return_value = True @@ -116,19 +118,20 @@ def test_no_changes(self): DETAIL.return_value = True with self.assertRaises(AnsibleExitJson) as result: - sap_user.main() + with set_module_args(args): + sap_user.main() self.assertEqual(result.exception.args[0]['msg'], 'No changes where made.') def test_absent(self): """test execute user delete success""" - set_module_args({ + args = { "state": "absent", "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", "username": "ADMIN", - }) + } with patch.object(self.module, 'check_user') as check: check.return_value = True @@ -139,19 +142,20 @@ def test_absent(self): 'PARAMETER': '', 'ROW': 0, 'SYSTEM': '', 'TYPE': 'S'}]} with self.assertRaises(AnsibleExitJson) as result: - sap_user.main() + with set_module_args(args): + sap_user.main() self.assertEqual(result.exception.args[0]['msg'], 'User ADMIN deleted') def test_lock(self): """test execute user lock success""" - set_module_args({ + args = { "state": "lock", "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", "username": "ADMIN", - }) + } with patch.object(self.module, 'check_user') as check: check.return_value = True @@ -162,19 +166,20 @@ def test_lock(self): 'PARAMETER': '', 'ROW': 0, 'SYSTEM': '', 'TYPE': 'S'}]} with self.assertRaises(AnsibleExitJson) as result: - sap_user.main() + with set_module_args(args): + sap_user.main() self.assertEqual(result.exception.args[0]['msg'], 'User ADMIN locked') def test_unlock(self): """test execute user lock success""" - set_module_args({ + args = { "state": "lock", "conn_username": "DDIC", "conn_password": "Test1234", "host": "10.1.8.9", "username": "ADMIN", - }) + } with patch.object(self.module, 'check_user') as check: check.return_value = True @@ -185,5 +190,6 @@ def test_unlock(self): 'PARAMETER': '', 'ROW': 0, 'SYSTEM': '', 'TYPE': 'S'}]} with self.assertRaises(AnsibleExitJson) as result: - sap_user.main() + with set_module_args(args): + sap_user.main() self.assertEqual(result.exception.args[0]['msg'], 'User ADMIN unlocked') diff --git a/tests/unit/plugins/modules/test_sapcar_extract.py b/tests/unit/plugins/modules/test_sapcar_extract.py index 77695e4..d5f8bb6 100644 --- a/tests/unit/plugins/modules/test_sapcar_extract.py +++ b/tests/unit/plugins/modules/test_sapcar_extract.py @@ -35,19 +35,20 @@ def tearDown(self): def test_without_required_parameters(self): """Failure must occurs when all parameters are missing.""" with self.assertRaises(AnsibleFailJson): - set_module_args({}) - self.module.main() + with set_module_args({}): + self.module.main() def test_sapcar_extract(self): """Check that result is changed.""" - set_module_args({ + args = { 'path': "/tmp/HANA_CLIENT_REV2_00_053_00_LINUX_X86_64.SAR", 'dest': "/tmp/test2", 'binary_path': "/tmp/sapcar" - }) + } with patch.object(basic.AnsibleModule, 'run_command') as run_command: run_command.return_value = 0, '', '' # successful execution, no output with self.assertRaises(AnsibleExitJson) as result: - sapcar_extract.main() + with set_module_args(args): + sapcar_extract.main() self.assertTrue(result.exception.args[0]['changed']) self.assertEqual(run_command.call_count, 1) From c14c821cbf96694ef3b9fc6e63f5b20ee4137178 Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Tue, 23 Sep 2025 14:30:31 +0200 Subject: [PATCH 3/9] updated workflow matrix --- .github/workflows/ansible-test.yml | 46 ++++++++++++++---------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml index b3b6fda..bd2be4e 100644 --- a/.github/workflows/ansible-test.yml +++ b/.github/workflows/ansible-test.yml @@ -17,23 +17,19 @@ env: jobs: sanity: + runs-on: ubuntu-latest name: Sanity (Ⓐ${{ matrix.ansible }}) strategy: + fail-fast: false # Disabled so we can see all failed combinations. matrix: ansible: - - stable-2.15 - - stable-2.16 - - stable-2.17 - - stable-2.18 - - stable-2.19 - - devel + - 'stable-2.16' + - 'stable-2.17' + - 'stable-2.18' + - 'stable-2.19' + - 'devel' # Currently 2.20 - runs-on: >- - ${{ contains(fromJson( - '["stable-2.13", "stable-2.14"]' - ), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }} steps: - - name: Check out code uses: actions/checkout@v4 @@ -42,28 +38,27 @@ jobs: with: ansible-core-version: ${{ matrix.ansible }} testing-type: sanity - units: - runs-on: >- - ${{ contains(fromJson( - '["stable-2.13", "stable-2.14"]' - ), matrix.ansible) && 'ubuntu-20.04' || 'ubuntu-latest' }} + runs-on: ubuntu-latest name: Units (Ⓐ${{ matrix.ansible }}+py${{ matrix.python }}) strategy: - # As soon as the first unit test fails, cancel the others to free up the CI queue - fail-fast: true + fail-fast: false # Disabled so we can see all failed combinations. matrix: ansible: - - stable-2.15 - - stable-2.16 - - stable-2.17 - - stable-2.18 - - stable-2.19 - - devel + - 'stable-2.16' + - 'stable-2.17' + - 'stable-2.18' + - 'stable-2.19' + - 'devel' # Currently 2.20 + python: + - '3.9' + - '3.10' + - '3.11' + - '3.12' + - '3.13' steps: - - name: Check out code uses: actions/checkout@v4 @@ -71,6 +66,7 @@ jobs: uses: ansible-community/ansible-test-gh-action@release/v1 with: ansible-core-version: ${{ matrix.ansible }} + target-python-version: ${{ matrix.python }} testing-type: units # Please consult the Readme for information on why we disabled integration tests temporarily. \ No newline at end of file From d4acab9a66e0ea59fa5b4f98fb071df778a1dda1 Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Tue, 23 Sep 2025 14:33:35 +0200 Subject: [PATCH 4/9] exclude python 3.13 for 2.16 and 2.17 --- .github/workflows/ansible-test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml index bd2be4e..38e4c50 100644 --- a/.github/workflows/ansible-test.yml +++ b/.github/workflows/ansible-test.yml @@ -57,6 +57,12 @@ jobs: - '3.11' - '3.12' - '3.13' + exclude: + # Exclude new python versions for older ansible versions + - ansible: 'stable-2.16' + python: '3.13' + - ansible: 'stable-2.17' + python: '3.13' steps: - name: Check out code From 5fa4a2e67595eeeff38178deaa4a412d6073fbf8 Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Tue, 23 Sep 2025 15:45:09 +0200 Subject: [PATCH 5/9] add 2.14 and 2.15 with exclusions for 3.12 and 3.13 --- .github/workflows/ansible-test.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml index 38e4c50..658d6b9 100644 --- a/.github/workflows/ansible-test.yml +++ b/.github/workflows/ansible-test.yml @@ -23,6 +23,8 @@ jobs: fail-fast: false # Disabled so we can see all failed combinations. matrix: ansible: + - 'stable-2.14' + - 'stable-2.15' - 'stable-2.16' - 'stable-2.17' - 'stable-2.18' @@ -46,6 +48,8 @@ jobs: fail-fast: false # Disabled so we can see all failed combinations. matrix: ansible: + - 'stable-2.14' + - 'stable-2.15' - 'stable-2.16' - 'stable-2.17' - 'stable-2.18' @@ -58,9 +62,23 @@ jobs: - '3.12' - '3.13' exclude: - # Exclude new python versions for older ansible versions + # Exclusions for stable-2.14 + - ansible: 'stable-2.14' + python: '3.12' + - ansible: 'stable-2.14' + python: '3.13' + + # Exclusions for stable-2.15 + - ansible: 'stable-2.15' + python: '3.12' + - ansible: 'stable-2.15' + python: '3.13' + + # Exclusions for stable-2.16 - ansible: 'stable-2.16' python: '3.13' + + # Exclusions for stable-2.17 - ansible: 'stable-2.17' python: '3.13' From 26240a1682ab299701719513daa0853f9e35d17c Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Wed, 24 Sep 2025 10:46:23 +0200 Subject: [PATCH 6/9] remove python 2 support and unused packages from unit folder --- README.md | 5 +- tests/unit/compat/__init__.py | 0 tests/unit/compat/builtins.py | 24 ---- tests/unit/compat/mock.py | 107 ----------------- tests/unit/compat/unittest.py | 38 ------ tests/unit/mock/__init__.py | 0 tests/unit/mock/loader.py | 103 ---------------- tests/unit/mock/path.py | 8 -- tests/unit/mock/procenv.py | 69 ----------- tests/unit/mock/vault_helper.py | 27 ----- tests/unit/mock/yaml_helper.py | 111 ------------------ .../unit/plugins/modules/test_sap_company.py | 2 +- .../plugins/modules/test_sap_control_exec.py | 2 +- tests/unit/plugins/modules/test_sap_hdbsql.py | 2 +- tests/unit/plugins/modules/test_sap_pyrfc.py | 2 +- tests/unit/plugins/modules/test_sap_snote.py | 2 +- .../plugins/modules/test_sap_system_facts.py | 2 +- .../modules/test_sap_task_list_execute.py | 2 +- tests/unit/plugins/modules/test_sap_user.py | 2 +- .../plugins/modules/test_sapcar_extract.py | 2 +- tests/unit/plugins/modules/utils.py | 6 +- tests/unit/requirements.txt | 7 +- 22 files changed, 16 insertions(+), 507 deletions(-) delete mode 100644 tests/unit/compat/__init__.py delete mode 100644 tests/unit/compat/builtins.py delete mode 100644 tests/unit/compat/mock.py delete mode 100644 tests/unit/compat/unittest.py delete mode 100644 tests/unit/mock/__init__.py delete mode 100644 tests/unit/mock/loader.py delete mode 100644 tests/unit/mock/path.py delete mode 100644 tests/unit/mock/procenv.py delete mode 100644 tests/unit/mock/vault_helper.py delete mode 100644 tests/unit/mock/yaml_helper.py diff --git a/README.md b/README.md index 75f0b35..9b6bfde 100644 --- a/README.md +++ b/README.md @@ -73,15 +73,14 @@ Tested Ansible versions: - devel Tested Python versions: -- 3.6 -- 3.7 -- 3.8 - 3.9 - 3.10 - 3.11 - 3.12 - 3.13 +Support for Python 2 has been dropped. The minimum required Python version is 3.9. + Due to SAP licensing and hardware requirements, integration tests are momentarily not feasible. The modules are tested manually against SAP systems until we found a solution or have some modules where we are able to execute integration test we decided to disable these tests. diff --git a/tests/unit/compat/__init__.py b/tests/unit/compat/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/compat/builtins.py b/tests/unit/compat/builtins.py deleted file mode 100644 index ced77f8..0000000 --- a/tests/unit/compat/builtins.py +++ /dev/null @@ -1,24 +0,0 @@ -# (c) 2014, Toshio Kuratomi -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -# -# Compat for python2.7 -# diff --git a/tests/unit/compat/mock.py b/tests/unit/compat/mock.py deleted file mode 100644 index 318d240..0000000 --- a/tests/unit/compat/mock.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (c) 2014, Toshio Kuratomi -# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) -# SPDX-License-Identifier: GPL-3.0-or-later - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -''' -Compat module for Python3.x's unittest.mock module -''' -import sys - -# Python 2.7 - -# Note: Could use the pypi mock library on python3.x as well as python2.x. It -# is the same as the python3 stdlib mock library - -try: - # Allow wildcard import because we really do want to import all of mock's - # symbols into this compat shim - # pylint: disable=wildcard-import,unused-wildcard-import - from unittest.mock import * # noqa: F401, pylint: disable=unused-import -except ImportError: - # Python 2 - # pylint: disable=wildcard-import,unused-wildcard-import - try: - from mock import * # noqa: F401, pylint: disable=unused-import - except ImportError: - print('You need the mock library installed on python2.x to run tests') - - -# Prior to 3.4.4, mock_open cannot handle binary read_data -if sys.version_info >= (3,) and sys.version_info < (3, 4, 4): - file_spec = None - - def _iterate_read_data(read_data): - # Helper for mock_open: - # Retrieve lines from read_data via a generator so that separate calls to - # readline, read, and readlines are properly interleaved - sep = b'\n' if isinstance(read_data, bytes) else '\n' - data_as_list = [l + sep for l in read_data.split(sep)] - - if data_as_list[-1] == sep: - # If the last line ended in a newline, the list comprehension will have an - # extra entry that's just a newline. Remove this. - data_as_list = data_as_list[:-1] - else: - # If there wasn't an extra newline by itself, then the file being - # emulated doesn't have a newline to end the last line remove the - # newline that our naive format() added - data_as_list[-1] = data_as_list[-1][:-1] - - yield from data_as_list - - def mock_open(mock=None, read_data=''): - """ - A helper function to create a mock to replace the use of `open`. It works - for `open` called directly or used as a context manager. - - The `mock` argument is the mock object to configure. If `None` (the - default) then a `MagicMock` will be created for you, with the API limited - to methods or attributes available on standard file handles. - - `read_data` is a string for the `read` methoddline`, and `readlines` of the - file handle to return. This is an empty string by default. - """ - def _readlines_side_effect(*args, **kwargs): - if handle.readlines.return_value is not None: - return handle.readlines.return_value - return list(_data) - - def _read_side_effect(*args, **kwargs): - if handle.read.return_value is not None: - return handle.read.return_value - return type(read_data)().join(_data) - - def _readline_side_effect(): - if handle.readline.return_value is not None: - while True: - yield handle.readline.return_value - yield from _data - - global file_spec - if file_spec is None: - import _io - file_spec = list(set(dir(_io.TextIOWrapper)).union(set(dir(_io.BytesIO)))) - - if mock is None: - mock = MagicMock(name='open', spec=open) - - handle = MagicMock(spec=file_spec) - handle.__enter__.return_value = handle - - _data = _iterate_read_data(read_data) - - handle.write.return_value = None - handle.read.return_value = None - handle.readline.return_value = None - handle.readlines.return_value = None - - handle.read.side_effect = _read_side_effect - handle.readline.side_effect = _readline_side_effect() - handle.readlines.side_effect = _readlines_side_effect - - mock.return_value = handle - return mock diff --git a/tests/unit/compat/unittest.py b/tests/unit/compat/unittest.py deleted file mode 100644 index 98f08ad..0000000 --- a/tests/unit/compat/unittest.py +++ /dev/null @@ -1,38 +0,0 @@ -# (c) 2014, Toshio Kuratomi -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -''' -Compat module for Python2.7's unittest module -''' - -import sys - -# Allow wildcard import because we really do want to import all of -# unittests's symbols into this compat shim -# pylint: disable=wildcard-import,unused-wildcard-import -if sys.version_info < (2, 7): - try: - # Need unittest2 on python2.6 - from unittest2 import * - except ImportError: - print('You need unittest2 installed on python2.6.x to run tests') -else: - from unittest import * diff --git a/tests/unit/mock/__init__.py b/tests/unit/mock/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/unit/mock/loader.py b/tests/unit/mock/loader.py deleted file mode 100644 index f7aff17..0000000 --- a/tests/unit/mock/loader.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) 2012-2014, Michael DeHaan -# -# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) -# SPDX-License-Identifier: GPL-3.0-or-later - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os - -from ansible.errors import AnsibleParserError -from ansible.parsing.dataloader import DataLoader -from ansible.module_utils.common.text.converters import to_bytes, to_text - - -class DictDataLoader(DataLoader): - - def __init__(self, file_mapping=None): - file_mapping = {} if file_mapping is None else file_mapping - assert isinstance(file_mapping, dict) - - super(DictDataLoader, self).__init__() - - self._file_mapping = file_mapping - self._build_known_directories() - self._vault_secrets = None - - def load_from_file(self, path, cache=True, unsafe=False): - path = to_text(path) - if path in self._file_mapping: - return self.load(self._file_mapping[path], path) - return None - - # TODO: the real _get_file_contents returns a bytestring, so we actually convert the - # unicode/text it's created with to utf-8 - def _get_file_contents(self, file_name): - path = to_text(file_name) - if path in self._file_mapping: - return (to_bytes(self._file_mapping[path]), False) - else: - raise AnsibleParserError("file not found: %s" % path) - - def path_exists(self, path): - path = to_text(path) - return path in self._file_mapping or path in self._known_directories - - def is_file(self, path): - path = to_text(path) - return path in self._file_mapping - - def is_directory(self, path): - path = to_text(path) - return path in self._known_directories - - def list_directory(self, path): - ret = [] - path = to_text(path) - for x in (list(self._file_mapping.keys()) + self._known_directories): - if x.startswith(path): - if os.path.dirname(x) == path: - ret.append(os.path.basename(x)) - return ret - - def is_executable(self, path): - # FIXME: figure out a way to make paths return true for this - return False - - def _add_known_directory(self, directory): - if directory not in self._known_directories: - self._known_directories.append(directory) - - def _build_known_directories(self): - self._known_directories = [] - for path in self._file_mapping: - dirname = os.path.dirname(path) - while dirname not in ('/', ''): - self._add_known_directory(dirname) - dirname = os.path.dirname(dirname) - - def push(self, path, content): - rebuild_dirs = False - if path not in self._file_mapping: - rebuild_dirs = True - - self._file_mapping[path] = content - - if rebuild_dirs: - self._build_known_directories() - - def pop(self, path): - if path in self._file_mapping: - del self._file_mapping[path] - self._build_known_directories() - - def clear(self): - self._file_mapping = dict() - self._known_directories = [] - - def get_basedir(self): - return os.getcwd() - - def set_vault_secrets(self, vault_secrets): - self._vault_secrets = vault_secrets diff --git a/tests/unit/mock/path.py b/tests/unit/mock/path.py deleted file mode 100644 index c1c075b..0000000 --- a/tests/unit/mock/path.py +++ /dev/null @@ -1,8 +0,0 @@ -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -from ansible_collections.community.general.tests.unit.compat.mock import MagicMock -from ansible.utils.path import unfrackpath - - -mock_unfrackpath_noop = MagicMock(spec_set=unfrackpath, side_effect=lambda x, *args, **kwargs: x) diff --git a/tests/unit/mock/procenv.py b/tests/unit/mock/procenv.py deleted file mode 100644 index b0bfadd..0000000 --- a/tests/unit/mock/procenv.py +++ /dev/null @@ -1,69 +0,0 @@ -# (c) 2016, Matt Davis -# (c) 2016, Toshio Kuratomi -# -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import sys -import json - -from contextlib import contextmanager -from io import BytesIO, StringIO -from ansible_collections.community.general.tests.unit.compat import unittest -from ansible.module_utils.common.text.converters import to_bytes - - -@contextmanager -def swap_stdin_and_argv(stdin_data='', argv_data=tuple()): - """ - context manager that temporarily masks the test runner's values for stdin and argv - """ - real_stdin = sys.stdin - real_argv = sys.argv - - fake_stream = StringIO(stdin_data) - fake_stream.buffer = BytesIO(to_bytes(stdin_data)) - - try: - sys.stdin = fake_stream - sys.argv = argv_data - - yield - finally: - sys.stdin = real_stdin - sys.argv = real_argv - - -@contextmanager -def swap_stdout(): - """ - context manager that temporarily replaces stdout for tests that need to verify output - """ - old_stdout = sys.stdout - - fake_stream = StringIO() - - try: - sys.stdout = fake_stream - - yield fake_stream - finally: - sys.stdout = old_stdout - - -class ModuleTestCase(unittest.TestCase): - def setUp(self, module_args=None): - if module_args is None: - module_args = {'_ansible_remote_tmp': '/tmp', '_ansible_keep_remote_files': False} - - args = json.dumps(dict(ANSIBLE_MODULE_ARGS=module_args)) - - # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually - self.stdin_swap = swap_stdin_and_argv(stdin_data=args) - self.stdin_swap.__enter__() - - def tearDown(self): - # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually - self.stdin_swap.__exit__(None, None, None) diff --git a/tests/unit/mock/vault_helper.py b/tests/unit/mock/vault_helper.py deleted file mode 100644 index 6bd2db9..0000000 --- a/tests/unit/mock/vault_helper.py +++ /dev/null @@ -1,27 +0,0 @@ -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -from ansible.module_utils.common.text.converters import to_bytes - -from ansible.parsing.vault import VaultSecret - - -class TextVaultSecret(VaultSecret): - '''A secret piece of text. ie, a password. Tracks text encoding. - - The text encoding of the text may not be the default text encoding so - we keep track of the encoding so we encode it to the same bytes.''' - - def __init__(self, text, encoding=None, errors=None, _bytes=None): - super(TextVaultSecret, self).__init__() - self.text = text - self.encoding = encoding or 'utf-8' - self._bytes = _bytes - self.errors = errors or 'strict' - - @property - def bytes(self): - '''The text encoded with encoding, unless we specifically set _bytes.''' - return self._bytes or to_bytes(self.text, encoding=self.encoding, errors=self.errors) diff --git a/tests/unit/mock/yaml_helper.py b/tests/unit/mock/yaml_helper.py deleted file mode 100644 index 0a62dad..0000000 --- a/tests/unit/mock/yaml_helper.py +++ /dev/null @@ -1,111 +0,0 @@ -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import io -import yaml - -from ansible.parsing.yaml.loader import AnsibleLoader -from ansible.parsing.yaml.dumper import AnsibleDumper - - -class YamlTestUtils(object): - """Mixin class to combine with a unittest.TestCase subclass.""" - def _loader(self, stream): - """Vault related tests will want to override this. - - Vault cases should setup a AnsibleLoader that has the vault password.""" - return AnsibleLoader(stream) - - def _dump_stream(self, obj, stream, dumper=None): - """Dump to a py2-unicode or py3-string stream.""" - return yaml.dump(obj, stream, Dumper=dumper) - - def _dump_string(self, obj, dumper=None): - """Dump to a py2-unicode or py3-string""" - return yaml.dump(obj, Dumper=dumper) - - def _dump_load_cycle(self, obj): - # Each pass though a dump or load revs the 'generation' - # obj to yaml string - string_from_object_dump = self._dump_string(obj, dumper=AnsibleDumper) - - # wrap a stream/file like StringIO around that yaml - stream_from_object_dump = io.StringIO(string_from_object_dump) - loader = self._loader(stream_from_object_dump) - # load the yaml stream to create a new instance of the object (gen 2) - obj_2 = loader.get_data() - - # dump the gen 2 objects directory to strings - string_from_object_dump_2 = self._dump_string(obj_2, - dumper=AnsibleDumper) - - # The gen 1 and gen 2 yaml strings - self.assertEqual(string_from_object_dump, string_from_object_dump_2) - # the gen 1 (orig) and gen 2 py object - self.assertEqual(obj, obj_2) - - # again! gen 3... load strings into py objects - stream_3 = io.StringIO(string_from_object_dump_2) - loader_3 = self._loader(stream_3) - obj_3 = loader_3.get_data() - - string_from_object_dump_3 = self._dump_string(obj_3, dumper=AnsibleDumper) - - self.assertEqual(obj, obj_3) - # should be transitive, but... - self.assertEqual(obj_2, obj_3) - self.assertEqual(string_from_object_dump, string_from_object_dump_3) - - def _old_dump_load_cycle(self, obj): - '''Dump the passed in object to yaml, load it back up, dump again, compare.''' - stream = io.StringIO() - - yaml_string = self._dump_string(obj, dumper=AnsibleDumper) - self._dump_stream(obj, stream, dumper=AnsibleDumper) - - yaml_string_from_stream = stream.getvalue() - - # reset stream - stream.seek(0) - - loader = self._loader(stream) - # loader = AnsibleLoader(stream, vault_password=self.vault_password) - obj_from_stream = loader.get_data() - - stream_from_string = io.StringIO(yaml_string) - loader2 = self._loader(stream_from_string) - # loader2 = AnsibleLoader(stream_from_string, vault_password=self.vault_password) - obj_from_string = loader2.get_data() - - stream_obj_from_stream = io.StringIO() - stream_obj_from_string = io.StringIO() - - yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper) - yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper) - - yaml_string_stream_obj_from_stream = stream_obj_from_stream.getvalue() - yaml_string_stream_obj_from_string = stream_obj_from_string.getvalue() - - stream_obj_from_stream.seek(0) - stream_obj_from_string.seek(0) - - yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper) - yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper) - - assert yaml_string == yaml_string_obj_from_stream - assert yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string - assert (yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string == yaml_string_stream_obj_from_stream == - yaml_string_stream_obj_from_string) - assert obj == obj_from_stream - assert obj == obj_from_string - assert obj == yaml_string_obj_from_stream - assert obj == yaml_string_obj_from_string - assert obj == obj_from_stream == obj_from_string == yaml_string_obj_from_stream == yaml_string_obj_from_string - return {'obj': obj, - 'yaml_string': yaml_string, - 'yaml_string_from_stream': yaml_string_from_stream, - 'obj_from_stream': obj_from_stream, - 'obj_from_string': obj_from_string, - 'yaml_string_obj_from_string': yaml_string_obj_from_string} diff --git a/tests/unit/plugins/modules/test_sap_company.py b/tests/unit/plugins/modules/test_sap_company.py index 46053ef..8ebe409 100644 --- a/tests/unit/plugins/modules/test_sap_company.py +++ b/tests/unit/plugins/modules/test_sap_company.py @@ -4,7 +4,7 @@ __metaclass__ = type import sys -from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch, MagicMock +from unittest.mock import patch, MagicMock from ansible_collections.community.sap_libs.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args sys.modules['pyrfc'] = MagicMock() diff --git a/tests/unit/plugins/modules/test_sap_control_exec.py b/tests/unit/plugins/modules/test_sap_control_exec.py index f185491..dbf9f9f 100644 --- a/tests/unit/plugins/modules/test_sap_control_exec.py +++ b/tests/unit/plugins/modules/test_sap_control_exec.py @@ -4,7 +4,7 @@ __metaclass__ = type import sys -from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch, MagicMock, Mock +from unittest.mock import patch, MagicMock, Mock from ansible_collections.community.sap_libs.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args sys.modules['suds.client'] = MagicMock() diff --git a/tests/unit/plugins/modules/test_sap_hdbsql.py b/tests/unit/plugins/modules/test_sap_hdbsql.py index c54667e..d7ccdad 100644 --- a/tests/unit/plugins/modules/test_sap_hdbsql.py +++ b/tests/unit/plugins/modules/test_sap_hdbsql.py @@ -13,7 +13,7 @@ ModuleTestCase, set_module_args, ) -from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch +from unittest.mock import patch from ansible.module_utils import basic diff --git a/tests/unit/plugins/modules/test_sap_pyrfc.py b/tests/unit/plugins/modules/test_sap_pyrfc.py index 6783b54..619ac1c 100644 --- a/tests/unit/plugins/modules/test_sap_pyrfc.py +++ b/tests/unit/plugins/modules/test_sap_pyrfc.py @@ -13,7 +13,7 @@ __metaclass__ = type import sys -from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch, MagicMock +from unittest.mock import patch, MagicMock from ansible_collections.community.sap_libs.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args sys.modules['pyrfc'] = MagicMock() diff --git a/tests/unit/plugins/modules/test_sap_snote.py b/tests/unit/plugins/modules/test_sap_snote.py index f7d2795..ead7578 100644 --- a/tests/unit/plugins/modules/test_sap_snote.py +++ b/tests/unit/plugins/modules/test_sap_snote.py @@ -4,7 +4,7 @@ __metaclass__ = type import sys -from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch, MagicMock, Mock +from unittest.mock import patch, MagicMock, Mock from ansible_collections.community.sap_libs.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args sys.modules['pyrfc'] = MagicMock() diff --git a/tests/unit/plugins/modules/test_sap_system_facts.py b/tests/unit/plugins/modules/test_sap_system_facts.py index 9d969ee..f391b5a 100644 --- a/tests/unit/plugins/modules/test_sap_system_facts.py +++ b/tests/unit/plugins/modules/test_sap_system_facts.py @@ -9,7 +9,7 @@ from ansible_collections.community.sap_libs.plugins.modules import sap_system_facts from ansible_collections.community.sap_libs.tests.unit.plugins.modules.utils import AnsibleExitJson, ModuleTestCase, set_module_args -from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch +from unittest.mock import patch from ansible.module_utils import basic diff --git a/tests/unit/plugins/modules/test_sap_task_list_execute.py b/tests/unit/plugins/modules/test_sap_task_list_execute.py index 5f649a3..18ca032 100644 --- a/tests/unit/plugins/modules/test_sap_task_list_execute.py +++ b/tests/unit/plugins/modules/test_sap_task_list_execute.py @@ -4,7 +4,7 @@ __metaclass__ = type import sys -from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch, MagicMock +from unittest.mock import patch, MagicMock from ansible_collections.community.sap_libs.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args sys.modules['pyrfc'] = MagicMock() diff --git a/tests/unit/plugins/modules/test_sap_user.py b/tests/unit/plugins/modules/test_sap_user.py index 02aa788..02f5196 100644 --- a/tests/unit/plugins/modules/test_sap_user.py +++ b/tests/unit/plugins/modules/test_sap_user.py @@ -4,7 +4,7 @@ __metaclass__ = type import sys -from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch, MagicMock +from unittest.mock import patch, MagicMock from ansible_collections.community.sap_libs.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args sys.modules['pyrfc'] = MagicMock() diff --git a/tests/unit/plugins/modules/test_sapcar_extract.py b/tests/unit/plugins/modules/test_sapcar_extract.py index d5f8bb6..4ba678c 100644 --- a/tests/unit/plugins/modules/test_sapcar_extract.py +++ b/tests/unit/plugins/modules/test_sapcar_extract.py @@ -8,7 +8,7 @@ from ansible_collections.community.sap_libs.plugins.modules import sapcar_extract from ansible_collections.community.sap_libs.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args -from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch +from unittest.mock import patch from ansible.module_utils import basic diff --git a/tests/unit/plugins/modules/utils.py b/tests/unit/plugins/modules/utils.py index 12532e7..6bf7656 100644 --- a/tests/unit/plugins/modules/utils.py +++ b/tests/unit/plugins/modules/utils.py @@ -6,8 +6,8 @@ import json import contextlib -from ansible_collections.community.sap_libs.tests.unit.compat import unittest -from ansible_collections.community.sap_libs.tests.unit.compat.mock import patch +from unittest import TestCase +from unittest.mock import patch from ansible.module_utils import basic from ansible.module_utils.common.text.converters import to_bytes @@ -53,7 +53,7 @@ def fail_json(*args, **kwargs): raise AnsibleFailJson(kwargs) -class ModuleTestCase(unittest.TestCase): +class ModuleTestCase(TestCase): def setUp(self): self.mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json) diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index 49654b8..20ad250 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -1,7 +1,4 @@ -unittest2 ; python_version < '2.7' -importlib ; python_version < '2.7' +# Python 2 support has been removed. # requirement sap_task_list_execute -lxml < 4.3.0 ; python_version < '2.7' # lxml 4.3.0 and later require python 2.7 or later -lxml ; python_version >= '2.7' -argparse ; python_version >= '2.6' \ No newline at end of file +lxml \ No newline at end of file From 48605ff7c6ec7c9b5366907a1e84fd017e721c06 Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Thu, 25 Sep 2025 09:55:53 +0200 Subject: [PATCH 7/9] split workflow into supported and eol, update gitignore --- .github/workflows/ansible-test.yml | 125 ++++++++++++++++++++--------- .gitignore | 3 + 2 files changed, 89 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml index 658d6b9..fa88f76 100644 --- a/.github/workflows/ansible-test.yml +++ b/.github/workflows/ansible-test.yml @@ -1,92 +1,139 @@ +--- +# Always check ansible-core support matrix before configuring units matrix. +# https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix + name: CI on: - # Run CI against all pushes (direct commits, also merged PRs), Pull Requests push: branches: - main - dev + pull_request: + # Run CI once per day (at 06:00 UTC) # This ensures that even if there haven't been commits that we are still testing against latest version of ansible-test for each ansible-base version schedule: - cron: '0 6 * * *' + workflow_dispatch: -env: - NAMESPACE: community - COLLECTION_NAME: sap_libs jobs: - sanity: + sanity-supported: runs-on: ubuntu-latest - name: Sanity (Ⓐ${{ matrix.ansible }}) + name: Sanity (Supported Ⓐ${{ matrix.ansible }}) strategy: fail-fast: false # Disabled so we can see all failed combinations. + # Define a build matrix to test compatibility across multiple Ansible versions. + # Each version listed below will spawn a separate job that runs in parallel. matrix: ansible: - - 'stable-2.14' - - 'stable-2.15' - - 'stable-2.16' - - 'stable-2.17' - - 'stable-2.18' - - 'stable-2.19' - - 'devel' # Currently 2.20 + # Supported versions (must pass) + - 'stable-2.18' # Python 3.11 - 3.13 + - 'stable-2.19' # Python 3.11 - 3.13 + - 'devel' # Test against the upcoming development version. + steps: + - uses: actions/checkout@v5 + + - name: ansible-test - sanity + uses: ansible-community/ansible-test-gh-action@release/v1 + with: + ansible-core-version: ${{ matrix.ansible }} + testing-type: sanity + + sanity-eol: + runs-on: ubuntu-latest + # This job only runs if the supported tests pass + needs: sanity-supported + name: Sanity (EOL Ⓐ${{ matrix.ansible }}) + continue-on-error: true # This entire job is allowed to fail + strategy: + fail-fast: false # Disabled so we can see all failed combinations. + # Define a build matrix to test compatibility across multiple Ansible versions. + # Each version listed below will spawn a separate job that runs in parallel. + matrix: + ansible: + # EOL versions (allowed to fail) + - 'stable-2.14' # Python 3.9 - 3.11 + - 'stable-2.15' # Python 3.9 - 3.11 + - 'stable-2.16' # Python 3.10 - 3.12 + - 'stable-2.17' # Python 3.10 - 3.12 steps: - - name: Check out code - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - name: Perform sanity testing + - name: ansible-test - sanity uses: ansible-community/ansible-test-gh-action@release/v1 with: ansible-core-version: ${{ matrix.ansible }} testing-type: sanity - units: + units-supported: runs-on: ubuntu-latest - name: Units (Ⓐ${{ matrix.ansible }}+py${{ matrix.python }}) + name: Units (Supported Ⓐ${{ matrix.ansible }}+py${{ matrix.python }}) strategy: fail-fast: false # Disabled so we can see all failed combinations. + # Define a build matrix to test compatibility across multiple Ansible versions. + # Each version listed below will spawn a separate job that runs in parallel. matrix: ansible: - - 'stable-2.14' - - 'stable-2.15' - - 'stable-2.16' - - 'stable-2.17' - - 'stable-2.18' - - 'stable-2.19' - - 'devel' # Currently 2.20 + - 'stable-2.18' # Python 3.11 - 3.13 + - 'stable-2.19' # Python 3.11 - 3.13 + - 'devel' # Test against the upcoming development version. + python: + - '3.11' + - '3.12' + - '3.13' + + steps: + - uses: actions/checkout@v5 + + - name: ansible-test - units + uses: ansible-community/ansible-test-gh-action@release/v1 + with: + ansible-core-version: ${{ matrix.ansible }} + target-python-version: ${{ matrix.python }} + testing-type: units + + units-eol: + runs-on: ubuntu-latest + # This job only runs if the supported tests pass + needs: units-supported + name: Units (EOL Ⓐ${{ matrix.ansible }}+py${{ matrix.python }}) + continue-on-error: true # This entire job is allowed to fail + strategy: + fail-fast: false # Disabled so we can see all failed combinations. + # Define a build matrix to test compatibility across multiple Ansible versions. + # Each version listed below will spawn a separate job that runs in parallel. + matrix: + ansible: + - 'stable-2.14' # Python 3.9 - 3.11 + - 'stable-2.15' # Python 3.9 - 3.11 + - 'stable-2.16' # Python 3.10 - 3.12 + - 'stable-2.17' # Python 3.10 - 3.12 python: - '3.9' - '3.10' - '3.11' - '3.12' - - '3.13' exclude: - # Exclusions for stable-2.14 + # Exclusions for incompatible Python versions. - ansible: 'stable-2.14' python: '3.12' - - ansible: 'stable-2.14' - python: '3.13' - # Exclusions for stable-2.15 - ansible: 'stable-2.15' python: '3.12' - - ansible: 'stable-2.15' - python: '3.13' - # Exclusions for stable-2.16 - ansible: 'stable-2.16' - python: '3.13' + python: '3.9' - # Exclusions for stable-2.17 - ansible: 'stable-2.17' - python: '3.13' + python: '3.9' steps: - - name: Check out code - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - - name: Perform unit testing + - name: ansible-test - units uses: ansible-community/ansible-test-gh-action@release/v1 with: ansible-core-version: ${{ matrix.ansible }} diff --git a/.gitignore b/.gitignore index b81a2ee..1e2ea7e 100644 --- a/.gitignore +++ b/.gitignore @@ -179,3 +179,6 @@ dmypy.json # Pyre type checker .pyre/ + +# Ignore ansible workspace +.ansible From d4f6ce730721ff3c619cf334f5abae8a930d9cd7 Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Thu, 25 Sep 2025 10:19:09 +0200 Subject: [PATCH 8/9] update readme to align with workflow matrix --- README.md | 39 +++++++++++++++++---------------------- meta/runtime.yml | 2 +- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 9b6bfde..7526078 100644 --- a/README.md +++ b/README.md @@ -62,31 +62,26 @@ The process of decision making in this collection is based on discussing and fin Every voice is important. If you have something on your mind, create an issue or dedicated discussion and let's discuss it! -## Tested with Ansible and the following Python versions - -Tested Ansible versions: -- 2.15 -- 2.16 -- 2.17 -- 2.18 -- 2.19 -- devel - -Tested Python versions: -- 3.9 -- 3.10 -- 3.11 -- 3.12 -- 3.13 - -Support for Python 2 has been dropped. The minimum required Python version is 3.9. - -Due to SAP licensing and hardware requirements, integration tests are momentarily not feasible. +## Tested with the following Ansible and Python version combinations + +Supported ansible-core versions: +- `2.18` with Python `3.11 - 3.13` +- `2.19` with Python `3.11 - 3.13` +- `devel` with Python `3.11 - 3.13` + +End-of-life ansible-core versions are only tested for backwards compatibility. +- `2.14` with Python `3.9 - 3.11` +- `2.15` with Python `3.9 - 3.11` +- `2.16` with Python `3.10 - 3.12` +- `2.17` with Python `3.10 - 3.12` + +**Support for Python 2 has been dropped in release `1.5.0`.** + +Due to SAP licensing and hardware requirements, integration tests are momentarily not feasible. The modules are tested manually against SAP systems until we found a solution or have some modules where we are able to execute integration test we decided to disable these tests. -The test support for Ansible versions 2.9 - 2.12 is disabled due to eol of these versions. -The modules may work with these versions but are not tested. +**NOTE:** All tests combinations were configured following official [ansible-core-support-matrix](https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-support-matrix). ## External requirements diff --git a/meta/runtime.yml b/meta/runtime.yml index db097ee..41d7bc1 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1,5 +1,5 @@ --- -requires_ansible: ">=2.9.10" +requires_ansible: ">=2.18.0" plugin_routing: modules: From dd96c4d1fc43695822b5e7572bccb0df30f53d4f Mon Sep 17 00:00:00 2001 From: Marcel Mamula Date: Thu, 25 Sep 2025 10:29:20 +0200 Subject: [PATCH 9/9] downgrade requires_ansible to allow EOL sanity tests --- .github/workflows/ansible-test.yml | 1 + meta/runtime.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ansible-test.yml b/.github/workflows/ansible-test.yml index fa88f76..13b283e 100644 --- a/.github/workflows/ansible-test.yml +++ b/.github/workflows/ansible-test.yml @@ -55,6 +55,7 @@ jobs: matrix: ansible: # EOL versions (allowed to fail) + # NOTE: Ensure that meta/runtime.yml `requires_ansible` version is aligned with tested versions. - 'stable-2.14' # Python 3.9 - 3.11 - 'stable-2.15' # Python 3.9 - 3.11 - 'stable-2.16' # Python 3.10 - 3.12 diff --git a/meta/runtime.yml b/meta/runtime.yml index 41d7bc1..981eb39 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -1,5 +1,5 @@ --- -requires_ansible: ">=2.18.0" +requires_ansible: ">=2.14.0" plugin_routing: modules: