From 20f46f76697d96fa2752d46e240f477069abd35c Mon Sep 17 00:00:00 2001 From: Alexei Znamensky <103110+russoz@users.noreply.github.com> Date: Sun, 25 Jul 2021 08:30:46 +1200 Subject: [PATCH] xfconf_info - new module (#3045) * xfconf_info initial commit * Update plugins/modules/system/xfconf_info.py Co-authored-by: Felix Fontein * Update plugins/modules/system/xfconf_info.py Co-authored-by: Felix Fontein * Update plugins/modules/system/xfconf_info.py Co-authored-by: Felix Fontein * added register to all examples * Update plugins/modules/system/xfconf_info.py Co-authored-by: Felix Fontein --- plugins/modules/system/xfconf_info.py | 190 ++++++++++++++++++ plugins/modules/xfconf_info.py | 1 + .../modules/system/test_xfconf_info.py | 171 ++++++++++++++++ 3 files changed, 362 insertions(+) create mode 100644 plugins/modules/system/xfconf_info.py create mode 120000 plugins/modules/xfconf_info.py create mode 100644 tests/unit/plugins/modules/system/test_xfconf_info.py diff --git a/plugins/modules/system/xfconf_info.py b/plugins/modules/system/xfconf_info.py new file mode 100644 index 0000000000..9cef821071 --- /dev/null +++ b/plugins/modules/system/xfconf_info.py @@ -0,0 +1,190 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021, Alexei Znamensky +# 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 +__metaclass__ = type + +DOCUMENTATION = ''' +module: xfconf_info +author: + - "Alexei Znamensky (@russoz)" +short_description: Retrieve XFCE4 configurations +version_added: 3.5.0 +description: + - This module allows retrieving Xfce 4 configurations with the help of C(xfconf-query). +options: + channel: + description: + - > + A Xfconf preference channel is a top-level tree key, inside of the + Xfconf repository that corresponds to the location for which all + application properties/keys are stored. + - If not provided, the module will list all available channels. + type: str + property: + description: + - > + A Xfce preference key is an element in the Xfconf repository + that corresponds to an application preference. + - If provided, then I(channel) is required. + - If not provided and a I(channel) is provided, then the module will list all available properties in that I(channel). + type: str +notes: + - See man xfconf-query(1) for more details. +''' + +EXAMPLES = """ +- name: Get list of all available channels + community.general.xfconf_info: {} + register: result + +- name: Get list of all properties in a specific channel + community.general.xfconf_info: + channel: xsettings + register: result + +- name: Retrieve the DPI value + community.general.xfconf_info: + channel: xsettings + property: /Xft/DPI + register: result + +- name: Get workspace names (4) + community.general.xfconf_info: + channel: xfwm4 + property: /general/workspace_names + register: result +""" + +RETURN = ''' + channels: + description: + - List of available channels. + - Returned when the module receives no parameter at all. + returned: success + type: list + elements: str + sample: + - xfce4-desktop + - displays + - xsettings + - xfwm4 + properties: + description: + - List of available properties for a specific channel. + - Returned by passed only the I(channel) parameter to the module. + returned: success + type: list + elements: str + sample: + - /Gdk/WindowScalingFactor + - /Gtk/ButtonImages + - /Gtk/CursorThemeSize + - /Gtk/DecorationLayout + - /Gtk/FontName + - /Gtk/MenuImages + - /Gtk/MonospaceFontName + - /Net/DoubleClickTime + - /Net/IconThemeName + - /Net/ThemeName + - /Xft/Antialias + - /Xft/Hinting + - /Xft/HintStyle + - /Xft/RGBA + is_array: + description: + - Flag indicating whether the property is an array or not. + returned: success + type: bool + value: + description: + - The value of the property. Empty if the property is of array type. + returned: success + type: str + sample: Monospace 10 + value_array: + description: + - The array value of the property. Empty if the property is not of array type. + returned: success + type: list + elements: str + sample: + - Main + - Work + - Tmp +''' + +from ansible_collections.community.general.plugins.module_utils.module_helper import CmdModuleHelper, ArgFormat + + +class XFConfException(Exception): + pass + + +class XFConfInfo(CmdModuleHelper): + module = dict( + argument_spec=dict( + channel=dict(type='str'), + property=dict(type='str'), + ), + required_by=dict( + property=['channel'] + ), + ) + + command = 'xfconf-query' + command_args_formats = dict( + channel=dict(fmt=['--channel', '{0}']), + property=dict(fmt=['--property', '{0}']), + _list_arg=dict(fmt="--list", style=ArgFormat.BOOLEAN), + ) + check_rc = True + + def __init_module__(self): + self.vars.set("_list_arg", False, output=False) + self.vars.set("is_array", False) + + def process_command_output(self, rc, out, err): + result = out.rstrip() + if "Value is an array with" in result: + result = result.split("\n") + result.pop(0) + result.pop(0) + self.vars.is_array = True + + return result + + def _process_list_properties(self, rc, out, err): + return out.splitlines() + + def _process_list_channels(self, rc, out, err): + lines = out.splitlines() + lines.pop(0) + lines = [s.lstrip() for s in lines] + return lines + + def __run__(self): + self.vars._list_arg = not (bool(self.vars.channel) and bool(self.vars.property)) + output = 'value' + proc = self.process_command_output + if self.vars.channel is None: + output = 'channels' + proc = self._process_list_channels + elif self.vars.property is None: + output = 'properties' + proc = self._process_list_properties + result = self.run_command(params=('_list_arg', 'channel', 'property'), process_output=proc) + if not self.vars._list_arg and self.vars.is_array: + output = "value_array" + self.vars.set(output, result) + + +def main(): + xfconf = XFConfInfo() + xfconf.run() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/xfconf_info.py b/plugins/modules/xfconf_info.py new file mode 120000 index 0000000000..5bf95b50b5 --- /dev/null +++ b/plugins/modules/xfconf_info.py @@ -0,0 +1 @@ +system/xfconf_info.py \ No newline at end of file diff --git a/tests/unit/plugins/modules/system/test_xfconf_info.py b/tests/unit/plugins/modules/system/test_xfconf_info.py new file mode 100644 index 0000000000..528622d0ee --- /dev/null +++ b/tests/unit/plugins/modules/system/test_xfconf_info.py @@ -0,0 +1,171 @@ +# Author: Alexei Znamensky (russoz@gmail.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) +__metaclass__ = type + +import json + +from ansible_collections.community.general.plugins.modules.system import xfconf_info + +import pytest + +TESTED_MODULE = xfconf_info.__name__ + + +@pytest.fixture +def patch_xfconf_info(mocker): + """ + Function used for mocking some parts of redhat_subscribtion module + """ + mocker.patch('ansible_collections.community.general.plugins.module_utils.mh.module_helper.AnsibleModule.get_bin_path', + return_value='/testbin/xfconf-query') + + +TEST_CASES = [ + [ + {'channel': 'xfwm4', 'property': '/general/inactive_opacity'}, + { + 'id': 'test_simple_property_get', + 'run_command.calls': [ + ( + # Calling of following command will be asserted + ['/testbin/xfconf-query', '--channel', 'xfwm4', '--property', '/general/inactive_opacity'], + # Was return code checked? + {'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': True}, + # Mock of returned code, stdout and stderr + (0, '100\n', '',), + ), + ], + 'is_array': False, + 'value': '100', + } + ], + [ + {'channel': 'xfwm4', 'property': '/general/i_dont_exist'}, + { + 'id': 'test_simple_property_get_nonexistent', + 'run_command.calls': [ + ( + # Calling of following command will be asserted + ['/testbin/xfconf-query', '--channel', 'xfwm4', '--property', '/general/i_dont_exist'], + # Was return code checked? + {'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': True}, + # Mock of returned code, stdout and stderr + (1, '', 'Property "/general/i_dont_exist" does not exist on channel "xfwm4".\n',), + ), + ], + 'is_array': False, + } + ], + [ + {'property': '/general/i_dont_exist'}, + { + 'id': 'test_property_no_channel', + 'run_command.calls': [], + } + ], + [ + {'channel': 'xfwm4', 'property': '/general/workspace_names'}, + { + 'id': 'test_property_get_array', + 'run_command.calls': [ + ( + # Calling of following command will be asserted + ['/testbin/xfconf-query', '--channel', 'xfwm4', '--property', '/general/workspace_names'], + # Was return code checked? + {'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': True}, + # Mock of returned code, stdout and stderr + (0, 'Value is an array with 3 items:\n\nMain\nWork\nTmp\n', '',), + ), + ], + 'is_array': True, + 'value_array': ['Main', 'Work', 'Tmp'], + }, + ], + [ + {}, + { + 'id': 'get_channels', + 'run_command.calls': [ + ( + # Calling of following command will be asserted + ['/testbin/xfconf-query', '--list'], + # Was return code checked? + {'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': True}, + # Mock of returned code, stdout and stderr + (0, 'Channels:\n a\n b\n c\n', '',), + ), + ], + 'is_array': False, + 'channels': ['a', 'b', 'c'], + }, + ], + [ + {'channel': 'xfwm4'}, + { + 'id': 'get_properties', + 'run_command.calls': [ + ( + # Calling of following command will be asserted + ['/testbin/xfconf-query', '--list', '--channel', 'xfwm4'], + # Was return code checked? + {'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': True}, + # Mock of returned code, stdout and stderr + (0, '/general/wrap_cycle\n/general/wrap_layout\n/general/wrap_resistance\n/general/wrap_windows\n' + '/general/wrap_workspaces\n/general/zoom_desktop\n', '',), + ), + ], + 'is_array': False, + 'properties': [ + '/general/wrap_cycle', + '/general/wrap_layout', + '/general/wrap_resistance', + '/general/wrap_windows', + '/general/wrap_workspaces', + '/general/zoom_desktop', + ], + }, + ], +] +TEST_CASES_IDS = [item[1]['id'] for item in TEST_CASES] + + +@pytest.mark.parametrize('patch_ansible_module, testcase', + TEST_CASES, + ids=TEST_CASES_IDS, + indirect=['patch_ansible_module']) +@pytest.mark.usefixtures('patch_ansible_module') +def test_xfconf_info(mocker, capfd, patch_xfconf_info, testcase): + """ + Run unit tests for test cases listen in TEST_CASES + """ + + # Mock function used for running commands first + call_results = [item[2] for item in testcase['run_command.calls']] + mock_run_command = mocker.patch( + 'ansible_collections.community.general.plugins.module_utils.mh.module_helper.AnsibleModule.run_command', + side_effect=call_results) + + # Try to run test case + with pytest.raises(SystemExit): + xfconf_info.main() + + out, err = capfd.readouterr() + results = json.loads(out) + print("testcase =\n%s" % testcase) + print("results =\n%s" % results) + + for conditional_test_result in ('value_array', 'value', 'is_array', 'properties', 'channels'): + if conditional_test_result in testcase: + assert conditional_test_result in results, "'{0}' not found in {1}".format(conditional_test_result, results) + assert results[conditional_test_result] == testcase[conditional_test_result], \ + "'{0}': '{1}' != '{2}'".format(conditional_test_result, results[conditional_test_result], testcase[conditional_test_result]) + + assert mock_run_command.call_count == len(testcase['run_command.calls']) + if mock_run_command.call_count: + call_args_list = [(item[0][0], item[1]) for item in mock_run_command.call_args_list] + expected_call_args_list = [(item[0], item[1]) for item in testcase['run_command.calls']] + print("call args list =\n%s" % call_args_list) + print("expected args list =\n%s" % expected_call_args_list) + assert call_args_list == expected_call_args_list