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)