Repository: ansible-collections/community.general

Issue:
Updates-alternatives slaves support
### Summary

Dear,

[alternatives module](https://github.com/ansible-collections/community.general/blob/118c040879c742cf3d97c452653efd6f91a4f91b/plugins/modules/system/alternatives.py#L1) seems to not support yet slaves

as records with `--install` we can use `--slave`

Usage:
```
update-alternatives  --help
Usage: update-alternatives [<option> ...] <command>

Commands:
  --install <link> <name> <path> <priority>
    [--slave <link> <name> <path>] ...
                           add a group of alternatives to the system.
  --remove <name> <path>   remove <path> from the <name> group alternative.
  --remove-all <name>      remove <name> group from the alternatives system.
  --auto <name>            switch the master link <name> to automatic mode.
  --display <name>         display information about the <name> group.
  --query <name>           machine parseable version of --display <name>.
  --list <name>            display all targets of the <name> group.
  --get-selections         list master alternative names and their status.
  --set-selections         read alternative status from standard input.
  --config <name>          show alternatives for the <name> group and ask the
                           user to select which one to use.
  --set <name> <path>      set <path> as alternative for <name>.
  --all                    call --config on all alternatives.

<link> is the symlink pointing to /etc/alternatives/<name>.
  (e.g. /usr/bin/pager)
<name> is the master name for this link group.
  (e.g. pager)
<path> is the location of one of the alternative target files.
  (e.g. /usr/bin/less)
<priority> is an integer; options with higher numbers have higher priority in
  automatic mode.

Options:
  --altdir <directory>     change the alternatives directory.
  --admindir <directory>   change the administrative directory.
  --instdir <directory>    change the installation directory.
  --root <directory>       change the filesystem root directory.
  --log <file>             change the log file.
  --force                  allow replacing files with alternative links.
  --skip-auto              skip prompt for alternatives correctly configured
                           in automatic mode (relevant for --config only)
  --quiet                  quiet operation, minimal output.
  --verbose                verbose operation, more output.
  --debug                  debug output, way more output.
  --help                   show this help message.
  --version                show the version.
```

Thanks

### Issue Type

Feature Idea

### Component Name

alternatives

### Additional Information

Can you add this feature as example with dict structure

```yaml
alternatives:
  name: 'foo'
  link: '/usr/local/bin/foo'
  path: '/opt/foo/1.0/bin/foo'
  slaves:
    - { 'name': 'foo_png', '/usr/local/pixmaps/foo.png' , 'path':  '/opt/foo/1.0/foo.png' }
  ```

### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/cache/yaml.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/cache/yaml.py)
* [`plugins/modules/system/alternatives.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/system/alternatives.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 @bcoca @mulby
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
Files identified in the description:
* [`plugins/modules/system/alternatives.py`](https://github.com/ansible-collections/community.general/blob/main/plugins/modules/system/alternatives.py)

If these files are incorrect, 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/devel/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
There was a pull request in the ansible repository that added this https://github.com/ansible/ansible/pull/52732. But it doesn't support changing the slaves once they are set. On the other hand, the priority also stays fixed after initially set, so maybe that's intended?
yes that seems a good thing.
If you have to update the slave part, you have to uninstall and install the `alternatives`
To my understanding `update-aletrnatives`command line tool do not support to updates only the slaves part so the features proposed seems good to me
For what it's worth I've just written [an alternatives role](https://github.com/webarch-coop/ansible-role-alternatives) which does have "slaves" support (it uses `ansible.builtin.command: update-alternatives --install`) -- however once the community.general.alternatives module has is released with subcommands support I'll probably update it to use this.

Resolving patch:
diff --git a/plugins/modules/system/alternatives.py b/plugins/modules/system/alternatives.py
--- a/plugins/modules/system/alternatives.py
+++ b/plugins/modules/system/alternatives.py
@@ -3,6 +3,7 @@
 
 # Copyright: (c) 2014, Gabe Mulley <gabe.mulley@gmail.com>
 # Copyright: (c) 2015, David Wittman <dwittman@gmail.com>
+# Copyright: (c) 2022, Marius Rieder <marius.rieder@scs.com>
 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
 
 from __future__ import absolute_import, division, print_function
@@ -17,6 +18,7 @@
     - Manages symbolic links using the 'update-alternatives' tool.
     - Useful when multiple programs are installed but provide similar functionality (e.g. different editors).
 author:
+    - Marius Rieder (@jiuka)
     - David Wittman (@DavidWittman)
     - Gabe Mulley (@mulby)
 options:
@@ -47,10 +49,36 @@
         not set it as the currently selected alternative for the group.
       - C(selected) - install the alternative (if not already installed), and
         set it as the currently selected alternative for the group.
-    choices: [ present, selected ]
+      - C(auto) - install the alternative (if not already installed), and
+        set the group to auto mode. Added in community.general 5.1.0.
+      - C(absent) - removes the alternative. Added in community.general 5.1.0.
+    choices: [ present, selected, auto, absent ]
     default: selected
     type: str
     version_added: 4.8.0
+  subcommands:
+    description:
+      - A list of subcommands.
+      - Each subcommand needs a name, a link and a path parameter.
+    type: list
+    elements: dict
+    aliases: ['slaves']
+    suboptions:
+      name:
+        description:
+          - The generic name of the subcommand.
+        type: str
+        required: true
+      path:
+        description:
+          - The path to the real executable that the subcommand should point to.
+        type: path
+        required: true
+      link:
+        description:
+          - The path to the symbolic link that should point to the real subcommand executable.
+        type: path
+    version_added: 5.1.0
 requirements: [ update-alternatives ]
 '''
 
@@ -78,6 +106,23 @@
     path: /usr/bin/python3.5
     link: /usr/bin/python
     state: present
+
+- name: Install Python 3.5 and reset selection to auto
+  community.general.alternatives:
+    name: python
+    path: /usr/bin/python3.5
+    link: /usr/bin/python
+    state: auto
+
+- name: keytool is a subcommand of java
+  community.general.alternatives:
+    name: java
+    link: /usr/bin/java
+    path: /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java
+    subcommands:
+      - name: keytool
+        link: /usr/bin/keytool
+        path: /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/keytool
 '''
 
 import os
@@ -90,10 +135,235 @@
 class AlternativeState:
     PRESENT = "present"
     SELECTED = "selected"
+    ABSENT = "absent"
+    AUTO = "auto"
 
     @classmethod
     def to_list(cls):
-        return [cls.PRESENT, cls.SELECTED]
+        return [cls.PRESENT, cls.SELECTED, cls.ABSENT, cls.AUTO]
+
+
+class AlternativesModule(object):
+    _UPDATE_ALTERNATIVES = None
+
+    def __init__(self, module):
+        self.module = module
+        self.result = dict(changed=False, diff=dict(before=dict(), after=dict()))
+        self.module.run_command_environ_update = {'LC_ALL': 'C'}
+        self.messages = []
+        self.run()
+
+    @property
+    def mode_present(self):
+        return self.module.params.get('state') in [AlternativeState.PRESENT, AlternativeState.SELECTED, AlternativeState.AUTO]
+
+    @property
+    def mode_selected(self):
+        return self.module.params.get('state') == AlternativeState.SELECTED
+
+    @property
+    def mode_auto(self):
+        return self.module.params.get('state') == AlternativeState.AUTO
+
+    def run(self):
+        self.parse()
+
+        if self.mode_present:
+            # Check if we need to (re)install
+            subcommands_parameter = self.module.params['subcommands']
+            if (
+                self.path not in self.current_alternatives or
+                self.current_alternatives[self.path].get('priority') != self.priority or
+                (subcommands_parameter is not None and (
+                    not all(s in subcommands_parameter for s in self.current_alternatives[self.path].get('subcommands')) or
+                    not all(s in self.current_alternatives[self.path].get('subcommands') for s in subcommands_parameter)
+                ))
+            ):
+                self.install()
+
+            # Check if we need to set the preference
+            if self.mode_selected and self.current_path != self.path:
+                self.set()
+
+            # Check if we need to reset to auto
+            if self.mode_auto and self.current_mode == 'manual':
+                self.auto()
+        else:
+            # Check if we need to uninstall
+            if self.path in self.current_alternatives:
+                self.remove()
+
+        self.result['msg'] = ' '.join(self.messages)
+        self.module.exit_json(**self.result)
+
+    def install(self):
+        if not os.path.exists(self.path):
+            self.module.fail_json(msg="Specified path %s does not exist" % self.path)
+        if not self.link:
+            self.module.fail_json(msg='Needed to install the alternative, but unable to do so as we are missing the link')
+
+        cmd = [self.UPDATE_ALTERNATIVES, '--install', self.link, self.name, self.path, str(self.priority)]
+
+        if self.subcommands is not None:
+            subcommands = [['--slave', subcmd['link'], subcmd['name'], subcmd['path']] for subcmd in self.subcommands]
+            cmd += [item for sublist in subcommands for item in sublist]
+
+        self.result['changed'] = True
+        self.messages.append("Install alternative '%s' for '%s'." % (self.path, self.name))
+
+        if not self.module.check_mode:
+            self.module.run_command(cmd, check_rc=True)
+
+        if self.module._diff:
+            self.result['diff']['after'] = dict(
+                state=AlternativeState.PRESENT,
+                path=self.path,
+                priority=self.priority,
+                link=self.link,
+            )
+            if self.subcommands:
+                self.result['diff']['after'].update(dict(
+                    subcommands=self.subcommands
+                ))
+
+    def remove(self):
+        cmd = [self.UPDATE_ALTERNATIVES, '--remove', self.name, self.path]
+        self.result['changed'] = True
+        self.messages.append("Remove alternative '%s' from '%s'." % (self.path, self.name))
+
+        if not self.module.check_mode:
+            self.module.run_command(cmd, check_rc=True)
+
+        if self.module._diff:
+            self.result['diff']['after'] = dict(state=AlternativeState.ABSENT)
+
+    def set(self):
+        cmd = [self.UPDATE_ALTERNATIVES, '--set', self.name, self.path]
+        self.result['changed'] = True
+        self.messages.append("Set alternative '%s' for '%s'." % (self.path, self.name))
+
+        if not self.module.check_mode:
+            self.module.run_command(cmd, check_rc=True)
+
+        if self.module._diff:
+            self.result['diff']['after']['state'] = AlternativeState.SELECTED
+
+    def auto(self):
+        cmd = [self.UPDATE_ALTERNATIVES, '--auto', self.name]
+        self.messages.append("Set alternative to auto for '%s'." % (self.name))
+        self.result['changed'] = True
+
+        if not self.module.check_mode:
+            self.module.run_command(cmd, check_rc=True)
+
+        if self.module._diff:
+            self.result['diff']['after']['state'] = AlternativeState.PRESENT
+
+    @property
+    def name(self):
+        return self.module.params.get('name')
+
+    @property
+    def path(self):
+        return self.module.params.get('path')
+
+    @property
+    def link(self):
+        return self.module.params.get('link') or self.current_link
+
+    @property
+    def priority(self):
+        return self.module.params.get('priority')
+
+    @property
+    def subcommands(self):
+        if self.module.params.get('subcommands') is not None:
+            return self.module.params.get('subcommands')
+        elif self.path in self.current_alternatives and self.current_alternatives[self.path].get('subcommands'):
+            return self.current_alternatives[self.path].get('subcommands')
+        return None
+
+    @property
+    def UPDATE_ALTERNATIVES(self):
+        if self._UPDATE_ALTERNATIVES is None:
+            self._UPDATE_ALTERNATIVES = self.module.get_bin_path('update-alternatives', True)
+        return self._UPDATE_ALTERNATIVES
+
+    def parse(self):
+        self.current_mode = None
+        self.current_path = None
+        self.current_link = None
+        self.current_alternatives = {}
+
+        # Run `update-alternatives --display <name>` to find existing alternatives
+        (rc, display_output, dummy) = self.module.run_command(
+            [self.UPDATE_ALTERNATIVES, '--display', self.name]
+        )
+
+        if rc != 0:
+            self.module.debug("No current alternative found. '%s' exited with %s" % (self.UPDATE_ALTERNATIVES, rc))
+            return
+
+        current_mode_regex = re.compile(r'\s-\s(?:status\sis\s)?(\w*)(?:\smode|.)$', re.MULTILINE)
+        current_path_regex = re.compile(r'^\s*link currently points to (.*)$', re.MULTILINE)
+        current_link_regex = re.compile(r'^\s*link \w+ is (.*)$', re.MULTILINE)
+        subcmd_path_link_regex = re.compile(r'^\s*slave (\S+) is (.*)$', re.MULTILINE)
+
+        alternative_regex = re.compile(r'^(\/.*)\s-\s(?:family\s\S+\s)?priority\s(\d+)((?:\s+slave.*)*)', re.MULTILINE)
+        subcmd_regex = re.compile(r'^\s+slave (.*): (.*)$', re.MULTILINE)
+
+        match = current_mode_regex.search(display_output)
+        if not match:
+            self.module.debug("No current mode found in output")
+            return
+        self.current_mode = match.group(1)
+
+        match = current_path_regex.search(display_output)
+        if not match:
+            self.module.debug("No current path found in output")
+        else:
+            self.current_path = match.group(1)
+
+        match = current_link_regex.search(display_output)
+        if not match:
+            self.module.debug("No current link found in output")
+        else:
+            self.current_link = match.group(1)
+
+        subcmd_path_map = dict(subcmd_path_link_regex.findall(display_output))
+        if not subcmd_path_map and self.subcommands:
+            subcmd_path_map = dict((s['name'], s['link']) for s in self.subcommands)
+
+        for path, prio, subcmd in alternative_regex.findall(display_output):
+            self.current_alternatives[path] = dict(
+                priority=int(prio),
+                subcommands=[dict(
+                    name=name,
+                    path=spath,
+                    link=subcmd_path_map.get(name)
+                ) for name, spath in subcmd_regex.findall(subcmd) if spath != '(null)']
+            )
+
+        if self.module._diff:
+            if self.path in self.current_alternatives:
+                self.result['diff']['before'].update(dict(
+                    state=AlternativeState.PRESENT,
+                    path=self.path,
+                    priority=self.current_alternatives[self.path].get('priority'),
+                    link=self.current_link,
+                ))
+                if self.current_alternatives[self.path].get('subcommands'):
+                    self.result['diff']['before'].update(dict(
+                        subcommands=self.current_alternatives[self.path].get('subcommands')
+                    ))
+                if self.current_mode == 'manual' and self.current_path != self.path:
+                    self.result['diff']['before'].update(dict(
+                        state=AlternativeState.SELECTED
+                    ))
+            else:
+                self.result['diff']['before'].update(dict(
+                    state=AlternativeState.ABSENT
+                ))
 
 
 def main():
@@ -109,109 +379,16 @@ def main():
                 choices=AlternativeState.to_list(),
                 default=AlternativeState.SELECTED,
             ),
+            subcommands=dict(type='list', elements='dict', aliases=['slaves'], options=dict(
+                name=dict(type='str', required=True),
+                path=dict(type='path', required=True),
+                link=dict(type='path'),
+            )),
         ),
         supports_check_mode=True,
     )
 
-    params = module.params
-    name = params['name']
-    path = params['path']
-    link = params['link']
-    priority = params['priority']
-    state = params['state']
-
-    UPDATE_ALTERNATIVES = module.get_bin_path('update-alternatives', True)
-
-    current_path = None
-    all_alternatives = []
-
-    # Run `update-alternatives --display <name>` to find existing alternatives
-    (rc, display_output, dummy) = module.run_command(
-        ['env', 'LC_ALL=C', UPDATE_ALTERNATIVES, '--display', name]
-    )
-
-    if rc == 0:
-        # Alternatives already exist for this link group
-        # Parse the output to determine the current path of the symlink and
-        # available alternatives
-        current_path_regex = re.compile(r'^\s*link currently points to (.*)$',
-                                        re.MULTILINE)
-        alternative_regex = re.compile(r'^(\/.*)\s-\s(?:family\s\S+\s)?priority', re.MULTILINE)
-
-        match = current_path_regex.search(display_output)
-        if match:
-            current_path = match.group(1)
-        all_alternatives = alternative_regex.findall(display_output)
-
-        if not link:
-            # Read the current symlink target from `update-alternatives --query`
-            # in case we need to install the new alternative before setting it.
-            #
-            # This is only compatible on Debian-based systems, as the other
-            # alternatives don't have --query available
-            rc, query_output, dummy = module.run_command(
-                ['env', 'LC_ALL=C', UPDATE_ALTERNATIVES, '--query', name]
-            )
-            if rc == 0:
-                for line in query_output.splitlines():
-                    if line.startswith('Link:'):
-                        link = line.split()[1]
-                        break
-
-    changed = False
-    if current_path != path:
-
-        # Check mode: expect a change if this alternative is not already
-        # installed, or if it is to be set as the current selection.
-        if module.check_mode:
-            module.exit_json(
-                changed=(
-                    path not in all_alternatives or
-                    state == AlternativeState.SELECTED
-                ),
-                current_path=current_path,
-            )
-
-        try:
-            # install the requested path if necessary
-            if path not in all_alternatives:
-                if not os.path.exists(path):
-                    module.fail_json(msg="Specified path %s does not exist" % path)
-                if not link:
-                    module.fail_json(msg="Needed to install the alternative, but unable to do so as we are missing the link")
-
-                module.run_command(
-                    [UPDATE_ALTERNATIVES, '--install', link, name, path, str(priority)],
-                    check_rc=True
-                )
-                changed = True
-
-            # set the current selection to this path (if requested)
-            if state == AlternativeState.SELECTED:
-                module.run_command(
-                    [UPDATE_ALTERNATIVES, '--set', name, path],
-                    check_rc=True
-                )
-                changed = True
-
-        except subprocess.CalledProcessError as cpe:
-            module.fail_json(msg=str(dir(cpe)))
-    elif current_path == path and state == AlternativeState.PRESENT:
-        # Case where alternative is currently selected, but state is set
-        # to 'present'. In this case, we set to auto mode.
-        if module.check_mode:
-            module.exit_json(changed=True, current_path=current_path)
-
-        changed = True
-        try:
-            module.run_command(
-                [UPDATE_ALTERNATIVES, '--auto', name],
-                check_rc=True,
-            )
-        except subprocess.CalledProcessError as cpe:
-            module.fail_json(msg=str(dir(cpe)))
-
-    module.exit_json(changed=changed)
+    AlternativesModule(module)
 
 
 if __name__ == '__main__':


Test patch:
diff --git a/tests/integration/targets/alternatives/tasks/main.yml b/tests/integration/targets/alternatives/tasks/main.yml
--- a/tests/integration/targets/alternatives/tasks/main.yml
+++ b/tests/integration/targets/alternatives/tasks/main.yml
@@ -49,6 +49,9 @@
   # Test that path is checked: alternatives must fail when path is nonexistent
   - import_tasks: path_is_checked.yml
 
+  # Test that subcommands commands work
+  - import_tasks: subcommands.yml
+
   # Test operation of the 'state' parameter
   - block:
     - include_tasks: remove_links.yml
diff --git a/tests/integration/targets/alternatives/tasks/subcommands.yml b/tests/integration/targets/alternatives/tasks/subcommands.yml
new file mode 100644
--- /dev/null
+++ b/tests/integration/targets/alternatives/tasks/subcommands.yml
@@ -0,0 +1,78 @@
+- name: Try with subcommands
+  alternatives:
+    name: dummymain
+    path: '/usr/bin/dummy1'
+    link: '/usr/bin/dummymain'
+    subcommands:
+      - name: dummysubcmd
+        path: '/usr/bin/dummy2'
+        link: '/usr/bin/dummysubcmd'
+  register: alternative
+
+- name: Check expected command was executed
+  assert:
+    that:
+      - 'alternative is changed'
+
+- name: Execute the current dummymain command
+  command: dummymain
+  register: cmd
+
+- name: Ensure that the expected command was executed
+  assert:
+    that:
+      - cmd.stdout == "dummy1"
+
+- name: Execute the current dummysubcmd command
+  command: dummysubcmd
+  register: cmd
+
+- name: Ensure that the expected command was executed
+  assert:
+    that:
+      - cmd.stdout == "dummy2"
+
+- name: Subcommands are not removed if not specified
+  alternatives:
+    name: dummymain
+    path: '/usr/bin/dummy1'
+    link: '/usr/bin/dummymain'
+  register: alternative
+
+- name: Check expected command was executed
+  assert:
+    that:
+      - 'alternative is not changed'
+
+- name: Execute the current dummysubcmd command
+  command: dummysubcmd
+  register: cmd
+
+- name: Ensure that the expected command was executed
+  assert:
+    that:
+      - cmd.stdout == "dummy2"
+
+- name: Subcommands are removed if set to an empty list
+  alternatives:
+    name: dummymain
+    path: '/usr/bin/dummy1'
+    link: '/usr/bin/dummymain'
+    subcommands: []
+  register: alternative
+
+- name: Check expected command was executed
+  assert:
+    that:
+      - 'alternative is changed'
+
+- name: Execute the current dummysubcmd command
+  command: dummysubcmd
+  register: cmd
+  ignore_errors: True
+
+- name: Ensure that the subcommand is gone
+  assert:
+    that:
+      - cmd.rc == 2
+      - '"No such file" in cmd.msg'
\ No newline at end of file
diff --git a/tests/integration/targets/alternatives/tasks/test.yml b/tests/integration/targets/alternatives/tasks/test.yml
--- a/tests/integration/targets/alternatives/tasks/test.yml
+++ b/tests/integration/targets/alternatives/tasks/test.yml
@@ -48,6 +48,4 @@
   when: ansible_os_family == 'RedHat' and not with_alternatives and item == 1
 
 - name: check that alternative has been updated
-  command: "grep -Pzq '/bin/dummy{{ item }}\\n' '{{ alternatives_dir }}/dummy'"
-  # priority doesn't seem updated
-  #command: "grep -Pzq '/bin/dummy{{ item }}\\n50' '{{ alternatives_dir }}/dummy'"
+  command: "grep -Pzq '/bin/dummy{{ item }}\\n50' '{{ alternatives_dir }}/dummy'"
diff --git a/tests/integration/targets/alternatives/tasks/tests_set_priority.yml b/tests/integration/targets/alternatives/tasks/tests_set_priority.yml
--- a/tests/integration/targets/alternatives/tasks/tests_set_priority.yml
+++ b/tests/integration/targets/alternatives/tasks/tests_set_priority.yml
@@ -21,3 +21,14 @@
 
 - name: check that alternative has been updated
   command: "grep -Pzq '/bin/dummy{{ item }}\\n{{ 60 + item|int }}' '{{ alternatives_dir }}/dummy'"
+
+- name: update dummy priority
+  alternatives:
+    name: dummy
+    path: '/usr/bin/dummy{{ item }}'
+    link: /usr/bin/dummy
+    priority: '{{ 70 + item|int }}'
+  register: alternative
+
+- name: check that alternative priority has been updated
+  command: "grep -Pzq '/bin/dummy{{ item }}\\n{{ 70 + item|int }}' '{{ alternatives_dir }}/dummy'"
\ No newline at end of file
diff --git a/tests/integration/targets/alternatives/tasks/tests_state.yml b/tests/integration/targets/alternatives/tasks/tests_state.yml
--- a/tests/integration/targets/alternatives/tasks/tests_state.yml
+++ b/tests/integration/targets/alternatives/tasks/tests_state.yml
@@ -49,6 +49,28 @@
       - cmd.stdout == "dummy4"
 
 # Set the currently selected alternative to state = 'present' (was previously
+# selected), and ensure that this results in the group not being set to 'auto'
+# mode, and the alternative is still selected.
+- name: Set current selected dummy to state = present
+  alternatives:
+    name: dummy
+    path: /usr/bin/dummy4
+    link: /usr/bin/dummy
+    state: present
+
+- name: Ensure that the link group is in auto mode
+  shell: 'head -n1 {{ alternatives_dir }}/dummy | grep "^manual$"'
+
+- name: Execute the current dummy command
+  shell: dummy
+  register: cmd
+
+- name: Ensure that the expected command was executed
+  assert:
+    that:
+      - cmd.stdout == "dummy4"
+
+# Set the currently selected alternative to state = 'auto' (was previously
 # selected), and ensure that this results in the group being set to 'auto'
 # mode, and the highest priority alternative is selected.
 - name: Set current selected dummy to state = present
@@ -56,7 +78,7 @@
     name: dummy
     path: /usr/bin/dummy4
     link: /usr/bin/dummy
-    state: present
+    state: auto
 
 - name: Ensure that the link group is in auto mode
   shell: 'head -n1 {{ alternatives_dir }}/dummy | grep "^auto$"'
@@ -69,3 +91,25 @@
   assert:
     that:
       - cmd.stdout == "dummy2"
+
+# Remove an alternative with state = 'absent' and make sure that
+# this change results in the alternative being removed.
+- name: Remove best dummy alternative with state = absent
+  alternatives:
+    name: dummy
+    path: /usr/bin/dummy2
+    state: absent
+
+- name: Ensure that the link group is in auto mode
+  shell: 'grep "/usr/bin/dummy2" {{ alternatives_dir }}/dummy'
+  register: cmd
+  failed_when: cmd.rc == 0
+
+- name: Execute the current dummy command
+  shell: dummy
+  register: cmd
+
+- name: Ensure that the expected command was executed
+  assert:
+    that:
+      - cmd.stdout == "dummy1"