[Test] Update touch_firmware_versions script.

Check in my local changes.  This includes:
- Handle reef-specific changes.
- Refactor code to use a firmwareInfo class, instead of just printing.
- Minor regex improvements, including allowing .hex filenames.
- Clean up a few logic errors that don't cause trouble with existing
ebuilds but could in the future.

BUG=None
TEST=result output does not change (except sorted and has reef changes)
Change-Id: Ib089b891ab13e8a5effc5071551daeee4924c8a5
Reviewed-on: https://chromium-review.googlesource.com/617511
Commit-Ready: Katherine Threlkeld <kathrelkeld@chromium.org>
Tested-by: Katherine Threlkeld <kathrelkeld@chromium.org>
Reviewed-by: Vinayak Suley <vsuley@chromium.org>
diff --git a/provingground/touch_firmware_versions.py b/provingground/touch_firmware_versions.py
index b91bc49..9c2d546 100755
--- a/provingground/touch_firmware_versions.py
+++ b/provingground/touch_firmware_versions.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python2
+#!/usr/bin/env python2
 # Copyright 2016 The Chromium OS Authors. All rights reserved.
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
@@ -29,10 +29,28 @@
 SRC_DIR = '../../../' # Relative path from this file to source directory
 
 
-def output_values(device, hw_id, fw_version, fw_file, ebuild_file):
-  """Formats and prints results for a single type of hardware."""
-  print('%s\t%s\t%s\t%s\t%s' % (
-      device, hw_id, fw_version, fw_file, ebuild_file))
+class firmwareInfo(object):
+  """Information about a particular firmware file."""
+  def __init__(self, device, path):
+    self.device = device
+    self.ebuild_filepath = path
+
+    self.hw_id = None
+    self.fw_id = None
+    self.fw_file = None
+
+    self.problem_found = False
+    self.error = ''
+
+  def __str__(self):
+    return ('%s\t%s\t%s\t%s\t%s' % (
+        self.device, self.hw_id, self.fw_id, self.fw_file,
+        self.ebuild_filepath))
+
+  def set_ids(self, hwid, fwid, filename):
+    self.hw_id = hwid
+    self.fw_id = fwid
+    self.fw_file = filename
 
 
 def find_value_of_variable(var, text):
@@ -57,11 +75,10 @@
 def reduce_string(link, text):
   """Finds the absolute value of the given string.
 
-  Assumes variables embedded in a string are in ${VAR} format.
+  Assumes variables embedded in a string are in ${VAR} or \s$VAR\s format.
   E.g., given '"${PRODUCT_ID_TP}_${FIRMWARE_VERSION_TP}.bin"' return
   '"85.0_7.0.bin"', after following all variables.
-  Assumes the entire string could be a varible of the form
-  $VAR (no { }).
+  Assumes the entire string could be a varible of the form $VAR (no { }).
 
   Args:
     link: the string which contains variables to reduce.
@@ -70,35 +87,37 @@
   Returns:
     The given string with all variables replaced, or '' if error.
   """
-  search = re.search(r'\$\{(.*?)\}', link)
-  if not search:
-    search = re.search(r'\$([^\s]*)', link)
-    if not search:
-      return link
+  var_formats = [r'\$\{(.*?)\}', r'\$([^\s]*)']
+  for var_format in var_formats:
+    search = re.search(var_format, link)
+    if search:
+      break
+  else:
+    return link
 
   variable = search.group(1)
   value = find_value_of_variable(variable, text)
   if not value:
     return ''
-  new_link = link.replace('${%s}' % variable, value)
+
+  # Whatever the variable format ended up being, swap in the value found.
+  variable_format = search.group(0).replace(search.group(1), '%s')
+  new_link = link.replace(variable_format % variable, value)
+
   return reduce_string(new_link, text)
 
 
-def find_values_from_line(device, line, text):
-  """Finds the values to be output for the given dosym line.
+def find_info_from_line(info, line, text):
+  """Finds the info to be output for the given dosym line.
 
   Makes exceptions for known formatting problems.
 
   Args:
-    device: the name of the device, e.g., 'squawks'
+    info: the firmwareInfo class for this firmware line.  This function will
+        update this object with hardware id, firmware id, and firmware path.
     line: the dosym line to evaluate.  Passed in the form of a list:
         [linked from argument, NA, linked to argument].
     text: the text of the ebuild file.
-
-  Returns:
-    The hardware version, firmware version, and firmware filename.
-    A value of None indicates that this entry should be skipped, and a value
-    of '' indicates an error.
   """
   link_from = reduce_string(line[0], text) # dosym FROM
   link_to = reduce_string(line[2], text) # dosym TO
@@ -106,34 +125,32 @@
   # Find needed value from the symlink filenames.
   hw_id, fw_version, fw_file = '', '', ''
   if link_from != '':
-    search = re.search(r'/([^/]*)_(.*)\.', link_from)
+    search = re.search(r'([^/"]*)_(.*)\.', link_from)
     if search:
       hw_id = search.group(1)
       fw_version = search.group(2)
   if link_to != '':
-    search = re.search(r'/([^/]*?)(\.bin|\.fw)?"', link_to)
+    search = re.search(r'/([^/]*?)(\.bin|\.fw|\.hex)?"', link_to)
     if search:
       fw_file = search.group(1)
 
+  info.set_ids(hw_id, fw_version, fw_file)
+
   # Exceptions for unfortunately formatted files.
-  if (device == 'lulu' or device == 'umaro') and fw_file == '':
+  if (info.device == 'lulu' or info.device == 'umaro') and fw_file == '':
     bad_fmt = 'SYNA_TP_SYM_LINK_PATH=}'
     good_fmt = bad_fmt.replace('=', '')
     new_line = list(line)
     if bad_fmt in line[2]:
       new_line[2] = line[2].replace(bad_fmt, good_fmt)
-      hw_id, fw_version, fw_file = find_values_from_line(
-          device, new_line, text)
-  elif device == 'kip' and link_from.find('dummy') >= 0:
-    hw_id = None
-  elif (device == 'sumo' and
-        link_to and 'fw.bin' not in link_to):
-    hw_id = None
-
-  return hw_id, fw_version, fw_file
+      find_info_from_line(info, new_line, text)
+  elif info.device == 'kip' and link_from.find('dummy') >= 0:
+    info.hw_id = None
+  elif info.device == 'sumo' and link_to and 'fw.bin' not in link_to:
+    info.hw_id = None
 
 
-def find_values_in_file(path):
+def find_firmwares_in_file(path):
   """Finds all dosym lines in the file and outputs values as needed.
 
   If anything went wrong, outputs the devicename for manual inspection.
@@ -141,55 +158,92 @@
   Args:
     path: the path to the ebuild file.
   """
-  device = re.search('overlay-(variant-)?([^/]*?)(-private)?/', path).group(2)
+  def create_error_info(device, path, error):
+    error_values = firmwareInfo(device, path)
+    error_values.problem_found = True
+    error_values.error = error
+    return error_values
+
+  search = re.search('/(overlay-)?(variant-)?(baseboard-)?([^/]*?)(-private)?/',
+                     path)
+  if not search:
+    return [create_error_info(
+        path, path, 'Project path did not match expected format!')]
+
+  device = search.group(4)
+
   with open(path) as fh:
     text = fh.read()
 
-    # Find all symlinks for firmware and config files.  Some lines are
-    # actually multiline in the file, so use findall to get the entirety.
+  # Special case for unified reef, look for "install_firmware".
+  if device == 'reef':
+    if not re.search('install_fw', text):
+      return [create_error_info(
+          device, path, 'No "install_fw" lines found in this file!')]
+
+    search = re.findall(r'install_fw\s([^\s]*)\s*(\\*\s*\n)?\s*([^\s]*)', text,
+                        re.MULTILINE)
+
+  # For all other boards, find all symlinks for firmware and config files.
+  else:
     if not re.search('dosym', text):
-      return
+      return [create_error_info(
+          device, path, 'No "dosym" lines found in this file!')]
+
     search = re.findall(r'dosym\s([^\s]*)\s*(\\*\s*\n)?\s*([^\s]*)', text,
                         re.MULTILINE)
-    if not search:
-      return device
+  if not search:
+    return [create_error_info(
+        device, path, 'The dosym lines did not match expected format!')]
 
-    # Each line has the format [from, N/A, to]
-    problems_found = False
-    for line in search:
-      hw_id, fw_version, fw_file = find_values_from_line(device, line, text)
-      # None = skip me.  '' = problem.
-      if hw_id != None:
-        if hw_id == '' or fw_version == '' or fw_file == '':
-          problems_found = True
-        else:
-          output_values(device, hw_id, fw_version, fw_file, path)
+  # Each line has the format [from, N/A, to]
+  values_list = []
+  for line in search:
+    values = firmwareInfo(device, path)
+    find_info_from_line(values, line, text)
+    if values.hw_id != None:
+      if values.hw_id == '' or values.fw_id == '' or values.fw_file == '':
+        values.problem_found = True
+        values.error = ('Values did not make sense: %s' % values)
+      values_list.append(values)
 
-    if problems_found:
-      return device
+  return values_list
 
-def main():
-  """Finds all touch-firmware files and find their firmware values."""
+
+def firmware_versions():
+  """Finds all touch-firmware files and their firmware values."""
   file_dir = os.path.realpath(os.path.dirname(__file__))
   src_dir = os.path.join(file_dir, SRC_DIR)
   os.chdir(src_dir)
+
+  # Find ebuild files, e.g. chromeos-touch-firmware-caroline-0.0.1.ebuild
   find_output = ''
-  cmd = 'find %s -name *touch-firmware-*-0.0.[0-9].ebuild'
+  cmd = 'find %s -regex .*touch-firmware-.*-[0-9]+\.[0-9]+\.[0-9]+\.ebuild'
   for d in ['private-overlays/', 'overlays/']:
     find_output += subprocess.check_output((cmd % d).split(' '))
   ebuilds = find_output.split()
   ebuilds.sort()
 
-  # Find and output values.
-  problem_devices = []
+  values_list = []
   for path in ebuilds:
-    err = find_values_in_file(path)
-    if err:
-      problem_devices.append(err)
+    values_list += find_firmwares_in_file(path)
+
+  values_list.sort(key=lambda x: x.device)
+  return values_list
+
+def main():
+  values_list = firmware_versions()
+  problem_devices = {}
+  for values in values_list:
+    if values.problem_found:
+      problem_devices[values.device] = values.error
+    else:
+      print(values)
 
   # Output any problematic devices found, if any.
   if len(problem_devices) > 0:
     print('ERROR: please review %s' % problem_devices)
 
+
 if __name__ == '__main__':
   main()