pgadmin4/web/regression/feature_tests/query_tool_tests.py

571 lines
22 KiB
Python

##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2024, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
import sys
import time
from selenium.common.exceptions import StaleElementReferenceException, \
ElementClickInterceptedException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from regression.feature_utils.base_feature_test import BaseFeatureTest
import config
from regression.feature_utils.locators import \
QueryToolLocators
DATA_OUTPUT_STR = "Data Output"
CREATE_TABLE_STR = 'CREATE TABLE'
class QueryToolFeatureTest(BaseFeatureTest):
"""
This feature test will test the different query tool features.
"""
scenarios = [
("Query tool feature test", dict())
]
data_output_tab_id = 'id-dataoutput'
table_creation_fail_error = '"CREATE TABLE message does not displayed"'
def before(self):
self.page.wait_for_spinner_to_disappear()
self.page.add_server(self.server)
self.page.expand_database_node("Server", self.server['name'],
self.server['db_password'],
self.test_db)
self.page.open_query_tool()
self.page.wait_for_spinner_to_disappear()
self.wait = WebDriverWait(self.page.driver, 10)
def runTest(self):
self._reset_options()
# pagination result on page change.
print("\nPagination query result... ",
file=sys.stderr, end="")
self._pagination_result()
self.page.clear_query_tool()
# explain query with verbose and cost
print("Explain query with verbose and cost... ",
file=sys.stderr, end="")
self._query_tool_explain_with_verbose_and_cost()
print("OK.", file=sys.stderr)
self.page.clear_query_tool()
# explain analyze query with buffers and timing
print("Explain analyze query with buffers and timing... ",
file=sys.stderr, end="")
self._query_tool_explain_analyze_with_buffers_and_timing()
print("OK.", file=sys.stderr)
self.page.clear_query_tool()
# auto commit disabled.
print("Auto commit disabled... ", file=sys.stderr, end="")
self._query_tool_auto_commit_disabled()
print("OK.", file=sys.stderr)
self.page.clear_query_tool()
# auto commit enabled.
print("Auto commit enabled... ", file=sys.stderr, end="")
self._query_tool_auto_commit_enabled()
print("OK.", file=sys.stderr)
self.page.clear_query_tool()
# auto rollback enabled.
print("Auto rollback enabled...", file=sys.stderr, end="")
self._query_tool_auto_rollback_enabled()
print(" OK.", file=sys.stderr)
self.page.clear_query_tool()
# cancel query.
print("Cancel query... ", file=sys.stderr, end="")
self._query_tool_cancel_query()
print("OK.", file=sys.stderr)
self.page.clear_query_tool()
# Notify Statements.
print("Capture Notify Statements... ", file=sys.stderr, end="")
self._query_tool_notify_statements()
self.page.clear_query_tool()
def after(self):
self.page.close_query_tool(False)
self.page.remove_server(self.server)
def _reset_options(self):
# this will set focus to correct iframe.
self.page.fill_codemirror_area_with('')
self.assertTrue(self.page.open_explain_options(),
'Unable to open Explain Options dropdown')
# disable Explain options and auto rollback only if they are enabled.
for op in (QueryToolLocators.btn_explain_verbose,
QueryToolLocators.btn_explain_costs,
QueryToolLocators.btn_explain_buffers,
QueryToolLocators.btn_explain_timing):
btn = self.page.find_by_css_selector(op)
if btn.get_attribute('data-checked') == 'true':
btn.click()
query_op = self.page.find_by_css_selector(
QueryToolLocators.btn_query_dropdown)
query_op.click()
# disable auto rollback only if they are enabled
self.page.uncheck_execute_option('auto_rollback')
# enable autocommit only if it's disabled
self.page.check_execute_option('auto_commit')
# close menu
query_op.click()
def _pagination_result(self):
query = """-- Pagination result
SELECT generate_series(1, {}) as id1, 'dummy' as id2""".format(
config.DATA_RESULT_ROWS_PER_PAGE * 2.5)
print("\nPagination result... ", file=sys.stderr, end="")
self.page.execute_query(query)
# wait for header of the table to be visible
self.wait.until(EC.visibility_of_element_located(
(By.CSS_SELECTOR, QueryToolLocators.query_output_canvas_css)))
self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR,
QueryToolLocators.query_output_cells)))
for i, page in enumerate([
{'page_info': '1 to 1000', 'cell_rownum': '1'},
{'page_info': '1001 to 2000', 'cell_rownum': '1001'},
{'page_info': '2001 to 2500', 'cell_rownum': '2001'}
]):
page_info = self.page.find_by_css_selector(
QueryToolLocators.pagination_inputs +
f' span:nth-of-type(1)')
self.assertEqual(page_info.text,
f"Showing rows: {page['page_info']}")
page_info = self.page.find_by_css_selector(
QueryToolLocators.pagination_inputs + ' span:nth-of-type(3)')
self.assertEqual(page_info.text, "of 3")
cell_rownum = self.page.find_by_css_selector(
QueryToolLocators.query_output_cells + ':nth-of-type(1)')
self.assertEqual(cell_rownum.text, page['cell_rownum'])
if i < 2:
self.page.find_by_css_selector(
QueryToolLocators.pagination_inputs +
' button[aria-label="Next Page"]').click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
def _query_tool_explain_with_verbose_and_cost(self):
query = """-- Explain query with verbose and cost
SELECT generate_series(1, 1000) as id order by id desc"""
self.page.fill_codemirror_area_with(query)
time.sleep(0.5)
self.assertTrue(self.page.open_explain_options(),
'Unable to open Explain Options dropdown')
# disable Explain options and auto rollback only if they are enabled.
for op in (QueryToolLocators.btn_explain_verbose,
QueryToolLocators.btn_explain_costs):
self.page.find_by_css_selector(op).click()
self.page.find_by_css_selector(
QueryToolLocators.btn_explain).click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab(DATA_OUTPUT_STR)
canvas = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, QueryToolLocators.query_output_canvas_css))
)
self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR,
QueryToolLocators.query_output_cells)))
# Search for 'Output' word in result (verbose option)
canvas.find_element(By.XPATH, "//*[contains(string(), 'Output')]")
# Search for 'Total Cost' word in result (cost option)
canvas.find_element(By.XPATH, "//*[contains(string(),'Total Cost')]")
def _query_tool_explain_analyze_with_buffers_and_timing(self):
query = """-- Explain analyze query with buffers and timing
SELECT generate_series(1, 1000) as id order by id desc"""
self.page.fill_codemirror_area_with(query)
self.assertTrue(self.page.open_explain_options(),
'Unable to open Explain Options')
# disable Explain options and auto rollback only if they are enabled.
for op in (QueryToolLocators.btn_explain_buffers,
QueryToolLocators.btn_explain_timing):
self.page.find_by_css_selector(op).click()
self.page.find_by_css_selector(
QueryToolLocators.btn_explain_analyze).click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab(DATA_OUTPUT_STR)
self.wait.until(EC.presence_of_element_located(
(By.XPATH, QueryToolLocators.output_cell_xpath.format(2, 2)))
)
result = self.page.find_by_xpath(
QueryToolLocators.output_cell_xpath.format(2, 2))
# Search for 'Shared Read Blocks' word in result (buffers option)
self.assertIn('Shared Read Blocks', result.text)
# Search for 'Actual Total Time' word in result (timing option)
self.assertIn('Actual Total Time', result.text)
def _query_tool_auto_commit_disabled(self):
table_name = 'query_tool_auto_commit_disabled_table'
query = """-- 1. Disable auto commit.
-- 2. Create table in public schema.
-- 3. ROLLBACK transaction.
-- 4. Check if table is *NOT* created.
CREATE TABLE public.{}();""".format(table_name)
self.page.fill_codemirror_area_with(query)
# disable auto commit option
query_op = self.page.find_by_css_selector(
QueryToolLocators.btn_query_dropdown)
query_op.click()
self.page.uncheck_execute_option('auto_commit')
# close option
query_op.click()
# execute query
self.page.click_execute_query_button()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.assertTrue(self.page.check_if_element_exist_by_xpath(
QueryToolLocators.sql_editor_message.format(CREATE_TABLE_STR)),
self.table_creation_fail_error)
# do the ROLLBACK and check if the table is present or not
self.page.clear_query_tool()
query = """-- 1. (Done) Disable auto commit.
-- 2. (Done) Create table in public schema.
-- 3. ROLLBACK transaction.
-- 4. Check if table is *NOT* created.
ROLLBACK;"""
self.page.execute_query(query)
self.page.click_tab('Messages')
self.assertTrue(self.page.check_if_element_exist_by_xpath(
QueryToolLocators.sql_editor_message.format('ROLLBACK')),
"ROLLBACK message does not displayed")
self.page.clear_query_tool()
query = """-- 1. (Done) Disable auto commit.
-- 2. (Done) Create table in public schema.
-- 3. (Done) ROLLBACK transaction.
-- 4. Check if table is *NOT* created.
SELECT relname FROM pg_catalog.pg_class
WHERE relkind IN ('r','s','t') and relnamespace = 2200::oid;"""
self.page.execute_query(query)
self.page.click_tab(DATA_OUTPUT_STR)
canvas = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, QueryToolLocators.query_output_canvas_css)))
el = canvas.find_elements(By.XPATH, QueryToolLocators.
output_column_data_xpath.format(table_name))
assert len(el) == 0, "Table '{}' created with auto commit disabled " \
"and without any explicit commit.".format(
table_name
)
# again roll back so that the auto commit drop down is enabled
query = """-- 1. (Done) Disable auto commit.
-- 2. (Done) Create table in public schema.
-- 3. ROLLBACK transaction.
-- 4. Check if table is *NOT* created.
ROLLBACK;"""
self.page.execute_query(query)
def _query_tool_auto_commit_enabled(self):
query = """-- 1. Enable auto commit.
-- 2. END any open transaction.
-- 3. Create table in public schema.
-- 4. ROLLBACK transaction
-- 5. Check if table is created event after ROLLBACK.
END;"""
self.page.fill_codemirror_area_with(query)
query_op = self.page.find_by_css_selector(
QueryToolLocators.btn_query_dropdown)
query_op.click()
# Enable auto_commit if it is disabled
self.page.check_execute_option('auto_commit')
query_op.click()
self.page.click_execute_query_button()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.clear_query_tool()
table_name = 'query_tool_auto_commit_enabled_table'
query = """-- 1. (Done) END any open transaction.
-- 2. Enable auto commit.
-- 3. Create table in public schema.
-- 4. ROLLBACK transaction
-- 5. Check if table is created event after ROLLBACK.
CREATE TABLE public.{}();""".format(table_name)
self.page.execute_query(query)
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.assertTrue(self.page.check_if_element_exist_by_xpath(
QueryToolLocators.sql_editor_message.format(CREATE_TABLE_STR)),
self.table_creation_fail_error)
self.page.clear_query_tool()
query = """-- 1. (Done) END any open transaction if any.
-- 2. (Done) Enable auto commit.
-- 3. (Done) Create table in public schema.
-- 4. ROLLBACK transaction
-- 5. Check if table is created event after ROLLBACK.
ROLLBACK;"""
self.page.execute_query(query)
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.assertTrue(self.page.check_if_element_exist_by_xpath(
QueryToolLocators.sql_editor_message.format('ROLLBACK')),
"ROLLBACK message does not displayed")
self.page.clear_query_tool()
query = """-- 1. (Done) END any open transaction if any.
-- 2. (Done) Enable auto commit.
-- 3. (Done) Create table in public schema.
-- 4. (Done) ROLLBACK transaction
-- 5. Check if table is created event after ROLLBACK.
SELECT relname FROM pg_catalog.pg_class
WHERE relkind IN ('r','s','t') and relnamespace = 2200::oid;"""
self.page.execute_query(query)
self.page.click_tab(DATA_OUTPUT_STR)
self.page.wait_for_query_tool_loading_indicator_to_disappear()
canvas = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, QueryToolLocators.query_output_canvas_css)))
el = canvas.find_elements(
By.XPATH, QueryToolLocators.output_column_data_xpath.format(
table_name))
assert len(el) != 0, "Table '{}' is not created with auto " \
"commit enabled.".format(table_name)
def _query_tool_auto_rollback_enabled(self):
table_name = 'query_tool_auto_rollback_enabled_table'
query = """-- 1. Enable auto rollback and disable auto commit.
-- 2. END any open transaction.
-- 3. Create table in public schema.
-- 4. Generate error in transaction.
-- 5. END transaction.
-- 6. Check if table is *NOT* created after ending transaction.
END;"""
self.page.fill_codemirror_area_with(query)
query_op = self.page.find_by_css_selector(
QueryToolLocators.btn_query_dropdown)
query_op.click()
# uncheck auto commit and check auto-rollback
self.page.uncheck_execute_option('auto_commit')
self.page.check_execute_option('auto_rollback')
query_op.click()
self.page.click_execute_query_button()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.clear_query_tool()
query = """-- 1. (Done) END any open transaction.
-- 2. Enable auto rollback and disable auto commit.
-- 3. Create table in public schema.
-- 4. Generate error in transaction.
-- 5. END transaction.
-- 6. Check if table is *NOT* created after ending transaction.
CREATE TABLE public.{}();""".format(table_name)
self.page.execute_query(query)
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.assertTrue(self.page.check_if_element_exist_by_xpath(
QueryToolLocators.sql_editor_message.format(CREATE_TABLE_STR)),
self.table_creation_fail_error)
self.page.clear_query_tool()
query = """-- 1. (Done) END any open transaction.
-- 2. (Done) Enable auto rollback and disable auto commit.
-- 3. (Done) Create table in public schema.
-- 4. Generate error in transaction.
-- 5. END transaction.
-- 6. Check if table is *NOT* created after ending transaction.
SELECT 1/0;"""
self.page.execute_query(query)
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.assertTrue(self.page.check_if_element_exist_by_xpath(
QueryToolLocators.sql_editor_message.format('division by zero')),
"division by zero message does not displayed")
self.page.clear_query_tool()
query = """-- 1. (Done) END any open transaction.
-- 2. (Done) Enable auto rollback and disable auto commit.
-- 3. (Done) Create table in public schema.
-- 4. (Done) Generate error in transaction.
-- 5. END transaction.
-- 6. Check if table is *NOT* created after ending transaction.
END;"""
self.page.execute_query(query)
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.assertTrue(self.page.check_if_element_exist_by_xpath(
QueryToolLocators.sql_editor_message.
format('Query returned successfully')),
"Query returned successfully message does not displayed")
self.page.clear_query_tool()
query = """-- 1. (Done) END any open transaction.
-- 2. (Done) Enable auto rollback and disable auto commit.
-- 3. (Done) Create table in public schema.
-- 4. (Done) Generate error in transaction.
-- 5. (Done) END transaction.
-- 6. Check if table is *NOT* created after ending transaction.
SELECT relname FROM pg_catalog.pg_class
WHERE relkind IN ('r','s','t') and relnamespace = 2200::oid;"""
self.page.execute_query(query)
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab(DATA_OUTPUT_STR)
canvas = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, QueryToolLocators.query_output_canvas_css)))
el = canvas.find_elements(
By.XPATH, QueryToolLocators.output_column_data_xpath.format(
table_name))
assert len(el) == 0, "Table '{}' created even after ROLLBACK due to " \
"sql error.".format(table_name)
def _query_tool_cancel_query(self):
query = """-- 1. END any open transaction.
-- 2. Enable auto commit and Disable auto rollback.
-- 3. Execute long running query.
-- 4. Cancel long running query execution.
END;
SELECT 1, pg_sleep(300)"""
self.page.fill_codemirror_area_with(query)
# query_button drop can be disabled so enable
commit_button = self.page.find_by_css_selector(
QueryToolLocators.btn_commit)
if not commit_button.get_attribute('disabled'):
commit_button.click()
time.sleep(2)
# enable auto-commit and disable auto-rollback
self.page.check_execute_option('auto_commit')
self.page.uncheck_execute_option('auto_rollback')
# Execute query
self.page.find_by_css_selector(
QueryToolLocators.btn_execute_query_css).click()
# Providing a second of sleep since clicks on the execute and stop
# query button is too quick that the query is not able run properly
time.sleep(1)
self.page.find_by_css_selector(
QueryToolLocators.btn_cancel_query).click()
self.page.wait_for_query_tool_loading_indicator_to_disappear()
self.page.click_tab('Messages')
self.assertTrue(
self.page.check_if_element_exist_by_xpath(
QueryToolLocators.sql_editor_message
.format('canceling statement due to user request')) or
self.page.check_if_element_exist_by_xpath(
QueryToolLocators.sql_editor_message
.format('Execution Cancelled!'))
)
def _query_tool_notify_statements(self):
print("\n\tListen on an event... ", file=sys.stderr, end="")
self.page.execute_query("LISTEN foo;")
self.page.click_tab('Messages')
self.assertTrue(self.page.check_if_element_exist_by_xpath(
QueryToolLocators.sql_editor_message.format('LISTEN')),
"LISTEN message does not displayed")
print("OK.", file=sys.stderr)
self.page.clear_query_tool()
print("\tNotify event without data... ", file=sys.stderr, end="")
self.page.execute_query("NOTIFY foo;")
self.page.click_tab('Notifications')
self.wait.until(EC.text_to_be_present_in_element(
(By.CSS_SELECTOR, "td[data-label='channel']"), "foo")
)
print("OK.", file=sys.stderr)
print("\tNotify event with data... ", file=sys.stderr, end="")
self.page.clear_query_tool()
self.page.execute_query("SELECT pg_notify('foo', 'Hello')")
self.page.click_tab('Notifications')
self.wait.until(WaitForAnyElementWithText(
(By.CSS_SELECTOR, "td[data-label='payload']"), "Hello"))
print("OK.", file=sys.stderr)
class WaitForAnyElementWithText():
def __init__(self, locator, text):
self.locator = locator
self.text = text
def __call__(self, driver):
try:
elements = driver.find_elements(*self.locator)
for elem in elements:
if self.text in elem.text:
return True
return False
except StaleElementReferenceException:
return False