From e5a56a833cb9e29bbe732b786f82a540633c4f59 Mon Sep 17 00:00:00 2001 From: Alex Willmer Date: Wed, 28 May 2025 12:47:52 +0100 Subject: [PATCH 1/2] docs: Add Ansible 12 to support table --- docs/ansible_detailed.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/ansible_detailed.rst b/docs/ansible_detailed.rst index 3d80a290..703e9b4b 100644 --- a/docs/ansible_detailed.rst +++ b/docs/ansible_detailed.rst @@ -141,7 +141,9 @@ Noteworthy Differences +-----------------+ 3.10 - 3.13 | | 10 | | +-----------------+-----------------+ - | 11 | 3.11 - 3.13 | + | 11 | | + +-----------------+ 3.11 - 3.13+ | + | 12 | | +-----------------+-----------------+ Verify your installation is running one of these versions by checking From 01874186974ee02e84589b0ba0768c7139fa14eb Mon Sep 17 00:00:00 2001 From: Alex Willmer Date: Thu, 29 May 2025 14:56:07 +0100 Subject: [PATCH 2/2] ansible_mitogen: alpha datatag handling & CI for Ansible 12 (ansible-core 2.19) refs #1258 --- .github/workflows/tests.yml | 12 ++++ ansible_mitogen/connection.py | 2 +- ansible_mitogen/mixins.py | 15 +++-- ansible_mitogen/planner.py | 5 +- ansible_mitogen/runner.py | 4 ++ ansible_mitogen/services.py | 8 ++- ansible_mitogen/utils/unsafe.py | 49 +++++++++++++-- docs/changelog.rst | 4 ++ docs/conf.py | 9 +++ .../integration/action/transfer_data.yml | 24 +++++++- .../runner/crashy_new_style_module.yml | 15 +++-- .../runner/custom_binary_producing_junk.yml | 1 + .../runner/custom_binary_single_null.yml | 4 +- .../integration/runner/missing_module.yml | 1 + tests/ansible/integration/ssh/timeouts.yml | 1 + .../integration/transport_config/password.yml | 25 ++++++++ ..._109__target_has_old_ansible_installed.yml | 3 +- .../issue_766__get_with_context.yml | 7 +++ tests/ansible/tests/utils_unsafe_test.py | 60 ++++++++++++++++--- 19 files changed, 219 insertions(+), 30 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6f6a6871..b48576ab 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -177,10 +177,16 @@ jobs: - name: Ans_313_11 python_version: '3.13' tox_env: py313-mode_ansible-ansible11 + - name: Ans_313_12 + python_version: '3.13' + tox_env: py313-mode_ansible-ansible12 - name: Van_313_11 python_version: '3.13' tox_env: py313-mode_ansible-ansible11-strategy_linear + - name: Van_313_12 + python_version: '3.13' + tox_env: py313-mode_ansible-ansible12-strategy_linear - name: Mito_313 python_version: '3.13' @@ -273,6 +279,12 @@ jobs: - name: Van_313_11 tox_env: py313-mode_localhost-ansible11-strategy_linear + - name: Loc_313_12 + tox_env: py313-mode_localhost-ansible12 + + - name: Van_313_12 + tox_env: py313-mode_localhost-ansible12-strategy_linear + steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 5053a5f5..3e02b971 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -767,7 +767,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): C.BECOME_ALLOW_SAME_USER): stack += (CONNECTION_METHOD[spec.become_method()](spec),) - return stack + return ansible_mitogen.utils.unsafe.cast(stack) def _build_stack(self): """ diff --git a/ansible_mitogen/mixins.py b/ansible_mitogen/mixins.py index dadf2c17..18518d69 100644 --- a/ansible_mitogen/mixins.py +++ b/ansible_mitogen/mixins.py @@ -402,15 +402,17 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase): if not self._mitogen_rediscovered_interpreter: result['ansible_facts'][self._discovered_interpreter_key] = self._discovered_interpreter - if self._discovery_warnings: + discovery_warnings = getattr(self, '_discovery_warnings', []) + if discovery_warnings: if result.get('warnings') is None: result['warnings'] = [] - result['warnings'].extend(self._discovery_warnings) + result['warnings'].extend(discovery_warnings) - if self._discovery_deprecation_warnings: + discovery_deprecation_warnings = getattr(self, '_discovery_deprecation_warnings', []) + if discovery_deprecation_warnings: if result.get('deprecations') is None: result['deprecations'] = [] - result['deprecations'].extend(self._discovery_deprecation_warnings) + result['deprecations'].extend(discovery_deprecation_warnings) return ansible.utils.unsafe_proxy.wrap_var(result) @@ -429,7 +431,10 @@ class ActionModuleMixin(ansible.plugins.action.ActionBase): "stderr": "stderr data" } """ - data = self._parse_returned_data(result) + if ansible_mitogen.utils.ansible_version[:2] >= (2, 19): + data = self._parse_returned_data(result, profile='legacy') + else: + data = self._parse_returned_data(result) # Cutpasted from the base implementation. if 'stdout' in data and 'stdout_lines' not in data: diff --git a/ansible_mitogen/planner.py b/ansible_mitogen/planner.py index 2915f4b7..5d0fbd78 100644 --- a/ansible_mitogen/planner.py +++ b/ansible_mitogen/planner.py @@ -170,6 +170,7 @@ class Planner(object): """ binding = self._inv.connection.get_binding() + kwargs = ansible_mitogen.utils.unsafe.cast(kwargs) new = dict((mitogen.core.UnicodeType(k), kwargs[k]) for k in kwargs) new.setdefault('good_temp_dir', @@ -204,7 +205,7 @@ class BinaryPlanner(Planner): module=self._inv.module_name, path=self._inv.module_path, json_args=json.dumps(self._inv.module_args), - env=self._inv.env, + env=ansible_mitogen.utils.unsafe.cast(self._inv.env), **kwargs ) @@ -546,7 +547,7 @@ def _invoke_async_task(invocation, planner): call_recv = context.call_async( ansible_mitogen.target.run_module_async, job_id=job_id, - timeout_secs=invocation.timeout_secs, + timeout_secs=ansible_mitogen.utils.unsafe.cast(invocation.timeout_secs), started_sender=started_recv.to_sender(), kwargs=planner.get_kwargs(), ) diff --git a/ansible_mitogen/runner.py b/ansible_mitogen/runner.py index b60e537c..ce7dceb9 100644 --- a/ansible_mitogen/runner.py +++ b/ansible_mitogen/runner.py @@ -73,6 +73,7 @@ except ImportError: from io import StringIO # Prevent accidental import of an Ansible module from hanging on stdin read. +# FIXME Should probably be b'{}' or None. Ansible 2.19 has bytes | None = None. import ansible.module_utils.basic ansible.module_utils.basic._ANSIBLE_ARGS = '{}' @@ -635,6 +636,7 @@ class NewStyleStdio(object): sys.stderr = StringIO() encoded = json.dumps({'ANSIBLE_MODULE_ARGS': args}) ansible.module_utils.basic._ANSIBLE_ARGS = utf8(encoded) + ansible.module_utils.basic._ANSIBLE_PROFILE = 'legacy' sys.stdin = StringIO(mitogen.core.to_text(encoded)) self.original_get_path = getattr(ansible.module_utils.basic, @@ -649,7 +651,9 @@ class NewStyleStdio(object): sys.stdout = self.original_stdout sys.stderr = self.original_stderr sys.stdin = self.original_stdin + # FIXME Should probably be b'{}' or None. Ansible 2.19 has bytes | None = None. ansible.module_utils.basic._ANSIBLE_ARGS = '{}' + ansible.module_utils.basic._ANSIBLE_PROFILE = None class ProgramRunner(Runner): diff --git a/ansible_mitogen/services.py b/ansible_mitogen/services.py index abc0e379..a48ab757 100644 --- a/ansible_mitogen/services.py +++ b/ansible_mitogen/services.py @@ -57,6 +57,7 @@ import mitogen.service import ansible_mitogen.loaders import ansible_mitogen.module_finder import ansible_mitogen.target +import ansible_mitogen.utils import ansible_mitogen.utils.unsafe @@ -338,7 +339,12 @@ class ContextService(mitogen.service.Service): 'ansible_mitogen.target', 'mitogen.fork', 'mitogen.service', - ) + ) + (( + 'ansible.module_utils._internal._json._profiles._module_legacy_c2m', + 'ansible.module_utils._internal._json._profiles._module_legacy_m2c', + 'ansible.module_utils._internal._json._profiles._module_modern_c2m', + 'ansible.module_utils._internal._json._profiles._module_legacy_m2c', + ) if ansible_mitogen.utils.ansible_version[:2] >= (2, 19) else ()) def _send_module_forwards(self, context): if hasattr(self.router.responder, 'forward_modules'): diff --git a/ansible_mitogen/utils/unsafe.py b/ansible_mitogen/utils/unsafe.py index c1bdaee7..a3aed462 100644 --- a/ansible_mitogen/utils/unsafe.py +++ b/ansible_mitogen/utils/unsafe.py @@ -16,8 +16,11 @@ __all__ = [ def _cast_to_dict(obj): return {cast(k): cast(v) for k, v in obj.items()} def _cast_to_list(obj): return [cast(v) for v in obj] +def _cast_to_set(obj): return set(cast(v) for v in obj) +def _cast_to_tuple(obj): return tuple(cast(v) for v in obj) def _cast_unsafe(obj): return obj._strip_unsafe() def _passthrough(obj): return obj +def _untag(obj): return obj._native_copy() # A dispatch table to cast objects based on their exact type. @@ -26,30 +29,64 @@ _CAST_DISPATCH = { bytes: bytes, dict: _cast_to_dict, list: _cast_to_list, - tuple: _cast_to_list, mitogen.core.UnicodeType: mitogen.core.UnicodeType, } _CAST_DISPATCH.update({t: _passthrough for t in mitogen.utils.PASSTHROUGH}) -if ansible_mitogen.utils.ansible_version[:2] >= (2, 19): +_CAST_SUBTYPES = [ + dict, + list, +] + +if hasattr(ansible.utils.unsafe_proxy, 'TrustedAsTemplate'): + import datetime + import ansible.module_utils._internal._datatag _CAST_DISPATCH.update({ + set: _cast_to_set, + tuple: _cast_to_tuple, + ansible.module_utils._internal._datatag._AnsibleTaggedBytes: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedDate: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedDateTime: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedDict: _cast_to_dict, + ansible.module_utils._internal._datatag._AnsibleTaggedFloat: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedInt: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedList: _cast_to_list, + ansible.module_utils._internal._datatag._AnsibleTaggedSet: _cast_to_set, + ansible.module_utils._internal._datatag._AnsibleTaggedStr: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedTime: _untag, + ansible.module_utils._internal._datatag._AnsibleTaggedTuple: _cast_to_tuple, ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: bytes, ansible.utils.unsafe_proxy.AnsibleUnsafeText: mitogen.core.UnicodeType, + datetime.date: _passthrough, + datetime.datetime: _passthrough, + datetime.time: _passthrough, }) + _CAST_SUBTYPES.extend([ + set, + tuple, + ]) elif hasattr(ansible.utils.unsafe_proxy.AnsibleUnsafeText, '_strip_unsafe'): _CAST_DISPATCH.update({ + tuple: _cast_to_list, ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: _cast_unsafe, ansible.utils.unsafe_proxy.AnsibleUnsafeText: _cast_unsafe, ansible.utils.unsafe_proxy.NativeJinjaUnsafeText: _cast_unsafe, }) + _CAST_SUBTYPES.extend([ + tuple, + ]) elif ansible_mitogen.utils.ansible_version[:2] <= (2, 16): _CAST_DISPATCH.update({ + tuple: _cast_to_list, ansible.utils.unsafe_proxy.AnsibleUnsafeBytes: bytes, ansible.utils.unsafe_proxy.AnsibleUnsafeText: mitogen.core.UnicodeType, }) + _CAST_SUBTYPES.extend([ + tuple, + ]) else: mitogen_ver = '.'.join(str(v) for v in mitogen.__version__) - raise ImportError("Mitogen %s can't unwrap Ansible %s AnsibleUnsafe objects" + raise ImportError("Mitogen %s can't cast Ansible %s objects" % (mitogen_ver, ansible.__version__)) @@ -78,7 +115,9 @@ def cast(obj): return unwrapper(obj) # Slow path: obj is some unknown subclass - if isinstance(obj, dict): return _cast_to_dict(obj) - if isinstance(obj, (list, tuple)): return _cast_to_list(obj) + for typ_ in _CAST_SUBTYPES: + if isinstance(obj, typ_): + unwrapper = _CAST_DISPATCH[typ_] + return unwrapper(obj) return mitogen.utils.cast(obj) diff --git a/docs/changelog.rst b/docs/changelog.rst index 9f059eed..01a5318d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,10 @@ In progress (unreleased) ------------------------ * :gh:issue:`1258` Initial Ansible 12 (ansible-core 2.19) support +* :gh:issue:`1258` :mod:`ansible_mitogen`: Initial Ansible datatag support + (:gh:anspull:`84621`) +* :gh:issue:`1258` :mod:`ansible_mitogen`: Ansible 12 (ansible-core 2.19) test + jobs v0.3.24 (2025-05-29) diff --git a/docs/conf.py b/docs/conf.py index 9ad5b534..4fb2b300 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,6 +64,15 @@ domainrefs = { 'text': '#%s', 'url': 'https://github.com/mitogen-hq/mitogen/pull/%s', }, + 'gh:ansissue': { + 'text': 'Ansible #%s', + 'url': 'https://github.com/ansible/ansible/issues/%s', + }, + 'gh:anspull': { + 'text': 'Ansible #%s', + 'url': 'https://github.com/ansible/ansible/pull/%s', + }, + 'ans:mod': { 'text': '%s module', 'url': 'https://docs.ansible.com/ansible/latest/modules/%s_module.html', diff --git a/tests/ansible/integration/action/transfer_data.yml b/tests/ansible/integration/action/transfer_data.yml index ab994683..7dd726ed 100644 --- a/tests/ansible/integration/action/transfer_data.yml +++ b/tests/ansible/integration/action/transfer_data.yml @@ -1,7 +1,14 @@ -- name: integration/action/transfer_data.yml +- name: integration/action/transfer_data.yml, json hosts: test-targets tasks: + - meta: end_play + when: + # Ansible >= 12 (ansible-core >= 2.19) only allows bytes|str through + # `ansible.plugins.action.ActionBase._transfer_data()`. + - ansible_version.full is version('2.18.999', '>', strict=True) + - not is_mitogen + - name: Cleanup transfer data file: path: /tmp/transfer-data @@ -15,26 +22,41 @@ data: { "I am JSON": true } + - name: Slurp JSON transfer data slurp: src: /tmp/transfer-data register: out + - assert: that: | out.content|b64decode == '{"I am JSON": true}' fail_msg: | out={{ out }} + - name: Cleanup transfer data + file: + path: /tmp/transfer-data + state: absent + tags: + - transfer_data + + +- name: integration/action/transfer_data.yml, text + hosts: test-targets + tasks: - name: Create text transfer data action_passthrough: method: _transfer_data kwargs: remote_path: /tmp/transfer-data data: "I am text." + - name: Slurp text transfer data slurp: src: /tmp/transfer-data register: out + - assert: that: out.content|b64decode == 'I am text.' diff --git a/tests/ansible/integration/runner/crashy_new_style_module.yml b/tests/ansible/integration/runner/crashy_new_style_module.yml index 80833ab8..5a4d6651 100644 --- a/tests/ansible/integration/runner/crashy_new_style_module.yml +++ b/tests/ansible/integration/runner/crashy_new_style_module.yml @@ -18,10 +18,17 @@ - not out.changed - out is failed # https://github.com/ansible/ansible/commit/62d8c8fde6a76d9c567ded381e9b34dad69afcd6 - - out.msg is match(msg_pattern) - - (out.module_stdout == "" and out.module_stderr is search(tb_pattern)) - or - (out.module_stdout is search(tb_pattern) and out.module_stderr is match("Shared connection to localhost closed.")) + - | + out.msg is match(msg_pattern) + or out.msg in ( + "Task failed: Module failed: name 'kaboom' is not defined", + 'Module result deserialization failed: No start of json char found', + ) + # - out.exception is undefined + # or out.exception | default('') is match(tb_pattern) + # or out.module_stderr is search(tb_pattern) + # - out.module_stdout == '' + # - out.module_stderr is search(tb_pattern) fail_msg: | out={{ out }} tags: diff --git a/tests/ansible/integration/runner/custom_binary_producing_junk.yml b/tests/ansible/integration/runner/custom_binary_producing_junk.yml index 2a05fb75..c8ab869a 100644 --- a/tests/ansible/integration/runner/custom_binary_producing_junk.yml +++ b/tests/ansible/integration/runner/custom_binary_producing_junk.yml @@ -30,6 +30,7 @@ - out.failed - out.results[0].failed - out.results[0].msg.startswith('MODULE FAILURE') + or out.results[0].msg.startswith('Module result deserialization failed') - out.results[0].rc == 0 fail_msg: | out={{ out }} diff --git a/tests/ansible/integration/runner/custom_binary_single_null.yml b/tests/ansible/integration/runner/custom_binary_single_null.yml index cfd401f8..bb5ec5d0 100644 --- a/tests/ansible/integration/runner/custom_binary_single_null.yml +++ b/tests/ansible/integration/runner/custom_binary_single_null.yml @@ -15,7 +15,9 @@ that: - "out.failed" - "out.results[0].failed" - - "out.results[0].msg.startswith('MODULE FAILURE')" + - | + out.results[0].msg.startswith('MODULE FAILURE') + or out.results[0].msg == 'Module result deserialization failed: No start of json char found' # On Ubuntu 16.04 /bin/sh is dash 0.5.8. It treats custom_binary_single_null # as a valid executable. There's no error message, and rc == 0. - | diff --git a/tests/ansible/integration/runner/missing_module.yml b/tests/ansible/integration/runner/missing_module.yml index 4d3f6823..b42e3af3 100644 --- a/tests/ansible/integration/runner/missing_module.yml +++ b/tests/ansible/integration/runner/missing_module.yml @@ -26,6 +26,7 @@ - assert: that: | 'The module missing_module was not found in configured module paths' in out.stdout + or "Cannot resolve 'missing_module' to an action or module" in out.stdout fail_msg: | out={{ out }} tags: diff --git a/tests/ansible/integration/ssh/timeouts.yml b/tests/ansible/integration/ssh/timeouts.yml index afc5e5a2..7ea905f5 100644 --- a/tests/ansible/integration/ssh/timeouts.yml +++ b/tests/ansible/integration/ssh/timeouts.yml @@ -43,6 +43,7 @@ '"unreachable": true' in out.stdout - | '"msg": "Connection timed out."' in out.stdout + or '"msg": "Task failed: Connection timed out."' in out.stdout fail_msg: | out={{ out }} tags: diff --git a/tests/ansible/integration/transport_config/password.yml b/tests/ansible/integration/transport_config/password.yml index 5a1968e0..b447b8b6 100644 --- a/tests/ansible/integration/transport_config/password.yml +++ b/tests/ansible/integration/transport_config/password.yml @@ -8,9 +8,14 @@ tasks: - include_tasks: ../_mitogen_only.yml - {mitogen_get_stack: {}, register: out} + - assert_equal: + left: out.result[0].kwargs.password + right: null + when: ansible_version.full is version('2.18.999', '>=', strict=True) - assert_equal: left: out.result[0].kwargs.password right: "" # actually null, but assert_equal limitation + when: ansible_version.full is version('2.18.999', '<', strict=True) tags: - mitogen_only @@ -23,9 +28,14 @@ - assert_equal: left: out.result[0].kwargs.password right: "ansi-ssh-pass" + - assert_equal: + left: out.result[1].kwargs.password + right: null + when: ansible_version.full is version('2.18.999', '>=', strict=True) - assert_equal: left: out.result[1].kwargs.password right: "" + when: ansible_version.full is version('2.18.999', '<', strict=True) tags: - mitogen_only @@ -48,9 +58,14 @@ tasks: - include_tasks: ../_mitogen_only.yml - {mitogen_get_stack: {}, register: out} + - assert_equal: + left: out.result[0].kwargs.password + right: null + when: ansible_version.full is version('2.18.999', '>=', strict=True) - assert_equal: left: out.result[0].kwargs.password right: "" + when: ansible_version.full is version('2.18.999', '<', strict=True) - assert_equal: left: out.result[1].kwargs.password right: "ansi-ssh-pass" @@ -76,9 +91,14 @@ tasks: - include_tasks: ../_mitogen_only.yml - {mitogen_get_stack: {}, register: out} + - assert_equal: + left: out.result[0].kwargs.password + right: null + when: ansible_version.full is version('2.18.999', '>=', strict=True) - assert_equal: left: out.result[0].kwargs.password right: "" + when: ansible_version.full is version('2.18.999', '<', strict=True) - assert_equal: left: out.result[1].kwargs.password right: "ansi-pass" @@ -104,9 +124,14 @@ tasks: - include_tasks: ../_mitogen_only.yml - {mitogen_get_stack: {}, register: out} + - assert_equal: + left: out.result[0].kwargs.password + right: null + when: ansible_version.full is version('2.18.999', '>=', strict=True) - assert_equal: left: out.result[0].kwargs.password right: "" + when: ansible_version.full is version('2.18.999', '<', strict=True) - assert_equal: left: out.result[1].kwargs.password right: "c.b.a" diff --git a/tests/ansible/regression/issue_109__target_has_old_ansible_installed.yml b/tests/ansible/regression/issue_109__target_has_old_ansible_installed.yml index a7ae0908..92bdfd7e 100644 --- a/tests/ansible/regression/issue_109__target_has_old_ansible_installed.yml +++ b/tests/ansible/regression/issue_109__target_has_old_ansible_installed.yml @@ -24,7 +24,8 @@ assert: that: - env.cwd == ansible_user_dir - - (not env.mitogen_loaded) or (env.python_path.count("") == 1) + - not env.mitogen_loaded + or (env.python_path | select('eq', '') | length == 1) fail_msg: | ansible_user_dir={{ ansible_user_dir }} env={{ env }} diff --git a/tests/ansible/regression/issue_766__get_with_context.yml b/tests/ansible/regression/issue_766__get_with_context.yml index 38e33275..09556939 100644 --- a/tests/ansible/regression/issue_766__get_with_context.yml +++ b/tests/ansible/regression/issue_766__get_with_context.yml @@ -28,6 +28,13 @@ - ansible_version.full is version('2.11', '>=', strict=True) - ansible_version.full is version('2.12', '<', strict=True) + - meta: end_play + when: + # TASK [Get running configuration and state data ] + # Error: : Task failed: ActionBase._parse_returned_data() missing 1 required positional argument: 'profile' + # https://github.com/ansible-collections/ansible.netcommon/issues/698#issuecomment-2910082548 + - ansible_version.full is version('2.18.999', '>=', strict=True) + - block: - name: Start container command: diff --git a/tests/ansible/tests/utils_unsafe_test.py b/tests/ansible/tests/utils_unsafe_test.py index 9aa461c5..2ca863b7 100644 --- a/tests/ansible/tests/utils_unsafe_test.py +++ b/tests/ansible/tests/utils_unsafe_test.py @@ -4,6 +4,7 @@ from ansible.utils.unsafe_proxy import AnsibleUnsafeBytes from ansible.utils.unsafe_proxy import AnsibleUnsafeText from ansible.utils.unsafe_proxy import wrap_var +import ansible_mitogen.utils import ansible_mitogen.utils.unsafe import mitogen.core @@ -17,7 +18,7 @@ class Text(mitogen.core.UnicodeType): pass class Tuple(tuple): pass -class CastTest(unittest.TestCase): +class CastMixin(unittest.TestCase): def assertIsType(self, obj, cls, msg=None): self.assertIs(type(obj), cls, msg) @@ -29,6 +30,8 @@ class CastTest(unittest.TestCase): self.assertEqual(cast(obj), expected) self.assertIsType(cast(obj), type(expected)) + +class CastKnownTest(CastMixin): def test_ansible_unsafe(self): self.assertCasts(AnsibleUnsafeBytes(b'abc'), b'abc') self.assertCasts(AnsibleUnsafeText(u'abc'), u'abc') @@ -47,14 +50,12 @@ class CastTest(unittest.TestCase): self.assertCasts(wrap_var({}), {}) self.assertCasts(wrap_var([]), []) self.assertCasts(wrap_var(u''), u'') - self.assertCasts(wrap_var(()), []) def test_subtypes_roundtrip(self): self.assertCasts(wrap_var(Bytes()), b'') self.assertCasts(wrap_var(Dict()), {}) self.assertCasts(wrap_var(List()), []) self.assertCasts(wrap_var(Text()), u'') - self.assertCasts(wrap_var(Tuple()), []) def test_subtype_nested_dict(self): obj = Dict(foo=Dict(bar=u'abc')) @@ -75,18 +76,59 @@ class CastTest(unittest.TestCase): self.assertIsType(unwrapped[0], list) self.assertIsType(unwrapped[0][0], mitogen.core.UnicodeType) - def test_subtype_roundtrip_tuple(self): - # wrap_var() preserves sequence types, cast() does not (for now) + +@unittest.skipIf( + ansible_mitogen.utils.ansible_version[:2] <= (2, 18), + 'Ansible <= 11 (ansible-core >= 2.18) does not send/receive sets', +) +class CastSetTest(CastMixin): + def test_set(self): + self.assertCasts(wrap_var(set()), set()) + + def test_set_subclass(self): + self.assertCasts(wrap_var(Set()), set()) + + +class CastTupleTest(CastMixin): + def test_tuple(self): + if ansible_mitogen.utils.ansible_version[:2] >= (2, 19): + expected = () + else: + expected = [] + self.assertCasts(wrap_var(Tuple()), expected) + + def test_tuple_subclass(self): + if ansible_mitogen.utils.ansible_version[:2] >= (2, 19): + expected = () + else: + expected = [] + self.assertCasts(wrap_var(()), expected) + + def test_tuple_subclass_with_contents(self): + if ansible_mitogen.utils.ansible_version[:2] >= (2, 19): + expected = ((u'abc',),) + else: + expected = [[u'abc']] + obj = Tuple([Tuple([u'abc'])]) wrapped = wrap_var(obj) unwrapped = ansible_mitogen.utils.unsafe.cast(wrapped) - self.assertEqual(unwrapped, [[u'abc']]) - self.assertIsType(unwrapped, list) - self.assertIsType(unwrapped[0], list) + self.assertEqual(unwrapped, expected) + self.assertIsType(unwrapped, type(expected)) + self.assertIsType(unwrapped[0], type(expected[0])) self.assertIsType(unwrapped[0][0], mitogen.core.UnicodeType) - def test_unknown_types_raise(self): + +class CastUknownTypeTest(unittest.TestCase): + @unittest.skipIf( + ansible_mitogen.utils.ansible_version[:2] >= (2, 19), + 'Ansible >= 12 (ansible-core >= 2.19) uses/preserves sets', + ) + def test_set_raises(self): cast = ansible_mitogen.utils.unsafe.cast self.assertRaises(TypeError, cast, set()) self.assertRaises(TypeError, cast, Set()) + + def test_complex_raises(self): + cast = ansible_mitogen.utils.unsafe.cast self.assertRaises(TypeError, cast, 4j)