Add Grafana dashboards UI test

Add common function to run selenium in headless mode.

Change-Id: Ia48cea1c3e956aab49ca498e736dceda51598f9e
This commit is contained in:
Rodion Promyshlennikov 2016-05-26 18:58:50 +03:00
parent 5a0720cddc
commit c7127bc49f
16 changed files with 525 additions and 29 deletions

View File

@ -24,7 +24,11 @@ for the detailed procedure).
. $VENV_PATH/bin/activate
4. Run the tests:
4. If you want to run UI test in headless mode, install these packages:
sudo apt-get install xvfb firefox -y
5. Run the tests:
./utils/jenkins/system_tests.sh -k -K -j fuelweb_test -t test -w $(pwd) -o --group=<your_test_group_to_run>

View File

@ -18,3 +18,9 @@ Destructive
.. automodule:: stacklight_tests.influxdb_grafana.test_destructive
:members:
Functional
==========
.. automodule:: stacklight_tests.influxdb_grafana.test_functional
:members:

View File

@ -16,3 +16,9 @@ export LMA_COLLECTOR_PLUGIN_PATH=$HOME/plugins/lma_collector-0.9-0.9.0-1.noarch.
export LMA_INFRA_ALERTING_PLUGIN_PATH=$HOME/plugins/lma_infrastructure_alerting-0.9-0.9.0-1.noarch.rpm
export ELASTICSEARCH_KIBANA_PLUGIN_PATH=$HOME/plugins/elasticsearch_kibana-0.9-0.9.0-1.noarch.rpm
export INFLUXDB_GRAFANA_PLUGIN_PATH=$HOME/plugins/influxdb_grafana-0.9-0.9.0-1.noarch.rpm
# UI Tests settings
export SELENIUM_HEADLESS=True
export SELENIUM_MAXIMIZE=True
export IMPLICIT_WAIT=5
# export DRIVER_PROXY=localhost:8080

View File

@ -3,3 +3,4 @@ requests
selenium
six
tox
xvfbwrapper

View File

View File

@ -0,0 +1,163 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import contextlib
from proboscis import asserts
import selenium.common.exceptions as Exceptions
from selenium.webdriver.remote import webelement
import selenium.webdriver.support.ui as Support
class BaseWebObject(object):
def __init__(self, driver, timeout=5):
self.driver = driver
self.timeout = timeout
def _turn_off_implicit_wait(self):
self.driver.implicitly_wait(0)
def _turn_on_implicit_wait(self):
self.driver.implicitly_wait(self.timeout)
@contextlib.contextmanager
def waits_disabled(self):
try:
self._turn_off_implicit_wait()
yield
finally:
self._turn_on_implicit_wait()
def _is_element_present(self, *locator):
with self.waits_disabled():
try:
self._get_element(*locator)
return True
except Exceptions.NoSuchElementException:
return False
def _is_element_visible(self, *locator):
try:
return self._get_element(*locator).is_displayed()
except (Exceptions.NoSuchElementException,
Exceptions.ElementNotVisibleException):
return False
def _is_element_displayed(self, element):
if element is None:
return False
try:
if isinstance(element, webelement.WebElement):
return element.is_displayed()
else:
return element.src_elem.is_displayed()
except (Exceptions.ElementNotVisibleException,
Exceptions.StaleElementReferenceException):
return False
def _is_text_visible(self, element, text, strict=True):
if not hasattr(element, 'text'):
return False
if strict:
return element.text == text
else:
return text in element.text
def _get_element(self, *locator):
return self.driver.find_element(*locator)
def _get_elements(self, *locator):
return self.driver.find_elements(*locator)
def _fill_field_element(self, data, field_element):
field_element.clear()
field_element.send_keys(data)
return field_element
def _select_dropdown(self, value, element):
select = Support.Select(element)
select.select_by_visible_text(value)
def _select_dropdown_by_value(self, value, element):
select = Support.Select(element)
select.select_by_value(value)
def _get_dropdown_options(self, element):
select = Support.Select(element)
return select.options
class PageObject(BaseWebObject):
"""Base class for page objects."""
PARTIAL_LOGIN_URL = 'login'
def __init__(self, driver):
"""Constructor."""
super(PageObject, self).__init__(driver)
self._page_title = None
@property
def page_title(self):
return self.driver.title
def is_the_current_page(self, do_assert=False):
found_expected_title = self.page_title.startswith(self._page_title)
if do_assert:
asserts.assert_true(
found_expected_title,
"Expected to find %s in page title, instead found: %s"
% (self._page_title, self.page_title))
return found_expected_title
def get_current_page_url(self):
return self.driver.current_url
def close_window(self):
return self.driver.close()
def is_nth_window_opened(self, n):
return len(self.driver.window_handles) == n
def switch_window(self, name=None, index=None):
"""Switches focus between the webdriver windows.
Args:
- name: The name of the window to switch to.
- index: The index of the window handle to switch to.
If the method is called without arguments it switches to the
last window in the driver window_handles list.
In case only one window exists nothing effectively happens.
Usage:
page.switch_window(name='_new')
page.switch_window(index=2)
page.switch_window()
"""
if name is not None and index is not None:
raise ValueError("switch_window receives the window's name or "
"the window's index, not both.")
if name is not None:
self.driver.switch_to.window(name)
elif index is not None:
self.driver.switch_to.window(
self.driver.window_handles[index])
else:
self.driver.switch_to.window(self.driver.window_handles[-1])
def go_to_previous_page(self):
self.driver.back()
def go_to_next_page(self):
self.driver.forward()
def refresh_page(self):
self.driver.refresh()

View File

@ -0,0 +1,35 @@
# Copyright 2016 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
# Use a virtual display server for running the tests headless or not
headless_mode = os.environ.get('SELENIUM_HEADLESS', False)
# The browser session will be started with given proxy,
# can be useful if you try to start UI tests on developer machine,
# but environment is on remote server
proxy_address = os.environ.get('DRIVER_PROXY', None)
# Maximize the current window that webdriver is using or not
maximize_window = os.environ.get('SELENIUM_MAXIMIZE', True)
# Sets a sticky timeout to implicitly wait for an element to be found,
# or a command to complete.
implicit_wait = os.environ.get('IMPLICIT_WAIT', 5)
# Set the amount of time to wait for a page load to complete
# before throwing an error.
page_timeout = os.environ.get('PAGE_TIMEOUT', 15)

View File

@ -12,21 +12,71 @@
# License for the specific language governing permissions and limitations
# under the License.
from proboscis import asserts
import contextlib
import socket
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.common import by
from selenium.webdriver.common import proxy
import xvfbwrapper
from stacklight_tests.helpers.ui import ui_settings
def get_driver(ip, anchor, title):
driver = webdriver.Firefox()
driver.get(ip)
WebDriverWait(driver, 120).until(
EC.presence_of_element_located((By.XPATH, anchor)))
asserts.assert_equal(True, title in driver.title,
"Title {0} was not found in {1}!".format(
title, driver.title))
@contextlib.contextmanager
def ui_driver(url, wait_element, title):
vdisplay = None
# Start a virtual display server for running the tests headless.
if ui_settings.headless_mode:
vdisplay = xvfbwrapper.Xvfb(width=1920, height=1080)
args = []
# workaround for memory leak in Xvfb taken from:
# http://blog.jeffterrace.com/2012/07/xvfb-memory-leak-workaround.html
args.append("-noreset")
# disables X access control
args.append("-ac")
if hasattr(vdisplay, 'extra_xvfb_args'):
# xvfbwrapper 0.2.8 or newer
vdisplay.extra_xvfb_args.extend(args)
else:
vdisplay.xvfb_cmd.extend(args)
vdisplay.start()
driver = get_driver(url, wait_element, title)
try:
yield driver
finally:
driver.quit()
if vdisplay is not None:
vdisplay.stop()
def get_driver(url, anchor, title, by_selector_type=by.By.XPATH):
proxy_address = ui_settings.proxy_address
# Increase the default Python socket timeout from nothing
# to something that will cope with slow webdriver startup times.
# This *just* affects the communication between this test process
# and the webdriver.
socket.setdefaulttimeout(60)
# Start the Selenium webdriver and setup configuration.
proxy_ex = None
if proxy_address is not None:
proxy_ex = proxy.Proxy(
{
'proxyType': proxy.ProxyType.MANUAL,
'socksProxy': proxy_address,
}
)
driver = webdriver.Firefox(proxy=proxy_ex)
if ui_settings.maximize_window:
driver.maximize_window()
driver.implicitly_wait(ui_settings.implicit_wait)
driver.set_page_load_timeout(ui_settings.page_timeout)
driver.get(url)
driver.find_element(by_selector_type, anchor)
assert title in driver.title
return driver

View File

@ -18,6 +18,7 @@ from proboscis import asserts
import requests
from stacklight_tests import base_test
from stacklight_tests.influxdb_grafana.grafana_ui import api as ui_api
from stacklight_tests.influxdb_grafana import plugin_settings
@ -137,3 +138,7 @@ class InfluxdbPluginApi(base_test.PluginApi):
def check_uninstall_failure(self):
return self.helpers.check_plugin_cannot_be_uninstalled(
self.settings.name, self.settings.version)
def check_grafana_dashboards(self):
grafana_url = self.get_grafana_url()
ui_api.check_grafana_dashboards(grafana_url)

View File

@ -0,0 +1,48 @@
# Copyright 2016 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from proboscis import asserts
from stacklight_tests.helpers.ui_tester import ui_driver
from stacklight_tests.influxdb_grafana.grafana_ui import pages
def check_grafana_dashboards(grafana_url):
login_key_xpath = '/html/body/div/div[2]/div/div/div[2]/form/div[2]/button'
with ui_driver(grafana_url, login_key_xpath, "Grafana") as driver:
login_page = pages.LoginPage(driver)
login_page.is_login_page()
home_page = login_page.login("grafana", "grafanapass")
home_page.is_main_page()
dashboard_names = {
"Apache", "Cinder", "Elasticsearch", "Glance", "HAProxy", "Heat",
"Hypervisor", "InfluxDB", "Keystone", "LMA self-monitoring",
"Memcached", "MySQL", "Neutron", "Nova", "RabbitMQ", "System"
}
dashboard_names = {
panel_name.lower() for panel_name in dashboard_names}
available_dashboards_names = {
dashboard.text.lower() for dashboard in home_page.dashboards}
msg = ("There is not enough panels in available panels, "
"panels that are not presented: {}")
# NOTE(rpromyshlennikov): should there be 'elasticsearch'
# and 'influxdb' dashboards?
asserts.assert_true(
dashboard_names.issubset(available_dashboards_names),
msg.format(dashboard_names - available_dashboards_names))
for name in available_dashboards_names:
dashboard_page = home_page.open_dashboard(name)
dashboard_page.get_back_to_home()

View File

@ -0,0 +1,127 @@
from selenium.webdriver.common import by
from selenium.webdriver.common import keys
from stacklight_tests.helpers.ui import base_pages
class DashboardPage(base_pages.PageObject):
_submenu_controls_locator = (
by.By.CLASS_NAME,
"submenu-controls"
)
def __init__(self, driver, dashboard_name):
super(DashboardPage, self).__init__(driver)
self._page_title = "Grafana - {}".format(dashboard_name)
def is_dashboards_page(self):
return (self.is_the_current_page() and
self._is_element_visible(*self._submenu_controls_locator))
def get_back_to_home(self):
self.go_to_previous_page()
return MainPage(self.driver)
class MainPage(base_pages.PageObject):
_dropdown_menu_locator = (
by.By.CLASS_NAME,
'top-nav-dashboards-btn')
_dashboards_list_locator = (
by.By.CLASS_NAME,
'search-results-container'
)
_dashboard_locator = (by.By.CLASS_NAME,
"search-item-dash-db")
def __init__(self, driver):
super(MainPage, self).__init__(driver)
self._page_title = "Grafana - Home"
def is_main_page(self):
return (self.is_the_current_page() and
self._is_element_visible(*self._dropdown_menu_locator))
@property
def dropdown_menu(self):
return self._get_element(*self._dropdown_menu_locator)
@property
def dashboards_list(self):
self.open_dropdown_menu()
return self._get_element(*self._dashboards_list_locator)
@property
def dashboards(self):
return self.dashboards_list.find_elements(*self._dashboard_locator)
def is_dropdown_menu_opened(self):
return self._is_element_present(*self._dashboards_list_locator)
def open_dropdown_menu(self):
if not self.is_dropdown_menu_opened():
self.dropdown_menu.click()
def open_dashboard(self, dashboard_name):
dashboards_mapping = {dashboard.text.lower(): dashboard
for dashboard in self.dashboards}
dashboards_mapping[dashboard_name.lower()].click()
dashboard_page = DashboardPage(self.driver, dashboard_name)
dashboard_page.is_dashboards_page()
return dashboard_page
class LoginPage(base_pages.PageObject):
_login_username_field_locator = (by.By.NAME, 'username')
_login_password_field_locator = (by.By.NAME, 'password')
_login_submit_button_locator = (
by.By.CLASS_NAME,
'login-submit-button-row')
def __init__(self, driver):
super(LoginPage, self).__init__(driver)
self._page_title = "Grafana"
def is_login_page(self):
return (self.is_the_current_page() and
self._is_element_visible(*self._login_submit_button_locator))
@property
def username(self):
return self._get_element(*self._login_username_field_locator)
@property
def password(self):
return self._get_element(*self._login_password_field_locator)
@property
def login_button(self):
return self._get_element(*self._login_submit_button_locator)
def _click_on_login_button(self):
self.login_button.click()
def _press_enter_on_login_button(self):
self.login_button.send_keys(keys.Keys.RETURN)
def login(self, user, password):
return self.login_with_mouse_click(user, password)
def login_with_mouse_click(self, user, password):
return self._do_login(user, password, self._click_on_login_button)
def login_with_enter_key(self, user, password):
return self._do_login(user, password,
self._press_enter_on_login_button)
def _do_login(self, user, password, login_method):
return self.login_as_user(user, password, login_method)
def login_as_user(self, user, password, login_method):
self._fill_field_element(user, self.username)
self._fill_field_element(password, self.password)
login_method()
return MainPage(self.driver)

View File

@ -39,7 +39,6 @@ class TestDestructiveInfluxdbPlugin(api.InfluxdbPluginApi):
6. Check that the cluster's state is okay
Duration 40m
Snapshot check_cluster_outage_influxdb_grafana
"""
self.env.revert_snapshot("deploy_ha_influxdb_grafana")
@ -52,8 +51,6 @@ class TestDestructiveInfluxdbPlugin(api.InfluxdbPluginApi):
self.helpers.run_ostf()
self.env.make_snapshot("check_cluster_outage_influxdb_grafana")
@test(depends_on_groups=["deploy_influxdb_grafana"],
groups=["check_disaster_influxdb_grafana", "influxdb_grafana",
"destructive", "check_node_outage_influxdb_grafana"])
@ -66,11 +63,10 @@ class TestDestructiveInfluxdbPlugin(api.InfluxdbPluginApi):
1. Revert the snapshot with 3 deployed nodes
2. Simulate network interruption on the InfluxDB/Grafana node
3. Wait for at least 30 seconds before recover network availability
5. Run OSTF
6. Check that plugin is working
4. Run OSTF
5. Check that plugin is working
Duration 20m
Snapshot check_node_outage_influxdb_grafana
"""
self.env.revert_snapshot("deploy_influxdb_grafana")
@ -83,5 +79,3 @@ class TestDestructiveInfluxdbPlugin(api.InfluxdbPluginApi):
self.check_plugin_online()
self.helpers.run_ostf()
self.env.make_snapshot("check_node_outage_influxdb_grafana")

View File

@ -0,0 +1,64 @@
# Copyright 2016 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from fuelweb_test.helpers.decorators import log_snapshot_after_test
from proboscis import test
from stacklight_tests.influxdb_grafana import api
@test(groups=["plugins"])
class TestFunctionalInfluxdbPlugin(api.InfluxdbPluginApi):
"""Class for functional testing of plugin."""
@test(depends_on_groups=["deploy_influxdb_grafana"],
groups=["check_display_dashboards_influxdb_grafana",
"influxdb_grafana", "functional"])
@log_snapshot_after_test
def check_display_dashboards_influxdb_grafana(self):
"""Verify that the dashboards show up in the Grafana UI.
Scenario:
1. Revert snapshot with 9 deployed nodes in HA configuration
2. Open the Grafana URL (
open the "Dashboard" tab and click the "Grafana" link)
3. Sign-in using the credentials provided
during the configuration of the environment
4. Go to the Main dashboard and verify that everything is ok
5. Repeat the previous step for the following dashboards:
* Apache
* Cinder
* Elasticsearch
* Glance
* HAProxy
* Heat
* Hypervisor
* InfluxDB
* Keystone
* LMA self-monitoring
* Memcached
* MySQL
* Neutron
* Nova
* RabbitMQ
* System
Duration 40m
"""
self.env.revert_snapshot("deploy_influxdb_grafana_plugin")
self.check_plugin_online()
self.check_grafana_dashboards()

View File

@ -66,8 +66,6 @@ class TestNodesInfluxdbPlugin(api.InfluxdbPluginApi):
self.helpers.run_ostf(should_fail=1)
self.env.make_snapshot("add_remove_controller_influxdb_grafana")
@test(depends_on_groups=["deploy_ha_influxdb_grafana"],
groups=["check_scaling_influxdb_grafana", "scaling",
"influxdb_grafana", "system",
@ -112,8 +110,6 @@ class TestNodesInfluxdbPlugin(api.InfluxdbPluginApi):
self.helpers.run_ostf(should_fail=1)
self.env.make_snapshot("add_remove_compute_influxdb_grafana")
@test(depends_on_groups=["deploy_ha_influxdb_grafana"],
groups=["check_scaling_influxdb_grafana", "scaling",
"influxdb_grafana", "system",
@ -162,8 +158,6 @@ class TestNodesInfluxdbPlugin(api.InfluxdbPluginApi):
self.helpers.run_ostf()
self.env.make_snapshot("add_remove_influxdb_grafana_node")
@test(depends_on_groups=["deploy_ha_influxdb_grafana"],
groups=["check_failover_influxdb_grafana" "failover",
"influxdb_grafana", "system", "destructive",
@ -197,5 +191,3 @@ class TestNodesInfluxdbPlugin(api.InfluxdbPluginApi):
# TODO(rpromyshlennikov): check no data lost
self.helpers.run_ostf()
self.env.make_snapshot("shutdown_influxdb_grafana_node")

View File

@ -44,6 +44,7 @@ def import_tests():
from stacklight_tests.elasticsearch_kibana import test_smoke_bvt # noqa
from stacklight_tests.elasticsearch_kibana import test_system # noqa
from stacklight_tests.influxdb_grafana import test_destructive # noqa
from stacklight_tests.influxdb_grafana import test_functional # noqa
from stacklight_tests.influxdb_grafana import test_smoke_bvt # noqa
from stacklight_tests.influxdb_grafana import test_system # noqa
from stacklight_tests.lma_collector import test_smoke_bvt # noqa