Systest hardbootme: Inventory diff file line numbers incorrect

To resolve false systest inventory difference, use Linux diff
instead of difflib. Python's difflib reports false differences.

Moved file diff code to the lib directory from syslib.

We want testing to continue even if SOL logging is not currently
available, so added  Ignore Error on Start SOL Console Logging.

Resolves openbmc/openbmc-test-automation#863
Change-Id: I04a412df584ca421e3ef129be9ae2452d7091d70
Signed-off-by: Steven Sombar <ssombar@us.ibm.com>
diff --git a/lib/utils_files.py b/lib/utils_files.py
new file mode 100644
index 0000000..9b1ed94
--- /dev/null
+++ b/lib/utils_files.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+
+r"""
+This module contains file functions such as file_diff.
+"""
+
+import time
+import os
+import re
+from gen_cmd import cmd_fnc_u
+robot_env = 1
+try:
+    from robot.libraries.BuiltIn import BuiltIn
+    from robot.libraries import DateTime
+except ImportError:
+    robot_env = 0
+
+
+##########################################################################
+def file_diff(file1_path,
+              file2_path,
+              diff_file_path,
+              skip_string):
+    r"""
+    Compare the contents of two text files.  The comparison uses the Unix
+    'diff' command.  Differences can be selectively ignored by use of
+    the skip_string parameter.  The output of diff command is written
+    to a user-specified file and is also written (logged) to the console.
+
+    Description of arguments:
+    file1_path       File containing text data.
+    file2_path       Text file to compare to file1.
+    diff_file_path   Text file which will contain the diff output.
+    skip_string      To allow for differences which may expected or immaterial,
+                     skip_string parameter is a word or a string of comma
+                     separated words which specify what should be ignored.
+                     For example, "size,speed".  Any line containing the word
+                     size or the word speed will be ignored when the diff is
+                     performed.  This parameter is optional.
+
+    Returns:
+    0 if both files contain the same information or they differ only in
+      items specified by the skip_string.
+    2 if FILES_DO_NOT_MATCH.
+    3 if INPUT_FILE_DOES_NOT_EXIST.
+    4 if IO_EXCEPTION_READING_FILE.
+    5 if IO_EXCEPTION_WRITING_FILE.
+    6 if INPUT_FILE_MALFORMED
+    """
+
+    FILES_MATCH = 0
+    FILES_DO_NOT_MATCH = 2
+    INPUT_FILE_DOES_NOT_EXIST = 3
+    IO_EXCEPTION_READING_FILE = 4
+    IO_EXCEPTION_WRITING_FILE = 5
+    INPUT_FILE_MALFORMED = 6
+
+    # The minimum size in bytes a file must be.
+    min_file_byte_size = 1
+
+    now = time.strftime("%Y-%m-%d %H:%M:%S")
+
+    if (not os.path.exists(file1_path) or (not os.path.exists(file2_path))):
+        return INPUT_FILE_DOES_NOT_EXIST
+    try:
+        with open(file1_path, 'r') as file:
+            initial = file.readlines()
+        with open(file2_path, 'r') as file:
+            final = file.readlines()
+    except IOError:
+        file.close()
+        return IO_EXCEPTION_READING_FILE
+    except ValueError:
+        file.close()
+        return INPUT_FILE_MALFORMED
+    else:
+        file.close()
+
+    # Must have more than a trivial number of bytes.
+    if len(initial) < min_file_byte_size:
+        return INPUT_FILE_MALFORMED
+
+    if (initial == final):
+        try:
+            file = open(diff_file_path, 'w')
+        except IOError:
+            file.close()
+        line_to_print = "Specified skip (ignore) string = " + \
+            skip_string + "\n\n"
+        file.write(line_to_print)
+        line_to_print = now + " found no difference between file " + \
+            file1_path + " and " + \
+            file2_path + "\n"
+        file.write(line_to_print)
+        file.close()
+        return FILES_MATCH
+
+    # Find the differences and write difference report to diff_file_path file
+    try:
+        file = open(diff_file_path, 'w')
+    except IOError:
+        file.close()
+        return IO_EXCEPTION_WRITING_FILE
+
+    # Form a UNIX diff command and its parameters as a string.  For example,
+    # if skip_string="size,capacity",  command = 'diff  -I "size"
+    # -I "capacity"  file1_path file2_path'.
+    skip_list = filter(None, re.split(r"[ ]*,[ ]*", skip_string))
+    ignore_string = ' '.join([("-I " + '"' + x + '"') for x in skip_list])
+    command = ' '.join(filter(None, ["diff", ignore_string, file1_path,
+                       file2_path]))
+
+    line_to_print = now + "   " + command + "\n"
+    file.write(line_to_print)
+
+    # Run the command and get the differences
+    rc, out_buf = cmd_fnc_u(command, quiet=0, print_output=0, show_err=0)
+
+    # Write the differences to the specified diff_file and console.
+    if robot_env == 1:
+        BuiltIn().log_to_console("DIFF:\n" + out_buf)
+    else:
+        print "DIFF:\n", out_buf
+
+    file.write(out_buf)
+    file.close()
+
+    if rc == 0:
+        # Any differences found were on the skip_string.
+        return FILES_MATCH
+    else:
+        # We have at least one difference not in the skip_string.
+        return FILES_DO_NOT_MATCH
+###############################################################################
diff --git a/syslib/utils_keywords.py b/syslib/utils_keywords.py
index c802065..89b6894 100644
--- a/syslib/utils_keywords.py
+++ b/syslib/utils_keywords.py
@@ -12,233 +12,9 @@
     pass
 import time
 import os
-import difflib
 
 
 ##########################################################################
-def json_inv_file_diff_check(file1_path,
-                             file2_path,
-                             diff_file_path,
-                             skip_string):
-    r"""
-    Compare the contents of two files which contain inventory data in
-    JSON format.  The comparison is similar to the unix 'diff' command but
-    the output lists the hardware subsystem (category) where differences
-    are found, and some differences are selectively ignored.  The items
-    ignored are defined by the skip_string.
-
-    Description of arguments:
-    file1_path       File containing JSON formatted data.
-    file2_path       File to compare to file1 to.
-    diff_file_path   File which will contain the resulting difference report.
-    skip_string      String which defines what inventory items
-                     to ignore if there are differences in inventory
-                     files -- some differences are expected or
-                     immaterial.  For example, assigned processor
-                     speed routinely varies depending upon the
-                     needs of OCC/tmgt/ondemand governor.
-                     Back-to-back inventory runs may show
-                     processor speed differences even if nothing
-                     else was run between them.
-                     Each item in this string is of the form
-                     category:leafname where category is a JSON
-                     hardware category such as "processor", "memory",
-                     "disk", "display", "network", etc.,
-                     and leafname is a leaf node (atribute name)
-                     within that category.
-                     For example: "processor:size", or
-                     "processor:size,network:speed,display:id".
-
-    Sample difference report:
-    Difference at line 102  (in section "memory":)
-     102 -   "slot": "UOPWR.BAR.1315ACA-DIMM0",
-     102 +   "slot": "0"
-    Difference at line 126  (in section "processor":)
-     126 -   "size": 2151000000,    +++ NOTE! This is an ignore item
-     126 +   "size": 2201000000,    +++ NOTE! This is an ignore item
-
-    Returns:
-    0 if both files contain the same information or they differ only in
-      items specified as those to ignore.
-    2 if FILES_DO_NOT_MATCH.
-    3 if INPUT_FILE_DOES_NOT_EXIST.
-    4 if IO_EXCEPTION_READING_FILE.
-    5 if IO_EXCEPTION_WRITING_FILE.
-    """
-
-    FILES_MATCH = 0
-    FILES_DO_NOT_MATCH = 2
-    INPUT_FILE_DOES_NOT_EXIST = 3
-    IO_EXCEPTION_READING_FILE = 4
-    IO_EXCEPTION_WRITING_FILE = 5
-
-    # Hardware categories which are reported in the JSON inventory files.
-    hardware_categories = ['\"processor\":', '\"memory\":', '\"disk\":',
-                           '\"I/O\":', '\"display\":', '\"generic\":',
-                           '\"network\":', '\"communication\":',
-                           '\"printer\":', '\"input\":', '\"multimedia\":',
-                           '\"tape\":']
-
-    # The minimum size in bytes a JSON file must be.
-    min_json_byte_size = 16
-
-    now = time.strftime("At %Y-%m-%d %H:%M:%S")
-
-    if (not os.path.exists(file1_path) or (not os.path.exists(file2_path))):
-        return INPUT_FILE_DOES_NOT_EXIST
-    try:
-        with open(file1_path, 'r') as file:
-            initial = file.readlines()
-        with open(file2_path, 'r') as file:
-            final = file.readlines()
-    except IOError:
-        file.close()
-        return IO_EXCEPTION_READING_FILE
-    except ValueError:
-        file.close()
-        return INPUT_FILE_MALFORMED
-    else:
-        file.close()
-
-    # Must have more than a trivial number of bytes.
-    if len(initial) <= min_json_byte_size:
-        return INPUT_FILE_MALFORMED
-
-    if (initial == final):
-        try:
-            file = open(diff_file_path, 'w')
-        except IOError:
-            file.close()
-        line_to_print = "Specified skip (ignore) string = " + \
-            skip_string + "\n\n"
-        file.write(line_to_print)
-        line_to_print = now + " found no difference between file " + \
-            file1_path + " and " + \
-            file2_path + "\n"
-        file.write(line_to_print)
-        file.close()
-        return FILES_MATCH
-
-    # Find the differences and write difference report to diff_file_path file.
-    try:
-        file = open(diff_file_path, 'w')
-    except IOError:
-        file.close()
-        return IO_EXCEPTION_WRITING_FILE
-
-    line_to_print = "Specified skip (ignore) string = " + skip_string + "\n\n"
-    file.write(line_to_print)
-    line_to_print = now + " compared files " + \
-        file1_path + " and " + \
-        file2_path + "\n"
-    file.write(line_to_print)
-
-    diff = difflib.ndiff(initial, final)
-    # The diff array contains all lines that match in the
-    # initial and final arrays, and also all lines that differ.
-    # The first two characters of each line
-    # are prefixed with two letters, defined as:
-    # '- ' This line is unique to initial
-    # '+ ' This line is line unique to final
-    # '  ' This line is common to both, and
-    # '? ' This line indicates approximate differences.
-    # For example,  comparing two three-line files:
-    #  This line is in both initial and final.
-    #  This line is too but the next line is different in each file.
-    # -                     "size": 2101000000,
-    # ?                              - ^
-    # +                     "size": 2002000000,
-    # ?                               ^^
-
-    print_header_flag = False
-    category = ""
-    row_num = 1
-    item_we_cannot_ignore = False
-
-    for my_line in diff:
-        diff_item = my_line.strip('\n')
-        # If it's a Category, such as processor or memory,
-        # save it.  We will print it out later.
-        # Most lines do not explicitly contain a category.  As such the
-        # category for that line is found in a previous line, which
-        # will be the category we last found.
-        for hdw_cat in hardware_categories:
-            if (hdw_cat in diff_item):
-                # If we don't have a match we will reuse
-                # the prvious category.
-                category = hdw_cat
-        # Lines beginning with minus or plus or q-mark are
-        # true difference items.
-        # We want to look at those in more detail.
-        if diff_item.startswith('? '):
-            # we can ignore these
-            continue
-        if (diff_item.startswith('- ') or diff_item.startswith('+ ')):
-            # If we have not printed the header line for this
-            # difference, print it now.
-            if print_header_flag is False:
-                line_to_print = "Difference at line " + \
-                 str(row_num) + "  (in section " + \
-                 category + ")\n"
-                file.write(line_to_print)
-            # If this is in the ignore string, we'll print
-            # it but also add text that it is an ignore item.
-            skipitem = False
-            # If a skip_string is specified, check if category and item are
-            # in the skip_string.
-            if skip_string:
-                skip_list = skip_string.split(",")
-                for item in skip_list:
-                    cat_and_value = item.split(":")
-                    ignore_category = cat_and_value[0].lower().strip()
-                    ignore_value = cat_and_value[1].lower().strip()
-                    if ((ignore_category in category.lower().strip()) and
-                       (ignore_value in diff_item.lower().strip())):
-                        line_to_print = "  " + \
-                            str(row_num) + " " + diff_item + \
-                            "    +++ NOTE! This line matches" + \
-                            " the inventory ignore list and" + \
-                            " can be ignored. +++\n"
-                        # Set flag indicating this item is a skip item.
-                        skipitem = True
-                        break
-            if skipitem is False:
-                # Its not a skip item, that is,
-                # this is not on the ignore list.
-                # Print the item and set the item_we_canot_ignore flag
-                # indicating we have an item not on the ignore list.  The
-                # flag will determine the return code we
-                # pass back to the user at the end.
-                item_we_cannot_ignore = True
-                line_to_print = "  " + \
-                    str(row_num) + " " + diff_item + "\n"
-            file.write(line_to_print)
-            print_header_flag = True
-
-        else:
-            # Adjust row numbering as a difference is only one line
-            # but it takes several lines in the diff file.
-            if print_header_flag is True:
-                row_num = row_num + 1
-                print_header_flag = False
-            row_num = row_num + 1
-
-    # Make sure we end the file.
-    file.write("\n")
-    file.close()
-
-    if item_we_cannot_ignore:
-        # We have at least one diff_item not on the ignore list.
-        return FILES_DO_NOT_MATCH
-    else:
-        # Any differences were on the ignore list.
-        return FILES_MATCH
-###############################################################################
-
-
-###############################################################################
-
-
 def run_until_keyword_fails(retry,
                             retry_interval,
                             name,
diff --git a/systest/htx_hardbootme_test.robot b/systest/htx_hardbootme_test.robot
index d1a1796..66fcfcb 100755
--- a/systest/htx_hardbootme_test.robot
+++ b/systest/htx_hardbootme_test.robot
@@ -26,19 +26,21 @@
 #                     This parameter is optional.  If not specified, an
 #                     initial inventory snapshot will be taken before
 #                     HTX startup.
-# INV_IGNORE_LIST     A comma-delimited list of colon-delimited pairs that
+# INV_IGNORE_LIST     A comma-delimited list of strings that
 #                     indicate what to ignore if there are inventory
-#                     differences.  For example, "processor:size,network:speed"
+#                     differences, (e.g., processor "size").
 #                     If differences are found during inventory checking
-#                     and those items are in this string, the
+#                     and those items are in this list, the
 #                     differences will be ignored.  This parameter is
 #                     optional.  If not specified the default value is
-#                     "processor:size".
+#                     "size".
 
 Resource        ../syslib/utils_os.robot
 Library         ../syslib/utils_keywords.py
+Library         ../lib/utils_files.py
 
-Suite Setup     Run Key  Start SOL Console Logging
+
+Suite Setup     Run Keyword And Ignore Error  Start SOL Console Logging
 Test Setup      Pre Test Case Execution
 Test Teardown   Post Test Case Execution
 
@@ -51,7 +53,7 @@
 ${json_diff_file_path}       ${EXECDIR}/os_inventory_diff.json
 ${last_inventory_file_path}  ${EMPTY}
 ${CHECK_INVENTORY}           True
-${INV_IGNORE_LIST}           processor:size
+${INV_IGNORE_LIST}           size
 ${PREV_INV_FILE_PATH}        ${EMPTY}
 
 
@@ -148,7 +150,7 @@
     # file1   A file that has an inventory snapshot in JSON format.
     # file2   A file that has an inventory snapshot, to compare with file1.
 
-    ${diff_rc}=  JSON_Inv_File_Diff_Check  ${file1}
+    ${diff_rc}=  File_Diff  ${file1}
      ...  ${file2}  ${json_diff_file_path}  ${INV_IGNORE_LIST}
     Run Keyword If  '${diff_rc}' != '${0}'
     ...  Report Inventory Mismatch  ${diff_rc}  ${json_diff_file_path}