pytest: Add approx match to verify component test

- Show the closest hardwares when the number of components from probed
  results and from device data are mismatched.

BUG=chromium:875167
Test=manually run on device

Change-Id: I8239e0f37be2695ee5bb440a635d58c1ac66b9ed
Reviewed-on: https://chromium-review.googlesource.com/1183054
Commit-Ready: Yuan-Yao Sung <yysung@google.com>
Tested-by: Yuan-Yao Sung <yysung@google.com>
Reviewed-by: Cheng-Han Yang <chenghan@chromium.org>
diff --git a/py/test/pytests/verify_component.py b/py/test/pytests/verify_component.py
index 8489c11..a05e00a 100644
--- a/py/test/pytests/verify_component.py
+++ b/py/test/pytests/verify_component.py
@@ -31,9 +31,15 @@
 
 class VerifyComponentTest(test_case.TestCase):
   ARGS = [
+      Arg('approx_match', bool,
+          'Enable apporximate matching results.',
+          default=True),
       Arg('enable_factory_server', bool,
           'Update hwid data from factory server.',
           default=True),
+      Arg('max_mismatch', int,
+          'The number of mismatched rules at most.',
+          default=1),
       Arg('verify_checksum', bool,
           'Enable converted statements checksum verification.',
           default=True)
@@ -47,6 +53,7 @@
     self.num_mismatch = []
     self.not_supported = []
     self.probed_results = {}
+    self.perfect_match_results = {}
     self.component_data = {}
     self.converted_statement_file = self.dut.path.join(
         self.tmpdir, 'converted_statement_file.json')
@@ -64,7 +71,10 @@
         session.console.info('Checksum passed.')
 
     self.probed_results = json_utils.LoadStr(self.factory_tools.CheckOutput(
-        ['probe', 'probe', '--config-file', self.converted_statement_file]))
+        ['probe', 'probe', '--config-file', self.converted_statement_file,
+         '--approx-match', '--mismatch-num',
+         '{}'.format(self.args.max_mismatch)]))
+    self.perfect_match_results = self._GetPerfectMatchProbeResult()
     self.component_data = {k[4:]: int(v) for k, v in
                            device_data.GetDeviceData('component').iteritems()
                            if k.startswith('has_')}
@@ -76,7 +86,8 @@
       self.ui.CallJSFunction('setFailedMessage')
       if self.num_mismatch:
         self.ui.CallJSFunction(
-            'createNumMismatchResult', self.num_mismatch)
+            'createNumMismatchResult', self.num_mismatch,
+            self.args.approx_match, self.probed_results)
 
       if self.not_supported:
         self.ui.CallJSFunction(
@@ -93,7 +104,7 @@
       return [comp['name'] for comp in comp_info]
 
     for comp_cls, correct_num in self.component_data.iteritems():
-      comp_info = self.probed_results.get(comp_cls, [])
+      comp_info = self.perfect_match_results.get(comp_cls, [])
       actual_num = len(comp_info)
       if correct_num != actual_num:
         self.num_mismatch.append((comp_cls, correct_num,
@@ -101,7 +112,7 @@
 
     # The number of component should be _NUMBER_NOT_IN_DEVICE_DATA
     # when the component is not in device data.
-    for comp_cls, comp_info in self.probed_results.iteritems():
+    for comp_cls, comp_info in self.perfect_match_results.iteritems():
       if comp_cls not in self.component_data:
         actual_num = len(comp_info)
         if actual_num != _NUMBER_NOT_IN_DEVICE_DATA:
@@ -111,7 +122,7 @@
 
   def _VerifyNotSupported(self):
     if self._CheckPhase():
-      for comp_cls, comp_info in self.probed_results.iteritems():
+      for comp_cls, comp_info in self.perfect_match_results.iteritems():
         for comp_item in comp_info:
           status = comp_item['information']['status']
           if status != common.COMPONENT_STATUS.supported:
@@ -129,3 +140,10 @@
     converted_statement = self.dut.ReadFile(self.converted_statement_file)
     converted_checksum = self.dut.ReadFile(converted_checksum_file)
     return converted_statement, converted_checksum
+
+  def _GetPerfectMatchProbeResult(self):
+    res = {}
+    for comp_cls, comp_info in self.probed_results.iteritems():
+      res[comp_cls] = [item for item in comp_info if item['perfect_match']]
+
+    return res
diff --git a/py/test/pytests/verify_component_static/verify_component.js b/py/test/pytests/verify_component_static/verify_component.js
index 08535e3..c86fc4b 100644
--- a/py/test/pytests/verify_component_static/verify_component.js
+++ b/py/test/pytests/verify_component_static/verify_component.js
@@ -2,36 +2,66 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-const createNumMismatchResult = (data) => {
+const createNumMismatchResult = (data, approxMatch, probedResults) => {
   const title = document.getElementById('verify-component-mismatch-label');
   title.classList.remove('hidden');
   const numMismatch = document.getElementById('verify-component-mismatch');
-  data.forEach(([comp_cls, expected_num, comp_names]) => {
+  data.forEach(([compCls, expectedNum, compNames]) => {
     const content = document.createElement('div');
     const contentTitle = document.createElement('h2');
-    contentTitle.appendChild(document.createTextNode(comp_cls));
+    contentTitle.appendChild(document.createTextNode(compCls));
     content.appendChild(contentTitle);
     const contentResult = document.createElement('div');
     contentResult.classList.add('verify-component-mismatch-result');
     const contentText = document.createElement('p');
     contentText.appendChild(document.createTextNode(
-        `Expected ${expected_num} component(s)`));
+        `Expected ${expectedNum} component(s)`));
     contentText.appendChild(document.createElement('br'));
     contentText.appendChild(document.createTextNode(
-        `Found ${comp_names.length} component(s):`));
+        `Found ${compNames.length} component(s):`));
     contentResult.appendChild(contentText);
     const contentListBody = document.createElement('ul');
-    comp_names.forEach((comp_name) => {
+    compNames.forEach((compName) => {
       const contentList = document.createElement('li');
-      contentList.appendChild(document.createTextNode(comp_name));
+      contentList.appendChild(document.createTextNode(compName));
       contentListBody.appendChild(contentList);
     });
     contentResult.appendChild(contentListBody);
+    if (approxMatch) {
+      createApproxMatchResult(contentResult, probedResults[compCls]);
+    }
     content.appendChild(contentResult);
     numMismatch.appendChild(content);
   });
 };
 
+const createApproxMatchResult = (contentResult, compInfo) => {
+  const approxText = document.createElement('p');
+  approxText.appendChild(document.createTextNode(
+      'Found almost matched components(s):'));
+  contentResult.appendChild(approxText);
+  compInfo.forEach((comp) => {
+    if (!comp.perfect_match) {
+      const approxResult = document.createElement('div');
+      const approxCompName = document.createElement('h2');
+      approxCompName.appendChild(document.createTextNode(comp['name']));
+      approxResult.appendChild(approxCompName);
+      const approxListBody = document.createElement('ul');
+      const rules = comp.approx_match.rule;
+      for (const rule in rules) {
+        if (!rules[rule].result) {
+          const approxList = document.createElement('li');
+          approxList.appendChild(document.createTextNode(
+              `${rule}: ${rules[rule].info}, found: ${comp.values[rule]}`));
+          approxListBody.append(approxList);
+        }
+      }
+      approxResult.appendChild(approxListBody);
+      contentResult.appendChild(approxResult)
+    }
+  });
+}
+
 const createNotSupportedResult = (data) => {
   const title = document.getElementById('verify-component-not-supported-label');
   title.classList.remove('hidden');