Repository: ansible/ansible
Pull request: Cause copy module to fail on empty string as source
##### SUMMARY

Cause `copy` to fail rather than copying the content of the current working directory if an empty string is specified as the source. Fixes #27363

Cleaned up formatting on integration tests.

Added integration test for this specific case.

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bugfix Pull Request


##### COMPONENT NAME

`action/copy.py`

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
2.4
```


##### ADDITIONAL INFORMATION

This has been a bug for quite some time. I don't believe fixing this will have any adverse effects as it seems it was the original intent to fail on no input as the source.

Problem statement:
copy module copies contents of CWD when src=""
##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report / Question

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
plugin/module: copy

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.3.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.12 (default, Nov 19 2016, 06:48:10) [GCC 5.4.0 20160609]
```

##### OS / ENVIRONMENT
```
Linux Mint 18.1 Serena (GNU/Linux 4.4.0-81-generic x86_64)
```

##### SUMMARY
<!--- Explain the problem briefly -->
When copy module is invoked with `src` set to an empty path it copies contents of the current working directory.

##### STEPS TO REPRODUCE
```
mkdir /tmp/foo /tmp/bar
touch /tmp/foo/baz
cd /tmp/foo && ansible -m copy -a "src= dest=/tmp/bar" -c local localhost
ls -l /tmp/bar/baz
```

##### EXPECTED RESULTS
Copy module fails.

##### ACTUAL RESULTS
Copy module copied contents of current working directory to destination.

Hints from issue discussion:
IMO this is a bug (probably a behavior of `_find_needle` that we should paper over by ensuring the source path is not empty in the action plugin).

Commit message: Add test for empty string as source

Patch to review:
diff --git a/lib/ansible/plugins/action/copy.py b/lib/ansible/plugins/action/copy.py
index 55aba4dc179ce9..638f943fcbd500 100644
--- a/lib/ansible/plugins/action/copy.py
+++ b/lib/ansible/plugins/action/copy.py
@@ -407,7 +407,7 @@ def run(self, tmp=None, task_vars=None):
         local_follow = boolean(self._task.args.get('local_follow', True), strict=False)
 
         result['failed'] = True
-        if (source is None and content is None) or dest is None:
+        if ((not source or source is None) and content is None) or dest is None:
             result['msg'] = "src (or content) and dest are required"
         elif source is not None and content is not None:
             result['msg'] = "src and content are mutually exclusive"
diff --git a/test/integration/targets/copy/tasks/main.yml b/test/integration/targets/copy/tasks/main.yml
index 7254b1c2d21886..274712a7e79f4f 100644
--- a/test/integration/targets/copy/tasks/main.yml
+++ b/test/integration/targets/copy/tasks/main.yml
@@ -10,7 +10,7 @@
         # output_dir is hardcoded in test/runner/lib/executor.py and created there
         remote_dir: '{{ output_dir }}'
 
-    - name: create remote unprivileged remote user
+    - name: Create remote unprivileged remote user
       user:
         name: '{{ remote_unprivileged_user }}'
       register: user
@@ -21,7 +21,7 @@
         state: directory
         mode: 0700
 
-    - name: 'duplicate authorized_keys'
+    - name: Duplicate authorized_keys
       copy:
         src: $HOME/.ssh/authorized_keys
         dest: '{{ user.home }}/.ssh/authorized_keys'
@@ -46,7 +46,7 @@
         state: absent
       connection: local
 
-    - name: remote unprivileged remote user
+    - name: Remote unprivileged remote user
       user:
         name: '{{ remote_unprivileged_user }}'
         state: absent
diff --git a/test/integration/targets/copy/tasks/tests.yml b/test/integration/targets/copy/tasks/tests.yml
index 7fcb424508a47f..f9b9455d4ab0d2 100644
--- a/test/integration/targets/copy/tasks/tests.yml
+++ b/test/integration/targets/copy/tasks/tests.yml
@@ -5,22 +5,28 @@
 # GNU General Public License v3 or later (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt )
 #
 
-- name: record the output directory
-  set_fact: remote_file={{remote_dir}}/foo.txt
+- name: Record the output directory
+  set_fact:
+    remote_file: "{{ remote_dir }}/foo.txt"
 
-- name: locate sha1sum/shasum
+- name: Locate sha1sum/shasum
   shell: which sha1sum || which shasum
   register: sha1sum
 
-- name: initiate a basic copy, and also test the mode
-  copy: src=foo.txt dest={{remote_file}} mode=0444
+- name: Initiate a basic copy, and also test the mode
+  copy:
+    src: foo.txt
+    dest: "{{ remote_file }}"
+    mode: 0444
   register: copy_result
 
-- name: check the mode of the output file
-  file: name={{remote_file}} state=file
+- name: Check the mode of the output file
+  file:
+    name: "{{ remote_file }}"
+    state: file
   register: file_result_check
 
-- name: assert the mode is correct
+- name: Assert the mode is correct
   assert:
     that:
       - "file_result_check.mode == '0444'"
@@ -32,10 +38,11 @@
 - set_fact:
     remote_dir_expanded: '{{ echo.stdout }}'
 
-#- debug:
-#    var: copy_result
+- debug:
+    var: copy_result
+    verbosity: 1
 
-- name: assert basic copy worked
+- name: Assert basic copy worked
   assert:
     that:
       - "'changed' in copy_result"
@@ -50,29 +57,32 @@
       - "'state' in copy_result"
       - "'uid' in copy_result"
 
-- name: verify that the file was marked as changed
+- name: Verify that the file was marked as changed
   assert:
     that:
       - "copy_result.changed == true"
 
-- name: verify that the file checksums are correct
+- name: Verify that the file checksums are correct
   assert:
     that:
       - "copy_result.checksum == ('foo.txt\n'|hash('sha1'))"
 
-- name: verify that the legacy md5sum is correct
+- name: Verify that the legacy md5sum is correct
   assert:
     that:
       - "copy_result.md5sum == ('foo.txt\n'|hash('md5'))"
   when: ansible_fips|bool != True
 
-- name: check the stat results of the file
-  stat: path={{remote_file}}
+- name: Check the stat results of the file
+  stat:
+    path: "{{ remote_file }}"
   register: stat_results
 
-#- debug: var=stat_results
+- debug:
+    var: stat_results
+    verbosity: 1
 
-- name: assert the stat results are correct
+- name: Assert the stat results are correct
   assert:
     that:
       - "stat_results.stat.exists == true"
@@ -82,32 +92,39 @@
       - "stat_results.stat.issock == false"
       - "stat_results.stat.checksum == ('foo.txt\n'|hash('sha1'))"
 
-- name: verify that the legacy md5sum is correct
+- name: Verify that the legacy md5sum is correct
   assert:
     that:
       - "stat_results.stat.md5 == ('foo.txt\n'|hash('md5'))"
   when: ansible_fips|bool != True
 
-- name: overwrite the file via same means
-  copy: src=foo.txt dest={{remote_file}}
+- name: Overwrite the file via same means
+  copy:
+    src: foo.txt
+    dest: "{{ remote_file }}"
   register: copy_result2
 
-- name: assert that the file was not changed
+- name: Assert that the file was not changed
   assert:
     that:
       - "not copy_result2|changed"
 
-- name: overwrite the file using the content system
-  copy: content="modified" dest={{remote_file}}
+- name: Overwrite the file using the content system
+  copy:
+    content: "modified"
+    dest: "{{ remote_file }}"
   register: copy_result3
 
-- name: check the stat results of the file
-  stat: path={{remote_file}}
+- name: Check the stat results of the file
+  stat:
+    path: "{{ remote_file }}"
   register: stat_results
 
-#- debug: var=stat_results
+- debug:
+    var: stat_results
+    verbosity: 1
 
-- name: assert that the file has changed
+- name: Assert that the file has changed
   assert:
      that:
        - "copy_result3|changed"
@@ -115,17 +132,23 @@
        - "stat_results.stat.checksum == ('modified'|hash('sha1'))"
        - "stat_results.stat.mode != '0700'"
 
-- name: overwrite the file again using the content system, also passing along file params
-  copy: content="modified" dest={{remote_file}} mode=0700
+- name: Overwrite the file again using the content system, also passing along file params
+  copy:
+    content: "modified"
+    dest: "{{ remote_file }}"
+    mode: 0700
   register: copy_result4
 
-- name: check the stat results of the file
-  stat: path={{remote_file}}
+- name: Check the stat results of the file
+  stat:
+    path: "{{ remote_file }}"
   register: stat_results
 
-#- debug: var=stat_results
+- debug:
+    var: stat_results
+    verbosity: 1
 
-- name: assert that the file has changed
+- name: Assert that the file has changed
   assert:
      that:
        - "copy_result3|changed"
@@ -133,17 +156,35 @@
        - "stat_results.stat.checksum == ('modified'|hash('sha1'))"
        - "stat_results.stat.mode == '0700'"
 
-- name: try invalid copy input location fails
-  copy: src=invalid_file_location_does_not_exist dest={{remote_dir}}/file.txt
+- name: Try invalid copy input location fails
+  copy:
+    src: invalid_file_location_does_not_exist
+    dest: "{{ remote_dir }}/file.txt"
   ignore_errors: True
   register: failed_copy
 
-- name: assert that invalid source failed
+- name: Assert that invalid source failed
   assert:
     that:
       - "failed_copy.failed"
       - "'invalid_file_location_does_not_exist' in failed_copy.msg"
 
+- name: Try empty source to ensure it fails
+  copy:
+    src: ''
+    dest: "{{ remote_dir }}"
+  ignore_errors: True
+  register: failed_copy
+
+- debug:
+    var: failed_copy
+    verbosity: 1
+
+- name: Assert that empty source failed
+  assert:
+    that:
+      - failed_copy | failed
+
 - name: Clean up
   file:
     path: "{{ remote_file }}"
@@ -156,14 +197,16 @@
     mode: 0500
   register: copy_results
 
-- name: check the stat results of the file
+- name: Check the stat results of the file
   stat:
     path: '{{ remote_file }}'
   register: stat_results
 
-#- debug: var=stat_results
+- debug:
+    var: stat_results
+    verbosity: 1
 
-- name: assert that the file has changed
+- name: Assert that the file has changed
   assert:
      that:
        - "copy_results|changed"
@@ -185,12 +228,12 @@
     mode: preserve
   register: copy_results
 
-- name: check the stat results of the file
+- name: Check the stat results of the file
   stat:
     path: '{{ remote_dir }}/copy-foo.txt'
   register: stat_results
 
-- name: assert that the file has changed and has correct mode
+- name: Assert that the file has changed and has correct mode
   assert:
      that:
        - "copy_results|changed"
@@ -208,80 +251,96 @@
     state: directory
   connection: local
 
-- name: set the output subdirectory
+- name: Set the output subdirectory
   set_fact: remote_subdir={{remote_dir}}/sub
 
-- name: make an output subdirectory
-  file: name={{remote_subdir}} state=directory
+- name: Make an output subdirectory
+  file:
+    name: "{{ remote_subdir }}"
+    state: directory
 
-- name: setup link target for absolute link
-  copy: dest=/tmp/ansible-test-abs-link content=target
+- name: Setup link target for absolute link
+  copy:
+    dest: /tmp/ansible-test-abs-link
+    content: target
   connection: local
 
-- name: setup link target dir for absolute link
-  file: dest=/tmp/ansible-test-abs-link-dir state=directory
+- name: Setup link target dir for absolute link
+  file:
+    dest: /tmp/ansible-test-abs-link-dir
+    state: directory
   connection: local
 
-- name: test recursive copy to directory no trailing slash, local_follow=False
-  copy: src=subdir dest={{remote_subdir}} directory_mode=0700 local_follow=False
+- name: Test recursive copy to directory no trailing slash, local_follow=False
+  copy:
+    src: subdir
+    dest: "{{ remote_subdir }}"
+    directory_mode: 0700
+    local_follow: False
   register: recursive_copy_result
 
-#- debug: var=recursive_copy_result
-- name: assert that the recursive copy did something
+- debug:
+    var: recursive_copy_result
+    verbosity: 1
+
+- name: Assert that the recursive copy did something
   assert:
     that:
       - "recursive_copy_result|changed"
 
-- name: check that a file in a directory was transferred
-  stat: path={{remote_dir}}/sub/subdir/bar.txt
+- name: Check that a file in a directory was transferred
+  stat:
+    path: "{{ remote_dir }}/sub/subdir/bar.txt"
   register: stat_bar
 
-- name: check that a file in a deeper directory was transferred
-  stat: path={{remote_dir}}/sub/subdir/subdir2/baz.txt
+- name: Check that a file in a deeper directory was transferred
+  stat:
+    path: "{{ remote_dir }}/sub/subdir/subdir2/baz.txt"
   register: stat_bar2
 
-- name: check that a file in a directory whose parent contains a directory alone was transferred
-  stat: path={{remote_dir}}/sub/subdir/subdir2/subdir3/subdir4/qux.txt
+- name: Check that a file in a directory whose parent contains a directory alone was transferred
+  stat:
+    path: "{{ remote_dir }}/sub/subdir/subdir2/subdir3/subdir4/qux.txt"
   register: stat_bar3
 
-- name: assert recursive copy files
+- name: Assert recursive copy files
   assert:
     that:
       - "stat_bar.stat.exists"
       - "stat_bar2.stat.exists"
       - "stat_bar3.stat.exists"
 
-- name: check symlink to absolute path
+- name: Check symlink to absolute path
   stat:
     path: '{{ remote_dir }}/sub/subdir/subdir1/ansible-test-abs-link'
   register: stat_abs_link
 
-- name: check symlink to relative path
+- name: Check symlink to relative path
   stat:
     path: '{{ remote_dir }}/sub/subdir/subdir1/bar.txt'
   register: stat_relative_link
 
-- name: check symlink to self
+- name: Check symlink to self
   stat:
     path: '{{ remote_dir }}/sub/subdir/subdir1/invalid'
   register: stat_self_link
 
-- name: check symlink to nonexistent file
+- name: Check symlink to nonexistent file
   stat:
     path: '{{ remote_dir }}/sub/subdir/subdir1/invalid2'
   register: stat_invalid_link
 
-- name: check symlink to directory in copy
+- name: Check symlink to directory in copy
   stat:
    path: '{{ remote_dir }}/sub/subdir/subdir1/subdir3'
   register: stat_dir_in_copy_link
 
-- name: check symlink to directory outside of copy
+- name: Check symlink to directory outside of copy
   stat:
     path: '{{ remote_dir }}/sub/subdir/subdir1/ansible-test-abs-link-dir'
   register: stat_dir_outside_copy_link
 
-- name: assert recursive copy symlinks local_follow=False
+- name: Assert recursive copy symlinks local_follow=False
   assert:
     that:
       - "stat_abs_link.stat.exists"
@@ -303,8 +362,9 @@
       - "stat_dir_outside_copy_link.stat.islnk"
       - "'/tmp/ansible-test-abs-link-dir' == stat_dir_outside_copy_link.stat.lnk_target"
 
-- name: stat the recursively copied directories
-  stat: path={{remote_dir}}/sub/{{item}}
+- name: Stat the recursively copied directories
+  stat:
+    path: "{{ remote_dir }}/sub/{{ item }}"
   register: dir_stats
   with_items:
     - "subdir"
@@ -314,104 +374,130 @@
     - "subdir/subdir2/subdir3"
     - "subdir/subdir2/subdir3/subdir4"
 
-#- debug: var=dir_stats
-- name: assert recursive copied directories mode (1)
+- debug:
+    var: stat_results
+    verbosity: 1
+
+- name: Assert recursive copied directories mode (1)
   assert:
     that:
       - "item.stat.exists"
       - "item.stat.mode == '0700'"
   with_items: "{{dir_stats.results}}"
 
-- name: test recursive copy to directory no trailing slash, local_follow=False second time
-  copy: src=subdir dest={{remote_subdir}} directory_mode=0700 local_follow=False
+- name: Test recursive copy to directory no trailing slash, local_follow=False second time
+  copy:
+    src: subdir
+    dest: "{{ remote_subdir }}"
+    directory_mode: 0700
+    local_follow: False
   register: recursive_copy_result
 
-- name: assert that the second copy did not change anything
+- name: Assert that the second copy did not change anything
   assert:
     that:
       - "not recursive_copy_result|changed"
 
-- name: cleanup the recursive copy subdir
-  file: name={{remote_subdir}} state=absent
+- name: Cleanup the recursive copy subdir
+  file:
+    name: "{{ remote_subdir }}"
+    state: absent
 
 #
 # Recursive copy with local_follow=False, trailing slash
 #
 
-- name: set the output subdirectory
-  set_fact: remote_subdir={{remote_dir}}/sub
+- name: Set the output subdirectory
+  set_fact:
+    remote_subdir: "{{ remote_dir }}/sub"
 
-- name: make an output subdirectory
-  file: name={{remote_subdir}} state=directory
+- name: Make an output subdirectory
+  file:
+    name: "{{ remote_subdir }}"
+    state: directory
 
-- name: setup link target for absolute link
-  copy: dest=/tmp/ansible-test-abs-link content=target
+- name: Setup link target for absolute link
+  copy:
+    dest: /tmp/ansible-test-abs-link
+    content: target
   connection: local
 
-- name: setup link target dir for absolute link
-  file: dest=/tmp/ansible-test-abs-link-dir state=directory
+- name: Setup link target dir for absolute link
+  file:
+    dest: /tmp/ansible-test-abs-link-dir
+    state: directory
   connection: local
 
-- name: test recursive copy to directory trailing slash, local_follow=False
-  copy: src=subdir/ dest={{remote_subdir}} directory_mode=0700 local_follow=False
+- name: Test recursive copy to directory trailing slash, local_follow=False
+  copy:
+    src: subdir/
+    dest: "{{ remote_subdir }}"
+    directory_mode: 0700
+    local_follow: False
   register: recursive_copy_result
 
-#- debug: var=recursive_copy_result
-- name: assert that the recursive copy did something
+- debug:
+    var: recursive_copy_result
+    verbosity: 1
+
+- name: Assert that the recursive copy did something
   assert:
     that:
       - "recursive_copy_result|changed"
 
-- name: check that a file in a directory was transferred
-  stat: path={{remote_dir}}/sub/bar.txt
+- name: Check that a file in a directory was transferred
+  stat:
+    path: "{{ remote_dir }}/sub/bar.txt"
   register: stat_bar
 
-- name: check that a file in a deeper directory was transferred
-  stat: path={{remote_dir}}/sub/subdir2/baz.txt
+- name: Check that a file in a deeper directory was transferred
+  stat:
+    path: "{{ remote_dir }}/sub/subdir2/baz.txt"
   register: stat_bar2
 
-- name: check that a file in a directory whose parent contains a directory alone was transferred
-  stat: path={{remote_dir}}/sub/subdir2/subdir3/subdir4/qux.txt
+- name: Check that a file in a directory whose parent contains a directory alone was transferred
+  stat:
+    path: "{{ remote_dir }}/sub/subdir2/subdir3/subdir4/qux.txt"
   register: stat_bar3
 
-- name: assert recursive copy files
+- name: Assert recursive copy files
   assert:
     that:
       - "stat_bar.stat.exists"
       - "stat_bar2.stat.exists"
       - "stat_bar3.stat.exists"
 
-- name: check symlink to absolute path
+- name: Check symlink to absolute path
   stat:
     path: '{{ remote_dir }}/sub/subdir1/ansible-test-abs-link'
   register: stat_abs_link
 
-- name: check symlink to relative path
+- name: Check symlink to relative path
   stat:
     path: '{{ remote_dir }}/sub/subdir1/bar.txt'
   register: stat_relative_link
 
-- name: check symlink to self
+- name: Check symlink to self
   stat:
     path: '{{ remote_dir }}/sub/subdir1/invalid'
   register: stat_self_link
 
-- name: check symlink to nonexistent file
+- name: Check symlink to nonexistent file
   stat:
     path: '{{ remote_dir }}/sub/subdir1/invalid2'
   register: stat_invalid_link
 
-- name: check symlink to directory in copy
+- name: Check symlink to directory in copy
   stat:
     path: '{{ remote_dir }}/sub/subdir1/subdir3'
   register: stat_dir_in_copy_link
 
-- name: check symlink to directory outside of copy
+- name: Check symlink to directory outside of copy
   stat:
     path: '{{ remote_dir }}/sub/subdir1/ansible-test-abs-link-dir'
   register: stat_dir_outside_copy_link
 
-- name: assert recursive copy symlinks local_follow=False trailing slash
+- name: Assert recursive copy symlinks local_follow=False trailing slash
   assert:
     that:
       - "stat_abs_link.stat.exists"
@@ -433,8 +519,9 @@
       - "stat_dir_outside_copy_link.stat.islnk"
       - "'/tmp/ansible-test-abs-link-dir' == stat_dir_outside_copy_link.stat.lnk_target"
 
-- name: stat the recursively copied directories
-  stat: path={{remote_dir}}/sub/{{item}}
+- name: Stat the recursively copied directories
+  stat:
+    path: "{{ remote_dir }}/sub/{{ item }}"
   register: dir_stats
   with_items:
     - "subdira"
@@ -443,70 +530,97 @@
     - "subdir2/subdir3"
     - "subdir2/subdir3/subdir4"
 
-#- debug: var=dir_stats
-- name: assert recursive copied directories mode (2)
+- debug:
+    var: dir_stats
+    verbosity: 1
+
+- name: Assert recursive copied directories mode (2)
   assert:
     that:
       - "item.stat.mode == '0700'"
   with_items: "{{dir_stats.results}}"
 
-- name: test recursive copy to directory trailing slash, local_follow=False second time
-  copy: src=subdir/ dest={{remote_subdir}} directory_mode=0700 local_follow=False
+- name: Test recursive copy to directory trailing slash, local_follow=False second time
+  copy:
+    src: subdir/
+    dest: "{{ remote_subdir }}"
+    directory_mode: 0700
+    local_follow: False
   register: recursive_copy_result
 
-- name: assert that the second copy did not change anything
+- name: Assert that the second copy did not change anything
   assert:
     that:
       - "not recursive_copy_result|changed"
 
-- name: cleanup the recursive copy subdir
-  file: name={{remote_subdir}} state=absent
+- name: Cleanup the recursive copy subdir
+  file:
+    name: "{{ remote_subdir }}"
+    state: absent
 
 #
 # test recursive copy local_follow=True, no trailing slash
 #
 
-- name: set the output subdirectory
-  set_fact: remote_subdir={{remote_dir}}/sub
+- name: Set the output subdirectory
+  set_fact:
+    remote_subdir: "{{ remote_dir }}/sub"
 
-- name: make an output subdirectory
-  file: name={{remote_subdir}} state=directory
+- name: Make an output subdirectory
+  file:
+    name: "{{ remote_subdir }}"
+    state: directory
 
-- name: setup link target for absolute link
-  copy: dest=/tmp/ansible-test-abs-link content=target
+- name: Setup link target for absolute link
+  copy:
+    dest: /tmp/ansible-test-abs-link
+    content: target
   connection: local
 
-- name: setup link target dir for absolute link
-  file: dest=/tmp/ansible-test-abs-link-dir state=directory
+- name: Setup link target dir for absolute link
+  file:
+    dest: /tmp/ansible-test-abs-link-dir
+    state: directory
   connection: local
 
-- name: test recursive copy to directory no trailing slash, local_follow=True
-  copy: src=subdir dest={{remote_subdir}} directory_mode=0700 local_follow=True
+- name: Test recursive copy to directory no trailing slash, local_follow=True
+  copy:
+    src: subdir
+    dest: "{{ remote_subdir }}"
+    directory_mode: 0700
+    local_follow: True
   register: recursive_copy_result
 
-#- debug: var=recursive_copy_result
-- name: assert that the recursive copy did something
+- debug:
+    var: recursive_copy_result
+    verbosity: 1
+
+- name: Assert that the recursive copy did something
   assert:
     that:
       - "recursive_copy_result|changed"
 
-- name: check that a file in a directory was transferred
-  stat: path={{remote_dir}}/sub/subdir/bar.txt
+- name: Check that a file in a directory was transferred
+  stat:
+    path: "{{ remote_dir }}/sub/subdir/bar.txt"
   register: stat_bar
 
-- name: check that a file in a deeper directory was transferred
-  stat: path={{remote_dir}}/sub/subdir/subdir2/baz.txt
+- name: Check that a file in a deeper directory was transferred
+  stat:
+    path: "{{ remote_dir }}/sub/subdir/subdir2/baz.txt"
   register: stat_bar2
 
-- name: check that a file in a directory whose parent contains a directory alone was transferred
-  stat: path={{remote_dir}}/sub/subdir/subdir2/subdir3/subdir4/qux.txt
+- name: Check that a file in a directory whose parent contains a directory alone was transferred
+  stat:
+    path: "{{ remote_dir }}/sub/subdir/subdir2/subdir3/subdir4/qux.txt"
   register: stat_bar3
 
-- name: check that a file in a directory whose parent is a symlink was transferred
-  stat: path={{remote_dir}}/sub/subdir/subdir1/subdir3/subdir4/qux.txt
+- name: Check that a file in a directory whose parent is a symlink was transferred
+  stat:
+    path: "{{ remote_dir }}/sub/subdir/subdir1/subdir3/subdir4/qux.txt"
   register: stat_bar4
 
-- name: assert recursive copy files
+- name: Assert recursive copy files
   assert:
     that:
       - "stat_bar.stat.exists"
@@ -514,37 +628,37 @@
       - "stat_bar3.stat.exists"
       - "stat_bar4.stat.exists"
 
-- name: check symlink to absolute path
+- name: Check symlink to absolute path
   stat:
     path: '{{ remote_dir }}/sub/subdir/subdir1/ansible-test-abs-link'
   register: stat_abs_link
 
-- name: check symlink to relative path
+- name: Check symlink to relative path
   stat:
     path: '{{ remote_dir }}/sub/subdir/subdir1/bar.txt'
   register: stat_relative_link
 
-- name: check symlink to self
+- name: Check symlink to self
   stat:
     path: '{{ remote_dir }}/sub/subdir/subdir1/invalid'
   register: stat_self_link
 
-- name: check symlink to nonexistent file
+- name: Check symlink to nonexistent file
   stat:
     path: '{{ remote_dir }}/sub/subdir/subdir1/invalid2'
   register: stat_invalid_link
 
-- name: check symlink to directory in copy
+- name: Check symlink to directory in copy
   stat:
     path: '{{ remote_dir }}/sub/subdir/subdir1/subdir3'
   register: stat_dir_in_copy_link
 
-- name: check symlink to directory outside of copy
+- name: Check symlink to directory outside of copy
   stat:
     path: '{{ remote_dir }}/sub/subdir/subdir1/ansible-test-abs-link-dir'
   register: stat_dir_outside_copy_link
 
-- name: assert recursive copy symlinks local_follow=True
+- name: Assert recursive copy symlinks local_follow=True
   assert:
     that:
       - "stat_abs_link.stat.exists"
@@ -567,8 +681,9 @@
       - "not stat_dir_outside_copy_link.stat.islnk"
       - "stat_dir_outside_copy_link.stat.isdir"
 
-- name: stat the recursively copied directories
-  stat: path={{remote_dir}}/sub/{{item}}
+- name: Stat the recursively copied directories
+  stat:
+    path: "{{ remote_dir }}/sub/{{ item }}"
   register: dir_stats
   with_items:
     - "subdir"
@@ -580,24 +695,33 @@
     - "subdir/subdir2/subdir3"
     - "subdir/subdir2/subdir3/subdir4"
 
-#- debug: var=dir_stats
-- name: assert recursive copied directories mode (3)
+- debug:
+    var: dir_stats
+    verbosity: 1
+
+- name: Assert recursive copied directories mode (3)
   assert:
     that:
       - "item.stat.mode == '0700'"
   with_items: "{{dir_stats.results}}"
 
-- name: test recursive copy to directory no trailing slash, local_follow=True second time
-  copy: src=subdir dest={{remote_subdir}} directory_mode=0700 local_follow=True
+- name: Test recursive copy to directory no trailing slash, local_follow=True second time
+  copy:
+    src: subdir
+    dest: "{{ remote_subdir }}"
+    directory_mode: 0700
+    local_follow: True
   register: recursive_copy_result
 
-- name: assert that the second copy did not change anything
+- name: Assert that the second copy did not change anything
   assert:
     that:
       - "not recursive_copy_result|changed"
 
-- name: cleanup the recursive copy subdir
-  file: name={{remote_subdir}} state=absent
+- name: Cleanup the recursive copy subdir
+  file:
+    name: "{{ remote_subdir }}"
+    state: absent
 
 #
 # Recursive copy of tricky symlinks
@@ -678,12 +802,12 @@
       - '{{ remote_dir_expanded[0] == "/" }}'
 
 - block:
-  - name: create a directory to copy
+  - name: Create a directory to copy
     file:
       path: '{{ local_temp_dir }}/source_recursive'
       state: directory
 
-  - name: create a file inside of the directory
+  - name: Create a file inside of the directory
     copy:
       content: "testing"
       dest: '{{ local_temp_dir }}/source_recursive/file'
@@ -699,16 +823,20 @@
     src: '{{ local_temp_dir }}/source_recursive'
     dest: '{{ remote_dir }}/destination'
 
-- name: stat the recursively copied directory
-  stat: path={{remote_dir}}/destination/{{item}}
+- name: Stat the recursively copied directory
+  stat:
+    path: "{{ remote_dir }}/destination/{{ item }}"
   register: copied_stat
   with_items:
     - "source_recursive"
     - "source_recursive/file"
     - "file"
 
-#- debug: var=copied_stat
-- name: assert with no trailing slash, directory and file is copied
+- debug:
+    var: copied_stat
+    verbosity: 1
+
+- name: Assert with no trailing slash, directory and file is copied
   assert:
     that:
       - "copied_stat.results[0].stat.exists"
@@ -732,16 +860,20 @@
     src: '{{ local_temp_dir }}/source_recursive/'
     dest: '{{ remote_dir }}/destination'
 
-- name: stat the recursively copied directory
-  stat: path={{ remote_dir }}/destination/{{ item }}
+- name: Stat the recursively copied directory
+  stat:
+    path: "{{ remote_dir }}/destination/{{ item }}"
   register: copied_stat
   with_items:
     - "source_recursive"
     - "source_recursive/file"
     - "file"
 
-#- debug: var=copied_stat
-- name: assert with trailing slash, only the file is copied
+- debug:
+    var: copied_stat
+    verbosity: 1
+
+- name: Assert with trailing slash, only the file is copied
   assert:
     that:
       - "not copied_stat.results[0].stat.exists"
@@ -752,7 +884,7 @@
 # issue 8394
 #
 
-- name: create a file with content and a literal multiline block
+- name: Create a file with content and a literal multiline block
   copy: |
     content='this is the first line
     this is the second line
@@ -763,9 +895,11 @@
     dest={{remote_dir}}/multiline.txt
   register: copy_result6
 
-#- debug: var=copy_result6
+- debug:
+    var: copy_result6
+    verbosity: 1
 
-- name: assert the multiline file was created correctly
+- name: Assert the multiline file was created correctly
   assert:
     that:
       - "copy_result6.changed"
@@ -775,76 +909,97 @@
 # test overwriting a file as an unprivileged user (pull request #8624)
 # this can't be relative to {{remote_dir}} as ~root usually has mode 700
 - block:
-  - name: create world writable directory
-    file: dest=/tmp/worldwritable state=directory mode=0777
+  - name: Create world writable directory
+    file:
+      dest: /tmp/worldwritable
+      state: directory
+      mode: 0777
 
-  - name: create world writable file
-    copy: dest=/tmp/worldwritable/file.txt content="bar" mode=0666
+  - name: Create world writable file
+    copy:
+      dest: /tmp/worldwritable/file.txt
+      content: "bar"
+      mode: 0666
 
-  - name: overwrite the file as user nobody
-    copy: dest=/tmp/worldwritable/file.txt content="baz"
+  - name: Overwrite the file as user nobody
+    copy:
+      dest: /tmp/worldwritable/file.txt
+      content: "baz"
     become: yes
     become_user: nobody
     register: copy_result7
 
-  - name: assert the file was overwritten
+  - name: Assert the file was overwritten
     assert:
       that:
         - "copy_result7.changed"
         - "copy_result7.dest == '/tmp/worldwritable/file.txt'"
         - "copy_result7.checksum == ('baz'|hash('sha1'))"
 
-  - name: clean up
-    file: dest=/tmp/worldwritable state=absent
+  - name: Clean up
+    file:
+      dest: /tmp/worldwritable
+      state: absent
 
   remote_user: root
 
 # test overwriting a link using "follow=yes" so that the link
 # is preserved and the link target is updated
 
-- name: create a test file to symlink to
-  copy: dest={{remote_dir}}/follow_test content="this is the follow test file\n"
+- name: Create a test file to symlink to
+  copy:
+    dest: "{{ remote_dir }}/follow_test"
+    content: "this is the follow test file\n"
 
-- name: create a symlink to the test file
-  file: path={{remote_dir}}/follow_link src='./follow_test' state=link
+- name: Create a symlink to the test file
+  file:
+    path: "{{ remote_dir }}/follow_link"
+    src: './follow_test'
+    state: link
 
-- name: update the test file using follow=True to preserve the link
-  copy: dest={{remote_dir}}/follow_link content="this is the new content\n" follow=yes
+- name: Update the test file using follow=True to preserve the link
+  copy:
+    dest: "{{ remote_dir }}/follow_link"
+    content: "this is the new content\n"
+    follow: yes
   register: replace_follow_result
 
-- name: stat the link path
-  stat: path={{remote_dir}}/follow_link
+- name: Stat the link path
+  stat:
+    path: "{{ remote_dir }}/follow_link"
   register: stat_link_result
 
-- name: assert that the link is still a link
+- name: Assert that the link is still a link
   assert:
     that:
     - stat_link_result.stat.islnk
 
-- name: get the checksum of the link target
+- name: Get the checksum of the link target
   shell: "{{ sha1sum.stdout }} {{remote_dir}}/follow_test | cut -f1 -sd ' '"
   register: target_file_result
 
-- name: assert that the link target was updated
+- name: Assert that the link target was updated
   assert:
     that:
     - replace_follow_result.checksum == target_file_result.stdout
 
-- name: update the test file using follow=False to overwrite the link
+- name: Update the test file using follow=False to overwrite the link
   copy:
     dest: '{{ remote_dir }}/follow_link'
     content: 'modified'
     follow: False
   register: copy_results
 
-- name: check the stat results of the file
+- name: Check the stat results of the file
   stat:
     path: '{{remote_dir}}/follow_link'
   register: stat_results
 
-#- debug: var=stat_results
+- debug:
+    var: stat_results
+    verbosity: 1
 
-- name: assert that the file has changed and is not a link
+- name: Assert that the file has changed and is not a link
   assert:
      that:
        - "copy_results|changed"
@@ -861,34 +1016,34 @@
 # Recursive copying with symlinks tests
 #
 - block:
-  - name: create a test dir to copy
+  - name: Create a test dir to copy
     file:
       path: '{{ local_temp_dir }}/top_dir'
       state: directory
 
-  - name: create a test dir to symlink to
+  - name: Create a test dir to symlink to
     file:
       path: '{{ local_temp_dir }}/linked_dir'
       state: directory
 
-  - name: create a file in the test dir
+  - name: Create a file in the test dir
     copy:
       dest: '{{ local_temp_dir }}/linked_dir/file1'
       content: 'hello world'
 
-  - name: create a link to the test dir
+  - name: Create a link to the test dir
     file:
       path: '{{ local_temp_dir }}/top_dir/follow_link_dir'
       src: '{{ local_temp_dir }}/linked_dir'
       state: link
 
-  - name: create a circular subdir
+  - name: Create a circular subdir
     file:
       path: '{{ local_temp_dir }}/top_dir/subdir'
       state: directory
 
   ### FIXME:  Also add a test for a relative symlink
-  - name: create a circular symlink
+  - name: Create a circular symlink
     file:
       path: '{{ local_temp_dir }}/top_dir/subdir/circle'
       src: '{{ local_temp_dir }}/top_dir/'
@@ -896,28 +1051,28 @@
 
   connection: local
 
-- name: copy the directory's link
+- name: Copy the directory's link
   copy:
     src: '{{ local_temp_dir }}/top_dir'
     dest: '{{ remote_dir }}/new_dir'
     local_follow: True
 
-- name: stat the copied path
+- name: Stat the copied path
   stat:
     path: '{{ remote_dir }}/new_dir/top_dir/follow_link_dir'
   register: stat_dir_result
 
-- name: stat the copied file
+- name: Stat the copied file
   stat:
     path: '{{ remote_dir }}/new_dir/top_dir/follow_link_dir/file1'
   register: stat_file_in_dir_result
 
-- name: stat the circular symlink
+- name: Stat the circular symlink
   stat:
     path: '{{ remote_dir }}/new_dir/top_dir/subdir/circle'
   register: stat_circular_symlink_result
 
-- name: assert that the directory exists
+- name: Assert that the directory exists
   assert:
     that:
     - stat_dir_result.stat.exists


Code review comments:
diff_hunk: @@ -407,7 +407,7 @@ def run(self, tmp=None, task_vars=None):
         local_follow = boolean(self._task.args.get('local_follow', True), strict=False)
 
         result['failed'] = True
-        if (source is None and content is None) or dest is None:
+        if ((not source or source is None) and content is None) or dest is None:
path: lib/ansible/plugins/action/copy.py
text: @user1:
I would split it out and have more specific error messages:
```python
    if not source and content is None:
        result['msg'] = "src (or content) is required"
    elif dest is not None:
        result['msg'] = "dest is required"
    elif source and content is not None:
        result['msg'] = "src and content are mutually exclusive"
```
Can dest be an empty string ? (current script directory ?)

@user1:
PS I also modified the last conditional in that list.