Repository: ansible-collections/community.general

Issue:
ini_file: Incorrect handling when multiple options with the same name exist
##### SUMMARY
Option handling is inconsistent when there's multiple of them and there is seemingly no way to keep multiple options.

##### ISSUE TYPE
- Bug Report

##### COMPONENT NAME
ini_file

##### ANSIBLE VERSION
```
ansible 2.9.7
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.8/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 3.8.2 (default, Apr  8 2020, 14:31:25) [GCC 9.3.0]
```

##### CONFIGURATION
```
N/A
```

##### OS / ENVIRONMENT
Arch Linux


##### STEPS TO REPRODUCE
Play:
```
- name: Ensure "Include=/etc/pacman.d/mirrorlist is in section "[multilib]" in specified file
  ini_file:
    path: /etc/pacman.conf
    section: multilib
    option: Include
    value: /etc/pacman.d/mirrorlist
```

How the file /etc/pacman.conf looks before the play:
```
[multilib]
Include = /etc/pacman.d/pacserve
Include = /etc/pacman.d/mirrorlist
```
After:

```
[multilib]
Include = /etc/pacman.d/mirrorlist
```

I'd like the play not to clobber existing Options, but there does not seem to be a way to do that.

Additionally, if the file looks like this, with the matching entry first, nothing is changed, which is inconsistent.

```
[multilib]
Include = /etc/pacman.d/mirrorlist
Include = /etc/pacman.d/pacserve
```





Discussion:
Files identified in the description:
* [`plugins/modules/files/ini_file.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/files/ini_file.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 --->
cc @jpmens @noseka1
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
There is no "The INI format", there are many different interpretations out there what valid INI formats should be. In particular, there is no consensus whether multiple options with the same name can exist or not (see https://en.wikipedia.org/wiki/INI_file#Duplicate_names). So this is rather a feature request than a bug, because the module documents nowhere that it supports duplicate names.

Also, as the module interface is right now (with `value` being string and not list), I would expect the module to always end up in the state where the option appears exactly once in the INI file, if it would support duplicate entries.

BTW, there used to be a PR adding this feature: ansible/ansible#19536 Unfortunately it does not mention the interface and all changes are gone (maybe accidentally rebased to nil).
>BTW, there used to be a PR adding this feature: ansible/ansible#19536 Unfortunately it does not mention the interface and all changes are gone (maybe accidentally rebased to nil).


I've contacted author of the PR and they don't have the fork anymore. 

Also tried unsuccessfully reaching @StoneISStephan about it, so I guess the PR is really gone.
Loks like whole StonelSStephan/ansible repository is gone as well. Last comments suggest that there wer some unseolved problems with that PR anyway.
BTW: how about multpiple sections with same name? ini_file module doesn't care about them at all, all changes seem to be applied only to first [section] appeareance. Any suggestions how to handle them properly? (combine together in one section in output OR leave multiple sections but treat all the options as if they were in one section OR fail with error?) Wikipedia says that 'Interpretation of multiple section declarations with the same name also varies' as well, so all possibilities may apply :/
Hmm, multiple sections with the same name sound... not that simple to handle, indeed.

For multiple values (in one section), a good interface would probably be having two options (mainly for backwards compatibility): `value` of type `str`, and `values` of type `list[str]`, which are mutually exclusive, and `value: v` is equivalent to `values: [v]`.

For multiple sections, it probably makes sense to provide an index so that the `i`-th section of that name is meant (with failure if a section with that index does not exist, or some configurability of what to do when it doesn't exist, like fail, use the last existing, or add a new one).
> For multiple values (in one section), a good interface would probably be having two options (mainly for backwards compatibility): `value` of type `str`, and `values` of type `list[str]`, which are mutually exclusive, and `value: v` is equivalent to `values: [v]`.

This would require repeating all `values` when you want to add only one more Include setting to section... I propose additonaly: `state: add/remove` with following semantics:
* absent: remove `option =` lines (all when `value[s] == None` or only `option = value[s]`)
* present: keep/insert `option = value[s]` lines and remove all others
* add/remove: just ensure that specified settings are added/removed but do not touch others

About multiple section with some name: they aren't very popular anyway, so maybe just have ini_file module operate on first `[section]` instance only?
Hi there,

I wanted to comment that systemd configuration frequently uses multiple options with a same name on their configuration files. Without support for multiple options with the same name in `ini_file` module, idempotent systemd configuration can be achieved with modules such as `template` or `blockinfile`, however those alternatives may unwillingly remove settings unmanaged by Ansible.

Support for multiple same-name options in `ini_file` module would be much appreciated.

## Example case 1

An administrator can define startup ordering by specifying multiple `After=` and `Before=` arguments, as documented in [systemd.unit(5)](https://www.freedesktop.org/software/systemd/man/systemd.unit.html) man page.

```
# /etc/systemd/system/foo.service
[Unit]
After=network.target
After=network-online.target
Before=httpd.service
Before=mariadb.service
```
## Example case 2

Environment variables for processes started via systemd can be defined by adding one or more `Environment=` options in `[Service]` section, as documented in [systemd.exec(5)](https://www.freedesktop.org/software/systemd/man/systemd.exec.html) man page.

```
# /etc/systemd/system/foo.service
[Service]
Environment=PIDFILE=/run/foo.pid
Environment=CACHEFILE=/var/lib/foo/cache.dat
Environment=APITOKEN=55166003-10db-4919-92aa-965b31093151
```

## Example case 3

An administrator willing to override a package-maintained configuration parameter usually create an override file, sets a blank value in order to clear the default values and, then, sets desired values after the blank one. Details are documented in [systemd.unit(5)](https://www.freedesktop.org/software/systemd/man/systemd.unit.html) man page.

```
# /etc/systemd/system/foo.service.d/override.conf
[Unit]
ConditionPathExists=
ConditionPathExists=/this/file/must/exist
ConditionPathExists=!/this/file/must/not/exist
```


Resolving patch:
diff --git a/plugins/modules/files/ini_file.py b/plugins/modules/files/ini_file.py
--- a/plugins/modules/files/ini_file.py
+++ b/plugins/modules/files/ini_file.py
@@ -47,7 +47,18 @@
     description:
       - The string value to be associated with an I(option).
       - May be omitted when removing an I(option).
+      - Mutually exclusive with I(values).
+      - I(value=v) is equivalent to I(values=[v]).
     type: str
+  values:
+    description:
+      - The string value to be associated with an I(option).
+      - May be omitted when removing an I(option).
+      - Mutually exclusive with I(value).
+      - I(value=v) is equivalent to I(values=[v]).
+    type: list
+    elements: str
+    version_added: 3.6.0
   backup:
     description:
       - Create a backup file including the timestamp information so you can get
@@ -56,10 +67,25 @@
     default: no
   state:
     description:
-      - If set to C(absent) the option or section will be removed if present instead of created.
+      - If set to C(absent) and I(exclusive) set to C(yes) all matching I(option) lines are removed.
+      - If set to C(absent) and I(exclusive) set to C(no) the specified C(option=value) lines are removed,
+        but the other I(option)s with the same name are not touched.
+      - If set to C(present) and I(exclusive) set to C(no) the specified C(option=values) lines are added,
+        but the other I(option)s with the same name are not touched.
+      - If set to C(present) and I(exclusive) set to C(yes) all given C(option=values) lines will be
+        added and the other I(option)s with the same name are removed.
     type: str
     choices: [ absent, present ]
     default: present
+  exclusive:
+    description:
+      - If set to C(yes) (default), all matching I(option) lines are removed when I(state=absent),
+        or replaced when I(state=present).
+      - If set to C(no), only the specified I(value(s)) are added when I(state=present),
+        or removed when I(state=absent), and existing ones are not modified.
+    type: bool
+    default: yes
+    version_added: 3.6.0
   no_extra_spaces:
     description:
       - Do not insert spaces before and after '=' symbol.
@@ -103,6 +129,27 @@
     option: temperature
     value: cold
     backup: yes
+
+- name: Add "beverage=lemon juice" is in section "[drinks]" in specified file
+  community.general.ini_file:
+    path: /etc/conf
+    section: drinks
+    option: beverage
+    value: lemon juice
+    mode: '0600'
+    state: present
+    exclusive: no
+
+- name: Ensure multiple values "beverage=coke" and "beverage=pepsi" are in section "[drinks]" in specified file
+  community.general.ini_file:
+    path: /etc/conf
+    section: drinks
+    option: beverage
+    values:
+      - coke
+      - pepsi
+    mode: '0600'
+    state: present
 '''
 
 import io
@@ -117,24 +164,37 @@
 
 def match_opt(option, line):
     option = re.escape(option)
-    return re.match('[#;]?( |\t)*%s( |\t)*(=|$)' % option, line)
+    return re.match('[#;]?( |\t)*(%s)( |\t)*(=|$)( |\t)*(.*)' % option, line)
 
 
 def match_active_opt(option, line):
     option = re.escape(option)
-    return re.match('( |\t)*%s( |\t)*(=|$)' % option, line)
+    return re.match('( |\t)*(%s)( |\t)*(=|$)( |\t)*(.*)' % option, line)
+
 
+def update_section_line(changed, section_lines, index, changed_lines, newline, msg):
+    option_changed = section_lines[index] != newline
+    changed = changed or option_changed
+    if option_changed:
+        msg = 'option changed'
+    section_lines[index] = newline
+    changed_lines[index] = 1
+    return (changed, msg)
 
-def do_ini(module, filename, section=None, option=None, value=None,
-           state='present', backup=False, no_extra_spaces=False, create=True,
-           allow_no_value=False):
+
+def do_ini(module, filename, section=None, option=None, values=None,
+           state='present', exclusive=True, backup=False, no_extra_spaces=False,
+           create=True, allow_no_value=False):
 
     if section is not None:
         section = to_text(section)
     if option is not None:
         option = to_text(option)
-    if value is not None:
-        value = to_text(value)
+
+    # deduplicate entries in values
+    values_unique = []
+    [values_unique.append(to_text(value)) for value in values if value not in values_unique and value is not None]
+    values = values_unique
 
     diff = dict(
         before='',
@@ -145,7 +205,7 @@ def do_ini(module, filename, section=None, option=None, value=None,
 
     if not os.path.exists(filename):
         if not create:
-            module.fail_json(rc=257, msg='Destination %s does not exist !' % filename)
+            module.fail_json(rc=257, msg='Destination %s does not exist!' % filename)
         destpath = os.path.dirname(filename)
         if not os.path.exists(destpath) and not module.check_mode:
             os.makedirs(destpath)
@@ -185,74 +245,134 @@ def do_ini(module, filename, section=None, option=None, value=None,
         section = fake_section_name
 
     within_section = not section
-    section_start = 0
+    section_start = section_end = 0
     msg = 'OK'
     if no_extra_spaces:
         assignment_format = u'%s=%s\n'
     else:
         assignment_format = u'%s = %s\n'
 
+    option_no_value_present = False
+
     non_blank_non_comment_pattern = re.compile(to_text(r'^[ \t]*([#;].*)?$'))
 
+    before = after = []
+    section_lines = []
+
     for index, line in enumerate(ini_lines):
+        # find start and end of section
         if line.startswith(u'[%s]' % section):
             within_section = True
             section_start = index
         elif line.startswith(u'['):
             if within_section:
-                if state == 'present':
-                    # insert missing option line at the end of the section
-                    for i in range(index, 0, -1):
-                        # search backwards for previous non-blank or non-comment line
-                        if not non_blank_non_comment_pattern.match(ini_lines[i - 1]):
-                            if option and value is not None:
-                                ini_lines.insert(i, assignment_format % (option, value))
-                                msg = 'option added'
-                                changed = True
-                            elif option and value is None and allow_no_value:
-                                ini_lines.insert(i, '%s\n' % option)
-                                msg = 'option added'
-                                changed = True
-                            break
-                elif state == 'absent' and not option:
-                    # remove the entire section
-                    del ini_lines[section_start:index]
-                    msg = 'section removed'
+                section_end = index
+                break
+
+    before = ini_lines[0:section_start]
+    section_lines = ini_lines[section_start:section_end]
+    after = ini_lines[section_end:len(ini_lines)]
+
+    # Keep track of changed section_lines
+    changed_lines = [0] * len(section_lines)
+
+    # handling multiple instances of option=value when state is 'present' with/without exclusive is a bit complex
+    #
+    # 1. edit all lines where we have a option=value pair with a matching value in values[]
+    # 2. edit all the remaing lines where we have a matching option
+    # 3. delete remaining lines where we have a matching option
+    # 4. insert missing option line(s) at the end of the section
+
+    if state == 'present' and option:
+        for index, line in enumerate(section_lines):
+            if match_opt(option, line):
+                match = match_opt(option, line)
+                if values and match.group(6) in values:
+                    matched_value = match.group(6)
+                    if not matched_value and allow_no_value:
+                        # replace existing option with no value line(s)
+                        newline = u'%s\n' % option
+                        option_no_value_present = True
+                    else:
+                        # replace existing option=value line(s)
+                        newline = assignment_format % (option, matched_value)
+                    (changed, msg) = update_section_line(changed, section_lines, index, changed_lines, newline, msg)
+                    values.remove(matched_value)
+                elif not values and allow_no_value:
+                    # replace existing option with no value line(s)
+                    newline = u'%s\n' % option
+                    (changed, msg) = update_section_line(changed, section_lines, index, changed_lines, newline, msg)
+                    option_no_value_present = True
+                    break
+
+    if state == 'present' and exclusive and not allow_no_value:
+        # override option with no value to option with value if not allow_no_value
+        if len(values) > 0:
+            for index, line in enumerate(section_lines):
+                if not changed_lines[index] and match_active_opt(option, section_lines[index]):
+                    newline = assignment_format % (option, values.pop(0))
+                    (changed, msg) = update_section_line(changed, section_lines, index, changed_lines, newline, msg)
+                    if len(values) == 0:
+                        break
+        # remove all remaining option occurrences from the rest of the section
+        for index in range(len(section_lines) - 1, 0, -1):
+            if not changed_lines[index] and match_active_opt(option, section_lines[index]):
+                del section_lines[index]
+                del changed_lines[index]
+                changed = True
+                msg = 'option changed'
+
+    if state == 'present':
+        # insert missing option line(s) at the end of the section
+        for index in range(len(section_lines), 0, -1):
+            # search backwards for previous non-blank or non-comment line
+            if not non_blank_non_comment_pattern.match(section_lines[index - 1]):
+                if option and values:
+                    # insert option line(s)
+                    for element in values[::-1]:
+                        # items are added backwards, so traverse the list backwards to not confuse the user
+                        # otherwise some of their options might appear in reverse order for whatever fancy reason ¯\_(ツ)_/¯
+                        if element is not None:
+                            # insert option=value line
+                            section_lines.insert(index, assignment_format % (option, element))
+                            msg = 'option added'
+                            changed = True
+                        elif element is None and allow_no_value:
+                            # insert option with no value line
+                            section_lines.insert(index, u'%s\n' % option)
+                            msg = 'option added'
+                            changed = True
+                elif option and not values and allow_no_value and not option_no_value_present:
+                    # insert option with no value line(s)
+                    section_lines.insert(index, u'%s\n' % option)
+                    msg = 'option added'
                     changed = True
                 break
+
+    if state == 'absent':
+        if option:
+            if exclusive:
+                # delete all option line(s) with given option and ignore value
+                new_section_lines = [line for line in section_lines if not (match_active_opt(option, line))]
+                if section_lines != new_section_lines:
+                    changed = True
+                    msg = 'option changed'
+                    section_lines = new_section_lines
+            elif not exclusive and len(values) > 0:
+                # delete specified option=value line(s)
+                new_section_lines = [i for i in section_lines if not (match_active_opt(option, i) and match_active_opt(option, i).group(6) in values)]
+                if section_lines != new_section_lines:
+                    changed = True
+                    msg = 'option changed'
+                    section_lines = new_section_lines
         else:
-            if within_section and option:
-                if state == 'present':
-                    # change the existing option line
-                    if match_opt(option, line):
-                        if value is None and allow_no_value:
-                            newline = u'%s\n' % option
-                        else:
-                            newline = assignment_format % (option, value)
-                        option_changed = ini_lines[index] != newline
-                        changed = changed or option_changed
-                        if option_changed:
-                            msg = 'option changed'
-                        ini_lines[index] = newline
-                        if option_changed:
-                            # remove all possible option occurrences from the rest of the section
-                            index = index + 1
-                            while index < len(ini_lines):
-                                line = ini_lines[index]
-                                if line.startswith(u'['):
-                                    break
-                                if match_active_opt(option, line):
-                                    del ini_lines[index]
-                                else:
-                                    index = index + 1
-                        break
-                elif state == 'absent':
-                    # delete the existing line
-                    if match_active_opt(option, line):
-                        del ini_lines[index]
-                        changed = True
-                        msg = 'option changed'
-                        break
+            # drop the entire section
+            section_lines = []
+            msg = 'section removed'
+            changed = True
+
+    # reassemble the ini_lines after manipulation
+    ini_lines = before + section_lines + after
 
     # remove the fake section line
     del ini_lines[0]
@@ -261,9 +381,10 @@ def do_ini(module, filename, section=None, option=None, value=None,
     if not within_section and state == 'present':
         ini_lines.append(u'[%s]\n' % section)
         msg = 'section and option added'
-        if option and value is not None:
-            ini_lines.append(assignment_format % (option, value))
-        elif option and value is None and allow_no_value:
+        if option and values:
+            for value in values:
+                ini_lines.append(assignment_format % (option, value))
+        elif option and not values and allow_no_value:
             ini_lines.append(u'%s\n' % option)
         else:
             msg = 'only section added'
@@ -303,12 +424,17 @@ def main():
             section=dict(type='str', required=True),
             option=dict(type='str'),
             value=dict(type='str'),
+            values=dict(type='list', elements='str'),
             backup=dict(type='bool', default=False),
             state=dict(type='str', default='present', choices=['absent', 'present']),
+            exclusive=dict(type='bool', default=True),
             no_extra_spaces=dict(type='bool', default=False),
             allow_no_value=dict(type='bool', default=False),
             create=dict(type='bool', default=True)
         ),
+        mutually_exclusive=[
+            ['value', 'values']
+        ],
         add_file_common_args=True,
         supports_check_mode=True,
     )
@@ -317,16 +443,23 @@ def main():
     section = module.params['section']
     option = module.params['option']
     value = module.params['value']
+    values = module.params['values']
     state = module.params['state']
+    exclusive = module.params['exclusive']
     backup = module.params['backup']
     no_extra_spaces = module.params['no_extra_spaces']
     allow_no_value = module.params['allow_no_value']
     create = module.params['create']
 
-    if state == 'present' and not allow_no_value and value is None:
-        module.fail_json("Parameter 'value' must be defined if state=present and allow_no_value=False")
+    if state == 'present' and not allow_no_value and value is None and not values:
+        module.fail_json(msg="Parameter 'value(s)' must be defined if state=present and allow_no_value=False.")
+
+    if value is not None:
+        values = [value]
+    elif values is None:
+        values = []
 
-    (changed, backup_file, diff, msg) = do_ini(module, path, section, option, value, state, backup, no_extra_spaces, create, allow_no_value)
+    (changed, backup_file, diff, msg) = do_ini(module, path, section, option, values, state, exclusive, backup, no_extra_spaces, create, allow_no_value)
 
     if not module.check_mode and os.path.exists(path):
         file_args = module.load_file_common_arguments(module.params)


Test patch:
diff --git a/tests/integration/targets/ini_file/tasks/main.yml b/tests/integration/targets/ini_file/tasks/main.yml
--- a/tests/integration/targets/ini_file/tasks/main.yml
+++ b/tests/integration/targets/ini_file/tasks/main.yml
@@ -23,545 +23,31 @@
 # along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 
 - name: record the output directory
-  set_fact: output_file={{ remote_tmp_dir }}/foo.ini
-
-- name: add "fav=lemonade" is in section "[drinks]" in specified file
-  ini_file:
-    path: "{{ output_file }}"
-    section: drinks
-    option: fav
-    value: lemonade
-  register: result1
-
-- name: verify ini_file 'changed' is true
-  assert:
-    that:
-      - result1 is changed
-      - result1.msg == 'section and option added'
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: set expected content and get current ini file content
-  set_fact:
-    expected1: |
-
-      [drinks]
-      fav = lemonade
-    content1: "{{ output_content.content | b64decode }}"
-
-- name: Verify content of ini file is as expected
-  assert:
-    that:
-      - content1 == expected1
-
-- name: add "fav=lemonade" is in section "[drinks]" again
-  ini_file:
-    path: "{{ output_file }}"
-    section: drinks
-    option: fav
-    value: lemonade
-  register: result2
-
-- name: Ensure unchanged
-  assert:
-    that:
-      - result2 is not changed
-      - result2.msg == 'OK'
-
-- name: Ensure "beverage=coke" is in section "[drinks]"
-  ini_file:
-    path: "{{ output_file }}"
-    section: drinks
-    option: beverage
-    value: coke
-  register: result3
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: set expected content and get current ini file content
-  set_fact:
-    expected3: |
-
-      [drinks]
-      fav = lemonade
-      beverage = coke
-    content3: "{{ output_content.content | b64decode }}"
-
-- name: assert 'changed' is true and content is OK
-  assert:
-    that:
-      - result3 is changed
-      - result3.msg == 'option added'
-      - content3 == expected3
-
-- name: Remove option "beverage=coke"
-  ini_file:
-    path: "{{ output_file }}"
-    section: drinks
-    option: beverage
-    state: absent
-  register: result4
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: get ini file content
-  set_fact:
-    content4: "{{ output_content.content | b64decode }}"
-
-- name: assert changed and content is as expected
-  assert:
-    that:
-      - result4 is changed
-      - result4.msg == 'option changed'
-      - content4 == expected1
-
-- name: remove section 'drinks'
-  ini_file:
-    path: "{{ output_file }}"
-    section: drinks
-    state: absent
-  register: result5
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: get current ini file content
-  set_fact:
-    content5: "{{ output_content.content | b64decode }}"
-
-- name: assert changed and content is empty
-  assert:
-    that:
-      - result5 is changed
-      - result5.msg == 'section removed'
-      - content5 == "\n"
-
-# allow_no_value
-
-- name: test allow_no_value
-  ini_file:
-    path: "{{ output_file }}"
-    section: mysqld
-    option: skip-name
-    allow_no_value: yes
-  register: result6
-
-- name: assert section and option added
-  assert:
-    that:
-      - result6 is changed
-      - result6.msg == 'section and option added'
-
-- name: test allow_no_value idempotency
-  ini_file:
-    path: "{{ output_file }}"
-    section: mysqld
-    option: skip-name
-    allow_no_value: yes
-  register: result6
-
-- name: assert 'changed' false
-  assert:
-    that:
-      - result6 is not changed
-      - result6.msg == 'OK'
-
-- name: test create empty section
-  ini_file:
-    path: "{{ output_file }}"
-    section: new_empty_section
-    allow_no_value: yes
-  register: result6a
-
-- name: assert section added
-  assert:
-    that:
-      - result6a is changed
-      - result6a.msg == 'only section added'
-
-- name: test create empty section idempotency
-  ini_file:
-    path: "{{ output_file }}"
-    section: new_empty_section
-    allow_no_value: yes
-  register: result6a
-
-- name: assert 'changed' false
-  assert:
-    that:
-      - result6a is not changed
-      - result6a.msg == 'OK'
-
-- name: test remove empty section
-  ini_file:
-    state: absent
-    path: "{{ output_file }}"
-    section: new_empty_section
-    allow_no_value: yes
-
-- name: test allow_no_value with loop
-  ini_file:
-    path: "{{ output_file }}"
-    section: mysqld
-    option: "{{ item.o }}"
-    value: "{{ item.v | d(omit) }}"
-    allow_no_value: yes
-  with_items:
-    - { o: "skip-name-resolve" }
-    - { o: "max_connections", v: "500" }
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: set expected content and get current ini file content
-  set_fact:
-    content7: "{{ output_content.content | b64decode }}"
-    expected7: |
-
-      [mysqld]
-      skip-name
-      skip-name-resolve
-      max_connections = 500
-
-- name: Verify content of ini file is as expected
-  assert:
-    that:
-      - content7 == expected7
-
-- name: change option with no value to option with value
-  ini_file:
-    path: "{{ output_file }}"
-    section: mysqld
-    option: skip-name
-    value: myvalue
-  register: result8
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: set expected content and get current ini file content
-  set_fact:
-    content8: "{{ output_content.content | b64decode }}"
-    expected8: |
-
-      [mysqld]
-      skip-name = myvalue
-      skip-name-resolve
-      max_connections = 500
-
-- name: assert 'changed' and msg 'option changed' and content is as expected
-  assert:
-    that:
-      - result8 is changed
-      - result8.msg == 'option changed'
-      - content8 == expected8
-
-- name: change option with value to option with no value
-  ini_file:
-    path: "{{ output_file }}"
-    section: mysqld
-    option: skip-name
-    allow_no_value: yes
-  register: result9
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: set expected content and get current ini file content
-  set_fact:
-    content9: "{{ output_content.content | b64decode }}"
-    expected9: |
-
-      [mysqld]
-      skip-name
-      skip-name-resolve
-      max_connections = 500
-
-- name: assert 'changed' and msg 'option changed' and content is as expected
-  assert:
-    that:
-      - result9 is changed
-      - result9.msg == 'option changed'
-      - content9 == expected9
-
-- name: Remove option with no value
-  ini_file:
-    path: "{{ output_file }}"
-    section: mysqld
-    option: skip-name-resolve
-    state: absent
-  register: result10
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: set expected content and get current ini file content
-  set_fact:
-    content10: "{{ output_content.content | b64decode }}"
-    expected10: |
-
-      [mysqld]
-      skip-name
-      max_connections = 500
-
-- name: assert 'changed' and msg 'option changed' and content is as expected
-  assert:
-    that:
-      - result10 is changed
-      - result10.msg == 'option changed'
-      - content10 == expected10
-
-- name: Clean test file
-  copy:
-    content: ""
-    dest: "{{ output_file }}"
-    force: yes
-
-- name: Ensure "beverage=coke" is created within no section
-  ini_file:
-    section:
-    path: "{{ output_file }}"
-    option: beverage
-    value: coke
-  register: result11
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: set expected content and get current ini file content
-  set_fact:
-    expected11: "beverage = coke\n\n"
-    content11: "{{ output_content.content | b64decode }}"
-
-- name: assert 'changed' is true and content is OK (no section)
-  assert:
-    that:
-      - result11 is changed
-      - result11.msg == 'option added'
-      - content11 == expected11
-
-- name: Ensure "beverage=coke" is modified as "beverage=water" within no section
-  ini_file:
-    path: "{{ output_file }}"
-    option: beverage
-    value: water
-    section:
-  register: result12
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: set expected content and get current ini file content
-  set_fact:
-    expected12: "beverage = water\n\n"
-
-    content12: "{{ output_content.content | b64decode }}"
-
-- name: assert 'changed' is true and content is OK (no section)
-  assert:
-    that:
-      - result12 is changed
-      - result12.msg == 'option changed'
-      - content12 == expected12
-
-- name: remove option 'beverage' within no section
-  ini_file:
-    section:
-    path: "{{ output_file }}"
-    option: beverage
-    state: absent
-  register: result13
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: get current ini file content
   set_fact:
-    content13: "{{ output_content.content | b64decode }}"
-
-- name: assert changed (no section)
-  assert:
-    that:
-      - result13 is changed
-      - result13.msg == 'option changed'
-      - content13 == "\n"
+    output_file: "{{ remote_tmp_dir }}/foo.ini"
+    non_existing_file: "{{ remote_tmp_dir }}/bar.ini"
 
-- name: Check add option without section before existing section
+- name: include tasks
   block:
-    - name: Add option with section
-      ini_file:
-        path: "{{ output_file }}"
-        section: drinks
-        option: beverage
-        value: water
-    - name: Add option without section
-      ini_file:
-        path: "{{ output_file }}"
-        section:
-        option: like
-        value: tea
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
-
-- name: set expected content and get current ini file content
-  set_fact:
-    expected14: |
-      like = tea
-
-      [drinks]
-      beverage = water
-    content14: "{{ output_content.content | b64decode }}"
 
-- name: Verify content of ini file is as expected
-  assert:
-    that:
-      - content14 == expected14
+    - name: include tasks to perform basic tests
+      include_tasks: tests/00-basic.yml
 
-- name: Check add option with empty string value
-  block:
-    - name: Remove drinks
-      ini_file:
-        path: "{{ output_file }}"
-        section: drinks
-        state: absent
-    - name: Remove tea
-      ini_file:
+    - name: reset output file
+      file:
         path: "{{ output_file }}"
-        section:
-        option: like
-        value: tea
         state: absent
 
-    # See https://github.com/ansible-collections/community.general/issues/3031
-    - name: Tests with empty strings
-      ini_file:
-        path: "{{ output_file }}"
-        section: "{{ item.section | d('extensions') }}"
-        option: "{{ item.option }}"
-        value: ""
-        allow_no_value: "{{ item.no_value | d(omit) }}"
-      loop:
-        - option: evolve
-        - option: regress
-        - section: foobar
-          option: foo
-          no_value: true
-        - option: improve
-          no_value: true
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
+    - name: include tasks to perform tests with parameter "value"
+      include_tasks: tests/01-value.yml
 
-- name: set expected content and get current ini file content
-  set_fact:
-    expected15: "\n[extensions]\nevolve = \nregress = \nimprove = \n[foobar]\nfoo = \n"
-    content15: "{{ output_content.content | b64decode }}"
-- debug: var=content15
-- name: Verify content of ini file is as expected
-  assert:
-    that:
-      - content15 == expected15
-
-- name: Create starting ini file
-  copy:
-    # The content below is the following text file with BOM:
-    # [section1]
-    # var1=aaa
-    # var2=bbb
-    # [section2]
-    # var3=ccc
-    content: !!binary |
-      77u/W3NlY3Rpb24xXQp2YXIxPWFhYQp2YXIyPWJiYgpbc2VjdGlvbjJdCnZhcjM9Y2NjCg==
-    dest: "{{ output_file }}"
-- name: Test ini breakage
-  ini_file:
-    path: "{{ output_file }}"
-    section: section1
-    option: var4
-    value: 0
-
-- name: read content from output file
-  slurp:
-    src: "{{ output_file }}"
-  register: output_content
+    - name: reset output file
+      file:
+        path: "{{ output_file }}"
+        state: absent
 
-- name: set expected content and get current ini file content
-  set_fact:
-    expected16: "[section1]\nvar1=aaa\nvar2=bbb\nvar4 = 0\n[section2]\nvar3=ccc\n"
-    content16: "{{ output_content.content | b64decode }}"
-- debug:
-    var: content16
-- name: Verify content of ini file is as expected
-  assert:
-    that:
-      - content16 == expected16
+    - name: include tasks to perform tests with parameter "values"
+      include_tasks: tests/02-values.yml
 
-# Regression test for https://github.com/ansible-collections/community.general/pull/2578#issuecomment-868092282
-- name: Create UTF-8 test file
-  copy:
-    content: !!binary |
-      W2FwcDptYWluXQphdmFpbGFibGVfbGFuZ3VhZ2VzID0gZW4gZnIgZXMgZGUgcHQgamEgbHQgemhf
-      VFcgaWQgZGEgcHRfQlIgcnUgc2wgaXQgbmxfTkwgdWsgdGEgc2kgY3MgbmIgaHUKIyBGdWxsIGxh
-      bmd1YWdlIG5hbWVzIGluIG5hdGl2ZSBsYW5ndWFnZSAoY29tbWEgc2VwYXJhdGVkKQphdmFpbGFi
-      bGVfbGFuZ3VhZ2VzX2Z1bGwgPSBFbmdsaXNoLCBGcmFuw6dhaXMsIEVzcGHDsW9sLCBEZXV0c2No
-      LCBQb3J0dWd1w6pzLCDml6XmnKzoqp4sIExpZXR1dm9zLCDkuK3mlocsIEluZG9uZXNpYSwgRGFu
-      c2ssIFBvcnR1Z3XDqnMgKEJyYXNpbCksINCg0YPRgdGB0LrQuNC5LCBTbG92ZW7FocSNaW5hLCBJ
-      dGFsaWFubywgTmVkZXJsYW5kcywg0KPQutGA0LDRl9C90YHRjNC60LAsIOCupOCuruCuv+CutOCv
-      jSwg4LeD4LeS4LaC4LeE4La9LCDEjGVza3ksIEJva23DpWwsIE1hZ3lhcgo=
-    dest: '{{ output_file }}'
-- name: Add entries
-  ini_file:
-    section: "{{ item.section }}"
-    option: "{{ item.option }}"
-    value: "{{ item.value }}"
-    path: '{{ output_file }}'
-    create: true
-  loop:
-    - section: app:main
-      option: sqlalchemy.url
-      value: postgresql://app:secret@database/app
-    - section: handler_filelog
-      option: args
-      value: (sys.stderr,)
-    - section: handler_filelog
-      option: class
-      value: StreamHandler
-    - section: handler_exc_handler
-      option: args
-      value: (sys.stderr,)
-    - section: båz
-      option: ﬀöø
-      value: ḃâŗ
-    - section: båz
-      option: ﬀöø
-      value: bar
+    - name: include tasks to test regressions
+      include_tasks: tests/03-encoding.yml
diff --git a/tests/integration/targets/ini_file/tasks/tests/00-basic.yml b/tests/integration/targets/ini_file/tasks/tests/00-basic.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/ini_file/tasks/tests/00-basic.yml
@@ -0,0 +1,38 @@
+---
+## basiscs
+
+- name: test-basic 1 - specify both "value" and "values" and fail
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    value: lemonade
+    values:
+      - coke
+      - sprite
+  register: result_basic_1
+  ignore_errors: true
+
+- name: test-basic 1 - verify error message
+  assert:
+    that:
+      - result_basic_1 is not changed
+      - result_basic_1 is failed
+      - "result_basic_1.msg == 'parameters are mutually exclusive: value|values'"
+
+
+- name: test-basic 2 - set "create=no" on non-existing file and fail
+  ini_file:
+    path: "{{ non_existing_file }}"
+    section: food
+    create: false
+    value: banana
+  register: result_basic_2
+  ignore_errors: true
+
+- name: test-basic 2 - verify error message
+  assert:
+    that:
+      - result_basic_2 is not changed
+      - result_basic_2 is failed
+      - result_basic_2.msg == "Destination {{ non_existing_file }} does not exist!"
diff --git a/tests/integration/targets/ini_file/tasks/tests/01-value.yml b/tests/integration/targets/ini_file/tasks/tests/01-value.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/ini_file/tasks/tests/01-value.yml
@@ -0,0 +1,589 @@
+---
+
+## testing value
+
+- name: test-value 1 - set "state=present" and "value=null" and "allow_no_value=false" and fail
+  ini_file:
+    path: "{{ output_file }}"
+    section: cars
+    option: audi
+    value: null
+    allow_no_value: false
+  register: result_value_1
+  ignore_errors: true
+
+- name: test-value 1 - verify error message
+  assert:
+    that:
+      - result_value_1 is not changed
+      - result_value_1 is failed
+      - result_value_1.msg == "Parameter 'value(s)' must be defined if state=present and allow_no_value=False."
+
+
+- name: test-value 2 - set "state=present" and omit "value" and "allow_no_value=false" and fail
+  ini_file:
+    path: "{{ output_file }}"
+    section: cars
+    option: audi
+    allow_no_value: false
+  register: result_value_2
+  ignore_errors: true
+
+- name: test-value 2 - verify error message
+  assert:
+    that:
+      - result_value_2 is not changed
+      - result_value_2 is failed
+      - result_value_2.msg == "Parameter 'value(s)' must be defined if state=present and allow_no_value=False."
+
+
+- name: test-value 3 - add "fav=lemonade" in section "[drinks]" in specified file
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    value: lemonade
+  register: result3
+
+- name: test-value 3 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 3 - set expected content and get current ini file content
+  set_fact:
+    expected3: |
+
+      [drinks]
+      fav = lemonade
+    content3: "{{ output_content.content | b64decode }}"
+
+- name: test-value 3 - Verify content of ini file is as expected and ini_file 'changed' is true
+  assert:
+    that:
+      - result3 is changed
+      - result3.msg == 'section and option added'
+      - content3 == expected3
+
+
+- name: test-value 4 - add "fav=lemonade" is in section "[drinks]" again
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    value: lemonade
+  register: result4
+
+- name: test-value 4 - Ensure unchanged
+  assert:
+    that:
+      - result4 is not changed
+      - result4.msg == 'OK'
+
+
+- name: test-value 5 - Ensure "beverage=coke" is in section "[drinks]"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: beverage
+    value: coke
+  register: result5
+
+- name: test-value 5 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 5 - set expected content and get current ini file content
+  set_fact:
+    expected5: |
+
+      [drinks]
+      fav = lemonade
+      beverage = coke
+    content5: "{{ output_content.content | b64decode }}"
+
+- name: test-value 5 - assert 'changed' is true and content is OK
+  assert:
+    that:
+      - result5 is changed
+      - result5.msg == 'option added'
+      - content5 == expected5
+
+
+- name: test-value 6 - Remove option "beverage=coke"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: beverage
+    state: absent
+  register: result6
+
+- name: test-value 6 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 6 - set expected content and get current ini file content
+  set_fact:
+    expected6: |
+
+      [drinks]
+      fav = lemonade
+    content6: "{{ output_content.content | b64decode }}"
+
+- name: test-value 6 - assert 'changed' is true and content is as expected
+  assert:
+    that:
+      - result6 is changed
+      - result6.msg == 'option changed'
+      - content6 == expected6
+
+
+- name: test-value 7 - remove section 'drinks'
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    state: absent
+  register: result7
+
+- name: test-value 7 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 7 - get current ini file content
+  set_fact:
+    content7: "{{ output_content.content | b64decode }}"
+
+- name: test-value 7 - assert 'changed' is true and content is empty
+  assert:
+    that:
+      - result7 is changed
+      - result7.msg == 'section removed'
+      - content7 == "\n"
+
+
+# allow_no_value
+
+- name: test-value 8 - test allow_no_value
+  ini_file:
+    path: "{{ output_file }}"
+    section: mysqld
+    option: skip-name
+    allow_no_value: yes
+  register: result8
+
+- name: test-value 8 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 8 - set expected content and get current ini file content
+  set_fact:
+    content8: "{{ output_content.content | b64decode }}"
+    expected8: |
+
+      [mysqld]
+      skip-name
+
+- name: test-value 8 - assert 'changed' is true and section and option added
+  assert:
+    that:
+      - result8 is changed
+      - result8.msg == 'section and option added'
+      - content8 == expected8
+
+
+- name: test-value 9 - test allow_no_value idempotency
+  ini_file:
+    path: "{{ output_file }}"
+    section: mysqld
+    option: skip-name
+    allow_no_value: yes
+  register: result9
+
+- name: test-value 9 - assert 'changed' is false
+  assert:
+    that:
+      - result9 is not changed
+      - result9.msg == 'OK'
+
+
+- name: test-value 10 - test create empty section
+  ini_file:
+    path: "{{ output_file }}"
+    section: new_empty_section
+    allow_no_value: yes
+  register: result10
+
+- name: test-value 10 - assert 'changed' is true and section added
+  assert:
+    that:
+      - result10 is changed
+      - result10.msg == 'only section added'
+
+
+- name: test-value 11 - test create empty section idempotency
+  ini_file:
+    path: "{{ output_file }}"
+    section: new_empty_section
+    allow_no_value: yes
+  register: result11
+
+- name: test-value 11 - assert 'changed' is false
+  assert:
+    that:
+      - result11 is not changed
+      - result11.msg == 'OK'
+
+
+- name: test-value 12 - test remove empty section
+  ini_file:
+    state: absent
+    path: "{{ output_file }}"
+    section: new_empty_section
+    allow_no_value: yes
+
+- name: test-value 12 - test allow_no_value with loop
+  ini_file:
+    path: "{{ output_file }}"
+    section: mysqld
+    option: "{{ item.o }}"
+    value: "{{ item.v | d(omit) }}"
+    allow_no_value: yes
+  loop:
+    - { o: "skip-name-resolve" }
+    - { o: "max_connections", v: "500" }
+
+- name: test-value 12 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 12 - set expected content and get current ini file content
+  set_fact:
+    content12: "{{ output_content.content | b64decode }}"
+    expected12: |
+
+      [mysqld]
+      skip-name
+      skip-name-resolve
+      max_connections = 500
+  
+- name: test-value 12 - Verify content of ini file is as expected
+  assert:
+    that:
+      - content12 == expected12
+
+
+- name: test-value 13 - change option with no value to option with value
+  ini_file:
+    path: "{{ output_file }}"
+    section: mysqld
+    option: skip-name
+    value: myvalue
+  register: result13
+
+- name: test-value 13 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 13 - set expected content and get current ini file content
+  set_fact:
+    content13: "{{ output_content.content | b64decode }}"
+    expected13: |
+
+      [mysqld]
+      skip-name = myvalue
+      skip-name-resolve
+      max_connections = 500
+
+- name: test-value 13 - assert 'changed' and msg 'option changed' and content is as expected
+  assert:
+    that:
+      - result13 is changed
+      - result13.msg == 'option changed'
+      - content13 == expected13
+
+
+- name: test-value 14 - change option with value to option with no value
+  ini_file:
+    path: "{{ output_file }}"
+    section: mysqld
+    option: skip-name
+    allow_no_value: yes
+  register: result14
+
+- name: test-value 14 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 14 - set expected content and get current ini file content
+  set_fact:
+    content14: "{{ output_content.content | b64decode }}"
+    expected14: |
+
+      [mysqld]
+      skip-name
+      skip-name-resolve
+      max_connections = 500
+
+- name: test-value 14 - assert 'changed' is true and msg 'option changed' and content is as expected
+  assert:
+    that:
+      - result14 is changed
+      - result14.msg == 'option changed'
+      - content14 == expected14
+
+
+- name: test-value 15 - Remove option with no value
+  ini_file:
+    path: "{{ output_file }}"
+    section: mysqld
+    option: skip-name-resolve
+    state: absent
+  register: result15
+
+- name: test-value 15 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 15 - set expected content and get current ini file content
+  set_fact:
+    content15: "{{ output_content.content | b64decode }}"
+    expected15: |
+
+      [mysqld]
+      skip-name
+      max_connections = 500
+
+- name: test-value 15 - assert 'changed' is true and msg 'option changed' and content is as expected
+  assert:
+    that:
+      - result15 is changed
+      - result15.msg == 'option changed'
+      - content15 == expected15
+
+
+- name: test-value 16 - Clean test file
+  copy:
+    content: ""
+    dest: "{{ output_file }}"
+    force: yes
+
+- name: test-value 16 - Ensure "beverage=coke" is created within no section
+  ini_file:
+    section:
+    path: "{{ output_file }}"
+    option: beverage
+    value: coke
+  register: result16
+
+- name: test-value 16 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 16 - set expected content and get current ini file content
+  set_fact:
+    expected16: |+
+      beverage = coke
+      
+    content16: "{{ output_content.content | b64decode }}"
+
+- name: test-value 16 - assert 'changed' is true and content is OK (no section)
+  assert:
+    that:
+      - result16 is changed
+      - result16.msg == 'option added'
+      - content16 == expected16
+
+
+- name: test-value 17 - Ensure "beverage=coke" is modified as "beverage=water" within no section
+  ini_file:
+    path: "{{ output_file }}"
+    option: beverage
+    value: water
+    section:
+  register: result17
+
+- name: test-value 17 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 17 - set expected content and get current ini file content
+  set_fact:
+    expected17: |+
+      beverage = water
+      
+    content17: "{{ output_content.content | b64decode }}"
+
+- name: test-value 17 - assert 'changed' is true and content is OK (no section)
+  assert:
+    that:
+      - result17 is changed
+      - result17.msg == 'option changed'
+      - content17 == expected17
+
+
+- name: test-value 18 - remove option 'beverage' within no section
+  ini_file:
+    section:
+    path: "{{ output_file }}"
+    option: beverage
+    state: absent
+  register: result18
+
+- name: test-value 18 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 18 - get current ini file content
+  set_fact:
+    content18: "{{ output_content.content | b64decode }}"
+
+- name: test-value 18 - assert 'changed' is true and option is removed (no section)
+  assert:
+    that:
+      - result18 is changed
+      - result18.msg == 'option changed'
+      - content18 == "\n"
+
+
+- name: test-value 19 - Check add option without section before existing section
+  block:
+    - name: test-value 19 - Add option with section
+      ini_file:
+        path: "{{ output_file }}"
+        section: drinks
+        option: beverage
+        value: water
+    - name: test-value 19 - Add option without section
+      ini_file:
+        path: "{{ output_file }}"
+        section:
+        option: like
+        value: tea
+
+- name: test-value 19 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 19 - set expected content and get current ini file content
+  set_fact:
+    expected19: |
+      like = tea
+
+      [drinks]
+      beverage = water
+    content19: "{{ output_content.content | b64decode }}"
+
+- name: test-value 19 - Verify content of ini file is as expected
+  assert:
+    that:
+      - content19 == expected19
+
+
+- name: test-value 20 - Check add option with empty string value
+  block:
+    - name: test-value 20 - Remove drinks
+      ini_file:
+        path: "{{ output_file }}"
+        section: drinks
+        state: absent
+    - name: test-value 20 - Remove tea
+      ini_file:
+        path: "{{ output_file }}"
+        section:
+        option: like
+        value: tea
+        state: absent
+    # See https://github.com/ansible-collections/community.general/issues/3031
+    - name: test-value 20 - Tests with empty strings
+      ini_file:
+        path: "{{ output_file }}"
+        section: "{{ item.section | d('extensions') }}"
+        option: "{{ item.option }}"
+        value: ""
+        allow_no_value: "{{ item.no_value | d(omit) }}"
+      loop:
+        - option: evolve
+        - option: regress
+        - section: foobar
+          option: foo
+          no_value: true
+        - option: improve
+          no_value: true
+
+- name: test-value 20 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 20 - set expected content and get current ini file content
+  set_fact:
+    expected20: |+
+      
+      [extensions]
+      evolve = 
+      regress = 
+      improve = 
+      [foobar]
+      foo = 
+    content20: "{{ output_content.content | b64decode }}"
+
+- name: test-value 20 - Verify content of ini file is as expected
+  assert:
+    that:
+      - content20 == expected20
+
+
+- name: test-value 21 - Create starting ini file
+  copy:
+    # The content below is the following text file with BOM:
+    # [section1]
+    # var1=aaa
+    # var2=bbb
+    # [section2]
+    # var3=ccc
+    content: !!binary |
+      77u/W3NlY3Rpb24xXQp2YXIxPWFhYQp2YXIyPWJiYgpbc2VjdGlvbjJdCnZhcjM9Y2NjCg==
+    dest: "{{ output_file }}"
+
+- name: test-value 21 -  Test ini breakage
+  ini_file:
+    path: "{{ output_file }}"
+    section: section1
+    option: var4
+    value: 0
+  register: result21
+
+- name: test-value 21 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 21 - set expected content and get current ini file content
+  set_fact:
+    expected21: |
+      [section1]
+      var1=aaa
+      var2=bbb
+      var4 = 0
+      [section2]
+      var3=ccc
+    content21: "{{ output_content.content | b64decode }}"
+
+- name: test-value 21 - Verify content of ini file is as expected
+  assert:
+    that:
+      - result21 is changed
+      - result21.msg == 'option added'
+      - content21 == expected21
diff --git a/tests/integration/targets/ini_file/tasks/tests/02-values.yml b/tests/integration/targets/ini_file/tasks/tests/02-values.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/ini_file/tasks/tests/02-values.yml
@@ -0,0 +1,1013 @@
+---
+
+## testing values
+
+- name: "test-values 1 - set 'state=present' and 'values=[]' and 'allow_no_value=false' and fail"
+  ini_file:
+    path: "{{ output_file }}"
+    section: cars
+    option: audi
+    values: []
+    allow_no_value: false
+  register: result1
+  ignore_errors: true
+
+- name: test-values 1 - verify error message
+  assert:
+    that:
+      - result1 is not changed
+      - result1 is failed
+      - result1.msg == "Parameter 'value(s)' must be defined if state=present and allow_no_value=False."
+
+
+- name: "test-values 2 - set 'state=present' and omit 'values' and 'allow_no_value=false' and fail"
+  ini_file:
+    path: "{{ output_file }}"
+    section: cars
+    option: audi
+    allow_no_value: false
+  register: result2
+  ignore_errors: true
+
+- name: test-values 2 - verify error message
+  assert:
+    that:
+      - result2 is not changed
+      - result2 is failed
+      - result2.msg == "Parameter 'value(s)' must be defined if state=present and allow_no_value=False."
+
+
+- name: "test-values 3 - ensure 'fav=lemonade' and 'fav=cocktail' is 'present' in section '[drinks]' in specified file"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - lemonade
+      - cocktail
+    state: present
+  register: result3
+
+- name: test-values 3 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 3 - set expected content and get current ini file content
+  set_fact:
+    expected3: |
+
+      [drinks]
+      fav = lemonade
+      fav = cocktail
+    content3: "{{ output_content.content | b64decode }}"
+
+- name: test-values 3 - Verify content of ini file is as expected and ini_file 'changed' is true
+  assert:
+    that:
+      - result3 is changed
+      - result3.msg == 'section and option added'
+      - content3 == expected3
+
+
+- name: "test-values 4 - remove option 'fav=lemonade' from section '[drinks]' in specified file"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - lemonade
+    state: absent
+    exclusive: false
+  register: result4
+
+- name: test-values 4 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 4 - set expected content and get current ini file content
+  set_fact:
+    expected4: |
+
+      [drinks]
+      fav = cocktail
+    content4: "{{ output_content.content | b64decode }}"
+
+- name: test-values 4 - Verify content of ini file is as expected and ini_file 'changed' is true
+  assert:
+    that:
+      - result4 is changed
+      - result4.msg == 'option changed'
+      - content4 == expected4
+
+
+- name: "test-values 5 - add option 'fav=lemonade' in section '[drinks]' in specified file"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - lemonade
+    state: present
+    exclusive: false
+  register: result5
+
+- name: test-values 5 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 5 - set expected content and get current ini file content
+  set_fact:
+    expected5: |
+
+      [drinks]
+      fav = cocktail
+      fav = lemonade
+    content5: "{{ output_content.content | b64decode }}"
+
+- name: test-values 5 - Verify content of ini file is as expected and ini_file 'changed' is true
+  assert:
+    that:
+      - result5 is changed
+      - result5.msg == 'option added'
+      - content5 == expected5
+
+
+- name: "test-values 6 - ensure 'fav=lemonade' and 'fav=cocktail' is 'present' in section '[drinks]' and check for idempotency"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - lemonade
+      - cocktail
+    state: present
+  register: result6
+
+- name: test-values 6 - Ensure unchanged
+  assert:
+    that:
+      - result6 is not changed
+      - result6.msg == 'OK'
+
+
+- name: "test-values 7 - ensure 'fav=cocktail' and 'fav=lemonade' (list reverse order) is 'present' in section '[drinks]' and check for idempotency"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - cocktail
+      - lemonade
+    state: present
+  register: result7
+
+- name: test-values 7 - Ensure unchanged
+  assert:
+    that:
+      - result7 is not changed
+      - result7.msg == 'OK'
+
+
+- name: "test-values 8 - add option 'fav=lemonade' in section '[drinks]' again and ensure idempotency"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - lemonade
+    state: present
+    exclusive: false
+  register: result8
+
+- name: test-values 8 - Ensure unchanged
+  assert:
+    that:
+      - result8 is not changed
+      - result8.msg == 'OK'
+
+
+- name: "test-values 9 - ensure only 'fav=lemonade' is 'present' in section '[drinks]' in specified file"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - lemonade
+    state: present
+  register: result9
+
+- name: test-values 9 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 9 - set expected content and get current ini file content
+  set_fact:
+    expected9: |
+
+      [drinks]
+      fav = lemonade
+    content9: "{{ output_content.content | b64decode }}"
+
+- name: test-values 9 - Verify content of ini file is as expected and ini_file 'changed' is true
+  assert:
+    that:
+      - result9 is changed
+      - result9.msg == 'option changed'
+      - content9 == expected9
+
+
+- name: "test-values 10 - remove non-existent 'fav=cocktail' from section '[drinks]' in specified file"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - cocktail
+    state: absent
+  register: result10
+
+- name: test-values 10 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 10 - set expected content and get current ini file content
+  set_fact:
+    expected10: |
+
+      [drinks]
+    content10: "{{ output_content.content | b64decode }}"
+
+
+- name: test-values 10 - Ensure unchanged
+  assert:
+    that:
+      - result10 is changed
+      - result10.msg == 'option changed'
+      - content10 == expected10
+
+
+- name: "test-values 11 - Ensure 'fav=lemonade' and 'beverage=coke' is 'present' in section '[drinks]'"
+  block:
+    - name: "test-values 11 - resetting ini_fie: Ensure 'fav=lemonade' is 'present' in section '[drinks]'"
+      ini_file:
+        path: "{{ output_file }}"
+        section: drinks
+        option: fav
+        values:
+          - lemonade
+        state: present
+    - name: "test-values 11 - Ensure 'beverage=coke' is 'present' in section '[drinks]'"
+      ini_file:
+        path: "{{ output_file }}"
+        section: drinks
+        option: beverage
+        values:
+          - coke
+        state: present
+      register: result11
+
+- name: test-values 11 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 11 - set expected content and get current ini file content
+  set_fact:
+    expected11: |
+
+      [drinks]
+      fav = lemonade
+      beverage = coke
+    content11: "{{ output_content.content | b64decode }}"
+
+- name: test-values 11 - assert 'changed' is true and content is OK
+  assert:
+    that:
+      - result11 is changed
+      - result11.msg == 'option added'
+      - content11 == expected11
+
+
+- name: "test-values 12 - add option 'fav=lemonade' in section '[drinks]' again and ensure idempotency"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - lemonade
+    state: present
+    exclusive: false
+  register: result12
+
+- name: test-values 12 - Ensure unchanged
+  assert:
+    that:
+      - result12 is not changed
+      - result12.msg == 'OK'
+
+
+- name: "test-values 13 - add option 'fav=cocktail' in section '[drinks]' in specified file"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - cocktail
+    state: present
+    exclusive: false
+  register: result13
+
+- name: test-values 13 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 13 - set expected content and get current ini file content
+  set_fact:
+    expected13: |
+
+      [drinks]
+      fav = lemonade
+      beverage = coke
+      fav = cocktail
+    content13: "{{ output_content.content | b64decode }}"
+
+- name: test-values 13 - Verify content of ini file is as expected and ini_file 'changed' is true
+  assert:
+    that:
+      - result13 is changed
+      - result13.msg == 'option added'
+      - content13 == expected13
+
+
+- name: "test-values 14 - Ensure 'refreshment=[water, juice, soft drink]' is 'present' in section '[drinks]'"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: refreshment
+    values:
+      - water
+      - juice
+      - soft drink
+    state: present
+  register: result14
+
+- name: test-values 14 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 14 - set expected content and get current ini file content
+  set_fact:
+    expected14: |
+
+      [drinks]
+      fav = lemonade
+      beverage = coke
+      fav = cocktail
+      refreshment = water
+      refreshment = juice
+      refreshment = soft drink
+    content14: "{{ output_content.content | b64decode }}"
+
+- name: test-values 14 - assert 'changed' is true and content is OK
+  assert:
+    that:
+      - result14 is changed
+      - result14.msg == 'option added'
+      - content14 == expected14
+
+
+- name: "test-values 15 - ensure 'fav=lemonade' and 'fav=cocktail' is 'present' in section '[drinks]' and check for idempotency"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - lemonade
+      - cocktail
+    state: present
+  register: result15
+
+- name: test-values 15 - Ensure unchanged
+  assert:
+    that:
+      - result15 is not changed
+      - result15.msg == 'OK'
+
+
+- name: "test-values 16 - ensure 'fav=cocktail' and 'fav=lemonade' (list reverse order) is 'present' in section '[drinks]' and check for idempotency"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - cocktail
+      - lemonade
+    state: present
+  register: result16
+
+- name: test-values 16 - Ensure unchanged
+  assert:
+    that:
+      - result16 is not changed
+      - result16.msg == 'OK'
+
+
+- name: "test-values 17 - Ensure option 'refreshment' is 'absent' in section '[drinks]'"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: refreshment
+    state: absent
+  register: result17
+
+- name: test-values 17 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 17 - set expected content and get current ini file content
+  set_fact:
+    expected17: |
+
+      [drinks]
+      fav = lemonade
+      beverage = coke
+      fav = cocktail
+    content17: "{{ output_content.content | b64decode }}"
+
+- name: test-values 17 - assert 'changed' is true and content is as expected
+  assert:
+    that:
+      - result17 is changed
+      - result17.msg == 'option changed'
+      - content17 == expected17
+
+
+- name: "test-values 18 - Ensure 'beverage=coke' is 'abesent' in section '[drinks]'"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: beverage
+    state: absent
+  register: result18
+
+- name: test-values 18 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 18 - set expected content and get current ini file content
+  set_fact:
+    expected18: |
+
+      [drinks]
+      fav = lemonade
+      fav = cocktail
+    content18: "{{ output_content.content | b64decode }}"
+
+- name: test-values 18 - assert 'changed' is true and content is as expected
+  assert:
+    that:
+      - result18 is changed
+      - result18.msg == 'option changed'
+      - content18 == expected18
+
+
+- name: "test-values 19 - Ensure non-existant 'beverage=coke' is 'abesent' in section '[drinks]'"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: beverage
+    values:
+      - coke
+    state: absent
+  register: result19
+
+- name: test-values 19 - Ensure unchanged
+  assert:
+    that:
+      - result19 is not changed
+      - result19.msg == 'OK'
+
+
+- name: test-values 20 - remove section 'drinks'
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    state: absent
+  register: result20
+
+- name: test-values 20 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 20 - get current ini file content
+  set_fact:
+    content20: "{{ output_content.content | b64decode }}"
+
+- name: test-values 20 - assert 'changed' is true and content is empty
+  assert:
+    that:
+      - result20 is changed
+      - result20.msg == 'section removed'
+      - content20 == "\n"
+
+
+- name: "test-values 21 - Ensure 'refreshment=[water, juice, soft drink, juice]' (duplicates removed) is 'present' in section '[drinks]'"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: refreshment
+    values:
+      - water
+      - juice
+      - soft drink
+      - juice
+    state: present
+  register: result21
+
+- name: test-values 21 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 21 - set expected content and get current ini file content
+  set_fact:
+    expected21: |
+
+      [drinks]
+      refreshment = water
+      refreshment = juice
+      refreshment = soft drink
+    content21: "{{ output_content.content | b64decode }}"
+
+- name: test-values 21 - assert 'changed' is true and content is OK
+  assert:
+    that:
+      - result21 is changed
+      - result21.msg == 'section and option added'
+      - content21 == expected21
+
+
+- name: test-values 22 - Create starting ini file
+  copy:
+    content: |
+      
+      # Some comment to test
+      [mysqld]
+      connect_timeout = 300
+      max_connections = 1000
+      [section1]
+      var1 = aaa
+      # comment in section
+      var2 = foo
+      # var2 = bar
+
+      [section2]
+      var3 = ccc
+      # comment after section
+    dest: "{{ output_file }}"
+
+- name: "test-values 22 - Ensure 'skip-name' with 'allow_no_value' is 'present' in section '[mysqld]' test allow_no_value"
+  ini_file:
+    path: "{{ output_file }}"
+    section: mysqld
+    option: skip-name
+    allow_no_value: true
+    state: present
+  register: result22
+
+- name: test-values 22 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 22 - set expected content and get current ini file content
+  set_fact:
+    expected22: |
+      
+      # Some comment to test
+      [mysqld]
+      connect_timeout = 300
+      max_connections = 1000
+      skip-name
+      [section1]
+      var1 = aaa
+      # comment in section
+      var2 = foo
+      # var2 = bar
+
+      [section2]
+      var3 = ccc
+      # comment after section
+    content22: "{{ output_content.content | b64decode }}"
+
+- name: test-values 22 - assert 'changed' is true and content is OK and option added
+  assert:
+    that:
+      - result22 is changed
+      - result22.msg == 'option added'
+      - content22 == expected22
+
+
+- name: "test-values 23 - Ensure 'var2=[foo, foobar]' is 'present' in section '[section1]'"
+  ini_file:
+    path: "{{ output_file }}"
+    section: section1
+    option: var2
+    values: 
+      - foo
+      - foobar
+    state: present
+  register: result23
+
+- name: test-values 23 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 23 - set expected content and get current ini file content
+  set_fact:
+    content23: "{{ output_content.content | b64decode }}"
+    expected23: |
+      
+      # Some comment to test
+      [mysqld]
+      connect_timeout = 300
+      max_connections = 1000
+      skip-name
+      [section1]
+      var1 = aaa
+      # comment in section
+      var2 = foo
+      var2 = foobar
+      # var2 = bar
+
+      [section2]
+      var3 = ccc
+      # comment after section
+- name: test-values 23 - assert 'changed' and msg 'option added' and content is as expected
+  assert:
+    that:
+      - result23 is changed
+      - result23.msg == 'option added'
+      - content23 == expected23
+
+
+- name: "test-values 24 - Ensure 'var2=[foo, foobar, bar]' is 'present' in section '[section1]' replacing commented option 'var2=bar'"
+  ini_file:
+    path: "{{ output_file }}"
+    section: section1
+    option: var2
+    values: 
+      - foo
+      - bar
+      - foobar
+    state: present
+  register: result24
+
+- name: test-values 24 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 24 - set expected content and get current ini file content
+  set_fact:
+    content24: "{{ output_content.content | b64decode }}"
+    expected24: |
+      
+      # Some comment to test
+      [mysqld]
+      connect_timeout = 300
+      max_connections = 1000
+      skip-name
+      [section1]
+      var1 = aaa
+      # comment in section
+      var2 = foo
+      var2 = foobar
+      var2 = bar
+
+      [section2]
+      var3 = ccc
+      # comment after section
+- name: test-values 24 - assert 'added' and msg 'option changed' and content is as expected
+  assert:
+    that:
+      - result24 is changed
+      - result24.msg == 'option changed'
+      - content24 == expected24
+
+
+- name: test-values 25 - Clean test file
+  copy:
+    content: ""
+    dest: "{{ output_file }}"
+    force: yes
+
+- name: "test-values 25 - Ensure 'beverage=[coke, pepsi]' is created within no section"
+  ini_file:
+    section:
+    path: "{{ output_file }}"
+    option: beverage
+    values:
+      - coke
+      - pepsi
+    state: present
+  register: result25
+
+- name: test-values 25 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 25 - set expected content and get current ini file content
+  set_fact:
+    expected25: |+
+      beverage = coke
+      beverage = pepsi
+
+    content25: "{{ output_content.content | b64decode }}"
+
+- name: test-values 25 - assert 'changed' is true and content is OK (no section)
+  assert:
+    that:
+      - result25 is changed
+      - result25.msg == 'option added'
+      - content25 == expected25
+
+
+- name: "test-values 26 - Ensure 'beverage=coke' and 'beverage=pepsi' are modified within no section"
+  ini_file:
+    path: "{{ output_file }}"
+    option: beverage
+    values:
+      - water
+      - orange juice
+    section:
+    state: present
+    exclusive: true
+  register: result26
+
+- name: test-values 26 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 26 - set expected content and get current ini file content
+  set_fact:
+    expected26: |+
+      beverage = water
+      beverage = orange juice
+      
+    content26: "{{ output_content.content | b64decode }}"
+
+- name: test-values 26 - assert 'changed' is true and content is OK (no section)
+  assert:
+    that:
+      - result26 is changed
+      - result26.msg == 'option changed'
+      - content26 == expected26
+
+
+- name: "test-values 27 - ensure option 'beverage' is 'absent' within no section"
+  ini_file:
+    section:
+    path: "{{ output_file }}"
+    option: beverage
+    state: absent
+  register: result27
+
+- name: test-values 27 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 27 - get current ini file content
+  set_fact:
+    content27: "{{ output_content.content | b64decode }}"
+
+- name: test-values 27 - assert changed (no section)
+  assert:
+    that:
+      - result27 is changed
+      - result27.msg == 'option changed'
+      - content27 == "\n"
+
+
+- name: "test-values 28 - Ensure option 'present' without section before existing section"
+  block:
+    - name: test-values 28 - ensure option present within section
+      ini_file:
+        path: "{{ output_file }}"
+        section: drinks
+        option: beverage
+        values: 
+          - water
+          - orange juice
+        state: present
+
+    - name: test-values 28 - ensure option present without section
+      ini_file:
+        path: "{{ output_file }}"
+        section:
+        option: like
+        values:
+          - tea
+          - coffee
+        state: present
+
+- name: test-values 28 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-values 28 - set expected content and get current ini file content
+  set_fact:
+    expected28: |
+      like = tea
+      like = coffee
+
+      [drinks]
+      beverage = water
+      beverage = orange juice
+    content28: "{{ output_content.content | b64decode }}"
+
+- name: test-values 28 - Verify content of ini file is as expected
+  assert:
+    that:
+      - content28 == expected28
+
+
+- name: test-value 29 - Create starting ini file
+  copy:
+    content: |
+      [drinks]
+      fav = cocktail
+      beverage = water
+      fav = lemonade
+      beverage = orange juice
+    dest: "{{ output_file }}"
+
+- name: "test-value 29 - Test 'state=absent' with 'exclusive=true' with multiple options in ini_file"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - cocktail
+    state: absent
+  register: result29
+
+- name: test-value 29 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 29 - set expected content and get current ini file content
+  set_fact:
+    expected29: |
+      [drinks]
+      beverage = water
+      beverage = orange juice
+    content29: "{{ output_content.content | b64decode }}"
+
+- name: test-value 29 - Verify content of ini file is as expected
+  assert:
+    that:
+      - result29 is changed
+      - result29.msg == 'option changed'
+      - content29 == expected29
+
+
+- name: test-value 30 - Create starting ini file
+  copy:
+    content: |
+      [drinks]
+      fav = cocktail
+      beverage = water
+      fav = lemonade
+      beverage = orange juice
+    dest: "{{ output_file }}"
+
+- name: "test-value 30 - Test 'state=absent' with 'exclusive=false' with multiple options in ini_file"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    values:
+      - cocktail
+    state: absent
+    exclusive: false
+  register: result30
+
+- name: test-value 30 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 30 - set expected content and get current ini file content
+  set_fact:
+    expected30: |
+      [drinks]
+      beverage = water
+      fav = lemonade
+      beverage = orange juice
+    content30: "{{ output_content.content | b64decode }}"
+
+- name: test-value 30 - Verify content of ini file is as expected
+  assert:
+    that:
+      - result30 is changed
+      - result30.msg == 'option changed'
+      - content30 == expected30
+
+
+- name: test-value 31 - Create starting ini file
+  copy:
+    content: |
+      [drinks]
+      fav = cocktail
+      beverage = water
+      fav = lemonade
+      beverage = orange juice
+    dest: "{{ output_file }}"
+
+- name: "test-value 31 - Test 'state=absent' with 'exclusive=true' and no value given with multiple options in ini_file"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    state: absent
+  register: result31
+
+- name: test-value 31 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 31 - set expected content and get current ini file content
+  set_fact:
+    expected31: |
+      [drinks]
+      beverage = water
+      beverage = orange juice
+    content31: "{{ output_content.content | b64decode }}"
+
+- name: test-value 31 - Verify content of ini file is as expected
+  assert:
+    that:
+      - result31 is changed
+      - result31.msg == 'option changed'
+      - content31 == expected31
+
+
+- name: test-value 32 - Create starting ini file
+  copy:
+    content: |
+      [drinks]
+      fav = cocktail
+      beverage = water
+      fav = lemonade
+      beverage = orange juice
+    dest: "{{ output_file }}"
+
+- name: "test-value 32 - Test 'state=absent' with 'exclusive=false' and no value given with multiple options in ini_file"
+  ini_file:
+    path: "{{ output_file }}"
+    section: drinks
+    option: fav
+    state: absent
+    exclusive: false
+  register: result32
+  diff: true
+
+- name: test-value 32 - read content from output file
+  slurp:
+    src: "{{ output_file }}"
+  register: output_content
+
+- name: test-value 32 - set expected content and get current ini file content
+  set_fact:
+    expected32: |
+      [drinks]
+      fav = cocktail
+      beverage = water
+      fav = lemonade
+      beverage = orange juice
+    content32: "{{ output_content.content | b64decode }}"
+
+- name: test-value 32 - Verify content of ini file is as expected
+  assert:
+    that:
+      - result32 is not changed
+      - result32.msg == 'OK'
+      - content32 == expected32
diff --git a/tests/integration/targets/ini_file/tasks/tests/03-encoding.yml b/tests/integration/targets/ini_file/tasks/tests/03-encoding.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/ini_file/tasks/tests/03-encoding.yml
@@ -0,0 +1,41 @@
+---
+
+# Regression test for https://github.com/ansible-collections/community.general/pull/2578#issuecomment-868092282
+- name: Create UTF-8 test file
+  copy:
+    content: !!binary |
+      W2FwcDptYWluXQphdmFpbGFibGVfbGFuZ3VhZ2VzID0gZW4gZnIgZXMgZGUgcHQgamEgbHQgemhf
+      VFcgaWQgZGEgcHRfQlIgcnUgc2wgaXQgbmxfTkwgdWsgdGEgc2kgY3MgbmIgaHUKIyBGdWxsIGxh
+      bmd1YWdlIG5hbWVzIGluIG5hdGl2ZSBsYW5ndWFnZSAoY29tbWEgc2VwYXJhdGVkKQphdmFpbGFi
+      bGVfbGFuZ3VhZ2VzX2Z1bGwgPSBFbmdsaXNoLCBGcmFuw6dhaXMsIEVzcGHDsW9sLCBEZXV0c2No
+      LCBQb3J0dWd1w6pzLCDml6XmnKzoqp4sIExpZXR1dm9zLCDkuK3mlocsIEluZG9uZXNpYSwgRGFu
+      c2ssIFBvcnR1Z3XDqnMgKEJyYXNpbCksINCg0YPRgdGB0LrQuNC5LCBTbG92ZW7FocSNaW5hLCBJ
+      dGFsaWFubywgTmVkZXJsYW5kcywg0KPQutGA0LDRl9C90YHRjNC60LAsIOCupOCuruCuv+CutOCv
+      jSwg4LeD4LeS4LaC4LeE4La9LCDEjGVza3ksIEJva23DpWwsIE1hZ3lhcgo=
+    dest: '{{ output_file }}'
+- name: Add entries
+  ini_file:
+    section: "{{ item.section }}"
+    option: "{{ item.option }}"
+    value: "{{ item.value }}"
+    path: '{{ output_file }}'
+    create: true
+  loop:
+    - section: app:main
+      option: sqlalchemy.url
+      value: postgresql://app:secret@database/app
+    - section: handler_filelog
+      option: args
+      value: (sys.stderr,)
+    - section: handler_filelog
+      option: class
+      value: StreamHandler
+    - section: handler_exc_handler
+      option: args
+      value: (sys.stderr,)
+    - section: båz
+      option: ﬀöø
+      value: ḃâŗ
+    - section: båz
+      option: ﬀöø
+      value: bar