Skip to content
This repository was archived by the owner on Jan 19, 2024. It is now read-only.

Commit 951c546

Browse files
authored
Merge pull request #209 from edx/jmbowman/TE-2635
TE-2635 More logging on test failures, v0.8.0
2 parents 0c2cd16 + 7f3f5c2 commit 951c546

File tree

8 files changed

+134
-18
lines changed

8 files changed

+134
-18
lines changed

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ python:
33
- '2.7'
44
- '3.5'
55
- '3.6'
6-
- 'pypy'
76
sudo: false
87
branches:
98
only:

CHANGELOG

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
v0.8.0 (07/18/18)
2+
* Preserve geckodriver log for Firefox test failures
3+
* Better handling of screenshot and log directory settings
4+
* Log more information on page load failures
5+
* Python 3.6 support
6+
17
v0.7.3 (05/21/18)
28
* Modified wait for page logic
39

bok_choy/browser.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@
22
Use environment variables to configure Selenium remote WebDriver.
33
For use with SauceLabs (via SauceConnect) or local browsers.
44
"""
5-
from __future__ import absolute_import
5+
from __future__ import absolute_import, print_function
66

7-
import os
7+
import errno
88
import logging
9-
from json import dumps
9+
import os
1010
import socket
11-
import errno
11+
from json import dumps
12+
from shutil import copyfile
1213

1314
from needle.driver import (NeedleFirefox, NeedleChrome, NeedleIe,
1415
NeedleSafari, NeedlePhantomJS, NeedleOpera)
1516
from selenium import webdriver
1617
from selenium.common.exceptions import WebDriverException
17-
from selenium.webdriver.chrome.options import Options
18+
from selenium.webdriver.chrome.options import Options as ChromeOptions
1819
from selenium.webdriver.firefox.firefox_binary import FirefoxBinary
20+
from selenium.webdriver.firefox.options import Options as FirefoxOptions
1921

2022
from bok_choy.promise import Promise
2123

@@ -109,9 +111,13 @@ def save_screenshot(driver, name):
109111
None
110112
"""
111113
if hasattr(driver, 'save_screenshot'):
112-
image_name = os.path.join(
113-
os.environ.get('SCREENSHOT_DIR'), name + '.png'
114-
)
114+
screenshot_dir = os.environ.get('SCREENSHOT_DIR')
115+
if not screenshot_dir:
116+
LOGGER.warning('The SCREENSHOT_DIR environment variable was not set; not saving a screenshot')
117+
return
118+
elif not os.path.exists(screenshot_dir):
119+
os.makedirs(screenshot_dir)
120+
image_name = os.path.join(screenshot_dir, name + '.png')
115121
driver.save_screenshot(image_name)
116122

117123
else:
@@ -139,16 +145,28 @@ def save_driver_logs(driver, prefix):
139145
None
140146
"""
141147
browser_name = os.environ.get('SELENIUM_BROWSER', 'firefox')
142-
if browser_name == "firefox":
148+
log_dir = os.environ.get('SELENIUM_DRIVER_LOG_DIR')
149+
if not log_dir:
150+
LOGGER.warning('The SELENIUM_DRIVER_LOG_DIR environment variable was not set; not saving logs')
151+
return
152+
elif not os.path.exists(log_dir):
153+
os.makedirs(log_dir)
154+
if browser_name == 'firefox':
155+
# Firefox doesn't yet provide logs to Selenium, but does log to a separate file
156+
# https://github.com/mozilla/geckodriver/issues/284
157+
# https://firefox-source-docs.mozilla.org/testing/geckodriver/geckodriver/TraceLogs.html
158+
log_path = os.path.join(os.getcwd(), 'geckodriver.log')
159+
if os.path.exists(log_path):
160+
dest_path = os.path.join(log_dir, '{}_geckodriver.log'.format(prefix))
161+
copyfile(log_path, dest_path)
143162
return
144163

145164
log_types = ['browser', 'driver', 'client', 'server']
146165
for log_type in log_types:
147166
try:
148167
log = driver.get_log(log_type)
149168
file_name = os.path.join(
150-
os.environ.get('SELENIUM_DRIVER_LOG_DIR'), '{}_{}.log'.format(
151-
prefix, log_type)
169+
log_dir, '{}_{}.log'.format(prefix, log_type)
152170
)
153171
with open(file_name, 'w') as output_file:
154172
for line in log:
@@ -345,9 +363,17 @@ def _local_browser_class(browser_name):
345363
name=browser_name, options=", ".join(list(BROWSERS.keys()))))
346364
else:
347365
if browser_name == 'firefox':
366+
# Remove geckodriver log data from previous test cases
367+
log_path = os.path.join(os.getcwd(), 'geckodriver.log')
368+
if os.path.exists(log_path):
369+
os.remove(log_path)
370+
371+
firefox_options = FirefoxOptions()
372+
firefox_options.log.level = 'trace'
348373
browser_args = []
349374
browser_kwargs = {
350375
'firefox_profile': _firefox_profile(),
376+
'options': firefox_options,
351377
}
352378

353379
firefox_path = os.environ.get('SELENIUM_FIREFOX_PATH')
@@ -367,7 +393,7 @@ def _local_browser_class(browser_name):
367393
})
368394

369395
elif browser_name == 'chrome':
370-
chrome_options = Options()
396+
chrome_options = ChromeOptions()
371397

372398
# Emulate webcam and microphone for testing purposes
373399
chrome_options.add_argument('--use-fake-device-for-media-stream')

bok_choy/page_object.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ def visit(self):
328328
try:
329329
self.browser.get(self.url)
330330
except (WebDriverException, socket.gaierror):
331+
LOGGER.warning(u"Unexpected page load exception:", exc_info=True)
331332
raise PageLoadError("Could not load page '{!r}' at URL '{}'".format(
332333
self, self.url
333334
))
@@ -361,7 +362,11 @@ def validate_url(cls, url):
361362
result = urllib_parse.urlsplit(url)
362363

363364
# Check that we have a protocol and hostname
364-
if not result.scheme or not result.netloc:
365+
if not result.scheme:
366+
LOGGER.warning(u"%s is missing a protocol", url)
367+
return False
368+
if not result.netloc:
369+
LOGGER.warning(u"%s is missing a hostname", url)
365370
return False
366371

367372
# Check that the port is an integer
@@ -370,8 +375,10 @@ def validate_url(cls, url):
370375
int(result.port)
371376
elif result.netloc.endswith(':'):
372377
# Valid URLs do not end with colons.
378+
LOGGER.warning(u"%s has a colon after the hostname but no port", url)
373379
return False
374380
except ValueError:
381+
LOGGER.warning(u"%s uses an invalid port", url)
375382
return False
376383
else:
377384
return True
@@ -440,7 +447,7 @@ def _is_document_ready():
440447
"return document.readyState=='complete'")
441448

442449
try:
443-
# Wait for page to laod completely i.e. for document.readyState to become complete
450+
# Wait for page to load completely i.e. for document.readyState to become complete
444451
EmptyPromise(
445452
_is_document_ready,
446453
"The document and all sub-resources have finished loading.",

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import codecs
44
from setuptools import setup
55

6-
VERSION = '0.7.3'
6+
VERSION = '0.8.0'
77
DESCRIPTION = 'UI-level acceptance test framework'
88

99

tests/test_browser.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,23 @@ def test_save_screenshot(self):
207207
# Check that the file is not empty
208208
assert os.stat(expected_file).st_size > 100
209209

210+
def test_save_screenshot_dir_not_set(self, caplog, monkeypatch):
211+
browser = self.browser
212+
monkeypatch.delenv('SCREENSHOT_DIR')
213+
bok_choy.browser.save_screenshot(browser, 'empty')
214+
assert 'The SCREENSHOT_DIR environment variable was not set; not saving a screenshot' in caplog.text
215+
210216
def test_save_screenshot_unsupported(self, caplog):
211217
browser = 'Some driver without save_screenshot()'
212218
bok_choy.browser.save_screenshot(browser, 'button_page')
213219
assert 'Browser does not support screenshots.' in caplog.text
214220

221+
def test_save_driver_logs_dir_not_set(self, caplog, monkeypatch):
222+
browser = self.browser
223+
monkeypatch.delenv('SELENIUM_DRIVER_LOG_DIR')
224+
bok_choy.browser.save_driver_logs(browser, 'empty')
225+
assert 'The SELENIUM_DRIVER_LOG_DIR environment variable was not set; not saving logs' in caplog.text
226+
215227
def test_save_driver_logs_unsupported(self):
216228
browser = self.browser
217229
tempdir_path = self.tempdir_path

tests/test_page_object.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,23 @@
44

55
from __future__ import absolute_import
66

7+
import logging
78
from unittest import TestCase
89

10+
import pytest
911
from mock import Mock
12+
from selenium.common.exceptions import WebDriverException
1013

1114
from bok_choy.page_object import PageObject, PageLoadError, unguarded, WrongPageError
1215
from bok_choy.promise import BrokenPromise
13-
from tests.pages import SitePage
16+
from tests.pages import ButtonPage, SitePage
17+
18+
19+
class InvalidPortPage(SitePage):
20+
"""
21+
Create a page that will return a URL with an invalid port.
22+
"""
23+
url = "http://localhost:8o/invalid"
1424

1525

1626
class InvalidURLPage(SitePage):
@@ -20,11 +30,19 @@ class InvalidURLPage(SitePage):
2030
url = "http://localhost:/invalid"
2131

2232

33+
class MissingHostnamePage(SitePage):
34+
"""
35+
Create a page that will return a URL with no hostname.
36+
"""
37+
url = "http:///invalid"
38+
39+
2340
class NeverOnPage(SitePage):
2441
"""
2542
Create a page that never successfully loads.
2643
"""
2744
url = "http://localhost/never_on"
45+
class_attr = ButtonPage
2846

2947
def is_browser_on_page(self):
3048
return False
@@ -129,3 +147,51 @@ def test_visit_timeout(self):
129147
# If the page doesn't load before the timeout, PageLoadError is raised
130148
with self.assertRaises(PageLoadError):
131149
NeverOnPage(Mock()).visit()
150+
151+
152+
def test_invalid_port_exception(caplog):
153+
with pytest.raises(PageLoadError):
154+
InvalidPortPage(Mock()).visit()
155+
assert u'uses an invalid port' in caplog.text
156+
157+
158+
def test_missing_hostname_exception(caplog):
159+
with pytest.raises(PageLoadError):
160+
MissingHostnamePage(Mock()).visit()
161+
assert u'is missing a hostname' in caplog.text
162+
163+
164+
def test_never_loads(caplog):
165+
attrs = {'execute_script.return_value': False}
166+
browser = Mock(**attrs)
167+
page = ButtonPage(browser)
168+
with pytest.raises(BrokenPromise):
169+
page.wait_for_page(timeout=1)
170+
assert u'document.readyState does not become complete for following url' in caplog.text
171+
172+
173+
def test_page_load_exception(caplog):
174+
attrs = {'get.side_effect': WebDriverException('Boom!')}
175+
browser = Mock(**attrs)
176+
page = ButtonPage(browser)
177+
with pytest.raises(PageLoadError):
178+
page.visit()
179+
assert u'Unexpected page load exception' in caplog.text
180+
181+
182+
def test_retry_errors(caplog):
183+
def promise_check_func():
184+
"""
185+
Check function which continuously fails with a WebDriverException until timeout
186+
"""
187+
raise WebDriverException('Boom!')
188+
page = ButtonPage(Mock())
189+
with pytest.raises(BrokenPromise):
190+
page.wait_for(promise_check_func, 'Never succeeds', timeout=1)
191+
assert u'Exception ignored during retry loop' in caplog.text
192+
193+
194+
def test_warning(caplog):
195+
page = SitePage(Mock())
196+
page.warning(u'Scary stuff')
197+
assert ('SitePage', logging.WARN, u'Scary stuff') in caplog.record_tuples

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ commands =
2727
pycodestyle tests
2828
mkdir -p $SELENIUM_DRIVER_LOG_DIR
2929
rm -rf $SELENIUM_DRIVER_LOG_DIR/*
30-
py.test {posargs:tests} --durations=10
30+
pytest {posargs:tests} --durations=10
3131

3232
[testenv:doc]
3333
deps =

0 commit comments

Comments
 (0)