github.com/apache/beam/sdks/v2@v2.48.2/python/apache_beam/runners/interactive/testing/integration/screen_diff.py (about)

     1  #
     2  # Licensed to the Apache Software Foundation (ASF) under one or more
     3  # contributor license agreements.  See the NOTICE file distributed with
     4  # this work for additional information regarding copyright ownership.
     5  # The ASF licenses this file to You under the Apache License, Version 2.0
     6  # (the "License"); you may not use this file except in compliance with
     7  # the License.  You may obtain a copy of the License at
     8  #
     9  #    http://www.apache.org/licenses/LICENSE-2.0
    10  #
    11  # Unless required by applicable law or agreed to in writing, software
    12  # distributed under the License is distributed on an "AS IS" BASIS,
    13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  # See the License for the specific language governing permissions and
    15  # limitations under the License.
    16  #
    17  
    18  """Module to conduct screen diff based notebook integration tests."""
    19  
    20  # pytype: skip-file
    21  
    22  import os
    23  import platform
    24  import threading
    25  import unittest
    26  from http.server import HTTPServer
    27  from http.server import SimpleHTTPRequestHandler
    28  
    29  import pytest
    30  
    31  from apache_beam.runners.interactive import interactive_environment as ie
    32  from apache_beam.runners.interactive.testing.integration import notebook_executor
    33  
    34  try:
    35    import chromedriver_binary  # pylint: disable=unused-import
    36    from needle.cases import NeedleTestCase
    37    from needle.driver import NeedleChrome
    38    from selenium.webdriver.chrome.options import Options
    39    from selenium.webdriver.common.by import By
    40    from selenium.webdriver.support import expected_conditions
    41    from selenium.webdriver.support.ui import WebDriverWait
    42    _interactive_integration_ready = (
    43        notebook_executor._interactive_integration_ready)
    44  except ImportError:
    45    _interactive_integration_ready = False
    46  
    47  # Web elements will be rendered differently on different platforms. List all
    48  # supported platforms with goldens here.
    49  _SUPPORTED_PLATFORMS = ['Darwin', 'Linux']
    50  
    51  
    52  class ScreenDiffIntegrationTestEnvironment(object):
    53    """A test environment to conduct screen diff integration tests for notebooks.
    54    """
    55    def __init__(self, test_notebook_path, golden_dir, cleanup=True):
    56      # type: (str, str, bool) -> None
    57  
    58      assert _interactive_integration_ready, (
    59          '[interactive_test] dependency is not installed.')
    60      assert os.path.exists(golden_dir), '{} does not exist.'.format(golden_dir)
    61      assert os.path.isdir(golden_dir), '{} is not a directory.'.format(
    62        golden_dir)
    63      self._golden_dir = golden_dir
    64      self._notebook_executor = notebook_executor.NotebookExecutor(
    65          test_notebook_path)
    66      self._cleanup = cleanup
    67      self._test_urls = {}
    68      self._server = None
    69  
    70    def __enter__(self):
    71      self._notebook_executor.execute()
    72      self._server = HTTPServer(('', 0), SimpleHTTPRequestHandler)
    73  
    74      def start_serving(server):
    75        server.serve_forever()
    76  
    77      threading.Thread(
    78          target=start_serving, args=[self._server], daemon=True).start()
    79  
    80      for test_id, output_path in\
    81        self._notebook_executor.output_html_paths.items():
    82        self._test_urls[test_id] = self.base_url + output_path
    83  
    84      return self
    85  
    86    def __exit__(self, exc_type, exc_value, traceback):
    87      if self._notebook_executor and self._cleanup:
    88        self._notebook_executor.cleanup()
    89      if self._server:
    90  
    91        def stop_serving(server):
    92          server.shutdown()
    93  
    94        threading.Thread(
    95            target=stop_serving, args=[self._server], daemon=True).start()
    96  
    97    @property
    98    def base_url(self):
    99      """The base url where the locally started server serving HTMLs generated by
   100      notebook executions."""
   101      assert self._server, 'Server has not started.'
   102      host_n_port = self._server.server_address
   103      return 'http://{}:{}/'.format(host_n_port[0], host_n_port[1])
   104  
   105    @property
   106    def test_urls(self):
   107      """Mapping from test_id/execution_id to urls serving the output HTML pages
   108      generated by the corresponding notebook executions."""
   109      return self._test_urls
   110  
   111    @property
   112    def notebook_path_to_test_id(self):
   113      """Mapping from input notebook paths to their obfuscated execution/test ids.
   114      """
   115      return self._notebook_executor.notebook_path_to_execution_id
   116  
   117  
   118  def should_skip():
   119    """Whether a screen diff test should be skipped."""
   120    return not (
   121        platform.system() in _SUPPORTED_PLATFORMS and
   122        ie.current_env().is_interactive_ready and _interactive_integration_ready)
   123  
   124  
   125  if should_skip():
   126  
   127    @unittest.skip(
   128        reason='[interactive] and [interactive_test] deps are both required.')
   129    @pytest.mark.skip(
   130        reason='[interactive] and [interactive_test] deps are both required.')
   131    class BaseTestCase(unittest.TestCase):
   132      """A skipped base test case if interactive_test dependency is not installed.
   133      """
   134      pass
   135  
   136  else:
   137  
   138    class BaseTestCase(NeedleTestCase):
   139      """A base test case to execute screen diff integration tests."""
   140      # Whether the browser should be headless.
   141      _headless = True
   142  
   143      def __init__(self, *args, **kwargs):
   144        """Initializes a test.
   145  
   146        Some kwargs that could be configured:
   147  
   148          #. golden_dir=<path>. A directory path pointing to all the golden
   149             screenshots as baselines for comparison.
   150          #. test_notebook_dir=<path>. A path pointing to a directory of
   151             notebook files in ipynb format.
   152          #. headless=<True/False>. Whether the browser should be headless when
   153             executing the tests.
   154          #. golden_size=<(int, int)>. The size of the screenshot to take and
   155             compare.
   156          #. cleanup=<True/False>. Whether to clean up the output directory.
   157             Should always be True in automated test environment. When debugging,
   158             turn it False to manually check the output for difference.
   159          #. threshold=<float>. An image difference threshold, when the image
   160             pixel distance is bigger than the value, the test will fail.
   161        """
   162        golden_root = kwargs.pop(
   163            'golden_dir',
   164            'apache_beam/runners/interactive/testing/integration/goldens')
   165        self._golden_dir = os.path.join(golden_root, platform.system())
   166        self._test_notebook_dir = kwargs.pop(
   167            'test_notebook_dir',
   168            'apache_beam/runners/interactive/testing/integration/test_notebooks')
   169        BaseTestCase._headless = kwargs.pop('headless', True)
   170        self._test_env = None
   171        self._viewport_width, self._viewport_height = kwargs.pop(
   172          'golden_size', (1024, 10000))
   173        self._cleanup = kwargs.pop('cleanup', True)
   174        self._threshold = kwargs.pop('threshold', 5000)
   175        self.baseline_directory = os.path.join(os.getcwd(), self._golden_dir)
   176        self.output_directory = os.path.join(
   177            os.getcwd(), self._test_notebook_dir, 'output')
   178        super().__init__(*args, **kwargs)
   179  
   180      @classmethod
   181      def get_web_driver(cls):
   182        chrome_options = Options()
   183        if cls._headless:
   184          chrome_options.add_argument('--headless')
   185        chrome_options.add_argument('--no-sandbox')
   186        chrome_options.add_argument('--disable-dev-shm-usage')
   187        chrome_options.add_argument('--force-color-profile=srgb')
   188        return NeedleChrome(options=chrome_options)
   189  
   190      def setUp(self):
   191        self.set_viewport_size(self._viewport_width, self._viewport_height)
   192  
   193      def run(self, result=None):
   194        with ScreenDiffIntegrationTestEnvironment(self._test_notebook_dir,
   195                                                  self._golden_dir,
   196                                                  self._cleanup) as test_env:
   197          self._test_env = test_env
   198          super().run(result)
   199  
   200      def explicit_wait(self):
   201        """Wait for common elements to be visible."""
   202        WebDriverWait(self.driver, 5).until(
   203            expected_conditions.visibility_of_element_located(
   204                (By.TAG_NAME, 'facets-overview')))
   205        WebDriverWait(self.driver, 5).until(
   206            expected_conditions.visibility_of_element_located(
   207                (By.TAG_NAME, 'facets-dive')))
   208  
   209      def assert_all(self):
   210        """Asserts screenshots for all notebooks in the test_notebook_path."""
   211        for test_id, test_url in self._test_env.test_urls.items():
   212          self.driver.get(test_url)
   213          self.explicit_wait()
   214          self.assertScreenshot('body', test_id, self._threshold)
   215  
   216      def assert_single(self, test_id):
   217        """Asserts the screenshot for a single test. The given test id will be the
   218        name of the golden screenshot."""
   219        test_url = self._test_env.test_urls.get(test_id, None)
   220        assert test_url, '{} is not a valid test id.'.format(test_id)
   221        self.driver.get(test_url)
   222        self.explicit_wait()
   223        self.assertScreenshot('body', test_id, self._threshold)
   224  
   225      def assert_notebook(self, notebook_name):
   226        """Asserts the screenshot for a single notebook. The notebook with the
   227        given notebook_name under test_notebook_dir will be executed and asserted.
   228        """
   229        if not notebook_name.endswith('.ipynb'):
   230          notebook_name += '.ipynb'
   231        notebook_path = os.path.join(self._test_notebook_dir, notebook_name)
   232        test_id = self._test_env.notebook_path_to_test_id.get(notebook_path, None)
   233        assert test_id, 'Cannot find notebook with name {}.'.format(notebook_name)
   234        self.assert_single(test_id)
   235  
   236  
   237  # This file contains no tests. Below lines are purely for passing lint.
   238  if __name__ == '__main__':
   239    unittest.main()