Add class to parse chrome histograms.

Existing hw decode video tests need to observe the output of
chrome://histograms/<some_histogram> to tell if hw was used.

Every test was repeating this logic. Moving this responsibility into one
simple
class.

BUG=None.
TEST=ran all modified video tests.

Change-Id: I3526706b79c7fda8e2129fe2c2707ed27b7b7583
Reviewed-on: https://chromium-review.googlesource.com/219875
Reviewed-by: Rohit Makasana <rohitbm@chromium.org>
Tested-by: Mussa Kiroga <mussa@chromium.org>
Commit-Queue: Mussa Kiroga <mussa@chromium.org>
diff --git a/client/cros/video/histogram_parser.py b/client/cros/video/histogram_parser.py
new file mode 100644
index 0000000..cfbf56c
--- /dev/null
+++ b/client/cros/video/histogram_parser.py
@@ -0,0 +1,187 @@
+# Copyright (c) 2013 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.
+
+import re
+from collections import namedtuple
+
+from autotest_lib.client.bin import utils
+from autotest_lib.client.common_lib import error
+
+
+BucketStats = namedtuple('BucketStats', 'value percent')
+
+
+class HistogramParser(object):
+    """
+    Parses a chrome histogram page and provide access to its values.
+
+    Example usage:
+    parser = histogram_parser.HistogramParser('some_histogram_name')
+
+    # Later access amazing magical values:
+    buckets = parser.buckets
+
+    if buckets and buckets[1] == ??:
+        # do cool stuff
+
+    """
+
+    def __init__(self, chrome, histogram, time_out_s=10):
+        """
+        @param chrome: Chrome instance representing the browser in current test.
+        @param histogram: string, name of the histogram of interest.
+        @param time_out_s: int, max duration in secs to wait for specified
+                           histogram to be loaded.
+
+        """
+        # This pattern was built by observing the chrome://histogram output
+        self._histogram_pattern = ('Histogram.*([0-9]+)'
+                                   'samples.*average.*([0-9]+\.[0-9]+)')
+
+        self._bucket_pattern = '(^[0-9]+).*\(([0-9]+)'
+
+        """
+        Match counts are based on the text that needs to be parsed.
+        E.g: "0   ---------------------------O (9 = 16.4%)" is a typical entry
+        in the list of buckets. In this case we want to match 0 and 9,
+        therefore the match count is 2.
+
+        """
+
+        self._histogram_match_count = 2
+        self._bucket_match_count = 2
+
+        self._histogram = histogram
+        self._time_out_s = time_out_s
+        self._raw_text = None
+        self._sample_count = None
+        self._average = None
+        self._buckets = {}
+        self.tab = chrome.browser.tabs.New()
+        self.wait_for_histogram_loaded()
+        self.parse()
+
+
+    @property
+    def buckets(self):
+        """
+        @returns the dictionary containing buckets and their values.
+
+        """
+        return self._buckets
+
+
+    @property
+    def sample_count(self):
+        """
+        @returns the count of all samples in histogram as int.
+
+        """
+        return self._sample_count
+
+
+    @property
+    def average(self):
+        """
+        @returns the average of bucket values as float.
+
+        """
+        return self._average
+
+
+    def wait_for_histogram_loaded(self):
+        """
+        Uses js to poll doc content until valid content is retrieved.
+
+        """
+        def loaded():
+            """
+            Checks if the histogram page has been fully loaded.
+
+            """
+
+            self.tab.Navigate('chrome://histograms/%s' % self._histogram)
+            self.tab.WaitForDocumentReadyStateToBeComplete()
+            docEle = 'document.documentElement'
+            self._raw_text = self.tab.EvaluateJavaScript(
+                    "{0} && {0}.innerText".format(docEle))
+            return self._histogram in self._raw_text
+
+        msg = "%s not loaded. Waited %ss" % (self._histogram, self._time_out_s)
+
+        utils.poll_for_condition(condition=loaded,
+                                 exception=error.TestError(msg),
+                                 sleep_interval=1)
+
+    def parse(self):
+        """
+        Parses histogram text to retrieve useful properties.
+
+        @raises whatever _check_match() raises.
+
+        """
+
+        histogram_entries = self._raw_text.split('\n')
+        found_hist_title = False
+
+        for entry in histogram_entries:
+            matches = self._check_match(self._histogram_pattern,
+                                        entry,
+                                        self._histogram_match_count)
+
+            if matches:
+                if not found_hist_title:
+                    self._sample_count = int(matches[0])
+                    self._average = matches[1]
+                    found_hist_title = True
+
+                else:  # this is another histogram, bail out
+                    return
+
+            else:
+                matches = self._check_match(self._bucket_pattern,
+                                            entry,
+                                            self._bucket_match_count)
+                if matches:
+                    self._buckets[int(matches[0])] = int(matches[1])
+
+        bucket_sum = sum(self._buckets.values())
+
+        for key, value in self._buckets.items():
+            percent = (float(value) / bucket_sum) * 100
+            percent = round(number=percent, ndigits=2)
+            self._buckets[key] = BucketStats(value, percent)
+
+
+    def _check_match(self, pattern, text, expected_match_count):
+        """
+        Checks if provided text contains a pattern and if so expected number of
+        matches is found.
+
+        @param pattern: string, regex pattern to search for.
+        @param text: string, text to search for patterns.
+        @param expected_match_count: int, number of matches expected.
+
+        @returns: tuple, match groups, none if no match was found.
+        @raises TestError if a match was found but number of matches is not
+                          equal to expected count.
+
+        """
+        m = re.match(pattern, text)
+
+        if not m:
+            return m
+
+        ln = len(m.groups())
+        if ln != expected_match_count:
+            msg = ('Expected %d matches. Got %d. Pattern: %s. Text: %s'
+                   % (expected_match_count, ln, pattern, text))
+            raise error.TestError(msg)
+
+        return m.groups()
+
+
+    def __str__(self):
+        return ("Histogram name: %s. Buckets: %s"
+                % (self._histogram, str(self._buckets)))
\ No newline at end of file
diff --git a/client/site_tests/video_ChromeHWDecodeUsed/video_ChromeHWDecodeUsed.py b/client/site_tests/video_ChromeHWDecodeUsed/video_ChromeHWDecodeUsed.py
index 415110c..a5929f7 100755
--- a/client/site_tests/video_ChromeHWDecodeUsed/video_ChromeHWDecodeUsed.py
+++ b/client/site_tests/video_ChromeHWDecodeUsed/video_ChromeHWDecodeUsed.py
@@ -2,12 +2,14 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-from autotest_lib.client.bin import test, utils
+from autotest_lib.client.bin import test
 from autotest_lib.client.common_lib import error
 from autotest_lib.client.common_lib.cros import chrome
+from autotest_lib.client.cros.video import histogram_parser
 
 
 MEDIA_GVD_INIT_STATUS = 'Media.GpuVideoDecoderInitializeStatus'
+MEDIA_GVD_BUCKET = 0
 
 
 class video_ChromeHWDecodeUsed(test.test):
@@ -26,25 +28,12 @@
             tab1.WaitForDocumentReadyStateToBeComplete()
 
             # Waits for histogram updated for the test video.
-            tab2 = cr.browser.tabs.New()
+            parser = histogram_parser.HistogramParser(cr, MEDIA_GVD_INIT_STATUS)
 
-            def search_histogram_text(text):
-                """Searches the histogram text in the second tab.
+            buckets = parser.buckets
 
-                @param text: Text to be searched in the histogram tab.
-                """
-                return tab2.EvaluateJavaScript('document.documentElement && '
-                         'document.documentElement.innerText.search('
-                         '\'%s\') != -1' % text)
+            if (not buckets or not buckets[MEDIA_GVD_BUCKET]
+                    or buckets[MEDIA_GVD_BUCKET].percent < 100.0):
 
-            def gpu_histogram_loaded():
-                """Loads the histogram in the second tab."""
-                tab2.Navigate('chrome://histograms/%s' % MEDIA_GVD_INIT_STATUS)
-                return search_histogram_text(MEDIA_GVD_INIT_STATUS)
-
-            utils.poll_for_condition(gpu_histogram_loaded,
-                    exception=error.TestError(
-                            'Histogram gpu status failed to load.'),
-                            sleep_interval=1)
-            if not search_histogram_text('average = 0.0'):
-                raise error.TestError('Video decode acceleration not working.')
+                raise error.TestError('%s not found or not at 100 percent. %s'
+                                      % MEDIA_GVD_BUCKET, str(parser))
\ No newline at end of file
diff --git a/client/site_tests/video_ChromeRTCHWDecodeUsed/video_ChromeRTCHWDecodeUsed.py b/client/site_tests/video_ChromeRTCHWDecodeUsed/video_ChromeRTCHWDecodeUsed.py
index 6c5903d..948f922 100755
--- a/client/site_tests/video_ChromeRTCHWDecodeUsed/video_ChromeRTCHWDecodeUsed.py
+++ b/client/site_tests/video_ChromeRTCHWDecodeUsed/video_ChromeRTCHWDecodeUsed.py
@@ -5,13 +5,12 @@
 from contextlib import closing
 import logging
 import os
-import re
-import time
 import urllib2
 
-from autotest_lib.client.bin import test, utils
+from autotest_lib.client.bin import test
 from autotest_lib.client.common_lib import error
 from autotest_lib.client.common_lib.cros import chrome
+from autotest_lib.client.cros.video import histogram_parser
 
 
 # Chrome flags to use fake camera and skip camera permission.
@@ -25,6 +24,7 @@
 RTC_VIDEO_DECODE_BUCKET = 1
 HISTOGRAMS_URL = 'chrome://histograms/'
 
+
 class video_ChromeRTCHWDecodeUsed(test.test):
     """The test verifies HW Encoding for WebRTC video."""
     version = 1
@@ -38,7 +38,7 @@
         """
         tab = cr.browser.tabs[0]
         tab.Navigate(cr.browser.http_server.UrlOf(
-                os.path.join(self.bindir, 'loopback.html')))
+            os.path.join(self.bindir, 'loopback.html')))
         tab.WaitForDocumentReadyStateToBeComplete()
 
 
@@ -50,31 +50,15 @@
 
         @raises error.TestError if decoding is not hardware accelerated.
         """
-        tab = cr.browser.tabs.New()
-        def histograms_loaded(histogram):
-            """Returns true if histogram is loaded."""
-            tab.Navigate(HISTOGRAMS_URL + histogram)
-            tab.WaitForDocumentReadyStateToBeComplete()
-            return tab.EvaluateJavaScript(
-                    'document.documentElement.innerText.search("%s") != -1'
-                    % histogram)
+        parser = histogram_parser.HistogramParser(cr.browser.tabs.New(),
+                                                  RTC_VIDEO_DECODE)
+        buckets = parser.buckets
 
-        def histogram_sucess(histogram, bucket):
-            lines = tab.EvaluateJavaScript('document.documentElement.innerText')
-            logging.info(lines)
-            re_string = '^'+ str(bucket) +'\s\s-(.*)100.0%(.*)'
-            if not re.findall(re_string, lines, re.MULTILINE):
-                raise error.TestError(
-                        '{0} didn\'t show up or is not 100%'
-                        ' successful.'.format(histogram))
+        if (not buckets or not buckets[RTC_VIDEO_DECODE_BUCKET]
+                or buckets[RTC_VIDEO_DECODE_BUCKET].percent < 100.0):
 
-        utils.poll_for_condition(
-                lambda: histograms_loaded(RTC_VIDEO_DECODE),
-                timeout=5,
-                exception=error.TestError('Cannot find %s histogram.' %
-                                          RTC_VIDEO_DECODE),
-                sleep_interval=1)
-        histogram_sucess(RTC_VIDEO_DECODE, RTC_VIDEO_DECODE_BUCKET)
+            raise error.TestError('%s not found or not at 100 percent. %s'
+                                  % RTC_VIDEO_DECODE, str(parser))
 
 
     def run_once(self):
diff --git a/client/site_tests/video_ChromeVidResChangeHWDecode/video_ChromeVidResChangeHWDecode.py b/client/site_tests/video_ChromeVidResChangeHWDecode/video_ChromeVidResChangeHWDecode.py
index 7192a74..c1864f2 100755
--- a/client/site_tests/video_ChromeVidResChangeHWDecode/video_ChromeVidResChangeHWDecode.py
+++ b/client/site_tests/video_ChromeVidResChangeHWDecode/video_ChromeVidResChangeHWDecode.py
@@ -8,9 +8,11 @@
 from autotest_lib.client.bin import test, utils
 from autotest_lib.client.common_lib import error
 from autotest_lib.client.common_lib.cros import chrome
+from autotest_lib.client.cros.video import histogram_parser
 
 
 MEDIA_GVD_INIT_STATUS = 'Media.GpuVideoDecoderInitializeStatus'
+MEDIA_GVD_BUCKET = 0
 
 
 class video_ChromeVidResChangeHWDecode(test.test):
@@ -33,28 +35,15 @@
                 'loadVideo("%s")' % (video_file))
 
             # Waits for histogram updated for the test video.
-            tab2 = cr.browser.tabs.New()
+            parser = histogram_parser.HistogramParser(cr.browser.tabs.New(),
+                                                      MEDIA_GVD_INIT_STATUS)
+            buckets = parser.buckets
 
-            def search_histogram_text(text):
-                """Searches the histogram text in the second tab.
+            if (not buckets or not buckets[MEDIA_GVD_BUCKET]
+                    or buckets[MEDIA_GVD_BUCKET].percent < 100.0):
 
-                @param text: Text to be searched in the histogram tab.
-                """
-                return tab2.EvaluateJavaScript('document.documentElement && '
-                         'document.documentElement.innerText.search('
-                         '\'%s\') != -1' % text)
-
-            def gpu_histogram_loaded():
-                """Loads the histogram in the second tab."""
-                tab2.Navigate('chrome://histograms/%s' % MEDIA_GVD_INIT_STATUS)
-                return search_histogram_text(MEDIA_GVD_INIT_STATUS)
-
-            utils.poll_for_condition(gpu_histogram_loaded,
-                    exception=error.TestError(
-                            'Histogram gpu status failed to load.'),
-                            sleep_interval=1)
-            if not search_histogram_text('average = 0.0'):
-                raise error.TestError('Video decode acceleration not working.')
+                raise error.TestError('%s not found or not at 100 percent. %s'
+                                      % MEDIA_GVD_BUCKET, str(parser))
 
             # Verify the video playback.
             for i in range(1, video_len/2):
@@ -66,7 +55,6 @@
             # Verify that video ends successfully.
             utils.poll_for_condition(
                     lambda: tab1.EvaluateJavaScript('testvideo.ended'),
-                    timeout=video_len - video_len/2,
-                    exception=error.TestError(
-                            'Video didn\'t end successfully.'),
+                    timeout=video_len,
+                    exception=error.TestError('Video did not end successfully'),
                     sleep_interval=1)