Skip to content

Commit

Permalink
Adding Sync implementation (#87)
Browse files Browse the repository at this point in the history
Co-authored-by: Chinmay Maheshwari <chinmay.m@browserstack.com>
  • Loading branch information
prklm10 and chinmay-browserstack authored Jan 31, 2024
1 parent 57b91bd commit bf9aad1
Show file tree
Hide file tree
Showing 10 changed files with 72 additions and 29 deletions.
6 changes: 4 additions & 2 deletions percy/lib/app_percy.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@ def screenshot(self, name: str, **kwargs):
orientation = kwargs.get('orientation')
if orientation and not isinstance(orientation, str):
raise TypeError('Argument orientation should be a string and portrait/landscape')
self.provider.screenshot(name, **kwargs)
return None
sync = kwargs.get('sync')
if sync and not isinstance(sync, bool):
raise TypeError('Argument sync should be a boolean')
return self.provider.screenshot(name, **kwargs)
15 changes: 8 additions & 7 deletions percy/lib/cli_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ def is_percy_enabled():
log(e, on_debug=True)
return False

def post_screenshots(self, name, tag, tiles, external_debug_url=None, ignored_elements_data=None, considered_elements_data=None):
body = self._request_body(name, tag, tiles, external_debug_url, ignored_elements_data, considered_elements_data)
def post_screenshots(self, name, tag, tiles, external_debug_url=None, ignored_elements_data=None, considered_elements_data=None, sync=None):
body = self._request_body(name, tag, tiles, external_debug_url, ignored_elements_data, considered_elements_data, sync)

body['client_info'] = Environment._get_client_info()
body['environment_info'] = Environment._get_env_info()

response = requests.post(f'{PERCY_CLI_API}/percy/comparison', json=body, timeout=60)
response = requests.post(f'{PERCY_CLI_API}/percy/comparison', json=body, timeout=600)
# Handle errors
response.raise_for_status()
data = response.json()
Expand Down Expand Up @@ -90,23 +90,24 @@ def post_poa_screenshots(self, name, session_id, command_executor_url, capabilit
body['client_info'] = Environment._get_client_info()
body['environment_info'] = Environment._get_env_info()

response = requests.post(f'{PERCY_CLI_API}/percy/automateScreenshot', json=body, timeout=30)
response = requests.post(f'{PERCY_CLI_API}/percy/automateScreenshot', json=body, timeout=600)
# Handle errors
response.raise_for_status()
data = response.json()

if response.status_code != 200:
raise CLIException(data.get('error', 'UnknownException'))
return data
return data.get('data', {})

@staticmethod
def _request_body(name, tag, tiles, external_debug_url, ignored_elements_data, considered_elements_data):
def _request_body(name, tag, tiles, external_debug_url, ignored_elements_data, considered_elements_data, sync):
tiles = list(map(dict, tiles))
return {
"name": name,
"tag": tag,
"tiles": tiles,
"ignored_elements_data": ignored_elements_data,
"external_debug_url": external_debug_url,
"considered_elements_data": considered_elements_data
"considered_elements_data": considered_elements_data,
"sync": sync
}
2 changes: 1 addition & 1 deletion percy/lib/percy_automate.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def screenshot(self, name: str, **kwargs):
options.pop(IGNORE_ELEMENT_KEY, None)
options.pop(CONSIDER_ELEMENT_KEY, None)

CLIWrapper().post_poa_screenshots(
return CLIWrapper().post_poa_screenshots(
name,
metadata.session_id,
metadata.command_executor_url,
Expand Down
13 changes: 9 additions & 4 deletions percy/providers/app_automate.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def supports(remote_url) -> bool:

def screenshot(self, name: str, **kwargs):
session_details = self.execute_percy_screenshot_begin(name)
sync = kwargs.get('sync', None)
# Device name and OS version retrieval is custom for App Automate users
if session_details is not None:
self.metadata._device_name = kwargs.get('device_name') or session_details.get("deviceName")
Expand All @@ -24,9 +25,11 @@ def screenshot(self, name: str, **kwargs):
try:
response = super().screenshot(name, **kwargs)
percy_screenshot_url = response.get('link', '')
self.execute_percy_screenshot_end(name, percy_screenshot_url, 'success')
self.execute_percy_screenshot_end(name, percy_screenshot_url, 'success', sync)
data = response.get('data', {})
return data
except Exception as e:
self.execute_percy_screenshot_end(name, percy_screenshot_url, 'failure', str(e))
self.execute_percy_screenshot_end(name, percy_screenshot_url, 'failure', sync, str(e))
raise e

def set_debug_url(self, session_details):
Expand Down Expand Up @@ -90,15 +93,17 @@ def execute_percy_screenshot_begin(self, name):
log(e, on_debug=True)
return None

def execute_percy_screenshot_end(self, name, percy_screenshot_url, status, status_message=None):
def execute_percy_screenshot_end(self, name, percy_screenshot_url, status, sync, status_message=None):
try:
request_body = {
'action': 'percyScreenshot',
'arguments': {
'state': 'end',
'percyScreenshotUrl': percy_screenshot_url,
'name': name,
'status': status }
'status': status,
'sync': sync
}
}
if status_message: request_body['arguments']['statusMessage'] = status_message
command = f'browserstack_executor: {json.dumps(request_body)}'
Expand Down
8 changes: 4 additions & 4 deletions percy/providers/generic_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ def screenshot(self, name, **kwargs):
custom_locations = kwargs.get("custom_consider_regions", [])
)
}

return self._post_screenshots(name, tag, tiles, self.get_debug_url(), ignore_regions, consider_regions)
sync = kwargs.get("sync", None)
return self._post_screenshots(name, tag, tiles, self.get_debug_url(), ignore_regions, consider_regions, sync)

def _get_tag(self, **kwargs):
name = kwargs.get('device_name', self.metadata.device_name)
Expand Down Expand Up @@ -156,8 +156,8 @@ def get_regions_by_location(self, elements_array, custom_locations):
log(f"Values passed in custom ignored region at index: {idx} is not valid")

@staticmethod
def _post_screenshots(name, tag, tiles, debug_url, ignored_regions, considered_regions):
response = CLIWrapper().post_screenshots(name, tag, tiles, debug_url, ignored_regions, considered_regions)
def _post_screenshots(name, tag, tiles, debug_url, ignored_regions, considered_regions, sync):
response = CLIWrapper().post_screenshots(name, tag, tiles, debug_url, ignored_regions, considered_regions, sync)
return response

def _write_screenshot(self, png_bytes, directory):
Expand Down
8 changes: 4 additions & 4 deletions tests/test_app_automate.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def test_app_automate_execute_percy_screenshot_begin(self, _mocked_log):

def test_app_automate_execute_percy_screenshot_end(self):
self.mock_webdriver.execute_script.return_value = {}
self.assertIsNone(self.app_automate.execute_percy_screenshot_end('Screenshot 1', self.comparison_response['link'], 'success'))
self.assertIsNone(self.app_automate.execute_percy_screenshot_end('Screenshot 1', self.comparison_response['link'], 'success', False))
self.mock_webdriver.execute_script.assert_called()

def test_app_automate_execute_percy_screenshot(self):
Expand All @@ -62,7 +62,7 @@ def test_app_automate_execute_percy_screenshot(self):
@patch('percy.providers.app_automate.log')
def test_execute_percy_screenshot_end_throws_error(self, mock_log):
self.mock_webdriver.execute_script.side_effect = Exception('SomeException')
self.app_automate.execute_percy_screenshot_end('Screenshot 1', 'snapshot-url', 'success')
self.app_automate.execute_percy_screenshot_end('Screenshot 1', 'snapshot-url', 'success', None)
mock_log.assert_called()

@patch.object(Metadata, 'session_id', PropertyMock(return_value='unique_session_id'))
Expand All @@ -72,7 +72,7 @@ def test_execute_percy_screenshot_end(self):
mock_screenshot_end = MagicMock(return_value=None)
self.app_automate.execute_percy_screenshot_end = mock_screenshot_end
self.app_automate.screenshot('name')
mock_screenshot_end.assert_called_once_with('name', 'https://link', 'success')
mock_screenshot_end.assert_called_once_with('name', 'https://link', 'success', None)

# check that code doesnt throw if begin fails
self.app_automate.execute_percy_screenshot_begin = MagicMock(return_value=None)
Expand All @@ -81,7 +81,7 @@ def test_execute_percy_screenshot_end(self):
with self.assertRaises(Exception) as e:
mock_screenshot_end.side_effect = Exception('RandomException')
self.app_automate.screenshot('name')
mock_screenshot_end.assert_called_with('name', 'https://link', 'failure', str(e.exception))
mock_screenshot_end.assert_called_with('name', 'https://link', 'failure', None, str(e.exception))

@patch.object(AppAutomate, 'execute_percy_screenshot', MagicMock(return_value={
"result":"[{\"sha\":\"sha-25568755\",\"status_bar\":null,\"nav_bar\":null,\"header_height\":120,\"footer_height\":80,\"index\":0}]"
Expand Down
19 changes: 19 additions & 0 deletions tests/test_app_percy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

class TestAppPercy(unittest.TestCase):
comparison_response = {'link': 'https://snapshot_url', 'success': True}
sync_response = {'link': 'https://snapshot_url', 'data': 'sync-data', 'success': True}

@patch('appium.webdriver.webdriver.WebDriver')
def setUp(self, mock_webdriver):
Expand Down Expand Up @@ -171,3 +172,21 @@ def test_invalid_driver(self):
with self.assertRaises(Exception) as e:
AppPercy(Mock())
self.assertIsInstance(e.exception, DriverNotSupported)

@patch.object(CLIWrapper, 'post_screenshots', MagicMock(return_value=sync_response))
@patch.object(GenericProvider, '_write_screenshot', MagicMock(return_value='path-to-png-file'))
@patch.object(AppAutomate, 'execute_percy_screenshot_begin', MagicMock(return_value={'deviceName': 'Google Pixel 4',
'osVersion': '12.0',
'buildHash': 'abc',
'sessionHash': 'def'
}))
@patch.object(AppAutomate, 'execute_percy_screenshot_end', MagicMock(return_value=None))
@patch.object(Metadata, 'session_id', PropertyMock(return_value='unique_session_id'))
@patch.dict(os.environ, {"PERCY_DISABLE_REMOTE_UPLOADS": "true"})
def test_app_percy_with_sync(self):
with patch('percy.metadata.AndroidMetadata.remote_url', new_callable=PropertyMock) as mock_remote_url:
mock_remote_url.return_value = 'url-of-browserstack-cloud'
app_percy = AppPercy(self.mock_android_webdriver)
self.assertEqual(app_percy.screenshot('screenshot 1', sync = True), 'sync-data')
self.assertTrue(isinstance(app_percy.metadata, AndroidMetadata))
self.assertTrue(isinstance(app_percy.provider, AppAutomate))
6 changes: 4 additions & 2 deletions tests/test_cli_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def test_request_body(self):
name = "some-name"
debug_url = "debug-url"
response = self.cli_wrapper._request_body(
name, tag, [tile], debug_url, self.ignored_elements_data, self.considered_elements_data
name, tag, [tile], debug_url, self.ignored_elements_data, self.considered_elements_data, False
)
self.assertEqual(response["name"], name)
self.assertEqual(response["external_debug_url"], debug_url)
Expand All @@ -118,6 +118,7 @@ def test_request_body(self):
self.assertDictEqual(
response["considered_elements_data"], self.considered_elements_data
)
self.assertEqual(response["sync"], False)

def test_request_body_when_optional_values_are_null(self):
tile = Tile("some-file-path", 10, 10, 20, 20)
Expand All @@ -127,10 +128,11 @@ def test_request_body_when_optional_values_are_null(self):
ignored_elements_data = None
considered_elements_data = None
response = self.cli_wrapper._request_body(
name, tag, [tile], debug_url, ignored_elements_data, considered_elements_data
name, tag, [tile], debug_url, ignored_elements_data, considered_elements_data, True
)
self.assertEqual(response["name"], name)
self.assertEqual(response["external_debug_url"], debug_url)
self.assertDictEqual(response["tag"], tag)
self.assertListEqual(response["tiles"], [dict(tile)])
self.assertEqual(response["ignored_elements_data"], None)
self.assertEqual(response["sync"], True)
2 changes: 1 addition & 1 deletion tests/test_generic_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def test_post_screenshots(self):
tag = self.generic_provider._get_tag()
tiles = self.generic_provider._get_tiles()
response = self.generic_provider._post_screenshots(
"screenshot 1", tag, tiles, "", [], []
"screenshot 1", tag, tiles, "", [], [], False
)
self.assertEqual(response, self.comparison_response)

Expand Down
22 changes: 18 additions & 4 deletions tests/test_screenshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@ def mock_screenshot(fail=False):
status=(500 if fail else 200),
)

def mock_poa_screenshot(fail=False):
def mock_poa_screenshot(fail=False, sync=False):
body = '{ "success": ' + ("true" if not fail else 'false, "error": "test"') + "}"
if sync: body = '{ "success": "true", "data": "sync-data" }'
httpretty.register_uri(
httpretty.POST,
"http://localhost:5338/percy/automateScreenshot",
body=(
'{ "success": ' + ("true" if not fail else 'false, "error": "test"') + "}"
),
body=body,
status=(500 if fail else 200),
)

Expand Down Expand Up @@ -287,5 +287,19 @@ def test_posts_screenshot_poa(self):
self.assertEqual(s2['options']['ignore_region_elements'], ['Dummy_id'])
self.assertEqual(s2['options']['consider_region_elements'], ['Consider_Dummy_id'])

def test_posts_screenshot_poa_with_sync(self):
mock_healthcheck(type="automate")
mock_poa_screenshot(False, True)

driver = Mock(spec=WebDriver)
driver.session_id = 'Dummy_session_id'
driver.capabilities = { 'key': 'value' }
driver.desired_capabilities = { 'key': 'value' }
driver.command_executor = Mock()
driver.command_executor._url = 'https://hub-cloud.browserstack.com/wd/hub'
self.mock_webdriver.capabilities = { 'key': 'value' }

self.assertEqual(percy_screenshot(driver, 'Snapshot 3', options = {'sync': True}), 'sync-data')

if __name__ == "__main__":
unittest.main()

0 comments on commit bf9aad1

Please sign in to comment.