From 37f3b8b14e9986a6fcffe870aa68bdbea960181f Mon Sep 17 00:00:00 2001 From: kschopmeyer Date: Mon, 10 Aug 2020 09:54:57 -0500 Subject: [PATCH] Move all functions associated with display_cimobjects to separate module. This is first part of adding the code for issue #249, display classes as tables. Because the new code is going to significantly increase the size of the functions associated with displaying objects as tables, it was logical to move this from _common.py to its own file. Fixes alse one pylint issue by changing code Fixes issue where pylint was reporting possible undefined variable in pick_one_from_list() when the variable was part of a for statement by not using that variable and creating a new variable to represent the same information. In the process we noted that there was no test for the correct pick of last item in the list and confirmation that if the next higher number was picked it treated as invalid a so test was added. --- docs/changes.rst | 3 + pywbemtools/pywbemcli/__init__.py | 1 + pywbemtools/pywbemcli/_cmd_class.py | 6 +- pywbemtools/pywbemcli/_cmd_instance.py | 5 +- pywbemtools/pywbemcli/_cmd_qualifier.py | 3 +- pywbemtools/pywbemcli/_common.py | 610 +++---------------- pywbemtools/pywbemcli/_display_cimobjects.py | 536 ++++++++++++++++ tests/unit/test_common.py | 545 +---------------- tests/unit/test_display_cimobjects.py | 554 +++++++++++++++++ 9 files changed, 1191 insertions(+), 1072 deletions(-) create mode 100644 pywbemtools/pywbemcli/_display_cimobjects.py create mode 100644 tests/unit/test_display_cimobjects.py diff --git a/docs/changes.rst b/docs/changes.rst index 35b656322..7b640b224 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -75,6 +75,9 @@ Released: not yet --connections-file general option in various places, for consistency. (Related to issue #708) +* Move code associated with display_cimobjects() to a separate module. This + is part of creating table representation of classes (See issue #249) + **Known issues:** * See `list of open issues`_. diff --git a/pywbemtools/pywbemcli/__init__.py b/pywbemtools/pywbemcli/__init__.py index 5f13ed805..d8ab94fe7 100644 --- a/pywbemtools/pywbemcli/__init__.py +++ b/pywbemtools/pywbemcli/__init__.py @@ -39,6 +39,7 @@ from ._association_shrub import * # noqa: F403,F401 from ._utils import * # noqa: F403,F401 from ._cimvalueformatter import * # noqa: F403,F401 +from ._display_cimobjects import * # noqa: F403,F401 from .._version import __version__ # noqa: F401 diff --git a/pywbemtools/pywbemcli/_cmd_class.py b/pywbemtools/pywbemcli/_cmd_class.py index 3dc191a10..08c4cf00a 100644 --- a/pywbemtools/pywbemcli/_cmd_class.py +++ b/pywbemtools/pywbemcli/_cmd_class.py @@ -29,10 +29,13 @@ CIM_ERR_NOT_FOUND, CIMClass from .pywbemcli import cli -from ._common import display_cim_objects, filter_namelist, \ +from ._common import filter_namelist, \ resolve_propertylist, CMD_OPTS_TXT, GENERAL_OPTS_TXT, SUBCMD_HELP_TXT, \ output_format_is_table, format_table, process_invokemethod, \ raise_pywbem_error_exception, warning_msg, validate_output_format + +from ._display_cimobjects import display_cim_objects + from ._common_options import add_options, propertylist_option, \ names_only_option, include_classorigin_class_option, namespace_option, \ summary_option, multiple_namespaces_option, class_filter_options, \ @@ -708,6 +711,7 @@ def cmd_class_get(context, classname, options): the class. If the class cannot be found, the server returns a CIMError exception. """ + format_group = get_format_group(context, options) output_format = validate_output_format(context.output_format, format_group) diff --git a/pywbemtools/pywbemcli/_cmd_instance.py b/pywbemtools/pywbemcli/_cmd_instance.py index 500e20354..61b2c81d2 100644 --- a/pywbemtools/pywbemcli/_cmd_instance.py +++ b/pywbemtools/pywbemcli/_cmd_instance.py @@ -28,13 +28,14 @@ CIM_ERR_NOT_FOUND from .pywbemcli import cli -from ._common import display_cim_objects, \ - pick_instance, resolve_propertylist, create_ciminstance, \ +from ._common import pick_instance, resolve_propertylist, create_ciminstance, \ filter_namelist, format_table, verify_operation, \ process_invokemethod, raise_pywbem_error_exception, \ parse_kv_pair, warning_msg, validate_output_format, \ CMD_OPTS_TXT, GENERAL_OPTS_TXT, SUBCMD_HELP_TXT +from ._display_cimobjects import display_cim_objects + from ._common_options import add_options, propertylist_option, \ names_only_option, include_classorigin_instance_option, namespace_option, \ summary_option, verify_option, multiple_namespaces_option, \ diff --git a/pywbemtools/pywbemcli/_cmd_qualifier.py b/pywbemtools/pywbemcli/_cmd_qualifier.py index 7db39cebc..81efc2213 100644 --- a/pywbemtools/pywbemcli/_cmd_qualifier.py +++ b/pywbemtools/pywbemcli/_cmd_qualifier.py @@ -27,9 +27,10 @@ from pywbem import Error from .pywbemcli import cli -from ._common import display_cim_objects, sort_cimobjects, \ +from ._common import sort_cimobjects, \ raise_pywbem_error_exception, validate_output_format, \ CMD_OPTS_TXT, GENERAL_OPTS_TXT, SUBCMD_HELP_TXT +from ._display_cimobjects import display_cim_objects from ._common_options import add_options, namespace_option, summary_option, \ help_option from ._click_extensions import PywbemcliGroup, PywbemcliCommand diff --git a/pywbemtools/pywbemcli/_common.py b/pywbemtools/pywbemcli/_common.py index 582b353c9..d5550849e 100644 --- a/pywbemtools/pywbemcli/_common.py +++ b/pywbemtools/pywbemcli/_common.py @@ -29,16 +29,13 @@ except ImportError: from ordereddict import OrderedDict # pylint: disable=import-error -from pydicti import odicti import six import click import tabulate from pywbem import CIMInstanceName, CIMInstance, CIMClass, \ CIMQualifierDeclaration, CIMProperty, CIMClassName, \ - cimvalue, ValueMapping - -from .config import USE_TERMINAL_WIDTH, DEFAULT_TABLE_WIDTH + cimvalue from ._cimvalueformatter import cimvalue_to_fmtd_string @@ -51,7 +48,6 @@ DEFAULT_MAX_CELL_WIDTH = 100 -INT_TYPE_PATTERN = re.compile(r'^[su]int(8|16|32|64)$') ############################################################## # @@ -263,8 +259,8 @@ def pick_one_from_list(context, options, title): Raises: ValueError if Ctrl-c input from console. - TODO: Possible Future This could be replaced by the python pick library - that would use curses for the selection process. + TODO/Future: Possible Future This could be replaced by the python + pick library that would use curses for the selection process. """ # If there is only a single choice, return that choice. @@ -278,9 +274,10 @@ def pick_one_from_list(context, options, title): click.echo(title) for index, str_ in enumerate(options): click.echo('{}: {}'.format(index, str_)) + max_option = len(options) - 1 selection = None msg = 'Input integer between 0 and {} or Ctrl-C to exit selection' \ - .format(index) + .format(max_option) # Loop for valid user choice until valid choice made or selection aborted # by user @@ -288,7 +285,7 @@ def pick_one_from_list(context, options, title): try: selection_txt = click.prompt(msg) selection = int(selection_txt) - if 0 <= selection <= index: + if 0 <= selection <= max_option: if context: context.spinner_start() return options[selection] @@ -814,33 +811,6 @@ def split_str_w_esc(astring, delimiter, escape='\\'): return ret -def get_cimtype(objects): - """ - Get the cim_type for any returned cim object. Normally this is the - name of the class name except that the classname return from - getclass and enumerate class is just unicode string - """ - # associators and references return tuple - if isinstance(objects, list): - test_object = objects[0] - elif objects: - test_object = object - else: - cim_type = 'unknown' - return None - - if isinstance(test_object, tuple): - # associator or reference class level return is tuple - cim_type = test_object[0].__class__.__name__ - else: - cim_type = test_object.__class__.__name__ - - # account for fact the enumerate class name operation returns uniicode. - if isinstance(test_object, six.string_types): - cim_type = 'CIMClassName' - return cim_type - - def process_invokemethod(context, objectname, methodname, options): # pylint: disable=line-too-long """ @@ -920,13 +890,6 @@ def create_params(classname, cim_method, kv_params): click.echo('{}={}'.format(pname, val[0])) -#################################################################### -# -# Display of CIM objects. -# -#################################################################### - - def sort_cimobjects(cim_objects): """ Sort lists of CIMClass, CIMCLassName, CIMQualifierDecl, CIMInstance or @@ -977,218 +940,6 @@ def sort_cimobjects(cim_objects): return [sort_dict[key] for key in sorted(sort_dict.keys())] -def display_cim_objects_summary(context, objects, output_format): - """ - Display a summary of the objects received. This displays the - count of objects. - """ - context.spinner_stop() - - if objects: - cim_type = get_cimtype(objects) - - if output_format_is_table(output_format): - rows = [[len(objects), cim_type]] - click.echo(format_table(rows, ['Count', 'CIM Type'], - title='Summary of {} returned' - .format(cim_type), - table_format=output_format)) - return - click.echo('{} {}(s) returned'.format(len(objects), cim_type)) - - else: - click.echo('0 objects returned') - - -def display_cim_objects(context, cim_objects, output_format, summary=False, - sort=False): - """ - Display CIM objects in form determined by input parameters. - - Input is either a list of cim objects or a single object. It may be - any of the CIM types. This is used to display: - - * CIMClass - - * CIMClassName: - - * CIMInstance - - * CIMInstanceName - - * CIMQualifierDeclaration - - * Or list of the above - - This function may override output type choice in cases where the output - choice is not available for the object type. Thus, for example, - mof output makes no sense for class names. In that case, the output is - the str of the type. - - Parameters: - - context (:class:`ContextObj`): - Click context contained in ContextObj object. - - objects (iterable of :class:`~pywbem.CIMInstance`, - :class:`~pywbem.CIMInstanceName`, :class:`~pywbem.CIMClass`, - :class:`~pywbem.CIMClassName`, - or :class:`~pywbem.CIMQualifierDeclaration`): - Iterable of zero or more CIM objects to be displayed. - - output_format (:term:`string`): - String defining the preferred output format. Must not be None since - the correct output_format must have been selected before this call. - Note that the output formats allowed may depend on a) whether - summary is True, b)the specific type because we do not have a table - output format for CIMClass. - - summary (:class:`py:bool`): - Boolean that defines whether the data in objects should be displayed - or just a summary of the objects (ex. count of number of objects). - """ - # Note: In the docstring above, the line for parameter 'objects' was way too - # long. Since we are not putting it into docmentation, we folded it. - - context.spinner_stop() - - if summary: - display_cim_objects_summary(context, cim_objects, output_format) - return - - if not cim_objects and context.verbose: - click.echo("No objects returned") - return - - if sort: - cim_objects = sort_cimobjects(cim_objects) - - # default when displaying cim objects is mof - assert output_format - - if isinstance(cim_objects, (list, tuple)): - # Table format output is processed as a group - if output_format_is_table(output_format): - _print_objects_as_table(cim_objects, output_format, context=context) - else: - # Call to display each object - for obj in cim_objects: - display_cim_objects(context, obj, output_format=output_format) - return - - # Display a single item. - object_ = cim_objects - # This allows passing single objects to the table formatter (i.e. not lists) - if output_format_is_table(output_format): - _print_objects_as_table([object_], output_format, context=context) - elif output_format == 'mof': - try: - click.echo(object_.tomof()) - except AttributeError: - # insert NL between instance names for readability - if isinstance(object_, CIMInstanceName): - click.echo("") - click.echo(object_) - elif isinstance(object_, (CIMClassName, six.string_types)): - click.echo(object_) - else: - raise click.ClickException('output_format {} invalid for {} ' - .format(output_format, - type(object_))) - elif output_format == 'xml': - try: - click.echo(object_.tocimxmlstr(indent=4)) - except AttributeError: - # no tocimxmlstr functionality - raise click.ClickException('Output Format {} not supported. ' - 'Default to\n{!r}' - .format(output_format, object_)) - elif output_format == 'repr': - try: - click.echo(repr(object_)) - except AttributeError: - raise click.ClickException('"repr" display of {!r} failed' - .format(object_)) - - elif output_format == 'txt': - try: - click.echo(object_) - except AttributeError: - raise click.ClickException('"txt" display of {!r} failed' - .format(object_)) - # elif output_format == 'tree': - # raise click.ClickException('Tree output format not allowed') - else: - raise click.ClickException('Invalid output format {}' - .format(output_format)) - - -def _print_classes_as_table(classes, table_width, table_format): - """ - TODO: Future extend to display classes as a table, showing the - properties for each class. This will display the properties that exist in - subclasses. The temp output - so we could create the function is to just output as mof - """ - # pylint: disable=unused-argument - - for class_ in classes: - click.echo(class_.tomof()) - - -def format_keys(obj, max_width): - """ - Format the keys of a dictionary of keybindings as text for display. Formats - multiple keybindings on each line within the max_width - - Parameters: - - obj (:class:`pwbem.CIMInstanceName`): - Instance name from which keybindings are to be extracted for - formatting. - - Returns: - :term:`string` containing the keys from the input obj formatted for - display at within the defined width. - """ - def get_wbemurikeys(obj): - """ - Create wbem_uri from CIMInstanceName and separate out key component - for return. - """ - wbem_uri = obj.to_wbem_uri() - wbem_uri_keys = wbem_uri[wbem_uri.find('.'):] - wbem_uri_keys = wbem_uri_keys[1:] - return wbem_uri_keys - - assert isinstance(obj, CIMInstanceName) - # clear the host and namespace - myobj = obj.copy() - myobj.host = None - myobj.namespace = None - wbem_uri_keys = get_wbemurikeys(myobj) - - # Too long for width. Fold the keys on multiple lines - if len(wbem_uri_keys) > max_width: - wbem_uri_keys = '' - line_len = 0 - for key, value in myobj.keybindings.items(): - one_key_obj = get_wbemurikeys((CIMInstanceName('x', {key: value}))) - if wbem_uri_keys: - if line_len + len(one_key_obj) > max_width: - wbem_uri_keys += '\n{}'.format(one_key_obj) - line_len = 0 - else: - wbem_uri_keys += ',{}'.format(one_key_obj) - line_len += len(one_key_obj) + 1 - - else: # must put on first line even if too long - wbem_uri_keys += one_key_obj - line_len = len(one_key_obj) + 1 - - return wbem_uri_keys - - def display_text(text, output_format=None): # pylint: disable=unused-argument """ Display the text output format. Currently this simply outputs to @@ -1251,300 +1002,57 @@ def shorten_path_str(path, replacements, fullpath): return name_str -def _print_paths_as_table(objects, table_width, table_format): - # pylint: disable=unused-argument - """ - Display paths as a table. This include CIMInstanceName, ClassPath, - and unicode (the return type for enumerateClasses). - """ - title = None - if objects: - if isinstance(objects[0], six.string_types): - title = 'Classnames:' - headers = ['Class Name'] - rows = [[obj] for obj in objects] - elif isinstance(objects[0], CIMClassName): - title = 'Classnames' - headers = ('host', 'namespace', 'class') - rows = [[obj.host, obj.namespace, obj.classname] for obj in objects] - elif isinstance(objects[0], CIMInstanceName): - title = 'InstanceNames: {}'.format(objects[0].classname) - host_hdr = 'host' - ns_hdr = 'namespace' - class_hdr = 'class' - host_hdr_len = len(host_hdr) + 4 - ns_hdr_len = len(ns_hdr) + 3 - class_hdr_len = len(class_hdr) + 3 - headers = (host_hdr, ns_hdr, class_hdr, 'keysbindings') - - host_lens = [len(obj.host) for obj in objects if obj.host] - host_max = max(host_lens) if host_lens else host_hdr_len - ns_lens = [len(obj.namespace) for obj in objects if obj.namespace] - ns_max = max(ns_lens) if ns_lens else ns_hdr_len - class_lens = [len(obj.classname) for obj in objects] - class_max = max(class_lens) if class_lens else class_hdr_len - - max_key_len = (table_width) - (host_max + ns_max + class_max + 3) - rows = [[obj.host, obj.namespace, obj.classname, - format_keys(obj, max_key_len)] for obj in objects] - else: - raise click.ClickException("{0} invalid type ({1})for path display". - format(objects[0], type(objects[0]))) - - click.echo(format_table(rows, headers, title=title, - table_format=table_format)) - - -def _print_qual_decls_as_table(qual_decls, table_width, table_format): - """ - Display the elements of qualifier declarations as a table with a - row for each qualifier declaration and a column for each of the attributes - of the qualifier declaration (name, type, Value, Array, Scopes, Flavors. - - The function displays all of the qualifier declarations in the - """ - rows = [] - headers = ['Name', 'Type', 'Value', 'Array', 'Scopes', 'Flavors'] - max_column_width = int((table_width / len(headers)) - 4) - for q in qual_decls: - scopes = '\n'.join([key for key in q.scopes if q.scopes[key]]) - flavors = [] - flavors.append('EnableOverride' if q.overridable else 'DisableOverride') - flavors.append('ToSubclass' if q.tosubclass else 'Restricted') - if q.translatable: - flavors.append('Translatable') - if sum([len(i) for i in flavors]) >= max_column_width: - sep = "\n" - else: - sep = ", " - flavors = sep.join(flavors) - - row = [q.name, q.type, q.value, q.is_array, scopes, flavors] - rows.append(row) - - click.echo(format_table(rows, headers, title='Qualifier Declarations', - table_format=table_format)) - - -def _format_instances_as_rows(insts, max_cell_width=DEFAULT_MAX_CELL_WIDTH, - include_classes=False, context=None, - prop_names=None): +def format_keys(obj, max_width): """ - Format the list of instances properties into as a list of the property - values for each instance( a row of the table) gathered into a list of - the rows. - - The prop_names parameter is the list of (originally cased) property names - to be output, in the desired output order. It could be determined from - the instances, but since it is determined already by the caller, it - is passed in as an optimization. For test convenience, None is permitted - and causes the properties to again be determined from the instances. - - Include_classes for each instance if True. Sets the classname as the first - column. - - max_width if not None folds col entries longer than the defined - max_cell_width. If max_width is None, the data length is ignored. + Format the keys of a dictionary of keybindings as text for display. Formats + multiple keybindings on each line within the max_width - The property values are formatted similar to MOF output. Properties that - have a ValueMap qualifier (effectively, in the creation class of the - instance) are shown with both the actual property value and the mapped - value in parenthesis. + Parameters: - NOTE: This is a separate function to allow testing of the table formatting - independently of print output. + obj (:class:`pwbem.CIMInstanceName`): + Instance name from which keybindings are to be extracted for + formatting. Returns: - list of strings where each string is a row in the table and each - item in a row is a cell entry - """ - # Avoid crash deeper in code if max_cell_width is None. - if max_cell_width is None: - max_cell_width = DEFAULT_MAX_CELL_WIDTH - lines = [] - - if prop_names is None: - prop_names = sorted_prop_names(insts) - - # Cache of ValueMapping objects for integer-typed properties. - # Key: classname.propertyname, both in lower case. - # A value of None indicates the property does not have a value mapping. - valuemappings = {} - - for inst in insts: - if not isinstance(inst, CIMInstance): - raise ValueError('Only accepts CIMInstance; not type {}' - .format(type(inst))) - - # Insert classname as first col if flag set - line = [inst.classname] if include_classes else [] - - # get value for each property in this object - for name in prop_names: - - # Account for possible instances without all properties - # Outputs empty string. Note that instance with no value - # results in same output as not instance name. - if name not in inst.properties: - val_str = '' - else: - value = inst.get(name) - p = inst.properties[name] - - # Cache value mappings for integer-typed properties - if INT_TYPE_PATTERN.match(p.type) and context: - vm_key = '{}.{}'.format( - inst.classname.lower(), name.lower()) - try: - valuemapping = valuemappings[vm_key] - except KeyError: - try: - valuemapping = ValueMapping.for_property( - context.conn, - context.conn.default_namespace, - inst.classname, - name) - except ValueError: - # Property does not have a value mapping. - valuemapping = None - valuemappings[vm_key] = valuemapping - else: - valuemapping = None - - if value is None: - val_str = u'' - else: - val_str, _ = cimvalue_to_fmtd_string( - p.value, p.type, indent=0, maxline=max_cell_width, - line_pos=0, end_space=0, avoid_splits=False, - valuemapping=valuemapping) - - line.append(val_str) - lines.append(line) - - return lines - - -def _print_instances_as_table(insts, table_width, table_format, - include_classes=False, context=None): - """ - Print the properties of the instances defined in insts as a table where - each row is an instance and each column is a property value. - - All properties in the instance are included. - - The header line consists of the property names. - - The property values are formatted similar to MOF output. Properties that - have a ValueMap qualifier (effectively, in the creation class of the - instance) are shown with both the actual property value and the mapped - value in parenthesis. - """ - - if table_width is None: - table_width = DEFAULT_TABLE_WIDTH - - for inst in insts: - if not isinstance(inst, CIMInstance): - raise ValueError('Only CIMInstance display allows table output') - - prop_names = sorted_prop_names(insts) - - # Try to estimate max cell width from number of cols - # This allows folding long data. However it is incomplete in - # that we do not fold the property name. Further, the actual output - # width of a column involves the tabulate outputter, output_format - # so this is not deterministic. - if prop_names: - num_cols = len(prop_names) - max_cell_width = int(table_width / num_cols) - 2 - else: - max_cell_width = table_width - - header_line = [] - if include_classes: - header_line.append("classname") - header_line.extend(prop_names) - - # Fold long property names - new_header_line = [] - for header in header_line: - if len(header) > max_cell_width: - new_header_line.append(fold_strings(header, max_cell_width)) - else: - new_header_line.append(header) - - rows = _format_instances_as_rows(insts, max_cell_width=max_cell_width, - include_classes=include_classes, - context=context, prop_names=prop_names) - - title = 'Instances: {}'.format(insts[0].classname) - click.echo(format_table(rows, new_header_line, title=title, - table_format=table_format)) - - -def sorted_prop_names(insts): + :term:`string` containing the keys from the input obj formatted for + display at within the defined width. """ - Return the list of (originally cased) property names that is the superset - of all properties in the input instances. + def get_wbemurikeys(obj): + """ + Create wbem_uri from CIMInstanceName and separate out key component + for return. + """ + wbem_uri = obj.to_wbem_uri() + wbem_uri_keys = wbem_uri[wbem_uri.find('.'):] + wbem_uri_keys = wbem_uri_keys[1:] + return wbem_uri_keys - The returned list has the key properties first, followed by the non-key - properties. Each group is sorted case insensitively. + assert isinstance(obj, CIMInstanceName) + # clear the host and namespace + myobj = obj.copy() + myobj.host = None + myobj.namespace = None + wbem_uri_keys = get_wbemurikeys(myobj) - The key properties are determined from the instance paths, if present. - The function tolerates it if only some of the instances have a path, - and if instances of subclasses have additional keys. - """ + # Too long for width. Fold the keys on multiple lines + if len(wbem_uri_keys) > max_width: + wbem_uri_keys = '' + line_len = 0 + for key, value in myobj.keybindings.items(): + one_key_obj = get_wbemurikeys((CIMInstanceName('x', {key: value}))) + if wbem_uri_keys: + if line_len + len(one_key_obj) > max_width: + wbem_uri_keys += '\n{}'.format(one_key_obj) + line_len = 0 + else: + wbem_uri_keys += ',{}'.format(one_key_obj) + line_len += len(one_key_obj) + 1 - all_props = odicti() # key: org prop name, value: lower cased prop name - key_props = odicti() # key: org prop name, value: lower cased prop name - for inst in insts: - inst_props = inst.keys() - for pn in inst_props: - all_props[pn] = pn.lower() - if inst.path: - key_prop_names = inst.path.keys() - for pn in inst_props: - if pn in key_prop_names: - key_props[pn] = pn.lower() - - nonkey_props = odicti() # key: org prop name, value: lower cased prop name - for pn in all_props: - if pn not in key_props: - nonkey_props[pn] = all_props[pn] - - key_prop_list = sorted(key_props.keys(), key=lambda p: p.lower()) - nonkey_prop_list = sorted(nonkey_props.keys(), key=lambda p: p.lower()) - key_prop_list.extend(nonkey_prop_list) - return key_prop_list - - -def _print_objects_as_table(objects, output_format, context=None): - """ - Call the method for each type of object to print that object type - information as a table. + else: # must put on first line even if too long + wbem_uri_keys += one_key_obj + line_len = len(one_key_obj) + 1 - Output format is retrieved from context. - """ - if USE_TERMINAL_WIDTH: - table_width = click.get_terminal_size()[0] - else: - table_width = DEFAULT_TABLE_WIDTH - - if objects: - if isinstance(objects[0], CIMInstance): - _print_instances_as_table(objects, table_width, output_format, - context=context) - elif isinstance(objects[0], CIMClass): - _print_classes_as_table(objects, table_width, output_format) - elif isinstance(objects[0], CIMQualifierDeclaration): - _print_qual_decls_as_table(objects, table_width, output_format) - elif isinstance(objects[0], (CIMClassName, CIMInstanceName, - six.string_types)): - _print_paths_as_table(objects, table_width, output_format) - else: - raise click.ClickException("Cannot print {} as table" - .format(type(objects[0]))) + return wbem_uri_keys def hide_empty_columns(headers, rows): @@ -1554,6 +1062,14 @@ def hide_empty_columns(headers, rows): 1. All entries for the column in all rows are None or "" if type string. 2. All entries for the column in all rows are None if number. + Parameters: + headers (list of :term:`string`) + The strings that represent the column titles of an array of rows. + + rows (list of list of TBD): + The rows of a table where each row is a list of the items that + represent the columns of the row. + Returns new rows and headers """ def column_is_empty(rows, column): @@ -1577,7 +1093,12 @@ def column_is_empty(rows, column): format(row, headers) for column in range(len(headers) - 1, -1, -1): if column_is_empty(rows, column): - del headers[column] + if isinstance(headers, tuple): + headersl = list(headers) + del headersl[column] + headers = tuple(headersl) + else: + del headers[column] for row in rows: del row[column] @@ -1585,7 +1106,7 @@ def column_is_empty(rows, column): def format_table(rows, headers, title=None, table_format='simple', - sort_columns=None): + sort_columns=None, hide_empty_cols=None): """ General print table function. Prints a list of lists in a table format where each inner list is a row. @@ -1619,12 +1140,19 @@ def format_table(rows, headers, title=None, table_format='simple', right). Note that entries in each row of the columns to be sorted must be of the same type (int, str, etc.) to be sortable. + hide_empty_cols (:class:`py:bool`): + If this flag is True any columns that are completely blank are + hiddend and the column header is removed from the headers. + Uses the function hide_empty_columns + Returns: :term:`string`: Returns the formatted table as a string Raises: click.ClickException if invalid table format string """ + if hide_empty_cols: + headers, rows = hide_empty_columns(headers, rows) if sort_columns is not None: if isinstance(sort_columns, int): rows = sorted(rows, key=itemgetter(sort_columns)) diff --git a/pywbemtools/pywbemcli/_display_cimobjects.py b/pywbemtools/pywbemcli/_display_cimobjects.py new file mode 100644 index 000000000..76363326e --- /dev/null +++ b/pywbemtools/pywbemcli/_display_cimobjects.py @@ -0,0 +1,536 @@ +# (C) Copyright 2020 IBM Corp. +# (C) Copyright 2020 Inova Development Inc. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Common function to display cim objects in multiple formats. +display_cimobjects() is the function that should be used for all CIM +object display in pywbemcli. +""" + +from __future__ import absolute_import, print_function, unicode_literals + +import re + +from pydicti import odicti +import six +import click + +from pywbem import CIMInstanceName, CIMInstance, CIMClass, \ + CIMQualifierDeclaration, CIMClassName, ValueMapping + +from ._common import format_table, fold_strings, DEFAULT_MAX_CELL_WIDTH, \ + output_format_is_table, sort_cimobjects, format_keys + +from .config import USE_TERMINAL_WIDTH, DEFAULT_TABLE_WIDTH + +from ._cimvalueformatter import cimvalue_to_fmtd_string + +INT_TYPE_PATTERN = re.compile(r'^[su]int(8|16|32|64)$') + +#################################################################### +# +# Display of CIM objects. +# +#################################################################### + + +def display_cim_objects(context, cim_objects, output_format, summary=False, + sort=False): + """ + Display CIM objects in form determined by input parameters. + + Input is either a list of cim objects or a single object. It may be + any of the CIM types. This is used to display: + + * CIMClass + + * CIMClassName: + + * CIMInstance + + * CIMInstanceName + + * CIMQualifierDeclaration + + * Or list of the above + + This function may override output type choice in cases where the output + choice is not available for the object type. Thus, for example, + mof output makes no sense for class names. In that case, the output is + the str of the type. + + Parameters: + + context (:class:`ContextObj`): + Click context contained in ContextObj object. + + objects (iterable of :class:`~pywbem.CIMInstance`, + :class:`~pywbem.CIMInstanceName`, :class:`~pywbem.CIMClass`, + :class:`~pywbem.CIMClassName`, + or :class:`~pywbem.CIMQualifierDeclaration`): + Iterable of zero or more CIM objects to be displayed. + + output_format (:term:`string`): + String defining the preferred output format. Must not be None since + the correct output_format must have been selected before this call. + Note that the output formats allowed may depend on a) whether + summary is True, b)the specific type because we do not have a table + output format for CIMClass. + + summary (:class:`py:bool`): + Boolean that defines whether the data in objects should be displayed + or just a summary of the objects (ex. count of number of objects). + """ + # Note: In the docstring above, the line for parameter 'objects' was way too + # long. Since we are not putting it into docmentation, we folded it. + + context.spinner_stop() + + if summary: + display_cim_objects_summary(context, cim_objects, output_format) + return + + if not cim_objects and context.verbose: + click.echo("No objects returned") + return + + if sort: + cim_objects = sort_cimobjects(cim_objects) + + # default when displaying cim objects is mof + assert output_format + + if isinstance(cim_objects, (list, tuple)): + # Table format output is processed as a group + if output_format_is_table(output_format): + _display_objects_as_table(cim_objects, output_format, + context=context) + else: + # Call to display each object + for obj in cim_objects: + display_cim_objects(context, obj, output_format=output_format) + return + + # Display a single item. + object_ = cim_objects + # This allows passing single objects to the table formatter (i.e. not lists) + if output_format_is_table(output_format): + _display_objects_as_table([object_], output_format, context=context) + elif output_format == 'mof': + try: + click.echo(object_.tomof()) + except AttributeError: + # insert NL between instance names for readability + if isinstance(object_, CIMInstanceName): + click.echo("") + click.echo(object_) + elif isinstance(object_, (CIMClassName, six.string_types)): + click.echo(object_) + else: + raise click.ClickException('output_format {} invalid for {} ' + .format(output_format, + type(object_))) + elif output_format == 'xml': + try: + click.echo(object_.tocimxmlstr(indent=4)) + except AttributeError: + # no tocimxmlstr functionality + raise click.ClickException('Output Format {} not supported. ' + 'Default to\n{!r}' + .format(output_format, object_)) + elif output_format == 'repr': + try: + click.echo(repr(object_)) + except AttributeError: + raise click.ClickException('"repr" display of {!r} failed' + .format(object_)) + + elif output_format == 'txt': + try: + click.echo(object_) + except AttributeError: + raise click.ClickException('"txt" display of {!r} failed' + .format(object_)) + # elif output_format == 'tree': + # raise click.ClickException('Tree output format not allowed') + else: + raise click.ClickException('Invalid output format {}' + .format(output_format)) + + +def _display_objects_as_table(objects, output_format, context=None): + """ + Call the method for each type of object to print that object type + information as a table. + + Output format is retrieved from context. + """ + if USE_TERMINAL_WIDTH: + table_width = click.get_terminal_size()[0] + else: + table_width = DEFAULT_TABLE_WIDTH + + if objects: + if isinstance(objects[0], CIMInstance): + _display_instances_as_table(objects, table_width, output_format, + context=context) + elif isinstance(objects[0], CIMClass): + _display_classes_as_table(objects, table_width, output_format) + elif isinstance(objects[0], CIMQualifierDeclaration): + _display_qual_decls_as_table(objects, table_width, output_format) + elif isinstance(objects[0], (CIMClassName, CIMInstanceName, + six.string_types)): + _display_paths_as_table(objects, table_width, output_format) + else: + raise click.ClickException("Cannot print {} as table" + .format(type(objects[0]))) + + +############################################################################ +# +# Support methods for displaying CIM objects. This includes multiple +# output formats (ie.e MOF, TABLE, TEXT) +# +############################################################################ + + +def get_cimtype(objects): + """ + Get the cim_type for any returned cim object. Normally this is the + name of the class name except that the classname return from + getclass and enumerate class is just unicode string + """ + # associators and references return tuple + if isinstance(objects, list): + test_object = objects[0] + elif objects: + test_object = object + else: + cim_type = 'unknown' + return None + + if isinstance(test_object, tuple): + # associator or reference class level return is tuple + cim_type = test_object[0].__class__.__name__ + else: + cim_type = test_object.__class__.__name__ + + # account for fact the enumerate class name operation returns uniicode. + if isinstance(test_object, six.string_types): + cim_type = 'CIMClassName' + return cim_type + + +def display_cim_objects_summary(context, objects, output_format): + """ + Display a summary of the objects received. This displays the + count of objects. + """ + context.spinner_stop() + + if objects: + cim_type = get_cimtype(objects) + + if output_format_is_table(output_format): + rows = [[len(objects), cim_type]] + click.echo(format_table(rows, ['Count', 'CIM Type'], + title='Summary of {} returned' + .format(cim_type), + table_format=output_format)) + return + click.echo('{} {}(s) returned'.format(len(objects), cim_type)) + + else: + click.echo('0 objects returned') + + +def _display_classes_as_table(classes, table_width, table_format): + """ + TODO: Future extend to display classes as a table, showing the + properties for each class. This will display the properties that exist in + subclasses. The temp output + so we could create the function is to just output as mof + """ + # pylint: disable=unused-argument + + for class_ in classes: + click.echo(class_.tomof()) + + +def _display_paths_as_table(objects, table_width, table_format): + # pylint: disable=unused-argument + """ + Display paths as a table. This include CIMInstanceName, ClassPath, + and unicode (the return type for enumerateClasses). + """ + title = None + if objects: + if isinstance(objects[0], six.string_types): + title = 'Classnames:' + headers = ['Class Name'] + rows = [[obj] for obj in objects] + elif isinstance(objects[0], CIMClassName): + title = 'Classnames' + headers = ('host', 'namespace', 'class') + rows = [[obj.host, obj.namespace, obj.classname] for obj in objects] + elif isinstance(objects[0], CIMInstanceName): + title = 'InstanceNames: {}'.format(objects[0].classname) + host_hdr = 'host' + ns_hdr = 'namespace' + class_hdr = 'class' + host_hdr_len = len(host_hdr) + 4 + ns_hdr_len = len(ns_hdr) + 3 + class_hdr_len = len(class_hdr) + 3 + headers = (host_hdr, ns_hdr, class_hdr, 'keysbindings') + + host_lens = [len(obj.host) for obj in objects if obj.host] + host_max = max(host_lens) if host_lens else host_hdr_len + ns_lens = [len(obj.namespace) for obj in objects if obj.namespace] + ns_max = max(ns_lens) if ns_lens else ns_hdr_len + class_lens = [len(obj.classname) for obj in objects] + class_max = max(class_lens) if class_lens else class_hdr_len + + max_key_len = (table_width) - (host_max + ns_max + class_max + 3) + rows = [[obj.host, obj.namespace, obj.classname, + format_keys(obj, max_key_len)] for obj in objects] + else: + raise click.ClickException("{0} invalid type ({1})for path display". + format(objects[0], type(objects[0]))) + + click.echo(format_table(rows, headers, title=title, + table_format=table_format)) + + +def _display_qual_decls_as_table(qual_decls, table_width, table_format): + """ + Display the elements of qualifier declarations as a table with a + row for each qualifier declaration and a column for each of the attributes + of the qualifier declaration (name, type, Value, Array, Scopes, Flavors. + + The function displays all of the qualifier declarations in the + """ + rows = [] + headers = ['Name', 'Type', 'Value', 'Array', 'Scopes', 'Flavors'] + max_column_width = int(table_width / len(headers)) - 4 + for q in qual_decls: + scopes = '\n'.join([key for key in q.scopes if q.scopes[key]]) + flavors = [] + flavors.append('EnableOverride' if q.overridable else 'DisableOverride') + flavors.append('ToSubclass' if q.tosubclass else 'Restricted') + if q.translatable: + flavors.append('Translatable') + if sum([len(i) for i in flavors]) >= max_column_width: + sep = "\n" + else: + sep = ", " + flavors = sep.join(flavors) + + row = [q.name, q.type, q.value, q.is_array, scopes, flavors] + rows.append(row) + + click.echo(format_table(rows, headers, title='Qualifier Declarations', + table_format=table_format)) + + +def _format_instances_as_rows(insts, max_cell_width=DEFAULT_MAX_CELL_WIDTH, + include_classes=False, context=None, + prop_names=None): + """ + Format the list of instances properties into as a list of the property + values for each instance( a row of the table) gathered into a list of + the rows. + + The prop_names parameter is the list of (originally cased) property names + to be output, in the desired output order. It could be determined from + the instances, but since it is determined already by the caller, it + is passed in as an optimization. For test convenience, None is permitted + and causes the properties to again be determined from the instances. + + Include_classes for each instance if True. Sets the classname as the first + column. + + max_width if not None folds col entries longer than the defined + max_cell_width. If max_width is None, the data length is ignored. + + The property values are formatted similar to MOF output. Properties that + have a ValueMap qualifier (effectively, in the creation class of the + instance) are shown with both the actual property value and the mapped + value in parenthesis. + + NOTE: This is a separate function to allow testing of the table formatting + independently of print output. + + Returns: + list of strings where each string is a row in the table and each + item in a row is a cell entry + """ + # Avoid crash deeper in code if max_cell_width is None. + if max_cell_width is None: + max_cell_width = DEFAULT_MAX_CELL_WIDTH + lines = [] + + if prop_names is None: + prop_names = sorted_prop_names(insts) + + # Cache of ValueMapping objects for integer-typed properties. + # Key: classname.propertyname, both in lower case. + # A value of None indicates the property does not have a value mapping. + valuemappings = {} + + for inst in insts: + if not isinstance(inst, CIMInstance): + raise ValueError('Only accepts CIMInstance; not type {}' + .format(type(inst))) + + # Insert classname as first col if flag set + line = [inst.classname] if include_classes else [] + + # get value for each property in this object + for name in prop_names: + + # Account for possible instances without all properties + # Outputs empty string. Note that instance with no value + # results in same output as not instance name. + if name not in inst.properties: + val_str = '' + else: + value = inst.get(name) + p = inst.properties[name] + + # Cache value mappings for integer-typed properties + if INT_TYPE_PATTERN.match(p.type) and context: + vm_key = '{}.{}'.format( + inst.classname.lower(), name.lower()) + try: + valuemapping = valuemappings[vm_key] + except KeyError: + try: + valuemapping = ValueMapping.for_property( + context.conn, + context.conn.default_namespace, + inst.classname, + name) + except ValueError: + # Property does not have a value mapping. + valuemapping = None + valuemappings[vm_key] = valuemapping + else: + valuemapping = None + + if value is None: + val_str = u'' + else: + val_str, _ = cimvalue_to_fmtd_string( + p.value, p.type, indent=0, maxline=max_cell_width, + line_pos=0, end_space=0, avoid_splits=False, + valuemapping=valuemapping) + + line.append(val_str) + lines.append(line) + + return lines + + +def _display_instances_as_table(insts, table_width, table_format, + include_classes=False, context=None): + """ + Print the properties of the instances defined in insts as a table where + each row is an instance and each column is a property value. + + All properties in the instance are included. + + The header line consists of the property names. + + The property values are formatted similar to MOF output. Properties that + have a ValueMap qualifier (effectively, in the creation class of the + instance) are shown with both the actual property value and the mapped + value in parenthesis. + """ + + if table_width is None: + table_width = DEFAULT_TABLE_WIDTH + + for inst in insts: + assert isinstance(inst, CIMInstance) + + prop_names = sorted_prop_names(insts) + + # Try to estimate max cell width from number of cols + # This allows folding long data. However it is incomplete in + # that we do not fold the property name. Further, the actual output + # width of a column involves the tabulate outputter, output_format + # so this is not deterministic. + if prop_names: + num_cols = len(prop_names) + max_cell_width = int(table_width / num_cols) - 2 + else: + max_cell_width = table_width + + header_line = [] + if include_classes: + header_line.append("classname") + header_line.extend(prop_names) + + # Fold long property names + new_header_line = [] + for header in header_line: + if len(header) > max_cell_width: + new_header_line.append(fold_strings(header, max_cell_width)) + else: + new_header_line.append(header) + + rows = _format_instances_as_rows(insts, max_cell_width=max_cell_width, + include_classes=include_classes, + context=context, prop_names=prop_names) + + title = 'Instances: {}'.format(insts[0].classname) + click.echo(format_table(rows, new_header_line, title=title, + table_format=table_format)) + + +def sorted_prop_names(insts): + """ + Return the list of (originally cased) property names that is the superset + of all properties in the input instances. + + The returned list has the key properties first, followed by the non-key + properties. Each group is sorted case insensitively. + + The key properties are determined from the instance paths, if present. + The function tolerates it if only some of the instances have a path, + and if instances of subclasses have additional keys. + """ + + all_props = odicti() # key: org prop name, value: lower cased prop name + key_props = odicti() # key: org prop name, value: lower cased prop name + for inst in insts: + inst_props = inst.keys() + for pn in inst_props: + all_props[pn] = pn.lower() + if inst.path: + key_prop_names = inst.path.keys() + for pn in inst_props: + if pn in key_prop_names: + key_props[pn] = pn.lower() + + nonkey_props = odicti() # key: org prop name, value: lower cased prop name + for pn in all_props: + if pn not in key_props: + nonkey_props[pn] = all_props[pn] + + key_prop_list = sorted(key_props.keys(), key=lambda p: p.lower()) + nonkey_prop_list = sorted(nonkey_props.keys(), key=lambda p: p.lower()) + key_prop_list.extend(nonkey_prop_list) + return key_prop_list diff --git a/tests/unit/test_common.py b/tests/unit/test_common.py index ca121b519..2477bece6 100755 --- a/tests/unit/test_common.py +++ b/tests/unit/test_common.py @@ -22,39 +22,29 @@ from __future__ import absolute_import, print_function import sys -from datetime import datetime import unittest from packaging.version import parse as parse_version import click from mock import patch import pytest -try: - from collections import OrderedDict -except ImportError: - from ordereddict import OrderedDict # pylint: disable=import-error - from pywbem import CIMClass, CIMProperty, CIMQualifier, CIMInstance, \ - CIMQualifierDeclaration, CIMInstanceName, Uint8, Uint32, Uint64, Sint32, \ - CIMDateTime, CIMClassName, __version__ + CIMQualifierDeclaration, CIMInstanceName, Uint8, Uint32, \ + CIMClassName, __version__ from tests.unit.pytest_extensions import simplified_test_function from pywbemtools.pywbemcli._common import parse_wbemuri_str, \ filter_namelist, parse_kv_pair, split_array_value, sort_cimobjects, \ create_ciminstance, compare_instances, resolve_propertylist, \ - _format_instances_as_rows, _print_instances_as_table, is_classname, \ - pick_one_from_list, pick_multiple_from_list, hide_empty_columns, \ - verify_operation, split_str_w_esc, format_keys, create_ciminstancename, \ - shorten_path_str, validate_output_format, fold_strings + is_classname, pick_one_from_list, pick_multiple_from_list, \ + hide_empty_columns, verify_operation, split_str_w_esc, format_keys, \ + create_ciminstancename, shorten_path_str, \ + validate_output_format, fold_strings from pywbemtools.pywbemcli._context_obj import ContextObj # from tests.unit.utils import assert_lines -DATETIME1_DT = datetime(2014, 9, 22, 10, 49, 20, 524789) -DATETIME1_OBJ = CIMDateTime(DATETIME1_DT) -DATETIME1_STR = '"20140922104920.524789+000"' - OK = True # mark tests OK when they execute correctly RUN = True # Mark OK = False and current test case being created RUN FAIL = False # Any test currently FAILING or not tested yet @@ -720,12 +710,21 @@ def test_split_str(testcase, input_str, delimiter, exp_rtn): dict(options=[u'ZERO', u'ONE', u'TWO'], choices=['1'], exp_rtn=u'ONE'), None, None, OK), + ('Verify returns correct choice, in this case TWO', + dict(options=[u'ZERO', u'ONE', u'TWO'], choices=['2'], exp_rtn=u'TWO'), + None, None, OK), + ('Verify returns correct choice, in this case ONE after one error', dict(options=[u'ZERO', u'ONE', u'TWO'], choices=['9', '1'], exp_rtn=u'ONE'), None, None, OK), - ('Verify returns correct choice, in this case ONE after multipleerror', + ('Verify returns correct choice, in this case ONE after one error', + dict(options=[u'ZERO', u'ONE', u'TWO'], choices=['3', '2'], + exp_rtn=u'TWO'), + None, None, OK), + + ('Verify returns correct choice, in this case ONE after multiple inputs', dict(options=[u'ZERO', u'ONE', u'TWO'], choices=['9', '-1', 'a', '2'], exp_rtn=u'TWO'), None, None, OK), @@ -754,8 +753,8 @@ def test_pick_one_from_list(testcase, options, choices, exp_rtn): None, None) act_rtn = pick_one_from_list(context, options, title) else: - # setup mock for this test - # mock the prompt with choices from the testcases as prompt response + # Setup mock for this test. + # Mock the prompt with choices from the testcases as prompt response mock_prompt_funct = 'pywbemtools.pywbemcli.click.prompt' # side_effect returns next item in choices for each prompt call with patch(mock_prompt_funct, side_effect=choices) as mock_prompt: @@ -2368,514 +2367,6 @@ def test_fold_strings(testcase, input_str, max_width, brk_long_wds, brk_hyphen, assert act_rtn == exp_rtn -# NOTE: The following methods are testcase parameters. They define instances -# used in TESTCASES_FMT_INSTANCE_AS_ROWS -def simple_instance(pvalue=None): - """ - Build a simple instance to test and return that instance. The properties - in the instance are sorted by (lower cased) property name. - - If the parameter pvalue is provided, it must be a scalar value and an - instance with a single property with that value is returned. - """ - if pvalue: - properties = [CIMProperty("P", pvalue)] - else: - properties = [ - CIMProperty("Pbf", value=False), - CIMProperty("Pbt", value=True), - CIMProperty("Pdt", DATETIME1_OBJ), - CIMProperty("Pint32", Uint32(99)), - CIMProperty("Pint64", Uint64(9999)), - CIMProperty("Pstr1", u"Test String"), - ] - inst = CIMInstance("CIM_Foo", properties) - return inst - - -def simple_instance_unsorted(pvalue=None): - """ - Build a simple instance to test and return that instance. The properties - in the instance are not sorted by (lower cased) property name, but - the property order when sorted is the same as in the instance returned by - simple_instance(). - - If the parameter pvalue is provided, it must be a scalar value and an - instance with a single property with that value is returned. - """ - if pvalue: - properties = [CIMProperty("P", pvalue)] - else: - properties = [ - CIMProperty("Pbt", value=True), - CIMProperty("Pbf", value=False), # out of order - CIMProperty("pdt", DATETIME1_OBJ), # lower cased - CIMProperty("PInt64", Uint64(9999)), # out of order when case ins. - CIMProperty("Pint32", Uint32(99)), - CIMProperty("Pstr1", u"Test String"), - ] - inst = CIMInstance("CIM_Foo", properties) - return inst - - -def simple_instance2(pvalue=None): - """ - Build a simple instance to test and return that instance. The properties - in the instance are sorted by (lower cased) property name. - - If the parameter pvalue is provided, it must be a scalar value and an - instance with a single property with that value is returned. - """ - if pvalue: - properties = [CIMProperty("P", pvalue)] - else: - properties = [ - CIMProperty("Pbf", value=False), - CIMProperty("Pbt", value=True), - CIMProperty("Pdt", DATETIME1_OBJ), - CIMProperty("Pint64", Uint64(9999)), - CIMProperty("Psint32", Sint32(-2147483648)), - CIMProperty("Pstr1", u"Test String"), - CIMProperty("Puint32", Uint32(4294967295)), - ] - inst = CIMInstance("CIM_Foo", properties) - return inst - - -def string_instance(tst_str): - """ - Build a CIM instance with a single property. - """ - properties = [CIMProperty("Pstr1", tst_str)] - inst = CIMInstance("CIM_Foo", properties) - return inst - - -# Testcases for _format_instances_as_rows() - - # Each list item is a testcase tuple with these items: - # * desc: Short testcase description. - # * kwargs: Keyword arguments for the test function: - # * args: Positional args for _format_instances_as_rows(). - # * kwargs: Keyword args for _format_instances_as_rows(). - # * exp_rtn: Expected return value of _format_instances_as_rows(). - # * exp_exc_types: Expected exception type(s), or None. - # * exp_rtn: Expected warning type(s), or None. - # * condition: Boolean condition for testcase to run, or 'pdb' for debugger - -TESTCASES_FORMAT_INSTANCES_AS_ROWS = [ - ( - "Verify simple instance to table", - dict( - args=([simple_instance()], None), - kwargs=dict(), - exp_rtn=[ - ["false", "true", DATETIME1_STR, "99", "9999", - u'"Test String"']], - ), - None, None, True, ), - - ( - "Verify simple instance to table with col limit", - dict( - args=([simple_instance()], 30), - kwargs=dict(), - exp_rtn=[ - ["false", "true", DATETIME1_STR, "99", "9999", - u'"Test String"']], - ), - None, None, True, ), - - ( - "Verify simple instance to table, unsorted", - dict( - args=([simple_instance_unsorted()], None), - kwargs=dict(), - exp_rtn=[ - ["false", "true", DATETIME1_STR, "99", "9999", - u'"Test String"']], - ), - None, None, True, ), - - ( - "Verify instance with 2 keys and 2 non-keys, unsorted", - dict( - args=(), - kwargs=dict( - insts=[ - CIMInstance( - "CIM_Foo", - properties=[ - CIMProperty("P2", value="V2"), - CIMProperty("p1", value="V1"), - CIMProperty("Q2", value="K2"), - CIMProperty("q1", value="K1"), - ], - path=CIMInstanceName( - "CIM_Foo", - keybindings=[ - CIMProperty("Q2", value="K2"), - CIMProperty("q1", value="K1"), - ] - ), - ), - ], - ), - exp_rtn=[ - ['"K1"', '"K2"', '"V1"', '"V2"'], - ], - ), - None, None, True, ), - - ( - "Verify 2 instances with different sets of properties", - dict( - args=(), - kwargs=dict( - insts=[ - CIMInstance( - "CIM_Foo", - properties=[ - CIMProperty("P2", value="VP2a"), - CIMProperty("p1", value="VP1a"), - CIMProperty("P3", value="VP3a"), - ], - ), - CIMInstance( - "CIM_FooSub", - properties=[ - CIMProperty("P2", value="VP2b"), - CIMProperty("p1", value="VP1b"), - CIMProperty("N1", value="VN1b"), - ], - ), - ], - ), - exp_rtn=[ - ['', '"VP1a"', '"VP2a"', '"VP3a"'], - ['"VN1b"', '"VP1b"', '"VP2b"', ''], - ], - ), - None, None, True, ), - - ( - "Verify 2 instances where second one has path", - dict( - args=(), - kwargs=dict( - insts=[ - CIMInstance( - "CIM_Foo", - properties=[ - CIMProperty("P2", value="VP2a"), - CIMProperty("p1", value="VP1a"), - ], - ), - CIMInstance( - "CIM_FooSub", - properties=[ - CIMProperty("P2", value="VP2b"), - CIMProperty("p1", value="VP1b"), - CIMProperty("Q2", value="K2b"), - CIMProperty("q1", value="K1b"), - ], - path=CIMInstanceName( - "CIM_Foo", - keybindings=[ - CIMProperty("q2", value="K2b"), - CIMProperty("Q1", value="K1b"), - ] - ), - ), - ], - ), - exp_rtn=[ - ['', '', '"VP1a"', '"VP2a"'], - ['"K1b"', '"K2b"', '"VP1b"', '"VP2b"'], - ], - ), - None, None, True, ), - - ( - "Verify simple instance with one string all components overflow line", - dict( - args=([simple_instance(pvalue="A B C D")], 4), - kwargs=dict(), - exp_rtn=[ - ['"A "\n"B "\n"C "\n"D"']], - ), - None, None, True, ), - - ( - "Verify simple instance with one string all components overflow line", - dict( - args=([simple_instance(pvalue="ABCD")], 4), - kwargs=dict(), - exp_rtn=[ - ['\n"AB"\n"CD"']], - ), - None, None, True, ), - - ( - "Verify simple instance with one string overflows line", - dict( - args=([simple_instance(pvalue="A B C D")], 8), - kwargs=dict(), - exp_rtn=[ - ['"A B C "\n"D"']], - ), - None, None, True, ), - - ( - "Verify simple instance withone unit32 max val", - dict( - args=([simple_instance(pvalue=Uint32(4294967295))], 8), - kwargs=dict(), - exp_rtn=[ - ['4294967295']], - ), - None, None, True, ), - - - ( - "Verify simple instance with one string fits on line", - dict( - args=([simple_instance(pvalue="A B C D")], 12), - kwargs=dict(), - exp_rtn=[ - ['"A B C D"']], - ), - None, None, True, ), - - ( - "Verify datetime property", - dict( - args=([simple_instance(pvalue=DATETIME1_OBJ)], 20), - kwargs=dict(), - exp_rtn=[ - ['\n"20140922104920.524"\n"789+000"']], - ), - None, None, True, ), - - ( - "Verify datetime property", - dict( - args=([simple_instance(pvalue=DATETIME1_OBJ)], 30), - kwargs=dict(), - exp_rtn=[ - ['"20140922104920.524789+000"']], - ), - None, None, True, ), - - ( - "Verify integer property where len too small", - dict( - args=([simple_instance(pvalue=Uint32(999999))], 4), - kwargs=dict(), - exp_rtn=[['999999']], - ), - None, None, True, ), - - ( - "Verify char16 property", - dict( - args=([CIMInstance('P', [CIMProperty('P', - type='char16', - value='f')])], 4), - kwargs=dict(), - exp_rtn=[[u"'f'"]], - ), - None, None, True, ), - - ( - "Verify properties with no value", - dict( - args=([CIMInstance('P', [CIMProperty('P', value=None, - type='char16'), - CIMProperty('Q', value=None, - type='uint32'), - CIMProperty('R', value=None, - type='string'), ])], 4), - kwargs=dict(), - exp_rtn=[[u'', u'', u'']], - ), - None, None, True, ), - - ( - "Verify format of instance with reference property as row entry", - dict( - args=([CIMInstance("TST_REFPROP", - [CIMProperty( - 'P', - type='reference', - reference_class="blah", - value=CIMInstanceName( - "REF_CLN", - keybindings=OrderedDict(k1='v1')))])], - 30), - kwargs=dict(), - exp_rtn=[ - [u'"/:REF_CLN.k1=\\"v1\\""']], - ), - None, None, True, ), -] - - -@pytest.mark.parametrize( - "desc, kwargs, exp_exc_types, exp_warn_types, condition", - TESTCASES_FORMAT_INSTANCES_AS_ROWS) -@simplified_test_function -def test_format_instances_as_rows(testcase, args, kwargs, exp_rtn): - """ - Test the output of the common _format_instances_as_rows() function - """ - - # The code to be tested - act_rtn = _format_instances_as_rows(*args, **kwargs) - - # Ensure that exceptions raised in the remainder of this function - # are not mistaken as expected exceptions - assert testcase.exp_exc_types is None - # result is list of lists. we want to test each item in inner list - - assert len(act_rtn) == len(exp_rtn), \ - "Unexpected number of lines in test desc: {}:\n" \ - "Expected line cnt={}:\n" \ - "{}\n\n" \ - "Actual line cnt={}:\n" \ - "{}\n". \ - format(testcase.desc, len(act_rtn), '\n'.join(act_rtn), - len(exp_rtn), '\n'.join(exp_rtn)) - - assert exp_rtn == act_rtn, \ - "Unequal values for test desc: {}:\n" \ - "Expected = {}:\n" \ - "Actual = {}:\n". \ - format(testcase.desc, exp_rtn, act_rtn) - - -# Testcases for _print_instances_as_table() - - # Each list item is a testcase tuple with these items: - # * desc: Short testcase description. - # * kwargs: Keyword arguments for the test function: - # * args: Positional args for _print_instances_as_table(). - # * kwargs: Keyword args for _print_instances_as_table(). - # * exp_stdout: Expected output on stdout. - # * exp_exc_types: Expected exception type(s), or None. - # * exp_warn_types: Expected warning type(s), or None. - # * condition: Boolean condition for testcase to run, or 'pdb' for debugger - -TESTCASES_PRINT_INSTANCES_AS_TABLE = [ - ( - "Verify print of simple instance to table", - dict( - args=([simple_instance()], None, 'simple'), - kwargs=dict(), - exp_stdout="""\ -Instances: CIM_Foo -Pbf Pbt Pdt Pint32 Pint64 Pstr1 ------ ----- ----------------------- -------- -------- ------------- -false true "20140922104920.524789" 99 9999 "Test String" - "+000" -""", - ), - None, None, not CLICK_ISSUE_1590 - ), - ( - "Verify print of simple instance to table with col limit", - dict( - args=([simple_instance2()], 80, 'simple'), - kwargs=dict(), - exp_stdout="""\ -Instances: CIM_Foo -Pbf Pbt Pdt Pint64 Psint32 Pstr1 Puint32 ------ ----- --------- -------- ----------- -------- ---------- -false true "2014092" 9999 -2147483648 "Test " 4294967295 - "2104920" "String" - ".524789" - "+000" -""", - ), - None, None, not CLICK_ISSUE_1590 - ), - ( - "Verify print of instance with reference property", - dict( - args=([CIMInstance("CIM_Foo", - [CIMProperty( - 'P', - type='reference', - reference_class="blah", - value=CIMInstanceName( - "REF_CLN", - keybindings=OrderedDict(k1='v1', - k2=32)))])], - 80, 'simple'), - kwargs=dict(), - exp_stdout="""\ -Instances: CIM_Foo -P ---------------------------- -"/:REF_CLN.k1=\\"v1\\",k2=32" -""", - ), - None, None, not CLICK_ISSUE_1590 and PYWBEM_1_0_0B1 - ), - - ( - "Verify fails if not instances", - dict( - args=([CIMClass("CIM_Foo")], - 80, 'simple'), - kwargs=dict(), - exp_stdout="", - ), - ValueError, None, not CLICK_ISSUE_1590 and PYWBEM_1_0_0B1 - ), -] - - -@pytest.mark.parametrize( - "desc, kwargs, exp_exc_types, exp_warn_types, condition", - TESTCASES_PRINT_INSTANCES_AS_TABLE) -def test_print_instances_as_table( - desc, kwargs, exp_exc_types, exp_warn_types, condition, capsys): - """ - Test the output of the print_insts_as_table function. This primarily - tests for overall format and the ability of the function to output to - stdout. The previous test tests the row formatting and handling of - multiple instances. - """ - if not condition: - pytest.skip("Testcase condition not satisfied") - - # This logic only supports successful testcases without warnings - # assert exp_exc_types is None - assert exp_warn_types is None - - args = kwargs['args'] - kwargs_ = kwargs['kwargs'] - exp_stdout = kwargs['exp_stdout'] - - # The code to be tested - if not exp_exc_types: - _print_instances_as_table(*args, **kwargs_) - - stdout, _ = capsys.readouterr() - assert exp_stdout == stdout, \ - "Unexpected output in test case: {}\n" \ - "Actual:\n" \ - "{}\n" \ - "Expected:\n" \ - "{}\n" \ - "End\n".format(desc, stdout, exp_stdout) - - else: - with pytest.raises(exp_exc_types): - _print_instances_as_table(*args, **kwargs_) - - # TODO Test compare and failure in compare_obj and with errors. diff --git a/tests/unit/test_display_cimobjects.py b/tests/unit/test_display_cimobjects.py new file mode 100644 index 000000000..8c40eeede --- /dev/null +++ b/tests/unit/test_display_cimobjects.py @@ -0,0 +1,554 @@ +# -*- coding: utf-8 -*- +# (C) Copyright 2020 IBM Corp. +# (C) Copyright 2020 Inova Development Inc. +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Tests for _common.py functions. +""" + +from __future__ import absolute_import, print_function + +import sys +from datetime import datetime +from packaging.version import parse as parse_version +import pytest + +try: + from collections import OrderedDict +except ImportError: + from ordereddict import OrderedDict # pylint: disable=import-error + +from pywbem import CIMProperty, CIMInstance, CIMInstanceName, Uint32, Uint64, \ + Sint32, CIMDateTime, __version__ + +from tests.unit.pytest_extensions import simplified_test_function + +from pywbemtools.pywbemcli._display_cimobjects import \ + _format_instances_as_rows, _display_instances_as_table + +OK = True # mark tests OK when they execute correctly +RUN = True # Mark OK = False and current test case being created RUN +FAIL = False # Any test currently FAILING or not tested yet +SKIP = False # mark tests that are to be skipped. + +DATETIME1_DT = datetime(2014, 9, 22, 10, 49, 20, 524789) +DATETIME1_OBJ = CIMDateTime(DATETIME1_DT) +DATETIME1_STR = '"20140922104920.524789+000"' + +# Click (as of 7.1.2) raises UnsupportedOperation in click.echo() when +# the pytest capsys fixture is used. That happens only on Windows. +# See Click issue https://github.com/pallets/click/issues/1590. This +# run condition skips the testcases on Windows. +CLICK_ISSUE_1590 = sys.platform == 'win32' + +_PYWBEM_VERSION = parse_version(__version__) +# pywbem 1.0.0b1 or later +PYWBEM_1_0_0B1 = _PYWBEM_VERSION.release >= (1, 0, 0) and \ + _PYWBEM_VERSION.dev is None +# pywbem 1.0.0 (dev, beta, final) or later +PYWBEM_1_0_0 = _PYWBEM_VERSION.release >= (1, 0, 0) + + +# NOTE: The following methods are testcase parameters. They define instances +# used in TESTCASES_FMT_INSTANCE_AS_ROWS +def simple_instance(pvalue=None): + """ + Build a simple instance to test and return that instance. The properties + in the instance are sorted by (lower cased) property name. + + If the parameter pvalue is provided, it must be a scalar value and an + instance with a single property with that value is returned. + """ + if pvalue: + properties = [CIMProperty("P", pvalue)] + else: + properties = [ + CIMProperty("Pbf", value=False), + CIMProperty("Pbt", value=True), + CIMProperty("Pdt", DATETIME1_OBJ), + CIMProperty("Pint32", Uint32(99)), + CIMProperty("Pint64", Uint64(9999)), + CIMProperty("Pstr1", u"Test String"), + ] + inst = CIMInstance("CIM_Foo", properties) + return inst + + +def simple_instance_unsorted(pvalue=None): + """ + Build a simple instance to test and return that instance. The properties + in the instance are not sorted by (lower cased) property name, but + the property order when sorted is the same as in the instance returned by + simple_instance(). + + If the parameter pvalue is provided, it must be a scalar value and an + instance with a single property with that value is returned. + """ + if pvalue: + properties = [CIMProperty("P", pvalue)] + else: + properties = [ + CIMProperty("Pbt", value=True), + CIMProperty("Pbf", value=False), # out of order + CIMProperty("pdt", DATETIME1_OBJ), # lower cased + CIMProperty("PInt64", Uint64(9999)), # out of order when case ins. + CIMProperty("Pint32", Uint32(99)), + CIMProperty("Pstr1", u"Test String"), + ] + inst = CIMInstance("CIM_Foo", properties) + return inst + + +def simple_instance2(pvalue=None): + """ + Build a simple instance to test and return that instance. The properties + in the instance are sorted by (lower cased) property name. + + If the parameter pvalue is provided, it must be a scalar value and an + instance with a single property with that value is returned. + """ + if pvalue: + properties = [CIMProperty("P", pvalue)] + else: + properties = [ + CIMProperty("Pbf", value=False), + CIMProperty("Pbt", value=True), + CIMProperty("Pdt", DATETIME1_OBJ), + CIMProperty("Pint64", Uint64(9999)), + CIMProperty("Psint32", Sint32(-2147483648)), + CIMProperty("Pstr1", u"Test String"), + CIMProperty("Puint32", Uint32(4294967295)), + ] + inst = CIMInstance("CIM_Foo", properties) + return inst + + +def string_instance(tst_str): + """ + Build a CIM instance with a single property. + """ + properties = [CIMProperty("Pstr1", tst_str)] + inst = CIMInstance("CIM_Foo", properties) + return inst + + +# Testcases for _format_instances_as_rows() + + # Each list item is a testcase tuple with these items: + # * desc: Short testcase description. + # * kwargs: Keyword arguments for the test function: + # * args: Positional args for _format_instances_as_rows(). + # * kwargs: Keyword args for _format_instances_as_rows(). + # * exp_rtn: Expected return value of _format_instances_as_rows(). + # * exp_exc_types: Expected exception type(s), or None. + # * exp_rtn: Expected warning type(s), or None. + # * condition: Boolean condition for testcase to run, or 'pdb' for debugger + +TESTCASES_FORMAT_INSTANCES_AS_ROWS = [ + ( + "Verify simple instance to table", + dict( + args=([simple_instance()], None), + kwargs=dict(), + exp_rtn=[ + ["false", "true", DATETIME1_STR, "99", "9999", + u'"Test String"']], + ), + None, None, True, ), + + ( + "Verify simple instance to table with col limit", + dict( + args=([simple_instance()], 30), + kwargs=dict(), + exp_rtn=[ + ["false", "true", DATETIME1_STR, "99", "9999", + u'"Test String"']], + ), + None, None, True, ), + + ( + "Verify simple instance to table, unsorted", + dict( + args=([simple_instance_unsorted()], None), + kwargs=dict(), + exp_rtn=[ + ["false", "true", DATETIME1_STR, "99", "9999", + u'"Test String"']], + ), + None, None, True, ), + + ( + "Verify instance with 2 keys and 2 non-keys, unsorted", + dict( + args=(), + kwargs=dict( + insts=[ + CIMInstance( + "CIM_Foo", + properties=[ + CIMProperty("P2", value="V2"), + CIMProperty("p1", value="V1"), + CIMProperty("Q2", value="K2"), + CIMProperty("q1", value="K1"), + ], + path=CIMInstanceName( + "CIM_Foo", + keybindings=[ + CIMProperty("Q2", value="K2"), + CIMProperty("q1", value="K1"), + ] + ), + ), + ], + ), + exp_rtn=[ + ['"K1"', '"K2"', '"V1"', '"V2"'], + ], + ), + None, None, True, ), + + ( + "Verify 2 instances with different sets of properties", + dict( + args=(), + kwargs=dict( + insts=[ + CIMInstance( + "CIM_Foo", + properties=[ + CIMProperty("P2", value="VP2a"), + CIMProperty("p1", value="VP1a"), + CIMProperty("P3", value="VP3a"), + ], + ), + CIMInstance( + "CIM_FooSub", + properties=[ + CIMProperty("P2", value="VP2b"), + CIMProperty("p1", value="VP1b"), + CIMProperty("N1", value="VN1b"), + ], + ), + ], + ), + exp_rtn=[ + ['', '"VP1a"', '"VP2a"', '"VP3a"'], + ['"VN1b"', '"VP1b"', '"VP2b"', ''], + ], + ), + None, None, True, ), + + ( + "Verify 2 instances where second one has path", + dict( + args=(), + kwargs=dict( + insts=[ + CIMInstance( + "CIM_Foo", + properties=[ + CIMProperty("P2", value="VP2a"), + CIMProperty("p1", value="VP1a"), + ], + ), + CIMInstance( + "CIM_FooSub", + properties=[ + CIMProperty("P2", value="VP2b"), + CIMProperty("p1", value="VP1b"), + CIMProperty("Q2", value="K2b"), + CIMProperty("q1", value="K1b"), + ], + path=CIMInstanceName( + "CIM_Foo", + keybindings=[ + CIMProperty("q2", value="K2b"), + CIMProperty("Q1", value="K1b"), + ] + ), + ), + ], + ), + exp_rtn=[ + ['', '', '"VP1a"', '"VP2a"'], + ['"K1b"', '"K2b"', '"VP1b"', '"VP2b"'], + ], + ), + None, None, True, ), + + ( + "Verify simple instance with one string all components overflow line", + dict( + args=([simple_instance(pvalue="A B C D")], 4), + kwargs=dict(), + exp_rtn=[ + ['"A "\n"B "\n"C "\n"D"']], + ), + None, None, True, ), + + ( + "Verify simple instance with one string all components overflow line", + dict( + args=([simple_instance(pvalue="ABCD")], 4), + kwargs=dict(), + exp_rtn=[ + ['\n"AB"\n"CD"']], + ), + None, None, True, ), + + ( + "Verify simple instance with one string overflows line", + dict( + args=([simple_instance(pvalue="A B C D")], 8), + kwargs=dict(), + exp_rtn=[ + ['"A B C "\n"D"']], + ), + None, None, True, ), + + ( + "Verify simple instance withone unit32 max val", + dict( + args=([simple_instance(pvalue=Uint32(4294967295))], 8), + kwargs=dict(), + exp_rtn=[ + ['4294967295']], + ), + None, None, True, ), + + + ( + "Verify simple instance with one string fits on line", + dict( + args=([simple_instance(pvalue="A B C D")], 12), + kwargs=dict(), + exp_rtn=[ + ['"A B C D"']], + ), + None, None, True, ), + + ( + "Verify datetime property", + dict( + args=([simple_instance(pvalue=DATETIME1_OBJ)], 20), + kwargs=dict(), + exp_rtn=[ + ['\n"20140922104920.524"\n"789+000"']], + ), + None, None, True, ), + + ( + "Verify datetime property", + dict( + args=([simple_instance(pvalue=DATETIME1_OBJ)], 30), + kwargs=dict(), + exp_rtn=[ + ['"20140922104920.524789+000"']], + ), + None, None, True, ), + + ( + "Verify integer property where len too small", + dict( + args=([simple_instance(pvalue=Uint32(999999))], 4), + kwargs=dict(), + exp_rtn=[['999999']], + ), + None, None, True, ), + + ( + "Verify char16 property", + dict( + args=([CIMInstance('P', [CIMProperty('P', + type='char16', + value='f')])], 4), + kwargs=dict(), + exp_rtn=[[u"'f'"]], + ), + None, None, True, ), + + ( + "Verify properties with no value", + dict( + args=([CIMInstance('P', [CIMProperty('P', value=None, + type='char16'), + CIMProperty('Q', value=None, + type='uint32'), + CIMProperty('R', value=None, + type='string'), ])], 4), + kwargs=dict(), + exp_rtn=[[u'', u'', u'']], + ), + None, None, True, ), + + ( + "Verify format of instance with reference property as row entry", + dict( + args=([CIMInstance("TST_REFPROP", + [CIMProperty( + 'P', + type='reference', + reference_class="blah", + value=CIMInstanceName( + "REF_CLN", + keybindings=OrderedDict(k1='v1')))])], + 30), + kwargs=dict(), + exp_rtn=[ + [u'"/:REF_CLN.k1=\\"v1\\""']], + ), + None, None, True, ), +] + + +@pytest.mark.parametrize( + "desc, kwargs, exp_exc_types, exp_warn_types, condition", + TESTCASES_FORMAT_INSTANCES_AS_ROWS) +@simplified_test_function +def test_format_instances_as_rows(testcase, args, kwargs, exp_rtn): + """ + Test the output of the common _format_instances_as_rows() function + """ + + # The code to be tested + act_rtn = _format_instances_as_rows(*args, **kwargs) + + # Ensure that exceptions raised in the remainder of this function + # are not mistaken as expected exceptions + assert testcase.exp_exc_types is None + # result is list of lists. we want to test each item in inner list + + assert len(act_rtn) == len(exp_rtn), \ + "Unexpected number of lines in test desc: {}:\n" \ + "Expected line cnt={}:\n" \ + "{}\n\n" \ + "Actual line cnt={}:\n" \ + "{}\n". \ + format(testcase.desc, len(act_rtn), '\n'.join(act_rtn), + len(exp_rtn), '\n'.join(exp_rtn)) + + assert exp_rtn == act_rtn, \ + "Unequal values for test desc: {}:\n" \ + "Expected = {}:\n" \ + "Actual = {}:\n". \ + format(testcase.desc, exp_rtn, act_rtn) + + +# Testcases for _display_instances_as_table() + + # Each list item is a testcase tuple with these items: + # * desc: Short testcase description. + # * kwargs: Keyword arguments for the test function: + # * args: Positional args for _display_instances_as_table(). + # * kwargs: Keyword args for _display_instances_as_table(). + # * exp_stdout: Expected output on stdout. + # * exp_exc_types: Expected exception type(s), or None. + # * exp_warn_types: Expected warning type(s), or None. + # * condition: Boolean condition for testcase to run, or 'pdb' for debugger + +TESTCASES_DISPLAY_INSTANCES_AS_TABLE = [ + ( + "Verify print of simple instance to table", + dict( + args=([simple_instance()], None, 'simple'), + kwargs=dict(), + exp_stdout="""\ +Instances: CIM_Foo +Pbf Pbt Pdt Pint32 Pint64 Pstr1 +----- ----- ----------------------- -------- -------- ------------- +false true "20140922104920.524789" 99 9999 "Test String" + "+000" +""", + ), + None, None, not CLICK_ISSUE_1590 + ), + ( + "Verify print of simple instance to table with col limit", + dict( + args=([simple_instance2()], 80, 'simple'), + kwargs=dict(), + exp_stdout="""\ +Instances: CIM_Foo +Pbf Pbt Pdt Pint64 Psint32 Pstr1 Puint32 +----- ----- --------- -------- ----------- -------- ---------- +false true "2014092" 9999 -2147483648 "Test " 4294967295 + "2104920" "String" + ".524789" + "+000" +""", + ), + None, None, not CLICK_ISSUE_1590 + ), + ( + "Verify print of instance with reference property", + dict( + args=([CIMInstance("CIM_Foo", + [CIMProperty( + 'P', + type='reference', + reference_class="blah", + value=CIMInstanceName( + "REF_CLN", + keybindings=OrderedDict(k1='v1', + k2=32)))])], + 80, 'simple'), + kwargs=dict(), + exp_stdout="""\ +Instances: CIM_Foo +P +--------------------------- +"/:REF_CLN.k1=\\"v1\\",k2=32" +""", + ), + None, None, not CLICK_ISSUE_1590 and PYWBEM_1_0_0B1 + ), +] + + +@pytest.mark.parametrize( + "desc, kwargs, exp_exc_types, exp_warn_types, condition", + TESTCASES_DISPLAY_INSTANCES_AS_TABLE) +def test_display_instances_as_table( + desc, kwargs, exp_exc_types, exp_warn_types, condition, capsys): + """ + Test the output of the print_insts_as_table function. This primarily + tests for overall format and the ability of the function to output to + stdout. The previous test tests the row formatting and handling of + multiple instances. + """ + if not condition: + pytest.skip("Testcase condition not satisfied") + + # This logic only supports successful testcases without warnings + assert exp_exc_types is None + assert exp_warn_types is None + + args = kwargs['args'] + kwargs_ = kwargs['kwargs'] + exp_stdout = kwargs['exp_stdout'] + + # The code to be tested + _display_instances_as_table(*args, **kwargs_) + + stdout, _ = capsys.readouterr() + assert exp_stdout == stdout, \ + "Unexpected output in test case: {}\n" \ + "Actual:\n" \ + "{}\n" \ + "Expected:\n" \ + "{}\n" \ + "End\n".format(desc, stdout, exp_stdout)