Repository: ansible/ansible

Issue:
until loop counter not available for tests
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest -->
 - Bug Report

##### COMPONENT NAME
<!---
Name of the module, plugin, task or feature
Do not include extra details here, e.g. "vyos_command" not "the network module vyos_command" or the full path
-->
task executor
##### ANSIBLE VERSION
<!--- Paste verbatim output from "ansible --version" between quotes below -->
```
ansible 2.4.2.0
```

##### CONFIGURATION
<!---
If using Ansible 2.4 or above, paste the results of "ansible-config dump --only-changed"
Otherwise, mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->
N/A
##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say "N/A" for anything that is not platform-specific.
Also mention the specific version of what you are trying to control,
e.g. if this is a network bug the version of firmware on the network device.
-->
N/A
##### SUMMARY
<!--- Explain the problem briefly -->
Task with `until` loop can not be evaluated by `attempts`
##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->
Run this playbook several times
<!--- Paste example playbooks or commands between quotes below -->
```yaml
- hosts: localhost
  tasks:
    - name: test shell
      shell: "head -c 100 /dev/urandom | grep -o '[0-9]' | head -c 1"
      register: sh_res
      until: sh_res.stdout|int > 5
      retries: 10
      delay: 1
      changed_when: sh_res.attempts > 1
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
`attempts` attribute is accessible in `changed_when` test, task reports changed if it took more than 1 attempt.
##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
`attempts` attribute is not defined in `changed_when` test
<!--- Paste verbatim command output between quotes below -->
```
The conditional check 'sh_res.attempts > 1' failed. The error was: error while evaluating conditional (sh_res.attempts > 1): 'dict object' has no attribute 'attempts'
```



Discussion:
Files identified in the description:
* [lib/ansible/executor/task_executor.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/executor/task_executor.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->

Resolving patch:
diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py
--- a/lib/ansible/executor/task_executor.py
+++ b/lib/ansible/executor/task_executor.py
@@ -535,7 +535,7 @@ def _execute(self, variables=None):
             # update the local copy of vars with the registered value, if specified,
             # or any facts which may have been generated by the module execution
             if self._task.register:
-                vars_copy[self._task.register] = wrap_var(result.copy())
+                vars_copy[self._task.register] = wrap_var(result)
 
             if self._task.async_val > 0:
                 if self._task.poll > 0 and not result.get('skipped') and not result.get('failed'):
@@ -575,10 +575,20 @@ def _evaluate_failed_when_result(result):
                 else:
                     result['failed'] = False
 
+            # Make attempts and retries available early to allow their use in changed/failed_when
+            result['attempts'] = attempt
+
             # set the changed property if it was missing.
             if 'changed' not in result:
                 result['changed'] = False
 
+            # re-update the local copy of vars with the registered value, if specified,
+            # or any facts which may have been generated by the module execution
+            # This gives changed/failed_when access to additional recently modified
+            # attributes of result
+            if self._task.register:
+                vars_copy[self._task.register] = wrap_var(result)
+
             # if we didn't skip this task, use the helpers to evaluate the changed/
             # failed_when properties
             if 'skipped' not in result:
@@ -588,7 +598,6 @@ def _evaluate_failed_when_result(result):
             if retries > 1:
                 cond = Conditional(loader=self._loader)
                 cond.when = self._task.until
-                result['attempts'] = attempt
                 if cond.evaluate_conditional(templar, vars_copy):
                     break
                 else:


Test patch:
diff --git a/test/integration/targets/until/tasks/main.yml b/test/integration/targets/until/tasks/main.yml
--- a/test/integration/targets/until/tasks/main.yml
+++ b/test/integration/targets/until/tasks/main.yml
@@ -29,4 +29,43 @@
 - assert:
     that: runcount.stdout | int == 6 # initial + 5 retries
 
-- file: path="{{ until_tempfile_path }}" state=absent
+- file:
+    path: "{{ until_tempfile_path }}"
+    state: absent
+
+- name: Test failed_when impacting until
+  shell: 'true'
+  register: failed_when_until
+  failed_when: True
+  until: failed_when_until is successful
+  retries: 3
+  delay: 0.5
+  ignore_errors: True
+
+- assert:
+    that:
+      - failed_when_until.attempts == 3
+
+- name: Test changed_when impacting until
+  shell: 'true'
+  register: changed_when_until
+  changed_when: False
+  until: changed_when_until is changed
+  retries: 3
+  delay: 0.5
+  ignore_errors: True
+
+- assert:
+    that:
+      - changed_when_until.attempts == 3
+
+# This task shouldn't fail, previously .attempts was not available to changed_when/failed_when
+# and would cause the conditional to fail due to ``'dict object' has no attribute 'attempts'``
+# https://github.com/ansible/ansible/issues/34139
+- name: Test access to attempts in changed_when/failed_when
+  shell: 'true'
+  register: changed_when_attempts
+  until: 1 == 0
+  retries: 5
+  delay: 0.5
+  failed_when: changed_when_attempts.attempts > 6