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()