From f374897940ad8f388c273af91db4863c51799c6d Mon Sep 17 00:00:00 2001
From: Abhijeet Kasurde <akasurde@redhat.com>
Date: Tue, 7 Sep 2021 10:21:47 +0530
Subject: [PATCH 1/2] Ensure that unsafe is more difficult to lose
 [stable-2.14] (#82295)

Origin: https://build.opensuse.org/projects/SUSE:SLE-15-SP3:Update/packages/ansible/files/0001-Ensure-that-unsafe-is-more-difficult-to-lose-stable-.patch?expand=1

* Ensure that unsafe is more difficult to lose

* Add Task.untemplated_args, and switch assert over to use it
* Don't use re in first_found, switch to using native string methods
* If nested templating results in unsafe, just error, don't continue

(cherry picked from commit 586f1924512b01305f896d9ae4732773023013a3)

* ci_complete

yaml dumper: Add YAML respresenter for AnsibleUndefined (#75078)

Fixes: #75072

Signed-off-by: Abhijeet Kasurde <akasurde@redhat.com>

Ensure single vaulted values aren't counted as sequences. Fixes #70784 (#70786)

AnsibleVaultEncryptedUnicode should be considered a string (#71609)

* AnsibleVaultEncryptedUnicode should be considered a string

* linting fix

* clog frag

2.12: Add YAML representers for NativeJinjaUnsafeText and NativeJinjaText (#77299)

* Add a YAML representer for NativeJinjaUnsafeText (#76186)

(cherry picked from commit dd220ddc2faf9510bdfedacf8b755798038591d9)

* Add a YAML representer for NativeJinjaText (#77282)

Fixes #77280

(cherry picked from commit c9db73f04e7a5fae7bbbdff8efbd585d15971d31)
---
 .../fragments/70784-vault-is-string.yml       |   3 +
 .../fragments/71609-is_string-vault.yml       |   3 +
 changelogs/fragments/75072_undefined_yaml.yml |   3 +
 changelogs/fragments/cve-2023-5764.yml        |   6 +
 .../nativejinjatext-yaml-representer.yml      |   2 +
 ...nativejinjaunsafetext-yaml-representer.yml |   2 +
 .../module_utils/common/collections.py        |   3 +-
 lib/ansible/module_utils/common/json.py       |  14 +-
 lib/ansible/parsing/yaml/dumper.py            |  50 +++-
 lib/ansible/playbook/conditional.py           |   9 +-
 lib/ansible/playbook/task.py                  |  24 ++
 lib/ansible/plugins/action/assert.py          |  23 +-
 lib/ansible/plugins/callback/__init__.py      |  90 +++++-
 lib/ansible/plugins/filter/core.py            |  25 +-
 lib/ansible/plugins/lookup/first_found.py     |  25 ++
 lib/ansible/template/__init__.py              |  23 +-
 lib/ansible/utils/unsafe_proxy.py             | 263 +++++++++++++++++-
 .../targets/apt_repository/tasks/apt.yml      |  11 +-
 .../assert/assert.out.nested_tmpl.stderr      |   4 +
 .../assert/assert.out.nested_tmpl.stdout      |  12 +
 ...t.quiet.stderr => assert.out.quiet.stderr} |   0
 ...t.quiet.stdout => assert.out.quiet.stdout} |   0
 .../targets/assert/nested_tmpl.yml            |   9 +
 test/integration/targets/assert/quiet.yml     |   4 +-
 test/integration/targets/assert/runme.sh      |   3 +-
 .../targets/command_shell/tasks/main.yml      |   2 +-
 test/integration/targets/copy/tasks/tests.yml |  42 +--
 test/integration/targets/debug/runme.sh       |   2 +
 test/integration/targets/debug/unsafe.yml     |  13 +
 .../integration/targets/expect/tasks/main.yml |   7 +-
 .../targets/file/tasks/state_link.yml         |   2 +-
 test/integration/targets/find/tasks/main.yml  |  88 +++++-
 .../gathering_facts/test_gathering_facts.yml  |   4 +-
 test/integration/targets/git/tasks/depth.yml  |   2 +-
 .../targets/git/tasks/localmods.yml           |   4 +-
 .../targets/git/tasks/submodules.yml          |  25 +-
 .../targets/include_vars/tasks/main.yml       |  20 +-
 .../tests/cli/merged.yaml                     |   9 +-
 .../tests/cli/replaced.yaml                   |  10 +-
 .../test_lookup_properties.yml                |   2 +-
 .../modules_test_multiple_roles.yml           |   2 +-
 ...ules_test_multiple_roles_reverse_order.yml |   2 +-
 .../multiple_roles/bar/tasks/main.yml         |   2 +-
 .../multiple_roles/foo/tasks/main.yml         |   2 +-
 .../integration/targets/script/tasks/main.yml |   4 +-
 test/integration/targets/slurp/tasks/main.yml |   2 +-
 .../targets/template/tasks/main.yml           |   2 +-
 .../targets/unarchive/tasks/test_mode.yml     |   8 +-
 .../tasks/test_unprivileged_user.yml          |   2 +-
 .../targets/unarchive/tasks/test_zip.yml      |   2 +-
 .../roles/test_vault_embedded/tasks/main.yml  |   2 +-
 .../tasks/main.yml                            |   2 +-
 .../vyos_config/tests/cli/check_config.yaml   |   4 +-
 .../vyos_interfaces/tests/cli/deleted.yaml    |   9 +-
 .../vyos_interfaces/tests/cli/overridden.yaml |  10 +-
 .../targets/wait_for/tasks/main.yml           |  20 +-
 .../module_utils/common/test_collections.py   |  15 +-
 test/units/parsing/test_ajson.py              |   1 +
 test/units/parsing/yaml/test_dumper.py        |  20 +-
 59 files changed, 828 insertions(+), 126 deletions(-)
 create mode 100644 changelogs/fragments/70784-vault-is-string.yml
 create mode 100644 changelogs/fragments/71609-is_string-vault.yml
 create mode 100644 changelogs/fragments/75072_undefined_yaml.yml
 create mode 100644 changelogs/fragments/cve-2023-5764.yml
 create mode 100644 changelogs/fragments/nativejinjatext-yaml-representer.yml
 create mode 100644 changelogs/fragments/nativejinjaunsafetext-yaml-representer.yml
 create mode 100644 test/integration/targets/assert/assert.out.nested_tmpl.stderr
 create mode 100644 test/integration/targets/assert/assert.out.nested_tmpl.stdout
 rename test/integration/targets/assert/{assert_quiet.out.quiet.stderr => assert.out.quiet.stderr} (100%)
 rename test/integration/targets/assert/{assert_quiet.out.quiet.stdout => assert.out.quiet.stdout} (100%)
 create mode 100644 test/integration/targets/assert/nested_tmpl.yml
 create mode 100644 test/integration/targets/debug/unsafe.yml

diff --git a/changelogs/fragments/70784-vault-is-string.yml b/changelogs/fragments/70784-vault-is-string.yml
new file mode 100644
index 0000000000..8dc1164a85
--- /dev/null
+++ b/changelogs/fragments/70784-vault-is-string.yml
@@ -0,0 +1,3 @@
+bugfixes:
+- JSON Encoder - Ensure we treat single vault encrypted values as strings
+  (https://github.com/ansible/ansible/issues/70784)
diff --git a/changelogs/fragments/71609-is_string-vault.yml b/changelogs/fragments/71609-is_string-vault.yml
new file mode 100644
index 0000000000..89ddd91913
--- /dev/null
+++ b/changelogs/fragments/71609-is_string-vault.yml
@@ -0,0 +1,3 @@
+bugfixes:
+- is_string/vault - Ensure the is_string helper properly identifies AnsibleVaultEncryptedUnicode
+  as a string (https://github.com/ansible/ansible/pull/71609)
diff --git a/changelogs/fragments/75072_undefined_yaml.yml b/changelogs/fragments/75072_undefined_yaml.yml
new file mode 100644
index 0000000000..227c24de1b
--- /dev/null
+++ b/changelogs/fragments/75072_undefined_yaml.yml
@@ -0,0 +1,3 @@
+---
+minor_changes:
+- yaml dumper - YAML representer for AnsibleUndefined (https://github.com/ansible/ansible/issues/75072).
diff --git a/changelogs/fragments/cve-2023-5764.yml b/changelogs/fragments/cve-2023-5764.yml
new file mode 100644
index 0000000000..c37127dac1
--- /dev/null
+++ b/changelogs/fragments/cve-2023-5764.yml
@@ -0,0 +1,6 @@
+security_fixes:
+- templating - Address issues where internal templating can cause unsafe
+  variables to lose their unsafe designation (CVE-2023-5764)
+breaking_changes:
+- assert - Nested templating may result in an inability for the conditional
+  to be evaluated. See the porting guide for more information.
diff --git a/changelogs/fragments/nativejinjatext-yaml-representer.yml b/changelogs/fragments/nativejinjatext-yaml-representer.yml
new file mode 100644
index 0000000000..ef2f460a09
--- /dev/null
+++ b/changelogs/fragments/nativejinjatext-yaml-representer.yml
@@ -0,0 +1,2 @@
+bugfixes:
+   - Add a YAML representer for ``NativeJinjaText``
diff --git a/changelogs/fragments/nativejinjaunsafetext-yaml-representer.yml b/changelogs/fragments/nativejinjaunsafetext-yaml-representer.yml
new file mode 100644
index 0000000000..e13486fb30
--- /dev/null
+++ b/changelogs/fragments/nativejinjaunsafetext-yaml-representer.yml
@@ -0,0 +1,2 @@
+bugfixes:
+  - Add a YAML representer for ``NativeJinjaUnsafeText``
diff --git a/lib/ansible/module_utils/common/collections.py b/lib/ansible/module_utils/common/collections.py
index 0a166cd4cf..123ee8354b 100644
--- a/lib/ansible/module_utils/common/collections.py
+++ b/lib/ansible/module_utils/common/collections.py
@@ -58,7 +58,8 @@ class ImmutableDict(Hashable, Mapping):
 
 def is_string(seq):
     """Identify whether the input has a string-like type (inclding bytes)."""
-    return isinstance(seq, (text_type, binary_type))
+    # AnsibleVaultEncryptedUnicode inherits from Sequence, but is expected to be a string like object
+    return isinstance(seq, (text_type, binary_type)) or getattr(seq, '__ENCRYPTED__', False)
 
 
 def is_iterable(seq, include_strings=False):
diff --git a/lib/ansible/module_utils/common/json.py b/lib/ansible/module_utils/common/json.py
index 3018e9e238..41f9b85fb7 100644
--- a/lib/ansible/module_utils/common/json.py
+++ b/lib/ansible/module_utils/common/json.py
@@ -15,14 +15,22 @@ from ansible.module_utils.common._collections_compat import Mapping
 from ansible.module_utils.common.collections import is_sequence
 
 
+def _is_unsafe(value):
+    return getattr(value, '__UNSAFE__', False) and not getattr(value, '__ENCRYPTED__', False)
+
+
+def _is_vault(value):
+    return getattr(value, '__ENCRYPTED__', False)
+
+
 def _preprocess_unsafe_encode(value):
     """Recursively preprocess a data structure converting instances of ``AnsibleUnsafe``
     into their JSON dict representations
 
     Used in ``AnsibleJSONEncoder.iterencode``
     """
-    if getattr(value, '__UNSAFE__', False) and not getattr(value, '__ENCRYPTED__', False):
-        value = {'__ansible_unsafe': to_text(value, errors='surrogate_or_strict', nonstring='strict')}
+    if _is_unsafe(value):
+        value = {'__ansible_unsafe': to_text(value._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')}
     elif is_sequence(value):
         value = [_preprocess_unsafe_encode(v) for v in value]
     elif isinstance(value, Mapping):
@@ -51,7 +59,7 @@ class AnsibleJSONEncoder(json.JSONEncoder):
                 value = {'__ansible_vault': to_text(o._ciphertext, errors='surrogate_or_strict', nonstring='strict')}
         elif getattr(o, '__UNSAFE__', False):
             # unsafe object, this will never be triggered, see ``AnsibleJSONEncoder.iterencode``
-            value = {'__ansible_unsafe': to_text(o, errors='surrogate_or_strict', nonstring='strict')}
+            value = {'__ansible_unsafe': to_text(o._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')}
         elif isinstance(o, Mapping):
             # hostvars and other objects
             value = dict(o)
diff --git a/lib/ansible/parsing/yaml/dumper.py b/lib/ansible/parsing/yaml/dumper.py
index 67a2efb36d..ddb1363a9e 100644
--- a/lib/ansible/parsing/yaml/dumper.py
+++ b/lib/ansible/parsing/yaml/dumper.py
@@ -21,9 +21,10 @@ __metaclass__ = type
 
 import yaml
 
-from ansible.module_utils.six import PY3
+from ansible.module_utils.six import PY3, text_type
 from ansible.parsing.yaml.objects import AnsibleUnicode, AnsibleSequence, AnsibleMapping, AnsibleVaultEncryptedUnicode
-from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes
+from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText, _is_unsafe
+from ansible.template import AnsibleUndefined
 from ansible.vars.hostvars import HostVars, HostVarsVars
 
 
@@ -44,12 +45,30 @@ def represent_vault_encrypted_unicode(self, data):
     return self.represent_scalar(u'!vault', data._ciphertext.decode(), style='|')
 
 
-if PY3:
-    represent_unicode = yaml.representer.SafeRepresenter.represent_str
-    represent_binary = yaml.representer.SafeRepresenter.represent_binary
-else:
-    represent_unicode = yaml.representer.SafeRepresenter.represent_unicode
-    represent_binary = yaml.representer.SafeRepresenter.represent_str
+def represent_unicode(self, data):
+    if _is_unsafe(data):
+        data = data._strip_unsafe()
+    if PY3:
+        return yaml.representer.SafeRepresenter.represent_str(self, text_type(data))
+    else:
+        return yaml.representer.SafeRepresenter.represent_unicode(self, text_type(data))
+
+
+def represent_binary(self, data):
+    if _is_unsafe(data):
+        data = data._strip_unsafe()
+    if PY3:
+        return yaml.representer.SafeRepresenter.represent_binary(self, binary_type(data))
+    else:
+        return yaml.representer.SafeRepresenter.represent_str(self, binary_type(data))
+
+
+def represent_undefined(self, data):
+    # Here bool will ensure _fail_with_undefined_error happens
+    # if the value is Undefined.
+    # This happens because Jinja sets __bool__ on StrictUndefined
+    return bool(data)
+
 
 AnsibleDumper.add_representer(
     AnsibleUnicode,
@@ -90,3 +109,18 @@ AnsibleDumper.add_representer(
     AnsibleVaultEncryptedUnicode,
     represent_vault_encrypted_unicode,
 )
+
+AnsibleDumper.add_representer(
+    AnsibleUndefined,
+    represent_undefined,
+)
+
+AnsibleDumper.add_representer(
+    NativeJinjaUnsafeText,
+    represent_unicode,
+)
+
+AnsibleDumper.add_representer(
+    NativeJinjaText,
+    represent_unicode,
+)
diff --git a/lib/ansible/playbook/conditional.py b/lib/ansible/playbook/conditional.py
index 2fadf77487..ac4fc0c568 100644
--- a/lib/ansible/playbook/conditional.py
+++ b/lib/ansible/playbook/conditional.py
@@ -26,7 +26,7 @@ from jinja2.compiler import generate
 from jinja2.exceptions import UndefinedError
 
 from ansible import constants as C
-from ansible.errors import AnsibleError, AnsibleUndefinedVariable
+from ansible.errors import AnsibleError, AnsibleUndefinedVariable, AnsibleTemplateError
 from ansible.module_utils.six import text_type
 from ansible.module_utils._text import to_native
 from ansible.playbook.attribute import FieldAttribute
@@ -138,9 +138,10 @@ class Conditional:
             if not isinstance(conditional, text_type) or conditional == "":
                 return conditional
 
-            # update the lookups flag, as the string returned above may now be unsafe
-            # and we don't want future templating calls to do unsafe things
-            disable_lookups |= hasattr(conditional, '__UNSAFE__')
+            # If the result of the first-pass template render (to resolve inline templates) is marked unsafe,
+            # explicitly fail since the next templating operation would never evaluate
+            if hasattr(conditional, '__UNSAFE__'):
+                raise AnsibleTemplateError('Conditional is marked as unsafe, and cannot be evaluated.')
 
             # First, we do some low-level jinja2 parsing involving the AST format of the
             # statement to ensure we don't do anything unsafe (using the disable_lookup flag above)
diff --git a/lib/ansible/playbook/task.py b/lib/ansible/playbook/task.py
index 7fd480c895..e24d27ba8a 100644
--- a/lib/ansible/playbook/task.py
+++ b/lib/ansible/playbook/task.py
@@ -298,6 +298,30 @@ class Task(Base, Conditional, Taggable, CollectionSearch):
 
         super(Task, self).post_validate(templar)
 
+    def _post_validate_args(self, attr, value, templar):
+        # smuggle an untemplated copy of the task args for actions that need more control over the templating of their
+        # input (eg, debug's var/msg, assert's "that" conditional expressions)
+        self.untemplated_args = value
+
+        # now recursively template the args dict
+        args = templar.template(value)
+
+        # FIXME: could we just nuke this entirely and/or wrap it up in ModuleArgsParser or something?
+        if '_variable_params' in args:
+            variable_params = args.pop('_variable_params')
+            if isinstance(variable_params, dict):
+                if C.INJECT_FACTS_AS_VARS:
+                    display.warning("Using a variable for a task's 'args' is unsafe in some situations "
+                                    "(see https://docs.ansible.com/ansible/devel/reference_appendices/faq.html#argsplat-unsafe)")
+                variable_params.update(args)
+                args = variable_params
+            else:
+                # if we didn't get a dict, it means there's garbage remaining after k=v parsing, just give up
+                # see https://github.com/ansible/ansible/issues/79862
+                raise AnsibleError(f"invalid or malformed argument: '{variable_params}'")
+
+        return args
+
     def _post_validate_loop(self, attr, value, templar):
         '''
         Override post validation for the loop field, which is templated
diff --git a/lib/ansible/plugins/action/assert.py b/lib/ansible/plugins/action/assert.py
index 7721a6b47c..e8ab6a9a4f 100644
--- a/lib/ansible/plugins/action/assert.py
+++ b/lib/ansible/plugins/action/assert.py
@@ -63,8 +63,29 @@ class ActionModule(ActionBase):
 
         quiet = boolean(self._task.args.get('quiet', False), strict=False)
 
+        # directly access 'that' via untemplated args from the task so we can intelligently trust embedded
+        # templates and preserve the original inputs/locations for better messaging on assert failures and
+        # errors.
+        # FIXME: even in devel, things like `that: item` don't always work properly (truthy string value
+        # is not really an embedded expression)
+        # we could fix that by doing direct var lookups on the inputs
+        # FIXME: some form of this code should probably be shared between debug, assert, and
+        # Task.post_validate, since they
+        # have a lot of overlapping needs
+        try:
+            thats = self._task.untemplated_args['that']
+        except KeyError:
+            # in the case of "we got our entire args dict from a template", we can just consult the
+            # post-templated dict (the damage has likely already been done for embedded templates anyway)
+            thats = self._task.args['that']
+
+        # FIXME: this is a case where we only want to resolve indirections, NOT recurse containers
+        # (and even then, the leaf-most expression being wrapped is at least suboptimal
+        # (since its expression will be "eaten").
+        if isinstance(thats, str):
+            thats = self._templar.template(thats)
+
         # make sure the 'that' items are a list
-        thats = self._task.args['that']
         if not isinstance(thats, list):
             thats = [thats]
 
diff --git a/lib/ansible/plugins/callback/__init__.py b/lib/ansible/plugins/callback/__init__.py
index 71287f8b5e..08b3b4cb29 100644
--- a/lib/ansible/plugins/callback/__init__.py
+++ b/lib/ansible/plugins/callback/__init__.py
@@ -22,6 +22,7 @@ __metaclass__ = type
 import difflib
 import json
 import os
+import re
 import sys
 import warnings
 
@@ -29,12 +30,15 @@ from copy import deepcopy
 
 from ansible import constants as C
 from ansible.module_utils.common._collections_compat import MutableMapping
-from ansible.module_utils.six import PY3
+from ansible.module_utils.six import PY3, text_type
 from ansible.module_utils._text import to_text
 from ansible.parsing.ajson import AnsibleJSONEncoder
+from ansible.parsing.yaml.dumper import AnsibleDumper
+from ansible.parsing.yaml.objects import AnsibleUnicode
 from ansible.plugins import AnsiblePlugin, get_plugin_class
 from ansible.utils.color import stringc
 from ansible.utils.display import Display
+from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText, _is_unsafe
 from ansible.vars.clean import strip_internal_keys, module_response_deepcopy
 
 if PY3:
@@ -51,6 +55,90 @@ __all__ = ["CallbackBase"]
 
 
 _DEBUG_ALLOWED_KEYS = frozenset(('msg', 'exception', 'warnings', 'deprecations'))
+_YAML_TEXT_TYPES = (text_type, AnsibleUnicode, AnsibleUnsafeText, NativeJinjaUnsafeText)
+# Characters that libyaml/pyyaml consider breaks
+_YAML_BREAK_CHARS = '\n\x85\u2028\u2029'  # NL, NEL, LS, PS
+# regex representation of libyaml/pyyaml of a space followed by a break character
+_SPACE_BREAK_RE = re.compile(fr' +([{_YAML_BREAK_CHARS}])')
+
+
+class _AnsibleCallbackDumper(AnsibleDumper):
+    def __init__(self, lossy=False):
+        self._lossy = lossy
+
+    def __call__(self, *args, **kwargs):
+        # pyyaml expects that we are passing an object that can be instantiated, but to
+        # smuggle the ``lossy`` configuration, we do that in ``__init__`` and then
+        # define this ``__call__`` that will mimic the ability for pyyaml to instantiate class
+        super().__init__(*args, **kwargs)
+        return self
+
+
+def _should_use_block(scalar):
+    """Returns true if string should be in block format based on the existence of various newline separators"""
+    # This method of searching is faster than using a regex
+    for ch in _YAML_BREAK_CHARS:
+        if ch in scalar:
+            return True
+    return False
+
+
+class _SpecialCharacterTranslator:
+    def __getitem__(self, ch):
+        # "special character" logic from pyyaml yaml.emitter.Emitter.analyze_scalar, translated to decimal
+        # for perf w/ str.translate
+        if (ch == 10 or
+            32 <= ch <= 126 or
+            ch == 133 or
+            160 <= ch <= 55295 or
+            57344 <= ch <= 65533 or
+            65536 <= ch < 1114111)\
+                and ch != 65279:
+            return ch
+        return None
+
+
+def _filter_yaml_special(scalar):
+    """Filter a string removing any character that libyaml/pyyaml declare as special"""
+    return scalar.translate(_SpecialCharacterTranslator())
+
+
+def _munge_data_for_lossy_yaml(scalar):
+    """Modify a string so that analyze_scalar in libyaml/pyyaml will allow block formatting"""
+    # we care more about readability than accuracy, so...
+    # ...libyaml/pyyaml does not permit trailing spaces for block scalars
+    scalar = scalar.rstrip()
+    # ...libyaml/pyyaml does not permit tabs for block scalars
+    scalar = scalar.expandtabs()
+    # ...libyaml/pyyaml only permits special characters for double quoted scalars
+    scalar = _filter_yaml_special(scalar)
+    # ...libyaml/pyyaml only permits spaces followed by breaks for double quoted scalars
+    return _SPACE_BREAK_RE.sub(r'\1', scalar)
+
+
+def _pretty_represent_str(self, data):
+    """Uses block style for multi-line strings"""
+    if _is_unsafe(data):
+        data = data._strip_unsafe()
+    data = text_type(data)
+    if _should_use_block(data):
+        style = '|'
+        if self._lossy:
+            data = _munge_data_for_lossy_yaml(data)
+    else:
+        style = self.default_style
+
+    node = yaml.representer.ScalarNode('tag:yaml.org,2002:str', data, style=style)
+    if self.alias_key is not None:
+        self.represented_objects[self.alias_key] = node
+    return node
+
+
+for data_type in _YAML_TEXT_TYPES:
+    _AnsibleCallbackDumper.add_representer(
+        data_type,
+        _pretty_represent_str
+    )
 
 
 class CallbackBase(AnsiblePlugin):
diff --git a/lib/ansible/plugins/filter/core.py b/lib/ansible/plugins/filter/core.py
index 1fc6790a4d..d39d8426bc 100644
--- a/lib/ansible/plugins/filter/core.py
+++ b/lib/ansible/plugins/filter/core.py
@@ -53,6 +53,7 @@ from ansible.utils.display import Display
 from ansible.utils.encrypt import passlib_or_crypt
 from ansible.utils.hashing import md5s, checksum_s
 from ansible.utils.unicode import unicode_wrap
+from ansible.utils.unsafe_proxy import _is_unsafe
 from ansible.utils.vars import merge_hash
 
 display = Display()
@@ -63,13 +64,19 @@ UUID_NAMESPACE_ANSIBLE = uuid.UUID('361E6D51-FAEC-444A-9079-341386DA8E2E')
 def to_yaml(a, *args, **kw):
     '''Make verbose, human readable yaml'''
     default_flow_style = kw.pop('default_flow_style', None)
-    transformed = yaml.dump(a, Dumper=AnsibleDumper, allow_unicode=True, default_flow_style=default_flow_style, **kw)
+    try:
+        transformed = yaml.dump(a, Dumper=AnsibleDumper, allow_unicode=True, default_flow_style=default_flow_style, **kw)
+    except Exception as e:
+        raise AnsibleFilterError("to_yaml - %s" % to_native(e), orig_exc=e)
     return to_text(transformed)
 
 
 def to_nice_yaml(a, indent=4, *args, **kw):
     '''Make verbose, human readable yaml'''
-    transformed = yaml.dump(a, Dumper=AnsibleDumper, indent=indent, allow_unicode=True, default_flow_style=False, **kw)
+    try:
+        transformed = yaml.dump(a, Dumper=AnsibleDumper, indent=indent, allow_unicode=True, default_flow_style=False, **kw)
+    except Exception as e:
+        raise AnsibleFilterError("to_nice_yaml - %s" % to_native(e), orig_exc=e)
     return to_text(transformed)
 
 
@@ -207,13 +214,23 @@ def regex_escape(string, re_type='python'):
 
 def from_yaml(data):
     if isinstance(data, string_types):
-        return yaml.safe_load(data)
+        # The ``text_type`` call here strips any custom
+        # string wrapper class, so that CSafeLoader can
+        # read the data
+        if _is_unsafe(data):
+            data = data._strip_unsafe()
+        return yaml_load(text_type(to_text(data, errors='surrogate_or_strict')))
     return data
 
 
 def from_yaml_all(data):
     if isinstance(data, string_types):
-        return yaml.safe_load_all(data)
+        # The ``text_type`` call here strips any custom
+        # string wrapper class, so that CSafeLoader can
+        # read the data
+        if _is_unsafe(data):
+            data = data._strip_unsafe()
+        return yaml_load_all(text_type(to_text(data, errors='surrogate_or_strict')))
     return data
 
 
diff --git a/lib/ansible/plugins/lookup/first_found.py b/lib/ansible/plugins/lookup/first_found.py
index a1828e6b9c..39a2f1aac3 100644
--- a/lib/ansible/plugins/lookup/first_found.py
+++ b/lib/ansible/plugins/lookup/first_found.py
@@ -102,6 +102,8 @@ RETURN = """
 """
 import os
 
+from collections.abc import Mapping, Sequence
+
 from jinja2.exceptions import UndefinedError
 
 from ansible.errors import AnsibleFileNotFound, AnsibleLookupError, AnsibleUndefinedVariable
@@ -110,6 +112,29 @@ from ansible.module_utils.parsing.convert_bool import boolean
 from ansible.plugins.lookup import LookupBase
 
 
+def _splitter(value, chars):
+    chars = set(chars)
+    v = ''
+    for c in value:
+        if c in chars:
+            yield v
+            v = ''
+            continue
+        v += c
+    yield v
+
+
+def _split_on(terms, spliters=','):
+    termlist = []
+    if isinstance(terms, string_types):
+        termlist = list(_splitter(terms, spliters))
+    else:
+        # added since options will already listify
+        for t in terms:
+            termlist.extend(_split_on(t, spliters))
+    return termlist
+
+
 class LookupModule(LookupBase):
 
     def run(self, terms, variables, **kwargs):
diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py
index a20b1bae68..94ab31e58d 100644
--- a/lib/ansible/template/__init__.py
+++ b/lib/ansible/template/__init__.py
@@ -35,7 +35,7 @@ try:
 except ImportError:
     from sha import sha as sha1
 
-from jinja2.exceptions import TemplateSyntaxError, UndefinedError
+from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError
 from jinja2.loaders import FileSystemLoader
 from jinja2.runtime import Context, StrictUndefined
 
@@ -53,6 +53,9 @@ from ansible.template.vars import AnsibleJ2Vars
 from ansible.utils.collection_loader import AnsibleCollectionRef
 from ansible.utils.display import Display
 from ansible.utils.unsafe_proxy import wrap_var
+from ansible.utils.listify import listify_lookup_plugin_terms
+from ansible.utils.native_jinja import NativeJinjaText
+from ansible.utils.unsafe_proxy import wrap_var, AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText
 
 display = Display()
 
@@ -259,10 +262,21 @@ class AnsibleContext(Context):
     flag is checked post-templating, and (when set) will result in the
     final templated result being wrapped in AnsibleUnsafe.
     '''
+    _disallowed_callables = frozenset({
+        AnsibleUnsafeText._strip_unsafe.__qualname__,
+        AnsibleUnsafeBytes._strip_unsafe.__qualname__,
+        NativeJinjaUnsafeText._strip_unsafe.__qualname__,
+    })
+
     def __init__(self, *args, **kwargs):
         super(AnsibleContext, self).__init__(*args, **kwargs)
         self.unsafe = False
 
+    def call(self, obj, *args, **kwargs):
+        if getattr(obj, '__qualname__', None) in self._disallowed_callables or obj in self._disallowed_callables:
+            raise SecurityError(f"{obj!r} is not safely callable")
+        return super().call(obj, *args, **kwargs)
+
     def _is_unsafe(self, val):
         '''
         Our helper function, which will also recursively check dict and
@@ -874,8 +888,11 @@ class Templar:
             rf = t.root_render_func(new_context)
 
             try:
-                res = j2_concat(rf)
-                unsafe = getattr(new_context, 'unsafe', False)
+                if self.jinja2_native:
+                    res = ansible_native_concat(rf)
+                else:
+                    res = j2_concat(rf)
+                unsafe = getattr(self.cur_context, 'unsafe', False)
                 if unsafe:
                     res = wrap_var(res)
             except TypeError as te:
diff --git a/lib/ansible/utils/unsafe_proxy.py b/lib/ansible/utils/unsafe_proxy.py
index ffb5f37503..0bfd6340ff 100644
--- a/lib/ansible/utils/unsafe_proxy.py
+++ b/lib/ansible/utils/unsafe_proxy.py
@@ -67,10 +67,267 @@ class AnsibleUnsafe(object):
 
 
 class AnsibleUnsafeBytes(binary_type, AnsibleUnsafe):
-    pass
+    def _strip_unsafe(self):
+        return super().__bytes__()
+
+    def __str__(self):  # pylint: disable=invalid-str-returned
+        return self.encode()
+
+    def __bytes__(self):  # pylint: disable=invalid-bytes-returned
+        return self
+
+    def __repr__(self):  # pylint: disable=invalid-repr-returned
+        return AnsibleUnsafeText(super().__repr__())
+
+    def __format__(self, format_spec):  # pylint: disable=invalid-format-returned
+        return self.__class__(super().__format__(format_spec))
+
+    def __getitem__(self, key):
+        return self.__class__(super().__getitem__(key))
+
+    def __iter__(self):
+        cls = self.__class__
+        return (cls(c) for c in super().__iter__())
+
+    def __reversed__(self):
+        return self[::-1]
+
+    def __add__(self, value):
+        return self.__class__(super().__add__(value))
+
+    def __radd__(self, value):
+        return self.__class__(value.__add__(self))
+
+    def __mul__(self, value):
+        return self.__class__(super().__mul__(value))
+
+    __rmul__ = __mul__
+
+    def __mod__(self, value):
+        return self.__class__(super().__mod__(value))
+
+    def __rmod__(self, value):
+        return self.__class__(super().__rmod__(value))
+
+    def capitalize(self):
+        return self.__class__(super().capitalize())
+
+    def casefold(self):
+        return self.__class__(super().casefold())
+
+    def center(self, width, fillchar=b' '):
+        return self.__class__(super().center(width, fillchar))
+
+    def decode(self, encoding='utf-8', errors='strict'):
+        return AnsibleUnsafeText(super().decode(encoding=encoding, errors=errors))
+
+    def removeprefix(self, prefix):
+        return self.__class__(super().removeprefix(prefix))
+
+    def removesuffix(self, suffix):
+        return self.__class__(super().removesuffix(suffix))
+
+    def expandtabs(self, tabsize=8):
+        return self.__class__(super().expandtabs(tabsize))
+
+    def format(self, *args, **kwargs):
+        return self.__class__(super().format(*args, **kwargs))
+
+    def format_map(self, mapping):
+        return self.__class__(super().format_map(mapping))
+
+    def join(self, iterable_of_bytes):
+        return self.__class__(super().join(iterable_of_bytes))
+
+    def ljust(self, width, fillchar=b' '):
+        return self.__class__(super().ljust(width, fillchar))
+
+    def lower(self):
+        return self.__class__(super().lower())
+
+    def lstrip(self, bytes=None):
+        return self.__class__(super().lstrip(bytes))
+
+    def partition(self, sep):
+        cls = self.__class__
+        return tuple(cls(e) for e in super().partition(sep))
+
+    def replace(self, old, new, count=-1):
+        return self.__class__(super().replace(old, new, count))
+
+    def rjust(self, width, fillchar=b' '):
+        return self.__class__(super().rjust(width, fillchar))
+
+    def rpartition(self, sep):
+        cls = self.__class__
+        return tuple(cls(e) for e in super().rpartition(sep))
+
+    def rstrip(self, bytes=None):
+        return self.__class__(super().rstrip(bytes))
+
+    def split(self, sep=None, maxsplit=-1):
+        cls = self.__class__
+        return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)]
+
+    def rsplit(self, sep=None, maxsplit=-1):
+        cls = self.__class__
+        return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)]
+
+    def splitlines(self, keepends=False):
+        cls = self.__class__
+        return [cls(e) for e in super().splitlines(keepends=keepends)]
+
+    def strip(self, bytes=None):
+        return self.__class__(super().strip(bytes))
+
+    def swapcase(self):
+        return self.__class__(super().swapcase())
+
+    def title(self):
+        return self.__class__(super().title())
+
+    def translate(self, table, delete=b''):
+        return self.__class__(super().translate(table, delete=delete))
+
+    def upper(self):
+        return self.__class__(super().upper())
+
+    def zfill(self, width):
+        return self.__class__(super().zfill(width))
 
 
 class AnsibleUnsafeText(text_type, AnsibleUnsafe):
+    # def __getattribute__(self, name):
+    #     print(f'attr: {name}')
+    #     return object.__getattribute__(self, name)
+
+    def _strip_unsafe(self):
+        return super().__str__()
+
+    def __str__(self):  # pylint: disable=invalid-str-returned
+        return self
+
+    def __repr__(self):  # pylint: disable=invalid-repr-returned
+        return self.__class__(super().__repr__())
+
+    def __format__(self, format_spec):  # pylint: disable=invalid-format-returned
+        return self.__class__(super().__format__(format_spec))
+
+    def __getitem__(self, key):
+        return self.__class__(super().__getitem__(key))
+
+    def __iter__(self):
+        cls = self.__class__
+        return (cls(c) for c in super().__iter__())
+
+    def __reversed__(self):
+        return self[::-1]
+
+    def __add__(self, value):
+        return self.__class__(super().__add__(value))
+
+    def __radd__(self, value):
+        return self.__class__(value.__add__(self))
+
+    def __mul__(self, value):
+        return self.__class__(super().__mul__(value))
+
+    __rmul__ = __mul__
+
+    def __mod__(self, value):
+        return self.__class__(super().__mod__(value))
+
+    def __rmod__(self, value):
+        return self.__class__(super().__rmod__(value))
+
+    def capitalize(self):
+        return self.__class__(super().capitalize())
+
+    def casefold(self):
+        return self.__class__(super().casefold())
+
+    def center(self, width, fillchar=' '):
+        return self.__class__(super().center(width, fillchar))
+
+    def encode(self, encoding='utf-8', errors='strict'):
+        return AnsibleUnsafeBytes(super().encode(encoding=encoding, errors=errors))
+
+    def removeprefix(self, prefix):
+        return self.__class__(super().removeprefix(prefix))
+
+    def removesuffix(self, suffix):
+        return self.__class__(super().removesuffix(suffix))
+
+    def expandtabs(self, tabsize=8):
+        return self.__class__(super().expandtabs(tabsize))
+
+    def format(self, *args, **kwargs):
+        return self.__class__(super().format(*args, **kwargs))
+
+    def format_map(self, mapping):
+        return self.__class__(super().format_map(mapping))
+
+    def join(self, iterable):
+        return self.__class__(super().join(iterable))
+
+    def ljust(self, width, fillchar=' '):
+        return self.__class__(super().ljust(width, fillchar))
+
+    def lower(self):
+        return self.__class__(super().lower())
+
+    def lstrip(self, chars=None):
+        return self.__class__(super().lstrip(chars))
+
+    def partition(self, sep):
+        cls = self.__class__
+        return tuple(cls(e) for e in super().partition(sep))
+
+    def replace(self, old, new, count=-1):
+        return self.__class__(super().replace(old, new, count))
+
+    def rjust(self, width, fillchar=' '):
+        return self.__class__(super().rjust(width, fillchar))
+
+    def rpartition(self, sep):
+        cls = self.__class__
+        return tuple(cls(e) for e in super().rpartition(sep))
+
+    def rstrip(self, chars=None):
+        return self.__class__(super().rstrip(chars))
+
+    def split(self, sep=None, maxsplit=-1):
+        cls = self.__class__
+        return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)]
+
+    def rsplit(self, sep=None, maxsplit=-1):
+        cls = self.__class__
+        return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)]
+
+    def splitlines(self, keepends=False):
+        cls = self.__class__
+        return [cls(e) for e in super().splitlines(keepends=keepends)]
+
+    def strip(self, chars=None):
+        return self.__class__(super().strip(chars))
+
+    def swapcase(self):
+        return self.__class__(super().swapcase())
+
+    def title(self):
+        return self.__class__(super().title())
+
+    def translate(self, table):
+        return self.__class__(super().translate(table))
+
+    def upper(self):
+        return self.__class__(super().upper())
+
+    def zfill(self, width):
+        return self.__class__(super().zfill(width))
+
+
+class NativeJinjaUnsafeText(NativeJinjaText, AnsibleUnsafeText):
     pass
 
 
@@ -133,3 +390,7 @@ def to_unsafe_bytes(*args, **kwargs):
 
 def to_unsafe_text(*args, **kwargs):
     return wrap_var(to_text(*args, **kwargs))
+
+
+def _is_unsafe(obj):
+    return getattr(obj, '__UNSAFE__', False) is True
diff --git a/test/integration/targets/apt_repository/tasks/apt.yml b/test/integration/targets/apt_repository/tasks/apt.yml
index 941335f2c6..8d0f4ad896 100644
--- a/test/integration/targets/apt_repository/tasks/apt.yml
+++ b/test/integration/targets/apt_repository/tasks/apt.yml
@@ -50,7 +50,7 @@
     that:
       - 'result.changed'
       - 'result.state == "present"'
-      - 'result.repo == "{{test_ppa_name}}"'
+      - 'result.repo == test_ppa_name'
 
 - name: 'examine apt cache mtime'
   stat: path='/var/cache/apt/pkgcache.bin'
@@ -81,7 +81,7 @@
     that:
       - 'result.changed'
       - 'result.state == "present"'
-      - 'result.repo == "{{test_ppa_name}}"'
+      - 'result.repo == test_ppa_name'
 
 - name: 'examine apt cache mtime'
   stat: path='/var/cache/apt/pkgcache.bin'
@@ -112,7 +112,7 @@
     that:
       - 'result.changed'
       - 'result.state == "present"'
-      - 'result.repo == "{{test_ppa_name}}"'
+      - 'result.repo == test_ppa_name'
 
 - name: 'examine apt cache mtime'
   stat: path='/var/cache/apt/pkgcache.bin'
@@ -146,7 +146,8 @@
     that:
       - 'result.changed'
       - 'result.state == "present"'
-      - 'result.repo == "{{test_ppa_spec}}"'
+      - 'result.repo == test_ppa_spec'
+      - result_cache is not changed
 
 - name: 'examine apt cache mtime'
   stat: path='/var/cache/apt/pkgcache.bin'
@@ -181,7 +182,7 @@
     that:
       - 'result.changed'
       - 'result.state == "present"'
-      - 'result.repo == "{{test_ppa_spec}}"'
+      - 'result.repo == test_ppa_spec'
 
 - name: 'examine source file'
   stat: path='/etc/apt/sources.list.d/{{test_ppa_filename}}.list'
diff --git a/test/integration/targets/assert/assert.out.nested_tmpl.stderr b/test/integration/targets/assert/assert.out.nested_tmpl.stderr
new file mode 100644
index 0000000000..ea208a41c7
--- /dev/null
+++ b/test/integration/targets/assert/assert.out.nested_tmpl.stderr
@@ -0,0 +1,4 @@
++ ansible-playbook -i localhost, -c local nested_tmpl.yml
+++ set +x
+[WARNING]: conditional statements should not include jinja2 templating
+delimiters such as {{ }} or {% %}. Found: "{{ foo }}" == "bar"
diff --git a/test/integration/targets/assert/assert.out.nested_tmpl.stdout b/test/integration/targets/assert/assert.out.nested_tmpl.stdout
new file mode 100644
index 0000000000..8ca3fb76d4
--- /dev/null
+++ b/test/integration/targets/assert/assert.out.nested_tmpl.stdout
@@ -0,0 +1,12 @@
+
+PLAY [localhost] ***************************************************************
+
+TASK [assert] ******************************************************************
+ok: [localhost] => {
+    "changed": false,
+    "msg": "All assertions passed"
+}
+
+PLAY RECAP *********************************************************************
+localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
+
diff --git a/test/integration/targets/assert/assert_quiet.out.quiet.stderr b/test/integration/targets/assert/assert.out.quiet.stderr
similarity index 100%
rename from test/integration/targets/assert/assert_quiet.out.quiet.stderr
rename to test/integration/targets/assert/assert.out.quiet.stderr
diff --git a/test/integration/targets/assert/assert_quiet.out.quiet.stdout b/test/integration/targets/assert/assert.out.quiet.stdout
similarity index 100%
rename from test/integration/targets/assert/assert_quiet.out.quiet.stdout
rename to test/integration/targets/assert/assert.out.quiet.stdout
diff --git a/test/integration/targets/assert/nested_tmpl.yml b/test/integration/targets/assert/nested_tmpl.yml
new file mode 100644
index 0000000000..3da4b1d80e
--- /dev/null
+++ b/test/integration/targets/assert/nested_tmpl.yml
@@ -0,0 +1,9 @@
+- hosts: localhost
+  gather_facts: False
+  tasks:
+    - assert:
+        that:
+          - '"{{ foo }}" == "bar"'
+          - foo == "bar"
+      vars:
+        foo: bar
diff --git a/test/integration/targets/assert/quiet.yml b/test/integration/targets/assert/quiet.yml
index 6834712c2c..1c425cb5ba 100644
--- a/test/integration/targets/assert/quiet.yml
+++ b/test/integration/targets/assert/quiet.yml
@@ -5,12 +5,12 @@
     item_A: yes
   tasks:
   - assert:
-      that: "{{ item }} is defined"
+      that: "item is defined"
       quiet: True
     with_items:
       - item_A
   - assert:
-      that: "{{ item }} is defined"
+      that: "item is defined"
       quiet: False
     with_items:
       - item_A
diff --git a/test/integration/targets/assert/runme.sh b/test/integration/targets/assert/runme.sh
index ca0a858726..b79072813d 100755
--- a/test/integration/targets/assert/runme.sh
+++ b/test/integration/targets/assert/runme.sh
@@ -45,7 +45,7 @@ cleanup() {
    fi
 }
 
-BASEFILE=assert_quiet.out
+BASEFILE=assert.out
 
 ORIGFILE="${BASEFILE}"
 OUTFILE="${BASEFILE}.new"
@@ -69,3 +69,4 @@ export ANSIBLE_NOCOLOR=1
 export ANSIBLE_RETRY_FILES_ENABLED=0
 
 run_test quiet
+run_test nested_tmpl
diff --git a/test/integration/targets/command_shell/tasks/main.yml b/test/integration/targets/command_shell/tasks/main.yml
index 9dc6c9beb6..d685443b55 100644
--- a/test/integration/targets/command_shell/tasks/main.yml
+++ b/test/integration/targets/command_shell/tasks/main.yml
@@ -286,7 +286,7 @@
   assert:
     that:
     - shell_result0 is changed
-    - shell_result0.cmd == '{{ output_dir_test }}/test.sh'
+    - shell_result0.cmd == output_dir_test ~ '/test.sh'
     - shell_result0.rc == 0
     - shell_result0.stderr == ''
     - shell_result0.stdout == 'win'
diff --git a/test/integration/targets/copy/tasks/tests.yml b/test/integration/targets/copy/tasks/tests.yml
index 900f86a66a..2a150c4217 100644
--- a/test/integration/targets/copy/tasks/tests.yml
+++ b/test/integration/targets/copy/tasks/tests.yml
@@ -1176,7 +1176,7 @@
   assert:
     that:
       - "copy_result6.changed"
-      - "copy_result6.dest == '{{remote_dir_expanded}}/multiline.txt'"
+      - "copy_result6.dest == remote_dir_expanded ~ '/multiline.txt'"
       - "copy_result6.checksum == '9cd0697c6a9ff6689f0afb9136fa62e0b3fee903'"
 
 # test overwriting a file as an unprivileged user (pull request #8624)
@@ -2060,26 +2060,26 @@
     assert:
       that:
       - testcase5 is changed
-      - "stat_new_dir_with_chown.stat.uid == {{ ansible_copy_test_user.uid }}"
-      - "stat_new_dir_with_chown.stat.gid == {{ ansible_copy_test_group.gid }}"
-      - "stat_new_dir_with_chown.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
-      - "stat_new_dir_with_chown.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
-      - "stat_new_dir_with_chown_file1.stat.uid == {{ ansible_copy_test_user.uid }}"
-      - "stat_new_dir_with_chown_file1.stat.gid == {{ ansible_copy_test_group.gid }}"
-      - "stat_new_dir_with_chown_file1.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
-      - "stat_new_dir_with_chown_file1.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
-      - "stat_new_dir_with_chown_subdir.stat.uid == {{ ansible_copy_test_user.uid }}"
-      - "stat_new_dir_with_chown_subdir.stat.gid == {{ ansible_copy_test_group.gid }}"
-      - "stat_new_dir_with_chown_subdir.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
-      - "stat_new_dir_with_chown_subdir.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
-      - "stat_new_dir_with_chown_subdir_file12.stat.uid == {{ ansible_copy_test_user.uid }}"
-      - "stat_new_dir_with_chown_subdir_file12.stat.gid == {{ ansible_copy_test_group.gid }}"
-      - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
-      - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
-      - "stat_new_dir_with_chown_link_file12.stat.uid == {{ ansible_copy_test_user.uid }}"
-      - "stat_new_dir_with_chown_link_file12.stat.gid == {{ ansible_copy_test_group.gid }}"
-      - "stat_new_dir_with_chown_link_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'"
-      - "stat_new_dir_with_chown_link_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'"
+      - "stat_new_dir_with_chown.stat.uid == ansible_copy_test_user.uid"
+      - "stat_new_dir_with_chown.stat.gid == ansible_copy_test_group.gid"
+      - "stat_new_dir_with_chown.stat.pw_name == ansible_copy_test_user_name"
+      - "stat_new_dir_with_chown.stat.gr_name == ansible_copy_test_user_name"
+      - "stat_new_dir_with_chown_file1.stat.uid == ansible_copy_test_user.uid"
+      - "stat_new_dir_with_chown_file1.stat.gid == ansible_copy_test_group.gid"
+      - "stat_new_dir_with_chown_file1.stat.pw_name == ansible_copy_test_user_name"
+      - "stat_new_dir_with_chown_file1.stat.gr_name == ansible_copy_test_user_name"
+      - "stat_new_dir_with_chown_subdir.stat.uid == ansible_copy_test_user.uid"
+      - "stat_new_dir_with_chown_subdir.stat.gid == ansible_copy_test_group.gid"
+      - "stat_new_dir_with_chown_subdir.stat.pw_name == ansible_copy_test_user_name"
+      - "stat_new_dir_with_chown_subdir.stat.gr_name == ansible_copy_test_user_name"
+      - "stat_new_dir_with_chown_subdir_file12.stat.uid == ansible_copy_test_user.uid"
+      - "stat_new_dir_with_chown_subdir_file12.stat.gid == ansible_copy_test_group.gid"
+      - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == ansible_copy_test_user_name"
+      - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == ansible_copy_test_user_name"
+      - "stat_new_dir_with_chown_link_file12.stat.uid == ansible_copy_test_user.uid"
+      - "stat_new_dir_with_chown_link_file12.stat.gid == ansible_copy_test_group.gid"
+      - "stat_new_dir_with_chown_link_file12.stat.pw_name == ansible_copy_test_user_name"
+      - "stat_new_dir_with_chown_link_file12.stat.gr_name == ansible_copy_test_user_name"
 
   always:
     - name: execute - remove the user for test
diff --git a/test/integration/targets/debug/runme.sh b/test/integration/targets/debug/runme.sh
index 5ccb1bfda6..05357c2a37 100755
--- a/test/integration/targets/debug/runme.sh
+++ b/test/integration/targets/debug/runme.sh
@@ -15,3 +15,5 @@ for i in 1 2 3; do
   grep "ok: \[localhost\] => (item=$i)" out
   grep "\"item\": $i" out
 done
+
+ansible-playbook unsafe.yml "$@"
diff --git a/test/integration/targets/debug/unsafe.yml b/test/integration/targets/debug/unsafe.yml
new file mode 100644
index 0000000000..6a78af1a69
--- /dev/null
+++ b/test/integration/targets/debug/unsafe.yml
@@ -0,0 +1,13 @@
+- hosts: localhost
+  gather_facts: false
+  vars:
+    unsafe_var: !unsafe undef()|mandatory
+  tasks:
+    - debug:
+        var: '{{ unsafe_var }}'
+      ignore_errors: true
+      register: result
+
+    - assert:
+        that:
+          - result is successful
diff --git a/test/integration/targets/expect/tasks/main.yml b/test/integration/targets/expect/tasks/main.yml
index 0c408d282d..b46474367f 100644
--- a/test/integration/targets/expect/tasks/main.yml
+++ b/test/integration/targets/expect/tasks/main.yml
@@ -109,10 +109,15 @@
       foo: bar
   register: chdir_result
 
+- name: get output_dir real path
+  raw: >
+    {{ ansible_python_interpreter }} -c 'import os; os.chdir("{{output_dir}}"); print(os.getcwd())'
+  register: output_dir_real_path
+
 - name: assert chdir works
   assert:
     that:
-    - "'{{chdir_result.stdout |expanduser | realpath }}' == '{{output_dir | expanduser | realpath}}'"
+    - "chdir_result.stdout | trim == output_dir_real_path.stdout | trim"
 
 - name: test timeout option
   expect:
diff --git a/test/integration/targets/file/tasks/state_link.yml b/test/integration/targets/file/tasks/state_link.yml
index d9ac96740c..9e13eccbd1 100644
--- a/test/integration/targets/file/tasks/state_link.yml
+++ b/test/integration/targets/file/tasks/state_link.yml
@@ -196,7 +196,7 @@
       - "missing_dst_no_follow_enable_force_use_mode2 is changed"
       - "missing_dst_no_follow_enable_force_use_mode3 is not changed"
       - "soft3_result['stat'].islnk"
-      - "soft3_result['stat'].lnk_target == '{{ user.home }}/nonexistent'"
+      - "soft3_result['stat'].lnk_target == user.home ~ '/nonexistent'"
 
 #
 # Test creating a link to a directory https://github.com/ansible/ansible/issues/1369
diff --git a/test/integration/targets/find/tasks/main.yml b/test/integration/targets/find/tasks/main.yml
index 028d3cb45c..f8ea1a45f6 100644
--- a/test/integration/targets/find/tasks/main.yml
+++ b/test/integration/targets/find/tasks/main.yml
@@ -116,4 +116,90 @@
 - name: assert we skipped the ogg file
   assert:
     that:
-      - '"{{ output_dir_test }}/e/f/g/h/8.ogg" not in find_test3_list'
\ No newline at end of file
+      - 'output_dir_test ~ "/e/f/g/h/8.ogg" not in find_test3_list'
+
+- name: patterns with regex
+  find:
+    paths: "{{ output_dir_test }}"
+    recurse: yes
+    use_regex: true
+    patterns: .*\.ogg
+  register: find_test4
+
+- name: assert we matched the ogg file
+  assert:
+    that:
+      - output_dir_test ~ "/e/f/g/h/8.ogg" in find_test4.files|map(attribute="path")
+
+- name: create our age/size testing sub-directory
+  file:
+    path: "{{ output_dir_test }}/astest"
+    state: directory
+
+- name: create test file with old timestamps
+  file:
+    path: "{{ output_dir_test }}/astest/old.txt"
+    state: touch
+    modification_time: "202001011200.0"
+
+- name: create test file with current timestamps
+  file:
+    path: "{{ output_dir_test }}/astest/new.txt"
+    state: touch
+
+- name: create hidden test file with current timestamps
+  file:
+    path: "{{ output_dir_test }}/astest/.hidden.txt"
+    state: touch
+
+- name: find files older than 1 week
+  find:
+    path: "{{ output_dir_test }}/astest"
+    age: 1w
+    hidden: true
+  register: result
+
+- set_fact:
+    astest_list: "{{ result.files|map(attribute='path') }}"
+
+- name: assert we only find the old file
+  assert:
+    that:
+      - result.matched == 1
+      - 'output_dir_test ~ "/astest/old.txt" in astest_list'
+
+- name: find files newer than 1 week
+  find:
+    path: "{{ output_dir_test }}/astest"
+    age: -1w
+  register: result
+
+- set_fact:
+    astest_list: "{{ result.files|map(attribute='path') }}"
+
+- name: assert we only find the current file
+  assert:
+    that:
+      - result.matched == 1
+      - 'output_dir_test ~ "/astest/new.txt" in astest_list'
+
+- name: add some content to the new file
+  shell: "echo hello world > {{ output_dir_test }}/astest/new.txt"
+
+- name: find files with MORE than 5 bytes, also get checksums
+  find:
+    path: "{{ output_dir_test }}/astest"
+    size: 5
+    hidden: true
+    get_checksum: true
+  register: result
+
+- set_fact:
+    astest_list: "{{ result.files|map(attribute='path') }}"
+
+- name: assert we only find the hello world file
+  assert:
+    that:
+      - result.matched == 1
+      - 'output_dir_test ~ "/astest/new.txt" in astest_list'
+      - '"checksum" in result.files[0]'
diff --git a/test/integration/targets/gathering_facts/test_gathering_facts.yml b/test/integration/targets/gathering_facts/test_gathering_facts.yml
index 5924a15649..c32bb3c8b8 100644
--- a/test/integration/targets/gathering_facts/test_gathering_facts.yml
+++ b/test/integration/targets/gathering_facts/test_gathering_facts.yml
@@ -371,7 +371,7 @@
     - name: Test reading facts from default fact_path
       assert:
         that:
-          - '"{{ ansible_local.testfact.fact_dir }}" == "default"'
+          - 'ansible_local.testfact.fact_dir == "default"'
 
 - hosts: facthost9
   tags: [ 'fact_local']
@@ -382,7 +382,7 @@
     - name: Test reading facts from custom fact_path
       assert:
         that:
-          - '"{{ ansible_local.testfact.fact_dir }}" == "custom"'
+          - 'ansible_local.testfact.fact_dir == "custom"'
 
 - hosts: facthost20
   tags: [ 'fact_facter_ohai' ]
diff --git a/test/integration/targets/git/tasks/depth.yml b/test/integration/targets/git/tasks/depth.yml
index 547f84f7b5..e0585ca39b 100644
--- a/test/integration/targets/git/tasks/depth.yml
+++ b/test/integration/targets/git/tasks/depth.yml
@@ -169,7 +169,7 @@
 - name: DEPTH | check update arrived
   assert:
     that:
-      - "{{ a_file.content | b64decode | trim }} == 3"
+      - a_file.content | b64decode | trim == "3"
       - git_fetch is changed
 
 - name: DEPTH | clear checkout_dir
diff --git a/test/integration/targets/git/tasks/localmods.yml b/test/integration/targets/git/tasks/localmods.yml
index 09a1326d58..0e0cf684ed 100644
--- a/test/integration/targets/git/tasks/localmods.yml
+++ b/test/integration/targets/git/tasks/localmods.yml
@@ -47,7 +47,7 @@
 - name: LOCALMODS | check update arrived
   assert:
     that:
-      - "{{ a_file.content | b64decode | trim }} == 2"
+      - a_file.content | b64decode | trim == "2"
       - git_fetch_force is changed
 
 - name: LOCALMODS | clear checkout_dir
@@ -105,7 +105,7 @@
 - name: LOCALMODS | check update arrived
   assert:
     that:
-      - "{{ a_file.content | b64decode | trim }} == 2"
+      - a_file.content | b64decode | trim == "2"
       - git_fetch_force is changed
 
 - name: LOCALMODS | clear checkout_dir
diff --git a/test/integration/targets/git/tasks/submodules.yml b/test/integration/targets/git/tasks/submodules.yml
index 647d1e23b4..1ba84afbde 100644
--- a/test/integration/targets/git/tasks/submodules.yml
+++ b/test/integration/targets/git/tasks/submodules.yml
@@ -32,7 +32,7 @@
 
 - name: SUBMODULES | Ensure submodu1 is at the appropriate commit
   assert:
-    that: '{{ submodule1.stdout_lines | length }} == 2'
+    that: 'submodule1.stdout_lines | length == 2'
 
 - name: SUBMODULES | clear checkout_dir
   file:
@@ -53,7 +53,7 @@
 
 - name: SUBMODULES | Ensure submodule1 is at the appropriate commit
   assert:
-    that: '{{ submodule1.stdout_lines | length }} == 4'
+    that: 'submodule1.stdout_lines | length == 4'
 
 - name: SUBMODULES | Copy the checkout so we can run several different tests on it
   command: 'cp -pr {{ checkout_dir }} {{ checkout_dir }}.bak'
@@ -84,8 +84,8 @@
 - name: SUBMODULES | Ensure both submodules are at the appropriate commit
   assert:
     that:
-      - '{{ submodule1.stdout_lines|length }} == 4'
-      - '{{ submodule2.stdout_lines|length }} == 2'
+      - 'submodule1.stdout_lines|length == 4'
+      - 'submodule2.stdout_lines|length == 2'
 
 
 - name: SUBMODULES | Remove checkout dir
@@ -112,7 +112,7 @@
 
 - name: SUBMODULES | Ensure submodule1 is at the appropriate commit
   assert:
-    that: '{{ submodule1.stdout_lines | length }} == 5'
+    that: 'submodule1.stdout_lines | length == 5'
 
 
 - name: SUBMODULES | Test that update with recursive found new submodules
@@ -121,4 +121,17 @@
 
 - name: SUBMODULES | Enusre submodule2 is at the appropriate commit
   assert:
-    that: '{{ submodule2.stdout_lines | length }} == 4'
+    that: 'submodule2.stdout_lines | length == 4'
+
+- name: SUBMODULES | clear checkout_dir
+  file:
+    state: absent
+    path: "{{ checkout_dir }}"
+
+
+- name: SUBMODULES | Clone main submodule repository
+  git:
+    repo: "{{ repo_submodules }}"
+    dest: "{{ checkout_dir }}/test.gitdir"
+    version: 45c6c07ef10fd9e453d90207e63da1ce5bd3ae1e
+    recursive: yes
diff --git a/test/integration/targets/include_vars/tasks/main.yml b/test/integration/targets/include_vars/tasks/main.yml
index 799d7b26a6..3c5816b0d4 100644
--- a/test/integration/targets/include_vars/tasks/main.yml
+++ b/test/integration/targets/include_vars/tasks/main.yml
@@ -15,7 +15,7 @@
     that:
       - "testing == 789"
       - "base_dir == 'environments/development'"
-      - "{{ included_one_file.ansible_included_var_files | length }} == 1"
+      - "included_one_file.ansible_included_var_files | length == 1"
       - "'vars/environments/development/all.yml' in included_one_file.ansible_included_var_files[0]"
 
 - name: include the vars/environments/development/all.yml and save results in all
@@ -51,7 +51,7 @@
   assert:
     that:
       - webapp_version is defined
-      - "'file_without_extension' in '{{ include_without_file_extension.ansible_included_var_files | join(' ') }}'"
+      - "'file_without_extension' in include_without_file_extension.ansible_included_var_files | join(' ')"
 
 - name: include every directory in vars
   include_vars:
@@ -65,7 +65,7 @@
       - "testing == 456"
       - "base_dir == 'services'"
       - "webapp_containers == 10"
-      - "{{ include_every_dir.ansible_included_var_files | length }} == 7"
+      - "include_every_dir.ansible_included_var_files | length == 7"
       - "'vars/all/all.yml' in include_every_dir.ansible_included_var_files[0]"
       - "'vars/environments/development/all.yml' in include_every_dir.ansible_included_var_files[1]"
       - "'vars/environments/development/services/webapp.yml' in include_every_dir.ansible_included_var_files[2]"
@@ -85,9 +85,9 @@
     that:
       - "testing == 789"
       - "base_dir == 'environments/development'"
-      - "{{ include_without_webapp.ansible_included_var_files | length }} == 4"
-      - "'webapp.yml' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'"
-      - "'file_without_extension' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'"
+      - "include_without_webapp.ansible_included_var_files | length == 4"
+      - "'webapp.yml' not in include_without_webapp.ansible_included_var_files | join(' ')"
+      - "'file_without_extension' not in include_without_webapp.ansible_included_var_files | join(' ')"
 
 - name: include only files matching webapp.yml
   include_vars:
@@ -101,9 +101,9 @@
       - "testing == 101112"
       - "base_dir == 'development/services'"
       - "webapp_containers == 20"
-      - "{{ include_match_webapp.ansible_included_var_files | length }} == 1"
+      - "include_match_webapp.ansible_included_var_files | length == 1"
       - "'vars/environments/development/services/webapp.yml' in include_match_webapp.ansible_included_var_files[0]"
-      - "'all.yml' not in '{{ include_match_webapp.ansible_included_var_files | join(' ') }}'"
+      - "'all.yml' not in include_match_webapp.ansible_included_var_files | join(' ')"
 
 - name: include only files matching webapp.yml and store results in webapp
   include_vars:
@@ -162,3 +162,7 @@
     that:
       - "'my_custom_service' == service_name_fqcn"
       - "'my_custom_service' == service_name_tmpl_fqcn"
+
+- assert:
+    that:
+      - baz.ansible_facts.foo|type_debug != "AnsibleUnsafeText"
diff --git a/test/integration/targets/iosxr_lacp_interfaces/tests/cli/merged.yaml b/test/integration/targets/iosxr_lacp_interfaces/tests/cli/merged.yaml
index be5134091b..a2947d2959 100644
--- a/test/integration/targets/iosxr_lacp_interfaces/tests/cli/merged.yaml
+++ b/test/integration/targets/iosxr_lacp_interfaces/tests/cli/merged.yaml
@@ -24,17 +24,17 @@
 
     - name: Assert that before dicts were correctly generated
       assert:
-        that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}"
+        that: "merged['before'] | symmetric_difference(result['before']) |length == 0"
 
     - name: Assert that correct set of commands were generated
       assert:
         that:
-          - "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
+          - "merged['commands'] | symmetric_difference(result['commands']) |length == 0"
 
     - name: Assert that after dicts was correctly generated
       assert:
         that:
-          - "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}"
+          - "merged['after'] | symmetric_difference(result['after']) |length == 0"
 
     - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT)
       iosxr_lacp_interfaces: *merged
@@ -49,6 +49,7 @@
     - name: Assert that before dicts were correctly generated
       assert:
         that:
-          - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}"
+          - "merged['after'] | symmetric_difference(result['before']) |length == 0"
+
   always:
     - include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/iosxr_lacp_interfaces/tests/cli/replaced.yaml b/test/integration/targets/iosxr_lacp_interfaces/tests/cli/replaced.yaml
index 0dcb8505e0..ccf9d803b2 100644
--- a/test/integration/targets/iosxr_lacp_interfaces/tests/cli/replaced.yaml
+++ b/test/integration/targets/iosxr_lacp_interfaces/tests/cli/replaced.yaml
@@ -21,17 +21,17 @@
     - name: Assert that correct set of commands were generated
       assert:
         that:
-          - "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
+          - "replaced['commands'] | symmetric_difference(result['commands']) |length == 0"
 
     - name: Assert that before dicts are correctly generated
       assert:
         that:
-          - "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
+          - "populate | symmetric_difference(result['before']) |length == 0"
 
     - name: Assert that after dict is correctly generated
       assert:
         that:
-          - "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}"
+          - "replaced['after'] | symmetric_difference(result['after']) |length == 0"
 
     - name: Replace device configurations of listed interfaces with provided configurarions (IDEMPOTENT)
       iosxr_lacp_interfaces: *replaced
@@ -46,7 +46,7 @@
     - name: Assert that before dict is correctly generated
       assert:
         that:
-          - "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}"
-  
+          - "replaced['after'] | symmetric_difference(result['before']) |length == 0"
+
   always:
     - include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/lookup_properties/test_lookup_properties.yml b/test/integration/targets/lookup_properties/test_lookup_properties.yml
index a8cad9de48..7c33a70b0b 100644
--- a/test/integration/targets/lookup_properties/test_lookup_properties.yml
+++ b/test/integration/targets/lookup_properties/test_lookup_properties.yml
@@ -10,7 +10,7 @@
         test_dot:         "{{lookup('ini', 'value.dot         type=properties  file=lookup.properties')}}"
         field_with_space: "{{lookup('ini', 'field.with.space  type=properties  file=lookup.properties')}}"
     - assert:
-        that: "{{item}} is defined"
+        that: "item is defined"
       with_items: [ 'test1', 'test2', 'test_dot', 'field_with_space' ]
     - name: "read ini value"
       set_fact:
diff --git a/test/integration/targets/module_precedence/modules_test_multiple_roles.yml b/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
index f4bd264957..182c2158e8 100644
--- a/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
+++ b/test/integration/targets/module_precedence/modules_test_multiple_roles.yml
@@ -14,4 +14,4 @@
   - assert:
       that:
         - '"location" in result'
-        - 'result["location"] == "{{ expected_location}}"'
+        - 'result["location"] == expected_location'
diff --git a/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml b/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
index 5403ae238c..ec5619f39e 100644
--- a/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
+++ b/test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml
@@ -13,4 +13,4 @@
   - assert:
       that:
         - '"location" in result'
-        - 'result["location"] == "{{ expected_location}}"'
+        - 'result["location"] == expected_location'
diff --git a/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml b/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
index 52c3402013..62b38a7cb5 100644
--- a/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
+++ b/test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml
@@ -7,4 +7,4 @@
   assert:
     that:
       - '"location" in result'
-      - 'result["location"] == "{{ expected_location }}"'
+      - 'result["location"] == expected_location'
diff --git a/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml b/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
index 52c3402013..62b38a7cb5 100644
--- a/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
+++ b/test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml
@@ -7,4 +7,4 @@
   assert:
     that:
       - '"location" in result'
-      - 'result["location"] == "{{ expected_location }}"'
+      - 'result["location"] == expected_location'
diff --git a/test/integration/targets/script/tasks/main.yml b/test/integration/targets/script/tasks/main.yml
index f1746f7c48..3a10a1fec4 100644
--- a/test/integration/targets/script/tasks/main.yml
+++ b/test/integration/targets/script/tasks/main.yml
@@ -197,7 +197,7 @@
   assert:
     that:
       - _check_mode_test2 is skipped
-      - '_check_mode_test2.msg == "{{ output_dir_test | expanduser }}/afile2.txt exists, matching creates option"'
+      - '_check_mode_test2.msg == output_dir_test | expanduser ~ "/afile2.txt exists, matching creates option"'
 
 - name: Remove afile2.txt
   file:
@@ -219,7 +219,7 @@
   assert:
     that:
       - _check_mode_test3 is skipped
-      - '_check_mode_test3.msg == "{{ output_dir_test | expanduser }}/afile2.txt does not exist, matching removes option"'
+      - '_check_mode_test3.msg == output_dir_test | expanduser ~ "/afile2.txt does not exist, matching removes option"'
 
 # executable
 
diff --git a/test/integration/targets/slurp/tasks/main.yml b/test/integration/targets/slurp/tasks/main.yml
index 4f3556fad4..fd61b7f4bc 100644
--- a/test/integration/targets/slurp/tasks/main.yml
+++ b/test/integration/targets/slurp/tasks/main.yml
@@ -33,7 +33,7 @@
       - 'slurp_existing.encoding == "base64"'
       - 'slurp_existing is not changed'
       - 'slurp_existing is not failed'
-      - '"{{ slurp_existing.content | b64decode }}" == "We are at the café"'
+      - 'slurp_existing.content | b64decode == "We are at the café"'
 
 - name: Create a binary file to test with
   copy:
diff --git a/test/integration/targets/template/tasks/main.yml b/test/integration/targets/template/tasks/main.yml
index da80343686..daed110855 100644
--- a/test/integration/targets/template/tasks/main.yml
+++ b/test/integration/targets/template/tasks/main.yml
@@ -356,7 +356,7 @@
 - assert:
     that:
       - "\"foo t'e~m\\plated\" in unusual_results.stdout_lines"
-      - "{{unusual_results.stdout_lines| length}} == 1"
+      - "unusual_results.stdout_lines| length == 1"
 
 - name: check that the unusual filename can be checked for changes
   template:
diff --git a/test/integration/targets/unarchive/tasks/test_mode.yml b/test/integration/targets/unarchive/tasks/test_mode.yml
index c69e3bd2b2..06fbc7b8d9 100644
--- a/test/integration/targets/unarchive/tasks/test_mode.yml
+++ b/test/integration/targets/unarchive/tasks/test_mode.yml
@@ -24,7 +24,7 @@
       - "unarchive06_stat.stat.mode == '0600'"
       # Verify that file list is generated
       - "'files' in unarchive06"
-      - "{{unarchive06['files']| length}} == 1"
+      - "unarchive06['files']| length == 1"
       - "'foo-unarchive.txt' in unarchive06['files']"
 
 - name: remove our tar.gz unarchive destination
@@ -74,7 +74,7 @@
       - "unarchive07.changed == false"
       # Verify that file list is generated
       - "'files' in unarchive07"
-      - "{{unarchive07['files']| length}} == 1"
+      - "unarchive07['files']| length == 1"
       - "'foo-unarchive.txt' in unarchive07['files']"
 
 - name: remove our tar.gz unarchive destination
@@ -108,7 +108,7 @@
       - "unarchive08_stat.stat.mode == '0601'"
       # Verify that file list is generated
       - "'files' in unarchive08"
-      - "{{unarchive08['files']| length}} == 3"
+      - "unarchive08['files']| length == 3"
       - "'foo-unarchive.txt' in unarchive08['files']"
       - "'foo-unarchive-777.txt' in unarchive08['files']"
       - "'FOO-UNAR.TXT' in unarchive08['files']"
@@ -140,7 +140,7 @@
       - "unarchive08_stat.stat.mode == '0601'"
       # Verify that file list is generated
       - "'files' in unarchive08"
-      - "{{unarchive08['files']| length}} == 3"
+      - "unarchive08['files']| length == 3"
       - "'foo-unarchive.txt' in unarchive08['files']"
       - "'foo-unarchive-777.txt' in unarchive08['files']"
       - "'FOO-UNAR.TXT' in unarchive08['files']"
diff --git a/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml b/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
index 6181e3bd62..b3653c0872 100644
--- a/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
+++ b/test/integration/targets/unarchive/tasks/test_unprivileged_user.yml
@@ -48,7 +48,7 @@
           - unarchive10 is changed
           # Verify that file list is generated
           - "'files' in unarchive10"
-          - "{{unarchive10['files']| length}} == 1"
+          - "unarchive10['files']| length == 1"
           - "'foo-unarchive.txt' in unarchive10['files']"
           - archive_path.stat.exists
 
diff --git a/test/integration/targets/unarchive/tasks/test_zip.yml b/test/integration/targets/unarchive/tasks/test_zip.yml
index aae57d8ec9..d11c5f7223 100644
--- a/test/integration/targets/unarchive/tasks/test_zip.yml
+++ b/test/integration/targets/unarchive/tasks/test_zip.yml
@@ -17,7 +17,7 @@
       - "unarchive03.changed == true"
       # Verify that file list is generated
       - "'files' in unarchive03"
-      - "{{unarchive03['files']| length}} == 3"
+      - "unarchive03['files']| length == 3"
       - "'foo-unarchive.txt' in unarchive03['files']"
       - "'foo-unarchive-777.txt' in unarchive03['files']"
       - "'FOO-UNAR.TXT' in unarchive03['files']"
diff --git a/test/integration/targets/vault/roles/test_vault_embedded/tasks/main.yml b/test/integration/targets/vault/roles/test_vault_embedded/tasks/main.yml
index eba938966d..98ef751b86 100644
--- a/test/integration/targets/vault/roles/test_vault_embedded/tasks/main.yml
+++ b/test/integration/targets/vault/roles/test_vault_embedded/tasks/main.yml
@@ -2,7 +2,7 @@
 - name: Assert that a embedded vault of a string with no newline works
   assert:
     that:
-      - '"{{ vault_encrypted_one_line_var }}" == "Setec Astronomy"'
+      - 'vault_encrypted_one_line_var == "Setec Astronomy"'
 
 - name: Assert that a multi line embedded vault works, including new line
   assert:
diff --git a/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml b/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
index e09004a1d9..107e65cb11 100644
--- a/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
+++ b/test/integration/targets/vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml
@@ -2,7 +2,7 @@
 - name: Assert that a vault encrypted file with embedded vault of a string with no newline works
   assert:
     that:
-      - '"{{ vault_file_encrypted_with_encrypted_one_line_var }}" == "Setec Astronomy"'
+      - 'vault_file_encrypted_with_encrypted_one_line_var == "Setec Astronomy"'
 
 - name: Assert that a vault encrypted file with multi line embedded vault works, including new line
   assert:
diff --git a/test/integration/targets/vyos_config/tests/cli/check_config.yaml b/test/integration/targets/vyos_config/tests/cli/check_config.yaml
index 65076b3c54..30c43599af 100644
--- a/test/integration/targets/vyos_config/tests/cli/check_config.yaml
+++ b/test/integration/targets/vyos_config/tests/cli/check_config.yaml
@@ -22,7 +22,7 @@
 - name: Check that multiple duplicate lines collapse into a single commands
   assert:
     that:
-      - "{{ result.commands|length }} == 1"
+      - "result.commands|length == 1"
 
 - name: Check that set is correctly prepended
   assert:
@@ -58,6 +58,6 @@
 
 - assert:
     that:
-      - "{{ result.filtered|length }} == 2"
+      - "result.filtered|length == 2"
 
 - debug: msg="END cli/config_check.yaml on connection={{ ansible_connection }}"
diff --git a/test/integration/targets/vyos_interfaces/tests/cli/deleted.yaml b/test/integration/targets/vyos_interfaces/tests/cli/deleted.yaml
index 5b08ea95f8..b2aa51fc77 100644
--- a/test/integration/targets/vyos_interfaces/tests/cli/deleted.yaml
+++ b/test/integration/targets/vyos_interfaces/tests/cli/deleted.yaml
@@ -16,17 +16,17 @@
     - name: Assert that the before dicts were correctly generated
       assert:
         that:
-          - "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
+          - "populate | symmetric_difference(result['before']) |length == 0"
 
     - name: Assert that the correct set of commands were generated
       assert:
         that:
-          - "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
+          - "deleted['commands'] | symmetric_difference(result['commands']) |length == 0"
 
     - name: Assert that the after dicts were correctly generated
       assert:
         that:
-          - "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}"
+          - "deleted['after'] | symmetric_difference(result['after']) |length == 0"
 
     - name: Delete attributes of given interfaces (IDEMPOTENT)
       vyos_interfaces: *deleted
@@ -40,7 +40,6 @@
     - name: Assert that the before dicts were correctly generated
       assert:
         that:
-          - "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}"
-
+          - "deleted['after'] | symmetric_difference(result['before']) |length == 0"
   always:
     - include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/vyos_interfaces/tests/cli/overridden.yaml b/test/integration/targets/vyos_interfaces/tests/cli/overridden.yaml
index 43040c1e67..4f2e323b36 100644
--- a/test/integration/targets/vyos_interfaces/tests/cli/overridden.yaml
+++ b/test/integration/targets/vyos_interfaces/tests/cli/overridden.yaml
@@ -22,17 +22,17 @@
     - name: Assert that before dicts were correctly generated
       assert:
         that:
-         - "{{ populate | symmetric_difference(result['before']) |length == 0 }}"
+          - "populate_intf | symmetric_difference(result['before']) |length == 0"
 
     - name: Assert that correct commands were generated
       assert:
         that:
-          - "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}"
+          - "overridden['commands'] | symmetric_difference(result['commands']) |length == 0"
 
     - name: Assert that after dicts were correctly generated
       assert:
         that:
-          - "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}"
+          - "overridden['after'] | symmetric_difference(result['after']) |length == 0"
 
     - name: Overrides all device configuration with provided configurations (IDEMPOTENT)
       vyos_interfaces: *overridden
@@ -46,7 +46,7 @@
     - name: Assert that before dicts were correctly generated
       assert:
         that:
-          - "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}"
-  
+          - "overridden['after'] | symmetric_difference(result['before']) |length == 0"
+
   always:
     - include_tasks: _remove_config.yaml
diff --git a/test/integration/targets/wait_for/tasks/main.yml b/test/integration/targets/wait_for/tasks/main.yml
index 1898fd1253..7bdbdd951a 100644
--- a/test/integration/targets/wait_for/tasks/main.yml
+++ b/test/integration/targets/wait_for/tasks/main.yml
@@ -29,7 +29,7 @@
   assert:
     that:
       - waitfor is successful
-      - waitfor.path == "{{ output_dir | expanduser }}/wait_for_file"
+      - waitfor.path == output_dir | expanduser ~ "/wait_for_file"
       - waitfor.elapsed >= 2
       - waitfor.elapsed <= 15
 
@@ -47,7 +47,7 @@
   assert:
     that:
       - waitfor is successful
-      - waitfor.path == "{{ output_dir | expanduser }}/wait_for_file"
+      - waitfor.path == output_dir | expanduser ~ "/wait_for_file"
       - waitfor.elapsed >= 2
       - waitfor.elapsed <= 15
 
@@ -135,7 +135,7 @@
     that:
       - waitfor is successful
       - waitfor is not changed
-      - "waitfor.port == {{ http_port }}"
+      - "waitfor.port == http_port"
 
 - name: install psutil using pip (non-Linux only)
   pip:
@@ -163,4 +163,16 @@
     that:
       - waitfor is successful
       - waitfor is not changed
-      - "waitfor.port == {{ http_port }}"
+      - "waitfor.port == http_port"
+
+- name: test wait_for with delay
+  wait_for:
+    timeout: 2
+    delay: 2
+  register: waitfor
+
+- name: verify test wait_for with delay
+  assert:
+    that:
+      - waitfor is successful
+      - waitfor.elapsed >= 4
diff --git a/test/units/module_utils/common/test_collections.py b/test/units/module_utils/common/test_collections.py
index eb6e376a2c..95b2a402f2 100644
--- a/test/units/module_utils/common/test_collections.py
+++ b/test/units/module_utils/common/test_collections.py
@@ -35,8 +35,21 @@ class IterableStub:
         return IteratorStub()
 
 
+class FakeAnsibleVaultEncryptedUnicode(Sequence):
+    __ENCRYPTED__ = True
+
+    def __init__(self, data):
+        self.data = data
+
+    def __getitem__(self, index):
+        return self.data[index]
+
+    def __len__(self):
+        return len(self.data)
+
+
 TEST_STRINGS = u'he', u'Україна', u'Česká republika'
-TEST_STRINGS = TEST_STRINGS + tuple(s.encode('utf-8') for s in TEST_STRINGS)
+TEST_STRINGS = TEST_STRINGS + tuple(s.encode('utf-8') for s in TEST_STRINGS) + (FakeAnsibleVaultEncryptedUnicode(u'foo'),)
 
 TEST_ITEMS_NON_SEQUENCES = (
     {}, object(), frozenset(),
diff --git a/test/units/parsing/test_ajson.py b/test/units/parsing/test_ajson.py
index 929d19966d..c38f43ea57 100644
--- a/test/units/parsing/test_ajson.py
+++ b/test/units/parsing/test_ajson.py
@@ -158,6 +158,7 @@ class TestAnsibleJSONEncoder:
         Test for passing AnsibleVaultEncryptedUnicode to AnsibleJSONEncoder.default().
         """
         assert ansible_json_encoder.default(test_input) == {'__ansible_vault': expected}
+        assert json.dumps(test_input, cls=AnsibleJSONEncoder, preprocess_unsafe=True) == '{"__ansible_vault": "%s"}' % expected.replace('\n', '\\n')
 
     @pytest.mark.parametrize(
         'test_input,expected',
diff --git a/test/units/parsing/yaml/test_dumper.py b/test/units/parsing/yaml/test_dumper.py
index 8129ca3ab2..ee9ea8b07a 100644
--- a/test/units/parsing/yaml/test_dumper.py
+++ b/test/units/parsing/yaml/test_dumper.py
@@ -19,13 +19,16 @@ from __future__ import (absolute_import, division, print_function)
 __metaclass__ = type
 
 import io
+import yaml
+
+from jinja2.exceptions import UndefinedError
 
 from units.compat import unittest
 from ansible.parsing import vault
 from ansible.parsing.yaml import dumper, objects
 from ansible.parsing.yaml.loader import AnsibleLoader
 from ansible.module_utils.six import PY2
-from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes
+from ansible.template import AnsibleUndefined
 
 from units.mock.yaml_helper import YamlTestUtils
 from units.mock.vault_helper import TextVaultSecret
@@ -64,8 +67,7 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
 
     def test_bytes(self):
         b_text = u'tréma'.encode('utf-8')
-        unsafe_object = AnsibleUnsafeBytes(b_text)
-        yaml_out = self._dump_string(unsafe_object, dumper=self.dumper)
+        yaml_out = self._dump_string(b_text, dumper=self.dumper)
 
         stream = self._build_stream(yaml_out)
         loader = self._loader(stream)
@@ -92,8 +94,7 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
 
     def test_unicode(self):
         u_text = u'nöel'
-        unsafe_object = AnsibleUnsafeText(u_text)
-        yaml_out = self._dump_string(unsafe_object, dumper=self.dumper)
+        yaml_out = self._dump_string(u_text, dumper=self.dumper)
 
         stream = self._build_stream(yaml_out)
         loader = self._loader(stream)
@@ -101,3 +102,12 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
         data_from_yaml = loader.get_single_data()
 
         self.assertEqual(u_text, data_from_yaml)
+
+    def test_undefined(self):
+        undefined_object = AnsibleUndefined()
+        try:
+            yaml_out = self._dump_string(undefined_object, dumper=self.dumper)
+        except UndefinedError:
+            yaml_out = None
+
+        self.assertIsNone(yaml_out)
-- 
2.44.0

