browser.py 10.1 KB
Newer Older
Will Daly committed
1 2 3 4
"""
Browser set up for acceptance tests.
"""

5
# pylint: disable=no-member
6
# pylint: disable=unused-argument
Will Daly committed
7

8 9 10
from lettuce import before, after, world
from splinter.browser import Browser
from logging import getLogger
11 12
from django.core.management import call_command
from django.conf import settings
13
from selenium.common.exceptions import WebDriverException
14
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
15
import requests
16 17
from base64 import encodestring
from json import dumps
18

19
import xmodule.modulestore.django
20
from xmodule.contentstore.django import _CONTENTSTORE
21

Will Daly committed
22 23
LOGGER = getLogger(__name__)
LOGGER.info("Loading the lettuce acceptance testing terrain file...")
Calen Pennington committed
24

25
MAX_VALID_BROWSER_ATTEMPTS = 20
26
GLOBAL_SCRIPT_TIMEOUT = 60
27

28

29
def get_saucelabs_username_and_key():
30 31 32 33
    """
    Returns the Sauce Labs username and access ID as set by environment variables
    """
    return {"username": settings.SAUCE.get('USERNAME'), "access-key": settings.SAUCE.get('ACCESS_ID')}
34

35

36
def set_saucelabs_job_status(jobid, passed=True):
37 38 39
    """
    Sets the job status on sauce labs
    """
40
    config = get_saucelabs_username_and_key()
41 42
    url = 'http://saucelabs.com/rest/v1/{}/jobs/{}'.format(config['username'], world.jobid)
    body_content = dumps({"passed": passed})
43
    base64string = encodestring('{}:{}'.format(config['username'], config['access-key']))[:-1]
44 45
    headers = {"Authorization": "Basic {}".format(base64string)}
    result = requests.put(url, data=body_content, headers=headers)
46
    return result.status_code == 200
47

48

49
def make_saucelabs_desired_capabilities():
50 51 52 53
    """
    Returns a DesiredCapabilities object corresponding to the environment sauce parameters
    """
    desired_capabilities = settings.SAUCE.get('BROWSER', DesiredCapabilities.CHROME)
54 55 56 57 58 59 60
    desired_capabilities['platform'] = settings.SAUCE.get('PLATFORM')
    desired_capabilities['version'] = settings.SAUCE.get('VERSION')
    desired_capabilities['device-type'] = settings.SAUCE.get('DEVICE')
    desired_capabilities['name'] = settings.SAUCE.get('SESSION')
    desired_capabilities['build'] = settings.SAUCE.get('BUILD')
    desired_capabilities['video-upload-on-pass'] = False
    desired_capabilities['sauce-advisor'] = False
61 62
    desired_capabilities['capture-html'] = True
    desired_capabilities['record-screenshots'] = True
63 64 65 66 67
    desired_capabilities['selenium-version'] = "2.34.0"
    desired_capabilities['max-duration'] = 3600
    desired_capabilities['public'] = 'public restricted'
    return desired_capabilities

68

69 70
@before.harvest
def initial_setup(server):
Will Daly committed
71 72 73
    """
    Launch the browser once before executing the tests.
    """
74
    world.absorb(settings.LETTUCE_SELENIUM_CLIENT, 'LETTUCE_SELENIUM_CLIENT')
75

76
    if world.LETTUCE_SELENIUM_CLIENT == 'local':
77 78
        browser_driver = getattr(settings, 'LETTUCE_BROWSER', 'chrome')

79 80
        if browser_driver == 'chrome':
            desired_capabilities = DesiredCapabilities.CHROME
81 82 83
            desired_capabilities['loggingPrefs'] = {
                'browser': 'ALL',
            }
84 85 86
        else:
            desired_capabilities = {}

87 88 89 90 91 92
        # There is an issue with ChromeDriver2 r195627 on Ubuntu
        # in which we sometimes get an invalid browser session.
        # This is a work-around to ensure that we get a valid session.
        success = False
        num_attempts = 0
        while (not success) and num_attempts < MAX_VALID_BROWSER_ATTEMPTS:
93

94 95 96
            # Load the browser and try to visit the main page
            # If the browser couldn't be reached or
            # the browser session is invalid, this will
97 98
            # raise a WebDriverException
            try:
99
                if browser_driver == 'firefox':
100 101 102
                    # Lettuce initializes differently for firefox, and sending
                    # desired_capabilities will not work. So initialize without
                    # sending desired_capabilities.
103 104 105
                    world.browser = Browser(browser_driver)
                else:
                    world.browser = Browser(browser_driver, desired_capabilities=desired_capabilities)
106
                world.browser.driver.set_script_timeout(GLOBAL_SCRIPT_TIMEOUT)
107
                world.visit('/')
108

109
            except WebDriverException:
110
                LOGGER.warn("Error acquiring %s browser, retrying", browser_driver, exc_info=True)
111 112
                if hasattr(world, 'browser'):
                    world.browser.quit()
113
                num_attempts += 1
114

115 116
            else:
                success = True
117

118 119 120
        # If we were unable to get a valid session within the limit of attempts,
        # then we cannot run the tests.
        if not success:
JonahStanley committed
121
            raise IOError("Could not acquire valid {driver} browser session.".format(driver=browser_driver))
Will Daly committed
122

123
        world.absorb(0, 'IMPLICIT_WAIT')
124
        world.browser.driver.set_window_size(1280, 1024)
125

126 127
    elif world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
        config = get_saucelabs_username_and_key()
128 129 130
        world.browser = Browser(
            'remote',
            url="http://{}:{}@ondemand.saucelabs.com:80/wd/hub".format(config['username'], config['access-key']),
131 132
            **make_saucelabs_desired_capabilities()
        )
133
        world.absorb(30, 'IMPLICIT_WAIT')
134
        world.browser.set_script_timeout(GLOBAL_SCRIPT_TIMEOUT)
135 136 137 138 139 140

    elif world.LETTUCE_SELENIUM_CLIENT == 'grid':
        world.browser = Browser(
            'remote',
            url=settings.SELENIUM_GRID.get('URL'),
            browser=settings.SELENIUM_GRID.get('BROWSER'),
141
        )
142
        world.absorb(30, 'IMPLICIT_WAIT')
143
        world.browser.driver.set_script_timeout(GLOBAL_SCRIPT_TIMEOUT)
144

145 146 147
    else:
        raise Exception("Unknown selenium client '{}'".format(world.LETTUCE_SELENIUM_CLIENT))

148
    world.browser.driver.implicitly_wait(world.IMPLICIT_WAIT)
149 150
    world.absorb(world.browser.driver.session_id, 'jobid')

Will Daly committed
151

152 153
@before.each_scenario
def reset_data(scenario):
Will Daly committed
154
    """
155
    Clean out the django test database defined in the
156
    envs/acceptance.py file: edx-platform/db/test_edx.db
Will Daly committed
157 158
    """
    LOGGER.debug("Flushing the test database...")
159
    call_command('flush', interactive=False, verbosity=0)
160 161 162
    world.absorb({}, 'scenario_dict')


163 164 165 166 167 168 169 170 171 172
@before.each_scenario
def configure_screenshots(scenario):
    """
    Before each scenario, turn off automatic screenshots.

    Args: str, scenario. Name of current scenario.
    """
    world.auto_capture_screenshots = False


173 174 175 176
@after.each_scenario
def clear_data(scenario):
    world.spew('scenario_dict')

177

178 179
@after.each_scenario
def reset_databases(scenario):
180 181 182 183 184
    '''
    After each scenario, all databases are cleared/dropped.  Contentstore data are stored in unique databases
    whereas modulestore data is in unique collection names.  This data is created implicitly during the scenarios.
    If no data is created during the test, these lines equivilently do nothing.
    '''
185
    xmodule.modulestore.django.modulestore()._drop_database()  # pylint: disable=protected-access
186
    xmodule.modulestore.django.clear_existing_modulestores()
187
    _CONTENTSTORE.clear()
188 189


190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
@world.absorb
def capture_screenshot(image_name):
    """
    Capture a screenshot outputting it to a defined directory.
    This function expects only the name of the file. It will generate
    the full path of the output screenshot.

    If the name contains spaces, they ill be converted to underscores.
    """
    output_dir = '{}/log/auto_screenshots'.format(settings.TEST_ROOT)
    image_name = '{}/{}.png'.format(output_dir, image_name.replace(' ', '_'))
    try:
        world.browser.driver.save_screenshot(image_name)
    except WebDriverException:
        LOGGER.error("Could not capture a screenshot '{}'".format(image_name))


207
@after.each_scenario
208
def screenshot_on_error(scenario):
Will Daly committed
209 210 211
    """
    Save a screenshot to help with debugging.
    """
212
    if scenario.failed:
213 214 215 216 217 218
        try:
            output_dir = '{}/log'.format(settings.TEST_ROOT)
            image_name = '{}/{}.png'.format(output_dir, scenario.name.replace(' ', '_'))
            world.browser.driver.save_screenshot(image_name)
        except WebDriverException:
            LOGGER.error('Could not capture a screenshot')
219

220

221 222 223 224 225 226 227 228 229 230 231
@after.each_scenario
def capture_console_log(scenario):
    """
    Save the console log to help with debugging.
    """
    if scenario.failed:
        log = world.browser.driver.get_log('browser')
        try:
            output_dir = '{}/log'.format(settings.TEST_ROOT)
            file_name = '{}/{}.log'.format(output_dir, scenario.name.replace(' ', '_'))

232
            with open(file_name, 'w') as output_file:
233 234 235 236 237 238 239
                for line in log:
                    output_file.write("{}{}".format(dumps(line), '\n'))

        except WebDriverException:
            LOGGER.error('Could not capture the console log')


240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
def capture_screenshot_for_step(step, when):
    """
    Useful method for debugging acceptance tests that are run in Vagrant.
    This method runs automatically before and after each step of an acceptance
    test scenario. The variable:

         world.auto_capture_screenshots

    either enables or disabled the taking of screenshots. To change the
    variable there is a convenient step defined:

        I (enable|disable) auto screenshots

    If you just want to capture a single screenshot at a desired point in code,
    you should use the method:

        world.capture_screenshot("image_name")
    """
    if world.auto_capture_screenshots:
259
        scenario_num = step.scenario.feature.scenarios.index(step.scenario) + 1
260 261
        step_num = step.scenario.steps.index(step) + 1
        step_func_name = step.defined_at.function.func_name
262
        image_name = "{prefix:03d}__{num:03d}__{name}__{postfix}".format(
263 264 265 266 267 268 269 270 271 272
            prefix=scenario_num,
            num=step_num,
            name=step_func_name,
            postfix=when
        )
        world.capture_screenshot(image_name)


@before.each_step
def before_each_step(step):
273
    capture_screenshot_for_step(step, '1_before')
274 275 276 277


@after.each_step
def after_each_step(step):
278
    capture_screenshot_for_step(step, '2_after')
279 280


281
@after.harvest
282
def saucelabs_status(total):
Will Daly committed
283
    """
284
    Collect data for saucelabs.
Will Daly committed
285
    """
286 287
    if world.LETTUCE_SELENIUM_CLIENT == 'saucelabs':
        set_saucelabs_job_status(world.jobid, total.scenarios_ran == total.scenarios_passed)